Skip to content
Source
Sign inSign up
Open History to see the last commit

markdownOrderedListNumbers.ts

62 lines2.2 KB

1import type { Root } from "mdast";
2import type { Transformer } from "unified";
3​
4interface MarkdownListNode {
5 readonly type: string;
6 readonly ordered?: boolean | null | undefined;
7 readonly start?: number | null | undefined;
8 readonly position?:
9 | { readonly start?: { readonly offset?: number | undefined } | undefined }
10 | undefined;
11 data?: { hProperties?: Record<string, unknown> | undefined } | undefined;
12 children?: MarkdownListNode[] | undefined;
14​
15const WRITTEN_NUMBER = /[ \t]*(\d{1,9})[.)]/y;
16​
17/**
18 * CommonMark numbers an ordered list from its first marker only, so answers to questions 3 and 5
19 * would render as 3 and 4. An item whose written number breaks the sequence keeps it. `source` is
20 * the text the node's offsets point into.
21 */
22export function keepOrderedListNumbers(node: MarkdownListNode, source: string): void {
23 if (!node.children) return;
24 if (node.type === "list" && node.ordered) {
25 let expected = node.start ?? 1;
26 for (const item of node.children) {
27 const offset = item.position?.start?.offset;
28 WRITTEN_NUMBER.lastIndex = offset ?? 0;
29 const digits = offset === undefined ? undefined : WRITTEN_NUMBER.exec(source)?.[1];
30 const value = digits === undefined ? expected : Number(digits);
31 if (value !== expected) {
32 item.data = {
33 ...item.data,
34 hProperties: { ...item.data?.hProperties, value: String(value) },
35 };
36 }
37 expected = value + 1;
38 }
39 }
40 for (const child of node.children) keepOrderedListNumbers(child, source);
42​
43const enabledProcessors = new WeakSet<object>();
44​
45/**
46 * Opts a processor in. User messages use it so the sent bubble shows the numbers the composer
47 * showed and the agent received; agent replies and files keep CommonMark numbering.
48 */
49export function remarkEnableOrderedListNumbers(this: object): undefined {
50 enabledProcessors.add(this);
52​
53export function keepsOrderedListNumbers(processor: object): boolean {
54 return enabledProcessors.has(processor);
56​
57/** Sits before list recovery so offsets still point into the file. */
58export function remarkKeepOrderedListNumbers(this: object): Transformer<Root> {
59 return (tree, file) => {
60 if (enabledProcessors.has(this)) keepOrderedListNumbers(tree, String(file));
61 };