/**
* 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 ",
"",
"[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 = [
"",
'',
'