UNPKG

ecmarkup

Version:

Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.

311 lines (310 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.warnEmdFailure = warnEmdFailure; exports.wrapEmdFailure = wrapEmdFailure; exports.emdTextNode = emdTextNode; exports.htmlToDom = htmlToDom; exports.textContentFromHTML = textContentFromHTML; exports.domWalkBackward = domWalkBackward; exports.replaceTextNode = replaceTextNode; exports.traverseWhile = traverseWhile; exports.logVerbose = logVerbose; exports.logWarning = logWarning; exports.shouldInline = shouldInline; exports.readFile = readFile; exports.readBinaryFile = readBinaryFile; exports.writeFile = writeFile; exports.copyFile = copyFile; exports.offsetToLineAndColumn = offsetToLineAndColumn; exports.attrLocation = attrLocation; exports.attrValueLocation = attrValueLocation; exports.validateEffects = validateEffects; exports.doesEffectPropagateToParent = doesEffectPropagateToParent; exports.zip = zip; const jsdom = require("jsdom"); const chalk = require("chalk"); const emd = require("ecmarkdown"); const fs = require("fs"); const path = require("path"); const utils_1 = require("./lint/utils"); function warnEmdFailure(report, node, e) { if (typeof e.line === 'number' && typeof e.column === 'number') { report({ type: 'contents', ruleId: 'invalid-emd', message: `ecmarkdown failed to parse: ${e.message}`, node, nodeRelativeLine: e.line, nodeRelativeColumn: e.column, }); } else { report({ type: 'node', ruleId: 'invalid-emd', message: `ecmarkdown failed to parse: ${e.message}`, node, }); } } function wrapEmdFailure(src) { return `#### ECMARKDOWN PARSE FAILED ####<pre>${src}</pre>`; } /** @internal */ function emdTextNode(spec, node, namespace) { const loc = spec.locate(node); let c; if ((loc === null || loc === void 0 ? void 0 : loc.endTag) == null) { c = node.textContent.replace(/</g, '&lt;'); } else { const start = loc.startTag.endOffset; const end = loc.endTag.startOffset; c = loc.source.slice(start, end); } let processed; try { const parts = emd.parseFragment(c); if (spec.opts.lintSpec && loc != null) { const nonterminals = (0, utils_1.collectNonterminalsFromEmd)(parts).map(({ name, loc }) => ({ name, loc, node, namespace, })); spec._ntStringRefs = spec._ntStringRefs.concat(nonterminals); } processed = emd.emit(parts); } catch (e) { warnEmdFailure(spec.warn, node, e); processed = wrapEmdFailure(c); } const template = spec.doc.createElement('template'); template.innerHTML = processed; replaceTextNode(node, template.content); } /** @internal */ function htmlToDom(html) { const virtualConsole = new jsdom.VirtualConsole(); virtualConsole.on('error', () => { // Suppress warnings from e.g. CSS features not supported by JSDOM }); return new jsdom.JSDOM(html, { includeNodeLocations: true, virtualConsole }); } /** @internal */ function textContentFromHTML(doc, html) { const ele = doc.createElement('span'); ele.innerHTML = html; return ele.textContent; } /** @internal */ function domWalkBackward(root, cb) { const childNodes = root.childNodes; const childLen = childNodes.length; for (let i = childLen - 1; i >= 0; i--) { const node = childNodes[i]; if (node.nodeType !== 1) continue; const cont = cb(node); if (cont === false) continue; domWalkBackward(node, cb); } } /** @internal */ function replaceTextNode(node, frag) { // Append all the nodes const parent = node.parentNode; if (!parent) return []; const newXrefNodes = Array.from(frag.querySelectorAll('emu-xref')); const first = frag.childNodes[0]; if (first.nodeType === 3) { node.textContent = first.textContent; frag.removeChild(first); } else { // set it to empty because we don't want to break iteration // (I think it should work to delete it... investigate possible jsdom bug) node.textContent = ''; } parent.insertBefore(frag, node.nextSibling); return newXrefNodes; } /** @internal */ function traverseWhile(node, relationship, predicate, options) { var _a; const once = (_a = options === null || options === void 0 ? void 0 : options.once) !== null && _a !== void 0 ? _a : false; while (node != null && predicate(node)) { node = node[relationship]; if (once) break; } return node; } /** @internal */ function logVerbose(str) { const dateString = new Date().toISOString(); console.error(chalk.gray('[' + dateString + '] ') + str); } /** @internal */ function logWarning(str) { const dateString = new Date().toISOString(); console.error(chalk.gray('[' + dateString + '] ') + chalk.red(str)); } const CLAUSE_LIKE = ['EMU-ANNEX', 'EMU-CLAUSE', 'EMU-INTRO', 'EMU-NOTE', 'BODY']; /** @internal */ function shouldInline(node) { var _a; const surrogateParentTags = ['EMU-GRAMMAR', 'EMU-IMPORT', 'INS', 'DEL']; const parent = traverseWhile(node.parentNode, 'parentNode', node => { var _a; return surrogateParentTags.includes((_a = node === null || node === void 0 ? void 0 : node.nodeName) !== null && _a !== void 0 ? _a : ''); }); if (!parent) return false; const clauseLikeParent = CLAUSE_LIKE.includes(parent.nodeName) || CLAUSE_LIKE.includes((_a = parent.getAttribute('data-simulate-tagname')) === null || _a === void 0 ? void 0 : _a.toUpperCase()); return !clauseLikeParent; } /** @internal */ function readFile(file) { return new Promise((resolve, reject) => { fs.readFile(file, 'utf8', (err, data) => (err ? reject(err) : resolve(data))); }); } /** @internal */ function readBinaryFile(file) { return new Promise((resolve, reject) => { fs.readFile(file, (err, data) => (err ? reject(err) : resolve(data))); }); } /** @internal */ function writeFile(file, content) { return new Promise((resolve, reject) => { // we could do this async, but it's not worth worrying about fs.mkdirSync(path.dirname(file), { recursive: true }); if (typeof content === 'string') { content = Buffer.from(content, 'utf8'); } fs.writeFile(file, content, err => (err ? reject(err) : resolve())); }); } /** @internal */ async function copyFile(src, dest) { const content = await readFile(src); await writeFile(dest, content); } function offsetToLineAndColumn(string, offset) { const lines = string.split('\n'); let line = 0; let seen = 0; while (true) { if (line >= lines.length) { throw new Error(`offset ${offset} exceeded string ${JSON.stringify(string)}`); } if (seen + lines[line].length >= offset) { break; } seen += lines[line].length + 1; // +1 for the '\n' ++line; } const column = offset - seen; return { line: line + 1, column: column + 1 }; } function attrLocation(source, loc, attr) { var _a; const attrLoc = (_a = loc.startTag.attrs) === null || _a === void 0 ? void 0 : _a[attr]; if (attrLoc == null) { return { line: loc.startTag.startLine, column: loc.startTag.startCol }; } else { return { line: attrLoc.startLine, column: attrLoc.startCol }; } } function attrValueLocation(source, loc, attr) { var _a, _b, _c; const attrLoc = (_a = loc.startTag.attrs) === null || _a === void 0 ? void 0 : _a[attr]; if (attrLoc == null || source == null) { return { line: loc.startTag.startLine, column: loc.startTag.startCol }; } else { const tagText = source.slice(attrLoc.startOffset, attrLoc.endOffset); // RegExp.escape when const matcher = new RegExp(attr.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&') + '="?', 'i'); return { line: attrLoc.startLine, column: attrLoc.startCol + ((_c = (_b = tagText.match(matcher)) === null || _b === void 0 ? void 0 : _b[0].length) !== null && _c !== void 0 ? _c : 0), }; } } const KNOWN_EFFECTS = ['user-code']; function validateEffects(spec, effectsRaw, node) { const effects = []; const unknownEffects = []; for (const e of effectsRaw) { if (KNOWN_EFFECTS.indexOf(e) !== -1) { effects.push(e); } else { unknownEffects.push(e); } } if (unknownEffects.length !== 0) { spec.warn({ type: 'node', ruleId: 'unknown-effects', message: `unknown effects: ${unknownEffects}`, node, }); } return effects; } function doesEffectPropagateToParent(node, effect) { var _a, _b; // Effects should not propagate past explicit fences in parent steps. // // Abstract Closures are considered automatic fences for the user-code // effect, since those are effectively nested functions. // // Calls to Abstract Closures that can call user code must be explicitly // marked as such with <emu-meta effects="user-code">...</emu-meta>. for (; node.parentElement; node = node.parentElement) { const parent = node.parentElement; // This is super hacky. It's checking the output of ecmarkdown. if (parent.tagName !== 'LI') continue; if (effect === 'user-code' && /be a new (\w+ )*Abstract Closure/.test((_a = parent.textContent) !== null && _a !== void 0 ? _a : '')) { return false; } if ((_b = parent .getAttribute('fence-effects')) === null || _b === void 0 ? void 0 : _b.split(',').map(s => s.trim()).includes(effect)) { return false; } } return true; } function* zip(as, bs, allowMismatchedLengths = false) { var _a, _b; const iterA = as[Symbol.iterator](); const iterB = bs[Symbol.iterator](); while (true) { const iterResultA = iterA.next(); const iterResultB = iterB.next(); if (iterResultA.done !== iterResultB.done) { if (allowMismatchedLengths) { if (iterResultA.done) { (_a = iterB.return) === null || _a === void 0 ? void 0 : _a.call(iterB); } else { (_b = iterA.return) === null || _b === void 0 ? void 0 : _b.call(iterA); } break; } throw new Error('zipping iterators which ended at different times'); } if (iterResultA.done) { break; } yield [iterResultA.value, iterResultB.value]; } }