Metadata-Version: 2.4
Name: glitterate
Version: 0.1.0
Summary: Extract annotated code blocks from markdown docs into runnable files
Project-URL: Homepage, https://github.com/SeriousBug/glitterate
Project-URL: Issues, https://github.com/SeriousBug/glitterate/issues
Author-email: Kaan Barmore-Genc <kaan@bgenc.dev>
License-Expression: MIT
License-File: LICENSE
Keywords: code blocks,documentation,markdown,testing
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Documentation
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# Glitterate

Extracts annotated code blocks out of markdown and writes them to real files, so CI can compile, lint, or run the examples in your docs. Language-agnostic: it only moves text around.

## Install

As a GitHub Action:

```yaml
- uses: actions/checkout@v4
- uses: SeriousBug/glitterate@v1
  with:
    input: '**/*.md'
    output-path-prefix: build/examples
- run: python build/examples/example.py
```

Or as a command:

```sh
pipx install glitterate   # or: uv tool install glitterate, pip install glitterate
glitterate --input '**/*.md' --output-path-prefix build/examples
```

Every action input below has a `--flag` of the same name.

## Directives

Annotate a fenced code block with an HTML comment on the line before it. The comment must sit directly before the block; blank lines between them are allowed, prose is not. Directives inside a code block are inert.

### Write a block to a file

````markdown
<!-- glitterate file="example.py" -->
```python
print("hello, world")
```
````

Writes `example.py`, under `output-path-prefix` if one is set.

### Append several blocks to one file

````markdown
<!-- glitterate append file="example.py" -->
```python
import sys
```

<!-- glitterate append file="example.py" -->
```python
print(sys.version)
```
````

The blocks are concatenated in document order.

### Append in an explicit order

````markdown
<!-- glitterate append=2 file="test.py" -->
```python
print(greet("world"))
```

<!-- glitterate append=1 file="test.py" -->
```python
def greet(name):
    return f"hello, {name}"
```
````

Blocks are concatenated by ascending number regardless of where they appear, so you can show the interesting part of an example first and the boilerplate later.

Rules for a single target file:

- Numbering must be consistent: either every append block for that file has an explicit number, or none do.
- Two blocks cannot claim the same `append=N`.
- A file cannot mix append and non-append blocks.
- When several markdown files append to the same target, their blocks are ordered by source path, then by position in the file.

### Hide setup code with `text=`

`text="..."` supplies the content in the directive itself, so it never renders in the markdown and no code block is consumed. Use it for what a reader does not need to see but the extracted file still needs: imports, a test harness, a `main()` call.

````markdown
<!-- glitterate append=1 file="example.py" text="
import sys
" -->

<!-- glitterate append=2 file="example.py" -->
```python
print(sys.version)
```
````

The reader sees one line; CI runs both. `text` combines with `append` and `append=N` like any other block.

The value runs from the opening `"` to the last `"` in the directive, so it can contain quotes of its own without escaping. Two consequences:

- `text` must be the last option in the directive.
- The text cannot contain `-->`, since that ends the HTML comment. Nothing else is escaped or substituted.

A leading newline right after the opening quote is dropped, common indentation is stripped, and a trailing newline is added if missing, so the literal can be indented to match the surrounding markdown.

### Allow an indented directive

A directive whose `<!--` line is indented four or more spaces is rejected:

```
docs/guide.md:12: directive is indented 4 spaces, so GitHub renders it as a code
block instead of hiding it; reduce the indentation of the <!-- line or set
allow-indented-directives: true
```

At that indentation CommonMark starts a code block, so the directive shows up as visible text in the rendered page and leaks whatever `text=` was hiding. Two or three spaces, the usual content column of a list item, are fine. Only the `<!--` line matters; the body of a `text=` literal and the closing `-->` can be indented freely.

The check measures raw indentation, so it has one false positive: a directive at the content column of a *nested* list is indented four or more spaces and still renders correctly. Add `indented` to that directive to accept it:

````markdown
- outer
  - inner step

    <!-- glitterate indented file="nested.py" -->
    ```python
    print("hello")
    ```
````

`allow-indented-directives: true` does the same for every file scanned. Prefer the per-directive keyword: it keeps the check active everywhere else, and it documents in the markdown that the indentation is deliberate.

Blocks in indented fences are dedented by the fence's own indentation, wherever they appear.

## Inputs

| Input | Default | Description |
| --- | --- | --- |
| `input` | `**/*.md` | Glob of markdown files to scan. One glob per line for several. |
| `exclude` | `` | Glob of markdown files to skip. One glob per line for several. |
| `output-path-prefix` | `` | Prefix every extracted file path with this directory. |
| `error-on-overwrite` | `true` | Fail if a file would be overwritten and the block is not in append mode. Covers both two blocks targeting one file and a file that already exists on disk. |
| `check` | `false` | Write nothing; fail if the files on disk differ from the markdown blocks. |
| `fail-if-empty` | `true` | Fail if no code blocks were extracted, which usually means a bad glob or a typo in a directive. |
| `allow-indented-directives` | `false` | Accept indented directives everywhere, rather than marking each one `indented`. |
| `manifest` | `` | Path to write a JSON manifest of extracted files and their source locations. |

Target paths are always relative and must stay inside `output-path-prefix`, so a stray `../` in a doc cannot write over the rest of the checkout. Absolute paths are rejected for the same reason.

## Outputs

| Output | Description |
| --- | --- |
| `files` | JSON array of the extracted file paths |
| `count` | Number of extracted files |

Drive a follow-up step with them:

```yaml
- uses: SeriousBug/glitterate@v1
  id: extract
  with:
    output-path-prefix: build/examples
- run: |
    for file in $(echo '${{ steps.extract.outputs.files }}' | jq -r '.[]'); do
      python "$file"
    done
```

The command prints the same paths, one per line.

## Check mode

`check: true` reverses the direction: nothing is written, and the run fails if a file on disk no longer matches the block that documents it. Use it when the examples are also real files in the repo and you want CI to catch docs drifting out of sync.

```yaml
- uses: SeriousBug/glitterate@v1
  with:
    input: docs/**/*.md
    check: true
```

## Manifest

`manifest: build/manifest.json` writes the mapping from extracted file back to the markdown that produced it, so a test runner can report failures against the doc:

```json
{
  "files": [
    {
      "path": "build/examples/example.py",
      "target": "example.py",
      "sources": [{ "file": "README.md", "line": 12 }]
    }
  ]
}
```

## Development

```sh
python3 -m pytest tests
```

`scripts/check_render.py` renders docs through GitHub's markdown API and fails if a directive survives into the rendered output, which is how the indentation rule above is verified:

```sh
GITHUB_TOKEN=$(gh auth token) python3 scripts/check_render.py 'docs/**/*.md'
```
