Updates.
This commit is contained in:
51
packages/python/docs/core-api.inc.tce
Normal file
51
packages/python/docs/core-api.inc.tce
Normal 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
|
||||
153
packages/python/docs/document-api.inc.tce
Normal file
153
packages/python/docs/document-api.inc.tce
Normal 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()
|
||||
47
packages/python/docs/index.tce
Normal file
47
packages/python/docs/index.tce
Normal 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
|
||||
175
packages/python/docs/reader-api.inc.tce
Normal file
175
packages/python/docs/reader-api.inc.tce
Normal 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}")
|
||||
265
packages/python/docs/recipes.inc.tce
Normal file
265
packages/python/docs/recipes.inc.tce
Normal 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}")
|
||||
Reference in New Issue
Block a user