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.
Files changed (63) hide show
  1. {outliner_cli-0.1.0/src/outliner_cli.egg-info → outliner_cli-0.2.0}/PKG-INFO +34 -26
  2. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/README.md +33 -25
  3. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/pyproject.toml +2 -2
  4. outliner_cli-0.2.0/src/outliner/cli.py +183 -0
  5. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/__init__.py +9 -2
  6. outliner_cli-0.2.0/src/outliner/parsers/html.py +217 -0
  7. outliner_cli-0.2.0/src/outliner/parsers/javascript.py +389 -0
  8. outliner_cli-0.2.0/src/outliner/types.py +19 -0
  9. {outliner_cli-0.1.0 → outliner_cli-0.2.0/src/outliner_cli.egg-info}/PKG-INFO +34 -26
  10. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner_cli.egg-info/SOURCES.txt +2 -0
  11. outliner_cli-0.2.0/src/outliner_cli.egg-info/entry_points.txt +2 -0
  12. outliner_cli-0.2.0/tests/test_cli.py +260 -0
  13. outliner_cli-0.2.0/tests/test_html.py +395 -0
  14. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_javascript.py +457 -29
  15. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_parsers.py +18 -0
  16. outliner_cli-0.1.0/src/outliner/cli.py +0 -115
  17. outliner_cli-0.1.0/src/outliner/parsers/javascript.py +0 -312
  18. outliner_cli-0.1.0/src/outliner/types.py +0 -8
  19. outliner_cli-0.1.0/src/outliner_cli.egg-info/entry_points.txt +0 -2
  20. outliner_cli-0.1.0/tests/test_cli.py +0 -130
  21. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/LICENSE +0 -0
  22. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/setup.cfg +0 -0
  23. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/__init__.py +0 -0
  24. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/asciidoc.py +0 -0
  25. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/c.py +0 -0
  26. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/clojure.py +0 -0
  27. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/csharp.py +0 -0
  28. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/go.py +0 -0
  29. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/java.py +0 -0
  30. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/markdown.py +0 -0
  31. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/orgmode.py +0 -0
  32. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/perl.py +0 -0
  33. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/php.py +0 -0
  34. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/python.py +0 -0
  35. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/rst.py +0 -0
  36. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/ruby.py +0 -0
  37. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/rust.py +0 -0
  38. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/scala.py +0 -0
  39. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/shell.py +0 -0
  40. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/swift.py +0 -0
  41. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/util.py +0 -0
  42. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner/parsers/zig.py +0 -0
  43. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner_cli.egg-info/dependency_links.txt +0 -0
  44. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/src/outliner_cli.egg-info/top_level.txt +0 -0
  45. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_asciidoc.py +0 -0
  46. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_c.py +0 -0
  47. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_clojure.py +0 -0
  48. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_csharp.py +0 -0
  49. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_go.py +0 -0
  50. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_java.py +0 -0
  51. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_markdown.py +0 -0
  52. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_orgmode.py +0 -0
  53. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_perl.py +0 -0
  54. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_php.py +0 -0
  55. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_python.py +0 -0
  56. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_rst.py +0 -0
  57. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_ruby.py +0 -0
  58. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_rust.py +0 -0
  59. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_scala.py +0 -0
  60. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_shell.py +0 -0
  61. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_swift.py +0 -0
  62. {outliner_cli-0.1.0 → outliner_cli-0.2.0}/tests/test_util.py +0 -0
  63. {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.1.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 with line ranges —
17
- so an LLM agent (or human) can navigate a file without reading it whole.
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
- # From within the outliner/ directory
64
- uv run outliner path/to/file.py
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
- # Outline an entire directory
70
- uv run --project outliner outliner src/
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
- Python, Go, Markdown, reStructuredText — with Java, Rust, JavaScript/TypeScript,
83
- C/C++, C#, and many more in progress.
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 files before reading anything:
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 locates every implementation of a repeated function in one command:
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 signatures, not bodies. It finds functions whose interface involves a concept, skipping internal uses, comments, and call sites:
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 every function where the type appears in parameters, return types, or generic bounds. Multi-line signatures are merged into a single line before matching, so nothing is missed:
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 with line ranges —
4
- so an LLM agent (or human) can navigate a file without reading it whole.
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
- # From within the outliner/ directory
51
- uv run outliner path/to/file.py
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
- # Outline an entire directory
57
- uv run --project outliner outliner src/
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
- Python, Go, Markdown, reStructuredText — with Java, Rust, JavaScript/TypeScript,
70
- C/C++, C#, and many more in progress.
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 files before reading anything:
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 locates every implementation of a repeated function in one command:
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 signatures, not bodies. It finds functions whose interface involves a concept, skipping internal uses, comments, and call sites:
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 every function where the type appears in parameters, return types, or generic bounds. Multi-line signatures are merged into a single line before matching, so nothing is missed:
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.1.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: