203 lines
5.1 KiB
TypeScript
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);
|
|
}
|