ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
380 lines (379 loc) • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SPECIAL_KINDS = exports.SPECIAL_KINDS_MAP = void 0;
exports.extractStructuredHeader = extractStructuredHeader;
const type_parser_1 = require("./type-parser");
const Builder_1 = require("./Builder");
const header_parser_1 = require("./header-parser");
const utils_1 = require("./utils");
const aoidTypes = [
'abstract operation',
'sdo',
'syntax-directed operation',
'host-defined abstract operation',
'implementation-defined abstract operation',
'numeric method',
];
exports.SPECIAL_KINDS_MAP = new Map([
['normative-optional', 'Normative Optional'],
['legacy', 'Legacy'],
['deprecated', 'Deprecated'],
]);
exports.SPECIAL_KINDS = [...exports.SPECIAL_KINDS_MAP.keys()];
function extractStructuredHeader(header) {
const dl = (0, utils_1.traverseWhile)(header.nextElementSibling, 'nextElementSibling', el => el.nodeName === 'DEL');
if (dl == null || dl.tagName !== 'DL' || !dl.classList.contains('header')) {
return null;
}
return dl;
}
class Clause extends Builder_1.default {
constructor(spec, node, parent, number) {
super(spec, node);
this.parentClause = parent;
this.id = node.getAttribute('id');
this.number = number;
this.subclauses = [];
this.notes = [];
this.editorNotes = [];
this.examples = [];
this.effects = [];
this.skipGlobalChecks = false;
this.skipReturnChecks = false;
this.isAnnex = node.nodeName === 'EMU-ANNEX';
this.isBackMatter = this.isAnnex && node.hasAttribute('back-matter');
this.isNormative = !this.isAnnex || node.hasAttribute('normative');
// namespace is either the entire spec or the parent clause's namespace.
let parentNamespace = spec.namespace;
if (parent) {
parentNamespace = parent.namespace;
}
if (node.hasAttribute('namespace')) {
this.namespace = node.getAttribute('namespace');
spec.biblio.createNamespace(this.namespace, parentNamespace);
}
else {
this.namespace = parentNamespace;
}
this.aoid = node.getAttribute('aoid');
if (this.aoid === '') {
// <emu-clause id=foo aoid> === <emu-clause id=foo aoid=foo>
this.aoid = node.id;
}
this.type = node.getAttribute('type');
if (this.type === '') {
this.type = null;
}
this.signature = null;
const header = (0, utils_1.traverseWhile)(this.node.firstElementChild, 'nextElementSibling',
// skip <del> and oldids
el => el.nodeName === 'DEL' || (el.nodeName === 'SPAN' && el.children.length === 0));
let headerH1 = (0, utils_1.traverseWhile)(header, 'firstElementChild', el => el.nodeName === 'INS', {
once: true,
});
if (headerH1 == null) {
this.spec.warn({
type: 'node',
ruleId: 'missing-header',
message: `could not locate header element`,
node: this.node,
});
headerH1 = null;
}
else if (headerH1.tagName !== 'H1') {
this.spec.warn({
type: 'node',
ruleId: 'missing-header',
message: `could not locate header element; found <${header.tagName.toLowerCase()}> before any <h1>`,
node: header,
});
headerH1 = null;
}
else {
this.buildStructuredHeader(headerH1, header);
}
this.header = headerH1;
if (headerH1 == null) {
this.title = 'UNKNOWN';
this.titleHTML = 'UNKNOWN';
}
}
/** @internal */ buildStructuredHeader(header, headerSurrogate = header) {
const dl = extractStructuredHeader(headerSurrogate);
if (dl === null) {
return;
}
// if we find such a DL, treat this as a structured header
const type = this.type;
let headerSource;
const headerLocation = this.spec.locate(header);
if (headerLocation != null) {
headerSource = headerLocation.source.slice(headerLocation.startTag.endOffset, headerLocation.endTag.startOffset);
}
else {
headerSource = header.innerHTML;
}
const parseResult = (0, header_parser_1.parseH1)(headerSource);
if (parseResult.type !== 'failure') {
try {
this.signature = parsedHeaderToSignature(parseResult);
}
catch (e) {
if (e instanceof type_parser_1.ParseError) {
const { line, column } = (0, utils_1.offsetToLineAndColumn)(headerSource, e.offset);
this.spec.warn({
type: 'contents',
ruleId: 'type-parsing',
message: e.message,
node: header,
nodeRelativeLine: line,
nodeRelativeColumn: column,
});
}
else {
throw e;
}
}
}
const { name, formattedHeader, formattedParams, formattedReturnType } = (0, header_parser_1.formatHeader)(this.spec, header, parseResult);
if (type === 'numeric method' && name != null && !name.includes('::')) {
this.spec.warn({
type: 'contents',
ruleId: 'numeric-method-for',
message: 'numeric methods should be of the form `Type::operation`',
node: header,
nodeRelativeLine: 1,
nodeRelativeColumn: 1,
});
}
if (type === 'sdo' && (formattedHeader !== null && formattedHeader !== void 0 ? formattedHeader : header.innerHTML).includes('(')) {
// SDOs are rendered without parameter lists in the header, for the moment
const currentHeader = formattedHeader !== null && formattedHeader !== void 0 ? formattedHeader : header.innerHTML;
header.innerHTML = (currentHeader.substring(0, currentHeader.indexOf('(')) +
currentHeader.substring(currentHeader.lastIndexOf(')') + 1)).trim();
if (header.children.length === 1 &&
['INS', 'DEL', 'MARK'].includes(header.children[0].tagName)) {
header.children[0].innerHTML = header.children[0].innerHTML.trim();
}
}
else if (formattedHeader != null) {
header.innerHTML = formattedHeader;
}
const { description, for: _for, effects, redefinition, skipGlobalChecks, skipReturnChecks, } = (0, header_parser_1.parseStructuredHeaderDl)(this.spec, type, dl);
const paras = (0, header_parser_1.formatPreamble)(this.spec, this.node, dl, type, name !== null && name !== void 0 ? name : 'UNKNOWN', formattedParams !== null && formattedParams !== void 0 ? formattedParams : 'UNPARSEABLE ARGUMENTS', formattedReturnType, _for, description);
dl.replaceWith(...paras);
if (!redefinition) {
if (this.node.hasAttribute('aoid')) {
this.spec.warn({
type: 'attr',
ruleId: 'header-format',
message: `nodes with structured headers should not include an AOID`,
node: this.node,
attr: 'aoid',
});
}
else if (name != null && type != null && aoidTypes.includes(type)) {
this.node.setAttribute('aoid', name);
this.aoid = name;
}
}
this.skipGlobalChecks = skipGlobalChecks;
this.skipReturnChecks = skipReturnChecks;
this.effects.push(...effects);
for (const effect of effects) {
if (!this.spec._effectWorklist.has(effect)) {
this.spec._effectWorklist.set(effect, []);
}
this.spec._effectWorklist.get(effect).push(this);
}
}
/** @internal */ buildNotes() {
if (this.notes.length === 1) {
this.notes[0].build();
}
else {
// pass along note index
this.notes.forEach((note, i) => {
note.build(i + 1);
});
}
this.editorNotes.forEach(note => note.build());
}
/** @internal */ buildExamples() {
if (this.examples.length === 1) {
this.examples[0].build();
}
else {
// pass along example index
this.examples.forEach((example, i) => {
example.build(i + 1);
});
}
}
canHaveEffect(effectName) {
// The following effects are runtime only:
//
// user-code: Only runtime can call user code.
if (this.title !== null && this.title.startsWith('Static Semantics:')) {
if (effectName === 'user-code')
return false;
}
return true;
}
static async enter({ spec, node, clauseStack, clauseNumberer }) {
if (!node.id) {
spec.warn({
type: 'node',
ruleId: 'missing-id',
message: "clause doesn't have an id",
node,
});
}
let nextNumber = '';
if (node.nodeName !== 'EMU-INTRO') {
nextNumber = clauseNumberer.next(clauseStack, node);
}
const parent = clauseStack[clauseStack.length - 1] || null;
const clause = new Clause(spec, node, parent, nextNumber);
if (parent) {
parent.subclauses.push(clause);
}
else {
spec.subclauses.push(clause);
}
clauseStack.push(clause);
}
static exit({ node, spec, clauseStack, inAlg, currentId }) {
var _a;
const clause = clauseStack[clauseStack.length - 1];
clause.buildExamples();
clause.buildNotes();
// prettier-ignore
const attributes = exports.SPECIAL_KINDS
.filter(kind => node.hasAttribute(kind))
.map(kind => exports.SPECIAL_KINDS_MAP.get(kind));
if (attributes.length > 0) {
const tag = spec.doc.createElement('div');
tag.className = 'attributes-tag';
const text = attributes.join(', ');
const contents = spec.doc.createTextNode(text);
tag.append(contents);
node.prepend(tag);
// we've already walked past the text node, so it won't get picked up by the usual process for autolinking
spec._textNodes[clause.namespace] = spec._textNodes[clause.namespace] || [];
spec._textNodes[clause.namespace].push({
node: contents,
clause,
inAlg,
currentId,
});
}
// clauses are always at the spec-level namespace.
const entry = {
type: 'clause',
id: clause.id,
aoid: clause.aoid,
title: clause.title,
titleHTML: clause.titleHTML,
number: clause.number,
};
if (clause.aoid) {
const existing = spec.biblio.keysForNamespace(spec.namespace);
if (existing.has(clause.aoid)) {
spec.warn({
type: 'node',
node,
ruleId: 'duplicate-definition',
message: `duplicate definition ${JSON.stringify(clause.aoid)}`,
});
}
else {
const signature = clause.signature;
let kind = clause.type != null && aoidTypes.includes(clause.type)
? clause.type
: undefined;
// @ts-ignore
if (kind === 'sdo') {
kind = 'syntax-directed operation';
}
const op = {
type: 'op',
aoid: clause.aoid,
refId: clause.id,
kind,
signature,
effects: clause.effects,
_node: clause.node,
_skipReturnChecks: clause.skipReturnChecks,
};
if (clause.skipGlobalChecks) {
op.skipGlobalChecks = true;
}
if (((_a = signature === null || signature === void 0 ? void 0 : signature.return) === null || _a === void 0 ? void 0 : _a.kind) === 'union' &&
signature.return.types.some(e => e.kind === 'completion') &&
signature.return.types.some(e => e.kind !== 'completion')) {
spec.warn({
type: 'node',
node: clause.header,
ruleId: 'completion-union',
message: `algorithms should return either completions or things which are not completions, never both`,
});
}
spec.biblio.add(op, spec.namespace);
}
}
spec.biblio.add(entry, spec.namespace);
clauseStack.pop();
}
getSecnumHTML() {
var _a;
if (!this.number || this.isBackMatter)
return '';
if (this.isAnnex) {
const isInnerAnnex = ((_a = this.node.parentElement) === null || _a === void 0 ? void 0 : _a.nodeName) === 'EMU-ANNEX';
if (isInnerAnnex) {
return `<span class="secnum">${this.number}</span> `;
}
else {
return `<span class="secnum">Annex ${this.number} <span class="annex-kind">(${this.isNormative ? 'normative' : 'informative'})</span></span> `;
}
}
else {
return `<span class="secnum">${this.number}</span> `;
}
}
}
Clause.elements = ['EMU-INTRO', 'EMU-CLAUSE', 'EMU-ANNEX'];
Clause.linkElements = Clause.elements;
exports.default = Clause;
function parseType(type, offset) {
try {
return type_parser_1.TypeParser.parse(type);
}
catch (e) {
if (e instanceof type_parser_1.ParseError) {
e.offset += offset;
}
throw e;
}
}
function parsedHeaderToSignature(parsedHeader) {
const ret = {
parameters: parsedHeader.params
.filter(p => p.wrappingTag !== 'del')
.map(p => ({
name: p.name,
type: p.type == null ? null : parseType(p.type, p.typeOffset),
})),
optionalParameters: parsedHeader.optionalParams
.filter(p => p.wrappingTag !== 'del')
.map(p => ({
name: p.name,
type: p.type == null ? null : parseType(p.type, p.typeOffset),
})),
return: parsedHeader.returnType == null
? null
: parseType(parsedHeader.returnType, parsedHeader.returnOffset),
};
return ret;
}