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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chiridion
|
|
4
|
+
class Engine
|
|
5
|
+
# Post-processes rendered markdown for consistent formatting.
|
|
6
|
+
#
|
|
7
|
+
# Handles normalization that's easier to do as a final pass rather than
|
|
8
|
+
# trying to get perfect output from templates. May grow into fuller
|
|
9
|
+
# linting/validation over time.
|
|
10
|
+
#
|
|
11
|
+
# Current normalizations:
|
|
12
|
+
# - Collapse multiple consecutive newlines to single newlines
|
|
13
|
+
# - Ensure 2 newlines before horizontal rules (---)
|
|
14
|
+
# - Preserve frontmatter formatting
|
|
15
|
+
class PostProcessor
|
|
16
|
+
# Normalize markdown content.
|
|
17
|
+
#
|
|
18
|
+
# @param content [String] Raw markdown content
|
|
19
|
+
# @return [String] Normalized content
|
|
20
|
+
def self.process(content)
|
|
21
|
+
new.process(content)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param content [String] Raw markdown content
|
|
25
|
+
# @return [String] Normalized content
|
|
26
|
+
def process(content)
|
|
27
|
+
# Split off frontmatter to preserve it exactly
|
|
28
|
+
frontmatter, body = split_frontmatter(content)
|
|
29
|
+
|
|
30
|
+
# Normalize the body
|
|
31
|
+
normalized = normalize_newlines(body)
|
|
32
|
+
|
|
33
|
+
# Reassemble
|
|
34
|
+
frontmatter ? "#{frontmatter}\n\n#{normalized}" : normalized
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Split YAML frontmatter from body content.
|
|
40
|
+
#
|
|
41
|
+
# @param content [String] Full markdown content
|
|
42
|
+
# @return [Array(String, String), Array(nil, String)] [frontmatter, body] or [nil, content]
|
|
43
|
+
def split_frontmatter(content)
|
|
44
|
+
return [nil, content] unless content.start_with?("---")
|
|
45
|
+
|
|
46
|
+
# Find closing ---
|
|
47
|
+
lines = content.lines
|
|
48
|
+
closing_idx = lines[1..].index { |l| l.strip == "---" }
|
|
49
|
+
return [nil, content] unless closing_idx
|
|
50
|
+
|
|
51
|
+
# closing_idx is relative to lines[1..], so actual index is closing_idx + 1
|
|
52
|
+
fm_end = closing_idx + 2 # +1 for 0-index, +1 for the closing line itself
|
|
53
|
+
frontmatter = lines[0...fm_end].join.strip
|
|
54
|
+
body = lines[fm_end..].join
|
|
55
|
+
|
|
56
|
+
[frontmatter, body]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Normalize newlines in body content.
|
|
60
|
+
#
|
|
61
|
+
# Rules (applied in order):
|
|
62
|
+
# 1. Collapse runs of 3+ newlines to exactly 2 (one blank line)
|
|
63
|
+
# 2. Ensure 2 newlines before horizontal rules (---)
|
|
64
|
+
# 3. Remove blank lines after horizontal rules (---)
|
|
65
|
+
# 4. Remove blank lines after headers (# ## ### ####)
|
|
66
|
+
#
|
|
67
|
+
# @param body [String] Body content (no frontmatter)
|
|
68
|
+
# @return [String] Normalized body
|
|
69
|
+
def normalize_newlines(body)
|
|
70
|
+
# Step 1: Collapse 3+ newlines to exactly 2
|
|
71
|
+
result = body.gsub(/\n{3,}/, "\n\n")
|
|
72
|
+
|
|
73
|
+
# Step 2: Ensure 2 newlines before horizontal rules
|
|
74
|
+
result = result.gsub(/\n(---)/, "\n\n\\1")
|
|
75
|
+
|
|
76
|
+
# Step 3: Remove blank lines after horizontal rules
|
|
77
|
+
result = result.gsub(/^(---)\n+/, "\\1\n")
|
|
78
|
+
|
|
79
|
+
# Step 4: Remove blank lines after headers
|
|
80
|
+
result = result.gsub(/^(\#{1,6}\s+.+)\n+/, "\\1\n")
|
|
81
|
+
|
|
82
|
+
result.strip
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chiridion
|
|
4
|
+
class Engine
|
|
5
|
+
# Loads RBS type signatures from sig/ directory for documentation enrichment.
|
|
6
|
+
#
|
|
7
|
+
# Parses RBS files and extracts method signatures to merge with YARD documentation,
|
|
8
|
+
# providing accurate type information in generated docs.
|
|
9
|
+
class RbsLoader
|
|
10
|
+
def initialize(rbs_path, verbose, logger)
|
|
11
|
+
@rbs_path = rbs_path
|
|
12
|
+
@verbose = verbose
|
|
13
|
+
@logger = logger
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Load all RBS files and return a hash of class -> method -> signature.
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash{String => Hash{String => Hash}}] Nested hash of signatures
|
|
19
|
+
def load
|
|
20
|
+
signatures = {}
|
|
21
|
+
rbs_files = Dir.glob("#{@rbs_path}/**/*.rbs")
|
|
22
|
+
|
|
23
|
+
return signatures if rbs_files.empty?
|
|
24
|
+
|
|
25
|
+
@logger.info "Loading #{rbs_files.size} RBS files..." if @verbose
|
|
26
|
+
rbs_files.each { |file| parse_file(file, signatures) }
|
|
27
|
+
signatures
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_file(file, signatures)
|
|
33
|
+
content = File.read(file)
|
|
34
|
+
current_class = nil
|
|
35
|
+
|
|
36
|
+
content.each_line do |line|
|
|
37
|
+
current_class = extract_class_name(line, signatures, current_class)
|
|
38
|
+
next unless current_class
|
|
39
|
+
|
|
40
|
+
extract_method_signature(line, signatures, current_class)
|
|
41
|
+
extract_attr_signature(line, signatures, current_class)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_class_name(line, signatures, current_class)
|
|
46
|
+
return current_class unless line =~ /^\s*(?:class|module)\s+([\w:]+)/
|
|
47
|
+
|
|
48
|
+
class_name = Regexp.last_match(1)
|
|
49
|
+
signatures[class_name] ||= {}
|
|
50
|
+
class_name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_method_signature(line, signatures, current_class)
|
|
54
|
+
return unless line =~ /^\s*def\s+(?:self\.)?(\w+[?!]?):\s*(.+)$/
|
|
55
|
+
|
|
56
|
+
method_name = Regexp.last_match(1)
|
|
57
|
+
full_sig = Regexp.last_match(2).strip
|
|
58
|
+
signatures[current_class][method_name] = parse_signature(full_sig)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_attr_signature(line, signatures, current_class)
|
|
62
|
+
return unless line =~ /^\s*attr_(?:reader|accessor)\s+(\w+):\s*(.+)$/
|
|
63
|
+
|
|
64
|
+
attr_name = Regexp.last_match(1)
|
|
65
|
+
attr_type = Regexp.last_match(2).strip
|
|
66
|
+
signatures[current_class][attr_name] = {
|
|
67
|
+
full: "() -> #{attr_type}",
|
|
68
|
+
params: {},
|
|
69
|
+
returns: attr_type
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Parse RBS signature into structured data.
|
|
74
|
+
#
|
|
75
|
+
# @param sig [String] RBS signature string
|
|
76
|
+
# @return [Hash] Parsed signature with :full, :params, :returns keys
|
|
77
|
+
def parse_signature(sig)
|
|
78
|
+
result = { full: sig, params: {}, returns: nil }
|
|
79
|
+
|
|
80
|
+
if sig =~ /\A\(([^)]*)\)\s*->\s*(.+)\z/
|
|
81
|
+
params_str = Regexp.last_match(1)
|
|
82
|
+
result[:returns] = Regexp.last_match(2).strip
|
|
83
|
+
parse_params(params_str, result[:params])
|
|
84
|
+
elsif sig =~ /\A\(\)\s*->\s*(.+)\z/
|
|
85
|
+
result[:returns] = Regexp.last_match(1).strip
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Parse RBS parameter list, handling nested brackets.
|
|
92
|
+
def parse_params(params_str, result)
|
|
93
|
+
return if params_str.strip.empty?
|
|
94
|
+
|
|
95
|
+
params = split_respecting_brackets(params_str)
|
|
96
|
+
|
|
97
|
+
params.each do |param|
|
|
98
|
+
param = param.strip
|
|
99
|
+
next if param.empty?
|
|
100
|
+
|
|
101
|
+
parse_single_param(param, result)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_single_param(param, result)
|
|
106
|
+
# Keyword arg: `?name: Type?` or `name: Type`
|
|
107
|
+
if param =~ /\A\??(\w+):\s*(.+)\z/
|
|
108
|
+
name = Regexp.last_match(1)
|
|
109
|
+
type = Regexp.last_match(2).strip.delete_suffix("?")
|
|
110
|
+
result[name] = type
|
|
111
|
+
# Positional: `?Type name` or `Type name`
|
|
112
|
+
elsif param =~ /\A\??(.+?)\s+(\w+)\z/
|
|
113
|
+
type = Regexp.last_match(1).strip
|
|
114
|
+
name = Regexp.last_match(2)
|
|
115
|
+
result[name] = type
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Split string by commas while respecting nested brackets [], {}, ().
|
|
120
|
+
def split_respecting_brackets(str)
|
|
121
|
+
result = []
|
|
122
|
+
current = +"" # Mutable string
|
|
123
|
+
depth = 0
|
|
124
|
+
|
|
125
|
+
str.each_char do |c|
|
|
126
|
+
case c
|
|
127
|
+
when "[", "{", "("
|
|
128
|
+
depth += 1
|
|
129
|
+
current << c
|
|
130
|
+
when "]", "}", ")"
|
|
131
|
+
depth -= 1
|
|
132
|
+
current << c
|
|
133
|
+
when ","
|
|
134
|
+
if depth.zero?
|
|
135
|
+
result << current
|
|
136
|
+
current = +"" # Mutable string
|
|
137
|
+
else
|
|
138
|
+
current << c
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
current << c
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
result << current unless current.empty?
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chiridion
|
|
4
|
+
class Engine
|
|
5
|
+
# Extracts RBS type alias definitions from generated .rbs files.
|
|
6
|
+
#
|
|
7
|
+
# Reads type aliases from RBS files (typically in sig/generated/) which have
|
|
8
|
+
# already been properly parsed by RBS::Inline. This is more reliable than
|
|
9
|
+
# re-parsing @rbs! blocks ourselves.
|
|
10
|
+
#
|
|
11
|
+
# RBS format is straightforward:
|
|
12
|
+
# module Namespace
|
|
13
|
+
# # Description comment
|
|
14
|
+
# type name = definition
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# loader = RbsTypeAliasLoader.new(true, logger, rbs_dir: "sig/generated")
|
|
19
|
+
# type_aliases = loader.load
|
|
20
|
+
# # => { "Archema" => [{ name: "attribute_value", definition: "...", ... }] }
|
|
21
|
+
#
|
|
22
|
+
class RbsTypeAliasLoader
|
|
23
|
+
def initialize(verbose, logger, rbs_dir: nil)
|
|
24
|
+
@verbose = verbose
|
|
25
|
+
@logger = logger
|
|
26
|
+
@rbs_dir = rbs_dir
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Extract type aliases from generated RBS files.
|
|
30
|
+
#
|
|
31
|
+
# @return [Hash{String => Array<Hash>}] namespace -> array of type definitions
|
|
32
|
+
def load
|
|
33
|
+
type_aliases = {}
|
|
34
|
+
return type_aliases unless @rbs_dir && Dir.exist?(@rbs_dir)
|
|
35
|
+
|
|
36
|
+
rbs_files = Dir.glob(File.join(@rbs_dir, "**/*.rbs"))
|
|
37
|
+
rbs_files.each do |file|
|
|
38
|
+
parse_rbs_file(file, type_aliases)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
count = type_aliases.values.flatten.size
|
|
42
|
+
@logger.info "Extracted #{count} type aliases from #{rbs_files.size} RBS files" if @verbose && count.positive?
|
|
43
|
+
type_aliases
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Parse a .rbs file for type aliases.
|
|
49
|
+
#
|
|
50
|
+
# RBS files have a clean, well-defined format:
|
|
51
|
+
# module Foo
|
|
52
|
+
# # comment
|
|
53
|
+
# type name = definition
|
|
54
|
+
# end
|
|
55
|
+
def parse_rbs_file(file, type_aliases)
|
|
56
|
+
content = File.read(file)
|
|
57
|
+
return unless content.include?("type ")
|
|
58
|
+
|
|
59
|
+
expanded_file = File.expand_path(file)
|
|
60
|
+
lines = content.lines
|
|
61
|
+
namespace_stack = []
|
|
62
|
+
pending_comment = nil
|
|
63
|
+
|
|
64
|
+
lines.each_with_index do |line, idx|
|
|
65
|
+
line_num = idx + 1
|
|
66
|
+
stripped = line.strip
|
|
67
|
+
|
|
68
|
+
# Track module/class context (RBS uses same syntax)
|
|
69
|
+
# Reset pending_comment - comments before class/module are for that class, not types inside
|
|
70
|
+
if stripped =~ /^(?:class|module)\s+([\w:]+)/
|
|
71
|
+
name = Regexp.last_match(1)
|
|
72
|
+
namespace_stack.push(name)
|
|
73
|
+
pending_comment = nil
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Track end statements
|
|
78
|
+
if stripped == "end"
|
|
79
|
+
namespace_stack.pop if namespace_stack.any?
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Collect comments (description for next type)
|
|
84
|
+
if stripped.start_with?("#")
|
|
85
|
+
# Accumulate multi-line comments, preserving newlines
|
|
86
|
+
comment_text = stripped.sub(/^#\s?/, "")
|
|
87
|
+
pending_comment = pending_comment ? "#{pending_comment}\n#{comment_text}" : comment_text
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Skip blank lines (preserve pending_comment across blanks)
|
|
92
|
+
next if stripped.empty?
|
|
93
|
+
|
|
94
|
+
# Parse type definition
|
|
95
|
+
if stripped =~ /^type\s+(\w+)\s*=\s*(.+)$/
|
|
96
|
+
type_name = Regexp.last_match(1)
|
|
97
|
+
type_def = Regexp.last_match(2)
|
|
98
|
+
|
|
99
|
+
namespace = namespace_stack.join("::")
|
|
100
|
+
type_aliases[namespace] ||= []
|
|
101
|
+
type_aliases[namespace] << {
|
|
102
|
+
name: type_name,
|
|
103
|
+
definition: type_def,
|
|
104
|
+
description: pending_comment,
|
|
105
|
+
file: expanded_file,
|
|
106
|
+
line: line_num
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Reset pending comment after any non-comment, non-blank line
|
|
111
|
+
pending_comment = nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|