(html: string)
| 90 | const CODE_PLACEHOLDER_RESTORE_RE = /_MUYA_FN_GUARD_(\d+)_/g; |
| 91 | |
| 92 | function transformFootnotes(html: string): string { |
| 93 | // 1. Lift every footnote-block out of the body, remembering the rendered |
| 94 | // definition html keyed by identifier. The body of the def is the inner |
| 95 | // html marked already produced — paragraphs, lists, code, etc. |
| 96 | const definitions = new Map<string, string>(); |
| 97 | let body = html.replace(FOOTNOTE_DEF_RE, (_, id: string, inner: string) => { |
| 98 | // First definition wins for duplicate identifiers — matches the way |
| 99 | // pandoc / GFM linkrefs treat repeated labels and what the plan asks |
| 100 | // for (Section 十, risk #2). |
| 101 | if (!definitions.has(id)) |
| 102 | definitions.set(id, inner); |
| 103 | return ''; |
| 104 | }); |
| 105 | |
| 106 | if (definitions.size === 0) |
| 107 | return html; |
| 108 | |
| 109 | // 2. Stash code spans / blocks so step 3 only scans live prose. |
| 110 | const codeSlots: string[] = []; |
| 111 | body = body.replace(CODE_GUARD_RE, (m) => { |
| 112 | codeSlots.push(m); |
| 113 | return `${CODE_PLACEHOLDER_PREFIX}${codeSlots.length - 1}_`; |
| 114 | }); |
| 115 | |
| 116 | // 3. Find inline `[^id]` references in source order. Numbering follows |
| 117 | // inline order (pandoc / GFM convention), not the order definitions |
| 118 | // appear in source. Orphan refs (no matching def) stay as plain text; |
| 119 | // repeats reuse the first-seen number. |
| 120 | const refNumber = new Map<string, number>(); |
| 121 | let nextN = 1; |
| 122 | body = body.replace(FOOTNOTE_REF_RE, (match, id: string) => { |
| 123 | if (!definitions.has(id)) |
| 124 | return match; |
| 125 | if (!refNumber.has(id)) |
| 126 | refNumber.set(id, nextN++); |
| 127 | const n = refNumber.get(id)!; |
| 128 | return `<sup class="footnote-ref"><a href="#fn-${n}" id="fnref-${n}">${n}</a></sup>`; |
| 129 | }); |
| 130 | |
| 131 | // 4. Restore the protected code regions. |
| 132 | body = body.replace(CODE_PLACEHOLDER_RESTORE_RE, (_, i) => codeSlots[Number(i)]); |
| 133 | |
| 134 | if (refNumber.size === 0) |
| 135 | return body; |
| 136 | |
| 137 | // 5. Build the footnotes section in numeric order. Orphan definitions |
| 138 | // (defined but never referenced inline) are dropped — same as the |
| 139 | // parser-extension behaviour marktext shipped. |
| 140 | const orderedRefs = Array.from(refNumber.entries()).sort( |
| 141 | (a, b) => a[1] - b[1], |
| 142 | ); |
| 143 | const items: string[] = []; |
| 144 | for (const [id, n] of orderedRefs) { |
| 145 | const inner = definitions.get(id) ?? ''; |
| 146 | items.push(`<li id="fn-${n}">${appendBackref(inner, n)}</li>`); |
| 147 | } |
| 148 | |
| 149 | const section = `\n<section class="footnotes">\n<ol>\n${items.join('\n')}\n</ol>\n</section>\n`; |
no test coverage detected