package terrace import ( "io" "strings" ) // Reader is the interface that reads lines from a document. type Reader interface { Read() (string, error) } // TerraceNode represents a single node/line in a Terrace document. type TerraceNode struct { lineData *LineData content string lineNumber int document *TerraceDocument } // Head returns the head of the node. func (n *TerraceNode) Head() string { return n.content[n.lineData.OffsetHead:n.lineData.OffsetTail] } // Tail returns the tail of the node. func (n *TerraceNode) Tail() string { if n.lineData.OffsetTail+1 >= len(n.content) { return "" } return n.content[n.lineData.OffsetTail+1:] } // Content returns the content of the node. func (n *TerraceNode) Content() string { return n.content[n.lineData.OffsetHead:] } // Level returns the indentation level of the node. func (n *TerraceNode) Level() int { return n.lineData.Level } // LineNumber returns the line number of the node. func (n *TerraceNode) LineNumber() int { return n.lineNumber } // IsEmpty returns true if the node represents an empty line. func (n *TerraceNode) IsEmpty() bool { return strings.TrimSpace(n.content) == "" } // Is returns true if the node's head matches the given value. func (n *TerraceNode) Is(value string) bool { return n.Head() == value } // Raw returns the raw content of the node starting from the given offset. func (n *TerraceNode) Raw(offset int) string { if offset >= len(n.content) { return "" } return n.content[offset:] } // TerraceDocument is the main document iterator. type TerraceDocument struct { reader Reader indent rune lineData *LineData currentLineNumber int pushedBackNodes []*TerraceNode isExhausted bool } // NewTerraceDocument creates a new Terrace document iterator. func NewTerraceDocument(reader Reader, indent rune) *TerraceDocument { return &TerraceDocument{ reader: reader, indent: indent, lineData: NewLineData(indent), currentLineNumber: -1, } } // Next returns the next node in the document. func (d *TerraceDocument) Next() (*TerraceNode, error) { if len(d.pushedBackNodes) > 0 { node := d.pushedBackNodes[len(d.pushedBackNodes)-1] d.pushedBackNodes = d.pushedBackNodes[:len(d.pushedBackNodes)-1] return node, nil } if d.isExhausted { return nil, io.EOF } line, err := d.reader.Read() if err != nil { if err == io.EOF { d.isExhausted = true return nil, io.EOF } return nil, err } d.currentLineNumber++ ParseLine(line, d.lineData) // Copy the lineData to avoid mutations affecting existing nodes lineDataCopy := &LineData{ Level: d.lineData.Level, Indent: d.lineData.Indent, OffsetHead: d.lineData.OffsetHead, OffsetTail: d.lineData.OffsetTail, } return &TerraceNode{ lineData: lineDataCopy, content: line, lineNumber: d.currentLineNumber, document: d, }, nil } // PushBack pushes a node back to the document. func (d *TerraceDocument) PushBack(node *TerraceNode) { d.pushedBackNodes = append(d.pushedBackNodes, node) }