Metadata-Version: 2.4
Name: mtangle
Version: 0.1.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>>`.

## 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).
mtangle warns when this happens.

## 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 |
|------|-------------|
| `-o`, `--output-dir DIR` | Where to write files (default: cwd) |
| `-n`, `--dry-run` | Parse and resolve everything but write nothing |
| `-v`, `--verbose` | Print progress to stderr |
| `-i`, `--ignore PATTERN` | Gitignore-style skip pattern (repeatable) |
| `--disable-ignore-dotfiles` | Include dotfiles/dotdirs (default: skipped) |
| `--respect-gitignore` | Also apply `.gitignore` at each input-dir root |
| `--disable-path-safety` | Permit `file=` targets outside the output dir |

### Ignore behavior

- Dotfiles and dotdirs (`.git`, `.venv`, etc.) are skipped by default.
- A `.mtangleignore` file at the root of each input directory is respected;
  every non-empty, non-`#` line is treated like an `-i` pattern
  (gitignore syntax).
- `.gitignore` is **not** consulted unless `--respect-gitignore` is set.
- Explicitly-named files (e.g. `mtangle README.md`) bypass all ignore rules.

### 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
```
