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 { 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 { 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 { while (true) { const node = await this._getNextNode(); if (node === null) break; yield node; } } async _getNextNode(): Promise { // 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 { const results: TerraceNode[] = []; for await (const node of this) { if (predicate(node)) { results.push(node); } } return results; } async find(predicate: (node: TerraceNode) => boolean): Promise { for await (const node of this) { if (predicate(node)) { return node; } } return undefined; } async map(mapper: (node: TerraceNode) => T | Promise): Promise { const results: T[] = []; for await (const node of this) { results.push(await mapper(node)); } return results; } async toArray(): Promise { 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); }