Add line number function to JS document parser.

This commit is contained in:
Joshua Bemenderfer 2024-09-03 16:14:16 -04:00
parent 8dca90037a
commit 70200a4091
3 changed files with 1570 additions and 30 deletions

1522
packages/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "@terrace-lang/js", "name": "@terrace-lang/js",
"description": "Terrace is a simple structured data syntax for configuration, content authoring, and DSLs.", "description": "Terrace is a simple structured data syntax for configuration, content authoring, and DSLs.",
"version": "0.1.1", "version": "0.1.2",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"repository": { "repository": {

View File

@ -1,16 +1,17 @@
import type { Reader } from './readers/reader.js' import type { Reader } from "./readers/reader.js";
import { createLineData, parseLine } from './parser.js' import { createLineData, parseLine } from "./parser.js";
// Container for a handful of convenience functions for parsing documents // Container for a handful of convenience functions for parsing documents
// Obtained from useDocument() below // Obtained from useDocument() below
export type Document = { export type Document = {
next: (levelScope?: number) => Promise<boolean> next: (levelScope?: number) => Promise<boolean>;
level: () => number, level: () => number;
line: (startOffset?: number) => string, lineNumber: () => number;
head: () => string, line: (startOffset?: number) => string;
tail: () => string, head: () => string;
match: (matchValue: string) => boolean tail: () => string;
} match: (matchValue: string) => boolean;
};
/** /**
* Provides a simple set of convenience functions around parseLine for more ergonomic parsing of Terrace documents * Provides a simple set of convenience functions around parseLine for more ergonomic parsing of Terrace documents
@ -19,15 +20,19 @@ export type Document = {
* @param {String} indent The character used for indentation in the document. Only a single character is permitted * @param {String} indent The character used for indentation in the document. Only a single character is permitted
* @returns {Document} A set of convenience functions for iterating through and parsing a document line by line * @returns {Document} A set of convenience functions for iterating through and parsing a document line by line
*/ */
export function useDocument (reader: Reader, indent: string = ' '): Document { export function useDocument(reader: Reader, indent: string = " "): Document {
if (indent.length !== 1) throw new Error(`Terrace currently only allows single-character indent strings - you passed "${indent}"`) if (indent.length !== 1)
throw new Error(
`Terrace currently only allows single-character indent strings - you passed "${indent}"`
);
const lineData = createLineData(indent) const lineData = createLineData(indent);
let currLine = '' let currLine = "";
let currLineNumber = -1;
// If `repeatCurrentLine` is `true`, the following call to `next()` will repeat the current line in // If `repeatCurrentLine` is `true`, the following call to `next()` will repeat the current line in
// the document and set `repeatCurrentLine` back to `false` // the document and set `repeatCurrentLine` back to `false`
let repeatCurrentLine = false let repeatCurrentLine = false;
/** /**
* Advances the current position in the terrace document and populates lineData * Advances the current position in the terrace document and populates lineData
* with the parsed information from that line * with the parsed information from that line
@ -54,28 +59,29 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
async function next(levelScope: number = -1): Promise<boolean> { async function next(levelScope: number = -1): Promise<boolean> {
// Repeat the current line instead of parsing a new one if the previous call to next() // Repeat the current line instead of parsing a new one if the previous call to next()
// determined the current line to be out of its scope. // determined the current line to be out of its scope.
if (repeatCurrentLine) repeatCurrentLine = false if (repeatCurrentLine) repeatCurrentLine = false;
// Otherwise parse the line normally. // Otherwise parse the line normally.
else { else {
// Load the next line from the line reader. // Load the next line from the line reader.
const line = await reader() const line = await reader();
// If there are no more lines, bail out. // If there are no more lines, bail out.
if (line == null) return false if (line == null) return false;
// Populate lineData with parsed information from the current line. // Populate lineData with parsed information from the current line.
currLine = line currLine = line;
parseLine(currLine, lineData) currLineNumber++;
parseLine(currLine, lineData);
} }
// If we shouldn't be handling this line, make the following call to next() repeat the current line. // If we shouldn't be handling this line, make the following call to next() repeat the current line.
// Allows a child loop to look forward, determine that the next line will be outside its purview, // Allows a child loop to look forward, determine that the next line will be outside its purview,
// and return control to the calling loop transparently without additional logic. // and return control to the calling loop transparently without additional logic.
if (level() <= levelScope) { if (level() <= levelScope) {
repeatCurrentLine = true repeatCurrentLine = true;
return false return false;
} }
return true return true;
} }
/** /**
@ -91,7 +97,14 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
* ``` * ```
* @returns {number} The indent level of the current line * @returns {number} The indent level of the current line
*/ */
const level = (): number => lineData.level const level = (): number => lineData.level;
/**
* Get the current line number, zero-indexed from first line read.
* @returns {number} The current line number, starting from zero.
*/
const lineNumber = (): number => currLineNumber;
/** /**
* Get a string with the current line contents. Skips all indent characters by default, but this can be configured with `startOffset` * Get a string with the current line contents. Skips all indent characters by default, but this can be configured with `startOffset`
* *
@ -109,7 +122,9 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
* @param {number} startOffset How many indent characters to skip before outputting the line contents. Defaults to the current indent level * @param {number} startOffset How many indent characters to skip before outputting the line contents. Defaults to the current indent level
* @returns {string} The line contents starting from `startOffset` * @returns {string} The line contents starting from `startOffset`
*/ */
const line = (startOffset: number = lineData.level): string => currLine.slice(startOffset) const line = (startOffset: number = lineData.level): string =>
currLine.slice(startOffset);
/** /**
* Get the first "word" of a line, starting from the first non-indent character to the first space or end of the line * Get the first "word" of a line, starting from the first non-indent character to the first space or end of the line
* Often used for deciding how to parse a block. * Often used for deciding how to parse a block.
@ -123,7 +138,9 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
* ``` * ```
* @returns {string} The `head` portion (first word) of a line * @returns {string} The `head` portion (first word) of a line
*/ */
const head = (): string => currLine.slice(lineData.offsetHead, lineData.offsetTail) const head = (): string =>
currLine.slice(lineData.offsetHead, lineData.offsetTail);
/** /**
* Get all text following the first "word" of a line, starting from the first character after the space at the end of `head()` * Get all text following the first "word" of a line, starting from the first character after the space at the end of `head()`
* *
@ -136,7 +153,7 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
* ``` * ```
* @returns {string} The remainder of the line following the `head()` portion, with no leading space * @returns {string} The remainder of the line following the `head()` portion, with no leading space
*/ */
const tail = (): string => currLine.slice(lineData.offsetTail + 1) // Skip the space const tail = (): string => currLine.slice(lineData.offsetTail + 1); // Skip the space
/** /**
* Quickly check if the current line head matches a specified value * Quickly check if the current line head matches a specified value
* *
@ -154,14 +171,15 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
* @param {string} matchValue A string to check against `head()` for equality * @param {string} matchValue A string to check against `head()` for equality
* @returns {boolean} * @returns {boolean}
*/ */
const match = (matchValue: string): boolean => matchValue === head() const match = (matchValue: string): boolean => matchValue === head();
return { return {
next, next,
level, level,
line, line,
lineNumber,
head, head,
tail, tail,
match match,
} };
} }