ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
332 lines (274 loc) • 9.32 kB
JavaScript
'use strict';
const Path = require('path');
const yaml = require('js-yaml');
const utils = require('./utils');
const hljs = require('highlight.js');
// Builders
const Import = require('./Import');
const Clause = require('./Clause');
const ClauseNumbers = require('./clauseNums');
const Algorithm = require('./Algorithm');
const Dfn = require('./Dfn');
const Note = require('./Note');
const Toc = require('./Toc');
const Menu = require('./Menu');
const Production = require('./Production');
const ProdRef = require('./ProdRef');
const Grammar = require('./Grammar');
const Xref = require('./Xref');
const Eqn = require('./Eqn');
const Figure = require('./Figure');
const NO_CLAUSE_AUTOLINK = ['PRE', 'CODE', 'EMU-CLAUSE', 'EMU-ALG', 'EMU-PRODUCTION', 'EMU-GRAMMAR', 'EMU-XREF', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'EMU-EQN'];
const clauseTextNodesUnder = utils.textNodesUnder(NO_CLAUSE_AUTOLINK);
const NO_ALG_AUTOLINK = ['PRE', 'CODE', 'EMU-XREF'];
const algTextNodesUnder = utils.textNodesUnder(NO_ALG_AUTOLINK);
module.exports = class Spec {
constructor (rootPath, fetch, doc, opts) {
opts = opts || {};
this.spec = this;
this.opts = {};
this.rootPath = rootPath;
this.rootDir = Path.dirname(this.rootPath);
this.doc = doc;
this.fetch = fetch;
this.subclauses = [];
this._numberer = ClauseNumbers.iterator();
this._figureCounts = {
table: 0,
figure: 0
};
this.externalBiblio = {
clauses: {},
ops: {},
productions: {},
terms: {},
examples: {},
notes: {},
tables: {},
figures: {}
};
this.biblio = {
clauses: {},
ops: {},
productions: {},
terms: {},
examples: {},
notes: {},
tables: {},
figures: {}
};
this._prodsByName = {};
this.processMetadata();
assign(this.opts, opts);
if (!this.opts.hasOwnProperty('toc')) {
this.opts.toc = true;
}
}
buildAll(selector, Builder, opts) {
opts = opts || {};
this._log('Building ' + selector + '...');
let elems;
if (typeof selector === 'string') {
elems = this.doc.querySelectorAll(selector);
} else {
elems = selector;
}
const builders = [];
const ps = [];
for (let i = 0; i < elems.length; i++) {
const b = new Builder(this, elems[i]);
builders.push(b);
}
if (opts.buildArgs) {
for (let i = 0; i < elems.length; i++) {
ps.push(builders[i].build.apply(builders[i], opts.buildArgs));
}
} else {
for (let i = 0; i < elems.length; i++) {
ps.push(builders[i].build());
}
}
return Promise.all(ps);
}
build() {
let p = this.buildAll('emu-import', Import)
.then(this.loadES6Biblio.bind(this))
.then(this.loadBiblios.bind(this))
.then(this.buildAll.bind(this, 'emu-clause, emu-intro, emu-annex', Clause))
.then(this.buildAll.bind(this, 'emu-grammar', Grammar))
.then(this.buildAll.bind(this, 'emu-eqn', Eqn))
.then(this.buildAll.bind(this, 'emu-alg', Algorithm))
.then(this.buildAll.bind(this, 'emu-production', Production))
.then(this.buildAll.bind(this, 'emu-prodref', ProdRef))
.then(this.buildAll.bind(this, 'emu-figure, emu-table', Figure))
.then(this.buildAll.bind(this, 'dfn', Dfn))
.then(this.highlightCode.bind(this))
.then(this.autolink.bind(this))
.then(this.buildAll.bind(this, 'emu-xref', Xref));
if (this.opts.toc) {
p = p.then(function() {
this._log('Building table of contents...');
let toc;
if (this.opts.oldToc) {
toc = new Toc(this);
} else {
toc = new Menu(this);
}
return toc.build();
}.bind(this));
}
return p.then(function () { return this; }.bind(this), function (err) {
process.stderr.write(err.stack);
});
}
toHTML() {
return '<!doctype html>\n' + this.doc.documentElement.innerHTML;
}
processMetadata() {
const block = this.doc.querySelector('pre.metadata');
if (!block) {
return;
}
let data;
try {
data = yaml.safeLoad(block.textContent);
} catch (e) {
console.log('Warning: metadata block failed to parse');
return;
} finally {
block.parentNode.removeChild(block);
}
assign(this.opts, data);
}
loadES6Biblio() {
this._log('Loading biblios...');
return this.fetch(Path.join(__dirname, '../es6biblio.json'))
.then(function (es6bib) {
importBiblio(this.externalBiblio, JSON.parse(es6bib));
}.bind(this));
}
loadBiblios() {
const spec = this;
const bibs = Array.prototype.slice.call(this.doc.querySelectorAll('emu-biblio'));
return Promise.all(
bibs.map(function (bib) {
const path = Path.join(spec.rootDir, bib.getAttribute('href'));
return spec.fetch(path)
.then(function (contents) {
importBiblio(spec.externalBiblio, JSON.parse(contents));
});
})
);
}
exportBiblio() {
if (!this.opts.location) {
console.log('Warning: no spec location specified. Biblio not generated. Try --location or setting the location in the document\'s metadata block.');
return {};
}
const biblio = {};
biblio[this.opts.location] = this.biblio;
return biblio;
}
getNextClauseNumber(depth, isAnnex) {
return this._numberer.next(depth, isAnnex).value;
}
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 lang = classAttr.replace(/lang(uage)?\-/, '');
const result = hljs.highlight(lang, codes[i].textContent);
codes[i].innerHTML = result.value;
codes[i].setAttribute('class', classAttr + ' hljs');
}
}
autolink() {
this._log('Autolinking terms and abstract ops...');
const termlinkmap = {};
const autolinkmap = {};
Object.keys(this.externalBiblio.terms).forEach(function (term) {
autolinkmap[term] = this.externalBiblio.terms[term].id;
termlinkmap[term] = this.externalBiblio.terms[term].id;
}, this);
Object.keys(this.externalBiblio.ops).filter(noCommonAbstractOps).forEach(function (op) {
autolinkmap[op] = this.externalBiblio.ops[op].id;
}, this);
Object.keys(this.biblio.terms).forEach(function (term) {
autolinkmap[term] = this.biblio.terms[term].id;
termlinkmap[term] = this.biblio.terms[term].id;
}, this);
Object.keys(this.biblio.ops).filter(noCommonAbstractOps).forEach(function (op) {
autolinkmap[op] = this.biblio.ops[op].id;
}, this);
const clauseReplacer = new RegExp(Object.keys(autolinkmap)
.sort(function (a, b) { return b.length - a.length; })
.map(function (k) { return '\\b' + k + '\\b'; })
.join('|'), 'g');
const algReplacer = new RegExp(Object.keys(termlinkmap)
.sort(function (a, b) { return b.length - a.length; })
.map(function (k) { return '\\b' + k + '\\b'; })
.join('|'), 'g');
autolinkWalk(clauseReplacer, autolinkmap, this, this);
const algs = this.doc.getElementsByTagName('emu-alg');
for (let i = 0; i < algs.length; i++) {
const alg = algs[i];
autolink(algReplacer, termlinkmap, this, alg);
}
}
lookupBiblioEntryById(id) {
const types = ['clause', 'production', 'example', 'note', 'figure', 'table'];
for (let i = 0; i < types.length; i++) {
const type = types[i];
const entry = this.spec.biblio[type + 's'][id] || this.spec.externalBiblio[type + 's'][id];
if (entry) {
return { type: type, entry: entry };
}
}
return null;
}
_log() {
if (!this.opts.verbose) return;
console.log.apply(console, arguments);
}
};
function importBiblio(target, source) {
Object.keys(source).forEach(function (site) {
Object.keys(source[site]).forEach(function (type) {
Object.keys(source[site][type]).forEach(function (entry) {
target[type][entry] = source[site][type][entry];
target[type][entry].location = site;
});
});
});
}
function autolinkWalk(replacer, autolinkmap, spec, rootClause) {
rootClause.subclauses.forEach(function (subclause) {
autolink(replacer, autolinkmap, spec, subclause.node, subclause.id);
autolinkWalk(replacer, autolinkmap, spec, subclause);
});
}
function autolink(replacer, autolinkmap, spec, node, parentId) {
const textNodes = clauseTextNodesUnder(node);
for (let i = 0; i < textNodes.length; i++) {
const node = textNodes[i];
const template = spec.doc.createElement('template');
template.innerHTML = node.textContent.replace(replacer, function (match) {
const entry = autolinkmap[match];
if (!entry || entry === parentId) {
return match;
}
return '<emu-xref href="#' + entry + '">' + match + '</emu-xref>';
});
utils.replaceTextNode(node, template);
}
}
function noCommonAbstractOps(op) {
return op !== 'Call' && op !== 'Set' && op !== 'Type' && op !== 'UTC';
}
function assign(target, source) {
Object.keys(source).forEach(function (k) {
target[k] = source[k];
});
}