Terrace/packages/js/src/document.ts
Joshua Bemenderfer 9d9757e868 Updates.
2025-09-08 16:24:38 -04:00

203 lines
5.1 KiB
TypeScript

import type { Reader } from "./readers/reader.js";
import { createLineData, parseLine, type LineData } from "./parser.js";
// Represents a single node/line in a Terrace document
export class TerraceNode {
private _lineData: LineData;
private _content: string;
private _lineNumber: number;
private _document: TerraceDocument;
constructor(
lineData: LineData,
content: string,
lineNumber: number,
document: TerraceDocument
) {
this._lineData = { ...lineData }; // Copy to avoid mutations
this._content = content;
this._lineNumber = lineNumber;
this._document = document;
}
// Current line properties (zero-allocation - just slice references)
get head(): string {
return this._content.slice(this._lineData.offsetHead, this._lineData.offsetTail);
}
get tail(): string {
return this._content.slice(this._lineData.offsetTail + 1);
}
get content(): string {
return this._content.slice(this._lineData.offsetHead);
}
get level(): number {
return this._lineData.level;
}
get lineNumber(): number {
return this._lineNumber;
}
// Convenience methods
is(value: string): boolean {
return this.head === value;
}
isEmpty(): boolean {
return this._content.trim() === '';
}
// Content access with different indent handling
raw(offset?: number): string {
return this._content.slice(offset ?? 0);
}
// Navigation (streaming-compatible)
async* children(): AsyncIterableIterator<TerraceNode> {
const parentLevel = this.level;
while (true) {
const node = await this._document._getNextNode();
if (node === null) break;
// If we encounter a node at or below parent level, it's not a child
if (node.level <= parentLevel) {
this._document._pushBack(node);
break;
}
// Yield any node that is deeper than the parent
// This supports arbitrary nesting as per Terrace spec
yield node;
}
}
async* siblings(): AsyncIterableIterator<TerraceNode> {
const currentLevel = this.level;
while (true) {
const node = await this._document._getNextNode();
if (node === null) break;
if (node.level < currentLevel) {
this._document._pushBack(node);
break;
}
if (node.level === currentLevel) {
yield node;
}
}
}
}
// Main document iterator
export class TerraceDocument {
private _reader: Reader;
private _indent: string;
private _lineData: LineData;
private _currentLineNumber: number;
private _pushedBackNodes: TerraceNode[] = [];
private _isExhausted: boolean = false;
constructor(reader: Reader, indent: string = " ") {
this._reader = reader;
this._indent = indent;
this._lineData = createLineData(indent);
this._currentLineNumber = 0;
}
async*[Symbol.asyncIterator](): AsyncIterableIterator<TerraceNode> {
while (true) {
const node = await this._getNextNode();
if (node === null) break;
yield node;
}
}
async _getNextNode(): Promise<TerraceNode | null> {
// Check for pushed back nodes first (LIFO order)
if (this._pushedBackNodes.length > 0) {
return this._pushedBackNodes.pop()!;
}
// If we've exhausted the reader, return null
if (this._isExhausted) {
return null;
}
const line = await this._reader();
if (line == null) {
this._isExhausted = true;
return null;
}
this._currentLineNumber++;
parseLine(line, this._lineData);
return new TerraceNode(
this._lineData,
line,
this._currentLineNumber,
this
);
}
_pushBack(node: TerraceNode): void {
this._pushedBackNodes.push(node);
}
// Utility methods for functional chaining
async filter(predicate: (node: TerraceNode) => boolean): Promise<TerraceNode[]> {
const results: TerraceNode[] = [];
for await (const node of this) {
if (predicate(node)) {
results.push(node);
}
}
return results;
}
async find(predicate: (node: TerraceNode) => boolean): Promise<TerraceNode | undefined> {
for await (const node of this) {
if (predicate(node)) {
return node;
}
}
return undefined;
}
async map<T>(mapper: (node: TerraceNode) => T | Promise<T>): Promise<T[]> {
const results: T[] = [];
for await (const node of this) {
results.push(await mapper(node));
}
return results;
}
async toArray(): Promise<TerraceNode[]> {
const results: TerraceNode[] = [];
for await (const node of this) {
results.push(node);
}
return results;
}
}
// Legacy Document type for backwards compatibility references
export type Document = TerraceDocument;
/**
* Creates a new Terrace document iterator
*
* @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 {TerraceDocument} An iterable document that can be used with for-await-of loops
*/
export function useDocument(reader: Reader, indent: string = " "): TerraceDocument {
return new TerraceDocument(reader, indent);
}