llm-docs-builder 0.12.0 → 1.0.0

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.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- llm-docs-builder (0.12.0)
4
+ llm-docs-builder (1.0.0)
5
5
  nokogiri (~> 1.17)
6
6
  zeitwerk (~> 2.6)
7
7
 
@@ -9,49 +9,31 @@ GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
11
  ast (2.4.3)
12
- byebug (12.0.0)
13
12
  coderay (1.1.3)
14
- date (3.5.0)
15
13
  diff-lcs (1.6.2)
16
- docile (1.4.1)
17
- erb (5.1.3)
18
- io-console (0.8.1)
19
- irb (1.15.3)
20
- pp (>= 0.6.0)
21
- rdoc (>= 4.0.0)
22
- reline (>= 0.4.2)
23
- json (2.16.0)
24
- language_server-protocol (3.17.0.5)
14
+ io-console (0.9.2)
15
+ json (2.21.2)
16
+ language_server-protocol (3.17.0.6)
25
17
  lint_roller (1.1.0)
26
18
  method_source (1.1.0)
27
- nokogiri (1.18.10-x86_64-linux-gnu)
19
+ mini_portile2 (2.8.9)
20
+ nokogiri (1.19.4)
21
+ mini_portile2 (~> 2.8.2)
28
22
  racc (~> 1.4)
29
- parallel (1.27.0)
30
- parser (3.3.10.0)
23
+ parallel (2.1.0)
24
+ parser (3.3.12.0)
31
25
  ast (~> 2.4.1)
32
26
  racc
33
- pp (0.6.3)
34
- prettyprint
35
- prettyprint (0.2.0)
36
- prism (1.6.0)
37
- pry (0.15.2)
27
+ prism (1.9.0)
28
+ pry (0.16.0)
38
29
  coderay (~> 1.1)
39
30
  method_source (~> 1.0)
40
- pry-byebug (3.11.0)
41
- byebug (~> 12.0)
42
- pry (>= 0.13, < 0.16)
43
- psych (5.2.6)
44
- date
45
- stringio
31
+ reline (>= 0.6.0)
46
32
  racc (1.8.1)
47
33
  rainbow (3.1.1)
48
- rake (13.3.1)
49
- rdoc (6.15.1)
50
- erb
51
- psych (>= 4.0.0)
52
- tsort
53
- regexp_parser (2.11.3)
54
- reline (0.6.3)
34
+ rake (13.4.2)
35
+ regexp_parser (2.12.0)
36
+ reline (0.7.0)
55
37
  io-console (~> 0.5)
56
38
  rspec (3.13.2)
57
39
  rspec-core (~> 3.13.0)
@@ -62,56 +44,49 @@ GEM
62
44
  rspec-expectations (3.13.5)
63
45
  diff-lcs (>= 1.2.0, < 2.0)
64
46
  rspec-support (~> 3.13.0)
65
- rspec-mocks (3.13.7)
47
+ rspec-mocks (3.13.8)
66
48
  diff-lcs (>= 1.2.0, < 2.0)
67
49
  rspec-support (~> 3.13.0)
68
- rspec-support (3.13.6)
69
- rubocop (1.81.7)
50
+ rspec-support (3.13.7)
51
+ rubocop (1.89.0)
70
52
  json (~> 2.3)
71
53
  language_server-protocol (~> 3.17.0.2)
72
54
  lint_roller (~> 1.1.0)
73
- parallel (~> 1.10)
55
+ parallel (>= 1.10)
74
56
  parser (>= 3.3.0.2)
75
57
  rainbow (>= 2.2.2, < 4.0)
76
58
  regexp_parser (>= 2.9.3, < 3.0)
77
- rubocop-ast (>= 1.47.1, < 2.0)
59
+ rubocop-ast (>= 1.49.0, < 2.0)
78
60
  ruby-progressbar (~> 1.7)
79
61
  unicode-display_width (>= 2.4.0, < 4.0)
80
- rubocop-ast (1.47.1)
62
+ rubocop-ast (1.50.0)
81
63
  parser (>= 3.3.7.2)
82
- prism (~> 1.4)
64
+ prism (~> 1.7)
83
65
  ruby-progressbar (1.13.0)
84
- simplecov (0.22.0)
85
- docile (~> 1.1)
86
- simplecov-html (~> 0.11)
87
- simplecov_json_formatter (~> 0.1)
88
- simplecov-html (0.13.2)
89
- simplecov_json_formatter (0.1.4)
90
- stringio (3.1.8)
91
- tsort (0.2.0)
66
+ simplecov (1.1.1)
92
67
  unicode-display_width (3.2.0)
