From 10f8feb772eea78d26fb53563293cf588b695fa7 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:03:48 +0200 Subject: [PATCH 1/3] chore: add a formatter, matching the style this repo already writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit singleQuote=false was chosen by counting this repo's own imports, not by fleet decree. The fleet is genuinely split and the two repos that already had a .prettierrc disagreed with each other, so there was no standard to restore. Quote style does not cross repo boundaries; having a gate does. Markdown is ignored for now — prettier rewraps prose, which would bury the real diff. Co-Authored-By: Claude Opus 5 --- .prettierignore | 21 +++++++++++++++++++++ .prettierrc | 9 +++++++++ package-lock.json | 19 ++++++++++++++++++- package.json | 7 +++++-- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1ed5ea5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +# Build output and vendored trees — formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a2f11f0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/package-lock.json b/package-lock.json index 9896d95..d4d8abd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,12 @@ "@types/node": "^22.10.2", "eslint": "^9.39.5", "globals": "^15.15.0", + "prettier": "^3.3.3", "typescript": "^5.8.2", "typescript-eslint": "^8.67.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1309,6 +1310,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 55f7634..b4c7952 100644 --- a/package.json +++ b/package.json @@ -45,14 +45,17 @@ "lint": "eslint .", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "node --test test/*.test.js", - "verify": "npm run lint && npm run typecheck && npm run build && npm test", - "prepare": "npm run build" + "verify": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm test", + "prepare": "npm run build", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "devDependencies": { "@eslint/js": "^9.39.5", "@types/node": "^22.10.2", "eslint": "^9.39.5", "globals": "^15.15.0", + "prettier": "^3.3.3", "typescript": "^5.8.2", "typescript-eslint": "^8.67.0" } From 7337a36f999567a0c4843ad1a7c5afc9704b82f3 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:04:30 +0200 Subject: [PATCH 2/3] style: format with prettier (7 files) Mechanical. No behaviour change. This SHA is listed in .git-blame-ignore-revs so `git blame` skips it. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 8 +- eslint.config.mjs | 14 +-- src/video-embed.ts | 4 +- test/package.test.js | 73 +++++++-------- test/parse-content.test.js | 167 +++++++++++++++++----------------- test/video-embed.test.js | 56 ++++++------ 7 files changed, 159 insertions(+), 165 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 366756a..ae8da0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: "22" - run: npm ci --ignore-scripts # build + the full test suite, identical to what runs locally - run: npm run verify diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e2e5e89..ecde591 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ name: Publish on: push: branches: [main] - tags: ['v*'] + tags: ["v*"] # The reconciler, and the reason this is reliable. A merge made by auto-merge # uses GITHUB_TOKEN, and a push with that token starts no workflow — so the # push trigger above silently does not fire for exactly the merges that matter. @@ -19,7 +19,7 @@ on: # package.json's version published? If a release was missed by any means, it # goes out within the hour without anyone noticing it was missed. schedule: - - cron: '29 * * * *' + - cron: "29 * * * *" workflow_dispatch: jobs: @@ -37,8 +37,8 @@ jobs: - uses: actions/setup-node@v4 with: # npm >= 11.5.1 is required for trusted publishing (OIDC); Node 24 ships it. - node-version: '24' - registry-url: 'https://registry.npmjs.org' + node-version: "24" + registry-url: "https://registry.npmjs.org" - name: Is this version already on the registry? id: check diff --git a/eslint.config.mjs b/eslint.config.mjs index 78efba7..e170142 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,25 +2,25 @@ // library code, and a bespoke rule set would be a second opinion to maintain // for no benefit. The floor is "lint runs and can fail", not "lint encodes // taste". -import js from '@eslint/js' -import globals from 'globals' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; export default tseslint.config( { // dist/ is generated by `tsc`; templates/ is copy-paste starter code that // lives in the consumer's app, not in this package's build. - ignores: ['dist/**', 'node_modules/**', 'templates/**'], + ignores: ["dist/**", "node_modules/**", "templates/**"], }, js.configs.recommended, ...tseslint.configs.recommended, { - files: ['**/*.ts'], + files: ["**/*.ts"], languageOptions: { globals: globals.node }, }, { // Tests are plain Node running under `node --test`. - files: ['test/**/*.js'], + files: ["test/**/*.js"], languageOptions: { globals: { ...globals.node, ...globals.nodeBuiltin } }, }, -) +); diff --git a/src/video-embed.ts b/src/video-embed.ts index 90fe90c..fd46f04 100644 --- a/src/video-embed.ts +++ b/src/video-embed.ts @@ -7,9 +7,7 @@ const YOUTUBE = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{6,})/; const VIMEO = /^(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/; -export function parseVideoEmbed( - url: string, -): { provider: "youtube" | "vimeo"; id: string } | null { +export function parseVideoEmbed(url: string): { provider: "youtube" | "vimeo"; id: string } | null { const trimmed = url.trim(); const yt = YOUTUBE.exec(trimmed); if (yt) return { provider: "youtube", id: yt[1] }; diff --git a/test/package.test.js b/test/package.test.js index 1aa8be0..0082997 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -1,9 +1,9 @@ -import { test, before } from 'node:test'; -import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; -import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, existsSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { test, before } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; /** * Consume the package the way npm does. @@ -19,84 +19,79 @@ import { join } from 'node:path'; * it by bare specifier from outside the repo. */ -const PUBLIC_API = [ - 'parseContentBlocks', - 'parseFrontmatter', - 'parseVideoEmbed', - 'videoEmbedSrc', -]; +const PUBLIC_API = ["parseContentBlocks", "parseFrontmatter", "parseVideoEmbed", "videoEmbedSrc"]; let workspace; let installed; let probe; before(() => { - workspace = mkdtempSync(join(tmpdir(), 'bip-kit-pack-')); - installed = join(workspace, 'node_modules', 'bip-kit'); + workspace = mkdtempSync(join(tmpdir(), "bip-kit-pack-")); + installed = join(workspace, "node_modules", "bip-kit"); mkdirSync(installed, { recursive: true }); - execFileSync('npm', ['pack', '--silent', '--pack-destination', workspace], { + execFileSync("npm", ["pack", "--silent", "--pack-destination", workspace], { cwd: process.cwd(), - stdio: ['ignore', 'ignore', 'inherit'], + stdio: ["ignore", "ignore", "inherit"], }); - const tarball = readdirSync(workspace).find(f => f.endsWith('.tgz')); - assert.ok(tarball, 'npm pack produced no tarball'); + const tarball = readdirSync(workspace).find((f) => f.endsWith(".tgz")); + assert.ok(tarball, "npm pack produced no tarball"); - execFileSync('tar', ['-xzf', join(workspace, tarball), '-C', installed, '--strip-components=1']); + execFileSync("tar", ["-xzf", join(workspace, tarball), "-C", installed, "--strip-components=1"]); // Each probe records failure as a *value*, never a throw. A broken exports map // that crashes this hook would fail every assertion in the file at once and // bury which entry point actually broke. writeFileSync( - join(workspace, 'probe.mjs'), + join(workspace, "probe.mjs"), [ - 'const out = {};', + "const out = {};", 'try { out.resolved = import.meta.resolve("bip-kit"); } catch { out.resolved = null; }', 'try { out.exports = Object.keys(await import("bip-kit")).sort(); } catch { out.exports = null; }', - 'console.log(JSON.stringify(out));', - ].join('\n') + "console.log(JSON.stringify(out));", + ].join("\n"), ); probe = JSON.parse( - execFileSync('node', [join(workspace, 'probe.mjs')], { + execFileSync("node", [join(workspace, "probe.mjs")], { cwd: workspace, - encoding: 'utf8', - }) + encoding: "utf8", + }), ); }); -test('the package resolves from a consumer install', () => { +test("the package resolves from a consumer install", () => { assert.ok(probe.resolved, '"bip-kit" did not resolve through its own exports map'); }); -test('the entry point exposes its whole public API', () => { +test("the entry point exposes its whole public API", () => { assert.ok(probe.exports, 'importing "bip-kit" from a consumer install threw'); for (const name of PUBLIC_API) { assert.ok(probe.exports.includes(name), `"${name}" is missing from the published entry point`); } }); -test('the type declarations it advertises are actually in the tarball', () => { +test("the type declarations it advertises are actually in the tarball", () => { const pkg = JSON.parse( - execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], { + execFileSync("node", ["-p", 'JSON.stringify(require("./package.json"))'], { cwd: installed, - encoding: 'utf8', - }) + encoding: "utf8", + }), ); - const types = pkg.exports['.'].types; + const types = pkg.exports["."].types; assert.ok( existsSync(join(installed, types)), - `the package advertises types at ${types}, which is not in the tarball` + `the package advertises types at ${types}, which is not in the tarball`, ); }); -test('the tarball carries the documentation npm will render', () => { - for (const file of ['README.md', 'LICENSE']) { +test("the tarball carries the documentation npm will render", () => { + for (const file of ["README.md", "LICENSE"]) { assert.ok(existsSync(join(installed, file)), `${file} is missing from the tarball`); } }); -test('the tarball ships built output, not raw TypeScript sources', () => { - assert.ok(existsSync(join(installed, 'dist', 'index.js')), 'dist/index.js missing'); - assert.ok(!existsSync(join(installed, 'src')), 'src/ leaked into the tarball'); +test("the tarball ships built output, not raw TypeScript sources", () => { + assert.ok(existsSync(join(installed, "dist", "index.js")), "dist/index.js missing"); + assert.ok(!existsSync(join(installed, "src")), "src/ leaked into the tarball"); }); diff --git a/test/parse-content.test.js b/test/parse-content.test.js index 9e88ace..369bd9e 100644 --- a/test/parse-content.test.js +++ b/test/parse-content.test.js @@ -1,151 +1,152 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { parseContentBlocks, parseFrontmatter } from '../dist/index.js'; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { parseContentBlocks, parseFrontmatter } from "../dist/index.js"; // ── parseContentBlocks ────────────────────────────────────────────────────── -test('headings: ## and ### become h2/h3, text trimmed', () => { - const blocks = parseContentBlocks('## Roadmap \n### Q3 '); +test("headings: ## and ### become h2/h3, text trimmed", () => { + const blocks = parseContentBlocks("## Roadmap \n### Q3 "); assert.deepEqual(blocks, [ - { type: 'h2', text: 'Roadmap' }, - { type: 'h3', text: 'Q3' }, + { type: "h2", text: "Roadmap" }, + { type: "h3", text: "Q3" }, ]); }); -test('consecutive plain lines join into one paragraph', () => { - const blocks = parseContentBlocks('First line\nsecond line\n\nNew paragraph'); +test("consecutive plain lines join into one paragraph", () => { + const blocks = parseContentBlocks("First line\nsecond line\n\nNew paragraph"); assert.deepEqual(blocks, [ - { type: 'p', text: 'First line second line' }, - { type: 'p', text: 'New paragraph' }, + { type: "p", text: "First line second line" }, + { type: "p", text: "New paragraph" }, ]); }); -test('CRLF input parses the same as LF', () => { +test("CRLF input parses the same as LF", () => { assert.deepEqual( - parseContentBlocks('## Title\r\n\r\nBody\r\n'), - parseContentBlocks('## Title\n\nBody\n'), + parseContentBlocks("## Title\r\n\r\nBody\r\n"), + parseContentBlocks("## Title\n\nBody\n"), ); }); -test('unordered list collects consecutive - items', () => { - const blocks = parseContentBlocks('- one\n- two\n\n- three'); +test("unordered list collects consecutive - items", () => { + const blocks = parseContentBlocks("- one\n- two\n\n- three"); assert.deepEqual(blocks, [ - { type: 'ul', items: ['one', 'two'] }, - { type: 'ul', items: ['three'] }, + { type: "ul", items: ["one", "two"] }, + { type: "ul", items: ["three"] }, ]); }); -test('ordered list collects consecutive numbered items', () => { - const blocks = parseContentBlocks('1. first\n2. second\n10. tenth'); - assert.deepEqual(blocks, [{ type: 'ol', items: ['first', 'second', 'tenth'] }]); +test("ordered list collects consecutive numbered items", () => { + const blocks = parseContentBlocks("1. first\n2. second\n10. tenth"); + assert.deepEqual(blocks, [{ type: "ol", items: ["first", "second", "tenth"] }]); }); -test('blockquote keeps one entry per quoted line', () => { - const blocks = parseContentBlocks('> line one\n> line two'); - assert.deepEqual(blocks, [{ type: 'blockquote', text: ['line one', 'line two'] }]); +test("blockquote keeps one entry per quoted line", () => { + const blocks = parseContentBlocks("> line one\n> line two"); + assert.deepEqual(blocks, [{ type: "blockquote", text: ["line one", "line two"] }]); }); -test('fenced code keeps language, inner newlines, and indentation', () => { - const blocks = parseContentBlocks('```ts\nconst a = 1;\n indented();\n```'); - assert.deepEqual(blocks, [ - { type: 'code', lang: 'ts', text: 'const a = 1;\n indented();' }, - ]); +test("fenced code keeps language, inner newlines, and indentation", () => { + const blocks = parseContentBlocks("```ts\nconst a = 1;\n indented();\n```"); + assert.deepEqual(blocks, [{ type: "code", lang: "ts", text: "const a = 1;\n indented();" }]); }); test('mermaid fences survive as code blocks with lang "mermaid"', () => { - const [block] = parseContentBlocks('```mermaid\ngraph TD; A-->B\n```'); - assert.equal(block.type, 'code'); - assert.equal(block.lang, 'mermaid'); + const [block] = parseContentBlocks("```mermaid\ngraph TD; A-->B\n```"); + assert.equal(block.type, "code"); + assert.equal(block.lang, "mermaid"); }); -test('an unterminated fence consumes to end of input without crashing', () => { - const blocks = parseContentBlocks('```\nno closing fence'); - assert.deepEqual(blocks, [{ type: 'code', lang: '', text: 'no closing fence' }]); +test("an unterminated fence consumes to end of input without crashing", () => { + const blocks = parseContentBlocks("```\nno closing fence"); + assert.deepEqual(blocks, [{ type: "code", lang: "", text: "no closing fence" }]); }); -test('GFM table needs a separator row; cells are trimmed', () => { - const blocks = parseContentBlocks('| A | B |\n|---|:--|\n| 1 | 2 |\n| 3 | 4 |'); +test("GFM table needs a separator row; cells are trimmed", () => { + const blocks = parseContentBlocks("| A | B |\n|---|:--|\n| 1 | 2 |\n| 3 | 4 |"); assert.deepEqual(blocks, [ - { type: 'table', headers: ['A', 'B'], rows: [['1', '2'], ['3', '4']] }, + { + type: "table", + headers: ["A", "B"], + rows: [ + ["1", "2"], + ["3", "4"], + ], + }, ]); }); -test('a pipe line without a separator row is a paragraph, not a table', () => { - const blocks = parseContentBlocks('| just | text |'); - assert.deepEqual(blocks, [{ type: 'p', text: '| just | text |' }]); +test("a pipe line without a separator row is a paragraph, not a table", () => { + const blocks = parseContentBlocks("| just | text |"); + assert.deepEqual(blocks, [{ type: "p", text: "| just | text |" }]); }); -test('standalone image line becomes an image block', () => { - const blocks = parseContentBlocks('![alt text](/img/shot.png)'); - assert.deepEqual(blocks, [{ type: 'image', alt: 'alt text', src: '/img/shot.png' }]); +test("standalone image line becomes an image block", () => { + const blocks = parseContentBlocks("![alt text](/img/shot.png)"); + assert.deepEqual(blocks, [{ type: "image", alt: "alt text", src: "/img/shot.png" }]); }); -test('lone YouTube and Vimeo URLs become embed blocks', () => { +test("lone YouTube and Vimeo URLs become embed blocks", () => { for (const url of [ - 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - 'https://youtu.be/dQw4w9WgXcQ', - 'https://vimeo.com/76979871', + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "https://youtu.be/dQw4w9WgXcQ", + "https://vimeo.com/76979871", ]) { - assert.deepEqual(parseContentBlocks(url), [{ type: 'embed', url }], url); + assert.deepEqual(parseContentBlocks(url), [{ type: "embed", url }], url); } }); -test('URLs from non-allowlisted hosts stay paragraphs', () => { - const blocks = parseContentBlocks('https://evil.example.com/watch?v=abc'); - assert.deepEqual(blocks, [{ type: 'p', text: 'https://evil.example.com/watch?v=abc' }]); +test("URLs from non-allowlisted hosts stay paragraphs", () => { + const blocks = parseContentBlocks("https://evil.example.com/watch?v=abc"); + assert.deepEqual(blocks, [{ type: "p", text: "https://evil.example.com/watch?v=abc" }]); }); -test('a URL inside prose does not split the paragraph', () => { - const blocks = parseContentBlocks('Watch https://youtu.be/dQw4w9WgXcQ for context'); - assert.deepEqual(blocks, [ - { type: 'p', text: 'Watch https://youtu.be/dQw4w9WgXcQ for context' }, - ]); +test("a URL inside prose does not split the paragraph", () => { + const blocks = parseContentBlocks("Watch https://youtu.be/dQw4w9WgXcQ for context"); + assert.deepEqual(blocks, [{ type: "p", text: "Watch https://youtu.be/dQw4w9WgXcQ for context" }]); }); -test('a paragraph ends where the next block type starts, without a blank line', () => { - const blocks = parseContentBlocks('Some text\n- item'); +test("a paragraph ends where the next block type starts, without a blank line", () => { + const blocks = parseContentBlocks("Some text\n- item"); assert.deepEqual(blocks, [ - { type: 'p', text: 'Some text' }, - { type: 'ul', items: ['item'] }, + { type: "p", text: "Some text" }, + { type: "ul", items: ["item"] }, ]); }); -test('empty and whitespace-only input produce no blocks', () => { - assert.deepEqual(parseContentBlocks(''), []); - assert.deepEqual(parseContentBlocks('\n \n\t\n'), []); +test("empty and whitespace-only input produce no blocks", () => { + assert.deepEqual(parseContentBlocks(""), []); + assert.deepEqual(parseContentBlocks("\n \n\t\n"), []); }); // ── parseFrontmatter ──────────────────────────────────────────────────────── -test('frontmatter is split from the body and parsed as key: value', () => { - const raw = '---\ntitle: Hello\ntags: a, b\n---\nBody here'; +test("frontmatter is split from the body and parsed as key: value", () => { + const raw = "---\ntitle: Hello\ntags: a, b\n---\nBody here"; const { meta, body } = parseFrontmatter(raw); - assert.deepEqual(meta, { title: 'Hello', tags: 'a, b' }); - assert.equal(body, 'Body here'); + assert.deepEqual(meta, { title: "Hello", tags: "a, b" }); + assert.equal(body, "Body here"); }); -test('matching quotes are stripped; mismatched quotes are kept', () => { - const { meta } = parseFrontmatter( - '---\na: "quoted"\nb: \'single\'\nc: "mismatched\'\n---\n', - ); - assert.equal(meta.a, 'quoted'); - assert.equal(meta.b, 'single'); - assert.equal(meta.c, '"mismatched\''); +test("matching quotes are stripped; mismatched quotes are kept", () => { + const { meta } = parseFrontmatter("---\na: \"quoted\"\nb: 'single'\nc: \"mismatched'\n---\n"); + assert.equal(meta.a, "quoted"); + assert.equal(meta.b, "single"); + assert.equal(meta.c, "\"mismatched'"); }); -test('values keep their own colons (URLs survive)', () => { - const { meta } = parseFrontmatter('---\nlink: https://example.com/x\n---\n'); - assert.equal(meta.link, 'https://example.com/x'); +test("values keep their own colons (URLs survive)", () => { + const { meta } = parseFrontmatter("---\nlink: https://example.com/x\n---\n"); + assert.equal(meta.link, "https://example.com/x"); }); -test('input without frontmatter is all body', () => { - const { meta, body } = parseFrontmatter('Just a body'); +test("input without frontmatter is all body", () => { + const { meta, body } = parseFrontmatter("Just a body"); assert.deepEqual(meta, {}); - assert.equal(body, 'Just a body'); + assert.equal(body, "Just a body"); }); -test('an unterminated frontmatter fence is treated as body, not swallowed', () => { - const raw = '---\ntitle: dangling\nno closing fence'; +test("an unterminated frontmatter fence is treated as body, not swallowed", () => { + const raw = "---\ntitle: dangling\nno closing fence"; const { meta, body } = parseFrontmatter(raw); assert.deepEqual(meta, {}); assert.equal(body, raw); diff --git a/test/video-embed.test.js b/test/video-embed.test.js index 8454196..d22ecaf 100644 --- a/test/video-embed.test.js +++ b/test/video-embed.test.js @@ -1,50 +1,50 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { parseVideoEmbed, videoEmbedSrc } from '../dist/index.js'; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { parseVideoEmbed, videoEmbedSrc } from "../dist/index.js"; -test('every allowlisted YouTube URL shape yields the same id', () => { +test("every allowlisted YouTube URL shape yields the same id", () => { for (const url of [ - 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - 'https://youtube.com/watch?v=dQw4w9WgXcQ', - 'https://youtu.be/dQw4w9WgXcQ', - 'https://www.youtube.com/embed/dQw4w9WgXcQ', - 'youtube.com/watch?v=dQw4w9WgXcQ', + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "https://youtube.com/watch?v=dQw4w9WgXcQ", + "https://youtu.be/dQw4w9WgXcQ", + "https://www.youtube.com/embed/dQw4w9WgXcQ", + "youtube.com/watch?v=dQw4w9WgXcQ", ]) { - assert.deepEqual(parseVideoEmbed(url), { provider: 'youtube', id: 'dQw4w9WgXcQ' }, url); + assert.deepEqual(parseVideoEmbed(url), { provider: "youtube", id: "dQw4w9WgXcQ" }, url); } }); -test('Vimeo URLs yield numeric ids', () => { - assert.deepEqual(parseVideoEmbed('https://vimeo.com/76979871'), { - provider: 'vimeo', - id: '76979871', +test("Vimeo URLs yield numeric ids", () => { + assert.deepEqual(parseVideoEmbed("https://vimeo.com/76979871"), { + provider: "vimeo", + id: "76979871", }); }); -test('surrounding whitespace is tolerated', () => { - assert.equal(parseVideoEmbed(' https://vimeo.com/76979871 ').id, '76979871'); +test("surrounding whitespace is tolerated", () => { + assert.equal(parseVideoEmbed(" https://vimeo.com/76979871 ").id, "76979871"); }); -test('anything off the allowlist is rejected, however video-shaped', () => { +test("anything off the allowlist is rejected, however video-shaped", () => { for (const url of [ - 'https://dailymotion.com/video/x123456', - 'https://evil.example.com/watch?v=dQw4w9WgXcQ', - 'https://youtube.com.evil.example.com/watch?v=dQw4w9WgXcQ', - 'javascript:alert(1)', - '', - 'not a url at all', + "https://dailymotion.com/video/x123456", + "https://evil.example.com/watch?v=dQw4w9WgXcQ", + "https://youtube.com.evil.example.com/watch?v=dQw4w9WgXcQ", + "javascript:alert(1)", + "", + "not a url at all", ]) { assert.equal(parseVideoEmbed(url), null, url); } }); -test('embed src is the privacy-preserving player, never the raw URL', () => { +test("embed src is the privacy-preserving player, never the raw URL", () => { assert.equal( - videoEmbedSrc({ provider: 'youtube', id: 'dQw4w9WgXcQ' }), - 'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ', + videoEmbedSrc({ provider: "youtube", id: "dQw4w9WgXcQ" }), + "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ", ); assert.equal( - videoEmbedSrc({ provider: 'vimeo', id: '76979871' }), - 'https://player.vimeo.com/video/76979871', + videoEmbedSrc({ provider: "vimeo", id: "76979871" }), + "https://player.vimeo.com/video/76979871", ); }); From 005250b2d73c85ab94e04bb69ed177aa37e84112 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:04:32 +0200 Subject: [PATCH 3/3] chore: teach git blame to skip the reformat Co-Authored-By: Claude Opus 5 --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..e161515 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs` +7337a36f999567a0c4843ad1a7c5afc9704b82f3 # prettier, 7 files