yard-markdown-relative-links 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fbcb5b063125d5b23b85d928be3f180acc9c225504b93cf2ed75eb536e5e3971
4
+ data.tar.gz: fe4b93bcd506fae09bb4975aa0313729e1c64654b3f599e4da49e061d6264674
5
+ SHA512:
6
+ metadata.gz: 2e403985e27ffc84edc382582b47530428cca504b38668a1410d7777d6c04b4be42482321a72af1ab174473d2f44a2cb91433b909b8aa0fa1923bb26d3a6ab0e
7
+ data.tar.gz: 13ad0ea218d206df70ca047eb183212e6c627587aacb63da22861a211034aff20f61411283329e9619208ae387cd371ab0ba781fe2e2b8565bf4322371b365f2
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-03-03
9
+
10
+ ### Changed
11
+
12
+ - Replaced `Nokogiri` with a stdlib-only anchor rewriter for link conversion
13
+ - Refactored file lookup caching to rebuild indexes when YARD file lists change
14
+
15
+ ### Added
16
+
17
+ - Test coverage for uppercase/single-quoted anchors and malformed HTML fragments
18
+
19
+ ## [0.1.0] - 2026-03-03
20
+
21
+ ### Added
22
+
23
+ - Initial release
24
+ - Convert relative Markdown links to YARD file references
25
+ - Support for files in subdirectories by resolving links relative to the current file's directory
26
+ - Preserve anchor/fragment links (e.g., `file.md#section`)
27
+ - Basename fallback matching when there's exactly one file with the matching name
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 Daniel Harrington
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # yard-markdown-relative-links
2
+
3
+ [![CI](https://github.com/rubiii/yard-markdown-relative-links/actions/workflows/ci.yml/badge.svg)](https://github.com/rubiii/yard-markdown-relative-links/actions/workflows/ci.yml)
4
+
5
+ A YARD plugin to convert relative links between Markdown files.
6
+
7
+ GitHub and YARD render Markdown files differently. In particular, relative links in Markdown files that work in GitHub don't work in YARD. For example, if you have `[hello](FOO.md)` in your README, YARD renders it as `<a href="FOO.md">hello</a>`, creating a broken link in your docs.
8
+
9
+ With this plugin enabled, you'll get `<a href="file.FOO.html">hello</a>` instead, which correctly links through to the rendered HTML file.
10
+
11
+ ## Features
12
+
13
+ - Converts relative Markdown links to YARD file references
14
+ - **Supports files in subdirectories** — links from `docs/index.md` to `getting-started.md` correctly resolve to `docs/getting-started.md`
15
+ - Preserves anchor/fragment links (e.g., `file.md#section`)
16
+ - Falls back to basename matching for simple cases
17
+ - No `Nokogiri` runtime dependency (stdlib-only link rewriting)
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's `Gemfile`:
22
+
23
+ ```ruby
24
+ gem 'yard-markdown-relative-links'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ ```sh
30
+ bundle install
31
+ ```
32
+
33
+ Or install it yourself as:
34
+
35
+ ```sh
36
+ gem install yard-markdown-relative-links
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ Add this line to your application's `.yardopts`:
42
+
43
+ ```
44
+ --plugin relative_markdown_links
45
+ ```
46
+
47
+ You'll also need to make sure your Markdown files are processed by YARD. To include all Markdown files in your project, add the following lines to the end of your application's `.yardopts`:
48
+
49
+ ```
50
+ -
51
+ **/*.md
52
+ ```
53
+
54
+ ### Example `.yardopts`
55
+
56
+ ```
57
+ --markup markdown
58
+ --plugin relative_markdown_links
59
+ --readme README.md
60
+ -
61
+ docs/*.md
62
+ lib/**/*.rb
63
+ ```
64
+
65
+ ## How It Works
66
+
67
+ When YARD processes your Markdown files, this plugin intercepts the HTML output and converts relative links to YARD's `{file:}` syntax.
68
+
69
+ The plugin resolves links in three ways:
70
+
71
+ 1. **Exact match** — If the link path exactly matches a file in YARD's file list (e.g., `docs/index.md`), it's used directly.
72
+
73
+ 2. **Relative to current file** — If processing `docs/index.md` and it contains a link to `getting-started.md`, the plugin resolves this relative to the current file's directory, finding `docs/getting-started.md`.
74
+
75
+ 3. **Basename fallback** — If there's exactly one file with the matching basename, it's used (e.g., `getting-started.md` matches `docs/getting-started.md` if that's the only file with that name).
76
+
77
+ ## Background
78
+
79
+ ### Origins
80
+
81
+ This gem is a replacement for the original [`yard-relative_markdown_links`](https://github.com/haines/yard-relative_markdown_links) gem created by Andrew Haines. The original gem was archived in February 2026.
82
+
83
+ The original gem solved an important problem: relative links between Markdown files that work on GitHub don't work in YARD-generated documentation. However, it had a limitation — it only matched links against exact file paths in YARD's file list, which meant links between files in subdirectories didn't work properly.
84
+
85
+ For example, if you had documentation in a `docs/` folder:
86
+
87
+ ```
88
+ docs/
89
+ ├── index.md # contains [Getting Started](getting-started.md)
90
+ └── getting-started.md
91
+ ```
92
+
93
+ The link in `index.md` would work on GitHub but not in YARD, because the original gem looked for `getting-started.md` in the file list, but YARD registered it as `docs/getting-started.md`.
94
+
95
+ ### Comparison with the Original Gem
96
+
97
+ | Feature | Original | This Gem |
98
+ |---------|----------|----------|
99
+ | Convert exact path matches | ✅ | ✅ |
100
+ | Preserve fragment/anchors (`#section`) | ✅ | ✅ |
101
+ | Skip absolute URLs | ✅ | ✅ |
102
+ | RDoc filename mapping | ✅ | ✅ |
103
+ | **Subdirectory support** | ❌ | ✅ |
104
+ | **Parent directory (`..`) resolution** | ❌ | ✅ |
105
+ | **Basename fallback matching** | ❌ | ✅ |
106
+ | **Malformed URI handling** | ❌ | ✅ |
107
+
108
+ ### Key Improvements
109
+
110
+ 1. **Subdirectory support** — When processing `docs/index.md`, a link to `getting-started.md` is resolved relative to the current file's directory, correctly finding `docs/getting-started.md`.
111
+
112
+ 2. **Parent directory resolution** — Links like `../other-file.md` are normalized and resolved correctly.
113
+
114
+ 3. **Basename fallback** — If there's exactly one file with a matching basename, it's used even without the full path.
115
+
116
+ 4. **Error handling** — Malformed URIs are gracefully skipped instead of raising exceptions.
117
+
118
+ ## Development
119
+
120
+ Run tests:
121
+
122
+ ```sh
123
+ bundle exec rake test
124
+ ```
125
+
126
+ Run linting:
127
+
128
+ ```sh
129
+ bundle exec rake lint
130
+ ```
131
+
132
+ ## License
133
+
134
+ Released under the [MIT License](LICENSE.txt).
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module RelativeMarkdownLinks
5
+ VERSION = '0.2.0'
6
+ end
7
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ require_relative 'relative_markdown_links/version'
6
+
7
+ module YARD
8
+ # A YARD plugin to convert relative links between Markdown files.
9
+ #
10
+ # GitHub and YARD render Markdown files differently. In particular, relative
11
+ # links in Markdown files that work in GitHub don't work in YARD. For example,
12
+ # if you have `[hello](docs/FOO.md)` in your README, YARD renders it as
13
+ # `<a href="docs/FOO.md">hello</a>`, creating a broken link in your docs.
14
+ #
15
+ # With this plugin enabled, you'll get `<a href="file.FOO.html">hello</a>`
16
+ # instead, which correctly links through to the rendered HTML file.
17
+ #
18
+ # This plugin also properly handles files in subdirectories. For example,
19
+ # if `docs/index.md` links to `getting-started.md`, the plugin resolves
20
+ # the link relative to the current file's directory, finding
21
+ # `docs/getting-started.md` in YARD's file list.
22
+ module RelativeMarkdownLinks
23
+ ANCHOR_TAG_PATTERN = %r{<a\b(?<attributes>[^>]*)>(?<content>.*?)</a>}im
24
+ HREF_ATTRIBUTE_PATTERN = /
25
+ \bhref
26
+ \s*=\s*
27
+ (?:
28
+ "(?<double>[^"]*)"
29
+ |
30
+ '(?<single>[^']*)'
31
+ |
32
+ (?<bare>[^\s"'=<>`]+)
33
+ )
34
+ /imx
35
+ RDOC_FILENAME_PATTERN = %r{\A(?<dirname>(?:[^/#]*/)*+)(?<basename>[^/#]+)\.(?<ext>rb|rdoc|md)\z}i
36
+
37
+ # Resolves relative links from Markdown files.
38
+ #
39
+ # @param text [String] the HTML fragment in which to resolve links
40
+ # @return [String] HTML with relative links to extra files converted to `{file:}` links
41
+ def resolve_links(text)
42
+ return super unless options.files
43
+
44
+ super(rewrite_anchor_links(text))
45
+ end
46
+
47
+ private
48
+
49
+ # Replace supported HTML anchors with YARD {file:} references when they resolve to known files.
50
+ #
51
+ # @param text [String] HTML fragment to rewrite
52
+ # @return [String]
53
+ def rewrite_anchor_links(text)
54
+ text.gsub(ANCHOR_TAG_PATTERN) do |anchor_tag|
55
+ attributes = Regexp.last_match[:attributes]
56
+ content = Regexp.last_match[:content]
57
+ href = extract_href(attributes)
58
+ file_ref = href && resolve_href(href)
59
+
60
+ file_ref ? "{file:#{file_ref} #{content}}" : anchor_tag
61
+ end
62
+ end
63
+
64
+ # Extract the href attribute value from an <a ...> attribute string.
65
+ #
66
+ # @param attributes [String]
67
+ # @return [String, nil]
68
+ def extract_href(attributes)
69
+ match = HREF_ATTRIBUTE_PATTERN.match(attributes)
70
+ return nil unless match
71
+
72
+ match[:double] || match[:single] || match[:bare]
73
+ end
74
+
75
+ # Resolve a raw href string into a YARD file reference target.
76
+ #
77
+ # @param raw_href [String]
78
+ # @return [String, nil]
79
+ def resolve_href(raw_href)
80
+ href = URI(raw_href)
81
+ return nil unless href.relative?
82
+ return nil if href.query
83
+
84
+ path = href.path
85
+ return nil if path.nil? || path.empty?
86
+
87
+ resolved_path = resolve_file_path(path)
88
+ return nil unless resolved_path
89
+
90
+ href.fragment ? "#{resolved_path}##{href.fragment}" : resolved_path
91
+ rescue URI::InvalidURIError
92
+ # Skip malformed URIs
93
+ nil
94
+ end
95
+
96
+ # Resolve a relative file path to a known file in the YARD file list.
97
+ #
98
+ # @param path [String] the relative path from the link
99
+ # @return [String, nil] the resolved path if found, nil otherwise
100
+ def resolve_file_path(path)
101
+ indexes = file_indexes
102
+
103
+ # First, try exact match (works for root-level files like docs/index.md from README)
104
+ return path if indexes[:filenames].include?(path)
105
+
106
+ # Try RDoc-style filename mapping (e.g., foo_bar_md.html -> foo_bar.md)
107
+ rdoc_resolved = indexes[:rdoc_filenames][path]
108
+ return rdoc_resolved if rdoc_resolved
109
+
110
+ # Try RDoc-style basename-only matching
111
+ rdoc_basename_resolved = resolve_rdoc_by_basename(path, indexes[:rdoc_basename_to_paths])
112
+ return rdoc_basename_resolved if rdoc_basename_resolved
113
+
114
+ # Try resolving relative to current file's directory
115
+ resolved = resolve_relative_to_current_file(path, indexes[:filenames])
116
+ return resolved if resolved
117
+
118
+ # Fallback: try to find by basename alone (for simple cases)
119
+ resolve_by_basename(path, indexes[:basename_to_paths])
120
+ end
121
+
122
+ # Build and memoize lookup structures for the current options.files list.
123
+ #
124
+ # @return [Hash]
125
+ def file_indexes
126
+ filenames = options.files.map(&:filename)
127
+ return @file_indexes if @file_indexes && @file_indexes_filenames == filenames
128
+
129
+ @file_indexes_filenames = filenames
130
+ @file_indexes = {
131
+ filenames: filenames.to_set,
132
+ basename_to_paths: build_basename_mapping(filenames),
133
+ rdoc_filenames: build_rdoc_filename_mapping(filenames),
134
+ rdoc_basename_to_paths: build_rdoc_basename_mapping(filenames)
135
+ }
136
+ end
137
+
138
+ # Resolve a path relative to the current file being processed.
139
+ #
140
+ # @param path [String] the relative path from the link
141
+ # @param filenames [Set<String>] known filenames
142
+ # @return [String, nil] the resolved path if found, nil otherwise
143
+ def resolve_relative_to_current_file(path, filenames)
144
+ current_dir = current_file_directory
145
+ return nil unless current_dir
146
+
147
+ resolved = File.join(current_dir, path)
148
+ resolved = normalize_path(resolved)
149
+ resolved if filenames.include?(resolved)
150
+ end
151
+
152
+ # Resolve a path by matching its basename against known files.
153
+ #
154
+ # @param path [String] the relative path from the link
155
+ # @param basename_to_paths [Hash{String => Array<String>}]
156
+ # @return [String, nil] the resolved path if exactly one match, nil otherwise
157
+ def resolve_by_basename(path, basename_to_paths)
158
+ basename = File.basename(path)
159
+ candidates = basename_to_paths[basename]
160
+ candidates.first if candidates&.size == 1
161
+ end
162
+
163
+ # Get the directory of the current file being processed.
164
+ #
165
+ # @return [String, nil] the directory path, or nil if not available
166
+ def current_file_directory
167
+ # Try @file first (set in layout template)
168
+ return File.dirname(@file.filename) if defined?(@file) && @file.respond_to?(:filename)
169
+
170
+ # Try options.file (set during serialization)
171
+ return File.dirname(options.file.filename) if options.respond_to?(:file) && options.file.respond_to?(:filename)
172
+
173
+ nil
174
+ end
175
+
176
+ # Build a mapping from basename to full paths for fallback resolution.
177
+ #
178
+ # @param filenames [Array<String>] known filenames
179
+ # @return [Hash{String => Array<String>}] mapping of basenames to full paths
180
+ def build_basename_mapping(filenames)
181
+ mapping = Hash.new { |h, k| h[k] = [] }
182
+ filenames.each do |filename|
183
+ mapping[File.basename(filename)] << filename
184
+ end
185
+ mapping
186
+ end
187
+
188
+ # Build a mapping from RDoc-style HTML filenames to original filenames.
189
+ #
190
+ # RDoc generates filenames like `foo_bar_md.html` for `foo_bar.md`.
191
+ # This mapping allows resolving such links back to the original files.
192
+ #
193
+ # @param filenames [Array<String>] known filenames
194
+ # @return [Hash{String => String}] mapping of RDoc HTML names to original filenames
195
+ # @see https://github.com/ruby/rdoc/blob/0e060c69f51ec4a877e5cde69b31d47eaeb2a2b9/lib/rdoc/markup/to_html.rb#L364-L366
196
+ def build_rdoc_filename_mapping(filenames)
197
+ filenames.filter_map do |filename|
198
+ match = RDOC_FILENAME_PATTERN.match(filename)
199
+ next unless match
200
+
201
+ rdoc_name = "#{match[:dirname]}#{match[:basename].tr('.', '_')}_#{match[:ext]}.html"
202
+ [rdoc_name, filename]
203
+ end.to_h
204
+ end
205
+
206
+ # Build a mapping from RDoc-style basenames to original filenames.
207
+ #
208
+ # @param filenames [Array<String>] known filenames
209
+ # @return [Hash{String => Array<String>}] mapping of RDoc basenames to original filenames
210
+ def build_rdoc_basename_mapping(filenames)
211
+ mapping = Hash.new { |h, k| h[k] = [] }
212
+ filenames.each do |filename|
213
+ match = RDOC_FILENAME_PATTERN.match(filename)
214
+ next unless match
215
+
216
+ rdoc_basename = "#{match[:basename].tr('.', '_')}_#{match[:ext]}.html"
217
+ mapping[rdoc_basename] << filename
218
+ end
219
+ mapping
220
+ end
221
+
222
+ # Resolve RDoc-style filename by basename alone.
223
+ #
224
+ # @param path [String] the RDoc-style HTML filename (e.g., getting-started_md.html)
225
+ # @param rdoc_basename_to_paths [Hash{String => Array<String>}]
226
+ # @return [String, nil] the resolved path if exactly one match, nil otherwise
227
+ def resolve_rdoc_by_basename(path, rdoc_basename_to_paths)
228
+ basename = File.basename(path)
229
+ candidates = rdoc_basename_to_paths[basename]
230
+ candidates.first if candidates&.size == 1
231
+ end
232
+
233
+ # Normalize a file path, resolving . and .. components.
234
+ #
235
+ # @param path [String] the path to normalize
236
+ # @return [String] the normalized path
237
+ def normalize_path(path)
238
+ parts = path.split('/')
239
+ result = []
240
+
241
+ parts.each do |part|
242
+ case part
243
+ when '.', ''
244
+ # Skip current directory markers and empty parts
245
+ next
246
+ when '..'
247
+ # Go up one directory
248
+ result.pop
249
+ else
250
+ result << part
251
+ end
252
+ end
253
+
254
+ result.join('/')
255
+ end
256
+ end
257
+
258
+ Templates::Template.extra_includes << RelativeMarkdownLinks
259
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'yard/relative_markdown_links'
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yard-markdown-relative-links
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Harrington
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: yard
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0.9'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '1'
32
+ description: |
33
+ A YARD plugin that converts relative Markdown links to work in generated documentation.
34
+ Supports files in subdirectories by resolving links relative to the current file's location.
35
+ Works seamlessly with GitHub-style relative links while generating correct YARD file references.
36
+ email:
37
+ - me@rubiii.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - CHANGELOG.md
43
+ - MIT-LICENSE
44
+ - README.md
45
+ - lib/yard-markdown-relative-links.rb
46
+ - lib/yard/relative_markdown_links.rb
47
+ - lib/yard/relative_markdown_links/version.rb
48
+ homepage: https://github.com/rubiii/yard-markdown-relative-links
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/rubiii/yard-markdown-relative-links
53
+ source_code_uri: https://github.com/rubiii/yard-markdown-relative-links
54
+ changelog_uri: https://github.com/rubiii/yard-markdown-relative-links/blob/main/CHANGELOG.md
55
+ bug_tracker_uri: https://github.com/rubiii/yard-markdown-relative-links/issues
56
+ rubygems_mfa_required: 'true'
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '3.3'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 4.0.3
72
+ specification_version: 4
73
+ summary: A YARD plugin to convert relative links between Markdown files
74
+ test_files: []