UNPKG

ecmarkup

Version:

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

260 lines (259 loc) 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.printGrammar = printGrammar; const grammarkdown_1 = require("grammarkdown"); const line_builder_1 = require("./line-builder"); // this whole thing is perverse, but I don't see a better way of doing it // I also could not figure out how to get grammarkdown to do its own indenting at any higher level than this const RAW_STRING_MARKER = 'RAW_STRING_MARKER'; class EmitterWithComments extends grammarkdown_1.GrammarkdownEmitter { constructor(options, sourceFile) { var _a; super(options); this.source = sourceFile.text; this.done = new Set(); this.rawParts = new Map(); // combine like LHSes const productions = new Map(); let needsReconstruction = false; for (const element of sourceFile.elements) { // TODO location information if (element.kind !== grammarkdown_1.SyntaxKind.Production) { continue; } if (element.body == null) { throw new Error('production is missing its body'); } if (element.name.text == null) { throw new Error('production is missing its name'); } const name = element.name.text; if (productions.has(name)) { const existing = productions.get(name); if (existing.some(p => !parameterListEquals(p.parameterList, element.parameterList))) { throw new Error(`productions for ${name} have mismatched parameter lists`); } if (element.body.kind === grammarkdown_1.SyntaxKind.OneOfList || // we only need to check the existing list the first time; after that it will hold by construction (existing.length === 1 && ((_a = existing[0].body) === null || _a === void 0 ? void 0 : _a.kind) === grammarkdown_1.SyntaxKind.OneOfList)) { throw new Error(`"one of" productions must not have any other right-hand sides`); } needsReconstruction = true; existing.push(element); } else { productions.set(name, [element]); } } this.commentsMap = new Map(); if (needsReconstruction) { const elements = []; for (const element of sourceFile.elements) { if (element.kind !== grammarkdown_1.SyntaxKind.Production) { elements.push(element); continue; } const prods = productions.get(element.name.text); if (prods.length === 1) { elements.push(element); continue; } if (prods[0] === element) { const allComments = prods.flatMap(p => this.getComments(p)); if (allComments.some(c => this.isIgnoreComment(c))) { elements.push(...prods); continue; } const newRhses = prods.flatMap(p => { if (p.body.kind === grammarkdown_1.SyntaxKind.RightHandSide) { return [p.body]; } else if (p.body.kind === grammarkdown_1.SyntaxKind.RightHandSideList) { return p.body.elements; } else { throw new Error('unexpected RHS kind'); } }); const newList = new grammarkdown_1.RightHandSideList(newRhses); const newProd = new grammarkdown_1.Production(element.name, element.parameterList, element.colonToken, newList); elements.push(newProd); this.commentsMap.set(newProd, allComments); this.commentsMap.set(newList, []); } // otherwise we've combined it with a prior production and do not need to emit it } sourceFile = new grammarkdown_1.SourceFile(sourceFile.filename, sourceFile.text, elements); } this.root = sourceFile; this.forceExpandRhs = sourceFile.elements.some(e => { var _a; return !(e.kind === grammarkdown_1.SyntaxKind.Production) || ((_a = e.body) === null || _a === void 0 ? void 0 : _a.kind) === grammarkdown_1.SyntaxKind.RightHandSideList; }); } static emit(grammar, options, indent) { const emitter = new EmitterWithComments(options, grammar.rootFiles[0]); let written; emitter.emit(emitter.root, grammar.resolver, grammar.diagnostics, (file, result) => { written = result; }); // @ts-ignore I promise written is initialized now const lines = written.split('\n'); if (lines[lines.length - 1] === '') { lines.pop(); } const output = new line_builder_1.LineBuilder(indent); for (const line of lines) { if (line.trimStart().startsWith(RAW_STRING_MARKER)) { output.appendLine(emitter.rawParts.get(line.trim()), true); continue; } // this is a bit gross, but grammarkdown only has 0 or 1 level of indentation, so it works const shouldIndent = line.startsWith(' '); if (shouldIndent) { ++output.indent; } output.appendLine(htmlEntitize(line)); if (shouldIndent) { --output.indent; } } return output; } isIgnoreComment(trivia) { if (trivia.kind === grammarkdown_1.SyntaxKind.SingleLineCommentTrivia || trivia.kind === grammarkdown_1.SyntaxKind.MultiLineCommentTrivia || trivia.kind === grammarkdown_1.SyntaxKind.HtmlCommentTrivia) { const source = this.source.substring(trivia.pos, trivia.end); return /(\/\/|\/\*)\s*emu-format ignore/.test(source); } return false; } emitNode(node) { var _a; if (node && node.kind !== grammarkdown_1.SyntaxKind.SourceFile) { const comments = this.getComments(node).filter(c => !this.done.has(c.pos)); if (comments.some(c => this.isIgnoreComment(c))) { if (node.kind !== grammarkdown_1.SyntaxKind.Production) { // TODO location information throw new Error(`"emu-format ignore" comments are only supported on full productions right now; if you need it elsewhere, open an issue on ecmarkup`); } // the source includes all comments and any leading HTML tags // it does not include trailing HTML tags, even though they are included in the AST const end = Math.max(node.end, ...((_a = node.trailingTrivia) !== null && _a !== void 0 ? _a : []).map(h => h.end)); const nodeSource = this.source.substring(node.pos, end); const marker = RAW_STRING_MARKER + '_' + this.rawParts.size; this.rawParts.set(marker, nodeSource); this.writer.writeln(marker); return; } } super.emitNode(node); } emitLeadingTriviaOfNode(node) { if (this.commentsMap.has(node)) { return this.emitTriviaNodes(this.commentsMap.get(node)); } return super.emitLeadingTriviaOfNode(node); } getComments(node) { var _a, _b; if (this.commentsMap.has(node)) { return this.commentsMap.get(node); } return [].concat((_a = node.detachedTrivia) !== null && _a !== void 0 ? _a : [], (_b = node.leadingTrivia) !== null && _b !== void 0 ? _b : []); } // for the collapsed case, keep it on one line emitOneOfList(node) { var _a, _b; if (this.root.elements.length === 1 && !this.source.trim().includes('\n')) { this.writer.write('one of '); for (let i = 0; i < ((_b = (_a = node.terminals) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); ++i) { if (i > 0) { this.writer.write(' '); } this.emitNode(node.terminals[i]); } } else { super.emitOneOfList(node); } } // we always want a blank line between productions, even when collapsed // also, we want to un-collapse productions which are in blocks with other un-collapsed productions emitProduction(node) { var _a, _b; if (this.forceExpandRhs && ((_a = node.body) === null || _a === void 0 ? void 0 : _a.kind) === grammarkdown_1.SyntaxKind.RightHandSide) { const newBody = new grammarkdown_1.RightHandSideList([node.body]); node = new grammarkdown_1.Production(node.name, node.parameterList, node.colonToken, newBody); } super.emitProduction(node); const productions = this.root.elements.filter(e => e.kind === grammarkdown_1.SyntaxKind.Production); if (productions.indexOf(node) < productions.length - 1 && ((_b = node.body) === null || _b === void 0 ? void 0 : _b.kind) === grammarkdown_1.SyntaxKind.RightHandSide) { this.writer.commitLine(); this.writer.writeln(); } } // we want specific spellings here emitTokenKind(kind) { if (kind === grammarkdown_1.SyntaxKind.LessThanExclamationToken || kind === grammarkdown_1.SyntaxKind.NotAnElementOfToken) { this.writer.write('&notin;'); } else if (kind === grammarkdown_1.SyntaxKind.LessThanMinusToken || kind === grammarkdown_1.SyntaxKind.ElementOfToken) { this.writer.write('&isin;'); } else { super.emitTokenKind(kind); } } // we need to avoid a literal `>` emitProse(node) { this.writer.write('&gt; '); node.fragments && this.emitNodes(node.fragments); } emitUnicodeCharacterLiteral(node) { var _a, _b, _c; if ((_a = node.text) === null || _a === void 0 ? void 0 : _a.startsWith('U+')) { return super.emitUnicodeCharacterLiteral(node); } if (!(((_b = node.text) === null || _b === void 0 ? void 0 : _b.startsWith('<')) && ((_c = node.text) === null || _c === void 0 ? void 0 : _c.endsWith('>')))) { throw new Error(`unreachable: unicode character literal is not wrapped in <>: ${JSON.stringify(node.text)}`); } this.writer.write('&lt;'); this.writer.write(node.text.slice(1, -1)); this.writer.write('&gt;'); } } // uuuuugh, grammarkdown is only async // that means everything else will need to be also // TODO for consistency this should probably take the Grammar object? // but grammarkdown does not make that very easy async function printGrammar(source, indent) { const grammarHost = grammarkdown_1.CoreAsyncHost.forFile(source); const options = { // for some reason grammarkdown does not expose its own emitter, so we can't just set the format here noChecks: true, newLine: grammarkdown_1.NewLineKind.LineFeed, }; const grammar = new grammarkdown_1.Grammar([grammarHost.file], options, grammarHost); await grammar.bind(); return EmitterWithComments.emit(grammar, options, indent); } const entities = { '“': '&ldquo;', '”': '&rdquo;', '≤': '&le;', '≥': '&ge;', }; const entityRegex = new RegExp(`[${Object.keys(entities).join('')}]`, 'ug'); function htmlEntitize(source) { return source.replace(entityRegex, r => entities[r]); } function parameterListEquals(a, b) { if ((a === null || a === void 0 ? void 0 : a.elements) == null && (a === null || a === void 0 ? void 0 : a.elements) == null) { return true; } if ((a === null || a === void 0 ? void 0 : a.elements) == null || (b === null || b === void 0 ? void 0 : b.elements) == null) { return false; } return (a.elements.length === b.elements.length && a.elements.every((e, i) => e.name.text === b.elements[i].name.text)); }