chiridion 0.3.4
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/lib/chiridion/config.rb +128 -0
- data/lib/chiridion/engine/class_linker.rb +204 -0
- data/lib/chiridion/engine/document_model.rb +299 -0
- data/lib/chiridion/engine/drift_checker.rb +146 -0
- data/lib/chiridion/engine/extractor.rb +311 -0
- data/lib/chiridion/engine/file_renderer.rb +717 -0
- data/lib/chiridion/engine/file_writer.rb +160 -0
- data/lib/chiridion/engine/frontmatter_builder.rb +248 -0
- data/lib/chiridion/engine/generated_rbs_loader.rb +344 -0
- data/lib/chiridion/engine/github_linker.rb +87 -0
- data/lib/chiridion/engine/inline_rbs_loader.rb +207 -0
- data/lib/chiridion/engine/post_processor.rb +86 -0
- data/lib/chiridion/engine/rbs_loader.rb +150 -0
- data/lib/chiridion/engine/rbs_type_alias_loader.rb +116 -0
- data/lib/chiridion/engine/renderer.rb +598 -0
- data/lib/chiridion/engine/semantic_extractor.rb +740 -0
- data/lib/chiridion/engine/semantic_renderer.rb +334 -0
- data/lib/chiridion/engine/spec_example_loader.rb +84 -0
- data/lib/chiridion/engine/template_renderer.rb +275 -0
- data/lib/chiridion/engine/type_merger.rb +126 -0
- data/lib/chiridion/engine/writer.rb +134 -0
- data/lib/chiridion/engine.rb +359 -0
- data/lib/chiridion/semantic_engine.rb +186 -0
- data/lib/chiridion/version.rb +5 -0
- data/lib/chiridion.rb +106 -0
- data/templates/constants.liquid +27 -0
- data/templates/document.liquid +48 -0
- data/templates/file.liquid +108 -0
- data/templates/index.liquid +21 -0
- data/templates/method.liquid +43 -0
- data/templates/methods.liquid +11 -0
- data/templates/type_aliases.liquid +26 -0
- data/templates/types.liquid +11 -0
- metadata +146 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0e87a90e1e1a9905e4ec8fe90fa2bb3dfbdf5c30c69d22dc40076e71ac282251
|
|
4
|
+
data.tar.gz: d5ca6cfe8f1d60040f0bc47a79a047b436f17cb6cd7ed94981cae3e4b4629396
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2280d3d512e5ac20601e33e63ccac2fed856b1e0e9cbd89ec22ca36b32b1ac944468a44b6518fbb674ea603b4c222ea9d034fb29024626d0c618bfe94b36128d
|
|
7
|
+
data.tar.gz: 67ec810bdd67b201303712dcddb97d5790b7991bf89494bedcf30bfe8d32e94f5ca449445b9526268ee3808b621faa5dda647137fbf3b86212517e02cee9c43a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.4] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Declare `logger` and `base64` as runtime dependencies. Both are Ruby
|
|
7
|
+
default-gem extractions (`base64` @ 3.4, `logger` @ 3.5/4.0) that
|
|
8
|
+
chiridion needs at runtime — `logger` directly (engine), `base64`
|
|
9
|
+
transitively via `liquid` (which does not declare it). Without these,
|
|
10
|
+
`require "chiridion"` raised `LoadError` for any consumer running
|
|
11
|
+
under bundler on Ruby >= 3.4. No behavior change.
|
|
12
|
+
|
|
13
|
+
> Note: this changelog was not maintained for 0.2.x–0.3.3; entries
|
|
14
|
+
> resume here rather than reconstruct unrecorded history.
|
|
15
|
+
|
|
16
|
+
## [0.1.0] - 2024-12-09
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Initial release
|
|
20
|
+
- YARD-based documentation extraction
|
|
21
|
+
- RBS type signature integration
|
|
22
|
+
- Liquid template rendering
|
|
23
|
+
- Obsidian-compatible wikilinks
|
|
24
|
+
- Spec example extraction
|
|
25
|
+
- Drift detection for CI/CD
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Joseph Wecker
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Chiridion
|
|
2
|
+
|
|
3
|
+
Agent-oriented documentation generator for Ruby projects.
|
|
4
|
+
|
|
5
|
+
**Chiridion** (from Greek χειρίδιον, "handbook" — the diminutive of χείρ "hand") generates documentation optimized for AI agents and LLMs working with Ruby codebases.
|
|
6
|
+
|
|
7
|
+
## Why Agent-Oriented Documentation?
|
|
8
|
+
|
|
9
|
+
Traditional documentation is written for human developers reading in browsers. Agent-oriented documentation is optimized for LLMs processing in context windows:
|
|
10
|
+
|
|
11
|
+
- **Structured frontmatter** with navigation metadata for programmatic traversal
|
|
12
|
+
- **Explicit type information** from RBS (not just prose descriptions)
|
|
13
|
+
- **Cross-reference wikilinks** that can be followed programmatically
|
|
14
|
+
- **Compact method signatures** that maximize information density
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **YARD Integration** — Extracts docstrings, @param, @return, @example tags
|
|
19
|
+
- **RBS Authority** — RBS type signatures (inline or in sig/) are authoritative over YARD
|
|
20
|
+
- **Inline RBS Preferred** — Supports `@rbs` inline annotations via rbs-inline
|
|
21
|
+
- **Spec Examples** — Extracts usage examples from RSpec files
|
|
22
|
+
- **Wikilinks** — Obsidian-compatible `[[Class::Name]]` cross-references
|
|
23
|
+
- **Drift Detection** — CI mode to ensure docs stay in sync with code
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add to your Gemfile:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem "chiridion", path: "~/src/chiridion" # Local development
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Configuration
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Chiridion.configure do |config|
|
|
39
|
+
config.source_path = "lib/myproject"
|
|
40
|
+
config.output = "docs/sys"
|
|
41
|
+
config.namespace_filter = "MyProject::"
|
|
42
|
+
config.github_repo = "user/repo"
|
|
43
|
+
config.include_specs = true
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Generate Documentation
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
Chiridion.refresh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or with explicit paths:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
engine = Chiridion::Engine.new(
|
|
57
|
+
paths: ['lib/myproject'],
|
|
58
|
+
output: 'docs/sys',
|
|
59
|
+
namespace_filter: 'MyProject::'
|
|
60
|
+
)
|
|
61
|
+
engine.refresh
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Check for Drift (CI)
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Chiridion.check # Exits with code 1 if drift detected
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Inline RBS (Preferred)
|
|
71
|
+
|
|
72
|
+
Chiridion prioritizes inline RBS annotations over separate sig/ files:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Calculator
|
|
76
|
+
# @rbs a: Integer -- first operand
|
|
77
|
+
# @rbs b: Integer -- second operand
|
|
78
|
+
# @rbs return: Integer
|
|
79
|
+
def add(a, b)
|
|
80
|
+
a + b
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This keeps types co-located with code and is the recommended approach. Separate `sig/*.rbs` files are supported as a fallback.
|
|
86
|
+
|
|
87
|
+
## Output Format
|
|
88
|
+
|
|
89
|
+
Generated markdown includes:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
---
|
|
93
|
+
generated: 2024-12-09 10:30 UTC
|
|
94
|
+
source: lib/myproject/calculator.rb:10-25
|
|
95
|
+
source_url: https://github.com/user/repo/blob/main/lib/myproject/calculator.rb#L10
|
|
96
|
+
type: class
|
|
97
|
+
parent: Object
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
# MyProject::Calculator
|
|
101
|
+
|
|
102
|
+
Calculator for basic arithmetic operations.
|
|
103
|
+
|
|
104
|
+
## Methods
|
|
105
|
+
|
|
106
|
+
### `#add`
|
|
107
|
+
|
|
108
|
+
```rbs
|
|
109
|
+
(Integer a, Integer b) -> Integer
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Adds two integers.
|
|
113
|
+
|
|
114
|
+
**Parameters:**
|
|
115
|
+
- `a` (`Integer`) first operand
|
|
116
|
+
- `b` (`Integer`) second operand
|
|
117
|
+
|
|
118
|
+
**Returns:** `Integer`
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Integration with Projects
|
|
122
|
+
|
|
123
|
+
### Archema
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Gemfile
|
|
127
|
+
gem "chiridion", path: "~/src/chiridion"
|
|
128
|
+
|
|
129
|
+
# Configure in tasks/docs.rb
|
|
130
|
+
Chiridion.configure do |config|
|
|
131
|
+
config.namespace_filter = "Archema::"
|
|
132
|
+
config.output = "docs/sys"
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### devex Integration
|
|
137
|
+
|
|
138
|
+
Create a `tools/docs.rb`:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
# frozen_string_literal: true
|
|
142
|
+
|
|
143
|
+
desc "Documentation generation tasks"
|
|
144
|
+
|
|
145
|
+
tool "refresh" do
|
|
146
|
+
desc "Regenerate API documentation"
|
|
147
|
+
|
|
148
|
+
def run
|
|
149
|
+
require_relative "../lib/myproject"
|
|
150
|
+
|
|
151
|
+
Chiridion.configure do |c|
|
|
152
|
+
c.source_path = "lib/myproject"
|
|
153
|
+
c.output = "docs/sys"
|
|
154
|
+
c.namespace_filter = "MyProject::"
|
|
155
|
+
c.verbose = verbose? # Uses global -v flag
|
|
156
|
+
end
|
|
157
|
+
Chiridion.refresh
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
tool "check" do
|
|
162
|
+
desc "Check for documentation drift (CI mode)"
|
|
163
|
+
|
|
164
|
+
def run
|
|
165
|
+
Chiridion.check
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Then run with `dx docs refresh` or `dx docs check`.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
Chiridion uses itself to generate its own API documentation (dogfooding). The generated docs live in `docs/sys/`.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Run tests
|
|
178
|
+
dx test
|
|
179
|
+
|
|
180
|
+
# Lint
|
|
181
|
+
dx lint
|
|
182
|
+
|
|
183
|
+
# Regenerate Chiridion's own documentation
|
|
184
|
+
dx docs refresh
|
|
185
|
+
|
|
186
|
+
# Verbose output
|
|
187
|
+
dx -v docs refresh
|
|
188
|
+
|
|
189
|
+
# Check for drift (CI mode - exits 1 if docs are out of sync)
|
|
190
|
+
dx docs check
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
This serves as both a live integration test and a reference for the output format.
|
|
194
|
+
|
|
195
|
+
## Name Origin
|
|
196
|
+
|
|
197
|
+
"Chiridion" is the Greek word for a small handbook or manual — appropriate for a tool that generates compact, structured documentation for AI assistants to reference.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chiridion
|
|
4
|
+
# Configuration for documentation generation.
|
|
5
|
+
#
|
|
6
|
+
# Chiridion can be configured globally or per-engine instance.
|
|
7
|
+
# All options have sensible defaults for common Ruby project layouts.
|
|
8
|
+
#
|
|
9
|
+
# @example Global configuration
|
|
10
|
+
# Chiridion.configure do |config|
|
|
11
|
+
# config.output = 'docs/sys'
|
|
12
|
+
# config.namespace_filter = 'MyProject::'
|
|
13
|
+
# config.github_repo = 'user/repo'
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @example Per-project configuration file
|
|
17
|
+
# # .chiridion.yml
|
|
18
|
+
# output: docs/sys
|
|
19
|
+
# namespace_filter: MyProject::
|
|
20
|
+
# github_repo: user/repo
|
|
21
|
+
# include_specs: true
|
|
22
|
+
#
|
|
23
|
+
class Config
|
|
24
|
+
# @return [String] Root directory of the project (defaults to current directory)
|
|
25
|
+
attr_accessor :root
|
|
26
|
+
|
|
27
|
+
# @return [String] Source directory to document (relative to root)
|
|
28
|
+
attr_accessor :source_path
|
|
29
|
+
|
|
30
|
+
# @return [String] Output directory for generated docs
|
|
31
|
+
attr_accessor :output
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] Namespace prefix to filter (e.g., "MyProject::")
|
|
34
|
+
# Only classes/modules starting with this prefix are documented.
|
|
35
|
+
# If nil, all classes are included.
|
|
36
|
+
attr_accessor :namespace_filter
|
|
37
|
+
|
|
38
|
+
# @return [String, nil] Namespace prefix to strip from output paths
|
|
39
|
+
# Defaults to namespace_filter value.
|
|
40
|
+
attr_writer :namespace_strip
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] GitHub repository for source links (e.g., "user/repo")
|
|
43
|
+
attr_accessor :github_repo
|
|
44
|
+
|
|
45
|
+
# @return [String] Git branch for source links
|
|
46
|
+
attr_accessor :github_branch
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] Whether to extract examples from spec files
|
|
49
|
+
attr_accessor :include_specs
|
|
50
|
+
|
|
51
|
+
# @return [String] Path to test directory (relative to root)
|
|
52
|
+
attr_accessor :spec_path
|
|
53
|
+
|
|
54
|
+
# @return [String] Path to RBS signatures directory (relative to root)
|
|
55
|
+
attr_accessor :rbs_path
|
|
56
|
+
|
|
57
|
+
# @return [Boolean] Verbose output during generation
|
|
58
|
+
attr_accessor :verbose
|
|
59
|
+
|
|
60
|
+
# @return [#info, #warn, #error, nil] Logger for output messages
|
|
61
|
+
attr_accessor :logger
|
|
62
|
+
|
|
63
|
+
# @return [Integer, nil] Maximum body lines for inline source display.
|
|
64
|
+
# Methods with body <= this many lines show their implementation inline.
|
|
65
|
+
# Set to nil or 0 to disable inline source. Default: 10.
|
|
66
|
+
attr_accessor :inline_source_threshold
|
|
67
|
+
|
|
68
|
+
# @return [Symbol] Output organization strategy (:per_class or :per_file)
|
|
69
|
+
attr_accessor :output_mode
|
|
70
|
+
|
|
71
|
+
def initialize
|
|
72
|
+
@root = Dir.pwd
|
|
73
|
+
@source_path = "lib"
|
|
74
|
+
@output = "docs/sys"
|
|
75
|
+
@namespace_filter = nil
|
|
76
|
+
@namespace_strip = nil
|
|
77
|
+
@github_repo = nil
|
|
78
|
+
@github_branch = "main"
|
|
79
|
+
@include_specs = false
|
|
80
|
+
@spec_path = "test"
|
|
81
|
+
@rbs_path = "sig"
|
|
82
|
+
@verbose = false
|
|
83
|
+
@logger = nil
|
|
84
|
+
@inline_source_threshold = 10
|
|
85
|
+
@output_mode = :per_file
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Namespace prefix to strip from output paths.
|
|
89
|
+
# Defaults to namespace_filter if not explicitly set.
|
|
90
|
+
def namespace_strip = @namespace_strip || @namespace_filter
|
|
91
|
+
|
|
92
|
+
# Load configuration from a YAML file.
|
|
93
|
+
#
|
|
94
|
+
# @param path [String] Path to YAML configuration file
|
|
95
|
+
# @return [Config] self
|
|
96
|
+
def load_file(path)
|
|
97
|
+
return self unless File.exist?(path)
|
|
98
|
+
|
|
99
|
+
require "yaml"
|
|
100
|
+
data = YAML.safe_load_file(path, symbolize_names: true)
|
|
101
|
+
load_hash(data)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Load configuration from a hash.
|
|
105
|
+
#
|
|
106
|
+
# @param data [Hash] Configuration values
|
|
107
|
+
# @return [Config] self
|
|
108
|
+
def load_hash(data)
|
|
109
|
+
data.each do |key, value|
|
|
110
|
+
setter = :"#{key}="
|
|
111
|
+
public_send(setter, value) if respond_to?(setter)
|
|
112
|
+
end
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [String] Full path to source directory
|
|
117
|
+
def full_source_path = File.join(root, source_path)
|
|
118
|
+
|
|
119
|
+
# @return [String] Full path to output directory
|
|
120
|
+
def full_output_path = File.join(root, output)
|
|
121
|
+
|
|
122
|
+
# @return [String] Full path to spec directory
|
|
123
|
+
def full_spec_path = File.join(root, spec_path)
|
|
124
|
+
|
|
125
|
+
# @return [String] Full path to RBS directory
|
|
126
|
+
def full_rbs_path = File.join(root, rbs_path)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chiridion
|
|
4
|
+
class Engine
|
|
5
|
+
# Converts class/module references to Obsidian wikilinks.
|
|
6
|
+
#
|
|
7
|
+
# Handles various reference formats:
|
|
8
|
+
# - Full paths: `Autopax::Foo::Bar` → `[[foo/bar|Bar]]`
|
|
9
|
+
# - YARD curly braces: `{Extractor}` → `[[extractor|Extractor]]`
|
|
10
|
+
# - Relative names: `Writer` → `[[writer|Writer]]` (within same namespace)
|
|
11
|
+
class ClassLinker
|
|
12
|
+
# @return [Hash<String, String>] Known classes mapped to doc paths
|
|
13
|
+
attr_reader :known_classes
|
|
14
|
+
|
|
15
|
+
# @return [String, nil] Namespace prefix to strip from paths
|
|
16
|
+
attr_reader :namespace_strip
|
|
17
|
+
|
|
18
|
+
def initialize(namespace_strip: nil)
|
|
19
|
+
@namespace_strip = namespace_strip
|
|
20
|
+
@known_classes = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Register known classes from the documentation structure.
|
|
24
|
+
#
|
|
25
|
+
# @param structure [Hash] Documentation structure from Extractor
|
|
26
|
+
def register_classes(structure)
|
|
27
|
+
(structure[:classes] + structure[:modules]).each do |obj|
|
|
28
|
+
path = obj[:path]
|
|
29
|
+
@known_classes[path] = doc_path(path)
|
|
30
|
+
# Also register short name for relative lookups
|
|
31
|
+
short_name = path.split("::").last
|
|
32
|
+
@known_classes[short_name] ||= doc_path(path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert a class path to a wikilink.
|
|
37
|
+
#
|
|
38
|
+
# @param class_path [String] Full or relative class path
|
|
39
|
+
# @param context [String, nil] Current class context for relative resolution
|
|
40
|
+
# @return [String] Wikilink like `[[path|Name]]` or original if not found
|
|
41
|
+
def link(class_path, context: nil)
|
|
42
|
+
display_name = class_path.split("::").last
|
|
43
|
+
resolved = resolve(class_path, context: context)
|
|
44
|
+
return display_name unless resolved
|
|
45
|
+
|
|
46
|
+
"[[#{resolved}|#{display_name}]]"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Process a docstring, converting {Class} references to wikilinks.
|
|
50
|
+
#
|
|
51
|
+
# @param text [String] Docstring text
|
|
52
|
+
# @param context [String, nil] Current class context
|
|
53
|
+
# @return [String] Text with {Class} converted to wikilinks
|
|
54
|
+
def linkify_docstring(text, context: nil)
|
|
55
|
+
return text if text.nil? || text.empty?
|
|
56
|
+
|
|
57
|
+
result = text.dup
|
|
58
|
+
|
|
59
|
+
# Convert YARD headings (= Title) to markdown headings
|
|
60
|
+
# Direct 1:1 conversion; normalize_headers filter will adjust levels for context
|
|
61
|
+
result.gsub!(/^(=+)\s+(.+)$/) do
|
|
62
|
+
level = Regexp.last_match(1).length
|
|
63
|
+
"#" * level + " " + Regexp.last_match(2)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Convert {Class} references to wikilinks
|
|
67
|
+
result.gsub(/\{([A-Z][\w:]*)\}/) do |_match|
|
|
68
|
+
class_ref = Regexp.last_match(1)
|
|
69
|
+
link(class_ref, context: context)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Convert a type annotation to include wikilinks where possible.
|
|
74
|
+
#
|
|
75
|
+
# Returns formatted string with backticks around non-link parts.
|
|
76
|
+
# Wikilinks must be outside backticks to render properly.
|
|
77
|
+
#
|
|
78
|
+
# @param type_str [String] Type like `Array<Autopax::Foo>` or `Hash{String => Bar}`
|
|
79
|
+
# @param context [String, nil] Current class context
|
|
80
|
+
# @return [String] Formatted type with proper backtick placement
|
|
81
|
+
def linkify_type(type_str, context: nil)
|
|
82
|
+
return "`Object`" if type_str.nil? || type_str.empty?
|
|
83
|
+
|
|
84
|
+
segments = build_type_segments(type_str, context: context)
|
|
85
|
+
format_type_segments(segments)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if a class is a known documentable class.
|
|
89
|
+
#
|
|
90
|
+
# @param class_name [String] Class name to check
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def known?(class_name) = @known_classes.key?(class_name)
|
|
93
|
+
|
|
94
|
+
SKIP_TYPES = %w[Array Hash String Integer Float Symbol Boolean Object TrueClass FalseClass NilClass Proc
|
|
95
|
+
Class Module Numeric Enumerable Comparable void untyped nil self].freeze
|
|
96
|
+
private_constant :SKIP_TYPES
|
|
97
|
+
|
|
98
|
+
def skip_type?(class_ref) = SKIP_TYPES.include?(class_ref)
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Build segments from type string, identifying linkable class refs.
|
|
103
|
+
def build_type_segments(type_str, context:)
|
|
104
|
+
segments = []
|
|
105
|
+
last_end = 0
|
|
106
|
+
|
|
107
|
+
type_str.scan(/\b([A-Z]\w*(?:::[A-Z]\w*)*)\b/) do
|
|
108
|
+
class_ref = Regexp.last_match(1)
|
|
109
|
+
match_start = Regexp.last_match.begin(0)
|
|
110
|
+
match_end = Regexp.last_match.end(0)
|
|
111
|
+
|
|
112
|
+
segments << [:text, type_str[last_end...match_start]] if match_start > last_end
|
|
113
|
+
segments << segment_for_class(class_ref, context: context)
|
|
114
|
+
last_end = match_end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
segments << [:text, type_str[last_end..]] if last_end < type_str.length
|
|
118
|
+
segments
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Create segment for a class reference (link or text).
|
|
122
|
+
def segment_for_class(class_ref, context:)
|
|
123
|
+
return [:text, class_ref] if skip_type?(class_ref)
|
|
124
|
+
|
|
125
|
+
resolved = resolve(class_ref, context: context)
|
|
126
|
+
return [:text, class_ref] unless resolved
|
|
127
|
+
|
|
128
|
+
[:link, "[[#{resolved}|#{class_ref.split('::').last}]]"]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Format segments into final string with proper backtick placement.
|
|
132
|
+
def format_type_segments(segments)
|
|
133
|
+
return "`Object`" if segments.empty?
|
|
134
|
+
return format_pure_text(segments) unless segments.any? { |type, _| type == :link }
|
|
135
|
+
return segments.first.last if pure_link?(segments)
|
|
136
|
+
|
|
137
|
+
format_mixed_segments(segments)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def format_pure_text(segments) = "`#{segments.map(&:last).join}`"
|
|
141
|
+
|
|
142
|
+
def pure_link?(segments) = segments.size == 1 && segments.first.first == :link
|
|
143
|
+
|
|
144
|
+
# Format mixed content: wrap text in backticks, leave links bare.
|
|
145
|
+
def format_mixed_segments(segments)
|
|
146
|
+
result = []
|
|
147
|
+
text_buffer = +""
|
|
148
|
+
|
|
149
|
+
segments.each do |type, content|
|
|
150
|
+
if type == :text
|
|
151
|
+
text_buffer << content
|
|
152
|
+
else
|
|
153
|
+
result << "`#{text_buffer}`" unless text_buffer.empty?
|
|
154
|
+
text_buffer.clear
|
|
155
|
+
result << content
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
result << "`#{text_buffer}`" unless text_buffer.empty?
|
|
160
|
+
result.join
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Resolve a class reference to its documentation path.
|
|
164
|
+
def resolve(class_ref, context: nil)
|
|
165
|
+
# Try exact match first
|
|
166
|
+
return @known_classes[class_ref] if @known_classes[class_ref]
|
|
167
|
+
|
|
168
|
+
# Try with namespace prefix
|
|
169
|
+
if @namespace_strip
|
|
170
|
+
full_path = "#{@namespace_strip}#{class_ref}"
|
|
171
|
+
return @known_classes[full_path] if @known_classes[full_path]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Try relative to context
|
|
175
|
+
if context
|
|
176
|
+
relative_path = "#{context}::#{class_ref}"
|
|
177
|
+
return @known_classes[relative_path] if @known_classes[relative_path]
|
|
178
|
+
|
|
179
|
+
# Try parent namespace
|
|
180
|
+
parent = context.split("::")[0..-2].join("::")
|
|
181
|
+
sibling_path = "#{parent}::#{class_ref}"
|
|
182
|
+
return @known_classes[sibling_path] if @known_classes[sibling_path]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Convert class path to documentation file path.
|
|
189
|
+
def doc_path(class_path)
|
|
190
|
+
stripped = @namespace_strip ? class_path.sub(/^#{Regexp.escape(@namespace_strip)}/, "") : class_path
|
|
191
|
+
parts = stripped.split("::")
|
|
192
|
+
kebab_parts = parts.map { |p| to_kebab_case(p) }
|
|
193
|
+
kebab_parts.join("/")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def to_kebab_case(str)
|
|
197
|
+
str.gsub(/([A-Za-z])([vV]\d+)/, '\1-\2')
|
|
198
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1-\2')
|
|
199
|
+
.gsub(/([a-z\d])([A-Z])/, '\1-\2')
|
|
200
|
+
.downcase
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|