mtangle 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mtangle-0.1.0/.gitignore +3 -0
- mtangle-0.1.0/.mtangleignore +1 -0
- mtangle-0.1.0/LICENSE +21 -0
- mtangle-0.1.0/PKG-INFO +151 -0
- mtangle-0.1.0/README.md +138 -0
- mtangle-0.1.0/mtangle/__init__.py +3 -0
- mtangle-0.1.0/mtangle/cli.py +99 -0
- mtangle-0.1.0/mtangle/core.py +417 -0
- mtangle-0.1.0/pyproject.toml +26 -0
- mtangle-0.1.0/tests/__init__.py +0 -0
- mtangle-0.1.0/tests/fixtures/basic_file/expected/src/something.py +2 -0
- mtangle-0.1.0/tests/fixtures/basic_file/input.md +8 -0
- mtangle-0.1.0/tests/fixtures/id_substitution/expected/src/my_funcs.py +6 -0
- mtangle-0.1.0/tests/fixtures/id_substitution/input.md +18 -0
- mtangle-0.1.0/tests/fixtures/indentation/expected/src/wrapper.py +4 -0
- mtangle-0.1.0/tests/fixtures/indentation/input.md +18 -0
- mtangle-0.1.0/tests/fixtures/multi_block_same_file/expected/src/combined.py +4 -0
- mtangle-0.1.0/tests/fixtures/multi_block_same_file/input.md +11 -0
- mtangle-0.1.0/tests/fixtures/multi_file_same_target/a.md +4 -0
- mtangle-0.1.0/tests/fixtures/multi_file_same_target/b.md +4 -0
- mtangle-0.1.0/tests/fixtures/multi_file_same_target/expected/src/shared.py +4 -0
- mtangle-0.1.0/tests/fixtures/nested_substitution/expected/src/nested.py +2 -0
- mtangle-0.1.0/tests/fixtures/nested_substitution/input.md +14 -0
- mtangle-0.1.0/tests/fixtures/untagged_ignored/expected/src/only_this.py +1 -0
- mtangle-0.1.0/tests/fixtures/untagged_ignored/input.md +9 -0
- mtangle-0.1.0/tests/test_core.py +341 -0
- mtangle-0.1.0/tests/test_fixtures.py +44 -0
- mtangle-0.1.0/uv.lock +169 -0
mtangle-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tests/
|
mtangle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brendan Baldwin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mtangle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mtangle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple literate programming utility for markdown
|
|
5
|
+
Project-URL: Homepage, https://github.com/usergenic/mtangle
|
|
6
|
+
Project-URL: Repository, https://github.com/usergenic/mtangle
|
|
7
|
+
Author: Brendan Baldwin
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: pathspec>=0.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# mtangle
|
|
15
|
+
|
|
16
|
+
A simple literate-programming utility that extracts codeblocks from markdown
|
|
17
|
+
files and writes them to source files.
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
Given a markdown file:
|
|
22
|
+
|
|
23
|
+
~~~markdown
|
|
24
|
+
```python file=src/something.py
|
|
25
|
+
def cool():
|
|
26
|
+
return 0
|
|
27
|
+
```
|
|
28
|
+
~~~
|
|
29
|
+
|
|
30
|
+
Running `mtangle` writes the block's contents to `src/something.py`.
|
|
31
|
+
|
|
32
|
+
Codeblocks are recognized by attributes on the info string:
|
|
33
|
+
|
|
34
|
+
- `file=PATH` — write this block's contents to `PATH` (relative to the output
|
|
35
|
+
directory).
|
|
36
|
+
- `id=NAME` — register this block under a name so it can be referenced from
|
|
37
|
+
another block.
|
|
38
|
+
|
|
39
|
+
Blocks without either attribute are ignored.
|
|
40
|
+
|
|
41
|
+
## Composition via `<<name>>`
|
|
42
|
+
|
|
43
|
+
Blocks with `file=` can reference `id=` blocks using `<<name>>` markers on their
|
|
44
|
+
own line:
|
|
45
|
+
|
|
46
|
+
~~~markdown
|
|
47
|
+
```python id=cool_func
|
|
48
|
+
def cool():
|
|
49
|
+
return 0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python id=neat_func
|
|
53
|
+
def neat():
|
|
54
|
+
return cool()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```python file=src/my_funcs.py
|
|
58
|
+
<<cool_func>>
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
<<neat_func>>
|
|
62
|
+
```
|
|
63
|
+
~~~
|
|
64
|
+
|
|
65
|
+
Produces `src/my_funcs.py`:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
def cool():
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def neat():
|
|
73
|
+
return cool()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Indentation
|
|
77
|
+
|
|
78
|
+
mtangle is language-agnostic. When a codeblock is parsed, its non-blank lines
|
|
79
|
+
are left-aligned against the block's least-indented line. When a `<<marker>>`
|
|
80
|
+
is substituted, the substituted content is re-indented to match the marker's
|
|
81
|
+
column. This handles the common case of embedding a snippet inside an already-
|
|
82
|
+
indented body.
|
|
83
|
+
|
|
84
|
+
### Escaping
|
|
85
|
+
|
|
86
|
+
- `<< name >>` (spaces inside) is not a marker — emitted literally.
|
|
87
|
+
- `\<<name>>` emits `<<name>>` literally (no substitution).
|
|
88
|
+
- `\\` emits a literal backslash.
|
|
89
|
+
- To emit a literal `\<<name>>`: use `\\\<<name>>`.
|
|
90
|
+
|
|
91
|
+
## Merging into the same file
|
|
92
|
+
|
|
93
|
+
Multiple codeblocks targeting the same `file=` are concatenated in the order
|
|
94
|
+
they are encountered (across all input markdown files, sorted by path).
|
|
95
|
+
mtangle warns when this happens.
|
|
96
|
+
|
|
97
|
+
## CLI
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
mtangle [SOURCES...] [-o OUTPUT_DIR] [OPTIONS]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
With no arguments, `mtangle` is equivalent to `mtangle . -o .` — it recursively
|
|
104
|
+
scans the current directory for `.md` files and writes tangled output to the
|
|
105
|
+
current directory.
|
|
106
|
+
|
|
107
|
+
### Options
|
|
108
|
+
|
|
109
|
+
| Flag | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `-o`, `--output-dir DIR` | Where to write files (default: cwd) |
|
|
112
|
+
| `-n`, `--dry-run` | Parse and resolve everything but write nothing |
|
|
113
|
+
| `-v`, `--verbose` | Print progress to stderr |
|
|
114
|
+
| `-i`, `--ignore PATTERN` | Gitignore-style skip pattern (repeatable) |
|
|
115
|
+
| `--disable-ignore-dotfiles` | Include dotfiles/dotdirs (default: skipped) |
|
|
116
|
+
| `--respect-gitignore` | Also apply `.gitignore` at each input-dir root |
|
|
117
|
+
| `--disable-path-safety` | Permit `file=` targets outside the output dir |
|
|
118
|
+
|
|
119
|
+
### Ignore behavior
|
|
120
|
+
|
|
121
|
+
- Dotfiles and dotdirs (`.git`, `.venv`, etc.) are skipped by default.
|
|
122
|
+
- A `.mtangleignore` file at the root of each input directory is respected;
|
|
123
|
+
every non-empty, non-`#` line is treated like an `-i` pattern
|
|
124
|
+
(gitignore syntax).
|
|
125
|
+
- `.gitignore` is **not** consulted unless `--respect-gitignore` is set.
|
|
126
|
+
- Explicitly-named files (e.g. `mtangle README.md`) bypass all ignore rules.
|
|
127
|
+
|
|
128
|
+
### Path safety
|
|
129
|
+
|
|
130
|
+
By default, `file=` targets that are absolute or that resolve outside the
|
|
131
|
+
output directory are warned and skipped. Use `--disable-path-safety` to opt
|
|
132
|
+
out.
|
|
133
|
+
|
|
134
|
+
## Install
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
uv pip install git+https://github.com/usergenic/mtangle.git
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Or add to your project:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
uv add git+https://github.com/usergenic/mtangle.git
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
uv sync
|
|
150
|
+
uv run pytest
|
|
151
|
+
```
|
mtangle-0.1.0/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# mtangle
|
|
2
|
+
|
|
3
|
+
A simple literate-programming utility that extracts codeblocks from markdown
|
|
4
|
+
files and writes them to source files.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
Given a markdown file:
|
|
9
|
+
|
|
10
|
+
~~~markdown
|
|
11
|
+
```python file=src/something.py
|
|
12
|
+
def cool():
|
|
13
|
+
return 0
|
|
14
|
+
```
|
|
15
|
+
~~~
|
|
16
|
+
|
|
17
|
+
Running `mtangle` writes the block's contents to `src/something.py`.
|
|
18
|
+
|
|
19
|
+
Codeblocks are recognized by attributes on the info string:
|
|
20
|
+
|
|
21
|
+
- `file=PATH` — write this block's contents to `PATH` (relative to the output
|
|
22
|
+
directory).
|
|
23
|
+
- `id=NAME` — register this block under a name so it can be referenced from
|
|
24
|
+
another block.
|
|
25
|
+
|
|
26
|
+
Blocks without either attribute are ignored.
|
|
27
|
+
|
|
28
|
+
## Composition via `<<name>>`
|
|
29
|
+
|
|
30
|
+
Blocks with `file=` can reference `id=` blocks using `<<name>>` markers on their
|
|
31
|
+
own line:
|
|
32
|
+
|
|
33
|
+
~~~markdown
|
|
34
|
+
```python id=cool_func
|
|
35
|
+
def cool():
|
|
36
|
+
return 0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python id=neat_func
|
|
40
|
+
def neat():
|
|
41
|
+
return cool()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python file=src/my_funcs.py
|
|
45
|
+
<<cool_func>>
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
<<neat_func>>
|
|
49
|
+
```
|
|
50
|
+
~~~
|
|
51
|
+
|
|
52
|
+
Produces `src/my_funcs.py`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
def cool():
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def neat():
|
|
60
|
+
return cool()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Indentation
|
|
64
|
+
|
|
65
|
+
mtangle is language-agnostic. When a codeblock is parsed, its non-blank lines
|
|
66
|
+
are left-aligned against the block's least-indented line. When a `<<marker>>`
|
|
67
|
+
is substituted, the substituted content is re-indented to match the marker's
|
|
68
|
+
column. This handles the common case of embedding a snippet inside an already-
|
|
69
|
+
indented body.
|
|
70
|
+
|
|
71
|
+
### Escaping
|
|
72
|
+
|
|
73
|
+
- `<< name >>` (spaces inside) is not a marker — emitted literally.
|
|
74
|
+
- `\<<name>>` emits `<<name>>` literally (no substitution).
|
|
75
|
+
- `\\` emits a literal backslash.
|
|
76
|
+
- To emit a literal `\<<name>>`: use `\\\<<name>>`.
|
|
77
|
+
|
|
78
|
+
## Merging into the same file
|
|
79
|
+
|
|
80
|
+
Multiple codeblocks targeting the same `file=` are concatenated in the order
|
|
81
|
+
they are encountered (across all input markdown files, sorted by path).
|
|
82
|
+
mtangle warns when this happens.
|
|
83
|
+
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
mtangle [SOURCES...] [-o OUTPUT_DIR] [OPTIONS]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
With no arguments, `mtangle` is equivalent to `mtangle . -o .` — it recursively
|
|
91
|
+
scans the current directory for `.md` files and writes tangled output to the
|
|
92
|
+
current directory.
|
|
93
|
+
|
|
94
|
+
### Options
|
|
95
|
+
|
|
96
|
+
| Flag | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `-o`, `--output-dir DIR` | Where to write files (default: cwd) |
|
|
99
|
+
| `-n`, `--dry-run` | Parse and resolve everything but write nothing |
|
|
100
|
+
| `-v`, `--verbose` | Print progress to stderr |
|
|
101
|
+
| `-i`, `--ignore PATTERN` | Gitignore-style skip pattern (repeatable) |
|
|
102
|
+
| `--disable-ignore-dotfiles` | Include dotfiles/dotdirs (default: skipped) |
|
|
103
|
+
| `--respect-gitignore` | Also apply `.gitignore` at each input-dir root |
|
|
104
|
+
| `--disable-path-safety` | Permit `file=` targets outside the output dir |
|
|
105
|
+
|
|
106
|
+
### Ignore behavior
|
|
107
|
+
|
|
108
|
+
- Dotfiles and dotdirs (`.git`, `.venv`, etc.) are skipped by default.
|
|
109
|
+
- A `.mtangleignore` file at the root of each input directory is respected;
|
|
110
|
+
every non-empty, non-`#` line is treated like an `-i` pattern
|
|
111
|
+
(gitignore syntax).
|
|
112
|
+
- `.gitignore` is **not** consulted unless `--respect-gitignore` is set.
|
|
113
|
+
- Explicitly-named files (e.g. `mtangle README.md`) bypass all ignore rules.
|
|
114
|
+
|
|
115
|
+
### Path safety
|
|
116
|
+
|
|
117
|
+
By default, `file=` targets that are absolute or that resolve outside the
|
|
118
|
+
output directory are warned and skipped. Use `--disable-path-safety` to opt
|
|
119
|
+
out.
|
|
120
|
+
|
|
121
|
+
## Install
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
uv pip install git+https://github.com/usergenic/mtangle.git
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or add to your project:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
uv add git+https://github.com/usergenic/mtangle.git
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
uv sync
|
|
137
|
+
uv run pytest
|
|
138
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mtangle.core import TangleError, tangle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: list[str] | None = None) -> int:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="mtangle",
|
|
13
|
+
description="Tangle codeblocks out of markdown files into source files.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"sources",
|
|
17
|
+
nargs="*",
|
|
18
|
+
help="Markdown files or directories to scan. Defaults to '.'",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-o",
|
|
22
|
+
"--output-dir",
|
|
23
|
+
default=".",
|
|
24
|
+
help="Directory to write tangled sources into (default: cwd)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-n",
|
|
28
|
+
"--dry-run",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="Parse and resolve everything but don't write any files",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-v",
|
|
34
|
+
"--verbose",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Print progress details to stderr",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--disable-path-safety",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help=(
|
|
42
|
+
"Allow file= targets to be absolute or resolve outside the output "
|
|
43
|
+
"directory. Off by default; such targets are warned and skipped."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"-i",
|
|
48
|
+
"--ignore",
|
|
49
|
+
action="append",
|
|
50
|
+
default=[],
|
|
51
|
+
metavar="PATTERN",
|
|
52
|
+
help=(
|
|
53
|
+
"Gitignore-style pattern to skip during directory walks. "
|
|
54
|
+
"May be given multiple times."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--disable-ignore-dotfiles",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Include dotfiles/dotdirs in directory walks (default: skipped).",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--respect-gitignore",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Also apply .gitignore patterns at each walk root (default: off).",
|
|
66
|
+
)
|
|
67
|
+
args = parser.parse_args(argv)
|
|
68
|
+
|
|
69
|
+
sources = args.sources or ["."]
|
|
70
|
+
inputs = [Path(s) for s in sources]
|
|
71
|
+
output_dir = Path(args.output_dir)
|
|
72
|
+
|
|
73
|
+
def info(msg: str) -> None:
|
|
74
|
+
if args.verbose:
|
|
75
|
+
print(msg, file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
results = tangle(
|
|
79
|
+
inputs,
|
|
80
|
+
output_dir,
|
|
81
|
+
on_info=info,
|
|
82
|
+
dry_run=args.dry_run,
|
|
83
|
+
path_safety=not args.disable_path_safety,
|
|
84
|
+
ignore_dotfiles=not args.disable_ignore_dotfiles,
|
|
85
|
+
ignore_patterns=args.ignore,
|
|
86
|
+
respect_gitignore=args.respect_gitignore,
|
|
87
|
+
)
|
|
88
|
+
except TangleError as e:
|
|
89
|
+
print(f"error: {e}", file=sys.stderr)
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
verb = "would write" if args.dry_run else "wrote"
|
|
93
|
+
for path in sorted(results):
|
|
94
|
+
print(f"{verb} {path}")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
raise SystemExit(main())
|