From 107a164ec817b0795c21591ebdd522d843dbd58e Mon Sep 17 00:00:00 2001 From: Joshua Bemenderfer Date: Wed, 15 Feb 2023 22:43:58 -0500 Subject: [PATCH] Document most of the JS 'Document' API --- docs/src/docs/c.tce | 2 +- docs/src/docs/javascript.tce | 168 +++++++++++++++++++++++++++++++---- packages/js/src/document.ts | 132 ++++++++++++++++++++++++--- pnpm-workspace.yaml | 1 - 4 files changed, 274 insertions(+), 29 deletions(-) diff --git a/docs/src/docs/c.tce b/docs/src/docs/c.tce index eed8c9a..91ff310 100644 --- a/docs/src/docs/c.tce +++ b/docs/src/docs/c.tce @@ -19,7 +19,7 @@ Section light Markdown Documentation is available for the following languages: - [C](/docs/c/) - 10% Complete - - [JavaScript](/docs/javascript/) - 20% Complete + - [JavaScript](/docs/javascript/) - 50% Complete - [Python](/docs/python/) - 0% Complete Heading 2 Getting Started diff --git a/docs/src/docs/javascript.tce b/docs/src/docs/javascript.tce index 66fddc3..eb24ae1 100644 --- a/docs/src/docs/javascript.tce +++ b/docs/src/docs/javascript.tce @@ -19,7 +19,7 @@ Section light Markdown Documentation is available for the following languages: - [C](/docs/c/) - 10% Complete - - [JavaScript](/docs/javascript/) - 20% Complete + - [JavaScript](/docs/javascript/) - 50% Complete - [Python](/docs/python/) - 0% Complete Heading 2 Getting Started @@ -129,10 +129,13 @@ Section light Heading 2 Core API class mt-12 + Markdown + **TODO:** Document Heading 3 LineData class my-6 CodeBlock typescript + // Type Definition type LineData = { line: string; indent: string; @@ -160,16 +163,30 @@ Section light Heading 3 useDocument() class my-6 + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | reader | [Reader](#reader) | When called, resolves to a string containing the next line in the document. + | indent | string | The character used for indentation in the document. Only a single character is permitted. + | **@returns** | [Document](#document) | A set of convenience functions for iterating through and parsing a document line by line. + + Provides a simple set of convenience functions around parseLine for more ergonomic parsing of Terrace documents. CodeBlock typescript + // Type Definition function useDocument (reader: Reader, indent: string = ' '): Document - CodeBlock javascript + + // Import Path import { useDocument } from '@terrace-lang/js/document' Heading 3 Document class my-6 + Markdown + Container for a handful of convenience functions for parsing documents. + Obtained from [useDocument()](#usedocument) above CodeBlock typescript + // Type Definition type Document = { - next: (startLevel?: number) => Promise + next: (levelScope?: number) => Promise level: () => number, line: (startOffset?: number) => string, head: () => string, @@ -179,79 +196,198 @@ Section light Heading 3 Document.next() class my-6 + + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | levelScope | number = -1 | If specified, `next()` will return `false` when it encounters a line with a level at or below `levelScope` + | **@returns** | Promise | Returns `true` after parsing a line, or `false` if the document has ended or a line at or below `levelScope` has been encountered. + + Advances the current position in the terrace document and populates lineData + with the parsed information from that line. + + Returns `true` after parsing the next line, or `false` upon reaching the end of the document. + If the `levelScope` parameter is provided, `next()` will return `false` when it encounters a line + with a level at or below `levelScope`. This allows you to iterate through subsections of a document. + + If a lower-level line was encountered, the following call to `next()` will repeat this line again. + This 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. + + Intended to be used inside a while loop to parse a section of a Terrace document. + CodeBlock typescript - next: (startLevel?: number) => Promise - CodeBlock javascript + // Type Definition + next: (levelScope?: number) => Promise + + // Import Path import { useDocument } from '@terrace-lang/js/document' + + // Usage const { next } = useDocument(...) + while (await next()) { + // Do something with each line. + } Heading 3 Document.level() class my-6 + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | **@returns** | number | The indent level of the current line + + Returns the number of indent characters of the current line. + + Given the following document, `level()` would return 0, 1, 2, and 5 respectively for each line. + CodeBlock terrace + block + block + block + block + CodeBlock typescript + // Type Definition level: () => number - CodeBlock javascript + + // Usage import { useDocument } from '@terrace-lang/js/document' const { level } = useDocument(...) Heading 3 Document.line() class my-6 + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | levelScope | startOffset = [level()](#document-level) | 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` + + Get a string with the current line contents. Skips all indent characters by default, but this can be configured with `startOffset` + + Given the following document + CodeBlock terrace + root + sub-line + Markdown + - Calling `line()` on the second line returns "sub-line", trimming off the leading indent characters. + - Calling `line(0)` however, returns "    sub-line", with all four leading spaces. + + `startOffset` is primarily used for parsing blocks that have literal indented multi-line text, such as markdown. + CodeBlock typescript + // Type Definition line: (startOffset?: number) => string - CodeBlock javascript + + // Usage import { useDocument } from '@terrace-lang/js/document' const { line } = useDocument(...) Heading 3 Document.head() class my-6 + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | **@returns** | string | The `head` portion (first word) of a 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. + + Terrace DSLs do not *need* to use head-tail line structure, but support for them is built into the parser + + Given the following line, [head()](#document-head) returns "title" + CodeBlock terrace + title An Important Document CodeBlock typescript + // Type Definition head: () => string - CodeBlock javascript + + // Usage import { useDocument } from '@terrace-lang/js/document' const { head } = useDocument(...) Heading 3 Document.tail() class my-6 + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | **@returns** | string | The remainder of the line following the [head()](#document-head) portion, with no leading space + + Get all text following the first "word" of a line, starting from the first character after the space at the end of [head()](#document-head) + + Terrace DSLs do not *need* to use head-tail line structure, but support for them is built into the parser + + Given the following line, [tail()](#document-tail) returns "An Important Document" + CodeBlock terrace + title An Important Document CodeBlock typescript + // Type Definition tail: () => string - CodeBlock javascript + + // Usage import { useDocument } from '@terrace-lang/js/document' const { tail } = useDocument(...) Heading 3 Document.match() class my-6 + Markdown + | Parameter | Type | Description + | -------------- | --------------------- | ----------------------------------------------------------------------- + | matchValue | string | A string to check against [head()](#document-head) for equality + | **@returns** | boolean | Whether the current [head()](#document-head) matches the passed value + + Quickly check if the current line head matches a specified value + + Shorthand for `matchValue === head()` + + Given the following line + CodeBlock terrace + title An Important Document + Markdown + - `match('title')` returns `true` + - `match('somethingElse`) returns `false` CodeBlock typescript + // Type Definition match: (matchValue: string) => boolean - CodeBlock javascript + + // Usage import { useDocument } from '@terrace-lang/js/document' const { match } = useDocument(...) Heading 2 Reader API class mt-12 + Markdown + **TODO:** Document Heading 3 Reader class my-6 CodeBlock typescript + // Type Definition type Reader = () => string|null|Promise Heading 3 createStringReader() class my-6 CodeBlock typescript - function createFileReader(path: string): Reader - CodeBlock javascript - import { createStdinReader } from '@terrace-lang/js/readers/js-string' + // Type Definition + function createStringReader(path: string): Reader + + // Import Path + import { createStringReader } from '@terrace-lang/js/readers/js-string' Heading 3 createFileReader() class my-6 CodeBlock typescript + // Type Definition function createFileReader(path: string): Reader - CodeBlock javascript - import { createStdinReader } from '@terrace-lang/js/readers/node-readline' + + // Import Path + import { createFileReader } from '@terrace-lang/js/readers/node-readline' Heading 3 createStdinReader() class my-6 CodeBlock typescript + // Type Definition function createStdinReader(): Reader - CodeBlock javascript + + // Import Path import { createStdinReader } from '@terrace-lang/js/readers/node-readline' Heading 2 Contributing diff --git a/packages/js/src/document.ts b/packages/js/src/document.ts index f853063..1350d3b 100644 --- a/packages/js/src/document.ts +++ b/packages/js/src/document.ts @@ -1,8 +1,10 @@ import type { Reader } from './readers/reader' import { createLineData, parseLine } from './parser' +// Container for a handful of convenience functions for parsing documents +// Obtained from useDocument() below export type Document = { - next: (startLevel?: number) => Promise + next: (levelScope?: number) => Promise level: () => number, line: (startOffset?: number) => string, head: () => string, @@ -10,39 +12,147 @@ export type Document = { match: (matchValue: string) => boolean } +/** + * Provides a simple set of convenience functions around parseLine for more ergonomic parsing of Terrace documents + * + * @param {Reader} reader When called, resolves to a string containing the next line in the document + * @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 + */ 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}"`) + const lineData = createLineData('', indent) - let repeat = false - async function next(startLevel: number = -1): Promise { + // If `repeatCurrentLine` is `true`, the following call to `next()` will repeat the current line in + // the document and set `repeatCurrentLine` back to `false` + let repeatCurrentLine = false + /** + * Advances the current position in the terrace document and populates lineData + * with the parsed information from that line + * + * Returns `true` after parsing the next line, or `false` upon reaching the end of the document. + * If the `levelScope` parameter is provided, `next()` will return `false` when it encounters a line + * with a level at or below `levelScope`. This allows you to iterate through subsections of a document. + * + * If a lower-level line was encountered, the following call to `next()` will repeat this line again. + * This 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. + * + * Intended to be used inside a while loop to parse a section of a Terrace document. + * + * ```javascript + * while (await next()) { + * // Do something with each line. + * } + * ``` + * + * @param {number} levelScope If specified, `next()` will return `false` when it encounters a line with a level at or below `levelScope` + * @returns {Promise} Returns `true` after parsing a line, or `false` if the document has ended or a line at or below `levelScope` has been encountered. + */ + async function next(levelScope: number = -1): Promise { // 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. - if (repeat) repeat = false + if (repeatCurrentLine) repeatCurrentLine = false // Otherwise parse the line normally. else { + // Load the next line from the line reader. const line = await reader() // If there are no more lines, bail out. if (line == null) return false + // Populate lineData with parsed information from the current line. lineData.line = line parseLine(lineData) } - // If we shouldn't be handling this line, make the next 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, // and return control to the calling loop transparently without additional logic. - if (level() <= startLevel) { - repeat = true + if (level() <= levelScope) { + repeatCurrentLine = true return false } return true } - const level = () => lineData.level - const line = (startOffset: number = lineData.offsetHead) => lineData.line.slice(startOffset) - const head = () => lineData.line.slice(lineData.offsetHead, lineData.offsetTail) - const tail = () => lineData.line.slice(lineData.offsetTail + 1) // Skip the space + /** + * Returns the number of indent characters of the current line + * + * Given the following document, `level()` would return 0, 1, 2, and 5 respectively for each line + * + * ```terrace + * block + * block + * block + * block + * ``` + * @returns {number} The indent level of the current line + */ + const level = (): number => lineData.level + /** + * Get a string with the current line contents. Skips all indent characters by default, but this can be configured with `startOffset` + * + * Given the following document + * + * ```terrace + * root + * sub-line + * ``` + * `line()` on the second line returns "sub-line", trimming off the leading indent characters + * `line(0)` however, returns " sub-line", with all four leading spaces + * + * `startOffset` is primarily used for parsing blocks that have literal indented multi-line text, such as markdown + * + * @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` + */ + const line = (startOffset: number = lineData.level): string => lineData.line.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 + * Often used for deciding how to parse a block. + * + * Terrace DSLs do not *need* to use head-tail line structure, but support for them is built into the parser + * + * Given the following line, `head()` returns "title" + * + * ```terrace + * title An Important Document + * ``` + * @returns {string} The `head` portion (first word) of a line + */ + const head = (): string => lineData.line.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()` + * + * Terrace DSLs do not *need* to use head-tail line structure, but support for them is built into the parser + * + * Given the following line, `tail()` returns "An Important Document" + * + * ```terrace + * title An Important Document + * ``` + * @returns {string} The remainder of the line following the `head()` portion, with no leading space + */ + const tail = (): string => lineData.line.slice(lineData.offsetTail + 1) // Skip the space + /** + * Quickly check if the current line head matches a specified value + * + * Shorthand for `matchValue === head()` + * + * Given the following line + * + * ```terrace + * title An Important Document + * ``` + * + * `match('title')` returns `true` + * `match('somethingElse`) returns `false` + * + * @param {string} matchValue A string to check against `head()` for equality + * @returns {boolean} + */ const match = (matchValue: string): boolean => matchValue === head() return { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 26dabac..e67fe5f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,4 +2,3 @@ packages: - "./docs" - "./test" - "./packages/*" - - "./experiments/*"