UNPKG

64.6 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11Object.defineProperty(exports, "__esModule", { value: true });
12const path = require("path");
13const fs = require("fs");
14const crypto = require("crypto");
15const yaml = require("js-yaml");
16const utils = require("./utils");
17const hljs = require("highlight.js");
18// Builders
19const Import_1 = require("./Import");
20const Clause_1 = require("./Clause");
21const clauseNums_1 = require("./clauseNums");
22const Algorithm_1 = require("./Algorithm");
23const Dfn_1 = require("./Dfn");
24const Example_1 = require("./Example");
25const Figure_1 = require("./Figure");
26const Note_1 = require("./Note");
27const Toc_1 = require("./Toc");
28const Menu_1 = require("./Menu");
29const Production_1 = require("./Production");
30const NonTerminal_1 = require("./NonTerminal");
31const ProdRef_1 = require("./ProdRef");
32const Grammar_1 = require("./Grammar");
33const Xref_1 = require("./Xref");
34const Eqn_1 = require("./Eqn");
35const Biblio_1 = require("./Biblio");
36const autolinker_1 = require("./autolinker");
37const lint_1 = require("./lint/lint");
38const prex_1 = require("prex");
39const DRAFT_DATE_FORMAT = {
40 year: 'numeric',
41 month: 'long',
42 day: 'numeric',
43 timeZone: 'UTC',
44};
45const STANDARD_DATE_FORMAT = {
46 year: 'numeric',
47 month: 'long',
48 timeZone: 'UTC',
49};
50const NO_EMD = new Set(['PRE', 'CODE', 'EMU-PRODUCTION', 'EMU-ALG', 'EMU-GRAMMAR', 'EMU-EQN']);
51const YES_EMD = new Set(['EMU-GMOD']); // these are processed even if they are nested in NO_EMD contexts
52const builders = [
53 Clause_1.default,
54 Algorithm_1.default,
55 Xref_1.default,
56 Dfn_1.default,
57 Eqn_1.default,
58 Grammar_1.default,
59 Production_1.default,
60 Example_1.default,
61 Figure_1.default,
62 NonTerminal_1.default,
63 ProdRef_1.default,
64 Note_1.default,
65];
66const visitorMap = builders.reduce((map, T) => {
67 T.elements.forEach(e => (map[e] = T));
68 return map;
69}, {});
70function wrapWarn(source, spec, warn) {
71 return (e) => {
72 const { message, ruleId } = e;
73 let line;
74 let column;
75 let nodeType;
76 let file = undefined;
77 switch (e.type) {
78 case 'global':
79 line = undefined;
80 column = undefined;
81 nodeType = 'html';
82 break;
83 case 'raw':
84 ({ line, column } = e);
85 nodeType = 'html';
86 if (e.file != null) {
87 file = e.file;
88 source = e.source;
89 }
90 break;
91 case 'node':
92 if (e.node.nodeType === 3 /* Node.TEXT_NODE */) {
93 const loc = spec.locate(e.node);
94 if (loc) {
95 file = loc.file;
96 source = loc.source;
97 ({ line, col: column } = loc);
98 }
99 nodeType = 'text';
100 }
101 else {
102 const loc = spec.locate(e.node);
103 if (loc) {
104 file = loc.file;
105 source = loc.source;
106 ({ line, col: column } = loc.startTag);
107 }
108 nodeType = e.node.tagName.toLowerCase();
109 }
110 break;
111 case 'attr': {
112 const loc = spec.locate(e.node);
113 if (loc) {
114 file = loc.file;
115 source = loc.source;
116 ({ line, column } = utils.attrLocation(source, loc, e.attr));
117 }
118 nodeType = e.node.tagName.toLowerCase();
119 break;
120 }
121 case 'attr-value': {
122 const loc = spec.locate(e.node);
123 if (loc) {
124 file = loc.file;
125 source = loc.source;
126 ({ line, column } = utils.attrValueLocation(source, loc, e.attr));
127 }
128 nodeType = e.node.tagName.toLowerCase();
129 break;
130 }
131 case 'contents': {
132 const { nodeRelativeLine, nodeRelativeColumn } = e;
133 if (e.node.nodeType === 3 /* Node.TEXT_NODE */) {
134 // i.e. a text node, which does not have a tag
135 const loc = spec.locate(e.node);
136 if (loc) {
137 file = loc.file;
138 source = loc.source;
139 line = loc.line + nodeRelativeLine - 1;
140 column = nodeRelativeLine === 1 ? loc.col + nodeRelativeColumn - 1 : nodeRelativeColumn;
141 }
142 nodeType = 'text';
143 }
144 else {
145 const loc = spec.locate(e.node);
146 if (loc) {
147 file = loc.file;
148 source = loc.source;
149 // We have to adjust both for start of tag -> end of tag and end of tag -> passed position
150 // TODO switch to using loc.startTag.end{Line,Col} once parse5 can be upgraded
151 const tagSrc = source.slice(loc.startTag.startOffset, loc.startTag.endOffset);
152 const tagEnd = utils.offsetToLineAndColumn(tagSrc, loc.startTag.endOffset - loc.startOffset);
153 line = loc.startTag.line + tagEnd.line + nodeRelativeLine - 2;
154 if (nodeRelativeLine === 1) {
155 if (tagEnd.line === 1) {
156 column = loc.startTag.col + tagEnd.column + nodeRelativeColumn - 2;
157 }
158 else {
159 column = tagEnd.column + nodeRelativeColumn - 1;
160 }
161 }
162 else {
163 column = nodeRelativeColumn;
164 }
165 }
166 nodeType = e.node.tagName.toLowerCase();
167 }
168 break;
169 }
170 }
171 warn({
172 message,
173 ruleId,
174 // we omit source for global errors so that we don't get a codeframe
175 source: e.type === 'global' ? undefined : source,
176 file,
177 nodeType,
178 line,
179 column,
180 });
181 };
182}
183function isEmuImportElement(node) {
184 return node.nodeType === 1 && node.nodeName === 'EMU-IMPORT';
185}
186function maybeAddClauseToEffectWorklist(effectName, clause, worklist) {
187 if (!worklist.includes(clause) &&
188 clause.canHaveEffect(effectName) &&
189 !clause.effects.includes(effectName)) {
190 clause.effects.push(effectName);
191 worklist.push(clause);
192 }
193}
194/*@internal*/
195class Spec {
196 constructor(rootPath, fetch, dom, opts, sourceText, token = prex_1.CancellationToken.none) {
197 var _a;
198 opts = opts || {};
199 this.spec = this;
200 this.opts = {};
201 this.rootPath = rootPath;
202 this.rootDir = path.dirname(this.rootPath);
203 this.sourceText = sourceText;
204 this.doc = dom.window.document;
205 this.dom = dom;
206 this._fetch = fetch;
207 this.subclauses = [];
208 this.imports = [];
209 this.node = this.doc.body;
210 this.nodeIds = new Set();
211 this.replacementAlgorithmToContainedLabeledStepEntries = new Map();
212 this.labeledStepsToBeRectified = new Set();
213 this.replacementAlgorithms = [];
214 this.cancellationToken = token;
215 this.generatedFiles = new Map();
216 this.log = (_a = opts.log) !== null && _a !== void 0 ? _a : (() => { });
217 this.warn = opts.warn ? wrapWarn(sourceText, this, opts.warn) : () => { };
218 this._figureCounts = {
219 table: 0,
220 figure: 0,
221 };
222 this._xrefs = [];
223 this._ntRefs = [];
224 this._ntStringRefs = [];
225 this._prodRefs = [];
226 this._textNodes = {};
227 this._effectWorklist = new Map();
228 this._effectfulAOs = new Map();
229 this.refsByClause = Object.create(null);
230 this.processMetadata();
231 Object.assign(this.opts, opts);
232 if (this.opts.multipage) {
233 if (this.opts.jsOut || this.opts.cssOut) {
234 throw new Error('Cannot use --multipage with --js-out or --css-out');
235 }
236 if (this.opts.outfile == null) {
237 this.opts.outfile = '';
238 }
239 if (this.opts.assets !== 'none') {
240 this.opts.jsOut = path.join(this.opts.outfile, 'ecmarkup.js');
241 this.opts.cssOut = path.join(this.opts.outfile, 'ecmarkup.css');
242 }
243 }
244 if (typeof this.opts.status === 'undefined') {
245 this.opts.status = 'proposal';
246 }
247 if (typeof this.opts.toc === 'undefined') {
248 this.opts.toc = true;
249 }
250 if (typeof this.opts.copyright === 'undefined') {
251 this.opts.copyright = true;
252 }
253 if (typeof this.opts.ecma262Biblio === 'undefined') {
254 this.opts.ecma262Biblio = true;
255 }
256 if (!this.opts.date) {
257 this.opts.date = new Date();
258 }
259 if (this.opts.stage != undefined) {
260 this.opts.stage = String(this.opts.stage);
261 }
262 if (!this.opts.location) {
263 this.opts.location = '<no location>';
264 }
265 this.namespace = this.opts.location;
266 this.biblio = new Biblio_1.default(this.opts.location);
267 }
268 fetch(file) {
269 return this._fetch(file, this.cancellationToken);
270 }
271 build() {
272 var _a;
273 return __awaiter(this, void 0, void 0, function* () {
274 /*
275 The Ecmarkup build process proceeds as follows:
276
277 1. Load biblios, making xrefs and auto-linking from external specs work
278 2. Load imports by recursively inlining the import files' content into the emu-import element
279 3. Generate boilerplate text
280 4. Do a walk of the DOM visting elements and text nodes. Text nodes are replaced by text and HTML nodes depending
281 on content. Elements are built by delegating to builders. Builders work by modifying the DOM ahead of them so
282 the new elements they make are visited during the walk. Elements added behind the current iteration must be
283 handled specially (eg. see static exit method of clause). Xref, nt, and prodref's are collected for linking
284 in the next step.
285 5. Linking. After the DOM walk we have a complete picture of all the symbols in the document so we proceed to link
286 the various xrefs to the proper place.
287 6. Adding charset, highlighting code, etc.
288 7. Add CSS & JS dependencies.
289 */
290 this.log('Loading biblios...');
291 if (this.opts.ecma262Biblio) {
292 yield this.loadECMA262Biblio();
293 }
294 yield this.loadBiblios();
295 this.log('Loading imports...');
296 yield this.loadImports();
297 this.log('Building boilerplate...');
298 this.buildBoilerplate();
299 const context = {
300 spec: this,
301 node: this.doc.body,
302 importStack: [],
303 clauseStack: [],
304 tagStack: [],
305 clauseNumberer: clauseNums_1.default(),
306 inNoAutolink: false,
307 inAlg: false,
308 inNoEmd: false,
309 followingEmd: null,
310 currentId: null,
311 };
312 const document = this.doc;
313 if (this.opts.lintSpec) {
314 this.log('Linting...');
315 const source = this.sourceText;
316 if (source === undefined) {
317 throw new Error('Cannot lint when source text is not available');
318 }
319 yield lint_1.lint(this.warn, source, this, document);
320 }
321 this.log('Walking document, building various elements...');
322 const walker = document.createTreeWalker(document.body, 1 | 4 /* elements and text nodes */);
323 yield walk(walker, context);
324 const sdoJs = this.generateSDOMap();
325 this.setReplacementAlgorithmOffsets();
326 this.autolink();
327 this.log('Propagating effects annotations...');
328 this.propagateEffects();
329 this.log('Linking xrefs...');
330 this._xrefs.forEach(xref => xref.build());
331 this.log('Linking non-terminal references...');
332 this._ntRefs.forEach(nt => nt.build());
333 if (this.opts.lintSpec) {
334 this._ntStringRefs.forEach(({ name, loc, node, namespace }) => {
335 if (this.biblio.byProductionName(name, namespace) == null) {
336 this.warn({
337 type: 'contents',
338 ruleId: 'undefined-nonterminal',
339 message: `could not find a definition for nonterminal ${name}`,
340 node,
341 nodeRelativeLine: loc.line,
342 nodeRelativeColumn: loc.column,
343 });
344 }
345 });
346 }
347 this.log('Linking production references...');
348 this._prodRefs.forEach(prod => prod.build());
349 this.log('Building reference graph...');
350 this.buildReferenceGraph();
351 this.highlightCode();
352 this.setCharset();
353 const wrapper = this.buildSpecWrapper();
354 let tocEles = [];
355 let tocJs = '';
356 if (this.opts.toc) {
357 this.log('Building table of contents...');
358 if (this.opts.oldToc) {
359 new Toc_1.default(this).build();
360 }
361 else {
362 ({ js: tocJs, eles: tocEles } = Menu_1.default(this));
363 }
364 }
365 for (const ele of tocEles) {
366 this.doc.body.insertBefore(ele, this.doc.body.firstChild);
367 }
368 const jsContents = (yield concatJs(sdoJs, tocJs)) + `\n;let usesMultipage = ${!!this.opts.multipage}`;
369 const jsSha = sha(jsContents);
370 if (this.opts.multipage) {
371 yield this.buildMultipage(wrapper, tocEles, jsSha);
372 }
373 yield this.buildAssets(jsContents, jsSha);
374 const file = this.opts.multipage
375 ? path.join(this.opts.outfile, 'index.html')
376 : (_a = this.opts.outfile) !== null && _a !== void 0 ? _a : null;
377 this.generatedFiles.set(file, this.toHTML());
378 return this;
379 });
380 }
381 toHTML() {
382 const htmlEle = this.doc.documentElement;
383 return '<!doctype html>\n' + (htmlEle.hasAttributes() ? htmlEle.outerHTML : htmlEle.innerHTML);
384 }
385 locate(node) {
386 let pointer = node;
387 while (pointer != null) {
388 if (isEmuImportElement(pointer)) {
389 break;
390 }
391 pointer = pointer.parentElement;
392 }
393 if (pointer == null) {
394 const loc = this.dom.nodeLocation(node);
395 if (loc) {
396 // we can't just spread `loc` because not all properties are own/enumerable
397 return {
398 source: this.sourceText,
399 startTag: loc.startTag,
400 endTag: loc.endTag,
401 startOffset: loc.startOffset,
402 endOffset: loc.endOffset,
403 attrs: loc.attrs,
404 line: loc.line,
405 col: loc.col,
406 };
407 }
408 }
409 else if (pointer.dom) {
410 const loc = pointer.dom.nodeLocation(node);
411 if (loc) {
412 return {
413 file: pointer.importPath,
414 source: pointer.source,
415 startTag: loc.startTag,
416 endTag: loc.endTag,
417 startOffset: loc.startOffset,
418 endOffset: loc.endOffset,
419 attrs: loc.attrs,
420 line: loc.line,
421 col: loc.col,
422 };
423 }
424 }
425 }
426 buildReferenceGraph() {
427 const refToClause = this.refsByClause;
428 const setParent = (node) => {
429 let pointer = node;
430 while (pointer && !['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'].includes(pointer.nodeName)) {
431 pointer = pointer.parentNode;
432 }
433 // @ts-ignore
434 if (pointer == null || pointer.id == null) {
435 // @ts-ignore
436 pointer = { id: 'sec-intro' };
437 }
438 // @ts-ignore
439 if (refToClause[pointer.id] == null) {
440 // @ts-ignore
441 refToClause[pointer.id] = [];
442 }
443 // @ts-ignore
444 refToClause[pointer.id].push(node.id);
445 };
446 let counter = 0;
447 this._xrefs.forEach(xref => {
448 let entry = xref.entry;
449 if (!entry || entry.namespace === 'global')
450 return;
451 if (!entry.id && entry.refId) {
452 entry = this.spec.biblio.byId(entry.refId);
453 }
454 if (!xref.id) {
455 const id = `_ref_${counter++}`;
456 xref.node.setAttribute('id', id);
457 xref.id = id;
458 }
459 setParent(xref.node);
460 entry.referencingIds.push(xref.id);
461 });
462 this._ntRefs.forEach(prod => {
463 const entry = prod.entry;
464 if (!entry || entry.namespace === 'global')
465 return;
466 // if this is the defining nt of an emu-production, don't create a ref
467 if (prod.node.parentNode.nodeName === 'EMU-PRODUCTION')
468 return;
469 const id = `_ref_${counter++}`;
470 prod.node.setAttribute('id', id);
471 setParent(prod.node);
472 entry.referencingIds.push(id);
473 });
474 }
475 checkValidSectionId(ele) {
476 if (!ele.id.startsWith('sec-')) {
477 this.warn({
478 type: 'node',
479 ruleId: 'top-level-section-id',
480 message: 'When using --multipage, top-level sections must have ids beginning with `sec-`',
481 node: ele,
482 });
483 return false;
484 }
485 if (!/^[A-Za-z0-9-_]+$/.test(ele.id)) {
486 this.warn({
487 type: 'node',
488 ruleId: 'top-level-section-id',
489 message: 'When using --multipage, top-level sections must have ids matching /^[A-Za-z0-9-_]+$/',
490 node: ele,
491 });
492 return false;
493 }
494 if (ele.id.toLowerCase() === 'sec-index') {
495 this.warn({
496 type: 'node',
497 ruleId: 'top-level-section-id',
498 message: 'When using --multipage, top-level sections must not be named "index"',
499 node: ele,
500 });
501 return false;
502 }
503 return true;
504 }
505 propagateEffects() {
506 for (const [effectName, worklist] of this._effectWorklist) {
507 this.propagateEffect(effectName, worklist);
508 }
509 }
510 propagateEffect(effectName, worklist) {
511 const usersOfAoid = new Map();
512 for (const xref of this._xrefs) {
513 if (xref.clause == null || xref.aoid == null)
514 continue;
515 if (!xref.canHaveEffect(effectName))
516 continue;
517 if (xref.hasAddedEffect(effectName)) {
518 maybeAddClauseToEffectWorklist(effectName, xref.clause, worklist);
519 }
520 const usedAoid = xref.aoid;
521 if (!usersOfAoid.has(usedAoid)) {
522 usersOfAoid.set(usedAoid, new Set());
523 }
524 usersOfAoid.get(usedAoid).add(xref.clause);
525 }
526 while (worklist.length !== 0) {
527 const clause = worklist.shift();
528 const aoid = clause.aoid;
529 if (aoid == null || !usersOfAoid.has(aoid)) {
530 continue;
531 }
532 this._effectfulAOs.set(aoid, clause);
533 for (const userClause of usersOfAoid.get(aoid)) {
534 maybeAddClauseToEffectWorklist(effectName, userClause, worklist);
535 }
536 }
537 }
538 getEffectsByAoid(aoid) {
539 if (this._effectfulAOs.has(aoid)) {
540 return this._effectfulAOs.get(aoid).effects;
541 }
542 return null;
543 }
544 buildMultipage(wrapper, tocEles, jsSha) {
545 return __awaiter(this, void 0, void 0, function* () {
546 let stillIntro = true;
547 const introEles = [];
548 const sections = [];
549 const containedIdToSection = new Map();
550 const sectionToContainedIds = new Map();
551 const clauseTypes = ['EMU-ANNEX', 'EMU-CLAUSE'];
552 // @ts-ignore
553 for (const child of wrapper.children) {
554 if (stillIntro) {
555 if (clauseTypes.includes(child.nodeName)) {
556 throw new Error('cannot make multipage build without intro');
557 }
558 else if (child.nodeName === 'EMU-INTRO') {
559 stillIntro = false;
560 if (child.id == null) {
561 this.warn({
562 type: 'node',
563 ruleId: 'top-level-section-id',
564 message: 'When using --multipage, top-level sections must have ids',
565 node: child,
566 });
567 continue;
568 }
569 if (child.id !== 'sec-intro') {
570 this.warn({
571 type: 'node',
572 ruleId: 'top-level-section-id',
573 message: 'When using --multipage, the introduction must have id "sec-intro"',
574 node: child,
575 });
576 continue;
577 }
578 const name = 'index';
579 introEles.push(child);
580 sections.push({ name, eles: introEles });
581 const contained = [];
582 sectionToContainedIds.set(name, contained);
583 for (const item of introEles) {
584 if (item.id) {
585 contained.push(item.id);
586 containedIdToSection.set(item.id, name);
587 }
588 }
589 // @ts-ignore
590 for (const item of [...introEles].flatMap(e => [...e.querySelectorAll('[id]')])) {
591 contained.push(item.id);
592 containedIdToSection.set(item.id, name);
593 }
594 }
595 else {
596 introEles.push(child);
597 }
598 }
599 else {
600 if (!clauseTypes.includes(child.nodeName)) {
601 throw new Error('non-clause children are not yet implemented: ' + child.nodeName);
602 }
603 if (child.id == null) {
604 this.warn({
605 type: 'node',
606 ruleId: 'top-level-section-id',
607 message: 'When using --multipage, top-level sections must have ids',
608 node: child,
609 });
610 continue;
611 }
612 if (!this.checkValidSectionId(child)) {
613 continue;
614 }
615 const name = child.id.substring(4);
616 const contained = [];
617 sectionToContainedIds.set(name, contained);
618 contained.push(child.id);
619 containedIdToSection.set(child.id, name);
620 for (const item of child.querySelectorAll('[id]')) {
621 contained.push(item.id);
622 containedIdToSection.set(item.id, name);
623 }
624 sections.push({ name, eles: [child] });
625 }
626 }
627 let htmlEle = '';
628 if (this.doc.documentElement.hasAttributes()) {
629 const clonedHtmlEle = this.doc.documentElement.cloneNode(false);
630 clonedHtmlEle.innerHTML = '';
631 const src = clonedHtmlEle.outerHTML;
632 htmlEle = src.substring(0, src.length - '<head></head><body></body></html>'.length);
633 }
634 const head = this.doc.head.cloneNode(true);
635 this.addStyle(head, 'ecmarkup.css'); // omit `../` because we rewrite `<link>` elements below
636 this.addStyle(head, `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${hljs.versionString}/styles/base16/solarized-light.min.css`);
637 const script = this.doc.createElement('script');
638 script.src = '../ecmarkup.js?cache=' + jsSha;
639 script.setAttribute('defer', '');
640 head.appendChild(script);
641 const containedMap = JSON.stringify(Object.fromEntries(sectionToContainedIds)).replace(/[\\`$]/g, '\\$&');
642 const multipageJsContents = `'use strict';
643let multipageMap = JSON.parse(\`${containedMap}\`);
644${yield utils.readFile(path.join(__dirname, '../js/multipage.js'))}
645`;
646 if (this.opts.assets !== 'none') {
647 this.generatedFiles.set(path.join(this.opts.outfile, 'multipage/multipage.js'), multipageJsContents);
648 }
649 const multipageScript = this.doc.createElement('script');
650 multipageScript.src = 'multipage.js?cache=' + sha(multipageJsContents);
651 multipageScript.setAttribute('defer', '');
652 head.appendChild(multipageScript);
653 for (const { name, eles } of sections) {
654 this.log(`Generating section ${name}...`);
655 const headClone = head.cloneNode(true);
656 const tocClone = tocEles.map(e => e.cloneNode(true));
657 const clones = eles.map(e => e.cloneNode(true));
658 const allClones = [headClone, ...tocClone, ...clones];
659 // @ts-ignore
660 const links = allClones.flatMap(e => [...e.querySelectorAll('a,link')]);
661 for (const link of links) {
662 if (linkIsAbsolute(link)) {
663 continue;
664 }
665 if (linkIsInternal(link)) {
666 let p = link.hash.substring(1);
667 if (!containedIdToSection.has(p)) {
668 try {
669 p = decodeURIComponent(p);
670 }
671 catch (_a) {
672 // pass
673 }
674 if (!containedIdToSection.has(p)) {
675 this.warn({
676 type: 'node',
677 ruleId: 'multipage-link-target',
678 message: 'could not find appropriate section for ' + link.hash,
679 node: link,
680 });
681 continue;
682 }
683 }
684 const targetSec = containedIdToSection.get(p);
685 link.href = (targetSec === 'index' ? './' : targetSec + '.html') + link.hash;
686 }
687 else if (linkIsPathRelative(link)) {
688 link.href = '../' + pathFromRelativeLink(link);
689 }
690 }
691 // @ts-ignore
692 for (const img of allClones.flatMap(e => [...e.querySelectorAll('img')])) {
693 if (!/^(http:|https:|:|\/)/.test(img.src)) {
694 img.src = '../' + img.src;
695 }
696 }
697 // prettier-ignore
698 // @ts-ignore
699 for (const object of allClones.flatMap(e => [...e.querySelectorAll('object[data]')])) {
700 if (!/^(http:|https:|:|\/)/.test(object.data)) {
701 object.data = '../' + object.data;
702 }
703 }
704 if (eles[0].hasAttribute('id')) {
705 const canonical = this.doc.createElement('link');
706 canonical.setAttribute('rel', 'canonical');
707 canonical.setAttribute('href', `../#${eles[0].id}`);
708 headClone.appendChild(canonical);
709 }
710 // @ts-ignore
711 const tocHTML = tocClone.map(e => e.outerHTML).join('\n');
712 // @ts-ignore
713 const clonesHTML = clones.map(e => e.outerHTML).join('\n');
714 const content = `<!doctype html>${htmlEle}\n${headClone.outerHTML}\n<body>${tocHTML}<div id='spec-container'>${clonesHTML}</div></body>`;
715 this.generatedFiles.set(path.join(this.opts.outfile, `multipage/${name}.html`), content);
716 }
717 });
718 }
719 buildAssets(jsContents, jsSha) {
720 return __awaiter(this, void 0, void 0, function* () {
721 const cssContents = yield utils.readFile(path.join(__dirname, '../css/elements.css'));
722 if (this.opts.jsOut) {
723 this.generatedFiles.set(this.opts.jsOut, jsContents);
724 }
725 if (this.opts.cssOut) {
726 this.generatedFiles.set(this.opts.cssOut, cssContents);
727 }
728 if (this.opts.assets === 'none')
729 return;
730 const outDir = this.opts.outfile
731 ? this.opts.multipage
732 ? this.opts.outfile
733 : path.dirname(this.opts.outfile)
734 : process.cwd();
735 if (this.opts.jsOut) {
736 let skipJs = false;
737 const scripts = this.doc.querySelectorAll('script');
738 for (let i = 0; i < scripts.length; i++) {
739 const script = scripts[i];
740 const src = script.getAttribute('src');
741 if (src && path.normalize(path.join(outDir, src)) === path.normalize(this.opts.jsOut)) {
742 this.log(`Found existing js link to ${src}, skipping inlining...`);
743 skipJs = true;
744 }
745 }
746 if (!skipJs) {
747 const script = this.doc.createElement('script');
748 script.src = path.relative(outDir, this.opts.jsOut) + '?cache=' + jsSha;
749 script.setAttribute('defer', '');
750 this.doc.head.appendChild(script);
751 }
752 }
753 else {
754 this.log('Inlining JavaScript assets...');
755 const script = this.doc.createElement('script');
756 script.textContent = jsContents;
757 this.doc.head.appendChild(script);
758 }
759 if (this.opts.cssOut) {
760 let skipCss = false;
761 const links = this.doc.querySelectorAll('link[rel=stylesheet]');
762 for (let i = 0; i < links.length; i++) {
763 const link = links[i];
764 const href = link.getAttribute('href');
765 if (href && path.normalize(path.join(outDir, href)) === path.normalize(this.opts.cssOut)) {
766 this.log(`Found existing css link to ${href}, skipping inlining...`);
767 skipCss = true;
768 }
769 }
770 if (!skipCss) {
771 this.addStyle(this.doc.head, path.relative(outDir, this.opts.cssOut));
772 }
773 }
774 else {
775 this.log('Inlining CSS assets...');
776 const style = this.doc.createElement('style');
777 style.textContent = cssContents;
778 this.doc.head.appendChild(style);
779 }
780 this.addStyle(this.doc.head, `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${hljs.versionString}/styles/base16/solarized-light.min.css`);
781 });
782 }
783 addStyle(head, href) {
784 const style = this.doc.createElement('link');
785 style.setAttribute('rel', 'stylesheet');
786 style.setAttribute('href', href);
787 // insert early so that the document's own stylesheets can override
788 const firstLink = head.querySelector('link[rel=stylesheet], style');
789 if (firstLink != null) {
790 head.insertBefore(style, firstLink);
791 }
792 else {
793 head.appendChild(style);
794 }
795 }
796 buildSpecWrapper() {
797 const elements = this.doc.body.childNodes;
798 const wrapper = this.doc.createElement('div');
799 wrapper.id = 'spec-container';
800 while (elements.length > 0) {
801 wrapper.appendChild(elements[0]);
802 }
803 this.doc.body.appendChild(wrapper);
804 return wrapper;
805 }
806 processMetadata() {
807 const block = this.doc.querySelector('pre.metadata');
808 if (!block || !block.parentNode) {
809 return;
810 }
811 let data;
812 try {
813 data = yaml.safeLoad(block.textContent);
814 }
815 catch (e) {
816 if (typeof (e === null || e === void 0 ? void 0 : e.mark.line) === 'number' && typeof (e === null || e === void 0 ? void 0 : e.mark.column) === 'number') {
817 this.warn({
818 type: 'contents',
819 ruleId: 'invalid-metadata',
820 message: `metadata block failed to parse: ${e.reason}`,
821 node: block,
822 nodeRelativeLine: e.mark.line + 1,
823 nodeRelativeColumn: e.mark.column + 1,
824 });
825 }
826 else {
827 this.warn({
828 type: 'node',
829 ruleId: 'invalid-metadata',
830 message: 'metadata block failed to parse',
831 node: block,
832 });
833 }
834 return;
835 }
836 finally {
837 block.parentNode.removeChild(block);
838 }
839 Object.assign(this.opts, data);
840 }
841 loadECMA262Biblio() {
842 return __awaiter(this, void 0, void 0, function* () {
843 this.cancellationToken.throwIfCancellationRequested();
844 yield this.loadBiblio(path.join(__dirname, '../ecma262biblio.json'));
845 });
846 }
847 loadBiblios() {
848 return __awaiter(this, void 0, void 0, function* () {
849 this.cancellationToken.throwIfCancellationRequested();
850 yield Promise.all(Array.from(this.doc.querySelectorAll('emu-biblio'), biblio => this.loadBiblio(path.join(this.rootDir, biblio.getAttribute('href')))));
851 });
852 }
853 loadBiblio(file) {
854 return __awaiter(this, void 0, void 0, function* () {
855 const contents = yield this.fetch(file);
856 this.biblio.addExternalBiblio(JSON.parse(contents));
857 });
858 }
859 loadImports() {
860 return __awaiter(this, void 0, void 0, function* () {
861 yield loadImports(this, this.spec.doc.body, this.rootDir);
862 });
863 }
864 exportBiblio() {
865 if (!this.opts.location) {
866 this.warn({
867 type: 'global',
868 ruleId: 'no-location',
869 message: "no spec location specified; biblio not generated. try --location or setting the location in the document's metadata block",
870 });
871 return {};
872 }
873 const biblio = {};
874 biblio[this.opts.location] = this.biblio.toJSON();
875 return biblio;
876 }
877 highlightCode() {
878 this.log('Highlighting syntax...');
879 const codes = this.doc.querySelectorAll('pre code');
880 for (let i = 0; i < codes.length; i++) {
881 const classAttr = codes[i].getAttribute('class');
882 if (!classAttr)
883 continue;
884 const language = classAttr.replace(/lang(uage)?-/, '');
885 let input = codes[i].textContent;
886 // remove leading and trailing blank lines
887 input = input.replace(/^(\s*[\r\n])+|([\r\n]\s*)+$/g, '');
888 // remove base inden based on indent of first non-blank line
889 const baseIndent = input.match(/^\s*/) || '';
890 const baseIndentRe = new RegExp('^' + baseIndent, 'gm');
891 input = input.replace(baseIndentRe, '');
892 // @ts-expect-error the type definitions for highlight.js are broken
893 const result = hljs.highlight(input, { language });
894 codes[i].innerHTML = result.value;
895 codes[i].setAttribute('class', classAttr + ' hljs');
896 }
897 }
898 buildBoilerplate() {
899 this.cancellationToken.throwIfCancellationRequested();
900 const status = this.opts.status;
901 const version = this.opts.version;
902 const title = this.opts.title;
903 const shortname = this.opts.shortname;
904 const location = this.opts.location;
905 const stage = this.opts.stage;
906 if (this.opts.copyright) {
907 if (status !== 'draft' && status !== 'standard' && !this.opts.contributors) {
908 this.warn({
909 type: 'global',
910 ruleId: 'no-contributors',
911 message: 'contributors not specified, skipping copyright boilerplate. specify contributors in your frontmatter metadata',
912 });
913 }
914 else {
915 this.buildCopyrightBoilerplate();
916 }
917 }
918 // no title boilerplate generated if title not specified
919 if (!title)
920 return;
921 // title
922 if (title && !this._updateBySelector('title', title)) {
923 const titleElem = this.doc.createElement('title');
924 titleElem.innerHTML = title;
925 this.doc.head.appendChild(titleElem);
926 const h1 = this.doc.createElement('h1');
927 h1.setAttribute('class', 'title');
928 h1.innerHTML = title;
929 this.doc.body.insertBefore(h1, this.doc.body.firstChild);
930 }
931 // version string, ala 6th Edition July 2016 or Draft 10 / September 26, 2015
932 let versionText = '';
933 let omitShortname = false;
934 if (version) {
935 versionText += version + ' / ';
936 }
937 else if (status === 'proposal' && stage) {
938 versionText += 'Stage ' + stage + ' Draft / ';
939 }
940 else if (shortname && status === 'draft') {
941 versionText += 'Draft ' + shortname + ' / ';
942 omitShortname = true;
943 }
944 else {
945 return;
946 }
947 const defaultDateFormat = status === 'standard' ? STANDARD_DATE_FORMAT : DRAFT_DATE_FORMAT;
948 const date = new Intl.DateTimeFormat('en-US', defaultDateFormat).format(this.opts.date);
949 versionText += date;
950 if (!this._updateBySelector('h1.version', versionText)) {
951 const h1 = this.doc.createElement('h1');
952 h1.setAttribute('class', 'version');
953 h1.innerHTML = versionText;
954 this.doc.body.insertBefore(h1, this.doc.body.firstChild);
955 }
956 // shortname and status, ala 'Draft ECMA-262
957 if (shortname && !omitShortname) {
958 // for proposals, link shortname to location
959 const shortnameLinkHtml = status === 'proposal' && location ? `<a href="${location}">${shortname}</a>` : shortname;
960 const shortnameHtml = status.charAt(0).toUpperCase() + status.slice(1) + ' ' + shortnameLinkHtml;
961 if (!this._updateBySelector('h1.shortname', shortnameHtml)) {
962 const h1 = this.doc.createElement('h1');
963 h1.setAttribute('class', 'shortname');
964 h1.innerHTML = shortnameHtml;
965 this.doc.body.insertBefore(h1, this.doc.body.firstChild);
966 }
967 }
968 }
969 buildCopyrightBoilerplate() {
970 let addressFile;
971 let copyrightFile;
972 let licenseFile;
973 if (this.opts.boilerplate) {
974 if (this.opts.boilerplate.address) {
975 addressFile = path.join(process.cwd(), this.opts.boilerplate.address);
976 }
977 if (this.opts.boilerplate.copyright) {
978 copyrightFile = path.join(process.cwd(), this.opts.boilerplate.copyright);
979 }
980 if (this.opts.boilerplate.license) {
981 licenseFile = path.join(process.cwd(), this.opts.boilerplate.license);
982 }
983 }
984 // Get content from files
985 let address = getBoilerplate(addressFile || 'address');
986 let copyright = getBoilerplate(copyrightFile || `${this.opts.status}-copyright`);
987 const license = getBoilerplate(licenseFile || 'software-license');
988 if (this.opts.status === 'proposal') {
989 address = '';
990 }
991 // Operate on content
992 copyright = copyright.replace(/!YEAR!/g, '' + this.opts.date.getFullYear());
993 if (this.opts.contributors) {
994 copyright = copyright.replace(/!CONTRIBUTORS!/g, this.opts.contributors);
995 }
996 let copyrightClause = this.doc.querySelector('.copyright-and-software-license');
997 if (!copyrightClause) {
998 let last;
999 utils.domWalkBackward(this.doc.body, node => {
1000 if (last)
1001 return false;
1002 if (node.nodeName === 'EMU-CLAUSE' || node.nodeName === 'EMU-ANNEX') {
1003 last = node;
1004 return false;
1005 }
1006 });
1007 copyrightClause = this.doc.createElement('emu-annex');
1008 copyrightClause.setAttribute('id', 'sec-copyright-and-software-license');
1009 if (last && last.parentNode) {
1010 last.parentNode.insertBefore(copyrightClause, last.nextSibling);
1011 }
1012 else {
1013 this.doc.body.appendChild(copyrightClause);
1014 }
1015 }
1016 copyrightClause.innerHTML = `
1017 <h1>Copyright &amp; Software License</h1>
1018 ${address}
1019 <h2>Copyright Notice</h2>
1020 ${copyright.replace('!YEAR!', '' + this.opts.date.getFullYear())}
1021 <h2>Software License</h2>
1022 ${license}
1023 `;
1024 }
1025 generateSDOMap() {
1026 var _a;
1027 const sdoMap = Object.create(null);
1028 this.log('Building SDO map...');
1029 const mainGrammar = new Set(this.doc.querySelectorAll('emu-grammar[type=definition]:not([example])'));
1030 // we can't just do `:not(emu-annex emu-grammar)` because that selector is too complicated for this version of jsdom
1031 for (const annexEle of this.doc.querySelectorAll('emu-annex emu-grammar[type=definition]')) {
1032 mainGrammar.delete(annexEle);
1033 }
1034 const productions = new Map();
1035 for (const grammar of mainGrammar) {
1036 for (const production of grammar.querySelectorAll('emu-production')) {
1037 if (!production.hasAttribute('name')) {
1038 // I don't think this is possible, but we can at least be graceful about it
1039 this.warn({
1040 type: 'node',
1041 // All of these elements are synthetic, and hence lack locations, so we point error messages to the containing emu-grammar
1042 node: grammar,
1043 ruleId: 'grammar-shape',
1044 message: 'expected emu-production node to have name',
1045 });
1046 continue;
1047 }
1048 const name = production.getAttribute('name');
1049 if (productions.has(name)) {
1050 this.warn({
1051 type: 'node',
1052 node: grammar,
1053 ruleId: 'grammar-shape',
1054 message: `found duplicate definition for production ${name}`,
1055 });
1056 continue;
1057 }
1058 const rhses = [...production.querySelectorAll('emu-rhs')];
1059 productions.set(name, rhses);
1060 }
1061 }
1062 const sdos = this.doc.querySelectorAll('emu-clause[type=sdo],emu-clause[type="syntax-directed operation"]');
1063 outer: for (const sdo of sdos) {
1064 let header;
1065 for (const child of sdo.children) {
1066 if (child.tagName === 'SPAN' && child.childNodes.length === 0) {
1067 // an `oldid` marker, presumably
1068 continue;
1069 }
1070 if (child.tagName === 'H1') {
1071 header = child;
1072 break;
1073 }
1074 this.warn({
1075 type: 'node',
1076 node: child,
1077 ruleId: 'sdo-name',
1078 message: 'expected H1 as first child of syntax-directed operation',
1079 });
1080 continue outer;
1081 }
1082 if (!header) {
1083 continue;
1084 }
1085 const clause = header.firstElementChild.textContent;
1086 const nameMatch = (_a = header.textContent) === null || _a === void 0 ? void 0 : _a.slice(clause.length + 1).match(/^(?:(?:Static|Runtime) Semantics: )?\s*(\w+)\b/);
1087 if (nameMatch == null) {
1088 this.warn({
1089 type: 'contents',
1090 node: header,
1091 ruleId: 'sdo-name',
1092 message: 'could not parse name of syntax-directed operation',
1093 nodeRelativeLine: 1,
1094 nodeRelativeColumn: 1,
1095 });
1096 continue;
1097 }
1098 const sdoName = nameMatch[1];
1099 for (const grammar of sdo.children) {
1100 if (grammar.tagName !== 'EMU-GRAMMAR') {
1101 continue;
1102 }
1103 for (const production of grammar.querySelectorAll('emu-production')) {
1104 if (!production.hasAttribute('name')) {
1105 // I don't think this is possible, but we can at least be graceful about it
1106 this.warn({
1107 type: 'node',
1108 node: grammar,
1109 ruleId: 'grammar-shape',
1110 message: 'expected emu-production node to have name',
1111 });
1112 continue;
1113 }
1114 const name = production.getAttribute('name');
1115 if (!productions.has(name)) {
1116 this.warn({
1117 type: 'node',
1118 node: grammar,
1119 ruleId: 'grammar-shape',
1120 message: `could not find definition corresponding to production ${name}`,
1121 });
1122 continue;
1123 }
1124 const mainRhses = productions.get(name);
1125 for (const rhs of production.querySelectorAll('emu-rhs')) {
1126 const matches = mainRhses.filter(r => rhsMatches(rhs, r));
1127 if (matches.length === 0) {
1128 this.warn({
1129 type: 'node',
1130 node: grammar,
1131 ruleId: 'grammar-shape',
1132 message: `could not find definition for rhs ${rhs.textContent}`,
1133 });
1134 continue;
1135 }
1136 if (matches.length > 1) {
1137 this.warn({
1138 type: 'node',
1139 node: grammar,
1140 ruleId: 'grammar-shape',
1141 message: `found multiple definitions for rhs ${rhs.textContent}`,
1142 });
1143 continue;
1144 }
1145 const match = matches[0];
1146 if (match.id == '') {
1147 match.id = 'prod-' + sha(`${name} : ${match.textContent}`);
1148 }
1149 const mainId = match.id;
1150 if (rhs.id == '') {
1151 rhs.id = 'prod-' + sha(`[${sdoName}] ${name} ${rhs.textContent}`);
1152 }
1153 if (!{}.hasOwnProperty.call(sdoMap, mainId)) {
1154 sdoMap[mainId] = Object.create(null);
1155 }
1156 const sdosForThisId = sdoMap[mainId];
1157 if (!{}.hasOwnProperty.call(sdosForThisId, sdoName)) {
1158 sdosForThisId[sdoName] = { clause, ids: [] };
1159 }
1160 else if (sdosForThisId[sdoName].clause !== clause) {
1161 this.warn({
1162 type: 'node',
1163 node: grammar,
1164 ruleId: 'grammar-shape',
1165 message: `SDO ${sdoName} found in multiple clauses`,
1166 });
1167 }
1168 sdosForThisId[sdoName].ids.push(rhs.id);
1169 }
1170 }
1171 }
1172 }
1173 const json = JSON.stringify(sdoMap);
1174 return `let sdoMap = JSON.parse(\`${json.replace(/[\\`$]/g, '\\$&')}\`);`;
1175 }
1176 setReplacementAlgorithmOffsets() {
1177 this.log('Finding offsets for replacement algorithm steps...');
1178 const pending = new Map();
1179 const setReplacementAlgorithmStart = (element, stepNumbers) => {
1180 const rootList = element.firstElementChild;
1181 rootList.start = stepNumbers[stepNumbers.length - 1];
1182 if (stepNumbers.length > 1) {
1183 // Note the off-by-one here: a length of 1 indicates we are replacing a top-level step, which means we do not need to adjust the styling.
1184 if (stepNumbers.length === 2) {
1185 rootList.classList.add('nested-once');
1186 }
1187 else if (stepNumbers.length === 3) {
1188 rootList.classList.add('nested-twice');
1189 }
1190 else if (stepNumbers.length === 4) {
1191 rootList.classList.add('nested-thrice');
1192 }
1193 else if (stepNumbers.length === 5) {
1194 rootList.classList.add('nested-four-times');
1195 }
1196 else {
1197 // At the sixth level and deeper (so once we have nested five times) we use a consistent line numbering style, so no further cases are necessary
1198 rootList.classList.add('nested-lots');
1199 }
1200 }
1201 // Fix up the biblio entries for any labeled steps in the algorithm
1202 for (const entry of this.replacementAlgorithmToContainedLabeledStepEntries.get(element)) {
1203 entry.stepNumbers = [...stepNumbers, ...entry.stepNumbers.slice(1)];
1204 this.labeledStepsToBeRectified.delete(entry.id);
1205 // Now that we've figured out where the step is, we can deal with algorithms replacing it
1206 if (pending.has(entry.id)) {
1207 const todo = pending.get(entry.id);
1208 pending.delete(entry.id);
1209 for (const replacementAlgorithm of todo) {
1210 setReplacementAlgorithmStart(replacementAlgorithm, entry.stepNumbers);
1211 }
1212 }
1213 }
1214 };
1215 for (const { element, target } of this.replacementAlgorithms) {
1216 if (this.labeledStepsToBeRectified.has(target)) {
1217 if (!pending.has(target)) {
1218 pending.set(target, []);
1219 }
1220 pending.get(target).push(element);
1221 }
1222 else {
1223 // When the target is not itself within a replacement, or is within a replacement which we have already rectified, we can just use its step number directly
1224 const targetEntry = this.biblio.byId(target);
1225 if (targetEntry == null) {
1226 this.warn({
1227 type: 'attr-value',
1228 attr: 'replaces-step',
1229 ruleId: 'invalid-replacement',
1230 message: `could not find step ${JSON.stringify(target)}`,
1231 node: element,
1232 });
1233 }
1234 else if (targetEntry.type !== 'step') {
1235 this.warn({
1236 type: 'attr-value',
1237 attr: 'replaces-step',
1238 ruleId: 'invalid-replacement',
1239 message: `expected algorithm to replace a step, not a ${targetEntry.type}`,
1240 node: element,
1241 });
1242 }
1243 else {
1244 setReplacementAlgorithmStart(element, targetEntry.stepNumbers);
1245 }
1246 }
1247 }
1248 if (pending.size > 0) {
1249 // todo consider line/column missing cases
1250 this.warn({
1251 type: 'global',
1252 ruleId: 'invalid-replacement',
1253 message: 'could not unambiguously determine replacement algorithm offsets - do you have a cycle in your replacement algorithms?',
1254 });
1255 }
1256 }
1257 autolink() {
1258 this.log('Autolinking terms and abstract ops...');
1259 const namespaces = Object.keys(this._textNodes);
1260 for (let i = 0; i < namespaces.length; i++) {
1261 const namespace = namespaces[i];
1262 const { replacer, autolinkmap } = autolinker_1.replacerForNamespace(namespace, this.biblio);
1263 const nodes = this._textNodes[namespace];
1264 for (let j = 0; j < nodes.length; j++) {
1265 const { node, clause, inAlg, currentId } = nodes[j];
1266 autolinker_1.autolink(node, replacer, autolinkmap, clause, currentId, inAlg);
1267 }
1268 }
1269 }
1270 setCharset() {
1271 let current = this.spec.doc.querySelector('meta[charset]');
1272 if (!current) {
1273 current = this.spec.doc.createElement('meta');
1274 this.spec.doc.head.insertBefore(current, this.spec.doc.head.firstChild);
1275 }
1276 current.setAttribute('charset', 'utf-8');
1277 }
1278 _updateBySelector(selector, contents) {
1279 const elem = this.doc.querySelector(selector);
1280 if (elem && elem.textContent.trim().length > 0) {
1281 return true;
1282 }
1283 if (elem) {
1284 elem.innerHTML = contents;
1285 return true;
1286 }
1287 return false;
1288 }
1289}
1290exports.default = Spec;
1291function getBoilerplate(file) {
1292 let boilerplateFile = file;
1293 try {
1294 if (fs.lstatSync(file).isFile()) {
1295 boilerplateFile = file;
1296 }
1297 }
1298 catch (error) {
1299 boilerplateFile = path.join(__dirname, '../boilerplate', `${file}.html`);
1300 }
1301 return fs.readFileSync(boilerplateFile, 'utf8');
1302}
1303function loadImports(spec, rootElement, rootPath) {
1304 return __awaiter(this, void 0, void 0, function* () {
1305 const imports = rootElement.querySelectorAll('EMU-IMPORT');
1306 for (let i = 0; i < imports.length; i++) {
1307 const node = imports[i];
1308 const imp = yield Import_1.default.build(spec, node, rootPath);
1309 yield loadImports(spec, node, imp.relativeRoot);
1310 }
1311 });
1312}
1313function walk(walker, context) {
1314 return __awaiter(this, void 0, void 0, function* () {
1315 const previousInNoAutolink = context.inNoAutolink;
1316 let previousInNoEmd = context.inNoEmd;
1317 const { spec } = context;
1318 context.node = walker.currentNode;
1319 context.tagStack.push(context.node);
1320 if (context.node === context.followingEmd) {
1321 context.followingEmd = null;
1322 context.inNoEmd = false;
1323 // inNoEmd is true because we're walking past the output of emd, rather than by being within a NO_EMD context
1324 // so reset previousInNoEmd so we exit it properly
1325 previousInNoEmd = false;
1326 }
1327 if (context.node.nodeType === 3) {
1328 // walked to a text node
1329 if (context.node.textContent.trim().length === 0)
1330 return; // skip empty nodes; nothing to do!
1331 const clause = context.clauseStack[context.clauseStack.length - 1] || context.spec;
1332 const namespace = clause ? clause.namespace : context.spec.namespace;
1333 if (!context.inNoEmd) {
1334 // new nodes as a result of emd processing should be skipped
1335 context.inNoEmd = true;
1336 let node = context.node;
1337 while (node && !node.nextSibling) {
1338 node = node.parentNode;
1339 }
1340 if (node) {
1341 // inNoEmd will be set to false when we walk to this node
1342 context.followingEmd = node.nextSibling;
1343 }
1344 // else, there's no more nodes to process and inNoEmd does not need to be tracked
1345 utils.emdTextNode(context.spec, context.node, namespace);
1346 }
1347 if (!context.inNoAutolink) {
1348 // stuff the text nodes into an array for auto-linking with later
1349 // (since we can't autolink at this point without knowing the biblio).
1350 context.spec._textNodes[namespace] = context.spec._textNodes[namespace] || [];
1351 context.spec._textNodes[namespace].push({
1352 node: context.node,
1353 clause,
1354 inAlg: context.inAlg,
1355 currentId: context.currentId,
1356 });
1357 }
1358 return;
1359 }
1360 // context.node is an HTMLElement (node type 1)
1361 // handle oldids
1362 const oldids = context.node.getAttribute('oldids');
1363 if (oldids) {
1364 if (!context.node.childNodes) {
1365 throw new Error('oldids found on unsupported element: ' + context.node.nodeName);
1366 }
1367 oldids
1368 .split(/,/g)
1369 .map(s => s.trim())
1370 .forEach(oid => {
1371 const s = spec.doc.createElement('span');
1372 s.setAttribute('id', oid);
1373 context.node.insertBefore(s, context.node.childNodes[0]);
1374 });
1375 }
1376 const parentId = context.currentId;
1377 if (context.node.hasAttribute('id')) {
1378 context.currentId = context.node.getAttribute('id');
1379 }
1380 if (autolinker_1.NO_CLAUSE_AUTOLINK.has(context.node.nodeName)) {
1381 context.inNoAutolink = true;
1382 }
1383 else if (autolinker_1.YES_CLAUSE_AUTOLINK.has(context.node.nodeName)) {
1384 context.inNoAutolink = false;
1385 }
1386 if (NO_EMD.has(context.node.nodeName)) {
1387 context.inNoEmd = true;
1388 }
1389 else if (YES_EMD.has(context.node.nodeName)) {
1390 context.inNoEmd = false;
1391 }
1392 const visitor = visitorMap[context.node.nodeName];
1393 if (visitor) {
1394 yield visitor.enter(context);
1395 }
1396 const firstChild = walker.firstChild();
1397 if (firstChild) {
1398 while (true) {
1399 yield walk(walker, context);
1400 const next = walker.nextSibling();
1401 if (!next)
1402 break;
1403 }
1404 walker.parentNode();
1405 context.node = walker.currentNode;
1406 }
1407 if (visitor)
1408 visitor.exit(context);
1409 context.inNoAutolink = previousInNoAutolink;
1410 context.inNoEmd = previousInNoEmd;
1411 context.currentId = parentId;
1412 context.tagStack.pop();
1413 });
1414}
1415const jsDependencies = ['sdoMap.js', 'menu.js', 'listNumbers.js'];
1416function concatJs(...extras) {
1417 return __awaiter(this, void 0, void 0, function* () {
1418 let dependencies = yield Promise.all(jsDependencies.map(dependency => utils.readFile(path.join(__dirname, '../js/' + dependency))));
1419 dependencies = dependencies.concat(extras);
1420 return dependencies.join('\n');
1421 });
1422}
1423// todo move this to utils maybe
1424// this is very similar to the identically-named method in lint/utils.ts, but operates on HTML elements rather than grammarkdown nodes
1425function rhsMatches(aRhs, bRhs) {
1426 const a = aRhs.firstElementChild;
1427 const b = bRhs.firstElementChild;
1428 if (a == null || b == null) {
1429 throw new Error('RHS must have content');
1430 }
1431 return symbolSpanMatches(a, b);
1432}
1433function symbolSpanMatches(a, b) {
1434 if (a == null || a.textContent === '[empty]') {
1435 return canBeEmpty(b);
1436 }
1437 if (b != null && symbolMatches(a, b)) {
1438 return symbolSpanMatches(a.nextElementSibling, b.nextElementSibling);
1439 }
1440 // sometimes when there is an optional terminal or nonterminal we give distinct implementations for each case, rather than one implementation which represents both
1441 // which means both `a b c` and `a c` must match `a b? c`
1442 if (b != null && canSkipSymbol(b)) {
1443 return symbolSpanMatches(a, b.nextElementSibling);
1444 }
1445 return false;
1446}
1447function canBeEmpty(b) {
1448 return b == null || (canSkipSymbol(b) && canBeEmpty(b.nextElementSibling));
1449}
1450function canSkipSymbol(a) {
1451 if (a.tagName === 'EMU-NT' && a.hasAttribute('optional')) {
1452 return true;
1453 }
1454 // 'gmod' is prose assertions
1455 // 'gann' is [empty], [nlth], and lookahead restrictions]
1456 return ['EMU-CONSTRAINTS', 'EMU-GMOD', 'EMU-GANN'].includes(a.tagName);
1457}
1458function symbolMatches(a, b) {
1459 const aKind = a.tagName.toLowerCase();
1460 const bKind = b.tagName.toLowerCase();
1461 if (aKind !== bKind) {
1462 return false;
1463 }
1464 switch (aKind) {
1465 case 'emu-t': {
1466 return a.textContent === b.textContent;
1467 }
1468 case 'emu-nt': {
1469 if (a.childNodes.length > 1 || b.childNodes.length > 1) {
1470 // NonTerminal.build() adds elements to represent parameters and "opt", but that happens after this phase
1471 throw new Error('emu-nt nodes should not have other contents at this phase');
1472 }
1473 return a.textContent === b.textContent;
1474 }
1475 case 'emu-gmod':
1476 case 'emu-gann': {
1477 return a.textContent === b.textContent;
1478 }
1479 default: {
1480 throw new Error('unimplemented: symbol matches ' + aKind);
1481 }
1482 }
1483}
1484function sha(str) {
1485 return crypto
1486 .createHash('sha256')
1487 .update(str)
1488 .digest('base64')
1489 .slice(0, 8)
1490 .replace(/\+/g, '-')
1491 .replace(/\//g, '_');
1492}
1493// jsdom does not handle the `.hostname` (etc) parts correctly, so we have to look at the href directly
1494// it also (some?) relative links as links to about:blank, for the purposes of testing the href
1495function linkIsAbsolute(link) {
1496 return !link.href.startsWith('about:blank') && /^[a-z]+:/.test(link.href);
1497}
1498function linkIsInternal(link) {
1499 return link.href.startsWith('#') || link.href.startsWith('about:blank#');
1500}
1501function linkIsPathRelative(link) {
1502 return !link.href.startsWith('/') && !link.href.startsWith('about:blank/');
1503}
1504function pathFromRelativeLink(link) {
1505 return link.href.startsWith('about:blank') ? link.href.substring(11) : link.href;
1506}