From 0226171b58957965d879e5ca5da1748ba46e992e Mon Sep 17 00:00:00 2001 From: Joshua Seigler Date: Thu, 10 Jul 2025 23:50:28 -0400 Subject: [PATCH] code block line numbers --- assets/scripts/main.ts | 15 +++ eleventy.config.js | 33 +---- package-lock.json | 29 ++-- package.json | 2 +- site/_includes/css/prism.css | 125 +++++++++++------- site/_includes/css/site.css | 50 +++++-- .../index.md | 4 +- 7 files changed, 159 insertions(+), 99 deletions(-) diff --git a/assets/scripts/main.ts b/assets/scripts/main.ts index 8bb795c..d69841b 100644 --- a/assets/scripts/main.ts +++ b/assets/scripts/main.ts @@ -58,6 +58,19 @@ function addEffect({ target }: UIEvent) { }) } +function addCodeBlockCopiers() { + document.querySelectorAll('pre[class^=language]').forEach(el => { + const copyButton = document.createElement('button') + copyButton.classList.add('copy-button') + copyButton.addEventListener('click', () => { + if (el instanceof HTMLElement) { + navigator.clipboard.writeText(el.innerText) + } + }) + el.appendChild(copyButton) + }) +} + function attend({ target}: UIEvent) { if (!isElement(target) || !target.matches("a[href][target=_blank]")) { return @@ -75,3 +88,5 @@ document.addEventListener("mouseleave", removeEffect, true) document.addEventListener("blur", removeEffect, true) document.addEventListener("click", attend, true) + +document.addEventListener("DOMContentLoaded", addCodeBlockCopiers) diff --git a/eleventy.config.js b/eleventy.config.js index 2ac05a2..530d1f2 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -5,14 +5,14 @@ import mdAnchor from "markdown-it-anchor" import { spoiler as mdSpoiler } from "@mdit/plugin-spoiler" import { footnote as mdFootnote } from "@mdit/plugin-footnote" import mdLinkAttributes from "markdown-it-link-attributes" -import mdPrism from "markdown-it-prism" import dayjs from "dayjs" import utc from "dayjs/plugin/utc.js" import clean from "eleventy-plugin-clean" import toc from "eleventy-plugin-toc" import EleventyFeedPlugin from "@11ty/eleventy-plugin-rss" -import EleventyVitePlugin from "@11ty/eleventy-plugin-vite" import { eleventyImageTransformPlugin } from "@11ty/eleventy-img" +import EleventySyntaxHighlightPlugin from "@11ty/eleventy-plugin-syntaxhighlight" +import EleventyVitePlugin from "@11ty/eleventy-plugin-vite" import { ViteMinifyPlugin } from "vite-plugin-minify" import { execSync } from "child_process" import fetch from "@11ty/eleventy-fetch" @@ -49,7 +49,6 @@ export default async (config) => { rel: "noopener", }, }) - .use(mdPrism) mdLib.renderer.rules.render_footnote_anchor = ( tokens, idx, @@ -126,30 +125,6 @@ export default async (config) => { ) }) - // config.addShortcode( - // "image", - // function imageShortcode(src, alt, sizes, loading, classNames, inputPath) { - // const file = relativeToInputPath(inputPath ?? this.page.inputPath, src) - // const formats = ["avif", "auto"] - // const imageOptions = { - // widths: [125, 250, 500, "auto"], - // formats, - // outputDir: path.join(config.dir.output, "img"), - // } - // EleventyImage(file, imageOptions) // async but we won't await it - // const metadata = EleventyImage.statsSync(file, imageOptions) - // const imageAttributes = { - // alt, - // sizes, - // loading, - // decoding: "async", - // ...(classNames ? { class: classNames } : {}), - // } - - // return EleventyImage.generateHTML(metadata, imageAttributes) - // } - // ) - config.addFilter("toISOString", (dateString) => { return new Date(dateString).toISOString() }) @@ -189,6 +164,10 @@ export default async (config) => { config.addPlugin(EleventyFeedPlugin) + config.addPlugin(EleventySyntaxHighlightPlugin, { + alwaysWrapLineHighlights: true, + }) + config.addPlugin(EleventyVitePlugin, { viteOptions: { appType: "mpa", diff --git a/package-lock.json b/package-lock.json index dbc864f..872bbf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@11ty/eleventy-fetch": "^5.1.0", "@11ty/eleventy-img": "^6.0.4", "@11ty/eleventy-plugin-rss": "^2.0.4", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.1", "@11ty/eleventy-plugin-vite": "^6.0.0", "@mdit/plugin-footnote": "^0.22.0", "@mdit/plugin-spoiler": "^0.21.0", @@ -25,7 +26,6 @@ "gh-pages": "^6.3.0", "markdown-it-anchor": "^9.0.1", "markdown-it-link-attributes": "^4.0.1", - "markdown-it-prism": "^3.0.0", "pagefind": "^1.3.0", "vite-plugin-minify": "^2.1.0" } @@ -230,6 +230,20 @@ "url": "https://opencollective.com/11ty" } }, + "node_modules/@11ty/eleventy-plugin-syntaxhighlight": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-syntaxhighlight/-/eleventy-plugin-syntaxhighlight-5.0.1.tgz", + "integrity": "sha512-xDPF3Ay38XlmWZe9ER0SLtMmNah7olUBlGORhUiCUkPh3jYGVCDTDayi4tbFI9Dxha8NwKlfBZ2FXM/s3aZzAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, "node_modules/@11ty/eleventy-plugin-vite": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-vite/-/eleventy-plugin-vite-6.0.0.tgz", @@ -3895,19 +3909,6 @@ "dev": true, "license": "MIT" }, - "node_modules/markdown-it-prism": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-prism/-/markdown-it-prism-3.0.0.tgz", - "integrity": "sha512-M0LQsbVzjbzufV95nYdtbs99Sm/8f7zLnqFblb5yQZtFTGLM+y6hxMZ8BdujY3FFqVvTbnZ52/uoicQUqZUYbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prismjs": "1.30.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/markdown-it/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", diff --git a/package.json b/package.json index 4e4a8ab..e202fca 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@11ty/eleventy-fetch": "^5.1.0", "@11ty/eleventy-img": "^6.0.4", "@11ty/eleventy-plugin-rss": "^2.0.4", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.1", "@11ty/eleventy-plugin-vite": "^6.0.0", "@mdit/plugin-footnote": "^0.22.0", "@mdit/plugin-spoiler": "^0.21.0", @@ -29,7 +30,6 @@ "gh-pages": "^6.3.0", "markdown-it-anchor": "^9.0.1", "markdown-it-link-attributes": "^4.0.1", - "markdown-it-prism": "^3.0.0", "pagefind": "^1.3.0", "vite-plugin-minify": "^2.1.0" }, diff --git a/site/_includes/css/prism.css b/site/_includes/css/prism.css index 201b5b6..3d643b7 100644 --- a/site/_includes/css/prism.css +++ b/site/_includes/css/prism.css @@ -7,8 +7,7 @@ pre[class*="language-"] { word-break: normal; word-wrap: normal; font-size: 0.8rem; - line-height: 1.5em; - color: var(--c-text-dim); + line-height: 1.4; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; @@ -20,16 +19,49 @@ pre[class*="language-"] { :not(pre) > code[class*="language-"] { background: var(--c-text-background-light); white-space: normal; - border-radius: 0.2em; + border-radius: 0.2rem; padding: 0.1em; } pre[class*="language-"] { background: color-mix(in lch, var(--c-text-background-light) 20%, transparent); - overflow: auto; + > code { + white-space: pre-wrap; + word-break: break-all; + } position: relative; margin: 0.5em 0; - padding: 1.25em 1em; + padding: 0.5em 1em 0.5em calc(var(--lineNumWidth) + 0.25rem); box-shadow: inset 0 0 6rem -2rem var(--c-highlight); + counter-reset: lineNumber; + --places: 1; + &:has(.highlight-line:nth-child(10)) { --places: 2 } + &:has(.highlight-line:nth-child(100)) { --places: 3 } + &:has(.highlight-line:nth-child(1000)) { --places: 4 } + --lineNumWidth: calc(1em + var(--places) * 1ch); + &::before { + content: ''; + position: absolute; + height: 100%; + top: 0; + left: var(--lineNumWidth); + width: 3rem; + opacity: 0.125; + z-index: -1; + background: linear-gradient(to right, var(--c-highlight), transparent); + border-radius: 0.5em; + } + .highlight-line { + position: relative; + } + .highlight-line::before { + content: counter(lineNumber); + counter-increment: lineNumber; + position: absolute; + top: 0; + right: calc(100% + 0.5em); + color: var(--c-highlight); + word-break: normal; + } } p:has(+ pre[class*="language-"]) > code { margin: 0; @@ -46,44 +78,47 @@ p:has(>code) + pre[class*="language-"] { .token.id { font-weight: bold; } .token.important { font-weight: bold; } -.language-css > code, -.language-sass > code, -.language-scss > code { - color: #fd9170; +@media screen { + pre[class*="language-"] { color: var(--c-text-dim); } + .language-css > code, + .language-sass > code, + .language-scss > code { + color: #fd9170; + } + .token.atrule { color: #c792ea; } + .token.attr-name { color: #ffcb6b; } + .token.attr-value { color: #a5e844; } + .token.attribute { color: #a5e844; } + .token.boolean { color: #c792ea; } + .token.builtin { color: #ffcb6b; } + .token.cdata { color: #80cbc4; } + .token.char { color: #80cbc4; } + .token.class { color: #ffcb6b; } + .token.class-name { color: #f2ff00; } + .token.comment { color: #616161; } + .token.constant { color: #c792ea; } + .token.deleted { color: #ff6666; } + .token.doctype { color: #616161; } + .token.entity { color: #ff6666; } + .token.function { color: #c792ea; } + .token.hexcode { color: #f2ff00; } + .token.id { color: #c792ea; } + .token.important { color: #c792ea; } + .token.inserted { color: #80cbc4; } + .token.keyword { color: #c792ea; } + .token.number { color: #fd9170; } + .token.operator { color: #89ddff; } + .token.prolog { color: #616161; } + .token.property { color: #80cbc4; } + .token.pseudo-class { color: #a5e844; } + .token.pseudo-element { color: #a5e844; } + .token.punctuation { color: #89ddff; } + .token.regex { color: #f2ff00; } + .token.selector { color: #ff6666; } + .token.string { color: #a5e844; } + .token.symbol { color: #c792ea; } + .token.tag { color: #ff6666; } + .token.unit { color: #fd9170; } + .token.url { color: #ff6666; } + .token.variable { color: #ff6666; } } -.token.atrule { color: #c792ea; } -.token.attr-name { color: #ffcb6b; } -.token.attr-value { color: #a5e844; } -.token.attribute { color: #a5e844; } -.token.boolean { color: #c792ea; } -.token.builtin { color: #ffcb6b; } -.token.cdata { color: #80cbc4; } -.token.char { color: #80cbc4; } -.token.class { color: #ffcb6b; } -.token.class-name { color: #f2ff00; } -.token.comment { color: #616161; } -.token.constant { color: #c792ea; } -.token.deleted { color: #ff6666; } -.token.doctype { color: #616161; } -.token.entity { color: #ff6666; } -.token.function { color: #c792ea; } -.token.hexcode { color: #f2ff00; } -.token.id { color: #c792ea; } -.token.important { color: #c792ea; } -.token.inserted { color: #80cbc4; } -.token.keyword { color: #c792ea; } -.token.number { color: #fd9170; } -.token.operator { color: #89ddff; } -.token.prolog { color: #616161; } -.token.property { color: #80cbc4; } -.token.pseudo-class { color: #a5e844; } -.token.pseudo-element { color: #a5e844; } -.token.punctuation { color: #89ddff; } -.token.regex { color: #f2ff00; } -.token.selector { color: #ff6666; } -.token.string { color: #a5e844; } -.token.symbol { color: #c792ea; } -.token.tag { color: #ff6666; } -.token.unit { color: #fd9170; } -.token.url { color: #ff6666; } -.token.variable { color: #ff6666; } diff --git a/site/_includes/css/site.css b/site/_includes/css/site.css index c3f9110..6ac234a 100644 --- a/site/_includes/css/site.css +++ b/site/_includes/css/site.css @@ -96,15 +96,17 @@ a[href] { margin: -0.1em; position: relative; } -a[target=_blank]::after { - display: inline-block; - content: ""; - height: 1em; - width: 1em; - vertical-align: middle; - background-color: currentColor; - mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg viewBox='-2 0 19 19' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.267 3.793v7.996a.477.477 0 0 1-.475.475h-2.356v2.472a.476.476 0 0 1-.475.475H1.208a.476.476 0 0 1-.475-.475V6.74a.476.476 0 0 1 .475-.475h2.356V3.793a.476.476 0 0 1 .475-.475h9.753a.476.476 0 0 1 .475.475zm-3.94 8.471H4.04a.477.477 0 0 1-.475-.475V8.626H1.84v5.476h8.487zm2.832-6.585H4.672v5.476h8.487z'/%3E%3C/svg%3E"); - opacity: 0.5; +@media screen { + a[target=_blank]::after { + display: inline-block; + content: ""; + height: 1em; + width: 1em; + vertical-align: middle; + background-color: currentColor; + mask-image: var(--icon-copy); + opacity: 0.5; + } } a:hover, a:focus-visible { @@ -127,6 +129,11 @@ a:focus-visible { outline: 2px solid var(--c-text); } +:root { + --icon-copy: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg viewBox='-2 0 19 19' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.267 3.793v7.996a.477.477 0 0 1-.475.475h-2.356v2.472a.476.476 0 0 1-.475.475H1.208a.476.476 0 0 1-.475-.475V6.74a.476.476 0 0 1 .475-.475h2.356V3.793a.476.476 0 0 1 .475-.475h9.753a.476.476 0 0 1 .475.475zm-3.94 8.471H4.04a.477.477 0 0 1-.475-.475V8.626H1.84v5.476h8.487zm2.832-6.585H4.672v5.476h8.487z'/%3E%3C/svg%3E"); + --icon-rss: url("/icons/rss.svg"); +} + main p img { max-width: 100%; } @@ -523,7 +530,7 @@ a.rss { width: 1lh; margin: 0 0 0 0.5rem; --glowColor: #F80A; - background-image: url("/icons/rss.svg"); + background-image: var(--icon-rss); border-radius: 0.25em; opacity: 0.25; transition: 5s opacity; @@ -553,6 +560,10 @@ a.rss { } } +pre[class*="language-"] button { + display: none; +} + @media screen { p:has(+ pre[class*="language-"]) > code:first-child:last-child { position: relative; @@ -575,6 +586,25 @@ a.rss { transparent ); } + pre[class*="language-"] button { + display: block; + position: absolute; + top: 0; + right: 0; + border-radius: 0.5rem; + font-size: var(--s-1); + padding: 0.25em; + &::before { + content: ""; + display: flex; + justify-content: center; + align-items: center; + width: 1lh; + height: 1lh; + background-color: currentColor; + mask-image: var(--icon-copy); + } + } } main { diff --git a/site/posts/2025-06-15-replacing-github-pages/index.md b/site/posts/2025-06-15-replacing-github-pages/index.md index 9cd51fe..8e9bd58 100644 --- a/site/posts/2025-06-15-replacing-github-pages/index.md +++ b/site/posts/2025-06-15-replacing-github-pages/index.md @@ -33,14 +33,14 @@ Here is the Caddyfile I made---you will need to change the domain names and the ```caddy # Global options block { - email you@example.com # <<<< CHANGE THIS <<<< + email you@example.com #### CHANGE THIS #### on_demand_tls { ask http://localhost/check } } # Webhooks -https://webhooks.subdomain.here.tld { <<<< CHANGE THIS <<<< +https://webhooks.subdomain.here.tld { #### CHANGE THIS #### reverse_proxy localhost:9000 }