feat: render richer blocks and inline nodes (task 0004)
Extend the core parser and the CLI renderer to cover the everyday Markdown body beyond headings and paragraphs: lists, blockquotes, fenced code, thematic breaks, and inline emphasis, strong, code spans, and links. Add remark-gfm to the pipeline so tables and strikethrough parse as their own nodes. Any block or inline node core does not model falls through to RawBlock / RawInline, whose verbatim value is position-sliced from the original input so unmodelled constructs round-trip byte-for-byte.
This commit was merged in pull request #3.
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import type { PhrasingContent, Root, RootContent } from "mdast";
|
||||
import type { ListItem, Node, PhrasingContent, Root, RootContent } from "mdast";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import { unified } from "unified";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import type { Block, DocumentAST, Frontmatter, InlineNode } from "./types.ts";
|
||||
import type { Block, DocumentAST, Frontmatter, InlineNode, ListItemBlock } from "./types.ts";
|
||||
|
||||
const processor = unified().use(remarkParse).use(remarkFrontmatter);
|
||||
const processor = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm);
|
||||
|
||||
export function parse(input: string): DocumentAST {
|
||||
const tree = processor.parse(input);
|
||||
const frontmatter = extractFrontmatter(tree);
|
||||
const blocks = tree.children.flatMap(translateBlock);
|
||||
const blocks = tree.children.flatMap((node) => translateBlock(node, input));
|
||||
return { frontmatter, blocks, diagnostics: [] };
|
||||
}
|
||||
|
||||
@@ -26,21 +27,74 @@ function extractFrontmatter(tree: Root): Frontmatter {
|
||||
return {};
|
||||
}
|
||||
|
||||
function translateBlock(node: RootContent): Block[] {
|
||||
if (node.type === "heading") {
|
||||
return [{ type: "heading", level: node.depth, children: translateInline(node.children) }];
|
||||
function translateBlock(node: RootContent, input: string): Block[] {
|
||||
switch (node.type) {
|
||||
case "heading":
|
||||
return [
|
||||
{ type: "heading", level: node.depth, children: translateInline(node.children, input) },
|
||||
];
|
||||
case "paragraph":
|
||||
return [{ type: "paragraph", children: translateInline(node.children, input) }];
|
||||
case "list":
|
||||
return [
|
||||
{
|
||||
type: "list",
|
||||
ordered: node.ordered ?? false,
|
||||
items: node.children.map((item) => translateListItem(item, input)),
|
||||
},
|
||||
];
|
||||
case "blockquote":
|
||||
return [
|
||||
{
|
||||
type: "blockquote",
|
||||
children: node.children.flatMap((child) => translateBlock(child, input)),
|
||||
},
|
||||
];
|
||||
case "code":
|
||||
return [{ type: "code", ...(node.lang ? { lang: node.lang } : {}), value: node.value }];
|
||||
case "thematicBreak":
|
||||
return [{ type: "thematicBreak" }];
|
||||
// Frontmatter is captured separately and must not double as a block.
|
||||
case "yaml":
|
||||
return [];
|
||||
default:
|
||||
return [{ type: "raw", value: slice(node, input) }];
|
||||
}
|
||||
if (node.type === "paragraph") {
|
||||
return [{ type: "paragraph", children: translateInline(node.children) }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function translateInline(nodes: PhrasingContent[]): InlineNode[] {
|
||||
return nodes.flatMap((node) => {
|
||||
if (node.type === "text") {
|
||||
return [{ type: "text", value: node.value }];
|
||||
function translateListItem(item: ListItem, input: string): ListItemBlock {
|
||||
return {
|
||||
type: "listItem",
|
||||
children: item.children.flatMap((child) => translateBlock(child, input)),
|
||||
};
|
||||
}
|
||||
|
||||
function translateInline(nodes: PhrasingContent[], input: string): InlineNode[] {
|
||||
return nodes.flatMap((node): InlineNode[] => {
|
||||
switch (node.type) {
|
||||
case "text":
|
||||
return [{ type: "text", value: node.value }];
|
||||
case "emphasis":
|
||||
return [{ type: "emphasis", children: translateInline(node.children, input) }];
|
||||
case "strong":
|
||||
return [{ type: "strong", children: translateInline(node.children, input) }];
|
||||
case "inlineCode":
|
||||
return [{ type: "codeSpan", value: node.value }];
|
||||
case "link":
|
||||
return [{ type: "link", href: node.url, children: translateInline(node.children, input) }];
|
||||
default:
|
||||
return [{ type: "rawInline", value: slice(node, input) }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Verbatim source for an unmodelled node, taken by position so it round-trips
|
||||
// byte-for-byte rather than being re-stringified through remark.
|
||||
function slice(node: Node, input: string): string {
|
||||
const start = node.position?.start.offset;
|
||||
const end = node.position?.end.offset;
|
||||
if (start === undefined || end === undefined) {
|
||||
return "";
|
||||
}
|
||||
return input.slice(start, end);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user