# API Reference

URL: /patitas/docs/reference/api/
Section: reference
Description: Functions, classes, and modules

---

> For a complete page index, fetch /patitas/llms.txt.

Core API for parsing and rendering Markdown.

## Public API Boundary

The stable integration surface is the top-level `patitas` package. Prefer imports like:

```python
from patitas import Markdown, parse, render, Document, Heading
```

Names exported from `patitas.__init__` are treated as downstream contracts. That includes
the parser facade, AST node types, source locations, parse cache helpers, visitor/diff
helpers, serialization helpers, sanitization helpers, directive registry builders, and
renderer protocols documented on this page.

Subpackages such as `patitas.lexer`, `patitas.parser`, `patitas.parsing`,
`patitas.renderers.html`, and implementation modules under `patitas.directives`,
`patitas.roles`, or `patitas.plugins` may still be useful for advanced users, but they are
not the preferred compatibility boundary before 1.0. If an integration needs an internal
module, open an issue so the missing public contract can be promoted intentionally.

## High-Level API

### parse()

Parse Markdown source into a typed AST.

```python
def parse(
    source: str,
    *,
    source_file: str | None = None,
    directive_registry: DirectiveRegistry | None = None,
    cache: ParseCache | None = None,
) -> Document
```

**Parameters:**
- `source`: Markdown source text
- `source_file`: Optional source file path for error messages
- `directive_registry`: Custom directive registry (uses defaults if None)
- `cache`: Optional content-addressed parse cache (see [Parse Cache](#parse-cache))

**Returns:** Document AST root node

**Example:**

```python
from patitas import parse

doc = parse("# Hello **World**")
print(doc.children[0])  # Heading(level=1, ...)

# With parse cache (faster incremental builds)
from patitas import parse, DictParseCache
cache = DictParseCache()
doc1 = parse("# Hello", cache=cache)
doc2 = parse("# Hello", cache=cache)  # Cache hit, no re-parse
```

### render()

Render a Patitas AST to HTML.

```python
def render(
    doc: Document,
    *,
    source: str = "",
    highlight: bool = False,
    directive_registry: DirectiveRegistry | None = None,
) -> str
```

**Parameters:**
- `doc`: Document AST to render
- `source`: Original Markdown source for zero-copy extraction
- `highlight`: Enable syntax highlighting for code blocks
- `directive_registry`: Custom directive registry for rendering

**Returns:** Rendered HTML string

**Example:**

```python
from patitas import parse, render

doc = parse("# Hello")
html = render(doc, source="# Hello")
print(html)  # <h1>Hello</h1>
```

### render_llm()

Render a Patitas AST to structured plain text for LLM consumption. No HTML; explicit
labels for code (`[code:lang]`), math (`[math] ... [/math]`), images (`[image: alt]`).
Skips HtmlBlock and HtmlInline for safety. Useful for RAG retrieval, context windows,
and model input.

```python
def render_llm(doc: Document, *, source: str = "") -> str
```

**Parameters:**
- `doc`: Document AST to render
- `source`: Original Markdown source for FencedCode zero-copy extraction

**Returns:** Structured plain text string

**Example:**

```python
from patitas import parse, render_llm

source = "# Hello **World**\n\n- item\n\n```python\nx = 1\n```"
doc = parse(source)
text = render_llm(doc, source=source)
# '# Hello World\n\n- item\n\n[code:python]\nx = 1\n[/code]\n\n'
```

See [LLM Safety](/docs/extending/llm-safety/) for the full parse → sanitize → render_llm pipeline.

### extract_text()

Extract plain text from any AST node. Skips HtmlBlock and HtmlInline. Used for heading
slugs, excerpts, and LLM pipelines.

```python
def extract_text(node: Node, *, source: str = "") -> str
```

**Parameters:**
- `node`: Any AST node (block or inline)
- `source`: Original source (required for FencedCode zero-copy; use `""` if unavailable)

**Returns:** Concatenated plain text from the node and its descendants

**Example:**

```python
from patitas import parse, extract_text

doc = parse("# Hello **World**")
extract_text(doc.children[0])  # 'Hello World'
```

### extract_excerpt() / extract_meta_description()

Structurally correct excerpt extraction that stops at block boundaries. Avoids mid-markdown
truncation and properly handles headings, paragraphs, and lists.

```python
def extract_excerpt(
    ast: Document | Sequence[Block],
    source: str = "",
    *,
    max_chars: int = 750,
    skip_leading_h1: bool = True,
    include_headings: bool = True,
    excerpt_as_html: bool = False,
) -> str

def extract_meta_description(
    ast: Document | Sequence[Block],
    source: str = "",
    *,
    max_chars: int = 160,
) -> str
```

**extract_excerpt** — Walks blocks in order, extracting text. Stops at block boundaries when
`max_chars` is reached. Optional HTML output with `<p>`, `<div class="excerpt-heading">`.

**extract_meta_description** — SEO-friendly ~160 chars, truncated at sentence boundary.

**Example:**

```python
from patitas import parse, extract_excerpt, extract_meta_description

source = "# Title\n\nFirst paragraph. Second sentence."
doc = parse(source)
extract_excerpt(doc, source)                        # Plain text
extract_excerpt(doc, source, excerpt_as_html=True) # HTML with structure
extract_meta_description(doc, source)               # ~160 chars for meta tags
```

### sanitize()

Apply a composable sanitization policy to strip unsafe content before LLM consumption or
web rendering. Policies compose via the `|` operator.

```python
def sanitize(doc: Document, *, policy: Policy | Callable[[Document], Document]) -> Document
```

**Parameters:**
- `doc`: Document to sanitize
- `policy`: `Policy` instance or callable `Document -> Document`

**Returns:** Sanitized document (immutable; original unchanged)

**Pre-built policies** (from `patitas.sanitize`):
- `llm_safe` — Strip HTML, dangerous URLs (javascript:, data:, vbscript:), zero-width/bidi chars (Trojan Source mitigation). Use for LLM context.
- `web_safe` — Alias for `llm_safe`. Same policy for web display of untrusted content.
- `strict` — llm_safe + strip images (replace with alt text) + strip raw code blocks

**Composable policies:** `strip_html`, `strip_html_comments`, `strip_dangerous_urls`,
`normalize_unicode`, `strip_images`, `strip_raw_code`. Use `allow_url_schemes(*schemes)` for
custom URL filtering.

**Example:**

```python
from patitas import parse, sanitize
from patitas.sanitize import llm_safe, strip_html, strip_dangerous_urls

doc = parse("# Title\n\n<script>alert(1)</script>\n\n[link](javascript:void(0))")
clean = sanitize(doc, policy=llm_safe)

# Custom policy
custom = strip_html | strip_dangerous_urls
clean = sanitize(doc, policy=custom)
```

See [LLM Safety](/docs/extending/llm-safety/) for the full pipeline.

### parse_notebook()

Parse a Jupyter notebook (`.ipynb`) to Markdown content and metadata. Zero dependencies — uses stdlib `json` only. Supports nbformat 4 and 5.

```python
def parse_notebook(
    content: str,
    source_path: Path | str | None = None,
) -> tuple[str, dict[str, Any]]
```

**Parameters:**
- `content`: Raw JSON content of the `.ipynb` file (caller handles I/O)
- `source_path`: Optional path for title fallback when notebook has no title

**Returns:** Tuple of `(markdown_content, metadata_dict)`

- `markdown_content`: Markdown string — markdown cells as-is, code cells as fenced blocks, outputs as HTML
- `metadata`: Dict with `title`, `type: "notebook"`, `notebook.kernel_name`, `notebook.cell_count`, etc.

**Raises:**
- `json.JSONDecodeError`: If content is not valid JSON
- `ValueError`: If nbformat is 3 or older

**Example:**

```python
from patitas import parse_notebook

with open("demo.ipynb") as f:
    content, metadata = parse_notebook(f.read(), "demo.ipynb")

# content: Markdown string ready for parse() or render
# metadata: title, type, notebook{kernel_name, cell_count}, etc.
print(metadata["notebook"]["kernel_name"])  # e.g. "python3"
```

Used by [Bengal](https://github.com/lbliii/bengal) for native notebook rendering — drop `.ipynb` into content and build.

### Markdown

High-level processor combining parsing and rendering.

```python
class Markdown:
    def __init__(
        self,
        *,
        highlight: bool = False,
        plugins: list[str] | None = None,
        directive_registry: DirectiveRegistry | None = None,
    ) -> None: ...

    def __call__(self, source: str) -> str: ...
    def parse(self, source: str, *, source_file: str | None = None, cache: ParseCache | None = None) -> Document: ...
    def parse_many(self, sources: Iterable[str], *, source_file: str | None = None, cache: ParseCache | None = None) -> list[Document]: ...
    def render(self, doc: Document, *, source: str = "") -> str: ...
```

**Example:**

```python
from patitas import Markdown

md = Markdown()
html = md("# Hello **World**")
print(html)  # <h1>Hello <strong>World</strong></h1>

# With plugins
md = Markdown(plugins=["table", "math", "strikethrough"])
html = md("| a | b |\n|---|---|\n| 1 | 2 |")
```

## Parse Cache

Content-addressed cache for parsed ASTs. Key is `(content_hash, config_hash)`; value is
`Document`. Enables faster incremental builds (undo/revert, duplicate content) and can
replace path-based snapshot caches in consumers like Bengal.

### ParseCache protocol

```python
class ParseCache(Protocol):
    def get(self, content_hash: str, config_hash: str) -> Document | None: ...
    def put(self, content_hash: str, config_hash: str, doc: Document) -> None: ...
```

### DictParseCache

In-memory implementation. Not thread-safe — for parallel parsing, use a cache with
internal locking.

```python
from patitas import parse, DictParseCache

cache = DictParseCache()
doc = parse("# Hello", cache=cache)
# Second call with same source hits cache
doc2 = parse("# Hello", cache=cache)
```

### hash_content() / hash_config()

Compute cache keys. `hash_content(source)` returns SHA256 of source. `hash_config(config)`
returns config hash, or `""` when `text_transformer` is set (cache bypassed).

See [Performance](/docs/about/performance/) for optimization details and
[Serialization](/docs/extending/serialization/) for persistence patterns.

## Serialization API

Convert AST nodes to/from JSON-compatible dicts and strings. Deterministic output
for cache-key stability. Useful for caching parsed ASTs (Bengal incremental builds)
and sending ASTs over the wire (Purr SSE).

### to_dict() / from_dict()

In-memory dict format — use for caching or when you need to inspect or modify
the structure before serializing to JSON.

```python
from patitas import parse, to_dict, from_dict

doc = parse("# Hello **World**")
data = to_dict(doc)
restored = from_dict(data)
assert doc == restored
```

**to_dict(node: Node) -> dict[str, Any]**

- `node`: Any AST node (Document, Heading, Paragraph, etc.)
- **Returns:** JSON-compatible dict with `_type` discriminator

**from_dict(data: dict[str, Any]) -> Node**

- `data`: Dict produced by `to_dict`
- **Returns:** Reconstructed typed AST node

### to_json() / from_json()

JSON string format — use for persistence, wire transfer, or human inspection.

```python
from patitas import parse, to_json, from_json

doc = parse("# Hello **World**")
json_str = to_json(doc)
restored = from_json(json_str)
assert doc == restored
```

**to_json(doc: Document, *, indent: int | None = None) -> str**

- `doc`: Document AST root
- `indent`: Optional indent for pretty-printing (None = compact)

**from_json(data: str) -> Document**

- `data`: JSON string from `to_json`
- **Returns:** Reconstructed Document

See [Serialization](/docs/extending/serialization/) for caching and wire-transfer patterns.

## Configuration API

Thread-local configuration for advanced use cases.

### ParseConfig

Immutable configuration dataclass.

```python
from patitas import ParseConfig

config = ParseConfig(
    tables_enabled=True,
    math_enabled=True,
    strikethrough_enabled=False,
    task_lists_enabled=False,
    footnotes_enabled=False,
    autolinks_enabled=False,
    directive_registry=None,
    strict_contracts=False,
    text_transformer=None,
)
```

### ParseConfig.from_dict()

Create a `ParseConfig` from a dictionary. Unknown keys are silently ignored,
making this safe for framework integration where config may come from YAML
files or external sources.

```python
from patitas import ParseConfig

config = ParseConfig.from_dict({
    "tables_enabled": True,
    "math_enabled": True,
    "unknown_key": "silently ignored",
})
# config.tables_enabled == True
# config.math_enabled == True
```

### parse_config_context()

Context manager for temporary config changes.

```python
from patitas import parse_config_context, ParseConfig, Parser

with parse_config_context(ParseConfig(tables_enabled=True)):
    parser = Parser("| a | b |")
    result = parser.parse()
# Config automatically reset after context
```

### get/set/reset functions

```python
from patitas import get_parse_config, set_parse_config, reset_parse_config

# Get current config
config = get_parse_config()

# Set custom config
set_parse_config(ParseConfig(math_enabled=True))

# Reset to defaults
reset_parse_config()
```

## Low-Level API

### Parser

The Markdown parser. Configuration is read from ContextVar.

```python
from patitas import Parser, parse_config_context, ParseConfig

# Simple usage (uses default config)
parser = Parser(source, source_file="example.md")
doc = parser.parse()

# With custom config
with parse_config_context(ParseConfig(tables_enabled=True)):
    parser = Parser(source)
    doc = parser.parse()
```

### Lexer

The state-machine lexer.

```python
from patitas.lexer import Lexer

lexer = Lexer(source)
tokens = list(lexer)
```

### HtmlRenderer

The HTML renderer.

```python
from patitas.renderers.html import HtmlRenderer

renderer = HtmlRenderer(source=source)
html = renderer.render(doc)
```

### LlmRenderer

The LLM-optimized renderer. Outputs structured plain text for model consumption.

```python
from patitas.renderers.llm import LlmRenderer

renderer = LlmRenderer(source=source)
text = renderer.render(doc)
```

## Extension Points

### set_highlighter()

Set the global syntax highlighter. Accepts a `Highlighter` protocol implementation or a
simple callable `(code: str, language: str) -> str`.

```python
from patitas.highlighting import set_highlighter

# Simple callable
set_highlighter(lambda code, lang: f"<pre><code class='{lang}'>{code}</code></pre>")
# Or pass None to clear
set_highlighter(None)
```

### set_icon_resolver()

Set the global icon resolver. Takes a callable `(name: str) -> str | None`.

```python
from patitas.icons import set_icon_resolver

set_icon_resolver(lambda name: f"<span class='icon-{name}'></span>")
# Or pass None to clear
set_icon_resolver(None)
```
