1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-27 17:25:43 -04:00

add language indicator to code blocks

This commit is contained in:
2025-05-07 20:01:12 -04:00
parent 6fd7c9fc4a
commit fff705f1e8
15 changed files with 90 additions and 59 deletions

View File

@ -176,7 +176,5 @@
}
@layer components {
.youtube-embed {
@import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
}
@import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
}

View File

@ -11,9 +11,9 @@ export const metadata = createMetadata({
export const WarningMarquee = () => (
<Marquee>
<span>
<span className="leading-none select-none">
🚨 Trigger warning: excessive marquees, animated GIFs, Comic Sans, popups,{" "}
<code style={{ fontWeight: "normal", fontSize: "0.9em" }}>
<code className="text-[0.9rem] font-normal">
color: <span style={{ color: "#32cd32" }}>limegreen</span>
</code>{" "}
ahead...
@ -23,7 +23,8 @@ export const WarningMarquee = () => (
export const PageStyles = () => (
<style>
{`body {
{`
body {
cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAZklEQVR4AWIAgn/uBT6A9uoAAwAQiIJo97/0Rgy0ANoJH8MPeEgtqwPQEACqCoQHAKECQKgAECoAhAoAoQJAqAAQxh1oPQfcW3kJpxHtL1AAHAwEwwdYiH8BIEgBTBRAAAEEEEAAG7mRt30hEhoLAAAAAElFTkSuQmCC") 2 1, auto;
}
a, button {
@ -78,8 +79,7 @@ _Previously on the [Cringey Chronicles&trade;](https://web.archive.org/web/20010
<iframe
src="https://jakejarvis.github.io/my-first-website/"
title="My Terrible, Horrible, No Good, Very Bad First Website"
className="border-ring w-full border-2"
style={{ height: "500px" }}
className="border-ring h-[500px] w-full border-2"
/>
_[November 2001](https://jakejarvis.github.io/my-first-website/) ([view
source](https://github.com/jakejarvis/my-first-website))_

View File

@ -14,9 +14,7 @@ export const Terminal = () => (
<div
className="relative mx-auto my-6 w-full rounded-lg"
style={{
backgroundImage: `url(${backgroundImg.src})`,
backgroundRepeat: "repeat",
backgroundPosition: "center",
background: `url(${backgroundImg.src}) center no-repeat`,
}}
>
<code className="border-ring block rounded-lg border border-solid bg-black/60 p-4 text-sm break-all text-white/90 backdrop-blur-xs backdrop-saturate-150">

View File

@ -1,16 +1,19 @@
import { codeToHtml } from "shiki";
import reactToText from "react-to-text";
import { CodeIcon } from "lucide-react";
import CopyButton from "@/components/copy-button";
import { cn } from "@/lib/utils";
import type { ComponentProps, ComponentPropsWithoutRef } from "react";
const CodeBlock = async ({
lineNumbers = false,
showLineNumbers = false,
showCopyButton = true,
className,
children,
...rest
}: ComponentPropsWithoutRef<"pre"> & {
lineNumbers?: boolean;
showLineNumbers?: boolean;
showCopyButton?: boolean;
}) => {
// escape hatch if this code wasn't meant to be highlighted
if (!children || typeof children !== "object" || !("props" in children)) {
@ -35,22 +38,57 @@ const CodeBlock = async ({
},
});
const getFullLang = (lang: string) => {
// replace the file extension with the full language name when it makes sense to
switch (lang.toLowerCase()) {
case "js":
return "JavaScript";
case "jsx":
return "JavaScript (JSX)";
case "ts":
return "TypeScript";
case "tsx":
return "TypeScript (JSX)";
case "sh":
case "bash":
case "zsh":
return "Shell";
case "py":
return "Python";
case "md":
return "Markdown";
case "mdx":
return "Markdown (MDX)";
default:
return lang;
}
};
const fullLang = getFullLang(lang);
return (
<div className={cn("bg-muted/35 relative isolate rounded-lg border-2 font-mono", className)}>
<div
className={cn(
"grid max-h-[500px] w-full overflow-x-auto p-4 **:bg-transparent! md:max-h-[650px] dark:**:text-[var(--shiki-dark)]! [&_pre]:whitespace-normal",
"grid max-h-[500px] w-full overflow-x-auto p-4 **:bg-transparent! data-language:pt-9 md:max-h-[650px] dark:**:text-[var(--shiki-dark)]! [&_pre]:whitespace-normal",
"[&_.line]:inline-block [&_.line]:min-w-full [&_.line]:py-1 [&_.line]:leading-none [&_.line]:whitespace-pre [&_.line]:after:hidden",
"data-line-numbers:[&_.line]:before:text-muted-foreground data-line-numbers:[counter-reset:line] data-line-numbers:[&_.line]:[counter-increment:line] data-line-numbers:[&_.line]:before:mr-5 data-line-numbers:[&_.line]:before:inline-block data-line-numbers:[&_.line]:before:w-5 data-line-numbers:[&_.line]:before:text-right data-line-numbers:[&_.line]:before:content-[counter(line)]"
)}
data-language={lang}
data-line-numbers={lineNumbers || undefined}
data-language={lang || undefined}
data-line-numbers={showLineNumbers || undefined}
dangerouslySetInnerHTML={{ __html: codeHighlighted }}
/>
<CopyButton
source={codeString}
className="text-foreground/85 hover:text-primary bg-muted/10 absolute top-0 right-0 size-10 rounded-tr-lg rounded-bl-lg border-b-2 border-l-2 p-0 backdrop-blur-xs [&_svg]:my-auto [&_svg]:inline-block [&_svg]:size-4.5 [&_svg]:align-text-bottom"
/>
{fullLang && (
<span className="text-foreground/75 bg-muted/40 absolute top-0 left-0 flex items-center rounded-tl-md rounded-br-lg border-r-2 border-b-2 py-[5px] pr-[10px] font-mono text-xs font-medium tracking-wide uppercase backdrop-blur-xs select-none">
<CodeIcon className="stroke-primary/90 mr-[8px] ml-[10px] inline-block size-[14px]" /> <span>{fullLang}</span>
</span>
)}
{showCopyButton && (
<CopyButton
source={codeString}
className="text-foreground/75 hover:text-primary bg-muted/40 absolute top-0 right-0 size-10 rounded-tr-md rounded-bl-lg border-b-2 border-l-2 p-0 backdrop-blur-xs select-none [&_svg]:my-auto [&_svg]:inline-block [&_svg]:size-4.5 [&_svg]:align-text-bottom"
/>
)}
</div>
);
};

View File

@ -3,13 +3,14 @@
import { forwardRef, useState, useEffect } from "react";
import copy from "copy-to-clipboard";
import { ClipboardIcon, CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import type { Ref, ComponentPropsWithoutRef, ComponentRef, MouseEventHandler } from "react";
const CopyButton = (
{
source,
timeout = 2000,
style,
className,
...rest
}: ComponentPropsWithoutRef<"button"> & {
source: string;
@ -51,7 +52,7 @@ const CopyButton = (
ref={ref}
onClick={handleCopy}
disabled={copied}
style={{ cursor: copied ? "default" : "pointer", ...style }}
className={cn("cursor-pointer disabled:cursor-default", className)}
{...rest}
>
{copied ? <CheckIcon className="stroke-success" /> : <ClipboardIcon />}

View File

@ -3,15 +3,10 @@
import YouTubeEmbed from "react-lite-youtube-embed";
import type { ComponentPropsWithoutRef } from "react";
// lite-youtube-embed CSS is imported in app/global.css to save a request
const YouTube = ({ ...rest }: Omit<ComponentPropsWithoutRef<typeof YouTubeEmbed>, "title">) => {
return (
<div
// lite-youtube-embed CSS is imported in app/global.css to save a request
className="youtube-embed"
>
<YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />
</div>
);
return <YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />;
};
export default YouTube;

View File

@ -127,9 +127,12 @@ export const getViews: {
slug?: any
): // eslint-disable-next-line @typescript-eslint/no-explicit-any
Promise<any> => {
// ensure the prefix is consistent for all keys in the KV store
const KEY_PREFIX = `hits:${POSTS_DIR}/`;
if (typeof slug === "string") {
try {
const views = await kv.get<string>(`hits:${POSTS_DIR}/${slug}`);
const views = await kv.get<string>(`${KEY_PREFIX}${slug}`);
return views ? parseInt(views, 10) : undefined;
} catch (error) {
@ -144,12 +147,10 @@ export const getViews: {
const pages: Record<string, number> = {};
// get the value (number of views) for each key (the slug of the page)
const values = await kv.mget<string[]>(...allSlugs.map((slug) => `hits:${POSTS_DIR}/${slug}`));
const values = await kv.mget<string[]>(...allSlugs.map((slug) => `${KEY_PREFIX}${slug}`));
// pair the slugs with their view counts
allSlugs.forEach(
(slug, index) => (pages[slug.replace(`hits:${POSTS_DIR}/`, "")] = parseInt(values[index], 10))
);
allSlugs.forEach((slug, index) => (pages[slug.replace(KEY_PREFIX, "")] = parseInt(values[index], 10)));
return pages;
} catch (error) {

View File

@ -72,7 +72,7 @@ export const useMDXComponents = (components: MDXComponents): MDXComponents => {
h3: ({ className, id, children, ...rest }) => (
<h3
className={cn(
"group mt-6 mb-2.5 scroll-m-4 text-lg leading-relaxed font-semibold md:text-xl [&_code]:text-[0.9em] [&_strong]:font-bold [&+*]:mt-0",
"group mt-6 mb-4 scroll-m-4 text-lg leading-relaxed font-semibold md:text-xl [&_code]:text-[0.9em] [&_strong]:font-bold [&+*]:mt-0",
className
)}
id={id}

View File

@ -24,7 +24,7 @@ Below are the code snippets you can grab and customize to make your own ["waving
## CSS
{/* prettier-ignore */}
```css lineNumbers
```css showLineNumbers
.wave {
animation-name: wave-animation; /* Refers to the name of your @keyframes element below */
animation-duration: 2.5s; /* Change to speed up or slow down */

View File

@ -34,8 +34,7 @@ I've written a simple implementation below, which...
<iframe
src="https://jakejarvis.github.io/dark-mode-example/"
title="Dark Mode Example"
className="border-ring w-full border-2"
style={{ height: "190px" }}
className="border-ring h-[190px] w-full border-2"
></iframe>
A _very_ barebones example is embedded above ([view the source here](https://github.com/jakejarvis/dark-mode-example), or [open in a new window](https://jakejarvis.github.io/dark-mode-example/) if your browser is blocking the frame) and you can try it out on this site by clicking the 💡 lightbulb in the upper right corner of this page. You'll notice that the dark theme sticks when refreshing this page, navigating between other pages, or if you were to return to this example weeks from now.
@ -46,7 +45,7 @@ A _very_ barebones example is embedded above ([view the source here](https://git
I have cleaned up this code a bit, added a few features, and packaged it as an [📦 NPM module](https://www.npmjs.com/package/dark-mode-switcheroo) (zero dependencies and still [only ~500 bytes](https://bundlephobia.com/package/dark-mode-switcheroo) minified and gzipped!). Here's a small snippet of the updated method for the browser (pulling the module from [UNPKG](https://unpkg.com/browse/dark-mode-switcheroo/)), but definitely [read the readme](https://github.com/jakejarvis/dark-mode#readme) for much more detail on the API.
```html lineNumbers
```html showLineNumbers
<button class="dark-mode-toggle" style="visibility: hidden;">💡 Click to see the light... or not.</button>
<script src="https://unpkg.com/dark-mode-switcheroo/dist/dark-mode.min.js"></script>
@ -94,7 +93,7 @@ The [example HTML and CSS below](#html-css) is still helpful for reference.
### Full JS:
{/* prettier-ignore */}
```js lineNumbers
```js showLineNumbers
/*! Dark mode switcheroo | MIT License | jrvs.io/darkmode */
(function () {
@ -177,7 +176,7 @@ The [example HTML and CSS below](#html-css) is still helpful for reference.
### HTML & CSS Example:
{/* prettier-ignore */}
```html lineNumbers
```html showLineNumbers
<!doctype html>
<html>
<head>

View File

@ -102,7 +102,7 @@ I removed the company's name because an important part of responsible _disclosur
The `poc-d4ca9e8ceb.html` proof-of-concept file contained this single, hidden line:
```html lineNumbers
```html showLineNumbers
<!-- subdomain takeover POC by @jakejarvis on Bugcrowd -->
```

View File

@ -43,7 +43,7 @@ If you're bored on a rainy day, potential activities could include:
Who cares if somebody wants to delete a post with the ID "`*`" no matter the author? ([delete_reply_submit.php](https://github.com/jakejarvis/jbb/blob/87b606797414b2fe563af85e269566fc5e076cc5/delete_reply_submit.php#L9))
```php lineNumbers
```php showLineNumbers
<?php
$query2 = "DELETE FROM jbb_replies
WHERE replyID ='$replyID'";
@ -54,7 +54,7 @@ $result2 = mysql_query ($query2)
Sessions based on storing an auto-incremented user ID in a cookie. ([login_submit.php](https://github.com/jakejarvis/jbb/blob/87b606797414b2fe563af85e269566fc5e076cc5/login_submit.php#L28))
```php lineNumbers
```php showLineNumbers
<?php
session_id($user->userID);
session_start();
@ -66,7 +66,7 @@ $_SESSION["ck_groupID"] = $user->groupID;
Viewing a "private" message based solely on a sequential message ID. ([pm_view.php](https://github.com/jakejarvis/jbb/blob/87b606797414b2fe563af85e269566fc5e076cc5/pm_view.php#L13))
```php lineNumbers
```php showLineNumbers
<?php
$query1 = "SELECT * FROM jbb_pm WHERE pmID = '$pmID'";
?>
@ -74,7 +74,7 @@ $query1 = "SELECT * FROM jbb_pm WHERE pmID = '$pmID'";
Incredibly ambitious emoticon and [BBCode](https://en.wikipedia.org/wiki/BBCode) support. I honestly can't begin to explain this logic. ([functions.php](https://github.com/jakejarvis/jbb/blob/87b606797414b2fe563af85e269566fc5e076cc5/functions.php#L18))
```php lineNumbers
```php showLineNumbers
<?php
$replacement = '<img src=images/emoticons/smile.gif>';
$replacement2 = '<img src=images/emoticons/bigsmile.gif>';
@ -111,7 +111,7 @@ $topicval = str_replace('
Saving new passwords as plaintext — probably the least problematic problem. ([register_submit.php](https://github.com/jakejarvis/jbb/blob/87b606797414b2fe563af85e269566fc5e076cc5/register_submit.php#L10))
```php lineNumbers
```php showLineNumbers
<?php
$query = "INSERT INTO jbb_users (username, password, email, avatar) VALUES ('$username','$password','$email','images/avatars/noavatar.gif')";
?>
@ -119,7 +119,7 @@ $query = "INSERT INTO jbb_users (username, password, email, avatar) VALUES ('$us
I guess I gave up on counting `$query`s by ones... ([functions.php](https://github.com/jakejarvis/jbb/blob/87b606797414b2fe563af85e269566fc5e076cc5/functions.php#L231))
```php lineNumbers
```php showLineNumbers
<?php
while ($topic = mysql_fetch_object($result30)) {
$query40 = "SELECT * FROM jbb_users WHERE userID = '$topic->userID'";

View File

@ -28,7 +28,7 @@ If you run your own server, these can be added by way of your Apache or nginx co
The following script can be added as a Worker and customized to your needs. Some can be extremely picky with syntax, so be sure to [read the documentation](https://www.netsparker.com/whitepaper-http-security-headers/) carefully. You can fiddle with it in [the playground](https://cloudflareworkers.com/), too. Simply modify the current headers to your needs, or add new ones to the `newHeaders` or `removeHeaders` arrays.
```js lineNumbers
```js showLineNumbers
let addHeaders = {
"Content-Security-Policy": "default-src 'self'; upgrade-insecure-requests",
"Strict-Transport-Security": "max-age=1000",

View File

@ -86,7 +86,7 @@
"@jakejarvis/eslint-config": "^4.0.7",
"@tailwindcss/postcss": "^4.1.5",
"@types/mdx": "^2.0.13",
"@types/node": "^22.15.14",
"@types/node": "^22.15.15",
"@types/prop-types": "^15.7.14",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
@ -131,7 +131,8 @@
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"react": "^19"
"react": "^19",
"react-dom": "^19"
}
}
}

14
pnpm-lock.yaml generated
View File

@ -208,8 +208,8 @@ importers:
specifier: ^2.0.13
version: 2.0.13
'@types/node':
specifier: ^22.15.14
version: 22.15.14
specifier: ^22.15.15
version: 22.15.15
'@types/prop-types':
specifier: ^15.7.14
version: 15.7.14
@ -1242,8 +1242,8 @@ packages:
'@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
'@types/node@22.15.14':
resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
'@types/node@22.15.15':
resolution: {integrity: sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
@ -5221,7 +5221,7 @@ snapshots:
'@types/concat-stream@2.0.3':
dependencies:
'@types/node': 22.15.14
'@types/node': 22.15.15
'@types/debug@4.1.12':
dependencies:
@ -5257,7 +5257,7 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/node@22.15.14':
'@types/node@22.15.15':
dependencies:
undici-types: 6.21.0
@ -8799,7 +8799,7 @@ snapshots:
'@types/concat-stream': 2.0.3
'@types/debug': 4.1.12
'@types/is-empty': 1.2.3
'@types/node': 22.15.14
'@types/node': 22.15.15
'@types/unist': 3.0.3
concat-stream: 2.0.0
debug: 4.4.0