265 lines
8.8 KiB
Plaintext
265 lines
8.8 KiB
Plaintext
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}") |