1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.maybeAddClauseToEffectWorklist = void 0;
|
4 | const path = require("path");
|
5 | const fs = require("fs");
|
6 | const crypto = require("crypto");
|
7 | const yaml = require("js-yaml");
|
8 | const utils = require("./utils");
|
9 | const hljs = require("highlight.js");
|
10 |
|
11 | const Import_1 = require("./Import");
|
12 | const Clause_1 = require("./Clause");
|
13 | const clauseNums_1 = require("./clauseNums");
|
14 | const Algorithm_1 = require("./Algorithm");
|
15 | const Dfn_1 = require("./Dfn");
|
16 | const Example_1 = require("./Example");
|
17 | const Figure_1 = require("./Figure");
|
18 | const Note_1 = require("./Note");
|
19 | const Toc_1 = require("./Toc");
|
20 | const Menu_1 = require("./Menu");
|
21 | const Production_1 = require("./Production");
|
22 | const NonTerminal_1 = require("./NonTerminal");
|
23 | const ProdRef_1 = require("./ProdRef");
|
24 | const Grammar_1 = require("./Grammar");
|
25 | const Xref_1 = require("./Xref");
|
26 | const Eqn_1 = require("./Eqn");
|
27 | const Biblio_1 = require("./Biblio");
|
28 | const Meta_1 = require("./Meta");
|
29 | const H1_1 = require("./H1");
|
30 | const autolinker_1 = require("./autolinker");
|
31 | const lint_1 = require("./lint/lint");
|
32 | const prex_1 = require("prex");
|
33 | const utils_1 = require("./lint/utils");
|
34 | const utils_2 = require("./utils");
|
35 | const expr_parser_1 = require("./expr-parser");
|
36 | const DRAFT_DATE_FORMAT = {
|
37 | year: 'numeric',
|
38 | month: 'long',
|
39 | day: 'numeric',
|
40 | timeZone: 'UTC',
|
41 | };
|
42 | const STANDARD_DATE_FORMAT = {
|
43 | year: 'numeric',
|
44 | month: 'long',
|
45 | timeZone: 'UTC',
|
46 | };
|
47 | const NO_EMD = new Set(['PRE', 'CODE', 'EMU-PRODUCTION', 'EMU-ALG', 'EMU-GRAMMAR', 'EMU-EQN']);
|
48 | const YES_EMD = new Set(['EMU-GMOD']);
|
49 | const builders = [
|
50 | Clause_1.default,
|
51 | Algorithm_1.default,
|
52 | Xref_1.default,
|
53 | Dfn_1.default,
|
54 | Eqn_1.default,
|
55 | Grammar_1.default,
|
56 | Production_1.default,
|
57 | Example_1.default,
|
58 | Figure_1.default,
|
59 | NonTerminal_1.default,
|
60 | ProdRef_1.default,
|
61 | Note_1.default,
|
62 | Meta_1.default,
|
63 | H1_1.default,
|
64 | ];
|
65 | const visitorMap = builders.reduce((map, T) => {
|
66 | T.elements.forEach(e => (map[e] = T));
|
67 | return map;
|
68 | }, {});
|
69 | function wrapWarn(source, spec, warn) {
|
70 | return (e) => {
|
71 | const { message, ruleId } = e;
|
72 | let line;
|
73 | let column;
|
74 | let nodeType;
|
75 | let file = undefined;
|
76 | switch (e.type) {
|
77 | case 'global':
|
78 | line = undefined;
|
79 | column = undefined;
|
80 | nodeType = 'html';
|
81 | break;
|
82 | case 'raw':
|
83 | ({ line, column } = e);
|
84 | nodeType = 'html';
|
85 | if (e.file != null) {
|
86 | file = e.file;
|
87 | source = e.source;
|
88 | }
|
89 | break;
|
90 | case 'node':
|
91 | if (e.node.nodeType === 3 ) {
|
92 | const loc = spec.locate(e.node);
|
93 | if (loc) {
|
94 | file = loc.file;
|
95 | source = loc.source;
|
96 | ({ startLine: line, startCol: column } = loc);
|
97 | }
|
98 | nodeType = 'text';
|
99 | }
|
100 | else {
|
101 | const loc = spec.locate(e.node);
|
102 | if (loc) {
|
103 | file = loc.file;
|
104 | source = loc.source;
|
105 | ({ startLine: line, startCol: column } = loc.startTag);
|
106 | }
|
107 | nodeType = e.node.tagName.toLowerCase();
|
108 | }
|
109 | break;
|
110 | case 'attr': {
|
111 | const loc = spec.locate(e.node);
|
112 | if (loc) {
|
113 | file = loc.file;
|
114 | source = loc.source;
|
115 | ({ line, column } = utils.attrLocation(source, loc, e.attr));
|
116 | }
|
117 | nodeType = e.node.tagName.toLowerCase();
|
118 | break;
|
119 | }
|
120 | case 'attr-value': {
|
121 | const loc = spec.locate(e.node);
|
122 | if (loc) {
|
123 | file = loc.file;
|
124 | source = loc.source;
|
125 | ({ line, column } = utils.attrValueLocation(source, loc, e.attr));
|
126 | }
|
127 | nodeType = e.node.tagName.toLowerCase();
|
128 | break;
|
129 | }
|
130 | case 'contents': {
|
131 | const { nodeRelativeLine, nodeRelativeColumn } = e;
|
132 | if (e.node.nodeType === 3 ) {
|
133 |
|
134 | const loc = spec.locate(e.node);
|
135 | if (loc) {
|
136 | file = loc.file;
|
137 | source = loc.source;
|
138 | line = loc.startLine + nodeRelativeLine - 1;
|
139 | column =
|
140 | nodeRelativeLine === 1 ? loc.startCol + 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 | line = loc.startTag.endLine + nodeRelativeLine - 1;
|
150 | if (nodeRelativeLine === 1) {
|
151 | column = loc.startTag.endCol + nodeRelativeColumn - 1;
|
152 | }
|
153 | else {
|
154 | column = nodeRelativeColumn;
|
155 | }
|
156 | }
|
157 | nodeType = e.node.tagName.toLowerCase();
|
158 | }
|
159 | break;
|
160 | }
|
161 | }
|
162 | warn({
|
163 | message,
|
164 | ruleId,
|
165 |
|
166 | source: e.type === 'global' ? undefined : source,
|
167 | file,
|
168 | nodeType,
|
169 | line,
|
170 | column,
|
171 | });
|
172 | };
|
173 | }
|
174 | function isEmuImportElement(node) {
|
175 | return node.nodeType === 1 && node.nodeName === 'EMU-IMPORT';
|
176 | }
|
177 | function maybeAddClauseToEffectWorklist(effectName, clause, worklist) {
|
178 | if (!worklist.some(i => i.aoid === clause.aoid) &&
|
179 | clause.canHaveEffect(effectName) &&
|
180 | !clause.effects.includes(effectName)) {
|
181 | clause.effects.push(effectName);
|
182 | worklist.push(clause);
|
183 | }
|
184 | }
|
185 | exports.maybeAddClauseToEffectWorklist = maybeAddClauseToEffectWorklist;
|
186 |
|
187 | class Spec {
|
188 | constructor(rootPath, fetch, dom, opts, sourceText, token = prex_1.CancellationToken.none) {
|
189 | var _a;
|
190 | opts = opts || {};
|
191 | this.spec = this;
|
192 | this.opts = {};
|
193 | this.rootPath = rootPath;
|
194 | this.rootDir = path.dirname(this.rootPath);
|
195 | this.sourceText = sourceText;
|
196 | this.doc = dom.window.document;
|
197 | this.dom = dom;
|
198 | this._fetch = fetch;
|
199 | this.subclauses = [];
|
200 | this.imports = [];
|
201 | this.node = this.doc.body;
|
202 | this.nodeIds = new Set();
|
203 | this.replacementAlgorithmToContainedLabeledStepEntries = new Map();
|
204 | this.labeledStepsToBeRectified = new Set();
|
205 | this.replacementAlgorithms = [];
|
206 | this.cancellationToken = token;
|
207 | this.generatedFiles = new Map();
|
208 | this.log = (_a = opts.log) !== null && _a !== void 0 ? _a : (() => { });
|
209 | this.warn = opts.warn ? wrapWarn(sourceText, this, opts.warn) : () => { };
|
210 | this._figureCounts = {
|
211 | table: 0,
|
212 | figure: 0,
|
213 | };
|
214 | this._xrefs = [];
|
215 | this._ntRefs = [];
|
216 | this._ntStringRefs = [];
|
217 | this._prodRefs = [];
|
218 | this._textNodes = {};
|
219 | this._effectWorklist = new Map();
|
220 | this._effectfulAOs = new Map();
|
221 | this._emuMetasToRender = new Set();
|
222 | this._emuMetasToRemove = new Set();
|
223 | this.refsByClause = Object.create(null);
|
224 | this.processMetadata();
|
225 | Object.assign(this.opts, opts);
|
226 | if (this.opts.multipage) {
|
227 | if (this.opts.jsOut || this.opts.cssOut) {
|
228 | throw new Error('Cannot use --multipage with --js-out or --css-out');
|
229 | }
|
230 | if (this.opts.outfile == null) {
|
231 | this.opts.outfile = '';
|
232 | }
|
233 | if (this.opts.assets !== 'none') {
|
234 | this.opts.jsOut = path.join(this.opts.outfile, 'ecmarkup.js');
|
235 | this.opts.cssOut = path.join(this.opts.outfile, 'ecmarkup.css');
|
236 | }
|
237 | }
|
238 | if (typeof this.opts.status === 'undefined') {
|
239 | this.opts.status = 'proposal';
|
240 | }
|
241 | if (typeof this.opts.toc === 'undefined') {
|
242 | this.opts.toc = true;
|
243 | }
|
244 | if (typeof this.opts.copyright === 'undefined') {
|
245 | this.opts.copyright = true;
|
246 | }
|
247 | if (!this.opts.date) {
|
248 | this.opts.date = new Date();
|
249 | }
|
250 | if (this.opts.stage != undefined) {
|
251 | this.opts.stage = String(this.opts.stage);
|
252 | }
|
253 | if (!this.opts.location) {
|
254 | this.opts.location = '<no location>';
|
255 | }
|
256 | this.namespace = this.opts.location;
|
257 | this.biblio = new Biblio_1.default(this.opts.location);
|
258 | }
|
259 | fetch(file) {
|
260 | return this._fetch(file, this.cancellationToken);
|
261 | }
|
262 | async build() {
|
263 | |
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | var _a;
|
280 | this.log('Loading biblios...');
|
281 | await this.loadBiblios();
|
282 | this.log('Loading imports...');
|
283 | await this.loadImports();
|
284 | this.log('Building boilerplate...');
|
285 | this.buildBoilerplate();
|
286 | const context = {
|
287 | spec: this,
|
288 | node: this.doc.body,
|
289 | importStack: [],
|
290 | clauseStack: [],
|
291 | tagStack: [],
|
292 | clauseNumberer: (0, clauseNums_1.default)(this),
|
293 | inNoAutolink: false,
|
294 | inAlg: false,
|
295 | inNoEmd: false,
|
296 | followingEmd: null,
|
297 | currentId: null,
|
298 | };
|
299 | const document = this.doc;
|
300 | if (this.opts.lintSpec) {
|
301 | this.log('Linting...');
|
302 | const source = this.sourceText;
|
303 | if (source === undefined) {
|
304 | throw new Error('Cannot lint when source text is not available');
|
305 | }
|
306 | await (0, lint_1.lint)(this.warn, source, this, document);
|
307 | }
|
308 | this.log('Walking document, building various elements...');
|
309 | const walker = document.createTreeWalker(document.body, 1 | 4 );
|
310 | await walk(walker, context);
|
311 | const sdoJs = this.generateSDOMap();
|
312 | this.setReplacementAlgorithmOffsets();
|
313 | this.autolink();
|
314 | if (this.opts.lintSpec) {
|
315 | this.log('Checking types...');
|
316 | this.typecheck();
|
317 | }
|
318 | this.log('Propagating effect annotations...');
|
319 | this.propagateEffects();
|
320 | this.log('Linking xrefs...');
|
321 | this._xrefs.forEach(xref => xref.build());
|
322 | this.log('Linking non-terminal references...');
|
323 | this._ntRefs.forEach(nt => nt.build());
|
324 | this._emuMetasToRender.forEach(node => {
|
325 | Meta_1.default.render(this, node);
|
326 | });
|
327 | this._emuMetasToRemove.forEach(node => {
|
328 | node.replaceWith(...node.childNodes);
|
329 | });
|
330 |
|
331 |
|
332 |
|
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 commonEles = [];
|
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: commonEles } = (0, Menu_1.default)(this));
|
363 | }
|
364 | }
|
365 | this.log('Building shortcuts help dialog...');
|
366 | commonEles.push(this.buildShortcutsHelp());
|
367 | for (const ele of commonEles) {
|
368 | this.doc.body.insertBefore(ele, this.doc.body.firstChild);
|
369 | }
|
370 | const jsContents = (await concatJs(sdoJs, tocJs)) + `\n;let usesMultipage = ${!!this.opts.multipage}`;
|
371 | const jsSha = sha(jsContents);
|
372 | if (this.opts.multipage) {
|
373 | await this.buildMultipage(wrapper, commonEles, jsSha);
|
374 | }
|
375 | await this.buildAssets(jsContents, jsSha);
|
376 | const file = this.opts.multipage
|
377 | ? path.join(this.opts.outfile, 'index.html')
|
378 | : (_a = this.opts.outfile) !== null && _a !== void 0 ? _a : null;
|
379 | this.generatedFiles.set(file, this.toHTML());
|
380 | return this;
|
381 | }
|
382 | labelClauses() {
|
383 | const label = (clause) => {
|
384 | var _a, _b;
|
385 | if (clause.header != null) {
|
386 | if (((_b = (_a = clause.signature) === null || _a === void 0 ? void 0 : _a.return) === null || _b === void 0 ? void 0 : _b.kind) === 'completion' &&
|
387 | clause.signature.return.completionType !== 'normal') {
|
388 |
|
389 |
|
390 | clause.header.innerHTML += `<span class="clause-tag abrupt-tag" title="this can return an abrupt completion">abrupt</span>`;
|
391 | }
|
392 |
|
393 |
|
394 |
|
395 | if (clause.node.querySelector('.e-user-code')) {
|
396 | clause.header.innerHTML += `<span class="clause-tag user-code-tag" title="this can invoke user code">user code</span>`;
|
397 | }
|
398 | }
|
399 | for (const sub of clause.subclauses) {
|
400 | label(sub);
|
401 | }
|
402 | };
|
403 | for (const sub of this.subclauses) {
|
404 | label(sub);
|
405 | }
|
406 | }
|
407 |
|
408 |
|
409 | typecheck() {
|
410 | const isUnused = (t) => {
|
411 | var _a;
|
412 | return t.kind === 'unused' ||
|
413 | (t.kind === 'completion' &&
|
414 | (t.completionType === 'abrupt' || ((_a = t.typeOfValueIfNormal) === null || _a === void 0 ? void 0 : _a.kind) === 'unused'));
|
415 | };
|
416 | const AOs = this.biblio
|
417 | .localEntries()
|
418 | .filter(e => { var _a; return e.type === 'op' && ((_a = e.signature) === null || _a === void 0 ? void 0 : _a.return) != null; });
|
419 | const onlyPerformed = new Map(AOs.filter(e => !isUnused(e.signature.return)).map(a => [a.aoid, null]));
|
420 | const alwaysAssertedToBeNormal = new Map(AOs.filter(e => e.signature.return.kind === 'completion').map(a => [a.aoid, null]));
|
421 |
|
422 | const opNames = this.biblio.getOpNames(this.namespace);
|
423 |
|
424 | for (const node of this.doc.querySelectorAll('emu-alg')) {
|
425 | if (node.hasAttribute('example') || !('ecmarkdownTree' in node)) {
|
426 | continue;
|
427 | }
|
428 |
|
429 | const tree = node.ecmarkdownTree;
|
430 | if (tree == null) {
|
431 | continue;
|
432 | }
|
433 |
|
434 | const originalHtml = node.originalHtml;
|
435 | const expressionVisitor = (expr, path) => {
|
436 | if (expr.type !== 'call' && expr.type !== 'sdo-call') {
|
437 | return;
|
438 | }
|
439 | const { callee, arguments: args } = expr;
|
440 | if (!(callee.parts.length === 1 && callee.parts[0].name === 'text')) {
|
441 | return;
|
442 | }
|
443 | const calleeName = callee.parts[0].contents;
|
444 | const warn = (message) => {
|
445 | const { line, column } = (0, utils_2.offsetToLineAndColumn)(originalHtml, callee.parts[0].location.start.offset);
|
446 | this.warn({
|
447 | type: 'contents',
|
448 | ruleId: 'typecheck',
|
449 | message,
|
450 | node,
|
451 | nodeRelativeLine: line,
|
452 | nodeRelativeColumn: column,
|
453 | });
|
454 | };
|
455 | const biblioEntry = this.biblio.byAoid(calleeName);
|
456 | if (biblioEntry == null) {
|
457 | if (![
|
458 | 'thisTimeValue',
|
459 | 'thisStringValue',
|
460 | 'thisBigIntValue',
|
461 | 'thisNumberValue',
|
462 | 'thisSymbolValue',
|
463 | 'thisBooleanValue',
|
464 | 'toUppercase',
|
465 | 'toLowercase',
|
466 | ].includes(calleeName)) {
|
467 |
|
468 | warn(`could not find definition for ${calleeName}`);
|
469 | }
|
470 | return;
|
471 | }
|
472 | if (biblioEntry.kind === 'syntax-directed operation' && expr.type === 'call') {
|
473 | warn(`${calleeName} is a syntax-directed operation and should not be invoked like a regular call`);
|
474 | }
|
475 | else if (biblioEntry.kind != null &&
|
476 | biblioEntry.kind !== 'syntax-directed operation' &&
|
477 | expr.type === 'sdo-call') {
|
478 | warn(`${calleeName} is not a syntax-directed operation but here is being invoked as one`);
|
479 | }
|
480 | if (biblioEntry.signature == null) {
|
481 | return;
|
482 | }
|
483 | const min = biblioEntry.signature.parameters.length;
|
484 | const max = min + biblioEntry.signature.optionalParameters.length;
|
485 | if (args.length < min || args.length > max) {
|
486 | const count = `${min}${min === max ? '' : `-${max}`}`;
|
487 |
|
488 | const message = `${calleeName} takes ${count} argument${count === '1' ? '' : 's'}, but this invocation passes ${args.length}`;
|
489 | warn(message);
|
490 | }
|
491 | const { return: returnType } = biblioEntry.signature;
|
492 | if (returnType == null) {
|
493 | return;
|
494 | }
|
495 | const consumedAsCompletion = isConsumedAsCompletion(expr, path);
|
496 |
|
497 |
|
498 | const isCompletion = returnType.kind === 'completion' ||
|
499 | (returnType.kind === 'union' && returnType.types[0].kind === 'completion');
|
500 | if (['Completion', 'ThrowCompletion', 'NormalCompletion'].includes(calleeName)) {
|
501 | if (consumedAsCompletion) {
|
502 | warn(`${calleeName} clearly creates a Completion Record; it does not need to be marked as such, and it would not be useful to immediately unwrap its result`);
|
503 | }
|
504 | }
|
505 | else if (isCompletion && !consumedAsCompletion) {
|
506 | warn(`${calleeName} returns a Completion Record, but is not consumed as if it does`);
|
507 | }
|
508 | else if (!isCompletion && consumedAsCompletion) {
|
509 | warn(`${calleeName} does not return a Completion Record, but is consumed as if it does`);
|
510 | }
|
511 | if (returnType.kind === 'unused' && !isCalledAsPerform(expr, path, false)) {
|
512 | warn(`${calleeName} does not return a meaningful value and should only be invoked as \`Perform ${calleeName}(...).\``);
|
513 | }
|
514 | if (onlyPerformed.has(calleeName) && onlyPerformed.get(calleeName) !== 'top') {
|
515 | const old = onlyPerformed.get(calleeName);
|
516 | const performed = isCalledAsPerform(expr, path, true);
|
517 | if (!performed) {
|
518 | onlyPerformed.set(calleeName, 'top');
|
519 | }
|
520 | else if (old === null) {
|
521 | onlyPerformed.set(calleeName, 'only performed');
|
522 | }
|
523 | }
|
524 | if (alwaysAssertedToBeNormal.has(calleeName) &&
|
525 | alwaysAssertedToBeNormal.get(calleeName) !== 'top') {
|
526 | const old = alwaysAssertedToBeNormal.get(calleeName);
|
527 | const asserted = isAssertedToBeNormal(expr, path);
|
528 | if (!asserted) {
|
529 | alwaysAssertedToBeNormal.set(calleeName, 'top');
|
530 | }
|
531 | else if (old === null) {
|
532 | alwaysAssertedToBeNormal.set(calleeName, 'always asserted normal');
|
533 | }
|
534 | }
|
535 | };
|
536 | const walkLines = (list) => {
|
537 | var _a;
|
538 | for (const line of list.contents) {
|
539 | const item = (0, expr_parser_1.parse)(line.contents, opNames);
|
540 | if (item.type === 'failure') {
|
541 | const { line, column } = (0, utils_2.offsetToLineAndColumn)(originalHtml, item.offset);
|
542 | this.warn({
|
543 | type: 'contents',
|
544 | ruleId: 'expression-parsing',
|
545 | message: item.message,
|
546 | node,
|
547 | nodeRelativeLine: line,
|
548 | nodeRelativeColumn: column,
|
549 | });
|
550 | }
|
551 | else {
|
552 | (0, expr_parser_1.walk)(expressionVisitor, item);
|
553 | }
|
554 | if (((_a = line.sublist) === null || _a === void 0 ? void 0 : _a.name) === 'ol') {
|
555 | walkLines(line.sublist);
|
556 | }
|
557 | }
|
558 | };
|
559 | walkLines(tree.contents);
|
560 | }
|
561 | for (const [aoid, state] of onlyPerformed) {
|
562 | if (state !== 'only performed') {
|
563 | continue;
|
564 | }
|
565 | const message = `${aoid} is only ever invoked with Perform, so it should return ~unused~ or a Completion Record which, if normal, contains ~unused~`;
|
566 | const ruleId = 'perform-not-unused';
|
567 | const biblioEntry = this.biblio.byAoid(aoid);
|
568 | if (biblioEntry._node) {
|
569 | this.spec.warn({
|
570 | type: 'node',
|
571 | ruleId,
|
572 | message,
|
573 | node: biblioEntry._node,
|
574 | });
|
575 | }
|
576 | else {
|
577 | this.spec.warn({
|
578 | type: 'global',
|
579 | ruleId,
|
580 | message,
|
581 | });
|
582 | }
|
583 | }
|
584 | for (const [aoid, state] of alwaysAssertedToBeNormal) {
|
585 | if (state !== 'always asserted normal') {
|
586 | continue;
|
587 | }
|
588 | if (aoid === 'AsyncGeneratorAwaitReturn') {
|
589 |
|
590 | continue;
|
591 | }
|
592 | const message = `every call site of ${aoid} asserts the return value is a normal completion; it should be refactored to not return a completion record at all`;
|
593 | const ruleId = 'always-asserted-normal';
|
594 | const biblioEntry = this.biblio.byAoid(aoid);
|
595 | if (biblioEntry._node) {
|
596 | this.spec.warn({
|
597 | type: 'node',
|
598 | ruleId,
|
599 | message,
|
600 | node: biblioEntry._node,
|
601 | });
|
602 | }
|
603 | else {
|
604 | this.spec.warn({
|
605 | type: 'global',
|
606 | ruleId,
|
607 | message,
|
608 | });
|
609 | }
|
610 | }
|
611 | }
|
612 | toHTML() {
|
613 | const htmlEle = this.doc.documentElement;
|
614 | return '<!doctype html>\n' + (htmlEle.hasAttributes() ? htmlEle.outerHTML : htmlEle.innerHTML);
|
615 | }
|
616 | locate(node) {
|
617 | let pointer = node;
|
618 | while (pointer != null) {
|
619 | if (isEmuImportElement(pointer)) {
|
620 | break;
|
621 | }
|
622 | pointer = pointer.parentElement;
|
623 | }
|
624 | const dom = pointer == null ? this.dom : pointer.dom;
|
625 | if (!dom) {
|
626 | return;
|
627 | }
|
628 |
|
629 | const loc = dom.nodeLocation(node);
|
630 | if (loc) {
|
631 |
|
632 | const out = {
|
633 | source: this.sourceText,
|
634 | startTag: loc.startTag,
|
635 | endTag: loc.endTag,
|
636 | startOffset: loc.startOffset,
|
637 | endOffset: loc.endOffset,
|
638 | attrs: loc.attrs,
|
639 | startLine: loc.startLine,
|
640 | startCol: loc.startCol,
|
641 | endLine: loc.endLine,
|
642 | endCol: loc.endCol,
|
643 | };
|
644 | if (pointer != null) {
|
645 | out.file = pointer.importPath;
|
646 | out.source = pointer.source;
|
647 | }
|
648 | return out;
|
649 | }
|
650 | }
|
651 | buildReferenceGraph() {
|
652 | const refToClause = this.refsByClause;
|
653 | const setParent = (node) => {
|
654 | let pointer = node;
|
655 | while (pointer && !['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'].includes(pointer.nodeName)) {
|
656 | pointer = pointer.parentNode;
|
657 | }
|
658 |
|
659 | if (pointer == null || pointer.id == null) {
|
660 |
|
661 | pointer = { id: 'sec-intro' };
|
662 | }
|
663 |
|
664 | if (refToClause[pointer.id] == null) {
|
665 |
|
666 | refToClause[pointer.id] = [];
|
667 | }
|
668 |
|
669 | refToClause[pointer.id].push(node.id);
|
670 | };
|
671 | let counter = 0;
|
672 | this._xrefs.forEach(xref => {
|
673 | let entry = xref.entry;
|
674 | if (!entry || entry.namespace === 'external')
|
675 | return;
|
676 | if (!entry.id && entry.refId) {
|
677 | entry = this.spec.biblio.byId(entry.refId);
|
678 | }
|
679 | if (!xref.id) {
|
680 | const id = `_ref_${counter++}`;
|
681 | xref.node.setAttribute('id', id);
|
682 | xref.id = id;
|
683 | }
|
684 | setParent(xref.node);
|
685 | entry.referencingIds.push(xref.id);
|
686 | });
|
687 | this._ntRefs.forEach(prod => {
|
688 | const entry = prod.entry;
|
689 | if (!entry || entry.namespace === 'external')
|
690 | return;
|
691 |
|
692 | if (prod.node.parentNode.nodeName === 'EMU-PRODUCTION')
|
693 | return;
|
694 | const id = `_ref_${counter++}`;
|
695 | prod.node.setAttribute('id', id);
|
696 | setParent(prod.node);
|
697 | entry.referencingIds.push(id);
|
698 | });
|
699 | }
|
700 | checkValidSectionId(ele) {
|
701 | if (!ele.id.startsWith('sec-')) {
|
702 | this.warn({
|
703 | type: 'node',
|
704 | ruleId: 'top-level-section-id',
|
705 | message: 'When using --multipage, top-level sections must have ids beginning with `sec-`',
|
706 | node: ele,
|
707 | });
|
708 | return false;
|
709 | }
|
710 | if (!/^[A-Za-z0-9-_]+$/.test(ele.id)) {
|
711 | this.warn({
|
712 | type: 'node',
|
713 | ruleId: 'top-level-section-id',
|
714 | message: 'When using --multipage, top-level sections must have ids matching /^[A-Za-z0-9-_]+$/',
|
715 | node: ele,
|
716 | });
|
717 | return false;
|
718 | }
|
719 | if (ele.id.toLowerCase() === 'sec-index') {
|
720 | this.warn({
|
721 | type: 'node',
|
722 | ruleId: 'top-level-section-id',
|
723 | message: 'When using --multipage, top-level sections must not be named "index"',
|
724 | node: ele,
|
725 | });
|
726 | return false;
|
727 | }
|
728 | return true;
|
729 | }
|
730 | propagateEffects() {
|
731 | for (const [effectName, worklist] of this._effectWorklist) {
|
732 | this.propagateEffect(effectName, worklist);
|
733 | }
|
734 | }
|
735 | propagateEffect(effectName, worklist) {
|
736 | const usersOfAoid = new Map();
|
737 | for (const xref of this._xrefs) {
|
738 | if (xref.clause == null || xref.aoid == null)
|
739 | continue;
|
740 | if (!xref.shouldPropagateEffect(effectName))
|
741 | continue;
|
742 | if (xref.hasAddedEffect(effectName)) {
|
743 | maybeAddClauseToEffectWorklist(effectName, xref.clause, worklist);
|
744 | }
|
745 | const usedAoid = xref.aoid;
|
746 | if (!usersOfAoid.has(usedAoid)) {
|
747 | usersOfAoid.set(usedAoid, new Set());
|
748 | }
|
749 | usersOfAoid.get(usedAoid).add(xref.clause);
|
750 | }
|
751 | while (worklist.length !== 0) {
|
752 | const clause = worklist.shift();
|
753 | const aoid = clause.aoid;
|
754 | if (aoid == null || !usersOfAoid.has(aoid)) {
|
755 | continue;
|
756 | }
|
757 | this._effectfulAOs.set(aoid, clause.effects);
|
758 | for (const userClause of usersOfAoid.get(aoid)) {
|
759 | maybeAddClauseToEffectWorklist(effectName, userClause, worklist);
|
760 | }
|
761 | }
|
762 | }
|
763 | getEffectsByAoid(aoid) {
|
764 | if (this._effectfulAOs.has(aoid)) {
|
765 | return this._effectfulAOs.get(aoid);
|
766 | }
|
767 | return null;
|
768 | }
|
769 | async buildMultipage(wrapper, commonEles, jsSha) {
|
770 | let stillIntro = true;
|
771 | const introEles = [];
|
772 | const sections = [];
|
773 | const containedIdToSection = new Map();
|
774 | const sectionToContainedIds = new Map();
|
775 | const clauseTypes = ['EMU-ANNEX', 'EMU-CLAUSE'];
|
776 |
|
777 | for (const child of wrapper.children) {
|
778 | if (stillIntro) {
|
779 | if (clauseTypes.includes(child.nodeName)) {
|
780 | throw new Error('cannot make multipage build without intro');
|
781 | }
|
782 | else if (child.nodeName === 'EMU-INTRO') {
|
783 | stillIntro = false;
|
784 | if (child.id == null) {
|
785 | this.warn({
|
786 | type: 'node',
|
787 | ruleId: 'top-level-section-id',
|
788 | message: 'When using --multipage, top-level sections must have ids',
|
789 | node: child,
|
790 | });
|
791 | continue;
|
792 | }
|
793 | if (child.id !== 'sec-intro') {
|
794 | this.warn({
|
795 | type: 'node',
|
796 | ruleId: 'top-level-section-id',
|
797 | message: 'When using --multipage, the introduction must have id "sec-intro"',
|
798 | node: child,
|
799 | });
|
800 | continue;
|
801 | }
|
802 | const name = 'index';
|
803 | introEles.push(child);
|
804 | sections.push({ name, eles: introEles });
|
805 | const contained = [];
|
806 | sectionToContainedIds.set(name, contained);
|
807 | for (const item of introEles) {
|
808 | if (item.id) {
|
809 | contained.push(item.id);
|
810 | containedIdToSection.set(item.id, name);
|
811 | }
|
812 | }
|
813 |
|
814 | for (const item of [...introEles].flatMap(e => [...e.querySelectorAll('[id]')])) {
|
815 | contained.push(item.id);
|
816 | containedIdToSection.set(item.id, name);
|
817 | }
|
818 | }
|
819 | else {
|
820 | introEles.push(child);
|
821 | }
|
822 | }
|
823 | else {
|
824 | if (!clauseTypes.includes(child.nodeName)) {
|
825 | throw new Error('non-clause children are not yet implemented: ' + child.nodeName);
|
826 | }
|
827 | if (child.id == null) {
|
828 | this.warn({
|
829 | type: 'node',
|
830 | ruleId: 'top-level-section-id',
|
831 | message: 'When using --multipage, top-level sections must have ids',
|
832 | node: child,
|
833 | });
|
834 | continue;
|
835 | }
|
836 | if (!this.checkValidSectionId(child)) {
|
837 | continue;
|
838 | }
|
839 | const name = child.id.substring(4);
|
840 | const contained = [];
|
841 | sectionToContainedIds.set(name, contained);
|
842 | contained.push(child.id);
|
843 | containedIdToSection.set(child.id, name);
|
844 | for (const item of child.querySelectorAll('[id]')) {
|
845 | contained.push(item.id);
|
846 | containedIdToSection.set(item.id, name);
|
847 | }
|
848 | sections.push({ name, eles: [child] });
|
849 | }
|
850 | }
|
851 | let htmlEle = '';
|
852 | if (this.doc.documentElement.hasAttributes()) {
|
853 | const clonedHtmlEle = this.doc.documentElement.cloneNode(false);
|
854 | clonedHtmlEle.innerHTML = '';
|
855 | const src = clonedHtmlEle.outerHTML;
|
856 | htmlEle = src.substring(0, src.length - '<head></head><body></body></html>'.length);
|
857 | }
|
858 | const head = this.doc.head.cloneNode(true);
|
859 | this.addStyle(head, 'ecmarkup.css');
|
860 | this.addStyle(head, `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${hljs.versionString}/styles/base16/solarized-light.min.css`);
|
861 | const script = this.doc.createElement('script');
|
862 | script.src = '../ecmarkup.js?cache=' + jsSha;
|
863 | script.setAttribute('defer', '');
|
864 | head.appendChild(script);
|
865 | const containedMap = JSON.stringify(Object.fromEntries(sectionToContainedIds)).replace(/[\\`$]/g, '\\$&');
|
866 | const multipageJsContents = `'use strict';
|
867 | let multipageMap = JSON.parse(\`${containedMap}\`);
|
868 | ${await utils.readFile(path.join(__dirname, '../js/multipage.js'))}
|
869 | `;
|
870 | if (this.opts.assets !== 'none') {
|
871 | this.generatedFiles.set(path.join(this.opts.outfile, 'multipage/multipage.js'), multipageJsContents);
|
872 | }
|
873 | const multipageScript = this.doc.createElement('script');
|
874 | multipageScript.src = 'multipage.js?cache=' + sha(multipageJsContents);
|
875 | multipageScript.setAttribute('defer', '');
|
876 | head.insertBefore(multipageScript, head.querySelector('script'));
|
877 | for (const { name, eles } of sections) {
|
878 | this.log(`Generating section ${name}...`);
|
879 | const headClone = head.cloneNode(true);
|
880 | const commonClone = commonEles.map(e => e.cloneNode(true));
|
881 | const clones = eles.map(e => e.cloneNode(true));
|
882 | const allClones = [headClone, ...commonClone, ...clones];
|
883 |
|
884 | const links = allClones.flatMap(e => [...e.querySelectorAll('a,link')]);
|
885 | for (const link of links) {
|
886 | if (linkIsAbsolute(link)) {
|
887 | continue;
|
888 | }
|
889 | if (linkIsInternal(link)) {
|
890 | let p = link.hash.substring(1);
|
891 | if (!containedIdToSection.has(p)) {
|
892 | try {
|
893 | p = decodeURIComponent(p);
|
894 | }
|
895 | catch {
|
896 |
|
897 | }
|
898 | if (!containedIdToSection.has(p)) {
|
899 | this.warn({
|
900 | type: 'node',
|
901 | ruleId: 'multipage-link-target',
|
902 | message: 'could not find appropriate section for ' + link.hash,
|
903 | node: link,
|
904 | });
|
905 | continue;
|
906 | }
|
907 | }
|
908 | const targetSec = containedIdToSection.get(p);
|
909 | link.href = (targetSec === 'index' ? './' : targetSec + '.html') + link.hash;
|
910 | }
|
911 | else if (linkIsPathRelative(link)) {
|
912 | link.href = '../' + pathFromRelativeLink(link);
|
913 | }
|
914 | }
|
915 |
|
916 | for (const img of allClones.flatMap(e => [...e.querySelectorAll('img')])) {
|
917 | if (!/^(http:|https:|:|\/)/.test(img.src)) {
|
918 | img.src = '../' + img.src;
|
919 | }
|
920 | }
|
921 |
|
922 |
|
923 | for (const object of allClones.flatMap(e => [...e.querySelectorAll('object[data]')])) {
|
924 | if (!/^(http:|https:|:|\/)/.test(object.data)) {
|
925 | object.data = '../' + object.data;
|
926 | }
|
927 | }
|
928 | if (eles[0].hasAttribute('id')) {
|
929 | const canonical = this.doc.createElement('link');
|
930 | canonical.setAttribute('rel', 'canonical');
|
931 | canonical.setAttribute('href', `../#${eles[0].id}`);
|
932 | headClone.appendChild(canonical);
|
933 | }
|
934 |
|
935 | const commonHTML = commonClone.map(e => e.outerHTML).join('\n');
|
936 |
|
937 | const clonesHTML = clones.map(e => e.outerHTML).join('\n');
|
938 | const content = `<!doctype html>${htmlEle}\n${headClone.outerHTML}\n<body>${commonHTML}<div id='spec-container'>${clonesHTML}</div></body>`;
|
939 | this.generatedFiles.set(path.join(this.opts.outfile, `multipage/${name}.html`), content);
|
940 | }
|
941 | }
|
942 | async buildAssets(jsContents, jsSha) {
|
943 | const cssContents = await utils.readFile(path.join(__dirname, '../css/elements.css'));
|
944 | if (this.opts.jsOut) {
|
945 | this.generatedFiles.set(this.opts.jsOut, jsContents);
|
946 | }
|
947 | if (this.opts.cssOut) {
|
948 | this.generatedFiles.set(this.opts.cssOut, cssContents);
|
949 | }
|
950 | if (this.opts.assets === 'none')
|
951 | return;
|
952 | const outDir = this.opts.outfile
|
953 | ? this.opts.multipage
|
954 | ? this.opts.outfile
|
955 | : path.dirname(this.opts.outfile)
|
956 | : process.cwd();
|
957 | if (this.opts.jsOut) {
|
958 | let skipJs = false;
|
959 | const scripts = this.doc.querySelectorAll('script');
|
960 | for (let i = 0; i < scripts.length; i++) {
|
961 | const script = scripts[i];
|
962 | const src = script.getAttribute('src');
|
963 | if (src && path.normalize(path.join(outDir, src)) === path.normalize(this.opts.jsOut)) {
|
964 | this.log(`Found existing js link to ${src}, skipping inlining...`);
|
965 | skipJs = true;
|
966 | }
|
967 | }
|
968 | if (!skipJs) {
|
969 | const script = this.doc.createElement('script');
|
970 | script.src = path.relative(outDir, this.opts.jsOut) + '?cache=' + jsSha;
|
971 | script.setAttribute('defer', '');
|
972 | this.doc.head.appendChild(script);
|
973 | }
|
974 | }
|
975 | else {
|
976 | this.log('Inlining JavaScript assets...');
|
977 | const script = this.doc.createElement('script');
|
978 | script.textContent = jsContents;
|
979 | this.doc.head.appendChild(script);
|
980 | }
|
981 | if (this.opts.cssOut) {
|
982 | let skipCss = false;
|
983 | const links = this.doc.querySelectorAll('link[rel=stylesheet]');
|
984 | for (let i = 0; i < links.length; i++) {
|
985 | const link = links[i];
|
986 | const href = link.getAttribute('href');
|
987 | if (href && path.normalize(path.join(outDir, href)) === path.normalize(this.opts.cssOut)) {
|
988 | this.log(`Found existing css link to ${href}, skipping inlining...`);
|
989 | skipCss = true;
|
990 | }
|
991 | }
|
992 | if (!skipCss) {
|
993 | this.addStyle(this.doc.head, path.relative(outDir, this.opts.cssOut));
|
994 | }
|
995 | }
|
996 | else {
|
997 | this.log('Inlining CSS assets...');
|
998 | const style = this.doc.createElement('style');
|
999 | style.textContent = cssContents;
|
1000 | this.doc.head.appendChild(style);
|
1001 | }
|
1002 | this.addStyle(this.doc.head, `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${hljs.versionString}/styles/base16/solarized-light.min.css`);
|
1003 | }
|
1004 | addStyle(head, href) {
|
1005 | const style = this.doc.createElement('link');
|
1006 | style.setAttribute('rel', 'stylesheet');
|
1007 | style.setAttribute('href', href);
|
1008 |
|
1009 | const firstLink = head.querySelector('link[rel=stylesheet], style');
|
1010 | if (firstLink != null) {
|
1011 | head.insertBefore(style, firstLink);
|
1012 | }
|
1013 | else {
|
1014 | head.appendChild(style);
|
1015 | }
|
1016 | }
|
1017 | buildSpecWrapper() {
|
1018 | const elements = this.doc.body.childNodes;
|
1019 | const wrapper = this.doc.createElement('div');
|
1020 | wrapper.id = 'spec-container';
|
1021 | while (elements.length > 0) {
|
1022 | wrapper.appendChild(elements[0]);
|
1023 | }
|
1024 | this.doc.body.appendChild(wrapper);
|
1025 | return wrapper;
|
1026 | }
|
1027 | buildShortcutsHelp() {
|
1028 | const shortcutsHelp = this.doc.createElement('div');
|
1029 | shortcutsHelp.setAttribute('id', 'shortcuts-help');
|
1030 | shortcutsHelp.innerHTML = `
|
1031 | <ul>
|
1032 | <li><span>Toggle shortcuts help</span><code>?</code></li>
|
1033 | <li><span>Toggle "can call user code" annotations</span><code>u</code></li>
|
1034 | ${this.opts.multipage ? `<li><span>Navigate to/from multipage</span><code>m</code></li>` : ''}
|
1035 | <li><span>Jump to search box</span><code>/</code></li>
|
1036 | </ul>`;
|
1037 | return shortcutsHelp;
|
1038 | }
|
1039 | processMetadata() {
|
1040 | const block = this.doc.querySelector('pre.metadata');
|
1041 | if (!block || !block.parentNode) {
|
1042 | return;
|
1043 | }
|
1044 | let data;
|
1045 | try {
|
1046 | data = yaml.safeLoad(block.textContent);
|
1047 | }
|
1048 | catch (e) {
|
1049 | 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') {
|
1050 | this.warn({
|
1051 | type: 'contents',
|
1052 | ruleId: 'invalid-metadata',
|
1053 | message: `metadata block failed to parse: ${e.reason}`,
|
1054 | node: block,
|
1055 | nodeRelativeLine: e.mark.line + 1,
|
1056 | nodeRelativeColumn: e.mark.column + 1,
|
1057 | });
|
1058 | }
|
1059 | else {
|
1060 | this.warn({
|
1061 | type: 'node',
|
1062 | ruleId: 'invalid-metadata',
|
1063 | message: 'metadata block failed to parse',
|
1064 | node: block,
|
1065 | });
|
1066 | }
|
1067 | return;
|
1068 | }
|
1069 | finally {
|
1070 | block.parentNode.removeChild(block);
|
1071 | }
|
1072 | Object.assign(this.opts, data);
|
1073 | }
|
1074 | async loadBiblios() {
|
1075 | var _a, _b;
|
1076 | this.cancellationToken.throwIfCancellationRequested();
|
1077 | const biblioPaths = [];
|
1078 | for (const biblioEle of this.doc.querySelectorAll('emu-biblio')) {
|
1079 | const href = biblioEle.getAttribute('href');
|
1080 | if (href == null) {
|
1081 | this.spec.warn({
|
1082 | type: 'node',
|
1083 | node: biblioEle,
|
1084 | ruleId: 'biblio-href',
|
1085 | message: 'emu-biblio elements must have an href attribute',
|
1086 | });
|
1087 | }
|
1088 | else {
|
1089 | biblioPaths.push(href);
|
1090 | }
|
1091 | }
|
1092 | const biblioContents = await Promise.all(biblioPaths.map(p => this.fetch(path.join(this.rootDir, p))));
|
1093 | const biblios = biblioContents.flatMap(c => JSON.parse(c));
|
1094 | for (const biblio of biblios.concat((_a = this.opts.extraBiblios) !== null && _a !== void 0 ? _a : [])) {
|
1095 | if ((biblio === null || biblio === void 0 ? void 0 : biblio.entries) == null) {
|
1096 | let message = Object.keys(biblio !== null && biblio !== void 0 ? biblio : {}).some(k => k.startsWith('http'))
|
1097 | ? 'This is an old-style biblio.'
|
1098 | : 'Biblio does not appear to be in the correct format, are you using an old-style biblio?';
|
1099 | message += ' You will need to update it to work with versions of ecmarkup >= 12.0.0.';
|
1100 | throw new Error(message);
|
1101 | }
|
1102 | this.biblio.addExternalBiblio(biblio);
|
1103 | for (const entry of biblio.entries) {
|
1104 | if (entry.type === 'op' && ((_b = entry.effects) === null || _b === void 0 ? void 0 : _b.length) > 0) {
|
1105 | this._effectfulAOs.set(entry.aoid, entry.effects);
|
1106 | for (const effect of entry.effects) {
|
1107 | if (!this._effectWorklist.has(effect)) {
|
1108 | this._effectWorklist.set(effect, []);
|
1109 | }
|
1110 | this._effectWorklist.get(effect).push(entry);
|
1111 | }
|
1112 | }
|
1113 | }
|
1114 | }
|
1115 | }
|
1116 | async loadImports() {
|
1117 | await loadImports(this, this.spec.doc.body, this.rootDir);
|
1118 | }
|
1119 | exportBiblio() {
|
1120 | if (!this.opts.location) {
|
1121 | this.warn({
|
1122 | type: 'global',
|
1123 | ruleId: 'no-location',
|
1124 | message: "no spec location specified; biblio not generated. try setting the location in the document's metadata block",
|
1125 | });
|
1126 | return null;
|
1127 | }
|
1128 | return this.biblio.export();
|
1129 | }
|
1130 | highlightCode() {
|
1131 | this.log('Highlighting syntax...');
|
1132 | const codes = this.doc.querySelectorAll('pre code');
|
1133 | for (let i = 0; i < codes.length; i++) {
|
1134 | const classAttr = codes[i].getAttribute('class');
|
1135 | if (!classAttr)
|
1136 | continue;
|
1137 | const language = classAttr.replace(/lang(uage)?-/, '');
|
1138 | let input = codes[i].textContent;
|
1139 |
|
1140 | input = input.replace(/^(\s*[\r\n])+|([\r\n]\s*)+$/g, '');
|
1141 |
|
1142 | const baseIndent = input.match(/^\s*/) || '';
|
1143 | const baseIndentRe = new RegExp('^' + baseIndent, 'gm');
|
1144 | input = input.replace(baseIndentRe, '');
|
1145 |
|
1146 | const result = hljs.highlight(input, { language });
|
1147 | codes[i].innerHTML = result.value;
|
1148 | codes[i].setAttribute('class', classAttr + ' hljs');
|
1149 | }
|
1150 | }
|
1151 | buildBoilerplate() {
|
1152 | this.cancellationToken.throwIfCancellationRequested();
|
1153 | const status = this.opts.status;
|
1154 | const version = this.opts.version;
|
1155 | const title = this.opts.title;
|
1156 | const shortname = this.opts.shortname;
|
1157 | const location = this.opts.location;
|
1158 | const stage = this.opts.stage;
|
1159 | if (this.opts.copyright) {
|
1160 | if (status !== 'draft' && status !== 'standard' && !this.opts.contributors) {
|
1161 | this.warn({
|
1162 | type: 'global',
|
1163 | ruleId: 'no-contributors',
|
1164 | message: 'contributors not specified, skipping copyright boilerplate. specify contributors in your frontmatter metadata',
|
1165 | });
|
1166 | }
|
1167 | else {
|
1168 | this.buildCopyrightBoilerplate();
|
1169 | }
|
1170 | }
|
1171 |
|
1172 | if (!title)
|
1173 | return;
|
1174 |
|
1175 | if (title && !this._updateBySelector('title', title)) {
|
1176 | const titleElem = this.doc.createElement('title');
|
1177 | titleElem.innerHTML = title;
|
1178 | this.doc.head.appendChild(titleElem);
|
1179 | const h1 = this.doc.createElement('h1');
|
1180 | h1.setAttribute('class', 'title');
|
1181 | h1.innerHTML = title;
|
1182 | this.doc.body.insertBefore(h1, this.doc.body.firstChild);
|
1183 | }
|
1184 |
|
1185 | let versionText = '';
|
1186 | let omitShortname = false;
|
1187 | if (version) {
|
1188 | versionText += version + ' / ';
|
1189 | }
|
1190 | else if (status === 'proposal' && stage) {
|
1191 | versionText += 'Stage ' + stage + ' Draft / ';
|
1192 | }
|
1193 | else if (shortname && status === 'draft') {
|
1194 | versionText += 'Draft ' + shortname + ' / ';
|
1195 | omitShortname = true;
|
1196 | }
|
1197 | else {
|
1198 | return;
|
1199 | }
|
1200 | const defaultDateFormat = status === 'standard' ? STANDARD_DATE_FORMAT : DRAFT_DATE_FORMAT;
|
1201 | const date = new Intl.DateTimeFormat('en-US', defaultDateFormat).format(this.opts.date);
|
1202 | versionText += date;
|
1203 | if (!this._updateBySelector('h1.version', versionText)) {
|
1204 | const h1 = this.doc.createElement('h1');
|
1205 | h1.setAttribute('class', 'version');
|
1206 | h1.innerHTML = versionText;
|
1207 | this.doc.body.insertBefore(h1, this.doc.body.firstChild);
|
1208 | }
|
1209 |
|
1210 | if (shortname && !omitShortname) {
|
1211 |
|
1212 | const shortnameLinkHtml = status === 'proposal' && location ? `<a href="${location}">${shortname}</a>` : shortname;
|
1213 | const shortnameHtml = status.charAt(0).toUpperCase() + status.slice(1) + ' ' + shortnameLinkHtml;
|
1214 | if (!this._updateBySelector('h1.shortname', shortnameHtml)) {
|
1215 | const h1 = this.doc.createElement('h1');
|
1216 | h1.setAttribute('class', 'shortname');
|
1217 | h1.innerHTML = shortnameHtml;
|
1218 | this.doc.body.insertBefore(h1, this.doc.body.firstChild);
|
1219 | }
|
1220 | }
|
1221 | }
|
1222 | buildCopyrightBoilerplate() {
|
1223 | let addressFile;
|
1224 | let copyrightFile;
|
1225 | let licenseFile;
|
1226 | if (this.opts.boilerplate) {
|
1227 | if (this.opts.boilerplate.address) {
|
1228 | addressFile = path.join(process.cwd(), this.opts.boilerplate.address);
|
1229 | }
|
1230 | if (this.opts.boilerplate.copyright) {
|
1231 | copyrightFile = path.join(process.cwd(), this.opts.boilerplate.copyright);
|
1232 | }
|
1233 | if (this.opts.boilerplate.license) {
|
1234 | licenseFile = path.join(process.cwd(), this.opts.boilerplate.license);
|
1235 | }
|
1236 | }
|
1237 |
|
1238 | let address = getBoilerplate(addressFile || 'address');
|
1239 | let copyright = getBoilerplate(copyrightFile || `${this.opts.status}-copyright`);
|
1240 | const license = getBoilerplate(licenseFile || 'software-license');
|
1241 | if (this.opts.status === 'proposal') {
|
1242 | address = '';
|
1243 | }
|
1244 |
|
1245 | copyright = copyright.replace(/!YEAR!/g, '' + this.opts.date.getFullYear());
|
1246 | if (this.opts.contributors) {
|
1247 | copyright = copyright.replace(/!CONTRIBUTORS!/g, this.opts.contributors);
|
1248 | }
|
1249 | let copyrightClause = this.doc.querySelector('.copyright-and-software-license');
|
1250 | if (!copyrightClause) {
|
1251 | let last;
|
1252 | utils.domWalkBackward(this.doc.body, node => {
|
1253 | if (last)
|
1254 | return false;
|
1255 | if (node.nodeName === 'EMU-CLAUSE' || node.nodeName === 'EMU-ANNEX') {
|
1256 | last = node;
|
1257 | return false;
|
1258 | }
|
1259 | });
|
1260 | copyrightClause = this.doc.createElement('emu-annex');
|
1261 | copyrightClause.setAttribute('id', 'sec-copyright-and-software-license');
|
1262 | if (last && last.parentNode) {
|
1263 | last.parentNode.insertBefore(copyrightClause, last.nextSibling);
|
1264 | }
|
1265 | else {
|
1266 | this.doc.body.appendChild(copyrightClause);
|
1267 | }
|
1268 | }
|
1269 | copyrightClause.innerHTML = `
|
1270 | <h1>Copyright & Software License</h1>
|
1271 | ${address}
|
1272 | <h2>Copyright Notice</h2>
|
1273 | ${copyright.replace('!YEAR!', '' + this.opts.date.getFullYear())}
|
1274 | <h2>Software License</h2>
|
1275 | ${license}
|
1276 | `;
|
1277 | }
|
1278 | generateSDOMap() {
|
1279 | var _a;
|
1280 | const sdoMap = Object.create(null);
|
1281 | this.log('Building SDO map...');
|
1282 | const mainGrammar = new Set(this.doc.querySelectorAll('emu-grammar[type=definition]:not([example])'));
|
1283 |
|
1284 | for (const annexEle of this.doc.querySelectorAll('emu-annex emu-grammar[type=definition]')) {
|
1285 | mainGrammar.delete(annexEle);
|
1286 | }
|
1287 | const mainProductions = new Map();
|
1288 | for (const grammarEle of mainGrammar) {
|
1289 | if (!('grammarSource' in grammarEle)) {
|
1290 |
|
1291 | continue;
|
1292 | }
|
1293 | for (const [name, { rhses }] of this.getProductions(grammarEle)) {
|
1294 | if (mainProductions.has(name)) {
|
1295 | mainProductions.set(name, mainProductions.get(name).concat(rhses));
|
1296 | }
|
1297 | else {
|
1298 | mainProductions.set(name, rhses);
|
1299 | }
|
1300 | }
|
1301 | }
|
1302 | const sdos = this.doc.querySelectorAll('emu-clause[type=sdo],emu-clause[type="syntax-directed operation"]');
|
1303 | outer: for (const sdo of sdos) {
|
1304 | let header;
|
1305 | for (const child of sdo.children) {
|
1306 | if (child.tagName === 'SPAN' && child.childNodes.length === 0) {
|
1307 |
|
1308 | continue;
|
1309 | }
|
1310 | if (child.tagName === 'H1') {
|
1311 | header = child;
|
1312 | break;
|
1313 | }
|
1314 | this.warn({
|
1315 | type: 'node',
|
1316 | node: child,
|
1317 | ruleId: 'sdo-name',
|
1318 | message: 'expected H1 as first child of syntax-directed operation',
|
1319 | });
|
1320 | continue outer;
|
1321 | }
|
1322 | if (!header) {
|
1323 | continue;
|
1324 | }
|
1325 | const clause = header.firstElementChild.textContent;
|
1326 | const nameMatch = (_a = header.textContent) === null || _a === void 0 ? void 0 : _a.slice(clause.length + 1).match(/^(?:(?:Static|Runtime) Semantics: )?\s*(\w+)\b/);
|
1327 | if (nameMatch == null) {
|
1328 | this.warn({
|
1329 | type: 'contents',
|
1330 | node: header,
|
1331 | ruleId: 'sdo-name',
|
1332 | message: 'could not parse name of syntax-directed operation',
|
1333 | nodeRelativeLine: 1,
|
1334 | nodeRelativeColumn: 1,
|
1335 | });
|
1336 | continue;
|
1337 | }
|
1338 | const sdoName = nameMatch[1];
|
1339 | for (const grammarEle of sdo.children) {
|
1340 | if (grammarEle.tagName !== 'EMU-GRAMMAR') {
|
1341 | continue;
|
1342 | }
|
1343 | if (!('grammarSource' in grammarEle)) {
|
1344 |
|
1345 | continue;
|
1346 | }
|
1347 |
|
1348 | for (const [name, { production, rhses }] of this.getProductions(grammarEle)) {
|
1349 | if (!mainProductions.has(name)) {
|
1350 | if (this.biblio.byProductionName(name) != null) {
|
1351 |
|
1352 |
|
1353 |
|
1354 | continue;
|
1355 | }
|
1356 | const { line, column } = (0, utils_1.getLocationInGrammarFile)(grammarEle.grammarSource, production.pos);
|
1357 | this.warn({
|
1358 | type: 'contents',
|
1359 | node: grammarEle,
|
1360 | nodeRelativeLine: line,
|
1361 | nodeRelativeColumn: column,
|
1362 | ruleId: 'grammar-shape',
|
1363 | message: `could not find definition corresponding to production ${name}`,
|
1364 | });
|
1365 | continue;
|
1366 | }
|
1367 | const mainRhses = mainProductions.get(name);
|
1368 | for (const { rhs, rhsEle } of rhses) {
|
1369 | const matches = mainRhses.filter(p => (0, utils_1.rhsMatches)(rhs, p.rhs));
|
1370 | if (matches.length === 0) {
|
1371 | const { line, column } = (0, utils_1.getLocationInGrammarFile)(grammarEle.grammarSource, rhs.pos);
|
1372 | this.warn({
|
1373 | type: 'contents',
|
1374 | node: grammarEle,
|
1375 | nodeRelativeLine: line,
|
1376 | nodeRelativeColumn: column,
|
1377 | ruleId: 'grammar-shape',
|
1378 | message: `could not find definition for rhs ${JSON.stringify(rhsEle.textContent)}`,
|
1379 | });
|
1380 | continue;
|
1381 | }
|
1382 | if (matches.length > 1) {
|
1383 | const { line, column } = (0, utils_1.getLocationInGrammarFile)(grammarEle.grammarSource, rhs.pos);
|
1384 | this.warn({
|
1385 | type: 'contents',
|
1386 | node: grammarEle,
|
1387 | nodeRelativeLine: line,
|
1388 | nodeRelativeColumn: column,
|
1389 | ruleId: 'grammar-shape',
|
1390 | message: `found multiple definitions for rhs ${JSON.stringify(rhsEle.textContent)}`,
|
1391 | });
|
1392 | continue;
|
1393 | }
|
1394 | const match = matches[0].rhsEle;
|
1395 | if (match.id === '') {
|
1396 | match.id = 'prod-' + sha(`${name} : ${match.textContent}`);
|
1397 | }
|
1398 | const mainId = match.id;
|
1399 | if (rhsEle.id === '') {
|
1400 | rhsEle.id = 'prod-' + sha(`[${sdoName}] ${name} ${rhsEle.textContent}`);
|
1401 | }
|
1402 | if (!{}.hasOwnProperty.call(sdoMap, mainId)) {
|
1403 | sdoMap[mainId] = Object.create(null);
|
1404 | }
|
1405 | const sdosForThisId = sdoMap[mainId];
|
1406 | if (!{}.hasOwnProperty.call(sdosForThisId, sdoName)) {
|
1407 | sdosForThisId[sdoName] = { clause, ids: [] };
|
1408 | }
|
1409 | else if (sdosForThisId[sdoName].clause !== clause) {
|
1410 | this.warn({
|
1411 | type: 'node',
|
1412 | node: grammarEle,
|
1413 | ruleId: 'grammar-shape',
|
1414 | message: `SDO ${sdoName} found in multiple clauses`,
|
1415 | });
|
1416 | }
|
1417 | sdosForThisId[sdoName].ids.push(rhsEle.id);
|
1418 | }
|
1419 | }
|
1420 | }
|
1421 | }
|
1422 | const json = JSON.stringify(sdoMap);
|
1423 | return `let sdoMap = JSON.parse(\`${json.replace(/[\\`$]/g, '\\$&')}\`);`;
|
1424 | }
|
1425 | getProductions(grammarEle) {
|
1426 |
|
1427 |
|
1428 |
|
1429 | const productions = new Map();
|
1430 | const productionEles = new Map();
|
1431 | for (const productionEle of grammarEle.querySelectorAll('emu-production')) {
|
1432 | if (!productionEle.hasAttribute('name')) {
|
1433 |
|
1434 | this.warn({
|
1435 | type: 'node',
|
1436 |
|
1437 | node: grammarEle,
|
1438 | ruleId: 'grammar-shape',
|
1439 | message: 'expected emu-production node to have name',
|
1440 | });
|
1441 | continue;
|
1442 | }
|
1443 | const name = productionEle.getAttribute('name');
|
1444 | const rhses = [...productionEle.querySelectorAll('emu-rhs')];
|
1445 | if (productionEles.has(name)) {
|
1446 | productionEles.set(name, productionEles.get(name).concat(rhses));
|
1447 | }
|
1448 | else {
|
1449 | productionEles.set(name, rhses);
|
1450 | }
|
1451 | }
|
1452 | const sourceFile = grammarEle.grammarSource;
|
1453 | for (const [name, { production, rhses }] of (0, utils_1.getProductions)([sourceFile])) {
|
1454 | if (!productionEles.has(name)) {
|
1455 | this.warn({
|
1456 | type: 'node',
|
1457 | node: grammarEle,
|
1458 | ruleId: 'rhs-consistency',
|
1459 | message: `failed to locate element for production ${name}. This is is a bug in ecmarkup; please report it.`,
|
1460 | });
|
1461 | continue;
|
1462 | }
|
1463 | const rhsEles = productionEles.get(name);
|
1464 | if (rhsEles.length !== rhses.length) {
|
1465 | this.warn({
|
1466 | type: 'node',
|
1467 | node: grammarEle,
|
1468 | ruleId: 'rhs-consistency',
|
1469 | message: `inconsistent RHS lengths for production ${name}. This is is a bug in ecmarkup; please report it.`,
|
1470 | });
|
1471 | continue;
|
1472 | }
|
1473 | productions.set(name, {
|
1474 | production,
|
1475 | rhses: rhses.map((rhs, i) => ({ rhs, rhsEle: rhsEles[i] })),
|
1476 | });
|
1477 | }
|
1478 | return productions;
|
1479 | }
|
1480 | setReplacementAlgorithmOffsets() {
|
1481 | this.log('Finding offsets for replacement algorithm steps...');
|
1482 | const pending = new Map();
|
1483 | const setReplacementAlgorithmStart = (element, stepNumbers) => {
|
1484 | const rootList = element.firstElementChild;
|
1485 | rootList.start = stepNumbers[stepNumbers.length - 1];
|
1486 | if (stepNumbers.length > 1) {
|
1487 |
|
1488 | if (stepNumbers.length === 2) {
|
1489 | rootList.classList.add('nested-once');
|
1490 | }
|
1491 | else if (stepNumbers.length === 3) {
|
1492 | rootList.classList.add('nested-twice');
|
1493 | }
|
1494 | else if (stepNumbers.length === 4) {
|
1495 | rootList.classList.add('nested-thrice');
|
1496 | }
|
1497 | else if (stepNumbers.length === 5) {
|
1498 | rootList.classList.add('nested-four-times');
|
1499 | }
|
1500 | else {
|
1501 |
|
1502 | rootList.classList.add('nested-lots');
|
1503 | }
|
1504 | }
|
1505 |
|
1506 | for (const entry of this.replacementAlgorithmToContainedLabeledStepEntries.get(element)) {
|
1507 | entry.stepNumbers = [...stepNumbers, ...entry.stepNumbers.slice(1)];
|
1508 | this.labeledStepsToBeRectified.delete(entry.id);
|
1509 |
|
1510 | if (pending.has(entry.id)) {
|
1511 | const todo = pending.get(entry.id);
|
1512 | pending.delete(entry.id);
|
1513 | for (const replacementAlgorithm of todo) {
|
1514 | setReplacementAlgorithmStart(replacementAlgorithm, entry.stepNumbers);
|
1515 | }
|
1516 | }
|
1517 | }
|
1518 | };
|
1519 | for (const { element, target } of this.replacementAlgorithms) {
|
1520 | if (this.labeledStepsToBeRectified.has(target)) {
|
1521 | if (!pending.has(target)) {
|
1522 | pending.set(target, []);
|
1523 | }
|
1524 | pending.get(target).push(element);
|
1525 | }
|
1526 | else {
|
1527 |
|
1528 | const targetEntry = this.biblio.byId(target);
|
1529 | if (targetEntry == null) {
|
1530 | this.warn({
|
1531 | type: 'attr-value',
|
1532 | attr: 'replaces-step',
|
1533 | ruleId: 'invalid-replacement',
|
1534 | message: `could not find step ${JSON.stringify(target)}`,
|
1535 | node: element,
|
1536 | });
|
1537 | }
|
1538 | else if (targetEntry.type !== 'step') {
|
1539 | this.warn({
|
1540 | type: 'attr-value',
|
1541 | attr: 'replaces-step',
|
1542 | ruleId: 'invalid-replacement',
|
1543 | message: `expected algorithm to replace a step, not a ${targetEntry.type}`,
|
1544 | node: element,
|
1545 | });
|
1546 | }
|
1547 | else {
|
1548 | setReplacementAlgorithmStart(element, targetEntry.stepNumbers);
|
1549 | }
|
1550 | }
|
1551 | }
|
1552 | if (pending.size > 0) {
|
1553 |
|
1554 | this.warn({
|
1555 | type: 'global',
|
1556 | ruleId: 'invalid-replacement',
|
1557 | message: 'could not unambiguously determine replacement algorithm offsets - do you have a cycle in your replacement algorithms?',
|
1558 | });
|
1559 | }
|
1560 | }
|
1561 | autolink() {
|
1562 | this.log('Autolinking terms and abstract ops...');
|
1563 | const namespaces = Object.keys(this._textNodes);
|
1564 | for (let i = 0; i < namespaces.length; i++) {
|
1565 | const namespace = namespaces[i];
|
1566 | const { replacer, autolinkmap } = (0, autolinker_1.replacerForNamespace)(namespace, this.biblio);
|
1567 | const nodes = this._textNodes[namespace];
|
1568 | for (let j = 0; j < nodes.length; j++) {
|
1569 | const { node, clause, inAlg, currentId } = nodes[j];
|
1570 | (0, autolinker_1.autolink)(node, replacer, autolinkmap, clause, currentId, inAlg);
|
1571 | }
|
1572 | }
|
1573 | }
|
1574 | setCharset() {
|
1575 | let current = this.spec.doc.querySelector('meta[charset]');
|
1576 | if (!current) {
|
1577 | current = this.spec.doc.createElement('meta');
|
1578 | this.spec.doc.head.insertBefore(current, this.spec.doc.head.firstChild);
|
1579 | }
|
1580 | current.setAttribute('charset', 'utf-8');
|
1581 | }
|
1582 | _updateBySelector(selector, contents) {
|
1583 | const elem = this.doc.querySelector(selector);
|
1584 | if (elem && elem.textContent.trim().length > 0) {
|
1585 | return true;
|
1586 | }
|
1587 | if (elem) {
|
1588 | elem.innerHTML = contents;
|
1589 | return true;
|
1590 | }
|
1591 | return false;
|
1592 | }
|
1593 | }
|
1594 | exports.default = Spec;
|
1595 | function getBoilerplate(file) {
|
1596 | let boilerplateFile = file;
|
1597 | try {
|
1598 | if (fs.lstatSync(file).isFile()) {
|
1599 | boilerplateFile = file;
|
1600 | }
|
1601 | }
|
1602 | catch (error) {
|
1603 | boilerplateFile = path.join(__dirname, '../boilerplate', `${file}.html`);
|
1604 | }
|
1605 | return fs.readFileSync(boilerplateFile, 'utf8');
|
1606 | }
|
1607 | async function loadImports(spec, rootElement, rootPath) {
|
1608 | const imports = rootElement.querySelectorAll('EMU-IMPORT');
|
1609 | for (let i = 0; i < imports.length; i++) {
|
1610 | const node = imports[i];
|
1611 | const imp = await Import_1.default.build(spec, node, rootPath);
|
1612 | await loadImports(spec, node, imp.relativeRoot);
|
1613 | }
|
1614 | }
|
1615 | async function walk(walker, context) {
|
1616 | const previousInNoAutolink = context.inNoAutolink;
|
1617 | let previousInNoEmd = context.inNoEmd;
|
1618 | const { spec } = context;
|
1619 | context.node = walker.currentNode;
|
1620 | context.tagStack.push(context.node);
|
1621 | if (context.node === context.followingEmd) {
|
1622 | context.followingEmd = null;
|
1623 | context.inNoEmd = false;
|
1624 |
|
1625 |
|
1626 | previousInNoEmd = false;
|
1627 | }
|
1628 | if (context.node.nodeType === 3 ) {
|
1629 | if (context.node.textContent.trim().length === 0)
|
1630 | return;
|
1631 | const clause = context.clauseStack[context.clauseStack.length - 1] || context.spec;
|
1632 | const namespace = clause ? clause.namespace : context.spec.namespace;
|
1633 | if (!context.inNoEmd) {
|
1634 |
|
1635 | context.inNoEmd = true;
|
1636 | let node = context.node;
|
1637 | function nextRealSibling(node) {
|
1638 | var _a;
|
1639 | while (((_a = node === null || node === void 0 ? void 0 : node.nextSibling) === null || _a === void 0 ? void 0 : _a.nodeType) === 8 ) {
|
1640 | node = node.nextSibling;
|
1641 | }
|
1642 | return node === null || node === void 0 ? void 0 : node.nextSibling;
|
1643 | }
|
1644 | while (node && !nextRealSibling(node)) {
|
1645 | node = node.parentNode;
|
1646 | }
|
1647 | if (node) {
|
1648 |
|
1649 | context.followingEmd = nextRealSibling(node);
|
1650 | }
|
1651 |
|
1652 | utils.emdTextNode(context.spec, context.node, namespace);
|
1653 | }
|
1654 | if (!context.inNoAutolink) {
|
1655 |
|
1656 |
|
1657 | context.spec._textNodes[namespace] = context.spec._textNodes[namespace] || [];
|
1658 | context.spec._textNodes[namespace].push({
|
1659 | node: context.node,
|
1660 | clause,
|
1661 | inAlg: context.inAlg,
|
1662 | currentId: context.currentId,
|
1663 | });
|
1664 | }
|
1665 | return;
|
1666 | }
|
1667 |
|
1668 |
|
1669 | const oldids = context.node.getAttribute('oldids');
|
1670 | if (oldids) {
|
1671 | if (!context.node.childNodes) {
|
1672 | throw new Error('oldids found on unsupported element: ' + context.node.nodeName);
|
1673 | }
|
1674 | oldids
|
1675 | .split(/,/g)
|
1676 | .map(s => s.trim())
|
1677 | .forEach(oid => {
|
1678 | const s = spec.doc.createElement('span');
|
1679 | s.setAttribute('id', oid);
|
1680 | context.node.insertBefore(s, context.node.childNodes[0]);
|
1681 | });
|
1682 | }
|
1683 | const parentId = context.currentId;
|
1684 | if (context.node.hasAttribute('id')) {
|
1685 | context.currentId = context.node.getAttribute('id');
|
1686 | }
|
1687 | if (autolinker_1.NO_CLAUSE_AUTOLINK.has(context.node.nodeName)) {
|
1688 | context.inNoAutolink = true;
|
1689 | }
|
1690 | else if (autolinker_1.YES_CLAUSE_AUTOLINK.has(context.node.nodeName)) {
|
1691 | context.inNoAutolink = false;
|
1692 | }
|
1693 | if (NO_EMD.has(context.node.nodeName)) {
|
1694 | context.inNoEmd = true;
|
1695 | }
|
1696 | else if (YES_EMD.has(context.node.nodeName)) {
|
1697 | context.inNoEmd = false;
|
1698 | }
|
1699 | const visitor = visitorMap[context.node.nodeName];
|
1700 | if (visitor) {
|
1701 | await visitor.enter(context);
|
1702 | }
|
1703 | const firstChild = walker.firstChild();
|
1704 | if (firstChild) {
|
1705 | while (true) {
|
1706 | await walk(walker, context);
|
1707 | const next = walker.nextSibling();
|
1708 | if (!next)
|
1709 | break;
|
1710 | }
|
1711 | walker.parentNode();
|
1712 | context.node = walker.currentNode;
|
1713 | }
|
1714 | if (visitor)
|
1715 | visitor.exit(context);
|
1716 | context.inNoAutolink = previousInNoAutolink;
|
1717 | context.inNoEmd = previousInNoEmd;
|
1718 | context.currentId = parentId;
|
1719 | context.tagStack.pop();
|
1720 | }
|
1721 | const jsDependencies = ['sdoMap.js', 'menu.js', 'listNumbers.js'];
|
1722 | async function concatJs(...extras) {
|
1723 | let dependencies = await Promise.all(jsDependencies.map(dependency => utils.readFile(path.join(__dirname, '../js/' + dependency))));
|
1724 | dependencies = dependencies.concat(extras);
|
1725 | return dependencies.join('\n');
|
1726 | }
|
1727 | function sha(str) {
|
1728 | return crypto
|
1729 | .createHash('sha256')
|
1730 | .update(str)
|
1731 | .digest('base64')
|
1732 | .slice(0, 8)
|
1733 | .replace(/\+/g, '-')
|
1734 | .replace(/\//g, '_');
|
1735 | }
|
1736 |
|
1737 |
|
1738 | function linkIsAbsolute(link) {
|
1739 | return !link.href.startsWith('about:blank') && /^[a-z]+:/.test(link.href);
|
1740 | }
|
1741 | function linkIsInternal(link) {
|
1742 | return link.href.startsWith('#') || link.href.startsWith('about:blank#');
|
1743 | }
|
1744 | function linkIsPathRelative(link) {
|
1745 | return !link.href.startsWith('/') && !link.href.startsWith('about:blank/');
|
1746 | }
|
1747 | function pathFromRelativeLink(link) {
|
1748 | return link.href.startsWith('about:blank') ? link.href.substring(11) : link.href;
|
1749 | }
|
1750 | function parentSkippingBlankSpace(expr, path) {
|
1751 | for (let pointer = expr, i = path.length - 1; i >= 0; pointer = path[i].parent, --i) {
|
1752 | const { parent } = path[i];
|
1753 | if (parent.type === 'seq' &&
|
1754 | parent.items.every(i => (i.type === 'prose' &&
|
1755 | i.parts.every(p => p.name === 'tag' || (p.name === 'text' && /^\s*$/.test(p.contents)))) ||
|
1756 | i === pointer)) {
|
1757 |
|
1758 | continue;
|
1759 | }
|
1760 | return path[i];
|
1761 | }
|
1762 | return null;
|
1763 | }
|
1764 | function previousText(expr, path) {
|
1765 | const part = parentSkippingBlankSpace(expr, path);
|
1766 | if (part == null) {
|
1767 | return null;
|
1768 | }
|
1769 | const { parent, index } = part;
|
1770 | if (parent.type === 'seq') {
|
1771 | return textFromPreviousPart(parent, index);
|
1772 | }
|
1773 | return null;
|
1774 | }
|
1775 | function textFromPreviousPart(seq, index) {
|
1776 | const prev = seq.items[index - 1];
|
1777 | if ((prev === null || prev === void 0 ? void 0 : prev.type) === 'prose' && prev.parts.length > 0) {
|
1778 | let prevIndex = prev.parts.length - 1;
|
1779 | while (prevIndex > 0 && prev.parts[prevIndex].name === 'tag') {
|
1780 | --prevIndex;
|
1781 | }
|
1782 | const prevProse = prev.parts[prevIndex];
|
1783 | if (prevProse.name === 'text') {
|
1784 | return prevProse.contents;
|
1785 | }
|
1786 | }
|
1787 | return null;
|
1788 | }
|
1789 | function isCalledAsPerform(expr, path, allowQuestion) {
|
1790 | const prev = previousText(expr, path);
|
1791 | return prev != null && (allowQuestion ? /\bperform ([?!]\s)?$/i : /\bperform $/i).test(prev);
|
1792 | }
|
1793 | function isAssertedToBeNormal(expr, path) {
|
1794 | const prev = previousText(expr, path);
|
1795 | return prev != null && /\s!\s$/.test(prev);
|
1796 | }
|
1797 | function isConsumedAsCompletion(expr, path) {
|
1798 | const part = parentSkippingBlankSpace(expr, path);
|
1799 | if (part == null) {
|
1800 | return false;
|
1801 | }
|
1802 | const { parent, index } = part;
|
1803 | if (parent.type === 'seq') {
|
1804 |
|
1805 | const text = textFromPreviousPart(parent, index);
|
1806 | if (text != null && /[!?]\s$/.test(text)) {
|
1807 | return true;
|
1808 | }
|
1809 | }
|
1810 | else if (parent.type === 'call' && index === 0 && parent.arguments.length === 1) {
|
1811 |
|
1812 | const { parts } = parent.callee;
|
1813 | if (parts.length === 1 && parts[0].name === 'text' && parts[0].contents === 'Completion') {
|
1814 | return true;
|
1815 | }
|
1816 | }
|
1817 | return false;
|
1818 | }
|