Metadata-Version: 2.4
Name: mtangle
Version: 0.2.0
Summary: A simple literate programming utility for markdown
Project-URL: Homepage, https://github.com/usergenic/mtangle
Project-URL: Repository, https://github.com/usergenic/mtangle
Author: Brendan Baldwin
License: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: pathspec>=0.12
Description-Content-Type: text/markdown

# mtangle

A simple literate-programming utility that extracts codeblocks from markdown
files and writes them to source files.

## What it does

Given a markdown file:

~~~markdown
```python file=src/something.py
def cool():
    return 0
```
~~~

Running `mtangle` writes the block's contents to `src/something.py`.

Codeblocks are recognized by attributes on the info string:

- `file=PATH` — write this block's contents to `PATH` (relative to the output
  directory).
- `id=NAME` — register this block under a name so it can be referenced from
  another block.

Blocks without either attribute are ignored.

## Composition via `<<name>>`

Blocks with `file=` can reference `id=` blocks using `<<name>>` markers on their
own line:

~~~markdown
```python id=cool_func
def cool():
    return 0
```

```python id=neat_func
def neat():
    return cool()
```

```python file=src/my_funcs.py
<<cool_func>>


<<neat_func>>
```
~~~

Produces `src/my_funcs.py`:

```python
def cool():
    return 0


def neat():
    return cool()
```

### Indentation

mtangle is language-agnostic. When a codeblock is parsed, its non-blank lines
are left-aligned against the block's least-indented line. When a `<<marker>>`
is substituted, the substituted content is re-indented to match the marker's
column. This handles the common case of embedding a snippet inside an already-
indented body.

### Escaping

- `<< name >>` (spaces inside) is not a marker — emitted literally.
- `\<<name>>` emits `<<name>>` literally (no substitution).
- `\\` emits a literal backslash.
- To emit a literal `\<<name>>`: use `\\\<<name>>`.

### Custom delimiters

If `<<` / `>>` conflict with the language you're writing (e.g. C++ stream
operators), use `-d/--delimiters` to pick a different pair:

```
mtangle -d "<<< >>>"
```

Or in `.mtangle`:

```
delimiters=<<< >>>
```

Delimiters must be non-empty, whitespace-free, and distinct. The escape
prefix is always `\` regardless of the delimiter (e.g. `\<<<name>>>`).

## Merging into the same file

Multiple codeblocks targeting the same `file=` are concatenated in the order
they are encountered (across all input markdown files, sorted by path).

## CLI

```
mtangle [SOURCES...] [-o OUTPUT_DIR] [OPTIONS]
```

With no arguments, `mtangle` is equivalent to `mtangle . -o .` — it recursively
scans the current directory for `.md` files and writes tangled output to the
current directory.

### Options

| Flag | Description |
|------|-------------|
| `-v`, `--version` | Print version and exit |
| `-o`, `--output-dir DIR` | Where to write files (default: cwd) |
| `-n`, `--dry-run[=on\|off]` | Parse and resolve everything but write nothing (default: off) |
| `-V`, `--verbose[=on\|off]` | Print progress to stderr (default: off) |
| `-i`, `--ignore PATTERN` | Gitignore-style skip pattern (repeatable) |
| `--ignore-dotfiles[=on\|off]` | Skip dotfiles/dotdirs (default: on) |
| `--use-gitignore[=on\|off]` | Apply `.gitignore` at each input-dir root (default: off) |
| `--path-safety[=on\|off]` | Refuse `file=` targets outside the output dir (default: on) |
| `-d`, `--delimiters "OPEN CLOSE"` | Substitution marker delimiters (default: `"<< >>"`) |

Boolean flags accept `=on` or `=off`. Bare form (e.g. `--verbose`) implies
`=on`.

### Ignore behavior

- Dotfiles and dotdirs (`.git`, `.venv`, etc.) are skipped by default.
- `.gitignore` is **not** consulted unless `--respect-gitignore` is set.
- Explicitly-named files (e.g. `mtangle README.md`) bypass all ignore rules.
- See the `.mtangle` config file section below for project-level ignore
  patterns.

### `.mtangle` config file

If a `.mtangle` file exists in the current working directory, mtangle reads
it before processing arguments. Each non-empty, non-`#` line names a CLI
flag in `key=value` (or bare `key` for booleans) form:

```
source=docs/**/*.md
source=examples
output-dir=build
ignore=vendor/
ignore=**/scratch/
verbose
use-gitignore=on
path-safety=off
```

Supported keys mirror the CLI: `source`, `output-dir`, `ignore`, `verbose`,
`dry-run`, `ignore-dotfiles`, `use-gitignore`, `path-safety`, `delimiters`.
Booleans take `on`/`off`; a bare key implies `on`. `source` and `ignore`
may repeat.

CLI arguments override the file for scalars (`output-dir`), union with it
for repeatables (`source`, `ignore`), and OR with it for booleans.

### Path safety

By default, `file=` targets that are absolute or that resolve outside the
output directory are warned and skipped. Use `--disable-path-safety` to opt
out.

## Install

```
uv pip install git+https://github.com/usergenic/mtangle.git
```

Or add to your project:

```
uv add git+https://github.com/usergenic/mtangle.git
```

## Development

```
uv sync
uv run pytest
```
