import { describe, expect, it } from "vite-plus/test";
import type { MarkdownNode } from "react-native-nitro-markdown/headless";
import {
nativeMarkdownChunkSpacing,
nativeMarkdownDocumentChunks,
nativeMarkdownDocumentRuns,
nativeMarkdownListItemBlocks,
nativeMarkdownTextRuns,
nativeMarkdownWithPreservedSoftBreaks,
} from "@t3tools/mobile-markdown-text/markdown";
describe("nativeMarkdownTextRuns", () => {
it("links a path-shaped code span without changing the same path in prose", () => {
expect(
nativeMarkdownTextRuns({
type: "paragraph",
children: [
{ type: "text", content: "/tmp/frame.png " },
{ type: "code_inline", content: "/tmp/frame.png" },
],
}),
).toEqual([
{ text: "/tmp/frame.png " },
{ text: "frame.png", href: "/tmp/frame.png", fileIcon: "image" },
]);
});
it("preserves the destination of a link with a code-formatted label", () => {
expect(
nativeMarkdownTextRuns({
type: "paragraph",
children: [
{
type: "link",
href: "https://example.com/docs",
children: [{ type: "code_inline", content: "src/main.ts" }],
},
],
}),
).toEqual([
{
text: "src/main.ts",
code: true,
href: "https://example.com/docs",
externalHost: "example.com",
},
]);
});
it("preserves inline emphasis and code styles", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{ type: "text", content: "plain " },
{ type: "bold", children: [{ type: "text", content: "bold" }] },
{ type: "text", content: " " },
{ type: "code_inline", content: "const value = 1" },
],
};
expect(nativeMarkdownTextRuns(node)).toEqual([
{ text: "plain " },
{ text: "bold", bold: true },
{ text: " " },
{ text: "const value = 1", code: true },
]);
});
it("normalizes external and file links for native presentation", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{
type: "link",
href: "https://example.com/docs",
children: [{ type: "text", content: "Docs" }],
},
{ type: "text", content: " " },
{
type: "link",
href: "file:///repo/README.md#L12",
children: [{ type: "text", content: "ignored label" }],
},
],
};
expect(nativeMarkdownTextRuns(node)).toEqual([
{
text: "Docs",
href: "https://example.com/docs",
externalHost: "example.com",
},
{ text: " " },
{
text: "README.md:12",
href: "file:///repo/README.md#L12",
fileIcon: "readme",
},
]);
});
it("keeps hard breaks and collapses soft breaks", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{ type: "text", content: "first" },
{ type: "soft_break" },
{ type: "text", content: "second" },
{ type: "line_break" },
{ type: "text", content: "third" },
],
};
expect(nativeMarkdownTextRuns(node)).toEqual([{ text: "first second\nthird" }]);
});
it("can preserve soft breaks for authored user messages", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{ type: "text", content: "first" },
{ type: "soft_break" },
{ type: "text", content: "second" },
],
};
expect(nativeMarkdownTextRuns(nativeMarkdownWithPreservedSoftBreaks(node))).toEqual([
{ text: "first\nsecond" },
]);
});
it("normalizes common inline HTML and entities", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{ type: "text", content: "Less than: < " },
{ type: "html_inline", content: "" },
{ type: "text", content: "ā" },
{ type: "html_inline", content: "" },
{ type: "html_inline", content: "
" },
{ type: "html_inline", content: "highlighted" },
],
};
expect(nativeMarkdownTextRuns(node)).toEqual([{ text: "Less than: < ā\nhighlighted" }]);
});
it("normalizes double-encoded entities and inline tags emitted as text", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{
type: "text",
content:
"Keyboard: ā + K; Less than: <; Greater than: >",
},
],
};
expect(nativeMarkdownTextRuns(node)).toEqual([
{ text: "Keyboard: ā + K; Less than: <; Greater than: >" },
]);
});
it.each([
["😀", "š"],
["🚀", "š"],
["", ""],
["", ""],
["�", ""],
["�", ""],
])("normalizes numeric entity %s without throwing", (content, expected) => {
const node: MarkdownNode = {
type: "paragraph",
children: [{ type: "text", content }],
};
expect(nativeMarkdownTextRuns(node)).toEqual([{ text: expected }]);
});
it("reads inline content from nested text nodes", () => {
const node: MarkdownNode = {
type: "paragraph",
children: [
{
type: "text",
children: [{ type: "text", content: "Plain text" }],
},
{ type: "text", content: " and " },
{
type: "code_inline",
children: [{ type: "text", content: "inline code" }],
},
],
};
expect(nativeMarkdownTextRuns(node)).toEqual([
{ text: "Plain text and " },
{ text: "inline code", code: true },
]);
});
});
describe("nativeMarkdownDocumentRuns", () => {
it("decorates known skill references as selectable skill links", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Use $ui for this." }],
},
],
};
expect(nativeMarkdownDocumentRuns(node, [{ name: "ui", displayName: "UI" }])).toEqual([
{ text: "Use ", role: "body" },
{
text: "$ui",
role: "body",
skillName: "ui",
skillLabel: "UI",
},
{ text: " for this.", role: "body" },
]);
});
it("decorates known skill references that begin with a digit", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Use $2spec for this." }],
},
],
};
expect(nativeMarkdownDocumentRuns(node, [{ name: "2spec", displayName: "2Spec" }])).toEqual([
{ text: "Use ", role: "body" },
{
text: "$2spec",
role: "body",
skillName: "2spec",
skillLabel: "2Spec",
},
{ text: " for this.", role: "body" },
]);
});
it("decorates known skill references inside blockquotes", () => {
const node: MarkdownNode = {
type: "blockquote",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Use $ui for this." }],
},
],
};
expect(nativeMarkdownDocumentRuns(node, [{ name: "ui", displayName: "UI" }])).toContainEqual({
text: "$ui",
role: "body",
skillName: "ui",
skillLabel: "UI",
});
});
it("leaves unknown skill-like text unchanged", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Use $unknown for this." }],
},
],
};
expect(nativeMarkdownDocumentRuns(node, [])).toEqual([
{ text: "Use $unknown for this.", role: "body" },
]);
});
it("keeps headings, paragraphs, and lists in one continuous document", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "heading",
level: 1,
children: [{ type: "text", content: "Header One" }],
},
{
type: "paragraph",
children: [
{ type: "text", content: "A paragraph with " },
{ type: "bold", children: [{ type: "text", content: "bold text" }] },
{ type: "text", content: "." },
],
},
{
type: "list",
ordered: false,
children: [
{
type: "list_item",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "First item" }],
},
],
},
{
type: "list_item",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Second item" }],
},
],
},
],
},
],
};
const runs = nativeMarkdownDocumentRuns(node);
expect(runs.map((run) => run.text).join("")).toBe(
"Header One\n\nA paragraph with bold text.\n\nā¢\tFirst item\nā¢\tSecond item",
);
expect(runs).toContainEqual({
text: "Header One\n",
role: "heading",
headingLevel: 1,
});
expect(runs).toContainEqual({
text: "bold text",
bold: true,
role: "body",
});
expect(runs).toContainEqual({
text: "ā¢\t",
role: "list-marker",
depth: 1,
firstLineHeadIndent: 0,
headIndent: 24,
paragraphSpacing: 2,
});
});
it("uses distinct section, heading-content, and body spacing", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Intro" }],
},
{
type: "heading",
level: 2,
children: [{ type: "text", content: "Section" }],
},
{
type: "paragraph",
children: [{ type: "text", content: "First paragraph" }],
},
{
type: "paragraph",
children: [{ type: "text", content: "Second paragraph" }],
},
],
};
expect(
nativeMarkdownDocumentRuns(node)
.filter((run) => run.role === "spacer")
.map((run) => run.spacing),
).toEqual([20, 10, 12]);
});
it("renders tight list items whose inline nodes are direct children", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "list",
children: [
{
type: "list_item",
children: [
{
type: "bold",
children: [{ type: "text", content: "Finding:" }],
},
{ type: "text", content: " details with " },
{ type: "code_inline", content: "inline code" },
{ type: "text", content: "." },
],
},
],
},
],
};
expect(nativeMarkdownDocumentRuns(node)).toEqual([
{
text: "ā¢\t",
role: "list-marker",
depth: 1,
firstLineHeadIndent: 0,
headIndent: 24,
paragraphSpacing: 2,
},
{ text: "Finding:", bold: true, role: "body", depth: 1 },
{ text: " details with ", role: "body", depth: 1 },
{ text: "inline code", code: true, role: "body", depth: 1 },
{ text: ".", role: "body", depth: 1 },
]);
});
it("preserves quotes and fenced code in document runs", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "blockquote",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Read this" }],
},
],
},
{
type: "code_block",
language: "ts",
content: "const answer = 42;",
},
],
};
const runs = nativeMarkdownDocumentRuns(node);
expect(runs.map((run) => run.text).join("")).toBe("ā\u00a0Read this\n\nTS\nconst answer = 42;");
expect(runs).toContainEqual({
text: "const answer = 42;",
code: true,
role: "code-block",
});
});
it("reads fenced code content from child text nodes", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "code_block",
language: "bash",
children: [{ type: "text", content: "pnpm install\n" }],
},
],
};
expect(
nativeMarkdownDocumentRuns(node)
.map((run) => run.text)
.join(""),
).toBe("BASH\npnpm install");
});
});
describe("nativeMarkdownListItemBlocks", () => {
it("groups consecutive inline nodes into one paragraph block", () => {
const item: MarkdownNode = {
type: "list_item",
children: [
{ type: "text", content: "Finding: " },
{ type: "bold", children: [{ type: "text", content: "important" }] },
{ type: "text", content: " details." },
{
type: "list",
children: [
{
type: "list_item",
children: [{ type: "text", content: "Nested" }],
},
],
},
{ type: "text", content: "Trailing prose." },
],
};
expect(nativeMarkdownListItemBlocks(item)).toEqual([
{
type: "paragraph",
children: item.children?.slice(0, 3),
},
item.children?.[3],
{
type: "paragraph",
children: [item.children?.[4]],
},
]);
});
});
describe("nativeMarkdownDocumentChunks", () => {
it("renders plain blockquotes as rich blocks so their marker spans wrapped lines", () => {
const blockquote: MarkdownNode = {
type: "blockquote",
beg: 0,
end: 120,
children: [
{
type: "paragraph",
children: [
{
type: "text",
content:
"Persistent random per-result keys are the strongest design, even when this text wraps.",
},
],
},
],
};
expect(
nativeMarkdownDocumentChunks({
type: "document",
children: [blockquote],
}),
).toEqual([
{
kind: "rich",
key: "rich:blockquote:0:120",
node: blockquote,
},
]);
});
it("keeps headings and plain lists in one selectable document", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "heading",
level: 2,
children: [{ type: "text", content: "Tasks" }],
},
{
type: "list",
children: [
{
type: "task_list_item",
checked: true,
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Completed" }],
},
],
},
{
type: "list_item",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Parent" }],
},
{
type: "list",
children: [
{
type: "list_item",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Nested" }],
},
],
},
],
},
],
},
],
},
],
};
const chunks = nativeMarkdownDocumentChunks(document);
expect(chunks).toHaveLength(1);
expect(chunks[0]).toMatchObject({ kind: "selectable" });
expect(
nativeMarkdownDocumentRuns(chunks[0]?.node ?? document)
.map((run) => run.text)
.join(""),
).toBe("Tasks\n\nāļø\tCompleted\nā¢\tParent\nā¦\tNested");
});
it("aligns ordered markers while keeping the list in one selectable string", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "list",
ordered: true,
start: 9,
children: [
{
type: "list_item",
children: [{ type: "text", content: "Ninth" }],
},
{
type: "list_item",
children: [{ type: "text", content: "Tenth" }],
},
],
},
],
};
expect(
nativeMarkdownDocumentRuns(document)
.map((run) => run.text)
.join(""),
).toBe("\u20079.\tNinth\n10.\tTenth");
});
it("keeps prose selectable while exposing rich AST blocks", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "heading",
level: 1,
beg: 0,
end: 9,
children: [{ type: "text", content: "Install" }],
},
{
type: "code_block",
language: "bash",
beg: 11,
end: 35,
children: [{ type: "text", content: "pnpm install\n" }],
},
{
type: "paragraph",
beg: 37,
end: 42,
children: [{ type: "text", content: "Done." }],
},
],
};
const chunks = nativeMarkdownDocumentChunks(document);
expect(chunks).toHaveLength(3);
expect(chunks[0]).toMatchObject({ kind: "selectable" });
expect(chunks[1]).toEqual({
kind: "rich",
key: "rich:code_block:11:35",
node: document.children?.[1],
});
expect(chunks[2]).toMatchObject({ kind: "selectable" });
});
it("keeps a list containing fenced code as one rich AST container", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "list",
beg: 0,
end: 45,
children: [
{
type: "list_item",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Install" }],
},
{
type: "code_block",
language: "bash",
children: [{ type: "text", content: "pnpm install\n" }],
},
],
},
],
},
],
};
expect(nativeMarkdownDocumentChunks(document)).toEqual([
{
kind: "rich",
key: "rich:list:0:45",
node: document.children?.[0],
},
]);
});
it("keeps surrounding prose selectable when rich nodes have no source offsets", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "heading",
level: 1,
children: [{ type: "text", content: "Before" }],
},
{ type: "horizontal_rule" },
{
type: "paragraph",
children: [{ type: "text", content: "After." }],
},
],
};
const chunks = nativeMarkdownDocumentChunks(document);
expect(chunks).toHaveLength(3);
expect(chunks[0]).toMatchObject({ kind: "selectable" });
expect(chunks[1]).toEqual({
kind: "rich",
key: "rich:horizontal_rule:1:1",
node: document.children?.[1],
});
expect(chunks[2]).toMatchObject({ kind: "selectable" });
});
it("keeps offset-free structural lists isolated without promoting the whole document", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Before." }],
},
{
type: "list",
ordered: true,
children: [
{
type: "list_item",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Install" }],
},
{
type: "code_block",
language: "bash",
children: [{ type: "text", content: "pnpm install\n" }],
},
],
},
],
},
{
type: "paragraph",
children: [{ type: "text", content: "After." }],
},
],
};
const chunks = nativeMarkdownDocumentChunks(document);
expect(chunks).toHaveLength(3);
expect(chunks[0]).toMatchObject({ kind: "selectable" });
expect(chunks[1]).toEqual({
kind: "rich",
key: "rich:list:1:1",
node: document.children?.[1],
});
expect(chunks[2]).toMatchObject({ kind: "selectable" });
});
it("never collapses a rich subtree into a second markdown parsing pass", () => {
const document: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Before." }],
},
{
type: "blockquote",
children: [
{
type: "list",
children: [
{
type: "list_item",
children: [
{ type: "text", content: "Run this" },
{
type: "code_block",
language: "sh",
children: [{ type: "text", content: "vp check\n" }],
},
],
},
],
},
],
},
{
type: "paragraph",
children: [{ type: "text", content: "After." }],
},
],
};
const chunks = nativeMarkdownDocumentChunks(document);
expect(chunks.map((chunk) => chunk.kind)).toEqual(["selectable", "rich", "selectable"]);
expect(chunks[1]).toMatchObject({
kind: "rich",
node: { type: "blockquote" },
});
});
it("keeps a plain list in one selectable native text container", () => {
const list: MarkdownNode = {
type: "list",
ordered: false,
children: [
{
type: "list_item",
children: [{ type: "text", content: "First" }],
},
],
};
const chunks = nativeMarkdownDocumentChunks({
type: "document",
children: [list],
});
expect(chunks).toHaveLength(1);
expect(chunks[0]).toMatchObject({
kind: "selectable",
node: { type: "document", children: [list] },
});
});
it("separates sections more than related rich blocks", () => {
const headingChunk = {
kind: "selectable" as const,
key: "heading",
node: {
type: "document",
children: [
{
type: "heading",
level: 2,
children: [{ type: "text", content: "Section" }],
},
],
} satisfies MarkdownNode,
};
const firstList = {
kind: "rich" as const,
key: "list-1",
node: { type: "list", children: [] } satisfies MarkdownNode,
};
const secondList = {
kind: "rich" as const,
key: "list-2",
node: { type: "list", children: [] } satisfies MarkdownNode,
};
expect(nativeMarkdownChunkSpacing(undefined, headingChunk)).toBe(0);
expect(nativeMarkdownChunkSpacing(headingChunk, firstList)).toBe(10);
expect(nativeMarkdownChunkSpacing(firstList, secondList)).toBe(12);
expect(nativeMarkdownChunkSpacing(firstList, headingChunk)).toBe(20);
});
});