93
68
  unicode-emoji (~> 4.1)
94
- unicode-emoji (4.1.0)
95
- yard (0.9.37)
96
- yard-lint (1.1.0)
97
- irb
69
+ unicode-emoji (4.2.0)
70
+ warning (1.6.0)
71
+ yard (0.9.45)
72
+ yard-lint (1.11.0)
98
73
  yard (~> 0.9)
99
74
  zeitwerk (~> 2.6)
100
- zeitwerk (2.7.3)
75
+ zeitwerk (2.8.3)
101
76
 
102
77
  PLATFORMS
103
- x86_64-linux
78
+ ruby
104
79
 
105
80
  DEPENDENCIES
106
- bundler (~> 2.0)
81
+ bundler
107
82
  llm-docs-builder!
108
83
  pry
109
- pry-byebug
110
84
  rake (~> 13.0)
111
85
  rspec (~> 3.0)
112
86
  rubocop (~> 1.0)
113
- simplecov (~> 0.21)
87
+ simplecov (~> 1.0)
88
+ warning
114
89
  yard-lint
115
90
 
116
91
  BUNDLED WITH
117
- 2.7.2
92
+ 4.0.17
@@ -129,8 +129,7 @@ module LlmDocsBuilder
129
129
 
130
130
  # Generate llms.txt from documentation directory or file
131
131
  #
132
- # Loads configuration, merges with CLI options, generates llms.txt content,
133
- # and optionally validates the output.
132
+ # Loads configuration, merges with CLI options, generates llms.txt content, and optionally validates the output.
134
133
  #
135
134
  # @param options [Hash] command options from parse_options
136
135
  # @option options [String] :config path to config file
@@ -399,7 +398,6 @@ module LlmDocsBuilder
399
398
  end
400
399
 
401
400
  # Display version information
402
- #
403
401
  def show_version
404
402
  puts "llm-docs-builder version #{LlmDocsBuilder::VERSION}"
405
403
  end
@@ -34,8 +34,7 @@ module LlmDocsBuilder
34
34
 
35
35
  # Generate llms.txt content from documentation
36
36
  #
37
- # Scans documentation files, extracts metadata, prioritizes them, and builds a formatted
38
- # llms.txt file.
37
+ # Scans documentation files, extracts metadata, prioritizes them, and builds a formatted llms.txt file.
39
38
  #
40
39
  # @return [String] generated llms.txt content
41
40
  def generate
@@ -14,8 +14,7 @@ module LlmDocsBuilder
14
14
  full_html_document?(content)
15
15
  end
16
16
 
17
- # Prepare a snippet of content for HTML detection by removing leading whitespace
18
- # and build metadata comments.
17
+ # Prepare a snippet of content for HTML detection by removing leading whitespace and build metadata comments.
19
18
  #
20
19
  # @param content [String]
21
20
  # @return [String, nil]
@@ -3,9 +3,8 @@
3
3
  module LlmDocsBuilder
4
4
  # Provides HTML to Markdown conversion functionality
5
5
  #
6
- # This module contains specialized renderers for converting HTML elements
7
- # to Markdown format, with support for complex structures like tables,
8
- # figures, and syntax-highlighted code blocks.
6
+ # This module contains specialized renderers for converting HTML elements to Markdown format, with support for complex
7
+ # structures like tables, figures, and syntax-highlighted code blocks.
9
8
  #
10
9
  # @api private
11
10
  module HtmlToMarkdown
@@ -120,7 +120,8 @@ module LlmDocsBuilder
120
120
  text = collapsed_inline_for(element)
121
121
  return '' if text.empty?
122
122
 
123
- "#{'#' * HEADING_LEVEL[tag]} #{text}"
123
+ effective_level = effective_heading_level(element, HEADING_LEVEL[tag])
124
+ "#{'#' * effective_level} #{text}"
124
125
  when 'blockquote'
125
126
  render_blockquote(element)
126
127
  when 'pre'
@@ -641,6 +642,37 @@ module LlmDocsBuilder
641
642
  out.join("\n\n")
642
643
  end
643
644
 
