jekyll-relative-links 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc58c9b968be5f460493796546b5da7dcb3ec9dc70521790e7475945eb7808ff
4
- data.tar.gz: ff9390b8df13ac3cbdd9034a2bfdac93b14e8f0d3a9396a07839f2be791b87ab
3
+ metadata.gz: 3ebb41a644bab20c56e388f5b63e9fcaea9e8405de7c6af7853b7b30ca80b5bd
4
+ data.tar.gz: 12fd6b72f8fe9acde53c5fe8059a409610b6d808d63c888e7585de25b93a2160
5
5
  SHA512:
6
- metadata.gz: 67c213b7308561caee8f28bb8132667d0f5fc8d9001c74eb65cbde328a81265a4183863762f5934f262ffa676504f19b9011eac679e2f5abc9583e1d019c46cb
7
- data.tar.gz: '012508fa52c056fcd891c15bb2d75e636e97053e3e53b244f5a63fa66edde0aa68ff760fbb0816fe07f149105770803f6be604da7e1029647a4af7d4120c3340'
6
+ metadata.gz: eb15617682d0df090db9ab9a25ae69c9fd8d50188a2ec49d7fa1a873d01e3ddbf638085ec0620c2a6cdc1a65f38e15f7615385c0132da974c496d0dab7f1b455
7
+ data.tar.gz: 5acc91f26ba6c9ee15341bf0575fc72c7ec4174166cbbc1c602ed80384a518702b74558b61dae9b0ac8f1cbcbb3c2c01eefc98d136dad5b04e0ed8b8170b0447
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllRelativeLinks
4
+ module Filter
5
+ # This filter processes HTML content that's already been converted by the markdownify
6
+ # filter and updates any relative links to markdown files to point to their HTML equivalents.
7
+ # Usage: {{ content | markdownify | rellinks }}
8
+ def rellinks(html)
9
+ return html if html.nil? || html.empty?
10
+ return html if @context.registers[:site].nil?
11
+
12
+ process_links(html, @context.registers[:site])
13
+ end
14
+
15
+ def process_links(html, site)
16
+ page = @context.registers[:page]
17
+ url_base = page ? File.dirname(page["path"].to_s) : ""
18
+
19
+ html.gsub(%r!<a href="([^"]+\.md)(#([^"]+))?"!) do |match|
20
+ process_link(match, Regexp.last_match, url_base, site)
21
+ end
22
+ end
23
+
24
+ def process_link(match, regex_match, url_base, site)
25
+ relative_path = regex_match[1]
26
+ fragment = regex_match[3] ? "##{regex_match[3]}" : ""
27
+
28
+ return match if absolute_url?(relative_path) || !relative_path.end_with?(".md")
29
+
30
+ path = path_from_root(relative_path, url_base)
31
+ url = url_for_path(path, site)
32
+ url ? "<a href=\"#{url}#{fragment}\"" : match
33
+ end
34
+
35
+ private
36
+
37
+ def absolute_url?(string)
38
+ return false unless string
39
+
40
+ Addressable::URI.parse(string).absolute?
41
+ rescue Addressable::URI::InvalidURIError
42
+ false
43
+ end
44
+
45
+ def path_from_root(relative_path, url_base)
46
+ is_absolute = relative_path.start_with? "/"
47
+
48
+ relative_path.delete_prefix!("/")
49
+ base = is_absolute ? "" : url_base
50
+ absolute_path = File.expand_path(relative_path, base)
51
+ absolute_path.sub(%r!\A#{Regexp.escape(Dir.pwd)}/!, "")
52
+ end
53
+
54
+ def url_for_path(path, site)
55
+ path = CGI.unescape(path)
56
+ potential_targets = site.pages + site.static_files + site.docs_to_write
57
+ target = potential_targets.find { |p| p.relative_path.delete_prefix("/") == path }
58
+ relative_url(target.url) if target&.url
59
+ end
60
+ end
61
+ end
62
+
63
+ Liquid::Template.register_filter(JekyllRelativeLinks::Filter)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
4
+
3
5
  module JekyllRelativeLinks
4
6
  class Generator < Jekyll::Generator
5
7
  attr_accessor :site, :config
@@ -7,7 +9,12 @@ module JekyllRelativeLinks
7
9
  # Use Jekyll's native relative_url filter
8
10
  include Jekyll::Filters::URLFilters
9
11
 
10
- LINK_TEXT_REGEX = %r!(.*?)!.freeze
12
+ # Matches link text, including images like ![alt](url) or ![alt]
13
+ # Pattern explanation:
14
+ # - !\[[^\]]*\](?:\([^\)]*\))? matches complete image patterns: ![...](...) or ![...]
15
+ # - (?!\]\() uses negative lookahead to match any char except when followed by ](
16
+ # This allows nested ] while stopping at the correct closing bracket
17
+ LINK_TEXT_REGEX = %r{((?:!\[[^\]]*\](?:\([^\)]*\))?|(?!\]\().)*?)}.freeze
11
18
  FRAGMENT_REGEX = %r!(#.+?|)?!.freeze
12
19
  TITLE_REGEX = %r{(\s+"(?:\\"|[^"])*(?<!\\)"|\s+"(?:\\'|[^'])*(?<!\\)')?}.freeze
13
20
  FRAG_AND_TITLE_REGEX = %r!#{FRAGMENT_REGEX}#{TITLE_REGEX}!.freeze
@@ -53,7 +60,7 @@ module JekyllRelativeLinks
53
60
  link = link_parts(Regexp.last_match)
54
61
  next original unless replaceable_link?(link.path)
55
62
 
56
- path = path_from_root(link.path, url_base)
63
+ path = path_from_root(CGI.unescape(link.path), url_base)
57
64
  url = url_for_path(path)
58
65
  next original unless url
59
66
 
@@ -94,8 +101,7 @@ module JekyllRelativeLinks
94
101
  end
95
102
 
96
103
  def url_for_path(path)
97
- path = CGI.unescape(path)
98
- target = potential_targets.find { |p| p.relative_path.sub(%r!\A/!, "") == path }
104
+ target = potential_targets_by_path[path]
99
105
  relative_url(target.url) if target&.url
100
106
  end
101
107
 
@@ -103,6 +109,19 @@ module JekyllRelativeLinks
103
109
  @potential_targets ||= site.pages + site.static_files + site.docs_to_write
104
110
  end
105
111
 
112
+ # Index `potential_targets` by the same key the previous linear `find`
113
+ # compared against, so each `url_for_path` lookup is O(1) instead of
114
+ # O(N). On a site with M markdown link matches and N potential
115
+ # targets, total link-resolution cost goes from O(M*N) to O(M+N).
116
+ # First-wins semantics are preserved against the (unlikely) case of
117
+ # two targets sharing the same `relative_path`.
118
+ def potential_targets_by_path
119
+ @potential_targets_by_path ||= potential_targets.each_with_object({}) do |p, h|
120
+ key = p.relative_path.sub(%r!\A/!, "")
121
+ h[key] = p unless h.key?(key)
122
+ end
123
+ end
124
+
106
125
  def path_from_root(relative_path, url_base)
107
126
  is_absolute = relative_path.start_with? "/"
108
127
 
@@ -124,11 +143,11 @@ module JekyllRelativeLinks
124
143
  end
125
144
 
126
145
  def absolute_url?(string)
127
- return unless string
146
+ return false unless string
128
147
 
129
148
  Addressable::URI.parse(string).absolute?
130
149
  rescue Addressable::URI::InvalidURIError
131
- nil
150
+ false
132
151
  end
133
152
 
134
153
  def fragment?(string)
@@ -170,7 +189,9 @@ module JekyllRelativeLinks
170
189
  end
171
190
 
172
191
  def replace_relative_links_excerpt!(document)
173
- document.data["excerpt"] = Jekyll::Excerpt.new(document) if document.data["excerpt"]
192
+ return unless document.data["excerpt"] && !document.data["excerpt"].is_a?(String)
193
+
194
+ document.data["excerpt"] = Jekyll::Excerpt.new(document)
174
195
  end
175
196
  end
176
197
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllRelativeLinks
4
+ # Register Jekyll hooks to process HTML output after conversion
5
+ Jekyll::Hooks.register :pages, :post_render do |page|
6
+ next unless JekyllRelativeLinks::Hooks.should_process?(page, page.site.config)
7
+
8
+ page.output = JekyllRelativeLinks::Hooks.process_html_links(page.output, page, page.site)
9
+ end
10
+
11
+ Jekyll::Hooks.register :documents, :post_render do |document|
12
+ next unless JekyllRelativeLinks::Hooks.should_process_document?(document, document.site.config)
13
+
14
+ document.output = JekyllRelativeLinks::Hooks.process_html_links(document.output, document,
15
+ document.site)
16
+ end
17
+
18
+ module Hooks
19
+ CONVERTER_CLASS = Jekyll::Converters::Markdown
20
+ CONFIG_KEY = "relative_links"
21
+ ENABLED_KEY = "enabled"
22
+ COLLECTIONS_KEY = "collections"
23
+
24
+ # Regex to match markdown links in HTML: <a href="*.md">
25
+ # Capture groups:
26
+ # (1) attributes before href, (2) the .md path,
27
+ # (3) optional fragment, (4) attributes after href
28
+ MARKDOWN_LINK_IN_HTML = %r!<a\s+([^>]*?\s+)?href="([^"]+\.md)(#[^"]*)?"\s*([^>]*)>!m.freeze
29
+
30
+ def self.should_process?(page, config)
31
+ return false if disabled?(config)
32
+ return false unless markdown_extension?(page.extname, page.site)
33
+ return false if excluded?(page, config, page.site)
34
+
35
+ true
36
+ end
37
+
38
+ def self.should_process_document?(document, config)
39
+ return false if disabled?(config)
40
+ return false unless collections_enabled?(config)
41
+ return false unless markdown_extension?(document.extname, document.site)
42
+ return false if excluded?(document, config, document.site)
43
+
44
+ true
45
+ end
46
+
47
+ def self.disabled?(config)
48
+ config[CONFIG_KEY] && config[CONFIG_KEY][ENABLED_KEY] == false
49
+ end
50
+
51
+ def self.collections_enabled?(config)
52
+ config[CONFIG_KEY] && config[CONFIG_KEY][COLLECTIONS_KEY] == true
53
+ end
54
+
55
+ def self.markdown_extension?(extension, site)
56
+ converter = site.find_converter_instance(CONVERTER_CLASS)
57
+ converter.matches(extension)
58
+ end
59
+
60
+ def self.excluded?(document, config, site)
61
+ return false unless config[CONFIG_KEY] && config[CONFIG_KEY]["exclude"]
62
+
63
+ entry_filter = if document.respond_to?(:collection)
64
+ document.collection.entry_filter
65
+ else
66
+ Jekyll::EntryFilter.new(site)
67
+ end
68
+
69
+ entry_filter.glob_include?(config[CONFIG_KEY]["exclude"], document.relative_path)
70
+ end
71
+
72
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
73
+ def self.process_html_links(html, document, site)
74
+ return html if html.nil? || html.empty?
75
+
76
+ url_base = File.dirname(document.relative_path)
77
+ potential_targets = site.pages + site.static_files + site.docs_to_write
78
+
79
+ # Process <a href="*.md"> links that were added via includes
80
+ # rubocop:disable Metrics/BlockLength
81
+ html.gsub(MARKDOWN_LINK_IN_HTML) do |match|
82
+ attributes_before = Regexp.last_match[1] || ""
83
+ relative_path = Regexp.last_match[2]
84
+ fragment = Regexp.last_match[3] || ""
85
+ attributes_after = Regexp.last_match[4] || ""
86
+
87
+ # Skip absolute URLs
88
+ begin
89
+ next match if Addressable::URI.parse(relative_path).absolute?
90
+ rescue Addressable::URI::InvalidURIError
91
+ next match
92
+ end
93
+
94
+ # Calculate path from root
95
+ is_absolute = relative_path.start_with?("/")
96
+ relative_path_clean = relative_path.delete_prefix("/")
97
+ base = is_absolute ? "" : url_base
98
+ absolute_path = File.expand_path(relative_path_clean, base)
99
+ path = absolute_path.sub(%r!\A#{Regexp.escape(Dir.pwd)}/!, "")
100
+
101
+ # Find the target page and get its URL
102
+ path = CGI.unescape(path)
103
+ target = potential_targets.find { |p| p.relative_path.delete_prefix("/") == path }
104
+
105
+ if target&.url
106
+ # Use Jekyll's URL
107
+ url = target.url
108
+ url = "/#{url}" unless url.start_with?("/")
109
+
110
+ # Build the replacement ensuring proper spacing
111
+ attrs = []
112
+ attrs << attributes_before.strip unless attributes_before.empty?
113
+ attrs << "href=\"#{url}#{fragment}\""
114
+ attrs << attributes_after.strip unless attributes_after.empty?
115
+ "<a #{attrs.join(" ")}>"
116
+ else
117
+ match
118
+ end
119
+ end
120
+ # rubocop:enable Metrics/BlockLength
121
+ end
122
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
123
+ end
124
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllRelativeLinks
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
@@ -3,6 +3,8 @@
3
3
  require "jekyll"
4
4
  require_relative "jekyll-relative-links/generator"
5
5
  require_relative "jekyll-relative-links/context"
6
+ require_relative "jekyll-relative-links/filter"
7
+ require_relative "jekyll-relative-links/hooks"
6
8
 
7
9
  module JekyllRelativeLinks
8
10
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-relative-links
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Balter
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-01-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: jekyll
@@ -72,6 +71,20 @@ dependencies:
72
71
  - - "~>"
73
72
  - !ruby/object:Gem::Version
74
73
  version: '1.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: rubocop-factory_bot
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 2.26.0
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 2.26.0
75
88
  - !ruby/object:Gem::Dependency
76
89
  name: rubocop-jekyll
77
90
  requirement: !ruby/object:Gem::Requirement
@@ -106,15 +119,70 @@ dependencies:
106
119
  requirements:
107
120
  - - "~>"
108
121
  - !ruby/object:Gem::Version
109
- version: '2.0'
122
+ version: '3.0'
110
123
  type: :development
111
124
  prerelease: false
112
125
  version_requirements: !ruby/object:Gem::Requirement
113
126
  requirements:
114
127
  - - "~>"
115
128
  - !ruby/object:Gem::Version
116
- version: '2.0'
117
- description:
129
+ version: '3.0'
130
+ - !ruby/object:Gem::Dependency
131
+ name: base64
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ - !ruby/object:Gem::Dependency
145
+ name: benchmark
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: ostruct
160
+ requirement: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ - !ruby/object:Gem::Dependency
173
+ name: tsort
174
+ requirement: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
118
186
  email:
119
187
  - ben.balter@github.com
120
188
  executables: []
@@ -123,13 +191,14 @@ extra_rdoc_files: []
123
191
  files:
124
192
  - lib/jekyll-relative-links.rb
125
193
  - lib/jekyll-relative-links/context.rb
194
+ - lib/jekyll-relative-links/filter.rb
126
195
  - lib/jekyll-relative-links/generator.rb
196
+ - lib/jekyll-relative-links/hooks.rb
127
197
  - lib/jekyll-relative-links/version.rb
128
198
  homepage: https://github.com/benbalter/jekyll-relative-links
129
199
  licenses:
130
200
  - MIT
131
201
  metadata: {}
132
- post_install_message:
133
202
  rdoc_options: []
134
203
  require_paths:
135
204
  - lib
@@ -144,8 +213,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
213
  - !ruby/object:Gem::Version
145
214
  version: '0'
146
215
  requirements: []
147
- rubygems_version: 3.2.33
148
- signing_key:
216
+ rubygems_version: 4.0.16
149
217
  specification_version: 4
150
218
  summary: A Jekyll plugin to convert relative links to markdown files to their rendered
151
219
  equivalents.