outliner-cli 0.1.0__tar.gz → 0.2.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.
- {outliner_cli-0.1.0/src/outliner_cli.egg-info → outliner_cli-0.2.0}/PKG-INFO +34 -26
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/README.md +33 -25
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/pyproject.toml +2 -2
- outliner_cli-0.2.0/src/outliner/cli.py +183 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/__init__.py +9 -2
- outliner_cli-0.2.0/src/outliner/parsers/html.py +217 -0
- outliner_cli-0.2.0/src/outliner/parsers/javascript.py +389 -0
- outliner_cli-0.2.0/src/outliner/types.py +19 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0/src/outliner_cli.egg-info}/PKG-INFO +34 -26
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner_cli.egg-info/SOURCES.txt +2 -0
- outliner_cli-0.2.0/src/outliner_cli.egg-info/entry_points.txt +2 -0
- outliner_cli-0.2.0/tests/test_cli.py +260 -0
- outliner_cli-0.2.0/tests/test_html.py +395 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_javascript.py +457 -29
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_parsers.py +18 -0
- outliner_cli-0.1.0/src/outliner/cli.py +0 -115
- outliner_cli-0.1.0/src/outliner/parsers/javascript.py +0 -312
- outliner_cli-0.1.0/src/outliner/types.py +0 -8
- outliner_cli-0.1.0/src/outliner_cli.egg-info/entry_points.txt +0 -2
- outliner_cli-0.1.0/tests/test_cli.py +0 -130
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/LICENSE +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/setup.cfg +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/__init__.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/asciidoc.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/c.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/clojure.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/csharp.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/go.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/java.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/markdown.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/orgmode.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/perl.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/php.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/python.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/rst.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/ruby.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/rust.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/scala.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/shell.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/swift.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/util.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/zig.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner_cli.egg-info/dependency_links.txt +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner_cli.egg-info/top_level.txt +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_asciidoc.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_c.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_clojure.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_csharp.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_go.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_java.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_markdown.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_orgmode.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_perl.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_php.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_python.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_rst.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_ruby.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_rust.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_scala.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_shell.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_swift.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_util.py +0 -0
- {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_zig.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: outliner-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Print the structural outline of source files for LLM navigation
|
|
5
5
|
Author: Per Cederberg
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,19 +13,21 @@ Dynamic: license-file
|
|
|
13
13
|
|
|
14
14
|
# outliner
|
|
15
15
|
|
|
16
|
-
Print the structural outline of source files — declarations
|
|
17
|
-
so an LLM agent (or human) can navigate a file
|
|
16
|
+
Print the structural outline of source files — useful declarations and callable
|
|
17
|
+
landmarks with line ranges — so an LLM agent (or human) can navigate a file
|
|
18
|
+
without reading it whole.
|
|
18
19
|
|
|
19
20
|
## Usage
|
|
20
21
|
|
|
21
22
|
```
|
|
22
|
-
outliner [OPTIONS] [FILE...]
|
|
23
|
+
outliner-cli [OPTIONS] [FILE...]
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
| Option | Description
|
|
26
|
-
| ------------------- |
|
|
27
|
-
| `-g, --grep EXPR` | Only show items whose signature matches EXPR (case-insensitive)
|
|
28
|
-
| `-s, --syntax LANG` | Override syntax auto-detection when it is ambiguous
|
|
26
|
+
| Option | Description |
|
|
27
|
+
| ------------------- | ----------------------------------------------------------------------------- |
|
|
28
|
+
| `-g, --grep EXPR` | Only show items whose signature matches EXPR (case-insensitive) |
|
|
29
|
+
| `-s, --syntax LANG` | Override syntax auto-detection when it is ambiguous |
|
|
30
|
+
| `-w, --width COLS` | Truncate output lines to COLS (`0`=unlimited, `auto`=terminal, default=`120`) |
|
|
29
31
|
|
|
30
32
|
Pass a file, a directory (walked recursively), or omit arguments to read stdin.
|
|
31
33
|
Use `-` to read stdin explicitly. `--syntax` is only needed when content
|
|
@@ -45,7 +47,8 @@ Each line: `<start>,<count> <signature>`
|
|
|
45
47
|
- `start` — 1-based line number, right-aligned
|
|
46
48
|
- `count` — number of lines covered by the item (including doc-comments above)
|
|
47
49
|
- `signature` — first non-comment line of the declaration; multi-line signatures
|
|
48
|
-
are merged into one line
|
|
50
|
+
are merged into one line; lines longer than the output width are truncated
|
|
51
|
+
with `...`
|
|
49
52
|
|
|
50
53
|
Nesting is visible in two ways: overlapping ranges (a class range contains its
|
|
51
54
|
methods) and native-format indentation in the signature (indented for code,
|
|
@@ -60,14 +63,11 @@ pip install outliner
|
|
|
60
63
|
## Running
|
|
61
64
|
|
|
62
65
|
```sh
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# From the repository root
|
|
67
|
-
uv run --project outliner outliner path/to/file.py
|
|
66
|
+
# With the package installed (pip or uvx):
|
|
67
|
+
uvx outliner-cli path/to/file.py
|
|
68
68
|
|
|
69
|
-
#
|
|
70
|
-
uv run
|
|
69
|
+
# From within the outliner/ directory
|
|
70
|
+
uv run outliner-cli path/to/file.py
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
## Running Tests
|
|
@@ -79,15 +79,17 @@ uv run pytest
|
|
|
79
79
|
|
|
80
80
|
## Supported Languages
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
AsciiDoc, C/C++, C#, Clojure, Go, HTML, Java, JavaScript/TypeScript, Markdown,
|
|
83
|
+
Org-mode, Perl, PHP, Python, reStructuredText, Ruby, Rust, Scala, Shell, Swift,
|
|
84
|
+
and Zig.
|
|
84
85
|
|
|
85
86
|
## Example Use Cases
|
|
86
87
|
|
|
87
|
-
**Structural overview** — Run on a directory to see all declarations across many
|
|
88
|
+
**Structural overview** — Run on a directory to see all declarations across many
|
|
89
|
+
files before reading anything:
|
|
88
90
|
|
|
89
91
|
```
|
|
90
|
-
$ outliner src/
|
|
92
|
+
$ uvx outliner-cli src/
|
|
91
93
|
==> src/billing.py <==
|
|
92
94
|
12,8 class Invoice
|
|
93
95
|
22,4 def create(customer_id: str, items: list[Item]) -> Invoice
|
|
@@ -98,10 +100,11 @@ $ outliner src/
|
|
|
98
100
|
14,12 def charge(method: PaymentMethod, amount: Decimal) -> Receipt
|
|
99
101
|
```
|
|
100
102
|
|
|
101
|
-
**Find all copies of a pattern** — `--grep serialize` across a source tree
|
|
103
|
+
**Find all copies of a pattern** — `--grep serialize` across a source tree
|
|
104
|
+
locates every implementation of a repeated function in one command:
|
|
102
105
|
|
|
103
106
|
```
|
|
104
|
-
$ outliner --grep serialize src/
|
|
107
|
+
$ uvx outliner-cli --grep serialize src/
|
|
105
108
|
==> src/invoice.py <==
|
|
106
109
|
44,5 def serialize(self) -> dict
|
|
107
110
|
|
|
@@ -109,18 +112,23 @@ $ outliner --grep serialize src/
|
|
|
109
112
|
31,3 def serialize(self) -> dict
|
|
110
113
|
```
|
|
111
114
|
|
|
112
|
-
**Find functions whose interface mentions a term** — `--grep` searches
|
|
115
|
+
**Find functions whose interface mentions a term** — `--grep` searches
|
|
116
|
+
signatures, not bodies. It finds functions whose interface involves a concept,
|
|
117
|
+
skipping internal uses, comments, and call sites:
|
|
113
118
|
|
|
114
119
|
```
|
|
115
|
-
$ outliner --grep payment src/
|
|
120
|
+
$ uvx outliner-cli --grep payment src/
|
|
116
121
|
14,12 def charge(method: PaymentMethod, amount: Decimal) -> Receipt
|
|
117
122
|
61,4 def refund(payment: Payment) -> bool
|
|
118
123
|
```
|
|
119
124
|
|
|
120
|
-
**Find functions accepting a specific type** — `--grep PaymentMethod` locates
|
|
125
|
+
**Find functions accepting a specific type** — `--grep PaymentMethod` locates
|
|
126
|
+
every function where the type appears in parameters, return types, or generic
|
|
127
|
+
bounds. Multi-line signatures are merged into a single line before matching, so
|
|
128
|
+
nothing is missed:
|
|
121
129
|
|
|
122
130
|
```
|
|
123
|
-
$ outliner --grep PaymentMethod src/
|
|
131
|
+
$ uvx outliner-cli --grep PaymentMethod src/
|
|
124
132
|
14,12 def charge(method: PaymentMethod, amount: Decimal) -> Receipt
|
|
125
133
|
88,4 def validate(m: PaymentMethod) -> bool
|
|
126
134
|
```
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# outliner
|
|
2
2
|
|
|
3
|
-
Print the structural outline of source files — declarations
|
|
4
|
-
so an LLM agent (or human) can navigate a file
|
|
3
|
+
Print the structural outline of source files — useful declarations and callable
|
|
4
|
+
landmarks with line ranges — so an LLM agent (or human) can navigate a file
|
|
5
|
+
without reading it whole.
|
|
5
6
|
|
|
6
7
|
## Usage
|
|
7
8
|
|
|
8
9
|
```
|
|
9
|
-
outliner [OPTIONS] [FILE...]
|
|
10
|
+
outliner-cli [OPTIONS] [FILE...]
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
| Option | Description
|
|
13
|
-
| ------------------- |
|
|
14
|
-
| `-g, --grep EXPR` | Only show items whose signature matches EXPR (case-insensitive)
|
|
15
|
-
| `-s, --syntax LANG` | Override syntax auto-detection when it is ambiguous
|
|
13
|
+
| Option | Description |
|
|
14
|
+
| ------------------- | ----------------------------------------------------------------------------- |
|
|
15
|
+
| `-g, --grep EXPR` | Only show items whose signature matches EXPR (case-insensitive) |
|
|
16
|
+
| `-s, --syntax LANG` | Override syntax auto-detection when it is ambiguous |
|
|
17
|
+
| `-w, --width COLS` | Truncate output lines to COLS (`0`=unlimited, `auto`=terminal, default=`120`) |
|
|
16
18
|
|
|
17
19
|
Pass a file, a directory (walked recursively), or omit arguments to read stdin.
|
|
18
20
|
Use `-` to read stdin explicitly. `--syntax` is only needed when content
|
|
@@ -32,7 +34,8 @@ Each line: `<start>,<count> <signature>`
|
|
|
32
34
|
- `start` — 1-based line number, right-aligned
|
|
33
35
|
- `count` — number of lines covered by the item (including doc-comments above)
|
|
34
36
|
- `signature` — first non-comment line of the declaration; multi-line signatures
|
|
35
|
-
are merged into one line
|
|
37
|
+
are merged into one line; lines longer than the output width are truncated
|
|
38
|
+
with `...`
|
|
36
39
|
|
|
37
40
|
Nesting is visible in two ways: overlapping ranges (a class range contains its
|
|
38
41
|
methods) and native-format indentation in the signature (indented for code,
|
|
@@ -47,14 +50,11 @@ pip install outliner
|
|
|
47
50
|
## Running
|
|
48
51
|
|
|
49
52
|
```sh
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# From the repository root
|
|
54
|
-
uv run --project outliner outliner path/to/file.py
|
|
53
|
+
# With the package installed (pip or uvx):
|
|
54
|
+
uvx outliner-cli path/to/file.py
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
uv run
|
|
56
|
+
# From within the outliner/ directory
|
|
57
|
+
uv run outliner-cli path/to/file.py
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Running Tests
|
|
@@ -66,15 +66,17 @@ uv run pytest
|
|
|
66
66
|
|
|
67
67
|
## Supported Languages
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
AsciiDoc, C/C++, C#, Clojure, Go, HTML, Java, JavaScript/TypeScript, Markdown,
|
|
70
|
+
Org-mode, Perl, PHP, Python, reStructuredText, Ruby, Rust, Scala, Shell, Swift,
|
|
71
|
+
and Zig.
|
|
71
72
|
|
|
72
73
|
## Example Use Cases
|
|
73
74
|
|
|
74
|
-
**Structural overview** — Run on a directory to see all declarations across many
|
|
75
|
+
**Structural overview** — Run on a directory to see all declarations across many
|
|
76
|
+
files before reading anything:
|
|
75
77
|
|
|
76
78
|
```
|
|
77
|
-
$ outliner src/
|
|
79
|
+
$ uvx outliner-cli src/
|
|
78
80
|
==> src/billing.py <==
|
|
79
81
|
12,8 class Invoice
|
|
80
82
|
22,4 def create(customer_id: str, items: list[Item]) -> Invoice
|
|
@@ -85,10 +87,11 @@ $ outliner src/
|
|
|
85
87
|
14,12 def charge(method: PaymentMethod, amount: Decimal) -> Receipt
|
|
86
88
|
```
|
|
87
89
|
|
|
88
|
-
**Find all copies of a pattern** — `--grep serialize` across a source tree
|
|
90
|
+
**Find all copies of a pattern** — `--grep serialize` across a source tree
|
|
91
|
+
locates every implementation of a repeated function in one command:
|
|
89
92
|
|
|
90
93
|
```
|
|
91
|
-
$ outliner --grep serialize src/
|
|
94
|
+
$ uvx outliner-cli --grep serialize src/
|
|
92
95
|
==> src/invoice.py <==
|
|
93
96
|
44,5 def serialize(self) -> dict
|
|
94
97
|
|
|
@@ -96,18 +99,23 @@ $ outliner --grep serialize src/
|
|
|
96
99
|
31,3 def serialize(self) -> dict
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
**Find functions whose interface mentions a term** — `--grep` searches
|
|
102
|
+
**Find functions whose interface mentions a term** — `--grep` searches
|
|
103
|
+
signatures, not bodies. It finds functions whose interface involves a concept,
|
|
104
|
+
skipping internal uses, comments, and call sites:
|
|
100
105
|
|
|
101
106
|
```
|
|
102
|
-
$ outliner --grep payment src/
|
|
107
|
+
$ uvx outliner-cli --grep payment src/
|
|
103
108
|
14,12 def charge(method: PaymentMethod, amount: Decimal) -> Receipt
|
|
104
109
|
61,4 def refund(payment: Payment) -> bool
|
|
105
110
|
```
|
|
106
111
|
|
|
107
|
-
**Find functions accepting a specific type** — `--grep PaymentMethod` locates
|
|
112
|
+
**Find functions accepting a specific type** — `--grep PaymentMethod` locates
|
|
113
|
+
every function where the type appears in parameters, return types, or generic
|
|
114
|
+
bounds. Multi-line signatures are merged into a single line before matching, so
|
|
115
|
+
nothing is missed:
|
|
108
116
|
|
|
109
117
|
```
|
|
110
|
-
$ outliner --grep PaymentMethod src/
|
|
118
|
+
$ uvx outliner-cli --grep PaymentMethod src/
|
|
111
119
|
14,12 def charge(method: PaymentMethod, amount: Decimal) -> Receipt
|
|
112
120
|
88,4 def validate(m: PaymentMethod) -> bool
|
|
113
121
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "outliner-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Print the structural outline of source files for LLM navigation"
|
|
9
9
|
authors = [{name = "Per Cederberg"}]
|
|
10
10
|
license = "MIT"
|
|
@@ -17,7 +17,7 @@ Homepage = "https://github.com/cederberg/incubator/tree/main/outliner"
|
|
|
17
17
|
Repository = "https://github.com/cederberg/incubator"
|
|
18
18
|
|
|
19
19
|
[project.scripts]
|
|
20
|
-
outliner = "outliner.cli:main"
|
|
20
|
+
outliner-cli = "outliner.cli:main"
|
|
21
21
|
|
|
22
22
|
[tool.setuptools.packages.find]
|
|
23
23
|
where = ["src"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""outliner — print structural outline of source files."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import fnmatch
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from outliner.parsers import NAMES, EXTENSIONS, detect, outline, syntax
|
|
11
|
+
from outliner.types import OutlineItem
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def die(msg: str, code: int = 2) -> None:
|
|
15
|
+
print(f"outliner: {msg}", file=sys.stderr)
|
|
16
|
+
sys.exit(code)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def guess_syntax(src: str) -> str | None:
|
|
20
|
+
return EXTENSIONS.get(os.path.splitext(src.lower())[1])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_gitignore(dirpath: str) -> list[str]:
|
|
24
|
+
try:
|
|
25
|
+
with open(os.path.join(dirpath, ".gitignore"), encoding="utf-8", errors="replace") as f:
|
|
26
|
+
return [ln.rstrip("\n") for ln in f if ln.strip() and not ln.startswith("#")]
|
|
27
|
+
except OSError:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _gitignore_match(name: str, relpath: str, patterns: list[str], is_dir: bool) -> bool:
|
|
32
|
+
result = False
|
|
33
|
+
for pat in patterns:
|
|
34
|
+
negate = pat.startswith("!")
|
|
35
|
+
p = pat[1:] if negate else pat
|
|
36
|
+
dir_only = p.endswith("/")
|
|
37
|
+
if dir_only:
|
|
38
|
+
if not is_dir:
|
|
39
|
+
continue
|
|
40
|
+
p = p[:-1]
|
|
41
|
+
target = relpath if "/" in p else name
|
|
42
|
+
if fnmatch.fnmatch(target, p.lstrip("/")):
|
|
43
|
+
result = not negate
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_ignored(name: str, root: str, gi: dict[str, list[str]], is_dir: bool) -> bool:
|
|
48
|
+
for gi_dir, pats in gi.items():
|
|
49
|
+
if gi_dir == root or root.startswith(gi_dir + os.sep):
|
|
50
|
+
rel = os.path.relpath(os.path.join(root, name), gi_dir)
|
|
51
|
+
if _gitignore_match(name, rel, pats, is_dir):
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _expand_sources(sources: list[str], types: set[str] | None = None) -> list[str]:
|
|
57
|
+
result = []
|
|
58
|
+
for src in sources:
|
|
59
|
+
if src == "-" or not os.path.isdir(src):
|
|
60
|
+
result.append(src)
|
|
61
|
+
continue
|
|
62
|
+
gi: dict[str, list[str]] = {}
|
|
63
|
+
for root, dirs, files in os.walk(src):
|
|
64
|
+
pats = _load_gitignore(root)
|
|
65
|
+
if pats:
|
|
66
|
+
gi[root] = pats
|
|
67
|
+
dirs[:] = sorted(d for d in dirs if not _is_ignored(d, root, gi, True))
|
|
68
|
+
for name in sorted(files):
|
|
69
|
+
match = guess_syntax(name)
|
|
70
|
+
supported = match and (not types or match in types)
|
|
71
|
+
if supported and not _is_ignored(name, root, gi, False):
|
|
72
|
+
result.append(os.path.join(root, name))
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _format_items(items: list[OutlineItem], grep: re.Pattern | None, line_width: int) -> list[str]:
|
|
77
|
+
if grep:
|
|
78
|
+
items = [it for it in items if grep.search(it.signature)]
|
|
79
|
+
if not items:
|
|
80
|
+
return []
|
|
81
|
+
num_width = max(it.num_width for it in items)
|
|
82
|
+
num_width = max(num_width, 3)
|
|
83
|
+
return [it.format(num_width, line_width) for it in items]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(argv: list[str] | None = None) -> int:
|
|
87
|
+
ap = argparse.ArgumentParser(
|
|
88
|
+
prog="outliner",
|
|
89
|
+
description="Print the structural outline of source files.",
|
|
90
|
+
)
|
|
91
|
+
ap.add_argument("files", nargs="*", metavar="FILE",
|
|
92
|
+
help="Files to outline (omit or use - for stdin)")
|
|
93
|
+
ap.add_argument("-g", "--grep", metavar="EXPR",
|
|
94
|
+
help="Only show items whose signature matches EXPR (case-insensitive)")
|
|
95
|
+
ap.add_argument("-s", "--syntax", metavar="LANG",
|
|
96
|
+
help=f"Override syntax auto-detection (available: {', '.join(NAMES)})")
|
|
97
|
+
ap.add_argument("-t", "--type", action="append", metavar="LANG",
|
|
98
|
+
help="Only include files of this language or extension (repeatable)")
|
|
99
|
+
ap.add_argument("-w", "--width", metavar="COLS", default="120",
|
|
100
|
+
help="Truncate output lines to COLS (0=unlimited, auto=terminal width, default=120)")
|
|
101
|
+
args = ap.parse_args(argv)
|
|
102
|
+
|
|
103
|
+
grep_re: re.Pattern | None = None
|
|
104
|
+
if args.grep:
|
|
105
|
+
try:
|
|
106
|
+
grep_re = re.compile(args.grep, re.IGNORECASE)
|
|
107
|
+
except re.error as exc:
|
|
108
|
+
die(f"invalid --grep expression: {exc}")
|
|
109
|
+
|
|
110
|
+
line_width: int
|
|
111
|
+
if args.width == "auto":
|
|
112
|
+
line_width = shutil.get_terminal_size(fallback=(120, 24)).columns
|
|
113
|
+
else:
|
|
114
|
+
try:
|
|
115
|
+
line_width = int(args.width)
|
|
116
|
+
except ValueError:
|
|
117
|
+
die(f"invalid --width value: {args.width}")
|
|
118
|
+
if line_width < 0:
|
|
119
|
+
die(f"--width must be >= 0")
|
|
120
|
+
|
|
121
|
+
if args.syntax:
|
|
122
|
+
original_syntax = args.syntax
|
|
123
|
+
args.syntax = syntax(args.syntax)
|
|
124
|
+
if args.syntax is None:
|
|
125
|
+
die(f"unknown syntax '{original_syntax}' (try a language name like 'python' or extension like '.py')")
|
|
126
|
+
|
|
127
|
+
types = None
|
|
128
|
+
if args.type:
|
|
129
|
+
types = set()
|
|
130
|
+
for name in args.type:
|
|
131
|
+
r = syntax(name)
|
|
132
|
+
if r is None:
|
|
133
|
+
die(f"unknown --type '{name}' (try a language name like 'python' or extension like '.py')")
|
|
134
|
+
types.add(r)
|
|
135
|
+
|
|
136
|
+
sources = args.files or ["-"]
|
|
137
|
+
if sources == ["-"] and sys.stdin.isatty():
|
|
138
|
+
ap.print_help()
|
|
139
|
+
return 0
|
|
140
|
+
sources = _expand_sources(sources, types)
|
|
141
|
+
multi = len(sources) > 1
|
|
142
|
+
|
|
143
|
+
exit_code = 0
|
|
144
|
+
for src in sources:
|
|
145
|
+
try:
|
|
146
|
+
if src == "-":
|
|
147
|
+
text = sys.stdin.read()
|
|
148
|
+
else:
|
|
149
|
+
with open(src, encoding="utf-8", errors="replace") as fh:
|
|
150
|
+
text = fh.read()
|
|
151
|
+
except OSError as exc:
|
|
152
|
+
print(f"outliner: {exc}", file=sys.stderr)
|
|
153
|
+
exit_code = 1
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
match = args.syntax or guess_syntax(src) or detect(text)
|
|
157
|
+
|
|
158
|
+
if match is None:
|
|
159
|
+
print(f"outliner: cannot auto-detect syntax for '{src}'; use --syntax",
|
|
160
|
+
file=sys.stderr)
|
|
161
|
+
exit_code = 2
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
items = outline(match, text)
|
|
165
|
+
if items is None:
|
|
166
|
+
available = ", ".join(NAMES)
|
|
167
|
+
print(f"outliner: unsupported syntax '{match}'; available: {available}",
|
|
168
|
+
file=sys.stderr)
|
|
169
|
+
exit_code = 2
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
output_lines = _format_items(items, grep_re, line_width)
|
|
173
|
+
|
|
174
|
+
if output_lines:
|
|
175
|
+
if multi:
|
|
176
|
+
print(f"\n==> {src} <==")
|
|
177
|
+
print("\n".join(output_lines))
|
|
178
|
+
|
|
179
|
+
return exit_code
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
sys.exit(main())
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
-
from . import python, scala, go, java, rust, swift, c, ruby, php, shell, javascript, csharp, perl, zig, clojure, asciidoc, orgmode, rst, markdown
|
|
3
|
+
from . import python, scala, go, java, rust, swift, c, ruby, php, shell, javascript, csharp, perl, zig, clojure, html, asciidoc, orgmode, rst, markdown
|
|
4
4
|
from outliner.types import OutlineItem
|
|
5
5
|
|
|
6
|
-
_MODULES = [python, scala, go, java, rust, swift, c, ruby, php, shell, javascript, csharp, perl, zig, clojure, asciidoc, orgmode, rst, markdown]
|
|
6
|
+
_MODULES = [python, scala, go, java, rust, swift, c, ruby, php, shell, javascript, csharp, perl, zig, clojure, html, asciidoc, orgmode, rst, markdown]
|
|
7
7
|
_PARSERS = {mod.SYNTAX: mod.parse for mod in _MODULES}
|
|
8
8
|
NAMES = sorted(_PARSERS)
|
|
9
9
|
EXTENSIONS = {ext: mod.SYNTAX for mod in _MODULES for ext in mod.EXTENSIONS}
|
|
@@ -16,6 +16,13 @@ def _strip_frontmatter(content: str) -> str:
|
|
|
16
16
|
return content[m.end():] if m else content
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def syntax(name: str) -> str | None:
|
|
20
|
+
if name in _PARSERS:
|
|
21
|
+
return name
|
|
22
|
+
ext = name if name.startswith(".") else "." + name
|
|
23
|
+
return EXTENSIONS.get(ext)
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def detect(content: str) -> str | None:
|
|
20
27
|
lines = _strip_frontmatter(content).splitlines()[:100]
|
|
21
28
|
for mod in _MODULES:
|