645
+ # Compute effective heading level adjusted for section nesting
646
+ #
647
+ # When HTML uses nested <section> elements with same-level headings, the inner headings should receive deeper
648
+ # markdown levels. The offset is calculated as the difference between the actual section ancestor count and the
649
+ # expected count for that heading tag (h1 expects 0 sections, h2 expects 1, etc.), capped at heading level 6.
650
+ #
651
+ # @param element [Nokogiri::XML::Element] heading element
652
+ # @param base_level [Integer] HTML heading level (1-6)
653
+ # @return [Integer] effective markdown heading level (1-6)
654
+ def effective_heading_level(element, base_level)
655
+ depth = section_ancestor_count(element)
656
+ offset = [depth - (base_level - 1), 0].max
657
+ [base_level + offset, 6].min
658
+ end
659
+
660
+ # Count the number of <section> ancestor elements
661
+ #
662
+ # @param element [Nokogiri::XML::Element]
663
+ # @return [Integer] number of section ancestors
664
+ def section_ancestor_count(element)
665
+ count = 0
666
+ node = element.parent
667
+
668
+ while node
669
+ count += 1 if node.element? && node.name.downcase == 'section'
670
+ node = node.parent
671
+ end
672
+
673
+ count
674
+ end
675
+
644
676
  # Helpers
645
677
 
646
678
  # Normalize whitespace in text
@@ -3,8 +3,7 @@
3
3
  module LlmDocsBuilder
4
4
  # Formats output for CLI display
5
5
  #
6
- # Provides formatting utilities for displaying comparison results,
7
- # byte sizes, and numbers in a user-friendly way.
6
+ # Provides formatting utilities for displaying comparison results, byte sizes, and numbers in a user-friendly way.
8
7
  #
9
8
  # @api private
10
9
  class OutputFormatter
@@ -30,7 +29,7 @@ module LlmDocsBuilder
30
29
 
31
30
  # Format number with comma separators for readability
32
31
  #
33
- # @param number [Integer] integer value
32
+ # @param number [Integer] value to format with comma separators
34
33
  # @return [String] formatted number with commas
35
34
  #
36
35
  # @example
@@ -3,9 +3,8 @@
3
3
  module LlmDocsBuilder
4
4
  # Parses llms.txt files into structured data
5
5
  #
6
- # Reads and parses llms.txt files according to the llms.txt specification,
7
- # extracting the title, description, and structured sections (Documentation,
8
- # Examples, Optional) with their links.
6
+ # Reads and parses llms.txt files according to the llms.txt specification, extracting the title, description, and
7
+ # structured sections (Documentation, Examples, Optional) with their links.
9
8
  #
10
9
  # @example Parse an llms.txt file
11
10
  # parser = LlmDocsBuilder::Parser.new('llms.txt')
@@ -109,8 +108,7 @@ module LlmDocsBuilder
109
108
 
110
109
  # Represents parsed llms.txt content with structured access to sections
111
110
  #
112
- # Provides convenient access to parsed llms.txt sections including title,
113
- # description, and link collections.
111
+ # Provides convenient access to parsed llms.txt sections including title, description, and link collections.
114
112
  #
115
113
  # @example Access parsed content
116
114
  # parsed.title # => "My Project"
@@ -3,9 +3,8 @@
3
3
  module LlmDocsBuilder
4
4
  # Advanced text compression techniques for reducing token count
5
5
  #
6
- # Provides more aggressive text compression methods including stopword removal,
7
- # duplicate content detection, and sentence deduplication. These methods are more
8
- # aggressive than basic markdown cleanup and should be used carefully.
6
+ # Provides more aggressive text compression methods including stopword removal, duplicate content detection, and
7
+ # sentence deduplication. These methods are more aggressive than basic markdown cleanup and should be used carefully.
9
8
  #
10
9
  # @example Basic usage
11
10
  # compressor = LlmDocsBuilder::TextCompressor.new
@@ -72,8 +71,8 @@ module LlmDocsBuilder
72
71
  # Removes common English stopwords that don't carry significant meaning.
73
72
  # Preserves code blocks, inline code, and technical terms.
74
73
  #
75
- # WARNING: This is an aggressive optimization that may affect readability.
76
- # Use with caution and test results carefully.
74
+ # @deprecated This is an aggressive optimization that may affect readability.
75
+ # Use with caution and test results carefully.
77
76
  #
78
77
  # @param content [String] text to process
79
78
  # @return [String] text with stopwords removed
@@ -3,9 +3,8 @@
3
3
  module LlmDocsBuilder
4
4
  # Estimates token count for text content using character-based approximation
5
5
  #
