Skip to content
Source
Sign inSign up

markdownListIndentation.ts

142 lines4.0 KB

1import { keepOrderedListNumbers, keepsOrderedListNumbers } from "./markdownOrderedListNumbers.ts";
2​
3interface MarkdownPosition {
4 readonly start?: {
5 readonly line?: number;
6 readonly offset?: number;
7 };
8}
9​
10interface MarkdownAstNode {
11 readonly type: string;
12 readonly value?: unknown;
13 readonly position?: MarkdownPosition;
14 children?: MarkdownAstNode[];
16​
17interface MarkdownFile {
18 readonly value?: unknown;
20​
21interface MarkdownParser {
22 parse(markdown: string): unknown;
24​
25interface RecoveredMarkdown {
26 readonly blocks: MarkdownAstNode[];
27 readonly source: string;
29​
30const INLINE_PARSE_PREFIX = "t3-markdown-inline-prefix:";
31​
32function isSameLineOverIndentedCode(
33 node: MarkdownAstNode,
34 parent: MarkdownAstNode | undefined,
35 markdown: string,
36): boolean {
37 if (
38 node.type !== "code" ||
39 parent?.type !== "listItem" ||
40 typeof node.value !== "string" ||
41 !/^[\t ]/.test(node.value)
42 ) {
43 return false;
44 }
45​
46 const nodeStart = node.position?.start;
47 const parentStart = parent.position?.start;
48 if (
49 nodeStart?.line === undefined ||
50 nodeStart.offset === undefined ||
51 parentStart?.line === undefined ||
52 nodeStart.line !== parentStart.line
53 ) {
54 return false;
55 }
56​
57 const sourceCharacter = markdown[nodeStart.offset];
58 return sourceCharacter !== "`" && sourceCharacter !== "~";
60​
61function parseRecoveredMarkdown(value: string, parser: MarkdownParser): RecoveredMarkdown {
62 // Prefix block-looking input to parse inline extensions; retain later block children after blank lines.
63 const source = `${INLINE_PARSE_PREFIX}${value}`;
64 const document = parser.parse(source) as MarkdownAstNode;
65 const blocks = document.children;
66 const paragraph = blocks?.[0];
67 const children = paragraph?.type === "paragraph" ? paragraph.children : undefined;
68 const first = children?.[0];
69 if (
70 !blocks ||
71 !children ||
72 first?.type !== "text" ||
73 typeof first.value !== "string" ||
74 !first.value.startsWith(INLINE_PARSE_PREFIX)
75 ) {
76 return { blocks: [{ type: "text", value }], source };
77 }
78​
79 const firstValue = first.value.slice(INLINE_PARSE_PREFIX.length);
80 return {
81 blocks: [
82 {
83 ...paragraph,
84 type: "paragraph",
85 children: [...(firstValue ? [{ ...first, value: firstValue }] : []), ...children.slice(1)],
86 },
87 ...blocks.slice(1),
88 ],
89 source,
90 };
92​
93function blocksFromIndentedCode(node: MarkdownAstNode, parser: MarkdownParser): RecoveredMarkdown {
94 const value = typeof node.value === "string" ? node.value.trim() : "";
95 const recovered = parseRecoveredMarkdown(value, parser);
96 if (keepsOrderedListNumbers(parser)) {
97 for (const block of recovered.blocks) keepOrderedListNumbers(block, recovered.source);
98 }
99 const first = recovered.blocks[0];
100 return {
101 ...recovered,
102 blocks:
103 first && node.position
104 ? [{ ...first, position: node.position }, ...recovered.blocks.slice(1)]
105 : recovered.blocks,
106 };
108​
109// CommonMark treats four or more spaces after a list marker as code.
110function attachListItemIndentationNormalizer(this: MarkdownParser) {
111 return (tree: MarkdownAstNode, file: MarkdownFile) => {
112 if (typeof file.value !== "string") {
113 return;
114 }
115 const markdown = file.value;
116​
117 const visit = (node: MarkdownAstNode, source: string) => {
118 if (!node.children) {
119 return;
120 }
121 if (!node.children.some((child) => isSameLineOverIndentedCode(child, node, source))) {
122 for (const child of node.children) visit(child, source);
123 return;
124 }
125 node.children = node.children.flatMap((child) => {
126 if (isSameLineOverIndentedCode(child, node, source)) {
127 const recovered = blocksFromIndentedCode(child, this);
128 for (const block of recovered.blocks) {
129 visit(block, recovered.source);
130 }
131 return recovered.blocks;
132 }
133 visit(child, source);
134 return [child];
135 });
136 };
137​
138 visit(tree, markdown);
139 };
141​
142export const remarkNormalizeListItemIndentation = attachListItemIndentationNormalizer;