emdp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/cjs/cli.js +36 -0
  4. package/dist/cjs/gfm.js +26 -0
  5. package/dist/cjs/index.js +26 -0
  6. package/dist/cjs/parser/block-parser.js +644 -0
  7. package/dist/cjs/parser/blocks/blockquote.js +28 -0
  8. package/dist/cjs/parser/blocks/code-block-fenced.js +58 -0
  9. package/dist/cjs/parser/blocks/code-block-indented.js +47 -0
  10. package/dist/cjs/parser/blocks/heading-atx.js +29 -0
  11. package/dist/cjs/parser/blocks/heading-setext.js +24 -0
  12. package/dist/cjs/parser/blocks/html-block.js +83 -0
  13. package/dist/cjs/parser/blocks/link-reference.js +109 -0
  14. package/dist/cjs/parser/blocks/list.js +155 -0
  15. package/dist/cjs/parser/blocks/paragraph.js +20 -0
  16. package/dist/cjs/parser/blocks/table.js +163 -0
  17. package/dist/cjs/parser/blocks/task-list.js +66 -0
  18. package/dist/cjs/parser/blocks/thematic-break.js +17 -0
  19. package/dist/cjs/parser/entities.js +2133 -0
  20. package/dist/cjs/parser/gfm/block-parser.js +773 -0
  21. package/dist/cjs/parser/gfm/index.js +125 -0
  22. package/dist/cjs/parser/gfm/inline-parser.js +813 -0
  23. package/dist/cjs/parser/gfm/renderer.js +513 -0
  24. package/dist/cjs/parser/index.js +104 -0
  25. package/dist/cjs/parser/inline-parser.js +564 -0
  26. package/dist/cjs/parser/inlines/autolink-extended.js +364 -0
  27. package/dist/cjs/parser/inlines/autolink.js +44 -0
  28. package/dist/cjs/parser/inlines/code-span.js +48 -0
  29. package/dist/cjs/parser/inlines/emphasis.js +64 -0
  30. package/dist/cjs/parser/inlines/entity.js +25 -0
  31. package/dist/cjs/parser/inlines/escape.js +25 -0
  32. package/dist/cjs/parser/inlines/footnote.js +41 -0
  33. package/dist/cjs/parser/inlines/hard-break.js +45 -0
  34. package/dist/cjs/parser/inlines/image.js +9 -0
  35. package/dist/cjs/parser/inlines/link.js +166 -0
  36. package/dist/cjs/parser/inlines/soft-break.js +18 -0
  37. package/dist/cjs/parser/inlines/strikethrough.js +48 -0
  38. package/dist/cjs/parser/inlines/text.js +20 -0
  39. package/dist/cjs/parser/renderer.js +345 -0
  40. package/dist/cjs/parser/types.js +5 -0
  41. package/dist/cjs/parser/utils.js +277 -0
  42. package/dist/cli.d.ts +6 -0
  43. package/dist/cli.js +36 -0
  44. package/dist/esm/cli.js +34 -0
  45. package/dist/esm/gfm.js +5 -0
  46. package/dist/esm/index.js +5 -0
  47. package/dist/esm/package.json +3 -0
  48. package/dist/esm/parser/block-parser.js +640 -0
  49. package/dist/esm/parser/blocks/blockquote.js +22 -0
  50. package/dist/esm/parser/blocks/code-block-fenced.js +52 -0
  51. package/dist/esm/parser/blocks/code-block-indented.js +42 -0
  52. package/dist/esm/parser/blocks/heading-atx.js +24 -0
  53. package/dist/esm/parser/blocks/heading-setext.js +19 -0
  54. package/dist/esm/parser/blocks/html-block.js +77 -0
  55. package/dist/esm/parser/blocks/link-reference.js +105 -0
  56. package/dist/esm/parser/blocks/list.js +145 -0
  57. package/dist/esm/parser/blocks/paragraph.js +15 -0
  58. package/dist/esm/parser/blocks/table.js +152 -0
  59. package/dist/esm/parser/blocks/task-list.js +61 -0
  60. package/dist/esm/parser/blocks/thematic-break.js +13 -0
  61. package/dist/esm/parser/entities.js +2130 -0
  62. package/dist/esm/parser/gfm/block-parser.js +769 -0
  63. package/dist/esm/parser/gfm/index.js +115 -0
  64. package/dist/esm/parser/gfm/inline-parser.js +809 -0
  65. package/dist/esm/parser/gfm/renderer.js +509 -0
  66. package/dist/esm/parser/index.js +80 -0
  67. package/dist/esm/parser/inline-parser.js +560 -0
  68. package/dist/esm/parser/inlines/autolink-extended.js +357 -0
  69. package/dist/esm/parser/inlines/autolink.js +40 -0
  70. package/dist/esm/parser/inlines/code-span.js +44 -0
  71. package/dist/esm/parser/inlines/emphasis.js +59 -0
  72. package/dist/esm/parser/inlines/entity.js +21 -0
  73. package/dist/esm/parser/inlines/escape.js +21 -0
  74. package/dist/esm/parser/inlines/footnote.js +38 -0
  75. package/dist/esm/parser/inlines/hard-break.js +41 -0
  76. package/dist/esm/parser/inlines/image.js +4 -0
  77. package/dist/esm/parser/inlines/link.js +156 -0
  78. package/dist/esm/parser/inlines/soft-break.js +14 -0
  79. package/dist/esm/parser/inlines/strikethrough.js +42 -0
  80. package/dist/esm/parser/inlines/text.js +16 -0
  81. package/dist/esm/parser/renderer.js +341 -0
  82. package/dist/esm/parser/types.js +4 -0
  83. package/dist/esm/parser/utils.js +254 -0
  84. package/dist/gfm.d.ts +6 -0
  85. package/dist/gfm.js +26 -0
  86. package/dist/index.d.ts +5 -0
  87. package/dist/index.js +26 -0
  88. package/dist/parser/block-parser.d.ts +25 -0
  89. package/dist/parser/block-parser.js +644 -0
  90. package/dist/parser/blocks/blockquote.d.ts +8 -0
  91. package/dist/parser/blocks/blockquote.js +28 -0
  92. package/dist/parser/blocks/code-block-fenced.d.ts +14 -0
  93. package/dist/parser/blocks/code-block-fenced.js +58 -0
  94. package/dist/parser/blocks/code-block-indented.d.ts +7 -0
  95. package/dist/parser/blocks/code-block-indented.js +47 -0
  96. package/dist/parser/blocks/heading-atx.d.ts +10 -0
  97. package/dist/parser/blocks/heading-atx.js +29 -0
  98. package/dist/parser/blocks/heading-setext.d.ts +8 -0
  99. package/dist/parser/blocks/heading-setext.js +24 -0
  100. package/dist/parser/blocks/html-block.d.ts +9 -0
  101. package/dist/parser/blocks/html-block.js +83 -0
  102. package/dist/parser/blocks/link-reference.d.ts +11 -0
  103. package/dist/parser/blocks/link-reference.js +109 -0
  104. package/dist/parser/blocks/list.d.ts +25 -0
  105. package/dist/parser/blocks/list.js +155 -0
  106. package/dist/parser/blocks/paragraph.d.ts +7 -0
  107. package/dist/parser/blocks/paragraph.js +20 -0
  108. package/dist/parser/blocks/table.d.ts +13 -0
  109. package/dist/parser/blocks/table.js +163 -0
  110. package/dist/parser/blocks/task-list.d.ts +10 -0
  111. package/dist/parser/blocks/task-list.js +66 -0
  112. package/dist/parser/blocks/thematic-break.d.ts +6 -0
  113. package/dist/parser/blocks/thematic-break.js +17 -0
  114. package/dist/parser/entities.d.ts +4 -0
  115. package/dist/parser/entities.js +2133 -0
  116. package/dist/parser/gfm/block-parser.d.ts +32 -0
  117. package/dist/parser/gfm/block-parser.js +773 -0
  118. package/dist/parser/gfm/index.d.ts +31 -0
  119. package/dist/parser/gfm/index.js +125 -0
  120. package/dist/parser/gfm/inline-parser.d.ts +25 -0
  121. package/dist/parser/gfm/inline-parser.js +813 -0
  122. package/dist/parser/gfm/renderer.d.ts +43 -0
  123. package/dist/parser/gfm/renderer.js +513 -0
  124. package/dist/parser/index.d.ts +33 -0
  125. package/dist/parser/index.js +104 -0
  126. package/dist/parser/inline-parser.d.ts +16 -0
  127. package/dist/parser/inline-parser.js +564 -0
  128. package/dist/parser/inlines/autolink-extended.d.ts +24 -0
  129. package/dist/parser/inlines/autolink-extended.js +364 -0
  130. package/dist/parser/inlines/autolink.d.ts +9 -0
  131. package/dist/parser/inlines/autolink.js +44 -0
  132. package/dist/parser/inlines/code-span.d.ts +9 -0
  133. package/dist/parser/inlines/code-span.js +48 -0
  134. package/dist/parser/inlines/emphasis.d.ts +14 -0
  135. package/dist/parser/inlines/emphasis.js +64 -0
  136. package/dist/parser/inlines/entity.d.ts +8 -0
  137. package/dist/parser/inlines/entity.js +25 -0
  138. package/dist/parser/inlines/escape.d.ts +8 -0
  139. package/dist/parser/inlines/escape.js +25 -0
  140. package/dist/parser/inlines/footnote.d.ts +9 -0
  141. package/dist/parser/inlines/footnote.js +41 -0
  142. package/dist/parser/inlines/hard-break.d.ts +9 -0
  143. package/dist/parser/inlines/hard-break.js +45 -0
  144. package/dist/parser/inlines/image.d.ts +4 -0
  145. package/dist/parser/inlines/image.js +9 -0
  146. package/dist/parser/inlines/link.d.ts +33 -0
  147. package/dist/parser/inlines/link.js +166 -0
  148. package/dist/parser/inlines/soft-break.d.ts +9 -0
  149. package/dist/parser/inlines/soft-break.js +18 -0
  150. package/dist/parser/inlines/strikethrough.d.ts +16 -0
  151. package/dist/parser/inlines/strikethrough.js +48 -0
  152. package/dist/parser/inlines/text.d.ts +6 -0
  153. package/dist/parser/inlines/text.js +20 -0
  154. package/dist/parser/renderer.d.ts +33 -0
  155. package/dist/parser/renderer.js +345 -0
  156. package/dist/parser/types.d.ts +152 -0
  157. package/dist/parser/types.js +5 -0
  158. package/dist/parser/utils.d.ts +32 -0
  159. package/dist/parser/utils.js +277 -0
  160. package/dist/types/cli.d.ts +6 -0
  161. package/dist/types/gfm.d.ts +6 -0
  162. package/dist/types/index.d.ts +5 -0
  163. package/dist/types/parser/block-parser.d.ts +25 -0
  164. package/dist/types/parser/blocks/blockquote.d.ts +8 -0
  165. package/dist/types/parser/blocks/code-block-fenced.d.ts +14 -0
  166. package/dist/types/parser/blocks/code-block-indented.d.ts +7 -0
  167. package/dist/types/parser/blocks/heading-atx.d.ts +10 -0
  168. package/dist/types/parser/blocks/heading-setext.d.ts +8 -0
  169. package/dist/types/parser/blocks/html-block.d.ts +9 -0
  170. package/dist/types/parser/blocks/link-reference.d.ts +11 -0
  171. package/dist/types/parser/blocks/list.d.ts +25 -0
  172. package/dist/types/parser/blocks/paragraph.d.ts +7 -0
  173. package/dist/types/parser/blocks/table.d.ts +13 -0
  174. package/dist/types/parser/blocks/task-list.d.ts +10 -0
  175. package/dist/types/parser/blocks/thematic-break.d.ts +6 -0
  176. package/dist/types/parser/entities.d.ts +4 -0
  177. package/dist/types/parser/gfm/block-parser.d.ts +32 -0
  178. package/dist/types/parser/gfm/index.d.ts +31 -0
  179. package/dist/types/parser/gfm/inline-parser.d.ts +25 -0
  180. package/dist/types/parser/gfm/renderer.d.ts +43 -0
  181. package/dist/types/parser/index.d.ts +33 -0
  182. package/dist/types/parser/inline-parser.d.ts +16 -0
  183. package/dist/types/parser/inlines/autolink-extended.d.ts +24 -0
  184. package/dist/types/parser/inlines/autolink.d.ts +9 -0
  185. package/dist/types/parser/inlines/code-span.d.ts +9 -0
  186. package/dist/types/parser/inlines/emphasis.d.ts +14 -0
  187. package/dist/types/parser/inlines/entity.d.ts +8 -0
  188. package/dist/types/parser/inlines/escape.d.ts +8 -0
  189. package/dist/types/parser/inlines/footnote.d.ts +9 -0
  190. package/dist/types/parser/inlines/hard-break.d.ts +9 -0
  191. package/dist/types/parser/inlines/image.d.ts +4 -0
  192. package/dist/types/parser/inlines/link.d.ts +33 -0
  193. package/dist/types/parser/inlines/soft-break.d.ts +9 -0
  194. package/dist/types/parser/inlines/strikethrough.d.ts +16 -0
  195. package/dist/types/parser/inlines/text.d.ts +6 -0
  196. package/dist/types/parser/renderer.d.ts +33 -0
  197. package/dist/types/parser/types.d.ts +152 -0
  198. package/dist/types/parser/utils.d.ts +32 -0
  199. package/package.json +54 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * GFM HTML renderer that adds tables, strikethrough, and task list rendering on top of CommonMark.
