/** * Tests for the template a markdown document builds. * * A Lit template is two things: static strings, which Lit parses as markup, * and values, which it puts in a text node or an attribute value without * parsing. The document is safe to render exactly when none of it reaches the * first, so the tests here read both halves apart and say which one each piece * of a document landed in. That is a stronger statement than any list of * payloads, and it needs no DOM: a template is inert data until something * renders it, which `cf-markdown.browser.test.ts` does. */ import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import { nothing } from "lit"; import { isObjectOrArray } from "@commonfabric/utils/types"; import { markdownTemplate } from "./markdown-template.ts"; /** A Lit template, as the tag function returns it. */ interface Template { strings: readonly string[]; values: readonly unknown[]; } /** A directive, which stands in for a value until Lit commits it. */ interface Directive { values: readonly unknown[]; } function isTemplate(value: unknown): value is Template { return isObjectOrArray(value) && "_$litType$" in value; } function isDirective(value: unknown): value is Directive { return isObjectOrArray(value) && "_$litDirective$" in value; } /** Renders a document, discarding what its checkboxes report. */ function templateFor(source: string): unknown { return markdownTemplate(source, { checkboxToggled: () => {} }); } /** Everything Lit parses as markup when it renders `value`. */ function markupOf(value: unknown): string { if (Array.isArray(value)) return value.map(markupOf).join(""); if (isTemplate(value)) { return value.strings.join("") + value.values.map(markupOf).join(""); } return ""; } /** Everything Lit binds without parsing when it renders `value`. */ function boundValues(value: unknown, into: unknown[] = []): unknown[] { if (Array.isArray(value)) { for (const item of value) boundValues(item, into); } else if (isTemplate(value)) { for (const item of value.values) boundValues(item, into); } else if (isDirective(value)) { for (const item of value.values) boundValues(item, into); } else { into.push(value); } return into; } /** The bound values that are text, which is what a reader ends up seeing. */ function boundText(source: string): string[] { return boundValues(templateFor(source)).filter((value) => typeof value === "string" ) as string[]; } /** * A document that reaches every kind of token, used where a test needs the * builder to have been down each branch rather than any one result. */ const KITCHEN_SINK = [ "# One", "## Two", "### Three", "#### Four", "##### Five", "###### Six", "", "A paragraph with **bold**, *italic*, ~~struck~~ and `code`.", "", "> A quote.", "", "- a", "- b", "", "1. first", "", "- [ ] undone", "- [x] done", "", "| A | B |", "| :-- | --: |", "| 1 | 2 |", "", "```js", "const a = 1;", "```", "", "---", "", "[label](https://example.test/a) and ![alt](https://example.test/a.png)", "", "[Name](/of:bafyabc/field)", ].join("\n"); describe("markdown-template", () => { describe("markdownTemplate()", () => { // Each of these is a way a markdown document can carry markup, a URL that // runs script, or an event handler. const HOSTILE = [ "", '', '
hover me
', '', 'an anchor', "[a link](javascript:steal())", "[a link](JaVaScRiPt:steal())", "![an image](javascript:steal())", "[a link](data:text/html;base64,c3RlYWwoKQ==)", "[a link](vbscript:steal())", "", "# ", "| |\n| --- |\n| `