ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
260 lines (259 loc) • 12.3 kB
JavaScript
"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('∉');
}
else if (kind === grammarkdown_1.SyntaxKind.LessThanMinusToken || kind === grammarkdown_1.SyntaxKind.ElementOfToken) {
this.writer.write('∈');
}
else {
super.emitTokenKind(kind);
}
}
// we need to avoid a literal `>`
emitProse(node) {
this.writer.write('> ');
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('<');
this.writer.write(node.text.slice(1, -1));
this.writer.write('>');
}
}
// 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 = {
'“': '“',
'”': '”',
'≤': '≤',
'≥': '≥',
};
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));
}