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/README.md +26 -0
- package/biome.json +24 -0
- package/bun.lock +58 -0
- package/dist/Cli.d.ts +3 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +54 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Document.d.ts +48 -0
- package/dist/Document.d.ts.map +1 -0
- package/dist/Document.js +426 -0
- package/dist/Document.js.map +1 -0
- package/dist/Xml.d.ts +4 -0
- package/dist/Xml.d.ts.map +1 -0
- package/dist/Xml.js +357 -0
- package/dist/Xml.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
- package/src/Cli.ts +61 -0
- package/src/Document.ts +501 -0
- package/src/Xml.ts +420 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +10 -0
package/src/Document.ts
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
export interface Author {
|
|
2
|
+
fullname: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
organization?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Meta {
|
|
8
|
+
title: string;
|
|
9
|
+
docName: string;
|
|
10
|
+
category?: string;
|
|
11
|
+
ipr?: string;
|
|
12
|
+
submissionType?: string;
|
|
13
|
+
consensus?: boolean;
|
|
14
|
+
authors: Author[];
|
|
15
|
+
abstract?: string;
|
|
16
|
+
area?: string;
|
|
17
|
+
workgroup?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Section {
|
|
21
|
+
anchor: string;
|
|
22
|
+
name: string;
|
|
23
|
+
content: string[];
|
|
24
|
+
children: Section[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CustomReference {
|
|
28
|
+
anchor: string;
|
|
29
|
+
originalName: string; // The original name as written in markdown, e.g., "Tempo Payment Method"
|
|
30
|
+
title: string;
|
|
31
|
+
author?: string;
|
|
32
|
+
date?: string;
|
|
33
|
+
target?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Document {
|
|
37
|
+
meta: Meta;
|
|
38
|
+
statusOfMemo?: string;
|
|
39
|
+
copyrightNotice?: string;
|
|
40
|
+
sections: Section[];
|
|
41
|
+
references: {
|
|
42
|
+
normative: string[];
|
|
43
|
+
informative: string[];
|
|
44
|
+
customNormative: CustomReference[];
|
|
45
|
+
customInformative: CustomReference[];
|
|
46
|
+
};
|
|
47
|
+
appendices: Section[];
|
|
48
|
+
acknowledgements?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const BCP14_KEYWORDS = [
|
|
52
|
+
"MUST NOT",
|
|
53
|
+
"SHALL NOT",
|
|
54
|
+
"SHOULD NOT",
|
|
55
|
+
"NOT RECOMMENDED",
|
|
56
|
+
"MUST",
|
|
57
|
+
"REQUIRED",
|
|
58
|
+
"SHALL",
|
|
59
|
+
"SHOULD",
|
|
60
|
+
"RECOMMENDED",
|
|
61
|
+
"MAY",
|
|
62
|
+
"OPTIONAL",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export function fromMarkdown(content: string): Document {
|
|
66
|
+
const { frontmatter, body } = extractFrontmatter(content);
|
|
67
|
+
const lines = body.split("\n");
|
|
68
|
+
|
|
69
|
+
const meta = extractMeta(frontmatter);
|
|
70
|
+
const abstract = extractAbstract(lines);
|
|
71
|
+
const statusOfMemo = extractSection(lines, "Status of This Memo");
|
|
72
|
+
const copyrightNotice = extractSection(lines, "Copyright Notice");
|
|
73
|
+
const sections = extractSections(lines);
|
|
74
|
+
const references = extractReferences(lines);
|
|
75
|
+
const appendices = extractAppendices(lines);
|
|
76
|
+
const acknowledgements = extractSection(lines, "Acknowledgements");
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
meta: { ...meta, abstract },
|
|
80
|
+
statusOfMemo,
|
|
81
|
+
copyrightNotice,
|
|
82
|
+
sections,
|
|
83
|
+
references,
|
|
84
|
+
appendices,
|
|
85
|
+
acknowledgements,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractFrontmatter(content: string): {
|
|
90
|
+
frontmatter: Record<string, unknown>;
|
|
91
|
+
body: string;
|
|
92
|
+
} {
|
|
93
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
94
|
+
if (!match) {
|
|
95
|
+
return { frontmatter: {}, body: content };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const [, yaml, body] = match;
|
|
99
|
+
const frontmatter = parseYaml(yaml);
|
|
100
|
+
return { frontmatter, body };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseYaml(yaml: string): Record<string, unknown> {
|
|
104
|
+
const result: Record<string, unknown> = {};
|
|
105
|
+
const lines = yaml.split("\n");
|
|
106
|
+
let _currentKey: string | null = null;
|
|
107
|
+
let currentArray: unknown[] | null = null;
|
|
108
|
+
let inArray = false;
|
|
109
|
+
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (line.trim() === "") continue;
|
|
112
|
+
|
|
113
|
+
const arrayItemMatch = line.match(/^ {2}- (.+)$/);
|
|
114
|
+
const nestedObjectMatch = line.match(/^ {2}- (\w+):\s*(.*)$/);
|
|
115
|
+
const keyValueMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
116
|
+
|
|
117
|
+
if (nestedObjectMatch && inArray && currentArray) {
|
|
118
|
+
const [, key, value] = nestedObjectMatch;
|
|
119
|
+
const obj: Record<string, string> = { [key]: value };
|
|
120
|
+
currentArray.push(obj);
|
|
121
|
+
} else if (inArray && currentArray && line.match(/^ {4}(\w+):\s*(.*)$/)) {
|
|
122
|
+
const nestedKeyValue = line.match(/^ {4}(\w+):\s*(.*)$/);
|
|
123
|
+
if (nestedKeyValue) {
|
|
124
|
+
const [, key, value] = nestedKeyValue;
|
|
125
|
+
const lastItem = currentArray[currentArray.length - 1] as Record<string, string>;
|
|
126
|
+
if (lastItem && typeof lastItem === "object") {
|
|
127
|
+
lastItem[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (arrayItemMatch && inArray && currentArray) {
|
|
131
|
+
currentArray.push(arrayItemMatch[1]);
|
|
132
|
+
} else if (keyValueMatch) {
|
|
133
|
+
const [, key, value] = keyValueMatch;
|
|
134
|
+
if (value === "") {
|
|
135
|
+
_currentKey = key;
|
|
136
|
+
currentArray = [];
|
|
137
|
+
inArray = true;
|
|
138
|
+
result[key] = currentArray;
|
|
139
|
+
} else {
|
|
140
|
+
inArray = false;
|
|
141
|
+
currentArray = null;
|
|
142
|
+
if (value === "true") {
|
|
143
|
+
result[key] = true;
|
|
144
|
+
} else if (value === "false") {
|
|
145
|
+
result[key] = false;
|
|
146
|
+
} else {
|
|
147
|
+
result[key] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractMeta(frontmatter: Record<string, unknown>): Meta {
|
|
157
|
+
const title = (frontmatter.title as string) || "";
|
|
158
|
+
const docName = (frontmatter.docName as string) || "";
|
|
159
|
+
const category = frontmatter.category as string | undefined;
|
|
160
|
+
const ipr = frontmatter.ipr as string | undefined;
|
|
161
|
+
const submissionType = frontmatter.submissionType as string | undefined;
|
|
162
|
+
const consensus = frontmatter.consensus as boolean | undefined;
|
|
163
|
+
const area = frontmatter.area as string | undefined;
|
|
164
|
+
const workgroup = frontmatter.workgroup as string | undefined;
|
|
165
|
+
|
|
166
|
+
const authors: Author[] = [];
|
|
167
|
+
const rawAuthors = frontmatter.author as Array<Record<string, string>> | undefined;
|
|
168
|
+
if (rawAuthors) {
|
|
169
|
+
for (const author of rawAuthors) {
|
|
170
|
+
authors.push({
|
|
171
|
+
fullname: author.fullname || "",
|
|
172
|
+
email: author.email,
|
|
173
|
+
organization: author.organization,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { title, docName, category, ipr, submissionType, consensus, authors, area, workgroup };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractAbstract(lines: string[]): string | undefined {
|
|
182
|
+
const startIdx = lines.findIndex((l) => /^##\s+Abstract/i.test(l));
|
|
183
|
+
if (startIdx === -1) return undefined;
|
|
184
|
+
|
|
185
|
+
const content: string[] = [];
|
|
186
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
187
|
+
if (lines[i].startsWith("## ") || lines[i] === "---") break;
|
|
188
|
+
content.push(lines[i]);
|
|
189
|
+
}
|
|
190
|
+
return content.join("\n").trim();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function extractSection(lines: string[], sectionName: string): string | undefined {
|
|
194
|
+
const regex = new RegExp(`^##\\s+${sectionName}`, "i");
|
|
195
|
+
const startIdx = lines.findIndex((l) => regex.test(l));
|
|
196
|
+
if (startIdx === -1) return undefined;
|
|
197
|
+
|
|
198
|
+
const content: string[] = [];
|
|
199
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
200
|
+
if (lines[i].startsWith("## ") || lines[i] === "---") break;
|
|
201
|
+
content.push(lines[i]);
|
|
202
|
+
}
|
|
203
|
+
return content.join("\n").trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractSections(lines: string[]): Section[] {
|
|
207
|
+
const sections: Section[] = [];
|
|
208
|
+
let currentSection: Section | null = null;
|
|
209
|
+
let currentSubsection: Section | null = null;
|
|
210
|
+
let currentSubsubsection: Section | null = null;
|
|
211
|
+
let inCodeBlock = false;
|
|
212
|
+
const skipSections = [
|
|
213
|
+
"abstract",
|
|
214
|
+
"status of this memo",
|
|
215
|
+
"copyright notice",
|
|
216
|
+
"table of contents",
|
|
217
|
+
"acknowledgements",
|
|
218
|
+
"authors' addresses",
|
|
219
|
+
"references",
|
|
220
|
+
"appendix",
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < lines.length; i++) {
|
|
224
|
+
const line = lines[i];
|
|
225
|
+
|
|
226
|
+
if (line.startsWith("```")) {
|
|
227
|
+
inCodeBlock = !inCodeBlock;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (inCodeBlock) {
|
|
231
|
+
if (currentSubsubsection) {
|
|
232
|
+
currentSubsubsection.content.push(line);
|
|
233
|
+
} else if (currentSubsection) {
|
|
234
|
+
currentSubsection.content.push(line);
|
|
235
|
+
} else if (currentSection) {
|
|
236
|
+
currentSection.content.push(line);
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const h2Match = line.match(/^##\s+(\d+)\.\s+(.+)/);
|
|
242
|
+
const h3Match = line.match(/^###\s+(\d+)\.(\d+)\.\s+(.+)/);
|
|
243
|
+
const h4Match = line.match(/^####\s+(\d+)\.(\d+)\.(\d+)\.\s+(.+)/);
|
|
244
|
+
const appendixMatch = line.match(/^##\s+Appendix\s+([A-Z])[:.]?\s*(.+)?/i);
|
|
245
|
+
const unnumberedH2 = line.match(/^##\s+(.+)/);
|
|
246
|
+
|
|
247
|
+
if (h4Match) {
|
|
248
|
+
const [, _sec, _sub, _subsub, name] = h4Match;
|
|
249
|
+
currentSubsubsection = {
|
|
250
|
+
anchor: slugify(name.trim()),
|
|
251
|
+
name: name.trim(),
|
|
252
|
+
content: [],
|
|
253
|
+
children: [],
|
|
254
|
+
};
|
|
255
|
+
if (currentSubsection) {
|
|
256
|
+
currentSubsection.children.push(currentSubsubsection);
|
|
257
|
+
}
|
|
258
|
+
} else if (h3Match) {
|
|
259
|
+
const [, _sec, _sub, name] = h3Match;
|
|
260
|
+
currentSubsubsection = null;
|
|
261
|
+
currentSubsection = {
|
|
262
|
+
anchor: slugify(name.trim()),
|
|
263
|
+
name: name.trim(),
|
|
264
|
+
content: [],
|
|
265
|
+
children: [],
|
|
266
|
+
};
|
|
267
|
+
if (currentSection) {
|
|
268
|
+
currentSection.children.push(currentSubsection);
|
|
269
|
+
}
|
|
270
|
+
} else if (h2Match) {
|
|
271
|
+
const [, _num, name] = h2Match;
|
|
272
|
+
const nameLower = name.toLowerCase();
|
|
273
|
+
currentSubsubsection = null;
|
|
274
|
+
currentSubsection = null;
|
|
275
|
+
// Skip sections that are handled separately (references, appendices, etc.)
|
|
276
|
+
if (skipSections.some((s) => nameLower.startsWith(s))) {
|
|
277
|
+
currentSection = null;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
currentSection = {
|
|
281
|
+
anchor: slugify(name.trim()),
|
|
282
|
+
name: name.trim(),
|
|
283
|
+
content: [],
|
|
284
|
+
children: [],
|
|
285
|
+
};
|
|
286
|
+
sections.push(currentSection);
|
|
287
|
+
} else if (appendixMatch || unnumberedH2) {
|
|
288
|
+
if (unnumberedH2) {
|
|
289
|
+
const name = unnumberedH2[1].toLowerCase();
|
|
290
|
+
if (skipSections.some((s) => name.startsWith(s))) {
|
|
291
|
+
currentSection = null;
|
|
292
|
+
currentSubsection = null;
|
|
293
|
+
currentSubsubsection = null;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (name.includes("reference")) {
|
|
297
|
+
currentSection = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else if (line === "---") {
|
|
301
|
+
} else {
|
|
302
|
+
if (currentSubsubsection) {
|
|
303
|
+
currentSubsubsection.content.push(line);
|
|
304
|
+
} else if (currentSubsection) {
|
|
305
|
+
currentSubsection.content.push(line);
|
|
306
|
+
} else if (currentSection) {
|
|
307
|
+
currentSection.content.push(line);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return sections;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function extractReferences(lines: string[]): {
|
|
316
|
+
normative: string[];
|
|
317
|
+
informative: string[];
|
|
318
|
+
customNormative: CustomReference[];
|
|
319
|
+
customInformative: CustomReference[];
|
|
320
|
+
} {
|
|
321
|
+
const normative = new Set<string>();
|
|
322
|
+
const informative = new Set<string>();
|
|
323
|
+
const customNormative: CustomReference[] = [];
|
|
324
|
+
const customInformative: CustomReference[] = [];
|
|
325
|
+
|
|
326
|
+
let currentSection: "normative" | "informative" | null = null;
|
|
327
|
+
|
|
328
|
+
for (let i = 0; i < lines.length; i++) {
|
|
329
|
+
const line = lines[i];
|
|
330
|
+
|
|
331
|
+
if (/^###?\s+(\d+\.\d+\.?\s+)?Normative References/i.test(line)) {
|
|
332
|
+
currentSection = "normative";
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (/^###?\s+(\d+\.\d+\.?\s+)?Informative References/i.test(line)) {
|
|
336
|
+
currentSection = "informative";
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (/^###?\s+(\d+\.|\d+\.\d+\.?)?\s*[A-Z]/.test(line) && !line.includes("References")) {
|
|
340
|
+
currentSection = null;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (currentSection) {
|
|
345
|
+
// Check for RFC references
|
|
346
|
+
const rfcMatches = line.matchAll(/\[RFC(\d+)\]/g);
|
|
347
|
+
for (const match of rfcMatches) {
|
|
348
|
+
if (currentSection === "normative") {
|
|
349
|
+
normative.add(match[1]);
|
|
350
|
+
} else {
|
|
351
|
+
informative.add(match[1]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check for custom references: - **[Anchor]** "Title", <URL>.
|
|
356
|
+
// Or: - **[Anchor]** Author, "Title", <URL>.
|
|
357
|
+
// Or: - **[Anchor]** "Title", Work in Progress.
|
|
358
|
+
const customRefMatch = line.match(
|
|
359
|
+
/^-\s+\*\*\[([^\]]+)\]\*\*\s+(.+)$/,
|
|
360
|
+
);
|
|
361
|
+
if (customRefMatch && !/^\[RFC\d+\]$/.test(`[${customRefMatch[1]}]`)) {
|
|
362
|
+
const anchor = customRefMatch[1];
|
|
363
|
+
const rest = customRefMatch[2];
|
|
364
|
+
|
|
365
|
+
// Collect continuation lines (indented lines that follow)
|
|
366
|
+
let fullText = rest;
|
|
367
|
+
while (i + 1 < lines.length && /^\s{2,}/.test(lines[i + 1]) && !lines[i + 1].match(/^-\s+\*\*/)) {
|
|
368
|
+
i++;
|
|
369
|
+
fullText += " " + lines[i].trim();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Parse the reference text
|
|
373
|
+
// Format 1: "Title", <URL>.
|
|
374
|
+
// Format 2: Author, "Title", <URL>.
|
|
375
|
+
// Format 3: Author, "Title", Work in Progress.
|
|
376
|
+
let title = "";
|
|
377
|
+
let author: string | undefined;
|
|
378
|
+
let target: string | undefined;
|
|
379
|
+
|
|
380
|
+
// Extract URL if present
|
|
381
|
+
const urlMatch = fullText.match(/<([^>]+)>/);
|
|
382
|
+
if (urlMatch) {
|
|
383
|
+
target = urlMatch[1];
|
|
384
|
+
fullText = fullText.replace(/<[^>]+>\.?/, "").trim();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Extract title in quotes
|
|
388
|
+
const titleMatch = fullText.match(/"([^"]+)"/);
|
|
389
|
+
if (titleMatch) {
|
|
390
|
+
title = titleMatch[1];
|
|
391
|
+
// Everything before the title is the author
|
|
392
|
+
const beforeTitle = fullText.substring(0, fullText.indexOf('"')).trim();
|
|
393
|
+
if (beforeTitle && beforeTitle !== ",") {
|
|
394
|
+
author = beforeTitle.replace(/,\s*$/, "").trim();
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// No quoted title, use the whole text as title
|
|
398
|
+
title = fullText.replace(/[.,]\s*$/, "").trim();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const customRef: CustomReference = {
|
|
402
|
+
anchor: slugify(anchor),
|
|
403
|
+
originalName: anchor,
|
|
404
|
+
title,
|
|
405
|
+
};
|
|
406
|
+
if (author) customRef.author = author;
|
|
407
|
+
if (target) customRef.target = target;
|
|
408
|
+
|
|
409
|
+
if (currentSection === "normative") {
|
|
410
|
+
customNormative.push(customRef);
|
|
411
|
+
} else {
|
|
412
|
+
customInformative.push(customRef);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
normative: [...normative].sort((a, b) => parseInt(a, 10) - parseInt(b, 10)),
|
|
420
|
+
informative: [...informative].sort((a, b) => parseInt(a, 10) - parseInt(b, 10)),
|
|
421
|
+
customNormative,
|
|
422
|
+
customInformative,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function slugify(text: string): string {
|
|
427
|
+
return text
|
|
428
|
+
.toLowerCase()
|
|
429
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
430
|
+
.replace(/^-|-$/g, "");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function extractAppendices(lines: string[]): Section[] {
|
|
434
|
+
const appendices: Section[] = [];
|
|
435
|
+
let currentAppendix: Section | null = null;
|
|
436
|
+
let currentSubsection: Section | null = null;
|
|
437
|
+
let inCodeBlock = false;
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < lines.length; i++) {
|
|
440
|
+
const line = lines[i];
|
|
441
|
+
|
|
442
|
+
if (line.startsWith("```")) {
|
|
443
|
+
inCodeBlock = !inCodeBlock;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (inCodeBlock) {
|
|
447
|
+
if (currentSubsection) {
|
|
448
|
+
currentSubsection.content.push(line);
|
|
449
|
+
} else if (currentAppendix) {
|
|
450
|
+
currentAppendix.content.push(line);
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const appendixMatch = line.match(/^##\s+Appendix\s+([A-Z])[:.]?\s*(.+)?/i);
|
|
456
|
+
const subsectionMatch = line.match(/^###\s+([A-Z])\.(\d+)\.?\s+(.+)/i);
|
|
457
|
+
|
|
458
|
+
if (appendixMatch) {
|
|
459
|
+
const [, letter, name] = appendixMatch;
|
|
460
|
+
currentSubsection = null;
|
|
461
|
+
currentAppendix = {
|
|
462
|
+
anchor: slugify(name || `Appendix ${letter}`),
|
|
463
|
+
name: (name || `Appendix ${letter}`).trim(),
|
|
464
|
+
content: [],
|
|
465
|
+
children: [],
|
|
466
|
+
};
|
|
467
|
+
appendices.push(currentAppendix);
|
|
468
|
+
} else if (subsectionMatch && currentAppendix) {
|
|
469
|
+
const [, _letter, _num, name] = subsectionMatch;
|
|
470
|
+
currentSubsection = {
|
|
471
|
+
anchor: slugify(name),
|
|
472
|
+
name: name.trim(),
|
|
473
|
+
content: [],
|
|
474
|
+
children: [],
|
|
475
|
+
};
|
|
476
|
+
currentAppendix.children.push(currentSubsection);
|
|
477
|
+
} else if (line.startsWith("## ") && !line.includes("Appendix")) {
|
|
478
|
+
if (line.includes("Acknowledgements") || line.includes("Authors' Addresses")) {
|
|
479
|
+
currentAppendix = null;
|
|
480
|
+
currentSubsection = null;
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
if (currentSubsection) {
|
|
484
|
+
currentSubsection.content.push(line);
|
|
485
|
+
} else if (currentAppendix) {
|
|
486
|
+
currentAppendix.content.push(line);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return appendices;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function wrapBcp14Keywords(text: string): string {
|
|
495
|
+
let result = text;
|
|
496
|
+
for (const keyword of BCP14_KEYWORDS) {
|
|
497
|
+
const regex = new RegExp(`(?<!<bcp14>)\\b${keyword}\\b(?!</bcp14>)`, "g");
|
|
498
|
+
result = result.replace(regex, `<bcp14>${keyword}</bcp14>`);
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|