markdown-toc-generator 1.0.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.
- markdown_toc_generator-1.0.0/LICENSE +21 -0
- markdown_toc_generator-1.0.0/PKG-INFO +139 -0
- markdown_toc_generator-1.0.0/README.md +98 -0
- markdown_toc_generator-1.0.0/pyproject.toml +38 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/__init__.py +0 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/__main__.py +3 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/arguments.py +93 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/heading.py +15 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/output_toc.py +82 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/parse_headings.py +32 -0
- markdown_toc_generator-1.0.0/src/markdown_toc_generator/toc.py +58 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Electronic Mango
|
|
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.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: markdown-toc-generator
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python scripts generating Table of Contents from markdown headers.
|
|
5
|
+
Keywords: Markdown,ToC,toc,table-of-contents,Table of Contents
|
|
6
|
+
Author: Electronic Mango
|
|
7
|
+
Author-email: Electronic Mango <78230210+Electronic-Mango@users.noreply.github.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Electronic Mango
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
36
|
+
Requires-Python: >=3.14
|
|
37
|
+
Project-URL: Homepage, https://github.com/Electronic-Mango/markdown-toc-generator
|
|
38
|
+
Project-URL: Documentation, https://electronic-mango.github.io/markdown-toc-generator
|
|
39
|
+
Project-URL: Repository, https://github.com/Electronic-Mango/markdown-toc-generator
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# Markdown Table of Contents generator
|
|
43
|
+
|
|
44
|
+
Basic Markdown Table of Contents generator written in `Python`.
|
|
45
|
+
|
|
46
|
+
The script generates ToC in a form of a nested list based on headings in Markdown files.
|
|
47
|
+
The ToC can be printed to console, or inserted/updated into analyzed files.
|
|
48
|
+
|
|
49
|
+
> **Warning**: Inserting/updating ToC into the files can be destructive, as entire file is read, ToC is inserted/updated, then entire file is overwritten. The remaining contents of the file shouldn't be affected, but be careful.
|
|
50
|
+
|
|
51
|
+
The project is managed by [uv](https://docs.astral.sh/uv/).
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
The main script is `toc.py`, it has a built-in help with all parameters described:
|
|
57
|
+
```bash
|
|
58
|
+
./src/toc.py --help
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Arguments
|
|
63
|
+
|
|
64
|
+
* **`--root`, `-r`** - **required**, path in which files will be analyzed (recursively)
|
|
65
|
+
* **`--exclude`, `-e`** - paths to files, or directories, which should be excluded from analysis, **relative to root**
|
|
66
|
+
* **`--in-place`, `-i`** - update analyzed files with generated ToC, **potentially destructive** and will request confirmation before any changes are done
|
|
67
|
+
* **`--force`, `-f`** - skip confirmation for potentially destructive operations, like for `--in-place` flag
|
|
68
|
+
* **`--skip`, `-s`** - skip *n* highest level headings from generated ToC
|
|
69
|
+
* **`--take`, `-t`** - control how many headings are inserted into the ToC, starting from not-skipped by `--skip` - e.g. `--skip 1 --take 2` will include levels 2-4
|
|
70
|
+
* **`--toc-regex`** - regex used for updating/inserting ToC into files when using `--in-place` flag
|
|
71
|
+
* **`--summary`** - generate summary from all analyzed headings into one output - all ToCs with their respective files generated into one Markdown output
|
|
72
|
+
* **`--summary-path`** - write the generated summary to a file under passed path (`--in-place` flag is still required), **potentially very destructive** as the summary will overwrite everything in that file; this path is automatically excluded; **NOT relative to root**
|
|
73
|
+
* **`--summary-heading`** - prefix added to the generated summary as the highest level heading
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
### ToC regular expression
|
|
77
|
+
|
|
78
|
+
The default regex used to insert ToC into the file itself is:
|
|
79
|
+
```
|
|
80
|
+
^(#[^#].+)$(\s*-.+\n)*\s*
|
|
81
|
+
```
|
|
82
|
+
It will look for the first heading available and treat the list right after it as the ToC to replace. So by default the script assumes, that file structure will be something like:
|
|
83
|
+
|
|
84
|
+
```markdown
|
|
85
|
+
# First heading in file (but doesn't have to be level 1)
|
|
86
|
+
|
|
87
|
+
- First element of ToC
|
|
88
|
+
- First subelement of ToC
|
|
89
|
+
- Second element of ToC
|
|
90
|
+
|
|
91
|
+
Something else, not a list, which won't be modified.
|
|
92
|
+
The rest of the file doesn't matter.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
These regexes should include two groups - first looks for the section right before the ToC (which won't be modified in the resulting file), the second looks for the ToC itself (which will be replaced).
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
### Summary
|
|
99
|
+
|
|
100
|
+
The generated summary will have a structure of:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
Summary heading as per `--summary-heading` flag, or "# Summary:" by default
|
|
104
|
+
|
|
105
|
+
## Link to directory with notes, text is the directory name
|
|
106
|
+
|
|
107
|
+
### Link to a note, text is taken from the heading level 1 from that note
|
|
108
|
+
|
|
109
|
+
- [Heading 2 name](link to file and section)
|
|
110
|
+
- [Heading 3 name](link to file and section)
|
|
111
|
+
- [Heading 2 name](link to file and section)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
And so on.
|
|
115
|
+
|
|
116
|
+
When flags `--in-place` and `--summary-path PATH_TO_FILE` are passed the resulting summary will be written to `PATH_TO_FILE` as is overwritting everything else in the file, so **it can be very destructive**.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
### Examples
|
|
120
|
+
|
|
121
|
+
Generate ToC based on files in `notes/stuff` subdirectory, except for `README.md` and files under `ignore/notes`; ignore the highest level heading and include only 2 levels after that; only print to console, without summary:
|
|
122
|
+
```bash
|
|
123
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The same as above, but print a summary as well, with `# Some stuff:` prefix:
|
|
127
|
+
```bash
|
|
128
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2 --summary --summary-heading '# Some stuff:'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Insert ToC into files, print summary to console:
|
|
132
|
+
```bash
|
|
133
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2 -i --summary --summary-heading '# Some stuff:'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Write summary to `README.md`:
|
|
137
|
+
```bash
|
|
138
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2 -i --summary --summary-heading '# Some stuff:' --summary-path README.md
|
|
139
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Markdown Table of Contents generator
|
|
2
|
+
|
|
3
|
+
Basic Markdown Table of Contents generator written in `Python`.
|
|
4
|
+
|
|
5
|
+
The script generates ToC in a form of a nested list based on headings in Markdown files.
|
|
6
|
+
The ToC can be printed to console, or inserted/updated into analyzed files.
|
|
7
|
+
|
|
8
|
+
> **Warning**: Inserting/updating ToC into the files can be destructive, as entire file is read, ToC is inserted/updated, then entire file is overwritten. The remaining contents of the file shouldn't be affected, but be careful.
|
|
9
|
+
|
|
10
|
+
The project is managed by [uv](https://docs.astral.sh/uv/).
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
The main script is `toc.py`, it has a built-in help with all parameters described:
|
|
16
|
+
```bash
|
|
17
|
+
./src/toc.py --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Arguments
|
|
22
|
+
|
|
23
|
+
* **`--root`, `-r`** - **required**, path in which files will be analyzed (recursively)
|
|
24
|
+
* **`--exclude`, `-e`** - paths to files, or directories, which should be excluded from analysis, **relative to root**
|
|
25
|
+
* **`--in-place`, `-i`** - update analyzed files with generated ToC, **potentially destructive** and will request confirmation before any changes are done
|
|
26
|
+
* **`--force`, `-f`** - skip confirmation for potentially destructive operations, like for `--in-place` flag
|
|
27
|
+
* **`--skip`, `-s`** - skip *n* highest level headings from generated ToC
|
|
28
|
+
* **`--take`, `-t`** - control how many headings are inserted into the ToC, starting from not-skipped by `--skip` - e.g. `--skip 1 --take 2` will include levels 2-4
|
|
29
|
+
* **`--toc-regex`** - regex used for updating/inserting ToC into files when using `--in-place` flag
|
|
30
|
+
* **`--summary`** - generate summary from all analyzed headings into one output - all ToCs with their respective files generated into one Markdown output
|
|
31
|
+
* **`--summary-path`** - write the generated summary to a file under passed path (`--in-place` flag is still required), **potentially very destructive** as the summary will overwrite everything in that file; this path is automatically excluded; **NOT relative to root**
|
|
32
|
+
* **`--summary-heading`** - prefix added to the generated summary as the highest level heading
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### ToC regular expression
|
|
36
|
+
|
|
37
|
+
The default regex used to insert ToC into the file itself is:
|
|
38
|
+
```
|
|
39
|
+
^(#[^#].+)$(\s*-.+\n)*\s*
|
|
40
|
+
```
|
|
41
|
+
It will look for the first heading available and treat the list right after it as the ToC to replace. So by default the script assumes, that file structure will be something like:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
# First heading in file (but doesn't have to be level 1)
|
|
45
|
+
|
|
46
|
+
- First element of ToC
|
|
47
|
+
- First subelement of ToC
|
|
48
|
+
- Second element of ToC
|
|
49
|
+
|
|
50
|
+
Something else, not a list, which won't be modified.
|
|
51
|
+
The rest of the file doesn't matter.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
These regexes should include two groups - first looks for the section right before the ToC (which won't be modified in the resulting file), the second looks for the ToC itself (which will be replaced).
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Summary
|
|
58
|
+
|
|
59
|
+
The generated summary will have a structure of:
|
|
60
|
+
|
|
61
|
+
```markdown
|
|
62
|
+
Summary heading as per `--summary-heading` flag, or "# Summary:" by default
|
|
63
|
+
|
|
64
|
+
## Link to directory with notes, text is the directory name
|
|
65
|
+
|
|
66
|
+
### Link to a note, text is taken from the heading level 1 from that note
|
|
67
|
+
|
|
68
|
+
- [Heading 2 name](link to file and section)
|
|
69
|
+
- [Heading 3 name](link to file and section)
|
|
70
|
+
- [Heading 2 name](link to file and section)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
And so on.
|
|
74
|
+
|
|
75
|
+
When flags `--in-place` and `--summary-path PATH_TO_FILE` are passed the resulting summary will be written to `PATH_TO_FILE` as is overwritting everything else in the file, so **it can be very destructive**.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
### Examples
|
|
79
|
+
|
|
80
|
+
Generate ToC based on files in `notes/stuff` subdirectory, except for `README.md` and files under `ignore/notes`; ignore the highest level heading and include only 2 levels after that; only print to console, without summary:
|
|
81
|
+
```bash
|
|
82
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The same as above, but print a summary as well, with `# Some stuff:` prefix:
|
|
86
|
+
```bash
|
|
87
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2 --summary --summary-heading '# Some stuff:'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Insert ToC into files, print summary to console:
|
|
91
|
+
```bash
|
|
92
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2 -i --summary --summary-heading '# Some stuff:'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Write summary to `README.md`:
|
|
96
|
+
```bash
|
|
97
|
+
./src/toc.py -r notes/stuff -e README.md ignore/notes -s 1 -t 2 -i --summary --summary-heading '# Some stuff:' --summary-path README.md
|
|
98
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "markdown-toc-generator"
|
|
3
|
+
authors = [{name = "Electronic Mango", email = "78230210+Electronic-Mango@users.noreply.github.com"}]
|
|
4
|
+
version = "1.0.0"
|
|
5
|
+
description = "Python scripts generating Table of Contents from markdown headers."
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = {file = "LICENSE"}
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
keywords = ["Markdown", "ToC", "toc", "table-of-contents", "Table of Contents"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Topic :: Software Development :: Build Tools",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/Electronic-Mango/markdown-toc-generator"
|
|
23
|
+
Documentation = "https://electronic-mango.github.io/markdown-toc-generator"
|
|
24
|
+
Repository = "https://github.com/Electronic-Mango/markdown-toc-generator"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.11.2,<0.12"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
markdown_toc_generator = "markdown_toc_generator.toc:main"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"black>=26.3.1",
|
|
36
|
+
"flake8>=7.3.0",
|
|
37
|
+
"isort>=8.0.1",
|
|
38
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from argparse import ArgumentParser, Namespace
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
TOC_REGEX = r"^(#[^#].+)$(\s*-.+\n)*\s*"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_arguments() -> Namespace:
|
|
8
|
+
parser = ArgumentParser(
|
|
9
|
+
description=(
|
|
10
|
+
"Markdown Table-of-Contents generator, print them to console, "
|
|
11
|
+
"or insert them into Markdown files themselves"
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"-r",
|
|
16
|
+
"--root",
|
|
17
|
+
type=Path,
|
|
18
|
+
default=Path(),
|
|
19
|
+
help="set root path for all operations, by default current path is used",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-e",
|
|
23
|
+
"--exclude",
|
|
24
|
+
type=Path,
|
|
25
|
+
nargs="*",
|
|
26
|
+
default=[],
|
|
27
|
+
help=(
|
|
28
|
+
"paths (relative to root) which should be excluded from analysis, "
|
|
29
|
+
"can be single files, can be entire directories"
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-i",
|
|
34
|
+
"--in-place",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help=(
|
|
37
|
+
"insert generated ToC into the files, POTENTIALLY DESCTRUCTIVE operation "
|
|
38
|
+
"as entire contents of the file is read, ToC is inserted, "
|
|
39
|
+
"then entire file is overwritten"
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-f",
|
|
44
|
+
"--force",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="skip confirmation for potentially destructive operations (e.g. for --in-place flag)",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"-s",
|
|
50
|
+
"--skip",
|
|
51
|
+
type=int,
|
|
52
|
+
default=0,
|
|
53
|
+
help="how many levels should be skipped from ToC (starting at the highest)",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-t",
|
|
57
|
+
"--take",
|
|
58
|
+
type=int,
|
|
59
|
+
default=0,
|
|
60
|
+
help=(
|
|
61
|
+
"how many levels should be added to ToC (starting from the highest) "
|
|
62
|
+
"relative to --take ('--skip 1 --take 3' results in levels 2-4 to be included in ToC)"
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--toc-regex",
|
|
67
|
+
default=TOC_REGEX,
|
|
68
|
+
help=(
|
|
69
|
+
"regex used to insert ToC into file with --in-place, "
|
|
70
|
+
"first capture group looks for a 'prefix' string for the ToC (which is preserved), "
|
|
71
|
+
"the second one looks for the ToC itself (which will be replaced), "
|
|
72
|
+
f"'{TOC_REGEX}' used by default"
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--summary", action="store_true", help="generate summary of all analyzed files"
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--summary-path",
|
|
80
|
+
type=Path,
|
|
81
|
+
help=(
|
|
82
|
+
"insert the generated summary into a file (--in-place flag is still required), "
|
|
83
|
+
"POTENTIALLY VERY DESCTRUCTIVE as entire file will be replaced by the summary, "
|
|
84
|
+
"no smart analysis is done, entire file is rewritten"
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--summary-heading",
|
|
89
|
+
type=str,
|
|
90
|
+
default="# Summary:",
|
|
91
|
+
help="main heading used for generated summary",
|
|
92
|
+
)
|
|
93
|
+
return parser.parse_args()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Heading(NamedTuple):
|
|
7
|
+
level: int
|
|
8
|
+
name: str
|
|
9
|
+
path: Path
|
|
10
|
+
section_link: str
|
|
11
|
+
|
|
12
|
+
def str(self, skip: int, section_only: bool) -> str:
|
|
13
|
+
list_prefix = " " * (self.level - skip - 1) * 2
|
|
14
|
+
file_link = quote(str(self.path)) if not section_only else ""
|
|
15
|
+
return f"{list_prefix}- [{self.name}]({file_link}{self.section_link})"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from itertools import groupby
|
|
2
|
+
from os import linesep
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from re import MULTILINE, sub
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from markdown_toc_generator.heading import Heading
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle_file_toc(
|
|
11
|
+
heading_data: dict[Path, list[Heading]], skip: int, take: int, in_place: bool, toc_regex: str
|
|
12
|
+
) -> None:
|
|
13
|
+
for path, headings in heading_data.items():
|
|
14
|
+
if not (toc := format_headings(headings, skip, take, True)):
|
|
15
|
+
continue
|
|
16
|
+
if in_place:
|
|
17
|
+
insert_toc(path, toc, toc_regex)
|
|
18
|
+
else:
|
|
19
|
+
print(f"{path}:{linesep}{toc}{linesep}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_summary_toc(
|
|
23
|
+
heading_data: dict[Path, list[Heading]],
|
|
24
|
+
skip: int,
|
|
25
|
+
take: int,
|
|
26
|
+
in_place: bool,
|
|
27
|
+
target_path: Path | None,
|
|
28
|
+
main_heading: str,
|
|
29
|
+
) -> None:
|
|
30
|
+
all_paths = {path for full_path in heading_data for path in full_path.parents[:-1]}
|
|
31
|
+
all_paths = sorted(all_paths, key=lambda path: (path, len(path.parents)))
|
|
32
|
+
heading_data_per_directory = {
|
|
33
|
+
group[0]: dict(values).values()
|
|
34
|
+
for group, values in groupby(heading_data.items(), lambda item: item[0].parents[:-1])
|
|
35
|
+
}
|
|
36
|
+
expanded_heading_data = {path: heading_data_per_directory.get(path, []) for path in all_paths}
|
|
37
|
+
toc = ""
|
|
38
|
+
for dir_path, dir_headings in expanded_heading_data.items():
|
|
39
|
+
level = 2
|
|
40
|
+
toc += format_path_heading(dir_path, level)
|
|
41
|
+
for file_headings in dir_headings:
|
|
42
|
+
# toc += format_path_heading(file_path, level + 1)
|
|
43
|
+
first_heading = file_headings[0]
|
|
44
|
+
toc += f"{'#' * (level + 1)}{first_heading.str(0, False)[1:]}{linesep * 2}"
|
|
45
|
+
toc += format_headings(file_headings, skip, take, False)
|
|
46
|
+
toc += linesep * 2
|
|
47
|
+
if in_place and target_path and target_path.is_file():
|
|
48
|
+
print(f"Updating {target_path}")
|
|
49
|
+
with open(target_path, "w") as file:
|
|
50
|
+
file.write(f"{main_heading}{linesep * 2}{toc}")
|
|
51
|
+
else:
|
|
52
|
+
print(f"{linesep * 2}{main_heading}{linesep * 2}{toc}{linesep}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_headings(headings: list[Heading], skip: int, take: int, section_only: bool) -> str:
|
|
56
|
+
return linesep.join(
|
|
57
|
+
heading.str(skip, section_only)
|
|
58
|
+
for heading in headings
|
|
59
|
+
if level_in_range(heading.level, skip, take)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def level_in_range(level: int, skip: int, take: int) -> bool:
|
|
64
|
+
return (level > skip and level <= (take + skip)) if take else (level > skip)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def insert_toc(path: Path, toc: str, toc_regex: str) -> None:
|
|
68
|
+
toc = (linesep * 2) + toc + (linesep * 3)
|
|
69
|
+
with open(path, "r") as file:
|
|
70
|
+
text = file.read()
|
|
71
|
+
if toc in text:
|
|
72
|
+
print(f"No changes made to: {path}")
|
|
73
|
+
return
|
|
74
|
+
print(f"Updating ToC in: {path}")
|
|
75
|
+
sub_regex = rf"\1{toc}"
|
|
76
|
+
new_text = sub(toc_regex, sub_regex, text, count=1, flags=MULTILINE)
|
|
77
|
+
with open(path, "w") as file:
|
|
78
|
+
file.write(new_text)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_path_heading(path: Path, level: int) -> str:
|
|
82
|
+
return f"{'#' * level} [{path.name}]({quote(str(path))}){linesep * 2}"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from re import search, sub
|
|
3
|
+
|
|
4
|
+
from markdown_toc_generator.heading import Heading
|
|
5
|
+
|
|
6
|
+
HEADER_REGEX = r"^(#+) (.+)"
|
|
7
|
+
CODE_BLOCK_REGEX = r"^```"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_headings_from_file(path: Path) -> list[Heading]:
|
|
11
|
+
with open(path, "r") as file:
|
|
12
|
+
text = file.readlines()
|
|
13
|
+
headings = get_all_headings(text)
|
|
14
|
+
return [Heading(level, name, path, create_section_link(name)) for level, name in headings]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_all_headings(lines: list[str]) -> list[tuple[int, str]]:
|
|
18
|
+
headings = []
|
|
19
|
+
is_code_block = False
|
|
20
|
+
for line in lines:
|
|
21
|
+
if search(CODE_BLOCK_REGEX, line):
|
|
22
|
+
is_code_block = not is_code_block
|
|
23
|
+
if is_code_block:
|
|
24
|
+
continue
|
|
25
|
+
if match := search(HEADER_REGEX, line):
|
|
26
|
+
headings.append((len(match.group(1)), match.group(2)))
|
|
27
|
+
return headings
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_section_link(name: str) -> str:
|
|
31
|
+
section_link = sub(r"[^0-9a-z-_ ]", "", name.lower()).replace(" ", "-")
|
|
32
|
+
return f"#{section_link}"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from markdown_toc_generator.arguments import parse_arguments
|
|
6
|
+
from markdown_toc_generator.heading import Heading
|
|
7
|
+
from markdown_toc_generator.output_toc import handle_file_toc, handle_summary_toc
|
|
8
|
+
from markdown_toc_generator.parse_headings import parse_headings_from_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
args = parse_arguments()
|
|
13
|
+
root = args.root.absolute()
|
|
14
|
+
normalized_excludes = get_all_excludes(root, args.exclude, args.summary_path)
|
|
15
|
+
in_place = verify_in_place(args.in_place, args.force)
|
|
16
|
+
notes_paths = get_all_notes_paths(root, normalized_excludes)
|
|
17
|
+
notes_paths.sort(key=lambda path: (len(path.parents), path))
|
|
18
|
+
heading_data = parse_all_headings(notes_paths)
|
|
19
|
+
handle_file_toc(heading_data, args.skip, args.take, in_place, args.toc_regex)
|
|
20
|
+
if args.summary or args.summary_path:
|
|
21
|
+
handle_summary_toc(heading_data, 1, 1, in_place, args.summary_path, args.summary_heading)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize(root: Path, path: Path) -> Path:
|
|
25
|
+
return path.absolute().relative_to(root)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_all_excludes(root: Path, exclude: list[Path], readme: Path | None) -> set[Path]:
|
|
29
|
+
return {normalize(root, path) for path in exclude + [readme] if path}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def verify_in_place(in_place: bool, force: bool) -> bool:
|
|
33
|
+
if not in_place or force:
|
|
34
|
+
return in_place
|
|
35
|
+
return input(
|
|
36
|
+
"Changing files in-place can lead to data loss, use at your own risk. "
|
|
37
|
+
"Continue with changes in-place? [y/n] "
|
|
38
|
+
).lower() in ("y", "yes")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_all_notes_paths(root: Path, exclude: set[Path]) -> list[Path]:
|
|
42
|
+
return [
|
|
43
|
+
normalize(root, path)
|
|
44
|
+
for path in root.rglob("*.md")
|
|
45
|
+
if not any(check_excluded_path(normalize(root, path), excluded) for excluded in exclude)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_excluded_path(path: Path, excluded: Path) -> bool:
|
|
50
|
+
return path == excluded if excluded.is_file() else excluded in path.parents
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_all_headings(notes_paths: list[Path]) -> dict[Path, list[Heading]]:
|
|
54
|
+
return {path: parse_headings_from_file(path) for path in notes_paths}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|