ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
194 lines (193 loc) • 7.36 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.YES_CLAUSE_AUTOLINK = exports.NO_CLAUSE_AUTOLINK = void 0;
exports.autolink = autolink;
exports.replacerForNamespace = replacerForNamespace;
const Xref_1 = require("./Xref");
const utils = require("./utils");
const escape = require('html-escape');
exports.NO_CLAUSE_AUTOLINK = new Set([
'PRE',
'CODE',
'SCRIPT',
'STYLE',
'EMU-CONST',
'EMU-PRODUCTION',
'EMU-GRAMMAR',
'EMU-XREF',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'EMU-VAR',
'EMU-VAL',
'VAR',
'A',
'DFN',
'SUB',
'EMU-NOT-REF',
]);
exports.YES_CLAUSE_AUTOLINK = new Set(['EMU-GMOD']); // these are processed even if they are nested in NO_CLAUSE_AUTOLINK contexts
function autolink(node, replacer, autolinkmap, clause, currentId, allowSameId) {
const spec = clause.spec;
const template = spec.doc.createElement('template');
const content = escape(node.textContent);
// null indicates we haven't done the analysis for this node yet
let isInAlg = null;
const autolinked = content.replace(replacer, (match, offset) => {
const entry = autolinkmap[narrowSpace(match)];
if (!entry) {
return match;
}
const entryId = entry.id || entry.refId;
const skipLinking = !allowSameId && currentId && entryId === currentId;
if (skipLinking) {
return match;
}
if (entry.aoid) {
let isInvocationAttribute = '';
// Matches function-style invocation with parentheses and SDO-style 'of'
// invocation. Exclude nodes which are outside of algorithms.
if ((isInAlg === true || isInAlg === null) &&
(content[offset + match.length] === '(' ||
(content[offset + match.length] === ' ' &&
content[offset + match.length + 1] === 'o' &&
content[offset + match.length + 2] === 'f'))) {
if (isInAlg === null) {
let pointer = node;
while (pointer != null) {
if (pointer.nodeName === 'EMU-ALG') {
isInAlg = true;
break;
}
pointer = pointer.parentNode;
}
if (isInAlg === null) {
isInAlg = false;
}
}
if (isInAlg) {
isInvocationAttribute = ' is-invocation';
}
}
let noAbruptCompletionAttribute = '';
// \xA0 is
if ((content[offset - 1] === ' ' || content[offset - 1] === '\xA0') &&
content[offset - 2] === '!') {
noAbruptCompletionAttribute = ' no-abrupt-completion';
}
return `<emu-xref aoid="${entry.aoid}"${isInvocationAttribute}${noAbruptCompletionAttribute}>${match}</emu-xref>`;
}
else {
return `<emu-xref href="#${entry.id || entry.refId}">${match}</emu-xref>`;
}
});
if (autolinked !== content) {
template.innerHTML = autolinked;
const newXrefNodes = utils.replaceTextNode(node, template.content);
const newXrefs = newXrefNodes.map(node => new Xref_1.default(spec, node, clause, clause.namespace, node.getAttribute('href'), node.getAttribute('aoid')));
spec._xrefs = spec._xrefs.concat(newXrefs);
}
}
function replacerForNamespace(namespace, biblio) {
const autolinkmap = biblio.getDefinedWords(namespace);
const replacer = new RegExp(regexpPatternForAutolinkKeys(autolinkmap, Object.keys(autolinkmap), 0), 'g');
return { replacer, autolinkmap };
}
function isCommonAbstractOp(op) {
return op === 'Call' || op === 'Set' || op === 'Type' || op === 'UTC' || op === 'remainder';
}
function lookAheadBeyond(key, entry) {
if (isCommonAbstractOp(key)) {
// must be followed by parentheses
return '(?=\\()';
}
if (entry.type !== 'term' || /^\w/.test(key)) {
// must not be followed by a letter, `.word`, `%%`, `]]`
return '(?!\\w|\\.\\w|%%|\\]\\])';
}
return '';
}
// returns a regexp string where each space can be many spaces or line breaks.
function widenSpace(str) {
return str.replace(/\s+/g, '\\s+');
}
// replaces multiple whitespace characters with a single space
function narrowSpace(str) {
return str.replace(/\s+/g, ' ');
}
function regexpEscape(str) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
}
function regexpUnion(alternatives) {
var _a;
if (alternatives.length < 2) {
return `${(_a = alternatives[0]) !== null && _a !== void 0 ? _a : ''}`;
}
return `(?:${alternatives.join('|')})`;
}
// Search a non-empty array of string `items` for the longest common
// substring starting at position `beginIndex`. The part of each string
// before `beginIndex` is ignored, and is not included in the result.
function longestCommonPrefix(items, beginIndex = 0) {
let endIndex = beginIndex;
OUTER: while (endIndex < items[0].length) {
const char = items[0][endIndex];
for (let i = 1; i < items.length; ++i) {
if (char !== items[i][endIndex]) {
break OUTER;
}
}
++endIndex;
}
return items[0].slice(beginIndex, endIndex);
}
function regexpPatternForAutolinkKeys(autolinkmap, subsetKeys, initialCommonLength) {
var _a;
const resultAlternatives = [];
const wordStartAlternatives = [];
const groups = {};
for (const key of subsetKeys) {
const char = key[initialCommonLength];
const group = ((_a = groups[char]) !== null && _a !== void 0 ? _a : (groups[char] = []));
group.push(key);
}
const matchEmpty = '' in groups;
if (matchEmpty) {
delete groups[''];
}
const longestFirst = (a, b) => b.length - a.length;
for (const groupChar of Object.keys(groups).sort()) {
// sort by length to ensure longer keys are tested first
const groupItems = groups[groupChar].sort(longestFirst);
const prefix = longestCommonPrefix(groupItems, initialCommonLength);
const prefixRegex = widenSpace(regexpEscape(prefix));
const suffixPos = initialCommonLength + prefix.length;
let suffixRegex;
if (groupItems.length > 5) {
// recursively split the group into smaller chunks
suffixRegex = regexpPatternForAutolinkKeys(autolinkmap, groupItems, suffixPos);
}
else {
suffixRegex = regexpUnion(groupItems.map(k => {
const item = widenSpace(regexpEscape(k.slice(suffixPos)));
return item + lookAheadBeyond(k, autolinkmap[k]);
}));
}
if (initialCommonLength === 0 && /^\w/.test(prefix)) {
wordStartAlternatives.push(prefixRegex + suffixRegex);
}
else {
resultAlternatives.push(prefixRegex + suffixRegex);
}
}
if (matchEmpty) {
resultAlternatives.push('');
}
if (wordStartAlternatives.length) {
resultAlternatives.unshift('\\b' + regexpUnion(wordStartAlternatives));
}
return regexpUnion(resultAlternatives);
}
;