ohm-js
Version:
174 lines (147 loc) • 5.68 kB
JavaScript
import * as common from './common.js';
// --------------------------------------------------------------------
// Private stuff
// --------------------------------------------------------------------
// Given an array of numbers `arr`, return an array of the numbers as strings,
// right-justified and padded to the same length.
function padNumbersToEqualLength(arr) {
let maxLen = 0;
const strings = arr.map(n => {
const str = n.toString();
maxLen = Math.max(maxLen, str.length);
return str;
});
return strings.map(s => common.padLeft(s, maxLen));
}
// Produce a new string that would be the result of copying the contents
// of the string `src` onto `dest` at offset `offest`.
function strcpy(dest, src, offset) {
const origDestLen = dest.length;
const start = dest.slice(0, offset);
const end = dest.slice(offset + src.length);
return (start + src + end).substr(0, origDestLen);
}
// Casts the underlying lineAndCol object to a formatted message string,
// highlighting `ranges`.
function lineAndColumnToMessage(...ranges) {
const lineAndCol = this;
const {offset} = lineAndCol;
const {repeatStr} = common;
const sb = new common.StringBuffer();
sb.append('Line ' + lineAndCol.lineNum + ', col ' + lineAndCol.colNum + ':\n');
// An array of the previous, current, and next line numbers as strings of equal length.
const lineNumbers = padNumbersToEqualLength([
lineAndCol.prevLine == null ? 0 : lineAndCol.lineNum - 1,
lineAndCol.lineNum,
lineAndCol.nextLine == null ? 0 : lineAndCol.lineNum + 1,
]);
// Helper for appending formatting input lines to the buffer.
const appendLine = (num, content, prefix) => {
sb.append(prefix + lineNumbers[num] + ' | ' + content + '\n');
};
// Include the previous line for context if possible.
if (lineAndCol.prevLine != null) {
appendLine(0, lineAndCol.prevLine, ' ');
}
// Line that the error occurred on.
appendLine(1, lineAndCol.line, '> ');
// Build up the line that points to the offset and possible indicates one or more ranges.
// Start with a blank line, and indicate each range by overlaying a string of `~` chars.
const lineLen = lineAndCol.line.length;
let indicationLine = repeatStr(' ', lineLen + 1);
for (let i = 0; i < ranges.length; ++i) {
let startIdx = ranges[i][0];
let endIdx = ranges[i][1];
common.assert(startIdx >= 0 && startIdx <= endIdx, 'range start must be >= 0 and <= end');
const lineStartOffset = offset - lineAndCol.colNum + 1;
startIdx = Math.max(0, startIdx - lineStartOffset);
endIdx = Math.min(endIdx - lineStartOffset, lineLen);
indicationLine = strcpy(indicationLine, repeatStr('~', endIdx - startIdx), startIdx);
}
const gutterWidth = 2 + lineNumbers[1].length + 3;
sb.append(repeatStr(' ', gutterWidth));
indicationLine = strcpy(indicationLine, '^', lineAndCol.colNum - 1);
sb.append(indicationLine.replace(/ +$/, '') + '\n');
// Include the next line for context if possible.
if (lineAndCol.nextLine != null) {
appendLine(2, lineAndCol.nextLine, ' ');
}
return sb.contents();
}
// --------------------------------------------------------------------
// Exports
// --------------------------------------------------------------------
let builtInRulesCallbacks = [];
// Since Grammar.BuiltInRules is bootstrapped, most of Ohm can't directly depend it.
// This function allows modules that do depend on the built-in rules to register a callback
// that will be called later in the initialization process.
export function awaitBuiltInRules(cb) {
builtInRulesCallbacks.push(cb);
}
export function announceBuiltInRules(grammar) {
builtInRulesCallbacks.forEach(cb => {
cb(grammar);
});
builtInRulesCallbacks = null;
}
// Return an object with the line and column information for the given
// offset in `str`.
export function getLineAndColumn(str, offset) {
let lineNum = 1;
let colNum = 1;
let currOffset = 0;
let lineStartOffset = 0;
let nextLine = null;
let prevLine = null;
let prevLineStartOffset = -1;
while (currOffset < offset) {
const c = str.charAt(currOffset++);
if (c === '\n') {
lineNum++;
colNum = 1;
prevLineStartOffset = lineStartOffset;
lineStartOffset = currOffset;
} else if (c !== '\r') {
colNum++;
}
}
// Find the end of the target line.
let lineEndOffset = str.indexOf('\n', lineStartOffset);
if (lineEndOffset === -1) {
lineEndOffset = str.length;
} else {
// Get the next line.
const nextLineEndOffset = str.indexOf('\n', lineEndOffset + 1);
nextLine =
nextLineEndOffset === -1 ?
str.slice(lineEndOffset) :
str.slice(lineEndOffset, nextLineEndOffset);
// Strip leading and trailing EOL char(s).
nextLine = nextLine.replace(/^\r?\n/, '').replace(/\r$/, '');
}
// Get the previous line.
if (prevLineStartOffset >= 0) {
// Strip trailing EOL char(s).
prevLine = str.slice(prevLineStartOffset, lineStartOffset).replace(/\r?\n$/, '');
}
// Get the target line, stripping a trailing carriage return if necessary.
const line = str.slice(lineStartOffset, lineEndOffset).replace(/\r$/, '');
return {
offset,
lineNum,
colNum,
line,
prevLine,
nextLine,
toString: lineAndColumnToMessage,
};
}
// Return a nicely-formatted string describing the line and column for the
// given offset in `str` highlighting `ranges`.
export function getLineAndColumnMessage(str, offset, ...ranges) {
return getLineAndColumn(str, offset).toString(...ranges);
}
export const uniqueId = (() => {
let idCounter = 0;
return prefix => '' + prefix + idCounter++;
})();