Fix lints in tangled spindle e848828e
This runs the biome installed by bun instead of the older version in the nixery image.
That should suppress the warning about the schema being mismatched.

Also fixes a number of lint issues along with tests for markdown, which also found a bug
where links were processed before images, so "!alt text" was left behind.
Heath Stewart · 2026-05-25 14:16 6 file(s) · +100 −19
.tangled/workflows/lint.yml +2 −2
17 17
  - name: "Install dependencies"
18 18
    command: "bun install"
19 19
  - name: "Lint check"
20 -
    command: "cd packages/cli && biome lint ."
20 +
    command: "cd packages/cli && bun run biome lint ."
21 21
  - name: "Format check"
22 -
    command: "cd packages/cli && biome format ."
22 +
    command: "cd packages/cli && bun run biome format ."
packages/cli/src/commands/inject.ts +1 −1
131 131
			let content = await fs.readFile(htmlPath, "utf-8");
132 132
133 133
			// Inject the tags
134 -
			let injected = injectLinkTags(
134 +
			const injected = injectLinkTags(
135 135
				dryRun,
136 136
				relativePath,
137 137
				content,
packages/cli/src/commands/publish.ts +0 −1
24 24
	createBlueskyPost,
25 25
	addBskyPostRefToDocument,
26 26
	COVER_IMAGE_MAX_SIZE,
27 -
	getPublication,
28 27
} from "../lib/atproto";
29 28
import {
30 29
	scanContentDirectory,
packages/cli/src/components/sequoia-subscribe.js +3 −1
161 161
		const expires = new Date(
162 162
			Date.now() + 365 * 24 * 60 * 60 * 1000,
163 163
		).toUTCString();
164 +
		// biome-ignore lint/suspicious/noDocumentCookie: back-compat with older browsers
164 165
		document.cookie = `sequoia_did=${encodeURIComponent(did)}; Expires=${expires}; Path=/; SameSite=Lax; Secure`;
165 166
	} catch {
166 167
		// Cookie write may fail in some embedded contexts
200 201
 */
201 202
function clearSubscriberDid() {
202 203
	try {
204 +
		// biome-ignore lint/suspicious/noDocumentCookie: back-compat with older browsers
203 205
		document.cookie =
204 206
			"sequoia_did=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; SameSite=Lax; Secure";
205 207
	} catch {
229 231
		changed = true;
230 232
	}
231 233
232 -
	if (did && did.startsWith("did:")) {
234 +
	if (did?.startsWith("did:")) {
233 235
		storeSubscriberDid(did);
234 236
		url.searchParams.delete("sequoia_did");
235 237
		changed = true;
packages/cli/src/lib/markdown.ts +16 −13
379 379
}
380 380
381 381
export function stripMarkdownForText(markdown: string): string {
382 -
	return markdown
383 -
		.replace(/#{1,6}\s/g, "") // Remove headers
384 -
		.replace(/\*\*([^*]+)\*\*/g, "$1") // Remove bold
385 -
		.replace(/\*([^*]+)\*/g, "$1") // Remove italic
386 -
		.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Remove links, keep text
387 -
		.replace(/`{3}[\s\S]*?`{3}/g, "") // Remove code blocks
388 -
		.replace(/`([^`]+)`/g, "$1") // Remove inline code formatting
389 -
		.replace(/!\[.*?\]\(.*?\)/g, "") // Remove images
390 -
		.replace(/\n{3,}/g, "\n\n") // Normalize multiple newlines
391 -
		.replace(/(~{1,2})([^~]+?)\1/g, "$2") // Remove strikethrough for ~text~ and ~~text~~
392 -
		.replace(/<!--([^]+)-->/g, "") // Remove any html comments
393 -
		.replace(/^\s*\[\^[^\]]+\]:[^\r\n]*(?:\r?\n[ \t]+[^\r\n]*)*/gm, "") // Remove any footnotes
394 -
		.trim();
382 +
	return (
383 +
		markdown
384 +
			.replace(/#{1,6}\s/g, "") // Remove headers
385 +
			.replace(/\*\*([^*]+)\*\*/g, "$1") // Remove bold
386 +
			.replace(/\*([^*]+)\*/g, "$1") // Remove italic
387 +
			// Must run before link regex: image syntax ![alt](url) shares [alt](url) with links
388 +
			.replace(/!\[.*?\]\(.*?\)/g, "") // Remove images
389 +
			.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Remove links, keep text
390 +
			.replace(/`{3}[\s\S]*?`{3}/g, "") // Remove code blocks
391 +
			.replace(/`([^`]+)`/g, "$1") // Remove inline code formatting
392 +
			.replace(/\n{3,}/g, "\n\n") // Normalize multiple newlines
393 +
			.replace(/(~{1,2})([^~]+?)\1/g, "$2") // Remove strikethrough for ~text~ and ~~text~~
394 +
			.replace(/<!--(.+)-->/g, "") // Remove any html comments
395 +
			.replace(/^\s*\[\^[^\]]+\]:[^\r\n]*(?:\r?\n[ \t]+[^\r\n]*)*/gm, "") // Remove any footnotes
396 +
			.trim()
397 +
	);
395 398
}
packages/cli/test/markdown.test.ts +78 −1
1 1
import { describe, expect, it } from "bun:test";
2 -
import { parseFrontmatter, resolvePostPath } from "../src/lib/markdown";
2 +
import {
3 +
	parseFrontmatter,
4 +
	resolvePostPath,
5 +
	stripMarkdownForText,
6 +
} from "../src/lib/markdown";
3 7
import type { BlogPost } from "../src/lib/types";
4 8
5 9
describe("parseFrontmatter", () => {
415 419
		);
416 420
	});
417 421
});
422 +
423 +
describe("stripMarkdownForText", () => {
424 +
	it("removes h1 through h6 headers", () => {
425 +
		expect(
426 +
			stripMarkdownForText("# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6"),
427 +
		).toBe("H1\nH2\nH3\nH4\nH5\nH6");
428 +
	});
429 +
430 +
	it("removes bold formatting", () => {
431 +
		expect(stripMarkdownForText("This is **bold** text.")).toBe(
432 +
			"This is bold text.",
433 +
		);
434 +
	});
435 +
436 +
	it("removes italic formatting", () => {
437 +
		expect(stripMarkdownForText("This is *italic* text.")).toBe(
438 +
			"This is italic text.",
439 +
		);
440 +
	});
441 +
442 +
	it("removes links but keeps link text", () => {
443 +
		expect(stripMarkdownForText("[click here](https://example.com)")).toBe(
444 +
			"click here",
445 +
		);
446 +
	});
447 +
448 +
	it("removes fenced code blocks", () => {
449 +
		const input = "before\n```\nconst x = 1;\n```\nafter";
450 +
		expect(stripMarkdownForText(input)).toBe("before\n\nafter");
451 +
	});
452 +
453 +
	it("removes inline code formatting", () => {
454 +
		expect(stripMarkdownForText("Run `npm install` to install.")).toBe(
455 +
			"Run npm install to install.",
456 +
		);
457 +
	});
458 +
459 +
	it("removes images", () => {
460 +
		expect(stripMarkdownForText("before ![alt text](image.png) after")).toBe(
461 +
			"before  after",
462 +
		);
463 +
	});
464 +
465 +
	it("normalizes three or more consecutive newlines to two", () => {
466 +
		expect(stripMarkdownForText("line 1\n\n\n\nline 2")).toBe(
467 +
			"line 1\n\nline 2",
468 +
		);
469 +
	});
470 +
471 +
	it("removes single tilde strikethrough", () => {
472 +
		expect(stripMarkdownForText("~strikethrough~")).toBe("strikethrough");
473 +
	});
474 +
475 +
	it("removes double tilde strikethrough", () => {
476 +
		expect(stripMarkdownForText("~~strikethrough~~")).toBe("strikethrough");
477 +
	});
478 +
479 +
	it("removes HTML comments but preserves -- outside comments", () => {
480 +
		expect(stripMarkdownForText("a -- b <!-- c -- d --> e -- f")).toBe(
481 +
			"a -- b  e -- f",
482 +
		);
483 +
	});
484 +
485 +
	it("removes footnote definitions but not inline references", () => {
486 +
		const input =
487 +
			"Text with[^1] a reference.\n\n[^1]: The footnote definition.";
488 +
		expect(stripMarkdownForText(input)).toBe("Text with[^1] a reference.");
489 +
	});
490 +
491 +
	it("trims leading and trailing whitespace", () => {
492 +
		expect(stripMarkdownForText("\n\nhello\n\n")).toBe("hello");
493 +
	});
494 +
});