6
- # Provides token estimation without requiring external tokenizer dependencies.
7
- # Uses the common heuristic that ~4 characters equals 1 token for English text,
8
- # which works reasonably well for documentation and markdown content.
6
+ # Provides token estimation without requiring external tokenizer dependencies. Uses the common heuristic that ~4
7
+ # characters equals 1 token for English text, which works reasonably well for documentation and markdown content.
9
8
  #
10
9
  # @example Basic usage
11
10
  # estimator = LlmDocsBuilder::TokenEstimator.new
@@ -3,9 +3,8 @@
3
3
  module LlmDocsBuilder
4
4
  # Provides content transformation functionality
5
5
  #
6
- # This module contains specialized transformers for modifying markdown content,
7
- # including cleanup operations, link processing, heading normalization, and
8
- # content enhancement for AI consumption.
6
+ # This module contains specialized transformers for modifying markdown content, including cleanup operations, link
7
+ # processing, heading normalization, and content enhancement for AI consumption.
9
8
  #
10
9
  # @api private
11
10
  module Transformers
@@ -4,9 +4,8 @@ module LlmDocsBuilder
4
4
  module Transformers
5
5
  # Normalizes headings to include hierarchical context
6
6
  #
7
- # Transforms markdown headings to include parent context, making each section
8
- # self-contained for RAG systems. This is particularly useful when documents
9
- # are chunked and retrieved independently.
7
+ # Transforms markdown headings to include parent context, making each section self-contained for RAG systems. This
8
+ # is particularly useful when documents are chunked and retrieved independently.
10
9
  #
11
10
  # @example Basic heading normalization
12
11
  # # Configuration
@@ -51,24 +50,36 @@ module LlmDocsBuilder
51
50
  next line if in_code_block
52
51
 
53
52
  # Match markdown headings (1-6 hash symbols followed by space and text)
