jekyll-link-decorator 1.6.0 → 1.7.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/lib/jekyll-link-decorator/version.rb +1 -1
- data/lib/jekyll-link-decorator.rb +49 -126
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff4ff4aab91fea08f0f9d818a35287da8ad337181e028a2a2da62245866b3420
|
|
4
|
+
data.tar.gz: 81737c19fc41c5134c6741b0b6ebfb28746ca376f591c46a95a16631d56cfebf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e912b0fbdc82c9640912f96b5a4f436d378e79fd4f9099eaba218759d7dc6ca688285b6785aa34163d89c749e153b9e5de37623277e66c16f892abe23fe3be02
|
|
7
|
+
data.tar.gz: 0ba21a73e60ad550d74415a22f06b2ada6bf83d8bef37521e778fcb546ca39bdb0695bdeefe1fb53c6fe65003608c2725f8446f3943f69664e105c4f0d704880
|
|
@@ -1,82 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
3
|
+
# This plugin decorates <a> tags in Markdown content with Bootstrap CSS
|
|
4
|
+
# classes, security attributes, and Font Awesome icons. Links inside a
|
|
5
|
+
# Bootstrap alert box get alert-specific classes; other links get the
|
|
6
|
+
# default classes; links carrying a `.btn` class are left untouched.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# For links to a different domain than the site's own (site.url), it adds
|
|
9
|
+
# target="_blank", the rel="noopener noreferrer" security attributes, and an
|
|
10
|
+
# external-link icon at the end of the link text.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# end of the link text.
|
|
12
|
+
# It can also inject a permalink icon into each heading (h1-h6) so every
|
|
13
|
+
# section gets a shareable URL.
|
|
13
14
|
#
|
|
14
|
-
#
|
|
15
|
+
# Turn these on and customize them from `_config.yml` with the
|
|
16
|
+
# `with_link_decorator`, `with_link_decorator_data`, `with_heading_anchor`,
|
|
17
|
+
# and `with_heading_anchor_data` keys. See docs/configuration.md for the
|
|
18
|
+
# full key reference, defaults, and worked `_config.yml` examples.
|
|
15
19
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# - Links with target="_blank" receive rel="noopener noreferrer" protection against
|
|
20
|
-
# security vulnerabilities (window.opener access, referrer leakage)
|
|
21
|
-
#
|
|
22
|
-
# === Configuration ===
|
|
23
|
-
#
|
|
24
|
-
# Customize the plugin's classes by adding `with_link_decorator` and
|
|
25
|
-
# `with_link_decorator_data` to your `_config.yml`.
|
|
26
|
-
#
|
|
27
|
-
# * `with_link_decorator`: Boolean (default: true) that controls whether the plugin runs.
|
|
28
|
-
# Set to false to disable automatic link styling and icon insertion.
|
|
29
|
-
#
|
|
30
|
-
# * `with_link_decorator_data`: A dictionary containing optional configuration keys:
|
|
31
|
-
# - `default_link_classes`: Classes applied to links outside an .alert.
|
|
32
|
-
# Example: `default_link_classes: "text-decoration-none"`
|
|
33
|
-
# If a link already carries its own `link-{color}`, `link-underline-*`,
|
|
34
|
-
# `link-offset-*`, `link-underline-opacity-*`, `link-opacity-*`, or `focus-ring(-{color})?` class,
|
|
35
|
-
# the plugin skips the matching default token automatically — see LINK_CLASS_FAMILIES.
|
|
36
|
-
# - `alert_link_classes`: Classes applied to links inside an .alert box.
|
|
37
|
-
# Example: `alert_link_classes: "text-decoration-underline"`
|
|
38
|
-
# - `external_link_icon`: Boolean (default: true) — show the icon on cross-domain
|
|
39
|
-
# external links. Set to false to disable.
|
|
40
|
-
# - `external_link_icon_excluded_tags`: List of HTML child tag names (default: []) whose presence
|
|
41
|
-
# inside an anchor suppresses the external-link icon. Security attributes
|
|
42
|
-
# (target="_blank", rel="noopener noreferrer") still apply.
|
|
43
|
-
# Example: `external_link_icon_excluded_tags: ["img", "svg"]`
|
|
44
|
-
#
|
|
45
|
-
# === Heading Anchor Support ===
|
|
46
|
-
#
|
|
47
|
-
# The plugin can inject a permalink icon inside each heading (h1–h6) so every
|
|
48
|
-
# section gets a shareable URL. Configure it via two top-level keys (separate
|
|
49
|
-
# from `with_link_decorator_data`):
|
|
50
|
-
#
|
|
51
|
-
# * `with_heading_anchor`: Boolean (default: false) — set to true to inject heading anchors.
|
|
52
|
-
#
|
|
53
|
-
# * `with_heading_anchor_data`: A dictionary containing optional configuration keys:
|
|
54
|
-
# - `icon`: CSS classes applied to the anchor's glyph element (default:
|
|
55
|
-
# "heading-anchor-hash pe-1", which renders a plain "#" via the
|
|
56
|
-
# jekyll-theme-centos-base theme's CSS). Accepts any class list, including
|
|
57
|
-
# Font Awesome icon classes (e.g. "fa-solid fa-hashtag") for sites that
|
|
58
|
-
# want an icon-font glyph instead.
|
|
59
|
-
# - `icon_size`: Optional size modifier class appended after the icon classes.
|
|
60
|
-
#
|
|
61
|
-
# Without configuration, the plugin uses its default classes and enables
|
|
62
|
-
# external-link icons.
|
|
63
|
-
#
|
|
64
|
-
# Example `_config.yml`:
|
|
65
|
-
#
|
|
66
|
-
# ```yaml
|
|
67
|
-
# with_link_decorator: true
|
|
68
|
-
# with_link_decorator_data:
|
|
69
|
-
# default_link_classes: "link link-offset-3 link-offset-3-hover link-underline-primary link-underline-opacity-0 link-underline-opacity-100-hover focus-ring"
|
|
70
|
-
# alert_link_classes: "alert-link focus-ring"
|
|
71
|
-
# external_link_icon: true
|
|
72
|
-
# external_link_icon_excluded_tags:
|
|
73
|
-
# - img
|
|
74
|
-
# - svg
|
|
75
|
-
#
|
|
76
|
-
# with_heading_anchor: true
|
|
77
|
-
# with_heading_anchor_data:
|
|
78
|
-
# icon: "heading-anchor-hash pe-1"
|
|
79
|
-
# ```
|
|
20
|
+
# A link that already carries its own Bootstrap link-utility class (see
|
|
21
|
+
# LINK_CLASS_FAMILIES below) keeps that class instead of getting the
|
|
22
|
+
# plugin's default for the same family.
|
|
80
23
|
|
|
81
24
|
require 'nokogiri'
|
|
82
25
|
require_relative 'jekyll-link-decorator/version'
|
|
@@ -141,32 +84,24 @@ module Jekyll
|
|
|
141
84
|
DEFAULT_EXTERNAL_LINK_ICON = true
|
|
142
85
|
DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS = [].freeze
|
|
143
86
|
DEFAULT_HEADING_ANCHOR = false
|
|
144
|
-
DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash
|
|
87
|
+
DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash'
|
|
145
88
|
|
|
146
89
|
def self.name
|
|
147
90
|
'LinkDecorator'
|
|
148
91
|
end
|
|
149
92
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
93
|
+
# Returns a URL's domain, stripped of scheme and path.
|
|
94
|
+
# "https://example.com/path?query=1#anchor" -> "example.com"
|
|
152
95
|
def extract_domain(url)
|
|
153
96
|
return '' if url.nil? || url.empty?
|
|
154
97
|
|
|
155
|
-
# Remove protocol (http:// or https://)
|
|
156
98
|
domain = url.sub(%r{^https?://}, '')
|
|
157
|
-
|
|
158
|
-
# Extract just the domain part (before /, ?, or #)
|
|
159
99
|
domain = domain.split(%r{[/?#]}).first
|
|
160
|
-
|
|
161
|
-
# Convert to lowercase for case-insensitive comparison
|
|
162
100
|
domain.downcase
|
|
163
101
|
end
|
|
164
102
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
# - Link starts with http:// or https://
|
|
168
|
-
# - Link domain is different from site.url domain
|
|
169
|
-
# - Link doesn't already have an external-link icon child
|
|
103
|
+
# True when this link needs the external-link icon: an absolute,
|
|
104
|
+
# cross-domain link that doesn't already have one.
|
|
170
105
|
def is_external_different_domain_link?(link, site_domain)
|
|
171
106
|
return false unless absolute_different_domain?(link, site_domain)
|
|
172
107
|
return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? }
|
|
@@ -174,8 +109,9 @@ module Jekyll
|
|
|
174
109
|
true
|
|
175
110
|
end
|
|
176
111
|
|
|
177
|
-
#
|
|
178
|
-
#
|
|
112
|
+
# True when href is an absolute http(s) URL on a domain other than
|
|
113
|
+
# site_domain. Shared by is_external_different_domain_link? and
|
|
114
|
+
# cross_domain_external_link? below.
|
|
179
115
|
def absolute_different_domain?(link, site_domain)
|
|
180
116
|
href = link['href'].to_s
|
|
181
117
|
return false unless %r{^https?://}.match?(href)
|
|
@@ -187,51 +123,34 @@ module Jekyll
|
|
|
187
123
|
end
|
|
188
124
|
private :absolute_different_domain?
|
|
189
125
|
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
# - Link is absolute (starts with http:// or https://)
|
|
193
|
-
# - Link domain is different from site.url domain
|
|
194
|
-
# Returns false if:
|
|
195
|
-
# - Link is relative (starts with /, ../)
|
|
196
|
-
# - Link domain matches site.url domain
|
|
126
|
+
# True when this link needs target="_blank": absolute and cross-domain.
|
|
127
|
+
# Relative URLs (/, ../) are always treated as internal.
|
|
197
128
|
def cross_domain_external_link?(link, site_domain)
|
|
198
129
|
href = link['href'].to_s
|
|
199
|
-
|
|
200
|
-
# Relative URLs (starting with / or ../) are internal links
|
|
201
130
|
return false if %r{^(/|\.\.)}.match?(href)
|
|
202
131
|
|
|
203
132
|
absolute_different_domain?(link, site_domain)
|
|
204
133
|
end
|
|
205
134
|
|
|
206
|
-
#
|
|
207
|
-
# Only adds if target is not already set
|
|
135
|
+
# Sets target="_blank" on a cross-domain link, unless it already has a target.
|
|
208
136
|
def add_external_target_blank(link, site_domain)
|
|
209
|
-
# Skip if link already has a target attribute
|
|
210
137
|
return if link['target'].to_s.strip != ''
|
|
211
|
-
|
|
212
|
-
# Add target="_blank" if this is a cross-domain external link
|
|
213
138
|
return unless cross_domain_external_link?(link, site_domain)
|
|
214
139
|
|
|
215
140
|
link['target'] = '_blank'
|
|
216
141
|
end
|
|
217
142
|
|
|
218
|
-
#
|
|
219
|
-
#
|
|
143
|
+
# Adds rel="noopener noreferrer" to a target="_blank" link, keeping any
|
|
144
|
+
# rel values the author already set.
|
|
220
145
|
def add_external_target_protection(link)
|
|
221
146
|
target = link['target'].to_s
|
|
222
|
-
|
|
223
|
-
# Only add protection for target="_blank"
|
|
224
147
|
return unless target == '_blank'
|
|
225
148
|
|
|
226
|
-
# Get existing rel attribute (if any)
|
|
227
149
|
existing_rel = link['rel'].to_s.strip
|
|
228
150
|
rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/)
|
|
229
|
-
|
|
230
|
-
# Add required security values if not already present
|
|
231
151
|
rel_values << 'noopener' unless rel_values.include?('noopener')
|
|
232
152
|
rel_values << 'noreferrer' unless rel_values.include?('noreferrer')
|
|
233
153
|
|
|
234
|
-
# Set the updated rel attribute
|
|
235
154
|
link['rel'] = rel_values.join(' ')
|
|
236
155
|
end
|
|
237
156
|
|
|
@@ -269,8 +188,8 @@ module Jekyll
|
|
|
269
188
|
html
|
|
270
189
|
end
|
|
271
190
|
|
|
272
|
-
#
|
|
273
|
-
#
|
|
191
|
+
# Logs a notice when disabled; returns the given enabled flag unchanged.
|
|
192
|
+
# The caller (decorate_html) decides what "enabled" defaults to.
|
|
274
193
|
def plugin_enabled?(enabled)
|
|
275
194
|
unless enabled
|
|
276
195
|
Jekyll.logger.info 'LinkDecorator:',
|
|
@@ -279,17 +198,16 @@ module Jekyll
|
|
|
279
198
|
enabled
|
|
280
199
|
end
|
|
281
200
|
|
|
282
|
-
#
|
|
201
|
+
# Applies alert_classes to links inside an .alert box, default_classes
|
|
202
|
+
# to every other link.
|
|
283
203
|
def apply_link_styles(doc, config)
|
|
284
204
|
default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES)
|
|
285
205
|
alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES)
|
|
286
206
|
|
|
287
|
-
# Apply alert classes to links inside p.alert or blockquote.alert
|
|
288
207
|
doc.css('p.alert a:not(.btn), blockquote.alert a:not(.btn)').each do |link|
|
|
289
208
|
add_classes(link, alert_classes)
|
|
290
209
|
end
|
|
291
210
|
|
|
292
|
-
# Apply default classes to all other non-alert links
|
|
293
211
|
doc.css('a:not(.btn)').each do |link|
|
|
294
212
|
next if link.ancestors('p.alert, blockquote.alert').any?
|
|
295
213
|
|
|
@@ -310,12 +228,12 @@ module Jekyll
|
|
|
310
228
|
end
|
|
311
229
|
end
|
|
312
230
|
|
|
313
|
-
# Add CSS classes to a link element
|
|
314
231
|
def add_classes(link, classes_string)
|
|
315
232
|
classes_string.split.each { |cls| link.add_class(cls) }
|
|
316
233
|
end
|
|
317
234
|
|
|
318
|
-
#
|
|
235
|
+
# Adds target="_blank", the external-link icon, and rel security
|
|
236
|
+
# attributes to every link on the page, in that order.
|
|
319
237
|
def add_external_link_features(doc, config)
|
|
320
238
|
site_domain = extract_site_domain
|
|
321
239
|
return if site_domain.empty?
|
|
@@ -327,14 +245,14 @@ module Jekyll
|
|
|
327
245
|
end
|
|
328
246
|
end
|
|
329
247
|
|
|
330
|
-
# Extract site domain from Jekyll configuration
|
|
331
248
|
def extract_site_domain
|
|
332
249
|
site_url = @config['url'] || @config['baseurl'] || ''
|
|
333
250
|
extract_domain(site_url)
|
|
334
251
|
end
|
|
335
252
|
|
|
336
|
-
#
|
|
337
|
-
#
|
|
253
|
+
# Injects a permalink anchor into each heading (h1-h6). The anchor's
|
|
254
|
+
# glyph defaults to a plain "#"; with_heading_anchor_data can swap it
|
|
255
|
+
# for a Font Awesome icon or any other class list.
|
|
338
256
|
def add_heading_anchors(doc)
|
|
339
257
|
enabled = @config.key?('with_heading_anchor') ? @config['with_heading_anchor'] : DEFAULT_HEADING_ANCHOR
|
|
340
258
|
return unless enabled
|
|
@@ -351,7 +269,7 @@ module Jekyll
|
|
|
351
269
|
id = resolve_heading_id(heading, id_counts)
|
|
352
270
|
|
|
353
271
|
anchor = Nokogiri::XML::Node.new('a', doc)
|
|
354
|
-
anchor['class'] = 'heading-anchor mx-1'
|
|
272
|
+
anchor['class'] = 'heading-anchor p-1 mx-1 focus-ring'
|
|
355
273
|
anchor['href'] = "##{id}"
|
|
356
274
|
anchor['data-copy-anchor'] = "##{id}"
|
|
357
275
|
anchor['aria-label'] = "Link to #{heading.text.strip}"
|
|
@@ -365,8 +283,9 @@ module Jekyll
|
|
|
365
283
|
end
|
|
366
284
|
end
|
|
367
285
|
|
|
368
|
-
#
|
|
369
|
-
# id_counts tracks
|
|
286
|
+
# Returns the heading's existing id, or generates one from its text and
|
|
287
|
+
# assigns it. id_counts tracks slugs already used, so repeated headings
|
|
288
|
+
# get unique "-1", "-2", ... suffixes.
|
|
370
289
|
def resolve_heading_id(heading, id_counts)
|
|
371
290
|
existing = heading['id'].to_s.strip
|
|
372
291
|
return existing unless existing.empty?
|
|
@@ -379,8 +298,10 @@ module Jekyll
|
|
|
379
298
|
final_id
|
|
380
299
|
end
|
|
381
300
|
|
|
382
|
-
#
|
|
383
|
-
#
|
|
301
|
+
# Converts heading text to a URL-safe slug, loosely mirroring the slug
|
|
302
|
+
# rules in jekyll-theme-centos-base's link.html.liquid (same characters
|
|
303
|
+
# stripped and replaced), plus collapsing repeated/edge hyphens so
|
|
304
|
+
# headings with unusual punctuation still get a clean id.
|
|
384
305
|
def slugify(text)
|
|
385
306
|
text.strip
|
|
386
307
|
.downcase
|
|
@@ -392,7 +313,9 @@ module Jekyll
|
|
|
392
313
|
|
|
393
314
|
private :add_heading_anchors, :resolve_heading_id, :slugify
|
|
394
315
|
|
|
395
|
-
#
|
|
316
|
+
# Appends the external-link icon to a cross-domain link, unless the
|
|
317
|
+
# icon is disabled, the link already has an icon-bearing tag excluded
|
|
318
|
+
# via external_link_icon_excluded_tags, or one is already present.
|
|
396
319
|
def add_external_icon(link, site_domain, config, doc)
|
|
397
320
|
external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON)
|
|
398
321
|
|