llm-docs-builder 0.13.0 → 1.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +13 -7
- data/.github/workflows/docker.yml +7 -7
- data/.github/workflows/push.yml +3 -3
- data/.ruby-version +1 -1
- data/.yard-lint.yml +35 -1
- data/CHANGELOG.md +36 -4
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -28
- data/README.md +23 -11
- data/lib/llm_docs_builder/bulk_transformer.rb +8 -3
- data/lib/llm_docs_builder/cli.rb +1 -2
- data/lib/llm_docs_builder/generator.rb +75 -7
- data/lib/llm_docs_builder/html_detector.rb +1 -2
- data/lib/llm_docs_builder/html_to_markdown/figure_code_block_renderer.rb +2 -3
- data/lib/llm_docs_builder/html_to_markdown_converter.rb +3 -5
- data/lib/llm_docs_builder/output_formatter.rb +2 -3
- data/lib/llm_docs_builder/parser.rb +3 -5
- data/lib/llm_docs_builder/text_compressor.rb +2 -3
- data/lib/llm_docs_builder/token_estimator.rb +2 -3
- data/lib/llm_docs_builder/transformers/base_transformer.rb +2 -3
- data/lib/llm_docs_builder/transformers/heading_transformer.rb +2 -3
- data/lib/llm_docs_builder/transformers/link_transformer.rb +325 -25
- data/lib/llm_docs_builder/transformers/whitespace_transformer.rb +1 -2
- data/lib/llm_docs_builder/validator.rb +1 -2
- data/lib/llm_docs_builder/version.rb +1 -1
- data/lib/llm_docs_builder.rb +3 -3
- data/llm-docs-builder.gemspec +2 -1
- data/llm-docs-builder.yml.example +2 -2
- data/package-lock.json +9 -9
- data/renovate.json +13 -2
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9ce6fbe87a426d69139719ecd76489c5aed030517167758ea0b2df452f3d636
|
|
4
|
+
data.tar.gz: 9ce3bfab8d25b4c3a9f11b899f20038da89463eb82ffde1b69106ce4934377c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f98c868bfde76f265dd69854a43da9c67b48f5310234611105a6dec00a62236070461484b52c8e08c85cf9fd7bfcbffea0a85e92ce0943ac879b9dc532d3b77
|
|
7
|
+
data.tar.gz: 6a6340a373df54139338af837a7755b3148fdaef4d58810779cd88b5e2c5f1cca6edda231cee9e3cdace9cc60d465b14e0d78ecf41c3e338fb9fc40029d3f4c0
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -19,10 +19,12 @@ jobs:
|
|
|
19
19
|
specs:
|
|
20
20
|
timeout-minutes: 15
|
|
21
21
|
runs-on: ubuntu-latest
|
|
22
|
+
continue-on-error: ${{ contains(matrix.ruby, 'preview') || contains(matrix.ruby, 'rc') }}
|
|
22
23
|
strategy:
|
|
23
24
|
fail-fast: false
|
|
24
25
|
matrix:
|
|
25
26
|
ruby:
|
|
27
|
+
- '4.1.0-preview1'
|
|
26
28
|
- '4.0'
|
|
27
29
|
- '3.4'
|
|
28
30
|
- '3.3'
|
|
@@ -30,12 +32,16 @@ jobs:
|
|
|
30
32
|
- ruby: '4.0'
|
|
31
33
|
coverage: 'true'
|
|
32
34
|
steps:
|
|
33
|
-
- uses: actions/checkout@
|
|
35
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
34
36
|
with:
|
|
35
37
|
fetch-depth: 0
|
|
36
38
|
|
|
39
|
+
- name: Remove Gemfile.lock for Ruby preview versions
|
|
40
|
+
if: contains(matrix.ruby, 'preview') || contains(matrix.ruby, 'rc')
|
|
41
|
+
run: rm -f Gemfile.lock
|
|
42
|
+
|
|
37
43
|
- name: Set up Ruby
|
|
38
|
-
uses: ruby/setup-ruby@
|
|
44
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
39
45
|
with:
|
|
40
46
|
ruby-version: ${{ matrix.ruby }}
|
|
41
47
|
bundler-cache: true
|
|
@@ -58,11 +64,11 @@ jobs:
|
|
|
58
64
|
timeout-minutes: 5
|
|
59
65
|
runs-on: ubuntu-latest
|
|
60
66
|
steps:
|
|
61
|
-
- uses: actions/checkout@
|
|
67
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
62
68
|
- name: Set up Ruby
|
|
63
|
-
uses: ruby/setup-ruby@
|
|
69
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
64
70
|
with:
|
|
65
|
-
ruby-version: '4.0.
|
|
71
|
+
ruby-version: '4.0.9'
|
|
66
72
|
bundler-cache: true
|
|
67
73
|
- name: Run yard-lint
|
|
68
74
|
run: bundle exec yard-lint lib/
|
|
@@ -71,11 +77,11 @@ jobs:
|
|
|
71
77
|
timeout-minutes: 5
|
|
72
78
|
runs-on: ubuntu-latest
|
|
73
79
|
steps:
|
|
74
|
-
- uses: actions/checkout@
|
|
80
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
75
81
|
with:
|
|
76
82
|
fetch-depth: 0
|
|
77
83
|
- name: Set up Node.js
|
|
78
|
-
uses: actions/setup-node@
|
|
84
|
+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
79
85
|
with:
|
|
80
86
|
node-version: '20'
|
|
81
87
|
cache: 'npm'
|
|
@@ -25,13 +25,13 @@ jobs:
|
|
|
25
25
|
id-token: write
|
|
26
26
|
steps:
|
|
27
27
|
- name: Checkout
|
|
28
|
-
uses: actions/checkout@
|
|
28
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
29
29
|
with:
|
|
30
30
|
fetch-depth: 0
|
|
31
31
|
|
|
32
32
|
- name: Docker meta
|
|
33
33
|
id: meta
|
|
34
|
-
uses: docker/metadata-action@
|
|
34
|
+
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
|
35
35
|
with:
|
|
36
36
|
images: |
|
|
37
37
|
mensfeld/llm-docs-builder
|
|
@@ -45,28 +45,28 @@ jobs:
|
|
|
45
45
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
46
46
|
|
|
47
47
|
- name: Set up QEMU
|
|
48
|
-
uses: docker/setup-qemu-action@
|
|
48
|
+
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
|
|
49
49
|
|
|
50
50
|
- name: Set up Docker Buildx
|
|
51
|
-
uses: docker/setup-buildx-action@
|
|
51
|
+
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
|
52
52
|
|
|
53
53
|
- name: Login to Docker Hub
|
|
54
54
|
if: github.event_name != 'pull_request'
|
|
55
|
-
uses: docker/login-action@
|
|
55
|
+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
|
56
56
|
with:
|
|
57
57
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
58
58
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
59
59
|
|
|
60
60
|
- name: Login to GitHub Container Registry
|
|
61
61
|
if: github.event_name != 'pull_request'
|
|
62
|
-
uses: docker/login-action@
|
|
62
|
+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
|
63
63
|
with:
|
|
64
64
|
registry: ghcr.io
|
|
65
65
|
username: ${{ github.actor }}
|
|
66
66
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
67
67
|
|
|
68
68
|
- name: Build and push
|
|
69
|
-
uses: docker/build-push-action@
|
|
69
|
+
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
|
70
70
|
with:
|
|
71
71
|
context: .
|
|
72
72
|
platforms: linux/amd64,linux/arm64
|
data/.github/workflows/push.yml
CHANGED
|
@@ -19,12 +19,12 @@ jobs:
|
|
|
19
19
|
id-token: write
|
|
20
20
|
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
23
23
|
with:
|
|
24
24
|
fetch-depth: 0
|
|
25
25
|
|
|
26
26
|
- name: Set up Ruby
|
|
27
|
-
uses: ruby/setup-ruby@
|
|
27
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
28
28
|
with:
|
|
29
29
|
bundler-cache: false
|
|
30
30
|
|
|
@@ -32,4 +32,4 @@ jobs:
|
|
|
32
32
|
run: |
|
|
33
33
|
bundle install --jobs 4 --retry 3
|
|
34
34
|
|
|
35
|
-
- uses: rubygems/release-gem@
|
|
35
|
+
- uses: rubygems/release-gem@7f9650160c1a4e7989fdc9855807bdbd421d8b6b # v1.4.1
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.9
|
data/.yard-lint.yml
CHANGED
|
@@ -62,6 +62,34 @@ Documentation/BlankLineBeforeDefinition:
|
|
|
62
62
|
SingleBlankLine: true
|
|
63
63
|
OrphanedDocs: true
|
|
64
64
|
|
|
65
|
+
Documentation/DuplicateNamespaceComment:
|
|
66
|
+
Description: Detects namespaces documented with a YARD comment in more than one file.
|
|
67
|
+
Enabled: true
|
|
68
|
+
Severity: error
|
|
69
|
+
|
|
70
|
+
Documentation/UnderfilledLines:
|
|
71
|
+
Description: Detects documentation prose that wraps too early and wastes horizontal space.
|
|
72
|
+
Enabled: true
|
|
73
|
+
Severity: error
|
|
74
|
+
# Aligned with RuboCop's Layout/LineLength so documentation prose wraps to the same
|
|
75
|
+
# width as code.
|
|
76
|
+
MaxLength: 120
|
|
77
|
+
|
|
78
|
+
Documentation/LineLength:
|
|
79
|
+
Description: Detects documentation lines that exceed the maximum length.
|
|
80
|
+
Enabled: true
|
|
81
|
+
Severity: error
|
|
82
|
+
# Aligned with RuboCop's Layout/LineLength.
|
|
83
|
+
MaxLength: 120
|
|
84
|
+
|
|
85
|
+
Documentation/TextSubstitution:
|
|
86
|
+
Description: Detects em/en-dashes in documentation and replaces them with hyphens.
|
|
87
|
+
Enabled: true
|
|
88
|
+
Severity: error
|
|
89
|
+
Substitutions:
|
|
90
|
+
"—": "-" # em-dash (U+2014)
|
|
91
|
+
"–": "-" # en-dash (U+2013)
|
|
92
|
+
|
|
65
93
|
# Tags validators
|
|
66
94
|
Tags/Order:
|
|
67
95
|
Description: Enforces consistent ordering of YARD tags.
|
|
@@ -70,9 +98,15 @@ Tags/Order:
|
|
|
70
98
|
EnforcedOrder:
|
|
71
99
|
- param
|
|
72
100
|
- option
|
|
101
|
+
- yield
|
|
102
|
+
- yieldparam
|
|
103
|
+
- yieldreturn
|
|
73
104
|
- return
|
|
74
105
|
- raise
|
|
106
|
+
- see
|
|
75
107
|
- example
|
|
108
|
+
- note
|
|
109
|
+
- todo
|
|
76
110
|
|
|
77
111
|
Tags/InvalidTypes:
|
|
78
112
|
Description: Validates type definitions in @param, @return, @option tags.
|
|
@@ -142,7 +176,7 @@ Tags/OptionTags:
|
|
|
142
176
|
Tags/ExampleSyntax:
|
|
143
177
|
Description: Validates Ruby syntax in @example tags.
|
|
144
178
|
Enabled: true
|
|
145
|
-
Severity:
|
|
179
|
+
Severity: error
|
|
146
180
|
|
|
147
181
|
Tags/RedundantParamDescription:
|
|
148
182
|
Description: Detects meaningless parameter descriptions that add no value.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0 (2026-11-02)
|
|
4
|
+
- [Enhancement] **Relative Link Resolution** - `base_url` link expansion now resolves relative links like a browser would.
|
|
5
|
+
- `../` and `./` segments are normalized (`../other.md` → `https://example.com/other.md` instead of `https://example.com/docs/../other.md`).
|
|
6
|
+
- `bulk-transform` resolves each link from its file's own directory, so `./setup.md` in `guides/intro.md` expands to `<base_url>/guides/setup.md`.
|
|
7
|
+
- Links starting with `/` resolve from `base_url`; anchors, query strings, absolute URLs and `mailto:`/`tel:`/`ftp:` links are unchanged.
|
|
8
|
+
- [Fix] **llms.txt Paths Are Percent-Encoded** - `generate` now percent-encodes each path segment in `llms.txt` entries, with and without `base_url`.
|
|
9
|
+
- `docs/my docs/a b.md` gives `my%20docs/a%20b.md`, so names with spaces or parentheses no longer break the Markdown link.
|
|
10
|
+
- Non-ASCII names are encoded as UTF-8 (`zażółć.md` → `za%C5%BC%C3%B3%C5%82%C4%87.md`).
|
|
11
|
+
- [Fix] **llms.txt Descriptions** - A page description is now always a single line, and the 200-character limit no longer cuts a Markdown link or image in half; a link or image that does not fit is left out.
|
|
12
|
+
- [Fix] **Transformed Files Picked Up as Sources** - `bulk-transform` and `generate` now skip files ending with the transform suffix (`*.llm.md` by default, or `*<suffix>.md`).
|
|
13
|
+
- A second `bulk-transform` run no longer writes `*.llm.llm.md` files.
|
|
14
|
+
- `generate` lists each page once, from its source file, instead of listing both `README.md` and `README.llm.md`.
|
|
15
|
+
- Source files whose names end with the suffix (e.g. `notes.llm.md`) are skipped too. An empty suffix skips nothing.
|
|
16
|
+
- [Fix] **Angle-Bracket Link Destinations** - `base_url` link expansion now resolves CommonMark `[text](<path with spaces>)` links like other relative links: the brackets are dropped and spaces are percent-encoded (`<../my docs/a b.md>` → `<base_url>/my%20docs/a%20b.md`) instead of producing a broken URL with a stray `>`.
|
|
17
|
+
- [Fix] **Query-Only Links** - `base_url` link expansion now resolves a query-only link like `[q](?tab=2)` to the current page (`guides/sub/page.md` gives `<base_url>/guides/sub/page.md?tab=2`, as RFC 3986 does) instead of the page's directory (`<base_url>/guides/sub/?tab=2`). Anchor-only links like `#self` are unchanged.
|
|
18
|
+
- [Fix] **`simplify_links` Strips Only One Prefix** - repeated filler prefixes are now all removed, so `Click here to see the docs` becomes `docs` (as documented) instead of `see the`.
|
|
19
|
+
- [Fix] **Links Inside Code Are Rewritten** - link transformations (`base_url` expansion, `convert_urls`, `simplify_links`) no longer change links in fenced code blocks, indented code blocks and inline code spans.
|
|
20
|
+
- [Fix] **`convert_urls` Skips Anchors, Queries and Relative Links** - `.html`/`.htm` URLs followed by an anchor or a query are now converted (`doc.html#sec` → `doc.md#sec`, `doc.html?x=1` → `doc.md?x=1`).
|
|
21
|
+
- Relative link targets are converted too, also without `base_url` (`[Intro](../intro.html)` → `[Intro](../intro.md)`).
|
|
22
|
+
- `.html` inside a query or an anchor and targets with a non-HTTP scheme (`mailto:`, `ftp:`, `sftp:`, `file:`, ...) are left unchanged.
|
|
23
|
+
- [Fix] **Links With Brackets in Their Text** - `base_url` link expansion now expands links whose text holds brackets, nested up to two levels deep (`[a [b] c](../intro.md)`), and linked images like `[](../intro.md)`, where both the image and the outer link are now expanded. Before, such links stayed relative, and a linked image got only its image expanded.
|
|
24
|
+
- [Fix] **Links With Any URI Scheme** - `base_url` link expansion now leaves every link with a URI scheme unchanged, in any case (`HTTPS://`, `Mailto:`, `sftp:`, `ssh:`, `git:`, `irc:`, `urn:`, `data:`, `javascript:`, ...). Before, only lowercase `http`, `https`, `mailto`, `ftp` and `tel` links were kept, and others became broken URLs like `<base_url>/guides/sftp:/host/x`.
|
|
25
|
+
- [Fix] **Link Reference Definitions** - `base_url` link expansion now expands reference-style link definitions (`[id]: ../intro.md "Title"`) like inline links, so `[text][id]`, `[id][]` and `[id]` links no longer stay relative while inline links in the same file are expanded. Titles are kept; definitions with a URI scheme, anchors, footnotes (`[^1]: ...`) and definitions in code are unchanged.
|
|
26
|
+
- `convert_urls` converts `.html`/`.htm` definition targets too (`[id]: ../intro.html` → `[id]: ../intro.md`).
|
|
27
|
+
- [Fix] **`compare` Web Server Link** - The "Configure your web server" link in the `compare` output now points at the README section "Serving Optimized Docs to AI Bots" (`#serving-optimized-docs-to-ai-bots`) instead of a missing `#serving-optimized-docs` anchor.
|
|
28
|
+
|
|
29
|
+
## 1.0.0 (2026-08-24)
|
|
30
|
+
- [Fix] **Link Expansion Corrupts Non-HTTP URI Schemes** - `LinkTransformer#expand_relative_links` no longer rewrites `mailto:`, `tel:`, and `ftp:` links into dead `https://...` URLs when `base_url` is set (#165).
|
|
31
|
+
- Any link whose scheme is in the known absolute-scheme list (`http`, `https`, `mailto`, `ftp`, `tel`) is now left untouched, matching `HtmlToMarkdownConverter`'s `SAFE_URI_SCHEMES`.
|
|
32
|
+
- Relative links (e.g. `./guide.md`) continue to expand as before.
|
|
33
|
+
- [Enhancement] Add Ruby warning category opt-in to test helpers
|
|
34
|
+
|
|
3
35
|
## 0.13.0 (2026-05-11)
|
|
4
36
|
- [Breaking] Dropped Ruby 3.2 support. Minimum required Ruby version is now 3.3.
|
|
5
|
-
- [Fix] **Heading Nesting in Normalize Mode**
|
|
37
|
+
- [Fix] **Heading Nesting in Normalize Mode** - Fixed `normalize_headings` producing incorrect heading levels when same-level headings are nested under a parent.
|
|
6
38
|
- Consecutive same-level headings (e.g., two `##` headings) now correctly adjust the child's markdown level to be deeper than the parent's (e.g., `##` → `###`)
|
|
7
39
|
- Effective level formula: `max(parent_effective_level + 1, original_level)`
|
|
8
40
|
- ATX closing hashes (`## Title ##`) are now stripped from titles before building hierarchical paths
|
|
9
|
-
- [Fix] **HTML-to-Markdown Section Nesting**
|
|
41
|
+
- [Fix] **HTML-to-Markdown Section Nesting** - Headings inside nested `<section>` elements now receive adjusted markdown levels based on section nesting depth.
|
|
10
42
|
- `<h2>` inside a nested `<section>` under another `<section>` with its own `<h2>` is now correctly rendered as `###` instead of `##`
|
|
11
43
|
|
|
12
44
|
## 0.12.0 (2025-11-12)
|
|
13
|
-
- [Feature] **HTML to Markdown Reverse Converter**
|
|
45
|
+
- [Feature] **HTML to Markdown Reverse Converter** - Added support for converting HTML content to markdown format.
|
|
14
46
|
- Enables processing of HTML documentation sources
|
|
15
47
|
- Integrates seamlessly with the transformer pipeline
|
|
16
48
|
- Useful for converting web-based docs to markdown for further processing
|
|
17
49
|
- By @Eric-Guo in PR #32.
|
|
18
50
|
|
|
19
51
|
## 0.11.0 (2025-11-03)
|
|
20
|
-
- [Feature] **Transform from URL**
|
|
52
|
+
- [Feature] **Transform from URL** - The `transform` command now accepts a remote URL via `--url` and processes fetched content through the standard transformer pipeline.
|
|
21
53
|
- Example: `llm-docs-builder transform --url https://example.com/docs/page.html`
|
|
22
54
|
- Applies all configured transformations and output options identically to local files
|
|
23
55
|
- By @Eric-Guo and @codex in PR #28.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
llm-docs-builder (
|
|
4
|
+
llm-docs-builder (1.1.0)
|
|
5
5
|
nokogiri (~> 1.17)
|
|
6
6
|
zeitwerk (~> 2.6)
|
|
7
7
|
|
|
@@ -11,30 +11,29 @@ GEM
|
|
|
11
11
|
ast (2.4.3)
|
|
12
12
|
coderay (1.1.3)
|
|
13
13
|
diff-lcs (1.6.2)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
language_server-protocol (3.17.0.5)
|
|
14
|
+
io-console (0.9.4)
|
|
15
|
+
json (3.0.2)
|
|
16
|
+
language_server-protocol (3.17.0.6)
|
|
18
17
|
lint_roller (1.1.0)
|
|
19
18
|
method_source (1.1.0)
|
|
20
19
|
mini_portile2 (2.8.9)
|
|
21
|
-
nokogiri (1.19.
|
|
20
|
+
nokogiri (1.19.4)
|
|
22
21
|
mini_portile2 (~> 2.8.2)
|
|
23
22
|
racc (~> 1.4)
|
|
24
|
-
parallel (2.
|
|
25
|
-
parser (3.3.
|
|
23
|
+
parallel (2.2.0)
|
|
24
|
+
parser (3.3.12.0)
|
|
26
25
|
ast (~> 2.4.1)
|
|
27
26
|
racc
|
|
28
27
|
prism (1.9.0)
|
|
29
|
-
pry (0.16.
|
|
28
|
+
pry (0.16.1)
|
|
30
29
|
coderay (~> 1.1)
|
|
31
30
|
method_source (~> 1.0)
|
|
32
31
|
reline (>= 0.6.0)
|
|
33
32
|
racc (1.8.1)
|
|
34
33
|
rainbow (3.1.1)
|
|
35
34
|
rake (13.4.2)
|
|
36
|
-
regexp_parser (2.
|
|
37
|
-
reline (0.
|
|
35
|
+
regexp_parser (2.13.1)
|
|
36
|
+
reline (0.7.0)
|
|
38
37
|
io-console (~> 0.5)
|
|
39
38
|
rspec (3.13.2)
|
|
40
39
|
rspec-core (~> 3.13.0)
|
|
@@ -49,8 +48,8 @@ GEM
|
|
|
49
48
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
50
49
|
rspec-support (~> 3.13.0)
|
|
51
50
|
rspec-support (3.13.7)
|
|
52
|
-
rubocop (1.
|
|
53
|
-
json (
|
|
51
|
+
rubocop (1.91.0)
|
|
52
|
+
json (>= 2.3)
|
|
54
53
|
language_server-protocol (~> 3.17.0.2)
|
|
55
54
|
lint_roller (~> 1.1.0)
|
|
56
55
|
parallel (>= 1.10)
|
|
@@ -60,24 +59,20 @@ GEM
|
|
|
60
59
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
61
60
|
ruby-progressbar (~> 1.7)
|
|
62
61
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
63
|
-
rubocop-ast (1.
|
|
62
|
+
rubocop-ast (1.50.0)
|
|
64
63
|
parser (>= 3.3.7.2)
|
|
65
64
|
prism (~> 1.7)
|
|
66
65
|
ruby-progressbar (1.13.0)
|
|
67
|
-
simplecov (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
unicode-emoji (~> 4.1)
|
|
75
|
-
unicode-emoji (4.2.0)
|
|
76
|
-
yard (0.9.43)
|
|
77
|
-
yard-lint (1.5.1)
|
|
66
|
+
simplecov (1.3.2)
|
|
67
|
+
unicode-display_width (3.3.0)
|
|
68
|
+
unicode-emoji (~> 4.3)
|
|
69
|
+
unicode-emoji (4.3.0)
|
|
70
|
+
warning (1.6.0)
|
|
71
|
+
yard (0.9.45)
|
|
72
|
+
yard-lint (1.13.0)
|
|
78
73
|
yard (~> 0.9)
|
|
79
74
|
zeitwerk (~> 2.6)
|
|
80
|
-
zeitwerk (2.
|
|
75
|
+
zeitwerk (2.8.3)
|
|
81
76
|
|
|
82
77
|
PLATFORMS
|
|
83
78
|
ruby
|
|
@@ -89,8 +84,9 @@ DEPENDENCIES
|
|
|
89
84
|
rake (~> 13.0)
|
|
90
85
|
rspec (~> 3.0)
|
|
91
86
|
rubocop (~> 1.0)
|
|
92
|
-
simplecov (~> 0
|
|
87
|
+
simplecov (~> 1.0)
|
|
88
|
+
warning
|
|
93
89
|
yard-lint
|
|
94
90
|
|
|
95
91
|
BUNDLED WITH
|
|
96
|
-
|
|
92
|
+
4.0.17
|
data/README.md
CHANGED
|
@@ -125,7 +125,7 @@ llm-docs-builder generate --config llm-docs-builder.yml
|
|
|
125
125
|
```yaml
|
|
126
126
|
# llm-docs-builder.yml
|
|
127
127
|
docs: ./docs
|
|
128
|
-
base_url: https://myproject.io
|
|
128
|
+
base_url: https://myproject.io # Base URL for expanding relative links
|
|
129
129
|
title: My Project
|
|
130
130
|
description: Brief description
|
|
131
131
|
body: Optional body content between description and sections
|
|
@@ -165,6 +165,12 @@ excludes:
|
|
|
165
165
|
- "**/drafts/**"
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
**Link expansion (`base_url`):** relative links become absolute URLs.
|
|
169
|
+
- Inline links, images (also linked images, `[](../intro.md)`) and reference-style definitions (`[a]: ../intro.md`) are expanded. `<path with spaces>` destinations are resolved and their spaces encoded as `%20`.
|
|
170
|
+
- `bulk-transform` resolves links from each file's directory under `docs`. Single-file `transform` (and `LlmDocsBuilder.transform_markdown`) resolves them from `base_url`: with `base_url: https://myproject.io/docs`, `../intro.md` becomes `https://myproject.io/intro.md`.
|
|
171
|
+
- Links starting with `/` resolve from `base_url`. In `bulk-transform`, a query-only link (`[q](?tab=2)`) resolves to the current page (`.../page.md?tab=2`).
|
|
172
|
+
- Left unchanged: links in fenced code, indented code and inline code; anchors (`#x`); protocol-relative URLs (`//cdn.io/a.md`); URLs with any scheme, in any case (`HTTPS:`, `sftp:`, `urn:`, `mailto:`); HTML `<a href>` and `<img src>`.
|
|
173
|
+
|
|
168
174
|
## CLI Commands
|
|
169
175
|
|
|
170
176
|
```bash
|
|
@@ -183,31 +189,34 @@ llm-docs-builder version # Show version
|
|
|
183
189
|
-d, --docs PATH Documentation path
|
|
184
190
|
-o, --output PATH Output file
|
|
185
191
|
-u, --url URL URL for comparison
|
|
192
|
+
-f, --file PATH Local markdown file for comparison
|
|
186
193
|
-v, --verbose Detailed output
|
|
187
194
|
```
|
|
188
195
|
|
|
189
196
|
## Serving Optimized Docs to AI Bots
|
|
190
197
|
|
|
191
|
-
After using `bulk-transform` with `suffix: .llm`, configure your web server to serve optimized versions to AI bots:
|
|
198
|
+
After using `bulk-transform` with `suffix: .llm`, configure your web server to serve optimized versions to AI bots. Pages without an `.llm.md` version are served as is:
|
|
192
199
|
|
|
193
200
|
**Apache (.htaccess):**
|
|
194
201
|
```apache
|
|
195
202
|
SetEnvIf User-Agent "(?i)(openai|anthropic|claude|gpt)" IS_LLM_BOT
|
|
203
|
+
RewriteEngine On
|
|
196
204
|
RewriteCond %{ENV:IS_LLM_BOT} !^$
|
|
205
|
+
RewriteCond %{REQUEST_URI} !\.llm\.md$
|
|
206
|
+
RewriteCond %{REQUEST_FILENAME} ^(.+)\.md$
|
|
207
|
+
RewriteCond %1.llm.md -f
|
|
197
208
|
RewriteRule ^(.*)\.md$ $1.llm.md [L]
|
|
198
209
|
```
|
|
199
210
|
|
|
200
211
|
**Nginx:**
|
|
201
212
|
```nginx
|
|
202
|
-
map $http_user_agent $
|
|
203
|
-
default
|
|
204
|
-
"~*(
|
|
213
|
+
map $http_user_agent $llm_suffix {
|
|
214
|
+
default "";
|
|
215
|
+
"~*(openai|anthropic|claude|gpt)" ".llm";
|
|
205
216
|
}
|
|
206
217
|
|
|
207
|
-
location ~ ^/docs/(
|
|
208
|
-
|
|
209
|
-
rewrite ^(.*)\.md$ $1.llm.md last;
|
|
210
|
-
}
|
|
218
|
+
location ~ ^/docs/(?<doc>.*)\.md$ {
|
|
219
|
+
try_files /docs/$doc$llm_suffix.md $uri =404;
|
|
211
220
|
}
|
|
212
221
|
```
|
|
213
222
|
|
|
@@ -258,7 +267,10 @@ api = API.new
|
|
|
258
267
|
```markdown
|
|
259
268
|
# API Documentation
|
|
260
269
|
|
|
261
|
-
|
|
270
|
+
|
|
271
|
+
> Important: This is a note
|
|
272
|
+
|
|
273
|
+
[Click here to see the complete API documentation](./api.md)
|
|
262
274
|
|
|
263
275
|
api = API.new
|
|
264
276
|
```
|
|
@@ -411,7 +423,7 @@ All compression features can be used individually for fine-grained control:
|
|
|
411
423
|
- `generate_toc: true` - Generate table of contents from headings with anchor links
|
|
412
424
|
- `custom_instruction: "text"` - Inject AI context message at document top
|
|
413
425
|
- `simplify_links: true` - Simplify verbose link text (e.g., "Click here to see the docs" → "docs")
|
|
414
|
-
- `convert_urls: true` - Convert `.html`/`.htm` URLs to `.md` format
|
|
426
|
+
- `convert_urls: true` - Convert `.html`/`.htm` URLs to `.md` format, keeping anchors and queries (`../intro.html#x` → `../intro.md#x`): absolute `http(s)` URLs anywhere in the text, and relative link and reference-style definition targets. URLs in code are unchanged
|
|
415
427
|
- `normalize_whitespace: true` - Reduce excessive blank lines and remove trailing whitespace
|
|
416
428
|
|
|
417
429
|
## License
|
|
@@ -76,16 +76,19 @@ module LlmDocsBuilder
|
|
|
76
76
|
|
|
77
77
|
# Recursively scans the docs directory for markdown files
|
|
78
78
|
#
|
|
79
|
-
# Skips hidden files (starting with dot) and
|
|
79
|
+
# Skips hidden files (starting with dot) and already transformed files (ending with
|
|
80
|
+
# the suffix, e.g. README.llm.md) and returns sorted array of paths
|
|
80
81
|
#
|
|
81
82
|
# @return [Array<String>] paths to markdown files
|
|
82
83
|
def find_markdown_files
|
|
83
84
|
files = []
|
|
85
|
+
output_ending = "#{options[:suffix]}.md".downcase
|
|
84
86
|
|
|
85
87
|
Find.find(docs_path) do |path|
|
|
86
88
|
next unless File.file?(path)
|
|
87
89
|
next unless path.match?(/\.md$/i)
|
|
88
90
|
next if File.basename(path).start_with?('.')
|
|
91
|
+
next if output_ending != '.md' && path.downcase.end_with?(output_ending)
|
|
89
92
|
|
|
90
93
|
files << path
|
|
91
94
|
end
|
|
@@ -109,12 +112,14 @@ module LlmDocsBuilder
|
|
|
109
112
|
|
|
110
113
|
# Applies markdown transformations to a single file
|
|
111
114
|
#
|
|
112
|
-
# Creates MarkdownTransformer instance and delegates transformation
|
|
115
|
+
# Creates MarkdownTransformer instance and delegates transformation. Passes the file's
|
|
116
|
+
# path relative to the docs directory so relative links resolve from its own location.
|
|
113
117
|
#
|
|
114
118
|
# @param file_path [String] path to markdown file
|
|
115
119
|
# @return [String] transformed content
|
|
116
120
|
def transform_file(file_path)
|
|
117
|
-
|
|
121
|
+
source_path = Pathname.new(file_path).relative_path_from(Pathname.new(docs_path)).to_s
|
|
122
|
+
transformer = MarkdownTransformer.new(file_path, options.merge(source_path: source_path))
|
|
118
123
|
transformer.transform
|
|
119
124
|
end
|
|
120
125
|
|
data/lib/llm_docs_builder/cli.rb
CHANGED
|
@@ -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
|
|
@@ -12,6 +12,10 @@ module LlmDocsBuilder
|
|
|
12
12
|
#
|
|
13
13
|
# @api public
|
|
14
14
|
class Generator
|
|
15
|
+
# Maximum length of a file description in llms.txt, in characters
|
|
16
|
+
MAX_DESCRIPTION_LENGTH = 200
|
|
17
|
+
private_constant :MAX_DESCRIPTION_LENGTH
|
|
18
|
+
|
|
15
19
|
# @return [String] path to documentation directory or file
|
|
16
20
|
attr_reader :docs_path
|
|
17
21
|
|
|
@@ -27,6 +31,7 @@ module LlmDocsBuilder
|
|
|
27
31
|
# @option options [String] :description project description (overrides auto-detection)
|
|
28
32
|
# @option options [String] :output output file path for saving
|
|
29
33
|
# @option options [Boolean] :verbose enable verbose output
|
|
34
|
+
# @option options [String] :suffix suffix of bulk-transformed files to skip (default: '.llm')
|
|
30
35
|
def initialize(docs_path, options = {})
|
|
31
36
|
@docs_path = docs_path
|
|
32
37
|
@options = options
|
|
@@ -34,8 +39,7 @@ module LlmDocsBuilder
|
|
|
34
39
|
|
|
35
40
|
# Generate llms.txt content from documentation
|
|
36
41
|
#
|
|
37
|
-
# Scans documentation files, extracts metadata, prioritizes them, and builds a formatted
|
|
38
|
-
# llms.txt file.
|
|
42
|
+
# Scans documentation files, extracts metadata, prioritizes them, and builds a formatted llms.txt file.
|
|
39
43
|
#
|
|
40
44
|
# @return [String] generated llms.txt content
|
|
41
45
|
def generate
|
|
@@ -69,11 +73,13 @@ module LlmDocsBuilder
|
|
|
69
73
|
|
|
70
74
|
# Recursively finds and analyzes markdown files in directory
|
|
71
75
|
#
|
|
72
|
-
# Sorts by priority (README, guides, etc.) and skips hidden files
|
|
76
|
+
# Sorts by priority (README, guides, etc.) and skips hidden files and bulk-transformed
|
|
77
|
+
# files (ending with the suffix, e.g. README.llm.md)
|
|
73
78
|
#
|
|
74
79
|
# @return [Array<Hash>] sorted array of analyzed file metadata
|
|
75
80
|
def find_markdown_files_in_directory
|
|
76
81
|
files = []
|
|
82
|
+
output_ending = "#{options.fetch(:suffix, '.llm')}.md".downcase
|
|
77
83
|
|
|
78
84
|
Find.find(docs_path) do |path|
|
|
79
85
|
# Skip hidden directories unless explicitly enabled
|
|
@@ -86,6 +92,7 @@ module LlmDocsBuilder
|
|
|
86
92
|
next unless File.file?(path)
|
|
87
93
|
next unless path.match?(/\.md$/i)
|
|
88
94
|
next if File.basename(path).start_with?('.')
|
|
95
|
+
next if output_ending != '.md' && path.downcase.end_with?(output_ending)
|
|
89
96
|
next if should_exclude?(path)
|
|
90
97
|
|
|
91
98
|
files << analyze_file(path)
|
|
@@ -158,7 +165,8 @@ module LlmDocsBuilder
|
|
|
158
165
|
|
|
159
166
|
# Extracts description from file content
|
|
160
167
|
#
|
|
161
|
-
# Takes first paragraph after title, truncated to
|
|
168
|
+
# Takes first paragraph after title as a single line, truncated to MAX_DESCRIPTION_LENGTH characters.
|
|
169
|
+
# A Markdown link or image that would be cut by the limit is dropped entirely.
|
|
162
170
|
#
|
|
163
171
|
# @param content [String] file content
|
|
164
172
|
# @return [String] extracted description
|
|
@@ -171,7 +179,62 @@ module LlmDocsBuilder
|
|
|
171
179
|
# Get first paragraph
|
|
172
180
|
first_paragraph = description_lines.take_while { |line| !line.strip.empty? }
|
|
173
181
|
|
|
174
|
-
first_paragraph.join(' ').
|
|
182
|
+
description = first_paragraph.join(' ').gsub(/\s+/, ' ').strip
|
|
183
|
+
return description if description.length <= MAX_DESCRIPTION_LENGTH
|
|
184
|
+
|
|
185
|
+
cut_link = link_spans(description).find do |span|
|
|
186
|
+
span.begin < MAX_DESCRIPTION_LENGTH && span.end >= MAX_DESCRIPTION_LENGTH
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return description.slice(0, MAX_DESCRIPTION_LENGTH) unless cut_link
|
|
190
|
+
|
|
191
|
+
description.slice(0, cut_link.begin).rstrip
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Finds the character ranges of Markdown links and images in text
|
|
195
|
+
#
|
|
196
|
+
# Brackets and parentheses are matched by nesting depth, so linked images, nested link text, URLs
|
|
197
|
+
# with parentheses and reference links are each one range. An unclosed bracket is not a link.
|
|
198
|
+
#
|
|
199
|
+
# @param text [String] single-line text
|
|
200
|
+
# @return [Array<Range>] inclusive index ranges
|
|
201
|
+
def link_spans(text)
|
|
202
|
+
closing = closing_indexes(text)
|
|
203
|
+
spans = []
|
|
204
|
+
index = 0
|
|
205
|
+
|
|
206
|
+
while (start = text.index('[', index))
|
|
207
|
+
stop = closing[start]
|
|
208
|
+
index = start + 1
|
|
209
|
+
next unless stop
|
|
210
|
+
|
|
211
|
+
stop = closing[stop + 1] while closing[stop + 1]
|
|
212
|
+
start -= 1 if start.positive? && text[start - 1] == '!'
|
|
213
|
+
spans << (start..stop)
|
|
214
|
+
index = stop + 1
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
spans
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Maps the index of each opening bracket or parenthesis to the index of the one that closes it
|
|
221
|
+
#
|
|
222
|
+
# @param text [String] text to search
|
|
223
|
+
# @return [Hash{Integer => Integer}] closing index by opening index, unclosed ones are absent
|
|
224
|
+
def closing_indexes(text)
|
|
225
|
+
closing = {}
|
|
226
|
+
open_indexes = { '[' => [], '(' => [] }
|
|
227
|
+
pairs = { ']' => '[', ')' => '(' }
|
|
228
|
+
|
|
229
|
+
text.each_char.with_index do |char, index|
|
|
230
|
+
if open_indexes.key?(char)
|
|
231
|
+
open_indexes[char] << index
|
|
232
|
+
elsif pairs.key?(char) && (opened = open_indexes[pairs[char]].pop)
|
|
233
|
+
closing[opened] = index
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
closing
|
|
175
238
|
end
|
|
176
239
|
|
|
177
240
|
# Assigns priority to file based on filename patterns
|
|
@@ -311,13 +374,18 @@ module LlmDocsBuilder
|
|
|
311
374
|
|
|
312
375
|
# Constructs full URL from path using base_url option if provided
|
|
313
376
|
#
|
|
377
|
+
# Each path segment is percent-encoded, so names with spaces, parentheses or
|
|
378
|
+
# non-ASCII characters give a valid Markdown link destination.
|
|
379
|
+
#
|
|
314
380
|
# @param path [String] relative path to file
|
|
315
381
|
# @return [String] full URL or relative path
|
|
316
382
|
def build_url(path)
|
|
383
|
+
encoded_path = path.split('/').map { |segment| ERB::Util.url_encode(segment) }.join('/')
|
|
384
|
+
|
|
317
385
|
if (base_url = options[:base_url])
|
|
318
|
-
File.join(base_url,
|
|
386
|
+
File.join(base_url, encoded_path)
|
|
319
387
|
else
|
|
320
|
-
|
|
388
|
+
encoded_path
|
|
321
389
|
end
|
|
322
390
|
end
|
|
323
391
|
|