54
- heading_match = line.match(/^(#+)\s+(.+)$/)
53
+ # Supports optional ATX closing hashes (e.g., "## Title ##")
54
+ heading_match = line.match(/^(#+)\s+(.+?)(?:\s+#+)?\s*$/)
55
55
 
56
- if heading_match && heading_match[1].count('#').between?(1, 6)
57
- level = heading_match[1].count('#')
56
+ if heading_match && heading_match[1].length.between?(1, 6)
57
+ level = heading_match[1].length
58
58
  title = heading_match[2].strip
59
59
 
60
60
  # Update heading stack to current level
61
61
  heading_stack = heading_stack[0...level - 1]
62
- heading_stack << title
62
+
63
+ # Compute the effective heading level. When same-level headings
64
+ # are nested under a parent (e.g., two consecutive ## headings),
65
+ # the child must receive a deeper markdown level so the output
66
+ # hierarchy is correct.
67
+ effective_level = if heading_stack.empty?
68
+ level
69
+ else
70
+ [heading_stack.last[:effective_level] + 1, level].max
71
+ end
72
+ effective_level = [effective_level, 6].min
73
+
74
+ heading_stack << { title: title, effective_level: effective_level }
63
75
 
64
76
  # Build hierarchical heading
65
77
  if level == 1
66
78
  # H1 stays as-is (top level)
67
79
  line
68
80
  else
69
- # H2+ gets parent context
70
- hierarchical_title = heading_stack.join(separator)
71
- "#{'#' * level} #{hierarchical_title}\n"
81
+ hierarchical_title = heading_stack.map { |e| e[:title] }.join(separator)
82
+ "#{'#' * effective_level} #{hierarchical_title}\n"
72
83
  end
73
84
  else
74
85
  line
@@ -4,8 +4,7 @@ module LlmDocsBuilder
4
4
  module Transformers
5
5
  # Transformer for link-related operations
6
6
  #
7
- # Handles expansion of relative links to absolute URLs and
8
- # conversion of HTML URLs to markdown format.
7
+ # Handles expansion of relative links to absolute URLs and conversion of HTML URLs to markdown format.
9
8
  #
10
9
  # @api public
11
10
  class LinkTransformer
@@ -30,6 +29,10 @@ module LlmDocsBuilder
30
29
 
31
30
  private
32
31
 
32
+ # URI schemes that must never be treated as relative paths when expanding
33
+ # links with a base URL (kept in sync with the converter's safe schemes)
34
+ ABSOLUTE_URL_SCHEMES = %w[http https mailto ftp tel].freeze
35
+
33
36
  # Expand relative links to absolute URLs
34
37
  #
35
38
  # @param content [String] markdown content
@@ -40,7 +43,7 @@ module LlmDocsBuilder
40
43
  text = ::Regexp.last_match(1)
41
44
  url = ::Regexp.last_match(2)
42
45
 
43
- if url.start_with?('http://', 'https://', '//', '#')
46
+ if url.start_with?('http://', 'https://', '//', '#') || absolute_url_scheme?(url)
44
47
  match
45
48
  else
46
49
  clean_url = url.gsub(%r{^\./}, '')
@@ -50,6 +53,15 @@ module LlmDocsBuilder
50
53
  end
51
54
  end
52
55
 
56
+ # Check whether a link target uses a known absolute URI scheme
57
+ #
58
+ # @param url [String] link target
59
+ # @return [Boolean] true when the target already carries its own scheme
60
+ def absolute_url_scheme?(url)
61
+ scheme = url.split(':', 2).first.to_s
62
+ ABSOLUTE_URL_SCHEMES.include?(scheme)
63
+ end
64
+
53
65
  # Convert HTML URLs to markdown format
54
66
  #
55
67
  # @param content [String] markdown content
@@ -4,8 +4,7 @@ module LlmDocsBuilder
4
4
  module Transformers
5
5
  # Transformer for whitespace normalization
6
6
  #
7
- # Reduces excessive blank lines and trailing whitespace to make
8
- # content more compact for LLM consumption.
7
+ # Reduces excessive blank lines and trailing whitespace to make content more compact for LLM consumption.
9
8
  #
10
9
  # @api public
11
10
  class WhitespaceTransformer
@@ -56,8 +56,7 @@ module LlmDocsBuilder
56
56
 
57
57
  # Validate content and return result
58
58
  #
59
- # Runs all validation checks, populates {#errors} array, and returns whether
60
- # the content is valid.
59
+ # Runs all validation checks, populates {#errors} array, and returns whether the content is valid.
61
60
  #
62
61
  # @return [Boolean] true if content is valid, false otherwise
63
62
  def validate!
@@ -2,5 +2,5 @@
2
2
 
3
3
  module LlmDocsBuilder
4
4
  # Current version of the LlmDocsBuilder gem
5
- VERSION = '0.12.0'
5
+ VERSION = '1.0.0'
6
6
  end
@@ -13,9 +13,8 @@ loader.setup
13
13
 
14
14
  # Build and optimize documentation for LLMs
15
15
  #
16
- # This gem provides tools for generating llms.txt files and transforming markdown
17
- # documentation to be AI-friendly. It can reduce token consumption by 67-95% while
18
- # preserving essential documentation content.
16
+ # This gem provides tools for generating llms.txt files and transforming markdown documentation to be AI-friendly. It
17
+ # can reduce token consumption by 67-95% while preserving essential documentation content.
19
18
  #
20
19
  # @api public
21
20
  module LlmDocsBuilder
@@ -20,13 +20,14 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.homepage = 'https://github.com/mensfeld/llm-docs-builder'
22
22
  spec.license = 'MIT'
23
- spec.required_ruby_version = '>= 3.2'
23
+ spec.required_ruby_version = '>= 3.3'
24
24
 
25
25
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
26
26
  spec.metadata['homepage_uri'] = spec.homepage
27
27
  spec.metadata['source_code_uri'] = 'https://github.com/mensfeld/llm-docs-builder'
28
28
  spec.metadata['changelog_uri'] = 'https://github.com/mensfeld/llm-docs-builder/blob/master/CHANGELOG.md'
29
29
  spec.metadata['documentation_uri'] = 'https://github.com/mensfeld/llm-docs-builder'
30
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/mensfeld/llm-docs-builder/issues'
30
31
  spec.metadata['rubygems_mfa_required'] = 'true'
31
32
 
32
33
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|test)/}) }
@@ -38,9 +39,9 @@ Gem::Specification.new do |spec|
38
39
  spec.add_dependency 'nokogiri', '~> 1.17'
39
40
  spec.add_dependency 'zeitwerk', '~> 2.6'
40
41
 
41
- spec.add_development_dependency 'bundler', '~> 2.0'
42
+ spec.add_development_dependency 'bundler'
42
43
  spec.add_development_dependency 'rake', '~> 13.0'
43
44
  spec.add_development_dependency 'rspec', '~> 3.0'
44
45
  spec.add_development_dependency 'rubocop', '~> 1.0'
45
- spec.add_development_dependency 'simplecov', '~> 0.21'
46
+ spec.add_development_dependency 'simplecov', '~> 1.0'
46
47
  end