ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
1,165 lines (1,163 loc) • 81.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.maybeAddClauseToEffectWorklist = maybeAddClauseToEffectWorklist;
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const yaml = require("js-yaml");
const utils = require("./utils");
const hljs = require("highlight.js");
// Builders
const Import_1 = require("./Import");
const Clause_1 = require("./Clause");
const clauseNums_1 = require("./clauseNums");
const Algorithm_1 = require("./Algorithm");
const Dfn_1 = require("./Dfn");
const Example_1 = require("./Example");
const Figure_1 = require("./Figure");
const Note_1 = require("./Note");
const Toc_1 = require("./Toc");
const Menu_1 = require("./Menu");
const Production_1 = require("./Production");
const NonTerminal_1 = require("./NonTerminal");
const ProdRef_1 = require("./ProdRef");
const Grammar_1 = require("./Grammar");
const Xref_1 = require("./Xref");
const Eqn_1 = require("./Eqn");
const Biblio_1 = require("./Biblio");
const Meta_1 = require("./Meta");
const H1_1 = require("./H1");
const autolinker_1 = require("./autolinker");
const lint_1 = require("./lint/lint");
const prex_1 = require("prex");
const utils_1 = require("./lint/utils");
const utils_2 = require("./utils");
const typechecker_1 = require("./typechecker");
const DRAFT_DATE_FORMAT = {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
};
const STANDARD_DATE_FORMAT = {
year: 'numeric',
month: 'long',
timeZone: 'UTC',
};
const NO_EMD = new Set([
'PRE',
'CODE',
'SCRIPT',
'STYLE',
'EMU-PRODUCTION',
'EMU-ALG',
'EMU-GRAMMAR',
'EMU-EQN',
]);
const YES_EMD = new Set(['EMU-GMOD']); // these are processed even if they are nested in NO_EMD contexts
// maps PostScript font names to files in fonts/
const FONT_FILES = new Map([
['IBMPlexSerif-Regular', 'IBMPlexSerif-Regular-SlashedZero.woff2'],
['IBMPlexSerif-Bold', 'IBMPlexSerif-Bold-SlashedZero.woff2'],
['IBMPlexSerif-Italic', 'IBMPlexSerif-Italic-SlashedZero.woff2'],
['IBMPlexSerif-BoldItalic', 'IBMPlexSerif-BoldItalic-SlashedZero.woff2'],
['IBMPlexSans-Regular', 'IBMPlexSans-Regular-SlashedZero.woff2'],
['IBMPlexSans-Bold', 'IBMPlexSans-Bold-SlashedZero.woff2'],
['IBMPlexSans-Italic', 'IBMPlexSans-Italic-SlashedZero.woff2'],
['IBMPlexSans-BoldItalic', 'IBMPlexSans-BoldItalic-SlashedZero.woff2'],
['IBMPlexMono-Regular', 'IBMPlexMono-Regular-SlashedZero.woff2'],
['IBMPlexMono-Bold', 'IBMPlexMono-Bold-SlashedZero.woff2'],
['IBMPlexMono-Italic', 'IBMPlexMono-Italic-SlashedZero.woff2'],
['IBMPlexMono-BoldItalic', 'IBMPlexMono-BoldItalic-SlashedZero.woff2'],
]);
const IMG_FILES = new Set(['ecma-header.svg', 'print-front-cover.svg', 'print-inside-cover.svg']);
const builders = [
Clause_1.default,
Algorithm_1.default,
Xref_1.default,
Dfn_1.default,
Eqn_1.default,
Grammar_1.default,
Production_1.default,
Example_1.default,
Figure_1.default,
NonTerminal_1.default,
ProdRef_1.default,
Note_1.default,
Meta_1.default,
H1_1.default,
];
const visitorMap = builders.reduce((map, T) => {
T.elements.forEach(e => (map[e] = T));
return map;
}, {});
function wrapWarn(source, spec, warn) {
return (e) => {
const { message, ruleId } = e;
let line;
let column;
let nodeType;
let file = undefined;
switch (e.type) {
case 'global':
line = undefined;
column = undefined;
nodeType = 'html';
break;
case 'raw':
({ line, column } = e);
nodeType = 'html';
if (e.file != null) {
file = e.file;
source = e.source;
}
break;
case 'node':
if (e.node.nodeType === 3 /* Node.TEXT_NODE */) {
const loc = spec.locate(e.node);
if (loc) {
file = loc.file;
source = loc.source;
({ startLine: line, startCol: column } = loc);
}
nodeType = 'text';
}
else {
const loc = spec.locate(e.node);
if (loc) {
file = loc.file;
source = loc.source;
({ startLine: line, startCol: column } = loc.startTag);
}
nodeType = e.node.tagName.toLowerCase();
}
break;
case 'attr': {
const loc = spec.locate(e.node);
if (loc) {
file = loc.file;
source = loc.source;
({ line, column } = utils.attrLocation(source, loc, e.attr));
}
nodeType = e.node.tagName.toLowerCase();
break;
}
case 'attr-value': {
const loc = spec.locate(e.node);
if (loc) {
file = loc.file;
source = loc.source;
({ line, column } = utils.attrValueLocation(source, loc, e.attr));
}
nodeType = e.node.tagName.toLowerCase();
break;
}
case 'contents': {
const { nodeRelativeLine, nodeRelativeColumn } = e;
if (e.node.nodeType === 3 /* Node.TEXT_NODE */) {
// i.e. a text node, which does not have a tag
const loc = spec.locate(e.node);
if (loc) {
file = loc.file;
source = loc.source;
line = loc.startLine + nodeRelativeLine - 1;
column =
nodeRelativeLine === 1 ? loc.startCol + nodeRelativeColumn - 1 : nodeRelativeColumn;
}
nodeType = 'text';
}
else {
const loc = spec.locate(e.node);
if (loc) {
file = loc.file;
source = loc.source;
line = loc.startTag.endLine + nodeRelativeLine - 1;
if (nodeRelativeLine === 1) {
column = loc.startTag.endCol + nodeRelativeColumn - 1;
}
else {
column = nodeRelativeColumn;
}
}
nodeType = e.node.tagName.toLowerCase();
}
break;
}
}
warn({
message,
ruleId,
// we omit source for global errors so that we don't get a codeframe
source: e.type === 'global' ? undefined : source,
file,
nodeType,
line,
column,
});
};
}
function maybeAddClauseToEffectWorklist(effectName, clause, worklist) {
if (!worklist.some(i => i.aoid === clause.aoid) &&
clause.canHaveEffect(effectName) &&
!clause.effects.includes(effectName)) {
clause.effects.push(effectName);
worklist.push(clause);
}
}
class Spec {
constructor(rootPath, fetch, dom, opts = {}, sourceText, token = prex_1.CancellationToken.none) {
var _a, _b, _c, _d, _e, _f;
var _g;
this.spec = this;
this.opts = {};
this.rootPath = rootPath;
this.rootDir = path.dirname(this.rootPath);
this.sourceText = sourceText;
this.doc = dom.window.document;
this.dom = dom;
this._fetch = fetch;
this.subclauses = [];
this.imports = [];
this.node = this.doc.body;
this.nodeIds = new Set();
this.replacementAlgorithmToContainedLabeledStepEntries = new Map();
this.labeledStepsToBeRectified = new Set();
this.replacementAlgorithms = [];
this.cancellationToken = token;
this.generatedFiles = new Map();
this.log = (_a = opts.log) !== null && _a !== void 0 ? _a : (() => { });
// TODO warnings should probably default to console.error, with a reasonable formatting
this.warn = opts.warn ? wrapWarn(sourceText, this, opts.warn) : () => { };
this._figureCounts = {
table: 0,
figure: 0,
};
this._xrefs = [];
this._ntRefs = [];
this._ntStringRefs = [];
this._prodRefs = [];
this._textNodes = {};
this._effectWorklist = new Map();
this._effectfulAOs = new Map();
this._emuMetasToRender = new Set();
this._emuMetasToRemove = new Set();
this.refsByClause = Object.create(null);
this.topLevelImportedNodes = new Map();
this.processMetadata();
Object.assign(this.opts, opts);
if (this.opts.jsOut || this.opts.cssOut) {
throw new Error('--js-out and --css-out have been removed; use --assets-dir instead');
}
if (this.opts.oldToc) {
throw new Error('--old-toc has been removed; specify --printable to get a printable document');
}
if (this.opts.assets != null &&
this.opts.assets !== 'external' &&
this.opts.assetsDir != null) {
throw new Error(`--assets=${this.opts.assets} cannot be used with --assets-dir"`);
}
if (this.opts.printable) {
if (this.opts.title == null) {
throw new Error(`--printable requires a title to be set in the metadata"`);
}
if (this.opts.shortname == null) {
throw new Error(`--printable requires a shortname to be set in the metadata"`);
}
}
if (this.opts.multipage) {
(_b = (_g = this.opts).outfile) !== null && _b !== void 0 ? _b : (_g.outfile = '');
const type = (_c = this.opts.assets) !== null && _c !== void 0 ? _c : 'external';
if (type === 'inline') {
throw new Error('assets cannot be inline for multipage builds');
}
else if (type === 'none') {
this.assets = { type: 'none' };
}
else {
this.assets = {
type: 'external',
directory: (_d = this.opts.assetsDir) !== null && _d !== void 0 ? _d : path.join(this.opts.outfile, 'assets'),
};
}
}
else {
const type = (_e = this.opts.assets) !== null && _e !== void 0 ? _e : (this.opts.assetsDir == null ? 'inline' : 'external');
if (type === 'inline') {
console.error('Warning: inlining assets; this should not be done for production builds.');
this.assets = { type: 'inline' };
}
else if (type === 'none') {
this.assets = { type: 'none' };
}
else {
this.assets = {
type: 'external',
directory: (_f = this.opts.assetsDir) !== null && _f !== void 0 ? _f : (this.opts.outfile ? path.join(path.dirname(this.opts.outfile), 'assets') : 'assets'),
};
}
}
if (typeof this.opts.status === 'undefined') {
this.opts.status = 'proposal';
}
if (typeof this.opts.toc === 'undefined') {
this.opts.toc = true;
}
if (typeof this.opts.copyright === 'undefined') {
this.opts.copyright = true;
}
if (!this.opts.date) {
this.opts.date = new Date();
}
if (this.opts.stage != undefined) {
this.opts.stage = String(this.opts.stage);
}
if (!this.opts.location) {
this.opts.location = '<no location>';
}
this.namespace = this.opts.location;
this.biblio = new Biblio_1.default(this.opts.location);
}
/** @internal */
fetch(file) {
return this._fetch(file, this.cancellationToken);
}
/** @internal */
async build() {
/*
The Ecmarkup build process proceeds as follows:
1. Load biblios, making xrefs and auto-linking from external specs work
2. Load imports by recursively inlining the import files' content into the emu-import element
3. Generate boilerplate text
4. Do a walk of the DOM visting elements and text nodes. Text nodes are replaced by text and HTML nodes depending
on content. Elements are built by delegating to builders. Builders work by modifying the DOM ahead of them so
the new elements they make are visited during the walk. Elements added behind the current iteration must be
handled specially (eg. see static exit method of clause). Xref, nt, and prodref's are collected for linking
in the next step.
5. Linking. After the DOM walk we have a complete picture of all the symbols in the document so we proceed to link
the various xrefs to the proper place.
6. Adding charset, highlighting code, etc.
7. Add CSS & JS dependencies.
*/
var _a, _b, _c, _d;
this.log('Loading biblios...');
await this.loadBiblios();
this.log('Loading imports...');
await this.loadImports();
this.log('Building boilerplate...');
this.buildBoilerplate();
const context = {
spec: this,
node: this.doc.body,
importStack: [],
clauseStack: [],
tagStack: [],
clauseNumberer: (0, clauseNums_1.default)(this),
inNoAutolink: false,
inAlg: false,
inNoEmd: false,
followingEmd: null,
currentId: null,
};
const document = this.doc;
if (this.opts.lintSpec) {
this.log('Linting...');
const source = this.sourceText;
if (source === undefined) {
throw new Error('Cannot lint when source text is not available');
}
await (0, lint_1.lint)(this.warn, source, this, document);
}
this.log('Walking document, building various elements...');
const walker = document.createTreeWalker(document.body, 1 | 4 /* elements and text nodes */);
await walk(walker, context);
const sdoJs = this.generateSDOMap();
this.setReplacementAlgorithmOffsets();
if (this.opts.lintSpec) {
this.log('Checking types...');
(0, typechecker_1.typecheck)(this);
}
this.autolink();
this.log('Propagating effect annotations...');
this.propagateEffects();
if (this.opts.printable) {
this.log('Annotating external links...');
this.annotateExternalLinks();
}
this.log('Linking xrefs...');
this._xrefs.forEach(xref => xref.build());
this.log('Linking non-terminal references...');
this._ntRefs.forEach(nt => nt.build());
this._emuMetasToRender.forEach(node => {
Meta_1.default.render(this, node);
});
this._emuMetasToRemove.forEach(node => {
node.replaceWith(...node.childNodes);
});
// TODO: make these look good
// this.log('Adding clause labels...');
// this.labelClauses();
if (this.opts.lintSpec) {
this._ntStringRefs.forEach(({ name, loc, node, namespace }) => {
if (this.biblio.byProductionName(name, namespace) == null) {
this.warn({
type: 'contents',
ruleId: 'undefined-nonterminal',
message: `could not find a definition for nonterminal ${name}`,
node,
nodeRelativeLine: loc.line,
nodeRelativeColumn: loc.column,
});
}
});
}
this.log('Linking production references...');
this._prodRefs.forEach(prod => prod.build());
this.log('Building reference graph...');
this.buildReferenceGraph();
this.highlightCode();
this.setMetaCharset();
this.setMetaViewport();
const wrapper = this.buildSpecWrapper();
if (this.opts.printable) {
this.log('Building covers and applying other print tweaks...');
const metadataEle = this.doc.querySelector('#metadata-block');
if (metadataEle) {
this.doc.querySelector('emu-intro').appendChild(metadataEle);
}
// front cover
const frontCover = document.createElement('div');
frontCover.classList.add('full-page-svg');
frontCover.setAttribute('id', 'front-cover');
const versionNode = this.doc.querySelector('h1.version');
if (versionNode != null) {
frontCover.append(versionNode);
}
// we know title & shortname exist because we enforce it in the constructor when using --printable
frontCover.append((_a = this.doc.querySelector('h1.title')) !== null && _a !== void 0 ? _a : '');
frontCover.append((_b = this.doc.querySelector('h1.shortname')) !== null && _b !== void 0 ? _b : '');
wrapper.before(frontCover);
// inside cover
const insideCover = document.createElement('div');
insideCover.classList.add('full-page-svg');
insideCover.setAttribute('id', 'inside-cover');
frontCover.after(insideCover);
}
let commonEles = [];
let tocJs = '';
if (this.opts.toc) {
this.log('Building table of contents...');
if (this.opts.printable) {
new Toc_1.default(this).build(2);
}
else {
({ js: tocJs, eles: commonEles } = (0, Menu_1.default)(this));
}
}
if (this.opts.printable) {
this.log('Applying tweaks for printable document...');
// The logo is present in ecma-262. We could consider removing it from the document instead of having this tweak.
const logo = document.getElementById('ecma-logo');
if (logo) {
if (logo.parentElement && logo.parentElement.tagName === 'P') {
logo.parentElement.remove();
}
else {
logo.remove();
}
}
(_c = this.doc
.querySelector('#spec-container > emu-clause:first-of-type')) === null || _c === void 0 ? void 0 : _c.before(this.doc.querySelector('h1.title').cloneNode(true));
}
else {
// apparently including this confuses Prince, even when it's `display: none`
this.log('Building shortcuts help dialog...');
commonEles.push(this.buildShortcutsHelp());
}
for (const ele of commonEles) {
this.doc.body.insertBefore(ele, this.doc.body.firstChild);
}
const jsContents = (await concatJs(sdoJs, tocJs)) + `\n;let usesMultipage = ${!!this.opts.multipage}`;
const jsSha = sha(jsContents);
await this.buildAssets(jsContents, jsSha);
if (this.opts.multipage) {
await this.buildMultipage(wrapper, commonEles);
}
const file = this.opts.multipage
? path.join(this.opts.outfile, 'index.html')
: (_d = this.opts.outfile) !== null && _d !== void 0 ? _d : null;
this.generatedFiles.set(file, this.toHTML());
return this;
}
labelClauses() {
const label = (clause) => {
var _a, _b;
if (clause.header != null) {
if (((_b = (_a = clause.signature) === null || _a === void 0 ? void 0 : _a.return) === null || _b === void 0 ? void 0 : _b.kind) === 'completion' &&
clause.signature.return.completionType !== 'normal') {
// style="border: 1px #B50000; background-color: #FFE6E6; padding: .2rem;border-radius: 5px;/*! color: white; */font-size: small;vertical-align: middle;/*! line-height: 1em; */border-style: dotted;color: #B50000;cursor: default;user-select: none;"
// TODO: make this a different color
clause.header.innerHTML += `<span class="clause-tag abrupt-tag" title="this can return an abrupt completion">abrupt</span>`;
}
// TODO: make this look like the [UC] annotation
// TODO: hide this if [UC] is not enabled (maybe)
// the querySelector is gross; you are welcome to replace it with a better analysis which actually keeps track of stuff
if (clause.node.querySelector('.e-user-code')) {
clause.header.innerHTML += `<span class="clause-tag user-code-tag" title="this can invoke user code">user code</span>`;
}
}
for (const sub of clause.subclauses) {
label(sub);
}
};
for (const sub of this.subclauses) {
label(sub);
}
}
// checks that AOs which do/don't return completion records are invoked appropriately
// also checks that the appropriate number of arguments are passed
toHTML() {
const htmlEle = this.doc.documentElement;
return '<!doctype html>\n' + (htmlEle.hasAttributes() ? htmlEle.outerHTML : htmlEle.innerHTML);
}
/** @internal */
locate(node) {
let pointer = node;
let dom;
let file;
let source;
search: {
while (pointer != null) {
if (this.topLevelImportedNodes.has(pointer)) {
const importNode = this.topLevelImportedNodes.get(pointer);
dom = importNode.dom;
file = importNode.importPath;
source = importNode.source;
break search;
}
pointer = pointer.parentElement;
}
// else
dom = this.dom;
}
const loc = dom.nodeLocation(node);
if (loc) {
// we can't just spread `loc` because not all properties are own/enumerable
const out = {
source: this.sourceText,
startTag: loc.startTag,
endTag: loc.endTag,
startOffset: loc.startOffset,
endOffset: loc.endOffset,
attrs: loc.attrs,
startLine: loc.startLine,
startCol: loc.startCol,
endLine: loc.endLine,
endCol: loc.endCol,
};
if (pointer != null) {
out.file = file;
out.source = source;
}
return out;
}
}
buildReferenceGraph() {
const refToClause = this.refsByClause;
const setParent = (node) => {
let pointer = node;
while (pointer && !['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'].includes(pointer.nodeName)) {
pointer = pointer.parentNode;
}
// @ts-ignore
if (pointer == null || pointer.id == null) {
// @ts-ignore
pointer = { id: 'sec-intro' };
}
// @ts-ignore
if (refToClause[pointer.id] == null) {
// @ts-ignore
refToClause[pointer.id] = [];
}
// @ts-ignore
refToClause[pointer.id].push(node.id);
};
let counter = 0;
this._xrefs.forEach(xref => {
let entry = xref.entry;
if (!entry || entry.namespace === 'external')
return;
if (!entry.id && entry.refId) {
entry = this.spec.biblio.byId(entry.refId);
}
if (!xref.id) {
const id = `_ref_${counter++}`;
xref.node.setAttribute('id', id);
xref.id = id;
}
setParent(xref.node);
entry.referencingIds.push(xref.id);
});
this._ntRefs.forEach(prod => {
const entry = prod.entry;
if (!entry || entry.namespace === 'external')
return;
// if this is the defining nt of an emu-production, don't create a ref
if (prod.node.parentNode.nodeName === 'EMU-PRODUCTION')
return;
const id = `_ref_${counter++}`;
prod.node.setAttribute('id', id);
setParent(prod.node);
entry.referencingIds.push(id);
});
}
readSectionId(ele) {
if (ele.id == null) {
this.warn({
type: 'node',
ruleId: 'top-level-section-id',
message: 'When using --multipage, top-level sections must have ids',
node: ele,
});
return undefined;
}
if (!ele.id.startsWith('sec-')) {
this.warn({
type: 'node',
ruleId: 'top-level-section-id',
message: 'When using --multipage, top-level sections must have ids beginning with `sec-`',
node: ele,
});
return undefined;
}
const name = ele.id.substring(4);
if (!/^[A-Za-z0-9-_]+$/.test(name)) {
this.warn({
type: 'node',
ruleId: 'top-level-section-id',
message: 'When using --multipage, top-level sections must have ids matching /^[A-Za-z0-9-_]+$/',
node: ele,
});
return undefined;
}
if (name.toLowerCase() === 'index') {
this.warn({
type: 'node',
ruleId: 'top-level-section-id',
message: 'When using --multipage, top-level sections must not be named "index"',
node: ele,
});
return undefined;
}
return name;
}
propagateEffects() {
for (const [effectName, worklist] of this._effectWorklist) {
this.propagateEffect(effectName, worklist);
}
}
propagateEffect(effectName, worklist) {
const usersOfAoid = new Map();
for (const xref of this._xrefs) {
if (xref.clause == null || xref.aoid == null)
continue;
if (!xref.shouldPropagateEffect(effectName))
continue;
if (xref.hasAddedEffect(effectName)) {
maybeAddClauseToEffectWorklist(effectName, xref.clause, worklist);
}
const usedAoid = xref.aoid;
if (!usersOfAoid.has(usedAoid)) {
usersOfAoid.set(usedAoid, new Set());
}
usersOfAoid.get(usedAoid).add(xref.clause);
}
while (worklist.length !== 0) {
const clause = worklist.shift();
const aoid = clause.aoid;
if (aoid == null || !usersOfAoid.has(aoid)) {
continue;
}
this._effectfulAOs.set(aoid, clause.effects);
for (const userClause of usersOfAoid.get(aoid)) {
maybeAddClauseToEffectWorklist(effectName, userClause, worklist);
}
}
}
/** @internal */
getEffectsByAoid(aoid) {
if (this._effectfulAOs.has(aoid)) {
return this._effectfulAOs.get(aoid);
}
return null;
}
annotateExternalLinks() {
for (const a of this.doc.querySelectorAll('a')) {
if (a.hostname !== '' && a.href !== a.textContent && a.protocol !== 'mailto:') {
a.setAttribute('data-print-href', '');
}
}
}
async buildMultipage(wrapper, commonEles) {
let stillIntro = true;
const introEles = [];
const sections = [];
const containedIdToSection = new Map();
const sectionToContainedIds = new Map();
const clauseTypes = ['EMU-ANNEX', 'EMU-CLAUSE'];
// @ts-ignore
for (const child of wrapper.children) {
let section;
if (stillIntro) {
if (clauseTypes.includes(child.nodeName)) {
throw new Error('cannot make multipage build without intro');
}
else if (child.nodeName !== 'EMU-INTRO') {
// anything before emu-intro is considered part of the introduction
introEles.push(child);
continue;
}
stillIntro = false;
if (child.id !== 'sec-intro') {
this.warn({
type: 'node',
ruleId: 'top-level-section-id',
message: 'When using --multipage, the introduction must have id "sec-intro"',
node: child,
});
continue;
}
const name = 'index';
introEles.push(child);
section = { name, eles: introEles };
}
else {
if (!clauseTypes.includes(child.nodeName)) {
throw new Error('non-clause children are not yet implemented: ' + child.nodeName);
}
const name = this.readSectionId(child);
if (name === undefined) {
continue;
}
section = { name, eles: [child] };
}
sections.push(section);
const contained = [];
sectionToContainedIds.set(section.name, contained);
for (const ele of section.eles) {
if (ele.id) {
contained.push(ele.id);
containedIdToSection.set(ele.id, section.name);
}
for (const item of ele.querySelectorAll('[id]')) {
contained.push(item.id);
containedIdToSection.set(item.id, section.name);
}
}
}
let htmlEle = '';
if (this.doc.documentElement.hasAttributes()) {
const clonedHtmlEle = this.doc.documentElement.cloneNode(false);
clonedHtmlEle.innerHTML = '';
const src = clonedHtmlEle.outerHTML;
htmlEle = src.substring(0, src.length - '<head></head><body></body></html>'.length);
}
const head = this.doc.head.cloneNode(true);
const containedMap = JSON.stringify(Object.fromEntries(sectionToContainedIds)).replace(/[\\`$]/g, '\\$&');
if (this.assets.type !== 'none') {
const multipageJsContents = `'use strict';
let multipageMap = JSON.parse(\`${containedMap}\`);
${await utils.readFile(path.join(__dirname, '../js/multipage.js'))}
`;
// assets are never internal for multipage builds
// @ts-expect-error
const multipageLocationOnDisk = path.join(this.assets.directory, 'js', 'multipage.js');
this.generatedFiles.set(multipageLocationOnDisk, multipageJsContents);
// the path will be rewritten below
// so it should initially be relative to outfile, not outfile/multipage
const multipageScript = this.doc.createElement('script');
multipageScript.src =
path.relative(this.opts.outfile, multipageLocationOnDisk) +
'?cache=' +
sha(multipageJsContents);
multipageScript.setAttribute('defer', '');
head.insertBefore(multipageScript, head.querySelector('script'));
}
for (const { name, eles } of sections) {
this.log(`Generating section ${name}...`);
const headClone = head.cloneNode(true);
const commonClone = commonEles.map(e => e.cloneNode(true));
const clones = eles.map(e => e.cloneNode(true));
const allClones = [headClone, ...commonClone, ...clones];
for (const anchor of allClones.flatMap(e => [...e.querySelectorAll('a')])) {
if (linkIsAbsolute(anchor)) {
continue;
}
if (linkIsInternal(anchor)) {
let p = anchor.hash.substring(1);
if (!containedIdToSection.has(p)) {
try {
p = decodeURIComponent(p);
}
catch {
// pass
}
if (!containedIdToSection.has(p)) {
this.warn({
type: 'node',
ruleId: 'multipage-link-target',
message: 'could not find appropriate section for ' + anchor.hash,
node: anchor,
});
continue;
}
}
const targetSec = containedIdToSection.get(p);
anchor.href = (targetSec === 'index' ? './' : targetSec + '.html') + anchor.hash;
}
else if (linkIsPathRelative(anchor)) {
anchor.href = path.relative('multipage', pathFromRelativeLink(anchor));
}
}
for (const link of allClones.flatMap(e => [...e.querySelectorAll('link')])) {
if (!linkIsAbsolute(link) && linkIsPathRelative(link)) {
link.href = path.relative('multipage', pathFromRelativeLink(link));
}
}
for (const img of allClones.flatMap(e => [...e.querySelectorAll('img')])) {
if (!/^(http:|https:|:|\/)/.test(img.src)) {
img.src = path.relative('multipage', img.src);
}
}
for (const script of allClones.flatMap(e => [...e.querySelectorAll('script')])) {
if (script.src != null && !/^(http:|https:|:|\/)/.test(script.src)) {
script.src = path.relative('multipage', script.src);
}
}
// prettier-ignore
for (const object of allClones.flatMap(e => [...e.querySelectorAll('object[data]')])) {
if (!/^(http:|https:|:|\/)/.test(object.data)) {
object.data = path.relative('multipage', object.data);
}
}
if (eles[0].hasAttribute('id')) {
const canonical = this.doc.createElement('link');
canonical.setAttribute('rel', 'canonical');
canonical.setAttribute('href', `../#${eles[0].id}`);
headClone.appendChild(canonical);
}
// @ts-expect-error
const commonHTML = commonClone.map(e => e.outerHTML).join('\n');
// @ts-expect-error
const clonesHTML = clones.map(e => e.outerHTML).join('\n');
const content = `<!doctype html>${htmlEle}\n${headClone.outerHTML}\n<body>${commonHTML}<div id='spec-container'>${clonesHTML}</div></body>`;
this.generatedFiles.set(path.join(this.opts.outfile, `multipage/${name}.html`), content);
}
}
async buildAssets(jsContents, jsSha) {
if (this.assets.type === 'none')
return;
this.log('Building assets...');
// check for very old manual 'ecmarkup.js'/'ecmarkup.css'
const oldEles = this.doc.querySelectorAll("script[src='ecmarkup.js'],link[href='ecmarkup.css']");
for (const item of oldEles) {
this.warn({
type: 'attr',
ruleId: 'old-script-or-style',
node: item,
attr: item.tagName === 'SCRIPT' ? 'src' : 'href',
message: 'ecmarkup will insert its own js/css; the input document should not include tags for them',
});
}
const FONT_FILE_CONTENTS = new Map((0, utils_2.zip)(FONT_FILES.values(), await Promise.all(Array.from(FONT_FILES.values()).map(fontFile => utils.readBinaryFile(path.join(__dirname, '..', 'fonts', fontFile))))));
const IMG_FILE_CONTENTS = new Map((0, utils_2.zip)(IMG_FILES.values(), await Promise.all(Array.from(IMG_FILES.values()).map(imgFile => utils.readBinaryFile(path.join(__dirname, '..', 'img', imgFile))))));
const [cssContents, printCssContents] = (await Promise.all(['../css/elements.css', '../css/print.css'].map(f => utils.readFile(path.join(__dirname, f))))).map(css => css
.replace(/^([ \t]*)src: +local\(([^)]+)\), +local\(([^)]+)\);$/gm, (match, indent, displayName, postScriptName) => {
var _a;
const fontFile = (_a = FONT_FILES.get(postScriptName)) !== null && _a !== void 0 ? _a : FONT_FILES.get(displayName);
if (fontFile == null) {
throw new Error(`Unrecognised font: ${JSON.stringify(postScriptName)}`);
}
const fontType = path.extname(fontFile).slice(1);
const urlRef = this.assets.type === 'inline'
? `data:font/${fontType};base64,${FONT_FILE_CONTENTS.get(fontFile).toString('base64')}`
: `../fonts/${fontFile}`;
return `${indent}src: local(${displayName}), local(${postScriptName}), url(${urlRef}) format('${fontType}');`;
})
.replace(/^([ \t]*)content: +url\(img\/([^)]+)\);$/gm, (match, indent, url) => {
if (!IMG_FILES.has(url)) {
throw new Error(`Unrecognised image: ${JSON.stringify(url)}`);
}
const imageType = path.extname(url).slice(1);
const urlRef = this.assets.type === 'inline'
? `data:image/${imageType};base64,${IMG_FILE_CONTENTS.get(url).toString('base64')}`
: `../img/${url}`;
return `${indent}content: url(${urlRef});`;
}));
if (this.assets.type === 'external') {
const outDir = this.opts.outfile
? this.opts.multipage
? this.opts.outfile
: path.dirname(this.opts.outfile)
: process.cwd();
const scriptLocationOnDisk = path.join(this.assets.directory, 'js', 'ecmarkup.js');
const styleLocationOnDisk = path.join(this.assets.directory, 'css', 'ecmarkup.css');
const printStyleLocationOnDisk = path.join(this.assets.directory, 'css', 'print.css');
this.generatedFiles.set(scriptLocationOnDisk, jsContents);
this.generatedFiles.set(styleLocationOnDisk, cssContents);
this.generatedFiles.set(printStyleLocationOnDisk, printCssContents);
for (const [, fontFile] of FONT_FILES) {
this.generatedFiles.set(path.join(this.assets.directory, 'fonts', fontFile), FONT_FILE_CONTENTS.get(fontFile));
}
for (const imgFile of IMG_FILES) {
this.generatedFiles.set(path.join(this.assets.directory, 'img', imgFile), IMG_FILE_CONTENTS.get(imgFile));
}
const script = this.doc.createElement('script');
script.src = path.relative(outDir, scriptLocationOnDisk) + '?cache=' + jsSha;
script.setAttribute('defer', '');
this.doc.head.appendChild(script);
this.addStyle(this.doc.head, path.relative(outDir, printStyleLocationOnDisk), this.opts.printable ? undefined : 'print');
this.addStyle(this.doc.head, path.relative(outDir, styleLocationOnDisk));
}
else {
// i.e. assets.type === 'inline'
this.log('Inlining JavaScript assets...');
const script = this.doc.createElement('script');
script.textContent = jsContents;
this.doc.head.appendChild(script);
this.log('Inlining CSS assets...');
const style = this.doc.createElement('style');
style.textContent = cssContents;
this.doc.head.appendChild(style);
const printStyle = this.doc.createElement('style');
if (this.opts.printable) {
printStyle.textContent = printCssContents;
}
else {
printStyle.textContent = `@media print {\n${printCssContents}\n}`;
}
this.doc.head.appendChild(printStyle);
}
const currentYearStyle = this.doc.createElement('style');
currentYearStyle.textContent = `
@media print {
@page :left {
@bottom-right {
content: '© Ecma International ${this.opts.date.getFullYear()}';
}
}
@page :right {
@bottom-left {
content: '© Ecma International ${this.opts.date.getFullYear()}';
}
}
@page :first {
@bottom-left {
content: '';
}
@bottom-right {
content: '';
}
}
@page :blank {
@bottom-left {
content: '';
}
@bottom-right {
content: '';
}
}
}
`;
this.doc.head.appendChild(currentYearStyle);
const solarizedStyle = this.doc.createElement('style');
solarizedStyle.textContent = `
@import url("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${hljs.versionString}/styles/base16/solarized-light.min.css");
@import url("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${hljs.versionString}/styles/a11y-dark.min.css") (prefers-color-scheme: dark);
`;
this.doc.head.appendChild(solarizedStyle);
if (this.opts.shortname === 'ECMA-262' && this.opts.status === 'draft') {
const festiveStyleContents = await utils.readFile(path.join(__dirname, '..', 'css', 'festive.css'));
const festiveScript = this.doc.createElement('script');
festiveScript.innerHTML = `
{
let d = new Date;
if (d.getMonth() === 3 && d.getDate() === 1) {
let s = document.createElement('style');
s.id = 'bd75b99add5f';
s.innerHTML = atob(${JSON.stringify(btoa(festiveStyleContents))});
document.head.appendChild(s);
}
}
`;
this.doc.head.appendChild(festiveScript);
}
}
addStyle(head, href, media) {
const style = this.doc.createElement('link');
style.setAttribute('rel', 'stylesheet');
style.setAttribute('href', href);
if (media != null) {
style.setAttribute('media', media);
}
// insert early so that the document's own stylesheets can override
const firstLink = head.querySelector('link[rel=stylesheet], style');
if (firstLink != null) {
head.insertBefore(style, firstLink);
}
else {
head.appendChild(style);
}
}
buildSpecWrapper() {
const elements = this.doc.body.childNodes;
const wrapper = this.doc.createElement('div');
wrapper.id = 'spec-container';
while (elements.length > 0) {
wrapper.appendChild(elements[0]);
}
this.doc.body.appendChild(wrapper);
return wrapper;
}
buildShortcutsHelp() {
const shortcutsHelp = this.doc.createElement('div');
shortcutsHelp.setAttribute('id', 'shortcuts-help');
shortcutsHelp.innerHTML = `
<ul>
<li><span>Toggle shortcuts help</span><code>?</code></li>
<li><span>Toggle "can call user code" annotations</span><code>u</code></li>
${this.opts.multipage ? `<li><span>Navigate to/from multipage</span><code>m</code></li>` : ''}
<li><span>Jump to search box</span><code>/</code></li>
<li><span>Toggle pinning of the current clause</span><code>p</code></li>
<li><span>Jump to the <i>n</i><sup>th</sup> pin</span><code>1-9</code></li>
<li><span>Jump to the 10<sup>th</sup> pin</span><code>0</code></li>
<li><span>Jump to the most recent link target</span><code>\`</code></li>
</ul>`;
return shortcutsHelp;
}
processMetadata() {
const block = this.doc.querySelector('pre.metadata');
if (!block || !block.parentNode) {
return;
}
let data;
try {
data = yaml.safeLoad(block.textContent);
}
catch (e) {
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') {
this.warn({
type: 'contents',
ruleId: 'invalid-metadata',
message: `metadata block failed to parse: ${e.reason}`,
node: block,
nodeRelativeLine: e.mark.line + 1,
nodeRelativeColumn: e.mark.column + 1,
});
}
else {
this.warn({
type: 'node',
ruleId: 'invalid-metadata',
message: 'metadata block failed to parse',
node: block,
});
}
return;
}
finally {
block.parentNode.removeChild(block);
}
Object.assign(this.opts, data);
}
async loadBiblios() {
var _a, _b;
this.cancellationToken.throwIfCancellationRequested();
const biblioPaths = [];
for (const biblioEle of this.doc.querySelectorAll('emu-biblio')) {
const href = biblioEle.getAttribute('href');
biblioEle.remove();
if (href == null) {
this.spec.warn({
type: 'node',
node: biblioEle,
ruleId: 'biblio-href',
message: 'emu-biblio elements must have an href attribute',
});
}
else {
biblioPaths.push(href);
}
}
const biblioContents = await Promise.all(biblioPaths.map(p => this.fetch(path.join(this.rootDir, p))));
const biblios = biblioContents.flatMap(c => JSON.parse(c));
for (const biblio of biblios.concat((_a = this.opts.extraBiblios) !== null && _a !== void 0 ? _a : [])) {
if ((biblio === null || biblio === void 0 ? void 0 : biblio.entries) == null) {
let message = Object.keys(biblio !== null && biblio !== void 0 ? biblio : {}).some(k => k.startsWith('http'))
? 'This is an old-style biblio.'
: 'Biblio does not appear to be in the correct format, are you using an old-style biblio?';
message += ' You will need to update it to work with versions of ecmarkup >= 12.0.0.';
throw new Error(message);
}
this.biblio.addExternalBiblio(biblio);
for (const entry of biblio.entries) {
if (entry.type === 'op' && ((_b = entry.effects) === null || _b === void 0 ? void 0 : _b.length) > 0) {
this._effectfulAOs.set(entry.aoid, entry.effects);
for (const effect of entry.effects) {
if (!this._effectWorklist.has(effect)) {
this._effectWorklist.set(effect, []);
}
this._effectWorklist.get(effect).push(entry);
}
}
}
}
}
async loadImports() {
const imports = this.doc.body.querySelectorAll('emu-import');
for (let i = 0; i < imports.length; i++) {
await (0, Import_1.buildImports)(this, imports[i], this.rootDir);
}
// we've already removed biblio elements in the main document in loadBiblios
// so any which are here now were in an import, which is illegal
for (const biblioEle of this.doc.querySelectorAll('emu-biblio')) {
this.warn({
type: 'node',
node: biblioEle,
ruleId: 'biblio-in-import',
message: 'emu-biblio elements cannot be used within emu-imports',
});
}
}
exportBiblio() {
if (!this.opts.location) {
this.warn({
type: 'global',
ruleId: 'no-location',
message: "no spec location specified; biblio not generated. try setting the location in the document's metadata block",
});
return null;
}
return this.biblio.export();
}
highlightCode() {
this.log('Highlighting syntax...');
const codes = this.doc.querySelectorAll('pre code');
for (let i = 0; i < codes.length; i++) {
const classAttr = codes[i].getAttribute('class');
if (!classAttr)
continue;
const language = classAttr.replace(/lang(uage)?-/, '');
let input = codes[i].textContent;
// remove leading and trailing blank lines
input = input.replace(/^(\s*[\r\n])+|([\r\n]\s*)+$/g, '');
// remove base inden based on indent of first non-blank line
const baseIndent = input.match(/^\s*/) || '';
const baseIndentRe = new RegExp('^' + baseIndent, 'gm');
input = input.replace(baseIndentRe, '');
// @ts-expect-error the type definitions for highlight.js are broken
const result = hljs.highlight(input, { language });
codes[i].innerHTML = result.value;
codes[i].setAttribute('class', classAttr + ' hljs');
}
}
buildBoilerplate() {