UNPKG

ohm-js

Version:

An object-oriented language for parsing and pattern matching

174 lines (147 loc) 5.68 kB
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++; })();