3
+ */
4
+ import { applyTagFilter, escapeHtml } from '../utils.js';
5
+ function encodeUrl(url) {
6
+ let result = '';
7
+ let i = 0;
8
+ while (i < url.length) {
9
+ const char = url[i];
10
+ const code = char.charCodeAt(0);
11
+ if (char === '%' && i + 2 < url.length && /^[0-9a-fA-F]{2}$/.test(url.slice(i + 1, i + 3))) {
12
+ result += url.slice(i, i + 3).toUpperCase();
13
+ i += 3;
14
+ }
15
+ else if (/[A-Za-z0-9\-._~:/?#@!$&'()*+,;=]/.test(char)) {
16
+ result += char;
17
+ i++;
18
+ }
19
+ else if (code >= 0xD800 && code <= 0xDBFF && i + 1 < url.length) {
20
+ const fullChar = url.slice(i, i + 2);
21
+ const encoded = encodeURIComponent(fullChar);
22
+ result += encoded;
23
+ i += 2;
24
+ }
25
+ else {
26
+ const encoded = encodeURIComponent(char);
27
+ result += encoded;
28
+ i++;
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+ export class GFMHtmlRenderer {
34
+ options;
35
+ footnoteDefinitions = new Map();
36
+ footnoteOrder = [];
37
+ footnoteRefCounts = new Map();
38
+ footnoteRefIds = new Map();
39
+ constructor(options = {}) {
40
+ this.options = {
41
+ softbreak: '\n',
42
+ safe: false,
43
+ ...options,
44
+ };
45
+ }
46
+ render(document) {
47
+ this.footnoteDefinitions = document.footnoteDefinitions ?? new Map();
48
+ this.footnoteOrder = [];
49
+ this.footnoteRefCounts = new Map();
50
+ this.footnoteRefIds = new Map();
51
+ const content = this.renderBlocks(document.children);
52
+ const footnotes = this.renderFootnotes();
53
+ const combined = [content, footnotes].filter(Boolean).join('\n');
54
+ return combined ? combined + '\n' : '';
55
+ }
56
+ renderBlocks(blocks, tight = false) {
57
+ return blocks.map(block => this.renderBlock(block, tight)).join('\n');
58
+ }
59
+ renderBlock(block, tight = false) {
60
+ switch (block.type) {
61
+ case 'paragraph':
62
+ return this.renderParagraph(block, tight);
63
+ case 'heading':
64
+ return this.renderHeading(block);
65
+ case 'thematic_break':
66
+ return this.renderThematicBreak(block);
67
+ case 'code_block':
68
+ return this.renderCodeBlock(block);
69
+ case 'blockquote':
70
+ return this.renderBlockquote(block);
71
+ case 'list':
72
+ return this.renderList(block);
73
+ case 'list_item':
74
+ return this.renderListItem(block);
75
+ case 'html_block':
76
+ return this.renderHtmlBlock(block);
77
+ case 'table':
78
+ return this.renderTable(block);
79
+ default:
80
+ return '';
81
+ }
82
+ }
83
+ renderParagraph(node, tight = false) {
84
+ const content = this.renderInlines(node.children);
85
+ if (tight) {
86
+ return content;
87
+ }
88
+ return `<p>${content}</p>`;
89
+ }
90
+ renderHeading(node) {
91
+ const content = this.renderInlines(node.children);
92
+ return `<h${node.level}>${content}</h${node.level}>`;
93
+ }
94
+ renderThematicBreak(_node) {
95
+ return '<hr />';
96
+ }
97
+ renderCodeBlock(node) {
98
+ const escaped = escapeHtml(node.literal);
99
+ if (node.info) {
100
+ const langPart = node.info.split(/\s+/)[0];
101
+ const lang = escapeHtml(langPart);
102
+ let metaAttr = '';
103
+ if (this.options.fullInfoString) {
104
+ const meta = node.info.slice(langPart.length).replace(/^\s+/, '').replace(/\u0000/g, '\ufffd');
105
+ if (meta) {
106
+ metaAttr = ` data-meta="${escapeHtml(meta)}"`;
107
+ }
108
+ }
109
+ return `<pre><code class="language-${lang}"${metaAttr}>${escaped}</code></pre>`;
110
+ }
111
+ return `<pre><code>${escaped}</code></pre>`;
112
+ }
113
+ renderBlockquote(node) {
114
+ const content = this.renderBlocks(node.children);
115
+ if (content) {
116
+ return `<blockquote>\n${content}\n</blockquote>`;
117
+ }
118
+ return '<blockquote>\n</blockquote>';
119
+ }
120
+ renderList(node) {
121
+ const tag = node.listType === 'bullet' ? 'ul' : 'ol';
122
+ const startAttr = node.listType === 'ordered' && node.start !== 1 ? ` start="${node.start}"` : '';
123
+ const items = node.children.map(item => this.renderListItem(item, node.tight)).join('\n');
124
+ return `<${tag}${startAttr}>\n${items}\n</${tag}>`;
125
+ }
126
+ renderListItem(node, tight = false) {
127
+ const hasCheckbox = typeof node.checked === 'boolean';
128
+ if (node.children.length === 0) {
129
+ if (hasCheckbox) {
130
+ const checkbox = node.checked
131
+ ? '<input checked="" disabled="" type="checkbox">'
132
+ : '<input disabled="" type="checkbox">';
133
+ return `<li>${checkbox}</li>`;
134
+ }
135
+ return '<li></li>';
136
+ }
137
+ if (hasCheckbox) {
138
+ const checkbox = node.checked
139
+ ? '<input checked="" disabled="" type="checkbox"> '
140
+ : '<input disabled="" type="checkbox"> ';
141
+ const firstIsParagraph = node.children[0].type === 'paragraph';
142
+ if (tight && firstIsParagraph) {
143
+ const firstContent = this.renderBlock(node.children[0], tight);
144
+ if (node.children.length === 1) {
145
+ return `<li>${checkbox}${firstContent}</li>`;
146
+ }
147
+ const restContent = this.renderBlocks(node.children.slice(1), tight);
148
+ return `<li>${checkbox}${firstContent}\n${restContent}\n</li>`;
149
+ }
150
+ const content = this.renderBlocks(node.children, tight);
151
+ if (content) {
152
+ const contentWithCheckbox = content.replace(/^(<p>)?/, `$1${checkbox}`);
153
+ return `<li>\n${contentWithCheckbox}\n</li>`;
154
+ }
155
+ return `<li>${checkbox}</li>`;
156
+ }
157
+ const content = this.renderBlocks(node.children, tight);
158
+ if (tight && node.children.length === 1 && node.children[0].type === 'paragraph') {
159
+ return `<li>${content}</li>`;
160
+ }
161
+ if (content) {
162
+ return `<li>\n${content}\n</li>`;
163
+ }
164
+ return '<li></li>';
165
+ }
166
+ renderHtmlBlock(node) {
167
+ if (this.options.safe) {
168
+ return '<!-- raw HTML omitted -->';
169
+ }
170
+ const literal = node.literal.replace(/\n$/, '');
171
+ return this.options.tagfilter ? applyTagFilter(literal) : literal;
172
+ }
173
+ renderTable(node) {
174
+ const lines = ['<table>'];
175
+ const headerRows = node.children.filter(row => row.isHeader);
176
+ const bodyRows = node.children.filter(row => !row.isHeader);
177
+ if (headerRows.length > 0) {
178
+ lines.push('<thead>');
179
+ for (const row of headerRows) {
180
+ lines.push(this.renderTableRow(row));
181
+ }
182
+ lines.push('</thead>');
183
+ }
184
+ if (bodyRows.length > 0) {
185
+ lines.push('<tbody>');
186
+ for (const row of bodyRows) {
187
+ lines.push(this.renderTableRow(row));
188
+ }
189
+ lines.push('</tbody>');
190
+ }
191
+ lines.push('</table>');
192
+ return lines.join('\n');
193
+ }
194
+ renderTableRow(row) {
195
+ const cells = row.children.map(cell => this.renderTableCell(cell)).join('\n');
196
+ return `<tr>\n${cells}\n</tr>`;
197
+ }
198
+ renderTableCell(cell) {
199
+ const tag = cell.isHeader ? 'th' : 'td';
200
+ const alignAttr = cell.align
201
+ ? this.options.tablePreferStyleAttributes
202
+ ? ` style="text-align: ${cell.align}"`
203
+ : ` align="${cell.align}"`
204
+ : '';
205
+ const content = this.renderInlines(cell.children);
206
+ return `<${tag}${alignAttr}>${content}</${tag}>`;
207
+ }
208
+ renderInlines(inlines) {
209
+ if (this.options.smart) {
210
+ return this.renderInlinesSmart(inlines);
211
+ }
212
+ return inlines.map(inline => this.renderInline(inline)).join('');
213
+ }
214
+ renderInline(inline) {
215
+ switch (inline.type) {
216
+ case 'text':
217
+ return this.renderText(inline);
218
+ case 'softbreak':
219
+ return this.renderSoftbreak(inline);
220
+ case 'hardbreak':
221
+ return this.renderHardbreak(inline);
222
+ case 'code_span':
223
+ return this.renderCodeSpan(inline);
224
+ case 'emphasis':
225
+ return this.renderEmphasis(inline);
226
+ case 'strong':
227
+ return this.renderStrong(inline);
228
+ case 'strikethrough':
229
+ return this.renderStrikethrough(inline);
230
+ case 'footnote_ref':
231
+ return this.renderFootnoteRef(inline);
232
+ case 'link':
233
+ return this.renderLink(inline);
234
+ case 'image':
235
+ return this.renderImage(inline);
236
+ case 'html_inline':
237
+ return this.renderHtmlInline(inline);
238
+ default:
239
+ return '';
240
+ }
241
+ }
242
+ renderText(node) {
243
+ return escapeHtml(node.literal);
244
+ }
245
+ renderSoftbreak(_node) {
246
+ return this.options.softbreak;
247
+ }
248
+ renderHardbreak(_node) {
249
+ return '<br />\n';
250
+ }
251
+ renderCodeSpan(node) {
252
+ return `<code>${escapeHtml(node.literal)}</code>`;
253
+ }
254
+ renderEmphasis(node) {
255
+ const content = this.renderInlines(node.children);
256
+ return `<em>${content}</em>`;
257
+ }
258
+ renderStrong(node) {
259
+ const content = this.renderInlines(node.children);
260
+ return `<strong>${content}</strong>`;
261
+ }
262
+ renderStrikethrough(node) {
263
+ const content = this.renderInlines(node.children);
264
+ return `<del>${content}</del>`;
265
+ }
266
+ renderFootnoteRef(node) {
267
+ const def = this.footnoteDefinitions.get(node.key);
268
+ if (!def) {
269
+ return `[^${escapeHtml(node.label)}]`;
270
+ }
271
+ let index = this.footnoteOrder.indexOf(node.key);
272
+ if (index === -1) {
273
+ this.footnoteOrder.push(node.key);
274
+ index = this.footnoteOrder.length - 1;
275
+ }
276
+ const number = index + 1;
277
+ const count = (this.footnoteRefCounts.get(node.key) ?? 0) + 1;
278
+ this.footnoteRefCounts.set(node.key, count);
279
+ const encodedLabel = encodeUrl(def.label);
280
+ const refId = count === 1 ? `fnref-${encodedLabel}` : `fnref-${encodedLabel}-${count}`;
281
+ const refIds = this.footnoteRefIds.get(node.key) ?? [];
282
+ refIds.push(refId);
283
+ this.footnoteRefIds.set(node.key, refIds);
284
+ return `<sup class="footnote-ref"><a href="#fn-${encodedLabel}" id="${refId}" data-footnote-ref>${number}</a></sup>`;
285
+ }
286
+ renderLink(node) {
287
+ if (this.options.safe && /^javascript:/i.test(node.destination)) {
288
+ return this.renderInlines(node.children);
289
+ }
290
+ const href = escapeHtml(encodeUrl(node.destination));
291
+ const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
292
+ const content = this.renderInlines(node.children);
293
+ return `<a href="${href}"${title}>${content}</a>`;
294
+ }
295
+ renderImage(node) {
296
+ if (this.options.safe && /^javascript:/i.test(node.destination)) {
297
+ return escapeHtml(node.alt);
298
+ }
299
+ const src = escapeHtml(encodeUrl(node.destination));
300
+ const alt = escapeHtml(node.alt);
301
+ const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
302
+ return `<img src="${src}" alt="${alt}"${title} />`;
303
+ }
304
+ renderHtmlInline(node) {
305
+ if (this.options.safe) {
306
+ return '<!-- raw HTML omitted -->';
307
+ }
308
+ return this.options.tagfilter ? applyTagFilter(node.literal) : node.literal;
309
+ }
310
+ renderInlinesSmart(inlines) {
311
+ const tokens = [];
312
+ const nodeTokens = new Map();
313
+ const pushTextTokens = (node) => {
314
+ const indices = [];
315
+ for (let i = 0; i < node.literal.length; i++) {
316
+ const idx = tokens.length;
317
+ tokens.push({
318
+ char: node.literal[i],
319
+ node,
320
+ nodeOffset: i,
321
+ noSmart: !!node.noSmart,
322
+ });
323
+ indices.push(idx);
324
+ }
325
+ nodeTokens.set(node, indices);
326
+ };
327
+ for (const inline of inlines) {
328
+ if (inline.type === 'text') {
329
+ pushTextTokens(inline);
330
+ }
331
+ else if (inline.type === 'softbreak' || inline.type === 'hardbreak') {
332
+ tokens.push({ char: '\n', node: null, nodeOffset: 0, noSmart: false });
333
+ }
334
+ else {
335
+ tokens.push({ char: 'A', node: null, nodeOffset: 0, noSmart: false });
336
+ }
337
+ }
338
+ const replacements = new Map();
339
+ const doubleStack = [];
340
+ const singleStack = [];
341
+ const isWhitespace = (c) => /\s/.test(c);
342
+ const isPunctuation = (c) => /[\p{P}\p{S}]/u.test(c);
343
+ const isAlphaNum = (c) => /[A-Za-z0-9]/.test(c);
344
+ for (let i = 0; i < tokens.length; i++) {
345
+ const token = tokens[i];
346
+ if (token.noSmart)
347
+ continue;
348
+ if (token.char !== '"' && token.char !== "'")
349
+ continue;
350
+ const before = i > 0 ? tokens[i - 1].char : '\n';
351
+ const after = i + 1 < tokens.length ? tokens[i + 1].char : '\n';
352
+ const beforeIsWhitespace = isWhitespace(before);
353
+ const afterIsWhitespace = isWhitespace(after);
354
+ const beforeIsPunct = isPunctuation(before);
355
+ const afterIsPunct = isPunctuation(after);
356
+ let leftFlanking = !afterIsWhitespace &&
357
+ (!afterIsPunct || beforeIsWhitespace || beforeIsPunct);
358
+ let rightFlanking = !beforeIsWhitespace &&
359
+ (!beforeIsPunct || afterIsWhitespace || afterIsPunct);
360
+ if (before === ')' || before === ']') {
361
+ leftFlanking = false;
362
+ }
363
+ if (token.char === "'") {
364
+ if (isAlphaNum(before) && isAlphaNum(after)) {
365
+ replacements.set(i, '’');
366
+ continue;
367
+ }
368
+ if ((before === ')' || before === ']') && isAlphaNum(after)) {
369
+ replacements.set(i, '’');
370
+ continue;
371
+ }
372
+ if (rightFlanking && singleStack.length > 0) {
373
+ const opener = singleStack.pop();
374
+ replacements.set(opener, '‘');
375
+ replacements.set(i, '’');
376
+ continue;
377
+ }
378
+ if (leftFlanking) {
379
+ singleStack.push(i);
380
+ continue;
381
+ }
382
+ replacements.set(i, '’');
383
+ }
384
+ else {
385
+ if (rightFlanking && doubleStack.length > 0) {
386
+ const opener = doubleStack.pop();
387
+ replacements.set(opener, '“');
388
+ replacements.set(i, '”');
389
+ continue;
390
+ }
391
+ if (leftFlanking) {
392
+ doubleStack.push(i);
393
+ continue;
394
+ }
395
+ replacements.set(i, '“');
396
+ }
397
+ }
398
+ for (const idx of doubleStack) {
399
+ replacements.set(idx, '“');
400
+ }
401
+ for (const idx of singleStack) {
402
+ replacements.set(idx, '’');
403
+ }
404
+ const renderTextNodes = new Map();
405
+ for (const [node, indices] of nodeTokens.entries()) {
406
+ if (node.noSmart) {
407
+ renderTextNodes.set(node, escapeHtml(node.literal));
408
+ continue;
409
+ }
410
+ let result = '';
411
+ for (let i = 0; i < indices.length; i++) {
412
+ const tokenIndex = indices[i];
413
+ const token = tokens[tokenIndex];
414
+ const replacement = replacements.get(tokenIndex);
415
+ result += replacement ?? token.char;
416
+ }
417
+ result = this.applyDashesAndEllipses(result);
418
+ renderTextNodes.set(node, escapeHtml(result));
419
+ }
420
+ return inlines.map(inline => {
421
+ if (inline.type === 'text') {
422
+ return renderTextNodes.get(inline) ?? '';
423
+ }
424
+ return this.renderInline(inline);
425
+ }).join('');
426
+ }
427
+ applyDashesAndEllipses(text) {
428
+ let result = text.replace(/\.{3}/g, '…');
429
+ result = result.replace(/-{2,}/g, (match) => {
430
+ const count = match.length;
431
+ if (count % 3 === 0) {
432
+ return '—'.repeat(count / 3);
433
+ }
434
+ if (count % 2 === 0) {
435
+ return '–'.repeat(count / 2);
436
+ }
437
+ let emCount = Math.floor(count / 3);
438
+ const remainder = count % 3;
439
+ let enCount = 0;
440
+ if (remainder === 1) {
441
+ emCount = Math.max(0, emCount - 1);
442
+ enCount = 2;
443
+ }
444
+ else if (remainder === 2) {
445
+ enCount = 1;
446
+ }
447
+ return '—'.repeat(emCount) + '–'.repeat(enCount);
448
+ });
449
+ return result;
450
+ }
451
+ renderFootnotes() {
452
+ if (this.footnoteOrder.length === 0) {
453
+ return '';
454
+ }
455
+ const lines = ['<section class="footnotes" data-footnotes>', '<ol>'];
456
+ for (let i = 0; i < this.footnoteOrder.length; i++) {
457
+ const key = this.footnoteOrder[i];
458
+ const def = this.footnoteDefinitions.get(key);
459
+ if (!def)
460
+ continue;
461
+ const encodedLabel = encodeUrl(def.label);
462
+ const number = i + 1;
463
+ const refIds = this.footnoteRefIds.get(key) ?? [];
464
+ const backrefs = this.renderFootnoteBackrefs(number, refIds);
465
+ const blocks = def.blocks;
466
+ lines.push(`<li id="fn-${encodedLabel}">`);
467
+ if (blocks.length === 0) {
468
+ if (backrefs) {
469
+ lines.push(backrefs);
470
+ }
471
+ }
472
+ else {
473
+ const lastBlock = blocks[blocks.length - 1];
474
+ const renderedBlocks = [];
475
+ for (let b = 0; b < blocks.length; b++) {
476
+ const block = blocks[b];
477
+ if (b === blocks.length - 1 && block.type === 'paragraph' && backrefs) {
478
+ const content = this.renderInlines(block.children);
479
+ renderedBlocks.push(`<p>${content}${backrefs}</p>`);
480
+ }
481
+ else {
482
+ renderedBlocks.push(this.renderBlock(block, false));
483
+ }
484
+ }
485
+ if (lastBlock.type !== 'paragraph' && backrefs) {
486
+ renderedBlocks.push(backrefs.trim());
487
+ }
488
+ lines.push(renderedBlocks.join('\n'));
489
+ }
490
+ lines.push('</li>');
491
+ }
492
+ lines.push('</ol>');
493
+ lines.push('</section>');
494
+ return lines.join('\n');
495
+ }
496
+ renderFootnoteBackrefs(number, refIds) {
497
+ if (refIds.length === 0) {
498
+ return '';
499
+ }
500
+ const links = refIds.map((refId, index) => {
501
+ const refIndex = index + 1;
502
+ const dataIdx = refIndex === 1 ? `${number}` : `${number}-${refIndex}`;
503
+ const label = `Back to reference ${dataIdx}`;
504
+ const suffix = refIndex === 1 ? '' : `<sup class="footnote-ref">${refIndex}</sup>`;
505
+ return `<a href="#${refId}" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="${dataIdx}" aria-label="${label}">↩${suffix}</a>`;
506
+ });
507
+ return ` ${links.join(' ')}`;
508
+ }
509
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Core parser module that coordinates block and inline parsing and exposes the main parser
3
+ * class and convenience helpers.
4
+ */
5
+ import { BlockParser } from './block-parser.js';
6
+ import { InlineParser } from './inline-parser.js';
7
+ import { HtmlRenderer } from './renderer.js';
8
+ export * from './types.js';
9
+ export { BlockParser } from './block-parser.js';
10
+ export { InlineParser } from './inline-parser.js';
11
+ export { HtmlRenderer } from './renderer.js';
12
+ export class MarkdownParser {
13
+ blockParser;
14
+ inlineParser;
15
+ options;
16
+ constructor(options = {}) {
17
+ this.options = options;
18
+ this.blockParser = new BlockParser();
19
+ this.inlineParser = new InlineParser();
20
+ }
21
+ parse(input) {
22
+ const { document, linkReferences } = this.blockParser.parse(input);
23
+ this.inlineParser.setLinkReferences(linkReferences);
24
+ this.processInlines(document, linkReferences);
25
+ return document;
26
+ }
27
+ processInlines(document, linkReferences) {
28
+ const processBlock = (block) => {
29
+ if (block.type === 'paragraph') {
30
+ const para = block;
31
+ const rawContent = para.rawContent;
32
+ if (rawContent) {
33
+ para.children = this.inlineParser.parse(rawContent);
34
+ delete para.rawContent;
35
+ }
36
+ }
37
+ else if (block.type === 'heading') {
38
+ const heading = block;
39
+ const rawContent = heading.rawContent;
40
+ if (rawContent) {
41
+ heading.children = this.inlineParser.parse(rawContent);
42
+ delete heading.rawContent;
43
+ }
44
+ }
45
+ else if (block.type === 'blockquote') {
46
+ block.children.forEach(processBlock);
47
+ }
48
+ else if (block.type === 'list') {
49
+ block.children.forEach(item => {
50
+ item.children.forEach(processBlock);
51
+ });
52
+ }
53
+ else if (block.type === 'list_item') {
54
+ block.children.forEach(processBlock);
55
+ }
56
+ };
57
+ document.children.forEach(processBlock);
58
+ }
59
+ }
60
+ export function parse(input, options) {
61
+ const parser = new MarkdownParser(options);
62
+ return parser.parse(input);
63
+ }
64
+ export function render(document, options) {
65
+ const renderer = new HtmlRenderer(options);
66
+ return renderer.render(document);
67
+ }
68
+ export function markdown(input, options) {
69
+ const document = parse(input, options);
70
+ return render(document, options);
71
+ }
72
+ export default {
73
+ parse,
74
+ render,
75
+ markdown,
76
+ MarkdownParser,
77
+ BlockParser,
78
+ InlineParser,
79
+ HtmlRenderer,
80
+ };