md2rfc 0.0.1

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.
package/src/Xml.ts ADDED
@@ -0,0 +1,420 @@
1
+ import * as Document from "./Document.js";
2
+
3
+ // Map of original reference names to their slugified anchors
4
+ let customRefMap: Map<string, string> = new Map();
5
+
6
+ export function fromMarkdown(markdown: string): string {
7
+ return fromDocument(Document.fromMarkdown(markdown));
8
+ }
9
+
10
+ export function fromDocument(doc: Document.Document): string {
11
+ // Build the custom reference map from both normative and informative refs
12
+ // Maps original name (e.g., "Tempo Payment Method") to anchor (e.g., "tempo-payment-method")
13
+ customRefMap = new Map();
14
+ for (const ref of [...doc.references.customNormative, ...doc.references.customInformative]) {
15
+ customRefMap.set(ref.originalName, ref.anchor);
16
+ }
17
+
18
+ const parts: string[] = [];
19
+
20
+ parts.push('<?xml version="1.0" encoding="UTF-8"?>');
21
+ parts.push("<!DOCTYPE rfc [");
22
+ parts.push(' <!ENTITY nbsp "&#160;">');
23
+ parts.push(' <!ENTITY zwsp "&#8203;">');
24
+ parts.push("]>");
25
+ parts.push('<rfc xmlns:xi="http://www.w3.org/2001/XInclude"');
26
+ parts.push(` ipr="${escapeXml(doc.meta.ipr || "trust200902")}"`);
27
+ parts.push(` docName="${escapeXml(doc.meta.docName)}"`);
28
+ parts.push(` submissionType="${escapeXml(doc.meta.submissionType || "IETF")}"`);
29
+ parts.push(` category="${escapeXml(doc.meta.category || "std")}"`);
30
+ parts.push(` consensus="${doc.meta.consensus !== false ? "true" : "false"}"`);
31
+ parts.push(' version="3">');
32
+ parts.push("");
33
+
34
+ parts.push(buildFront(doc));
35
+ parts.push("");
36
+ parts.push(buildMiddle(doc));
37
+ parts.push("");
38
+ parts.push(buildBack(doc));
39
+ parts.push("");
40
+ parts.push("</rfc>");
41
+
42
+ return parts.join("\n");
43
+ }
44
+
45
+ function buildFront(doc: Document.Document): string {
46
+ const lines: string[] = [];
47
+ lines.push(" <front>");
48
+ lines.push(` <title>${escapeXml(doc.meta.title)}</title>`);
49
+ lines.push(` <seriesInfo name="Internet-Draft" value="${escapeXml(doc.meta.docName)}"/>`);
50
+
51
+ for (const author of doc.meta.authors) {
52
+ lines.push(` <author fullname="${escapeXml(author.fullname)}">`);
53
+ if (author.organization || author.email) {
54
+ lines.push(" <address>");
55
+ if (author.email) {
56
+ lines.push(` <email>${escapeXml(author.email)}</email>`);
57
+ }
58
+ lines.push(" </address>");
59
+ }
60
+ lines.push(" </author>");
61
+ }
62
+
63
+ lines.push(" <date/>");
64
+
65
+ if (doc.meta.abstract) {
66
+ lines.push(" <abstract>");
67
+ const paragraphs = doc.meta.abstract.split(/\n\n+/);
68
+ for (const para of paragraphs) {
69
+ if (para.trim()) {
70
+ lines.push(` <t>${processInlineElements(para.trim())}</t>`);
71
+ }
72
+ }
73
+ lines.push(" </abstract>");
74
+ }
75
+
76
+ lines.push(" </front>");
77
+ return lines.join("\n");
78
+ }
79
+
80
+ function buildMiddle(doc: Document.Document): string {
81
+ const lines: string[] = [];
82
+ lines.push(" <middle>");
83
+
84
+ for (const section of doc.sections) {
85
+ lines.push(buildSection(section, 2));
86
+ }
87
+
88
+ lines.push(" </middle>");
89
+ return lines.join("\n");
90
+ }
91
+
92
+ function buildSection(section: Document.Section, indent: number): string {
93
+ const pad = " ".repeat(indent);
94
+ const lines: string[] = [];
95
+
96
+ lines.push(`${pad}<section anchor="${section.anchor}">`);
97
+ lines.push(`${pad} <name>${escapeXml(section.name)}</name>`);
98
+
99
+ const content = section.content.join("\n");
100
+ lines.push(processContent(content, indent + 1));
101
+
102
+ for (const child of section.children) {
103
+ lines.push(buildSection(child, indent + 1));
104
+ }
105
+
106
+ lines.push(`${pad}</section>`);
107
+ return lines.join("\n");
108
+ }
109
+
110
+ function buildCustomReference(ref: Document.CustomReference, indent: number): string {
111
+ const pad = " ".repeat(indent);
112
+ const lines: string[] = [];
113
+
114
+ if (ref.target) {
115
+ lines.push(`${pad}<reference anchor="${escapeXml(ref.anchor)}" target="${escapeXml(ref.target)}">`);
116
+ } else {
117
+ lines.push(`${pad}<reference anchor="${escapeXml(ref.anchor)}">`);
118
+ }
119
+ lines.push(`${pad} <front>`);
120
+ lines.push(`${pad} <title>${escapeXml(ref.title)}</title>`);
121
+ if (ref.author) {
122
+ // Try to parse author - could be "Org" or "LastName, FirstName" or just a name
123
+ const orgMatch = ref.author.match(/^([A-Z0-9]+)$/);
124
+ if (orgMatch) {
125
+ lines.push(`${pad} <author><organization>${escapeXml(ref.author)}</organization></author>`);
126
+ } else {
127
+ lines.push(`${pad} <author fullname="${escapeXml(ref.author)}"/>`);
128
+ }
129
+ } else {
130
+ lines.push(`${pad} <author/>`);
131
+ }
132
+ if (ref.date) {
133
+ lines.push(`${pad} <date year="${escapeXml(ref.date)}"/>`);
134
+ }
135
+ lines.push(`${pad} </front>`);
136
+ lines.push(`${pad}</reference>`);
137
+
138
+ return lines.join("\n");
139
+ }
140
+
141
+ function buildBack(doc: Document.Document): string {
142
+ const lines: string[] = [];
143
+ lines.push(" <back>");
144
+
145
+ const hasNormative = doc.references.normative.length > 0 || doc.references.customNormative.length > 0;
146
+ const hasInformative = doc.references.informative.length > 0 || doc.references.customInformative.length > 0;
147
+
148
+ if (hasNormative || hasInformative) {
149
+ lines.push(" <references>");
150
+ lines.push(" <name>References</name>");
151
+
152
+ if (hasNormative) {
153
+ lines.push(" <references>");
154
+ lines.push(" <name>Normative References</name>");
155
+ for (const rfc of doc.references.normative) {
156
+ lines.push(
157
+ ` <xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.${rfc}.xml"/>`,
158
+ );
159
+ }
160
+ for (const ref of doc.references.customNormative) {
161
+ lines.push(buildCustomReference(ref, 4));
162
+ }
163
+ lines.push(" </references>");
164
+ }
165
+
166
+ if (hasInformative) {
167
+ lines.push(" <references>");
168
+ lines.push(" <name>Informative References</name>");
169
+ for (const rfc of doc.references.informative) {
170
+ lines.push(
171
+ ` <xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.${rfc}.xml"/>`,
172
+ );
173
+ }
174
+ for (const ref of doc.references.customInformative) {
175
+ lines.push(buildCustomReference(ref, 4));
176
+ }
177
+ lines.push(" </references>");
178
+ }
179
+
180
+ lines.push(" </references>");
181
+ }
182
+
183
+ for (const appendix of doc.appendices) {
184
+ lines.push(buildSection(appendix, 2));
185
+ }
186
+
187
+ if (doc.acknowledgements) {
188
+ lines.push(' <section anchor="acknowledgements" numbered="false">');
189
+ lines.push(" <name>Acknowledgements</name>");
190
+ lines.push(processContent(doc.acknowledgements, 3));
191
+ lines.push(" </section>");
192
+ }
193
+
194
+ lines.push(" </back>");
195
+ return lines.join("\n");
196
+ }
197
+
198
+ function processContent(content: string, indent: number): string {
199
+ const pad = " ".repeat(indent);
200
+ const lines: string[] = [];
201
+ const rawLines = content.split("\n");
202
+
203
+ let i = 0;
204
+ while (i < rawLines.length) {
205
+ const line = rawLines[i];
206
+
207
+ if (line.startsWith("```")) {
208
+ const langMatch = line.match(/^```(\w*)/);
209
+ const lang = langMatch?.[1] || "";
210
+ const codeLines: string[] = [];
211
+ i++;
212
+ while (i < rawLines.length && !rawLines[i].startsWith("```")) {
213
+ codeLines.push(rawLines[i]);
214
+ i++;
215
+ }
216
+ i++;
217
+
218
+ const isAsciiArt =
219
+ lang === "" &&
220
+ codeLines.some((l) => l.includes("+--") || l.includes("│") || l.includes("├"));
221
+ const tag = isAsciiArt ? "artwork" : "sourcecode";
222
+ const typeAttr =
223
+ lang && !isAsciiArt ? ` type="${lang}"` : isAsciiArt ? ' type="ascii-art"' : "";
224
+
225
+ lines.push(`${pad}<${tag}${typeAttr}><![CDATA[`);
226
+ lines.push(codeLines.join("\n"));
227
+ lines.push(`]]></${tag}>`);
228
+ continue;
229
+ }
230
+
231
+ if (line.startsWith("| ")) {
232
+ const tableLines: string[] = [];
233
+ while (i < rawLines.length && rawLines[i].startsWith("|")) {
234
+ tableLines.push(rawLines[i]);
235
+ i++;
236
+ }
237
+ lines.push(processTable(tableLines, indent));
238
+ continue;
239
+ }
240
+
241
+ if (line.match(/^[-*]\s+/)) {
242
+ const listItems: string[] = [];
243
+ while (i < rawLines.length && rawLines[i].match(/^[-*]\s+/)) {
244
+ listItems.push(rawLines[i].replace(/^[-*]\s+/, ""));
245
+ i++;
246
+ }
247
+ lines.push(`${pad}<ul>`);
248
+ for (const item of listItems) {
249
+ lines.push(`${pad} <li>${processInlineElements(item)}</li>`);
250
+ }
251
+ lines.push(`${pad}</ul>`);
252
+ continue;
253
+ }
254
+
255
+ if (line.match(/^\d+\.\s+/)) {
256
+ const listItems: string[] = [];
257
+ while (i < rawLines.length && rawLines[i].match(/^\d+\.\s+/)) {
258
+ listItems.push(rawLines[i].replace(/^\d+\.\s+/, ""));
259
+ i++;
260
+ }
261
+ lines.push(`${pad}<ol type="1">`);
262
+ for (const item of listItems) {
263
+ lines.push(`${pad} <li>${processInlineElements(item)}</li>`);
264
+ }
265
+ lines.push(`${pad}</ol>`);
266
+ continue;
267
+ }
268
+
269
+ if (line.match(/^\*\*[^*]+\*\*\s*$/) && rawLines[i + 1]?.startsWith(": ")) {
270
+ const dlItems: { term: string; def: string[] }[] = [];
271
+ while (i < rawLines.length) {
272
+ const termMatch = rawLines[i].match(/^\*\*([^*]+)\*\*\s*$/);
273
+ if (!termMatch) break;
274
+ const term = termMatch[1];
275
+ i++;
276
+ if (rawLines[i]?.startsWith(": ")) {
277
+ const defLines: string[] = [rawLines[i].slice(2)];
278
+ i++;
279
+ while (
280
+ i < rawLines.length &&
281
+ rawLines[i].trim() !== "" &&
282
+ !rawLines[i].match(/^\*\*[^*]+\*\*\s*$/) &&
283
+ !rawLines[i].startsWith("## ")
284
+ ) {
285
+ defLines.push(rawLines[i].startsWith(" ") ? rawLines[i].slice(2) : rawLines[i]);
286
+ i++;
287
+ }
288
+ dlItems.push({ term, def: defLines });
289
+ } else {
290
+ break;
291
+ }
292
+ while (i < rawLines.length && rawLines[i].trim() === "") i++;
293
+ }
294
+ lines.push(`${pad}<dl>`);
295
+ for (const { term, def } of dlItems) {
296
+ lines.push(`${pad} <dt>${escapeXml(term)}</dt>`);
297
+ lines.push(`${pad} <dd><t>${processInlineElements(def.join(" "))}</t></dd>`);
298
+ }
299
+ lines.push(`${pad}</dl>`);
300
+ continue;
301
+ }
302
+
303
+ if (line.trim() === "" || line === "---") {
304
+ i++;
305
+ continue;
306
+ }
307
+
308
+ if (line.startsWith("#")) {
309
+ i++;
310
+ continue;
311
+ }
312
+
313
+ let paragraph = line;
314
+ i++;
315
+ while (
316
+ i < rawLines.length &&
317
+ rawLines[i].trim() !== "" &&
318
+ !rawLines[i].startsWith("```") &&
319
+ !rawLines[i].startsWith("| ") &&
320
+ !rawLines[i].match(/^[-*]\s+/) &&
321
+ !rawLines[i].match(/^\d+\.\s+/) &&
322
+ !rawLines[i].startsWith("#")
323
+ ) {
324
+ paragraph += ` ${rawLines[i]}`;
325
+ i++;
326
+ }
327
+
328
+ if (paragraph.trim()) {
329
+ const standaloneBoldMatch = paragraph.trim().match(/^\*\*([^*]+)\*\*$/);
330
+ if (standaloneBoldMatch) {
331
+ lines.push(`${pad}<t>${escapeXml(standaloneBoldMatch[1])}</t>`);
332
+ } else {
333
+ lines.push(`${pad}<t>${processInlineElements(paragraph.trim())}</t>`);
334
+ }
335
+ }
336
+ }
337
+
338
+ return lines.join("\n");
339
+ }
340
+
341
+ function processTable(tableLines: string[], indent: number): string {
342
+ const pad = " ".repeat(indent);
343
+ const lines: string[] = [];
344
+
345
+ const rows = tableLines
346
+ .filter((l) => !l.match(/^\|[-:| ]+\|$/))
347
+ .map((l) =>
348
+ l
349
+ .split("|")
350
+ .slice(1, -1)
351
+ .map((c) => c.trim()),
352
+ );
353
+
354
+ if (rows.length === 0) return "";
355
+
356
+ lines.push(`${pad}<table>`);
357
+
358
+ const [header, ...body] = rows;
359
+ lines.push(`${pad} <thead>`);
360
+ lines.push(`${pad} <tr>`);
361
+ for (const cell of header) {
362
+ lines.push(`${pad} <th>${processInlineElements(cell)}</th>`);
363
+ }
364
+ lines.push(`${pad} </tr>`);
365
+ lines.push(`${pad} </thead>`);
366
+
367
+ if (body.length > 0) {
368
+ lines.push(`${pad} <tbody>`);
369
+ for (const row of body) {
370
+ lines.push(`${pad} <tr>`);
371
+ for (const cell of row) {
372
+ lines.push(`${pad} <td>${processInlineElements(cell)}</td>`);
373
+ }
374
+ lines.push(`${pad} </tr>`);
375
+ }
376
+ lines.push(`${pad} </tbody>`);
377
+ }
378
+
379
+ lines.push(`${pad}</table>`);
380
+ return lines.join("\n");
381
+ }
382
+
383
+ function processInlineElements(text: string): string {
384
+ let result = escapeXml(text);
385
+
386
+ result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
387
+ result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
388
+ result = result.replace(/`([^`]+)`/g, "<tt>$1</tt>");
389
+ result = result.replace(/\[RFC(\d+)\]/g, '<xref target="RFC$1"/>');
390
+
391
+ // Convert custom references like [Tempo Payment Method] to xref
392
+ result = result.replace(/\[([^\]]+)\]/g, (match, name) => {
393
+ // Skip if it's a markdown link (will be handled below)
394
+ if (result.includes(`${match}(`)) {
395
+ return match;
396
+ }
397
+ // Check if this is a known custom reference
398
+ const anchor = customRefMap.get(name);
399
+ if (anchor) {
400
+ return `<xref target="${anchor}"/>`;
401
+ }
402
+ // Not a known reference, leave as-is
403
+ return match;
404
+ });
405
+
406
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<eref target="$2">$1</eref>');
407
+
408
+ result = Document.wrapBcp14Keywords(result);
409
+
410
+ return result;
411
+ }
412
+
413
+ function escapeXml(text: string): string {
414
+ return text
415
+ .replace(/&/g, "&amp;")
416
+ .replace(/</g, "&lt;")
417
+ .replace(/>/g, "&gt;")
418
+ .replace(/"/g, "&quot;")
419
+ .replace(/'/g, "&apos;");
420
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * as Document from "./Document.js";
2
+ export * as Xml from "./Xml.js";
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["bun"]
9
+ }
10
+ }