This commit is contained in:
Joshua Bemenderfer
2025-09-08 16:24:38 -04:00
parent 70200a4091
commit 9d9757e868
79 changed files with 11705 additions and 3554 deletions

View File

@@ -0,0 +1,51 @@
Heading 2 Core API
class mt-12 mb-6
Markdown
The core Python API provides parsing utilities and data structures for handling Terrace document structures.
Heading 3 Types
class mt-8 mb-4
CodeBlock python
from typing import TypedDict, Optional, Callable
# Type for line data
class LineData(TypedDict):
indent: str
level: int
offsetHead: int
offsetTail: int
# Type for a reader function
Reader = Callable[[], Optional[str]]
Heading 3 Parser Functions
class mt-8 mb-4
Markdown
Core parsing functions for processing Terrace document lines:
CodeBlock python
def createLineData(indent: str = ' ') -> LineData:
"""Initialize a LineData instance with default values"""
def parseLine(line: str, lineData: LineData) -> None:
"""Parse a line and update the LineData in-place"""
Heading 3 Usage Example
class mt-8 mb-4
CodeBlock python
from terrace import createLineData, parseLine
# Initialize line data with space indentation
line_data = createLineData(' ')
# Parse a line
line = " config database localhost"
parseLine(line, line_data)
print(f"Level: {line_data['level']}") # 2
print(f"Head start: {line_data['offsetHead']}") # 2
print(f"Head end: {line_data['offsetTail']}") # 8

View File

@@ -0,0 +1,153 @@
Heading 2 Document API
class mt-12
Markdown
The Document API provides a higher-level interface for parsing Terrace documents with Python idioms and best practices.
Heading 3 TerraceDocument
class mt-8 mb-4
Markdown
Main document iterator for Terrace documents that supports Python's iteration protocols.
CodeBlock python
class TerraceDocument:
"""Main document iterator for Terrace documents"""
def __init__(self, reader: Reader, indent: str = ' '):
"""Create a new TerraceDocument with the given reader"""
def __iter__(self) -> Iterator[TerraceNode]:
"""Make the document iterable"""
Heading 3 TerraceNode
class mt-8 mb-4
Markdown
Represents a single node/line in a Terrace document with convenient property access.
CodeBlock python
class TerraceNode:
"""Represents a single node/line in a Terrace document"""
@property
def head(self) -> str:
"""Get the first word of the line"""
@property
def tail(self) -> str:
"""Get everything after the first word"""
@property
def content(self) -> str:
"""Get the line content without indentation"""
@property
def level(self) -> int:
"""Get the indentation level"""
@property
def line_number(self) -> int:
"""Get the line number (zero-indexed)"""
def is_(self, value: str) -> bool:
"""Check if the head matches the given value"""
def is_empty(self) -> bool:
"""Check if the line is empty/blank"""
def raw(self, offset: Optional[int] = None) -> str:
"""Get raw content with custom offset"""
def children(self) -> Generator[TerraceNode, None, None]:
"""Iterate through all descendant nodes"""
def siblings(self) -> Generator[TerraceNode, None, None]:
"""Iterate through sibling nodes at the same level"""
Heading 3 Factory Functions
class mt-8 mb-4
CodeBlock python
def use_document(reader: Reader, indent: str = ' ') -> TerraceDocument:
"""Create a new Terrace document iterator"""
# Convenience functions for creating readers
def create_string_reader(content: str) -> Reader:
"""Create a reader from a string"""
def create_file_reader(file_path: str) -> Reader:
"""Create a reader from a file path"""
def create_lines_reader(lines: List[str]) -> Reader:
"""Create a reader from a list of lines"""
Heading 3 Usage Examples
class mt-8 mb-4
Markdown
**Basic Document Iteration**
CodeBlock python
from terrace import use_document, create_string_reader
content = """
title My Document
section Introduction
paragraph This is an example.
section Conclusion
paragraph That's all!
"""
reader = create_string_reader(content)
doc = use_document(reader)
for node in doc:
if node.is_('title'):
print(f"Document title: {node.tail}")
elif node.is_('section'):
print(f"Section: {node.tail} (level {node.level})")
elif node.is_('paragraph'):
print(f" - {node.tail}")
Markdown
**Working with Child Nodes**
CodeBlock python
from terrace import use_document, create_string_reader
content = """
config
database
host localhost
port 5432
server
port 8080
"""
reader = create_string_reader(content)
doc = use_document(reader)
for node in doc:
if node.is_('config'):
print("Configuration:")
for child in node.children():
print(f" {child.head}: {child.tail}")
for grandchild in child.children():
print(f" {grandchild.head}: {grandchild.tail}")
Markdown
**Functional Programming Style**
CodeBlock python
# Filter nodes by predicate
titles = doc.filter(lambda node: node.is_('title'))
# Find first matching node
first_section = doc.find(lambda node: node.is_('section'))
# Map nodes to values
all_heads = doc.map(lambda node: node.head)
# Convert to list
all_nodes = doc.to_list()

