ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
338 lines (337 loc) • 13.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const Builder_1 = require("./Builder");
const utils_1 = require("./utils");
class Xref extends Builder_1.default {
constructor(spec, node, clause, namespace, href, aoid) {
super(spec, node);
this.namespace = namespace;
this.href = href;
this.aoid = aoid;
this.clause = clause;
this.id = node.getAttribute('id');
this.isInvocation = node.hasAttribute('is-invocation');
node.removeAttribute('is-invocation');
// Check if there's metadata adding or suppressing effects
this.addEffects = null;
this.suppressEffects = null;
if (node.parentElement &&
node.parentElement.tagName === 'EMU-META' &&
node.parentElement.children[0] === node) {
if (node.parentElement.hasAttribute('effects')) {
const addEffects = node.parentElement
.getAttribute('effects')
.split(',')
.map(e => e.trim());
if (addEffects.length !== 0) {
this.addEffects = (0, utils_1.validateEffects)(spec, addEffects, node.parentElement);
}
}
if (node.parentElement.hasAttribute('suppress-effects')) {
const suppressEffects = node.parentElement
.getAttribute('suppress-effects')
.split(',')
.map(e => e.trim());
if (suppressEffects.length !== 0) {
this.suppressEffects = (0, utils_1.validateEffects)(spec, suppressEffects, node.parentElement);
}
}
if (this.addEffects !== null && this.suppressEffects !== null) {
for (const e of this.addEffects) {
if (this.suppressEffects.includes(e)) {
throw new Error('effect suppression is contradictory');
}
}
for (const e of this.suppressEffects) {
if (this.addEffects.includes(e)) {
throw new Error('effect suppression is contradictory');
}
}
}
spec._emuMetasToRender.delete(node.parentElement);
spec._emuMetasToRemove.add(node.parentElement);
}
// Invocations prefixed with ! supress the 'user-code' effect by
// default. There are exceptions, but they should manually add the effect back.
if (node.hasAttribute('no-abrupt-completion')) {
if (this.addEffects === null || !this.addEffects.includes('user-code')) {
if (this.suppressEffects === null) {
this.suppressEffects = ['user-code'];
}
else {
this.suppressEffects.push('user-code');
}
}
node.removeAttribute('no-abrupt-completion');
}
}
shouldPropagateEffect(effectName) {
if (!this.isInvocation)
return false;
if (this.clause) {
if (!(0, utils_1.doesEffectPropagateToParent)(this.node, effectName)) {
return false;
}
if (!this.clause.canHaveEffect(effectName)) {
return false;
}
}
if (this.suppressEffects !== null) {
return !this.suppressEffects.includes(effectName);
}
return true;
}
hasAddedEffect(effectName) {
if (!this.isInvocation)
return false;
if (this.addEffects !== null) {
return this.addEffects.includes(effectName);
}
return false;
}
static async enter({ node, spec, clauseStack }) {
const href = node.getAttribute('href');
const aoid = node.getAttribute('aoid');
const parentClause = clauseStack[clauseStack.length - 1];
let namespace;
if (node.hasAttribute('namespace')) {
namespace = node.getAttribute('namespace');
}
else {
namespace = parentClause ? parentClause.namespace : spec.namespace;
}
if (href && aoid) {
spec.warn({
type: 'node',
ruleId: 'invalid-xref',
message: "xref can't have both href and aoid",
node,
});
return;
}
if (!href && !aoid) {
spec.warn({
type: 'node',
ruleId: 'invalid-xref',
message: 'xref has neither href nor aoid',
node,
});
return;
}
const xref = new Xref(spec, node, parentClause, namespace, href, aoid);
spec._xrefs.push(xref);
}
/** @internal */
build() {
const spec = this.spec;
const href = this.href;
const node = this.node;
const aoid = this.aoid;
const namespace = this.namespace;
if (href) {
if (href[0] !== '#') {
spec.warn({
type: 'attr-value',
attr: 'href',
ruleId: 'invalid-xref',
message: `xref to anything other than a fragment id is not supported (is ${JSON.stringify(href)}). try href="#sec-id" instead`,
node: this.node,
});
return;
}
const id = href.slice(1);
this.entry = spec.biblio.byId(id);
if (!this.entry) {
spec.warn({
type: 'attr-value',
attr: 'href',
ruleId: 'xref-not-found',
message: `can't find clause, production, note or example with id ${JSON.stringify(id)}`,
node: this.node,
});
return;
}
switch (this.entry.type) {
case 'clause':
buildClauseLink(node, this.entry);
break;
case 'production':
buildProductionLink(node, this.entry);
break;
case 'example':
buildFigureLink(spec, this.clause, node, this.entry, 'Example');
break;
case 'note':
buildFigureLink(spec, this.clause, node, this.entry, 'Note');
break;
case 'table':
buildFigureLink(spec, this.clause, node, this.entry, 'Table');
break;
case 'figure':
buildFigureLink(spec, this.clause, node, this.entry, 'Figure');
break;
case 'term':
buildTermLink(node, this.entry);
break;
case 'step':
buildStepLink(spec, node, this.entry);
break;
default: {
spec.warn({
type: 'node',
ruleId: 'unknown-biblio',
message: `found unknown biblio entry ${this.entry.type} (this is a bug, please file it with ecmarkup)`,
node: this.node,
});
}
}
}
else if (aoid) {
this.entry = spec.biblio.byAoid(aoid, namespace);
if (this.entry) {
let effects = null;
let classNames = null;
if (this.spec.opts.markEffects) {
if (this.isInvocation) {
effects = spec.getEffectsByAoid(aoid);
}
if (this.addEffects !== null) {
if (effects !== null) {
effects = effects.concat(...this.addEffects);
}
else {
effects = this.addEffects;
}
}
if (effects) {
if (this.suppressEffects !== null) {
effects = effects.filter(e => !this.suppressEffects.includes(e));
}
if (effects.length !== 0) {
const parentClause = this.clause;
effects = parentClause ? effects.filter(e => parentClause.canHaveEffect(e)) : effects;
if (effects.length !== 0) {
classNames = effects.map(e => `e-${e}`).join(' ');
}
}
}
}
buildAOLink(node, this.entry, classNames);
return;
}
const namespaceSuffix = namespace === '<no location>' ? '' : ` in namespace ${JSON.stringify(namespace)}`;
spec.warn({
type: 'attr-value',
attr: 'aoid',
ruleId: 'xref-not-found',
message: `can't find abstract op with aoid ${JSON.stringify(aoid)}` + namespaceSuffix,
node: this.node,
});
}
}
}
Xref.elements = ['EMU-XREF'];
exports.default = Xref;
function buildClauseLink(xref, entry) {
if (xref.textContent.trim() === '') {
if (xref.hasAttribute('title')) {
// titleHTML might not be present from older biblio files.
xref.innerHTML = buildXrefLink(entry, entry.titleHTML || entry.title);
}
else {
xref.innerHTML = buildXrefLink(entry, entry.number);
}
}
else {
xref.innerHTML = buildXrefLink(entry, xref.innerHTML);
}
}
function buildProductionLink(xref, entry) {
if (xref.textContent.trim() === '') {
xref.innerHTML = buildXrefLink(entry, '<emu-nt>' + entry.name + '</emu-nt>');
}
else {
xref.innerHTML = buildXrefLink(entry, xref.innerHTML);
}
}
function buildAOLink(xref, entry, classNames) {
if (xref.textContent.trim() === '') {
xref.innerHTML = buildXrefLink(entry, xref.getAttribute('aoid'), classNames);
}
else {
xref.innerHTML = buildXrefLink(entry, xref.innerHTML, classNames);
}
}
function buildTermLink(xref, entry) {
if (xref.textContent.trim() === '') {
xref.innerHTML = buildXrefLink(entry, entry.term);
}
else {
xref.innerHTML = buildXrefLink(entry, xref.innerHTML);
}
}
function buildFigureLink(spec, parentClause, xref, entry, type) {
if (xref.textContent.trim() === '') {
if (entry.clauseId) {
// first need to find the associated clause
const clauseEntry = spec.biblio.byId(entry.clauseId);
if ((clauseEntry === null || clauseEntry === void 0 ? void 0 : clauseEntry.type) !== 'clause') {
throw new Error(`${type} with id ${entry.id} has a \`clauseId\` which does not correspond to a clause - this should be impossible; please file an issue on ecmarkup`);
}
if (parentClause && parentClause.id === clauseEntry.id) {
xref.innerHTML = buildXrefLink(entry, type + ' ' + entry.number);
}
else {
if (xref.hasAttribute('title')) {
xref.innerHTML = buildXrefLink(entry, clauseEntry.title + ' ' + type + ' ' + entry.number);
}
else {
xref.innerHTML = buildXrefLink(entry, clauseEntry.number + ' ' + type + ' ' + entry.number);
}
}
}
else {
xref.innerHTML = buildXrefLink(entry, type + ' ' + entry.number);
}
}
else {
xref.innerHTML = buildXrefLink(entry, xref.innerHTML);
}
}
const decimalBullet = Array.from({ length: 100 }).map((a, i) => '' + (i + 1));
const alphaBullet = Array.from({ length: 26 }).map((a, i) => String.fromCharCode('a'.charCodeAt(0) + i));
// prettier-ignore
const romanBullet = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix', 'x', 'xi', 'xii', 'xiii', 'xiv', 'xv', 'xvi', 'xvii', 'xviii', 'xix', 'xx', 'xxi', 'xxii', 'xxiii', 'xxiv', 'xxv'];
const bullets = [decimalBullet, alphaBullet, romanBullet, decimalBullet, alphaBullet, romanBullet];
function buildStepLink(spec, xref, entry) {
if (xref.innerHTML !== '') {
spec.warn({
type: 'contents',
ruleId: 'step-xref-contents',
message: 'the contents of emu-xrefs to steps are ignored',
node: xref,
nodeRelativeLine: 1,
nodeRelativeColumn: 1,
});
}
const stepBullets = entry.stepNumbers.map((s, i) => {
const applicable = bullets[Math.min(i, 5)];
if (s > applicable.length) {
spec.warn({
type: 'attr-value',
ruleId: 'high-step-number',
message: `ecmarkup does not know how to deal with step numbers as high as ${s}; if you need this, open an issue on ecmarkup`,
node: xref,
attr: 'href',
});
return '?';
}
return applicable[s - 1];
});
const text = stepBullets.join('.');
xref.innerHTML = buildXrefLink(entry, text);
}
function buildXrefLink(entry, contents, classNames = null) {
const classSnippet = classNames == null ? '' : ' class="' + classNames + '"';
return `<a href="${entry.location}#${entry.id || entry.refId}"${classSnippet}>${contents}</a>`;
}
;