Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/docs-rich-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@frontlit/text-editor": minor
---

Add editable documentation blocks for callouts, tabs, steps, cards, accordions, code groups, and type tables, with Markdown import and export.
2 changes: 2 additions & 0 deletions apps/api/src/documentation/authoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ export async function draftReport(
}
for (const mark of node.marks ?? [])
if (mark.type === "link") links.push({ href: mark.attrs.href, path });
if (node.type === "card" && node.attrs?.href)
links.push({ href: node.attrs.href, path: `${path}.attrs.href` });
for (const [i, child] of (node.content ?? []).entries())
walk(child, `${path}.content[${i}]`);
}
Expand Down
42 changes: 42 additions & 0 deletions apps/api/src/documentation/chunking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,46 @@ describe("extractChunks", () => {
]);
expect(chunks[1].content).toBe("Setup works.");
});

it("indexes rich blocks: step headings split chunks and attribute text is searchable", () => {
const chunks = extractChunks({
type: "doc",
content: [
{
type: "steps",
content: [
{ type: "step", content: [heading("Install"), paragraph("Run it.")] },
{ type: "step", content: [heading("Configure"), paragraph("Set it.")] },
],
},
{
type: "callout",
attrs: { type: "warning", title: "Heads up" },
content: [paragraph("Keep keys secret.")],
},
{
type: "cards",
content: [
{
type: "card",
attrs: { title: "Quick start" },
content: [paragraph("Begin.")],
},
],
},
{
type: "typeTable",
attrs: {
rows: [
{ name: "apiKey", type: "string", default: null, description: "Key." },
],
},
},
],
});
expect(chunks.map((chunk) => chunk.anchor)).toEqual(["install", "configure"]);
expect(chunks[1].content).toBe(
"Set it.\n\nHeads up\n\nKeep keys secret.\n\nQuick start\nBegin.\n\napiKey string Key.",
);
});
});
68 changes: 50 additions & 18 deletions apps/api/src/documentation/chunking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ export interface DocChunk {
const CHUNK_TARGET_WORDS = 300;
const MIN_CHUNK_WORDS = 20;

type TipTapNode = { type?: string; text?: string; content?: TipTapNode[] };
type TipTapNode = {
type?: string;
text?: string;
attrs?: Record<string, any>;
content?: TipTapNode[];
};

/** Plain text of one TipTap node, its marks/attrs ignored \u2014 chunking only
* needs the words, not formatting. */
Expand All @@ -24,26 +29,53 @@ function extractText(node: TipTapNode): string {
function extractDocsText(node: TipTapNode): string {
if (node.text) return node.text;
if (node.type === "hardBreak") return "\n";
return (node.content ?? [])
.map(extractDocsText)
.join(node.type === "paragraph" || node.type === "heading" ? "" : "\n");
if (node.type === "typeTable")
return (node.attrs?.rows ?? [])
.map((row: Record<string, unknown>) =>
[row.name, row.type, row.default, row.description].filter(Boolean).join(" "),
)
.join("\n");
const children = (node.content ?? []).map(extractDocsText);
// A card's title is an attribute; its children are the description.
if (node.type === "card") children.unshift(node.attrs?.title ?? "");
return children.join(
node.type === "paragraph" || node.type === "heading" ? "" : "\n",
);
}

/** Blocks whose children are read as top-level blocks, so their headings start chunks. */
const CONTAINERS = [
"blockquote",
"bulletList",
"orderedList",
"listItem",
"table",
"tableRow",
"tableHeader",
"tableCell",
"callout",
"tabs",
"tab",
"steps",
"step",
"cards",
"accordions",
"accordion",
"codeGroup",
];

function docsBlocks(nodes: TipTapNode[]): TipTapNode[] {
return nodes.flatMap((node) =>
[
"blockquote",
"bulletList",
"orderedList",
"listItem",
"table",
"tableRow",
"tableHeader",
"tableCell",
].includes(node.type ?? "")
? docsBlocks(node.content ?? [])
: [node],
);
return nodes.flatMap((node) => {
if (!CONTAINERS.includes(node.type ?? "")) return [node];
// Callout and accordion titles and tab labels are attributes; keep them searchable.
const label = node.attrs?.title ?? node.attrs?.label;
return [
...(typeof label === "string" && label
? [{ type: "paragraph", content: [{ text: label }] }]
: []),
...docsBlocks(node.content ?? []),
];
});
}

function wordCount(text: string): number {
Expand Down
22 changes: 19 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/text-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"@floating-ui/dom": "^1.6.0",
"@tiptap/core": "^3.0.0",
"@tiptap/extension-bubble-menu": "^3.10.4",
"@tiptap/extension-code": "3.31.3",
"@tiptap/extension-code-block": "3.31.3",
"@tiptap/extension-code-block-lowlight": "^3.0.0",
"@tiptap/extension-dropcursor": "^3.0.0",
"@tiptap/extension-gapcursor": "^3.0.0",
Expand All @@ -85,14 +87,18 @@
"@tiptap/extension-link": "^3.0.0",
"@tiptap/extension-placeholder": "^3.0.0",
"@tiptap/extension-table": "^3.10.7",
"@tiptap/extensions": "3.31.3",
"@tiptap/pm": "^3.0.0",
"@tiptap/react": "^3.0.0",
"@tiptap/starter-kit": "^3.0.0",
"@tiptap/static-renderer": "^3.10.7",
"acorn": "^8.19.0",
"clsx": "^2.1.1",
"highlight.js": "^11.10.0",
"lowlight": "^2.9.0",
"lucide-react": "^0.553.0",
"mdast-util-mdx-jsx": "^3.2.0",
"micromark-extension-mdx-jsx": "^3.0.2",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
Expand Down
Loading
Loading