View File

@@ -0,0 +1,47 @@
layout layout.njk
title Python Documentation - Terrace
description
Python language documentation for the Terrace programming language
Section light
class flex flex-col md:flex-row gap-16
Block
class w-full lg:w-1/3
TableOfContents
Block
Heading 1 Terrace Python Documentation
class -ml-2
Markdown
Documentation is available for the following languages:
- [C](/docs/c/) - 75% Complete
- [JavaScript](/docs/javascript/) - 75% Complete
- [Go](/docs/go/) - 50% Complete
- [Python](/docs/python/) - 100% Complete
- [Rust](/docs/rust/) - 100% Complete
Heading 2 Getting Started
class mt-12 mb-6
Markdown
Install Terrace using [pip](https://pip.pypa.io/):
CodeBlock bash
# Install from PyPI
$ pip install terrace-lang
# Or using Poetry
$ poetry add terrace-lang
Include ./core-api.inc.tce
Include ./document-api.inc.tce
Include ./reader-api.inc.tce
Include ./recipes.inc.tce
Heading 2 Contributing
class mt-12
Section dark
Footer
class w-full

View File

@@ -0,0 +1,175 @@
Heading 2 Reader API
class mt-12
Markdown
The Reader API provides functions and utilities for creating readers that supply lines to the Document API.
Heading 3 Reader Type
class mt-8 mb-4
Markdown
A `Reader` is a callable that returns the next line in a document or `None` when the end is reached.
CodeBlock python
from typing import Optional, Callable
# Type definition
Reader = Callable[[], Optional[str]]
Heading 3 Built-in Readers
class mt-8 mb-4
Markdown
**String Reader**
Create a reader from a string, splitting on newlines.
CodeBlock python
def create_string_reader(content: str) -> Reader:
"""Create a reader from a string"""
lines = content.split('\n')
# Remove trailing empty line if content ended with newline
if len(lines) > 0 and content.endswith('\n') and lines[-1] == '':
lines = lines[:-1]
index = 0
def reader() -> Optional[str]:
nonlocal index
if index >= len(lines):
return None
line = lines[index]
index += 1
return line
return reader
Markdown
**File Reader**
Create a reader from a file path.
CodeBlock python
def create_file_reader(file_path: str) -> Reader:
"""Create a reader from a file path"""
file_handle = open(file_path, 'r', encoding='utf-8')
def reader() -> Optional[str]:
line = file_handle.readline()
if not line:
file_handle.close()
return None
return line.rstrip('\n\r')
return reader
Markdown
**Lines Reader**
Create a reader from a list of strings.
CodeBlock python
def create_lines_reader(lines: List[str]) -> Reader:
"""Create a reader from a list of lines"""
index = 0
def reader() -> Optional[str]:
nonlocal index
if index >= len(lines):
return None
line = lines[index]
index += 1
return line.rstrip('\n\r')
return reader
Heading 3 Custom Readers
class mt-8 mb-4
Markdown
You can create custom readers for any data source by implementing a function that returns `Optional[str]`.
CodeBlock python
import json
from typing import Iterator
def create_json_lines_reader(file_path: str) -> Reader:
"""Create a reader that processes JSON Lines format"""
def generator() -> Iterator[str]:
with open(file_path, 'r') as f:
for line in f:
try:
data = json.loads(line.strip())
# Convert JSON object to Terrace format
yield f"entry {data.get('name', 'unnamed')}"
for key, value in data.items():
if key != 'name':
yield f" {key} {value}"
except json.JSONDecodeError:
continue
iterator = generator()
def reader() -> Optional[str]:
try:
return next(iterator)
except StopIteration:
return None
return reader
Heading 3 Usage Examples
class mt-8 mb-4
Markdown
**Reading from a string**
CodeBlock python
from terrace import use_document, create_string_reader
content = """
title My Document
author John Doe
date 2023-12-01
"""
reader = create_string_reader(content)
doc = use_document(reader)
for node in doc:
print(f"Level {node.level}: {node.content}")
Markdown
**Reading from a file**
CodeBlock python
from terrace import use_document, create_file_reader
reader = create_file_reader('document.tce')
doc = use_document(reader)
for node in doc:
if node.is_('title'):
print(f"Document: {node.tail}")
Markdown
**Reading from a list of lines**
CodeBlock python
from terrace import use_document, create_lines_reader
lines = [
"config",
" host localhost",
" port 8080",
"routes",
" / home",
" /api api"
]
reader = create_lines_reader(lines)
doc = use_document(reader)
for node in doc:
print(f"{' ' * node.level}{node.head}: {node.tail}")

View File

@@ -0,0 +1,265 @@
Heading 2 Recipes
class mt-12
Markdown
Common patterns and recipes for working with Terrace documents in Python.
Heading 3 Configuration File Parser
class mt-8 mb-4
Markdown
Parse a hierarchical configuration file with sections and key-value pairs.
CodeBlock python
from terrace import use_document, create_file_reader
from typing import Dict, Any
def parse_config_file(file_path: str) -> Dict[str, Any]:
"""Parse a Terrace configuration file into a nested dictionary"""
reader = create_file_reader(file_path)
doc = use_document(reader)
config = {}
stack = [config]
for node in doc:
# Adjust stack to current level
while len(stack) > node.level + 1:
stack.pop()
current_dict = stack[-1]
if node.tail: # Key-value pair
current_dict[node.head] = node.tail
else: # Section header
current_dict[node.head] = {}
stack.append(current_dict[node.head])
return config
# Usage
config = parse_config_file('app.tce')
print(config['database']['host'])
Heading 3 Document Outline Generator
class mt-8 mb-4
Markdown
Extract a document outline based on heading levels.
CodeBlock python
from terrace import use_document, create_string_reader
from dataclasses import dataclass
from typing import List
@dataclass
class Heading:
level: int
title: str
children: List['Heading']
def extract_outline(content: str) -> List[Heading]:
"""Extract document outline from Terrace content"""
reader = create_string_reader(content)
doc = use_document(reader)
headings = []
stack = []
for node in doc:
if node.is_('heading') or node.is_('h1') or node.is_('h2') or node.is_('h3'):
heading = Heading(level=node.level, title=node.tail, children=[])
# Find the right parent in the stack
while stack and stack[-1].level >= heading.level:
stack.pop()
if stack:
stack[-1].children.append(heading)
else:
headings.append(heading)
stack.append(heading)
return headings
Heading 3 Template Engine
class mt-8 mb-4
Markdown
Simple template engine that processes Terrace templates with variables.
CodeBlock python
from terrace import use_document, create_string_reader
from typing import Dict, Any
import re
def render_template(template: str, variables: Dict[str, Any]) -> str:
"""Render a Terrace template with variable substitution"""
reader = create_string_reader(template)
doc = use_document(reader)
output = []
indent_str = " " # Two spaces per level
for node in doc:
# Apply indentation
indentation = indent_str * node.level
if node.is_('var'):
# Variable substitution: var name -> value
var_name = node.tail
value = variables.get(var_name, f"{{undefined: {var_name}}}")
output.append(f"{indentation}{value}")
elif node.is_('if'):
# Conditional rendering: if variable_name
condition = node.tail
if variables.get(condition):
# Process children if condition is truthy
for child in node.children():
child_line = f"{indent_str * child.level}{child.content}"
# Substitute variables in child content
child_line = re.sub(r'\{\{(\w+)\}\}',
lambda m: str(variables.get(m.group(1), m.group(0))),
child_line)
output.append(child_line)
elif node.is_('loop'):
# Loop over array: loop items
array_name = node.tail
items = variables.get(array_name, [])
for item in items:
for child in node.children():
child_line = f"{indent_str * child.level}{child.content}"
# Make item available as 'item' variable
temp_vars = {**variables, 'item': item}
child_line = re.sub(r'\{\{(\w+)\}\}',
lambda m: str(temp_vars.get(m.group(1), m.group(0))),
child_line)
output.append(child_line)
else:
# Regular content with variable substitution
content = node.content
content = re.sub(r'\{\{(\w+)\}\}',
lambda m: str(variables.get(m.group(1), m.group(0))),
content)
output.append(f"{indentation}{content}")
return '\n'.join(output)
# Usage
template = """
title {{page_title}}
if show_author
author {{author_name}}
content
loop articles
article {{item.title}}
summary {{item.summary}}
"""
variables = {
'page_title': 'My Blog',
'show_author': True,
'author_name': 'John Doe',
'articles': [
{'title': 'First Post', 'summary': 'This is the first post'},
{'title': 'Second Post', 'summary': 'This is the second post'}
]
}
result = render_template(template, variables)
print(result)
Heading 3 Data Validation
class mt-8 mb-4
Markdown
Validate Terrace document structure against a schema.
CodeBlock python
from terrace import use_document, create_string_reader
from typing import Dict, List, Set, Optional
from dataclasses import dataclass
@dataclass
class ValidationError:
line_number: int
message: str
class TerraceValidator:
def __init__(self):
self.required_fields: Dict[str, Set[str]] = {}
self.allowed_fields: Dict[str, Set[str]] = {}
self.field_types: Dict[str, type] = {}
def require_fields(self, context: str, fields: List[str]):
"""Require specific fields in a context"""
self.required_fields[context] = set(fields)
def allow_fields(self, context: str, fields: List[str]):
"""Allow specific fields in a context"""
self.allowed_fields[context] = set(fields)
def validate(self, content: str) -> List[ValidationError]:
"""Validate content and return list of errors"""
reader = create_string_reader(content)
doc = use_document(reader)
errors = []
context_stack = ['root']
found_fields = {'root': set()}
for node in doc:
# Update context stack based on indentation
target_depth = node.level + 1
while len(context_stack) > target_depth:
# Check required fields when leaving context
leaving_context = context_stack.pop()
required = self.required_fields.get(leaving_context, set())
found = found_fields.get(leaving_context, set())
missing = required - found
if missing:
errors.append(ValidationError(
node.line_number,
f"Missing required fields in {leaving_context}: {', '.join(missing)}"
))
found_fields.pop(leaving_context, None)
current_context = context_stack[-1]
# Check if field is allowed
allowed = self.allowed_fields.get(current_context, None)
if allowed is not None and node.head not in allowed:
errors.append(ValidationError(
node.line_number,
f"Field '{node.head}' not allowed in context '{current_context}'"
))
# Track found fields
found_fields.setdefault(current_context, set()).add(node.head)
# If this node has children, it becomes a new context
if any(True for _ in node.children()): # Check if has children
context_stack.append(node.head)
found_fields[node.head] = set()
return errors
# Usage
validator = TerraceValidator()
validator.require_fields('root', ['title', 'content'])
validator.allow_fields('root', ['title', 'author', 'date', 'content'])
validator.allow_fields('content', ['section', 'paragraph'])
content = """
title My Document
content
section Introduction
paragraph Hello world
"""
errors = validator.validate(content)
for error in errors:
print(f"Line {error.line_number}: {error.message}")