jekyll-link-decorator 1.5.2 → 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 +59 -131
- 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-*`, or `link-opacity-*` 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"
|
|
70
|
-
# alert_link_classes: "alert-link"
|
|
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'
|
|
@@ -97,8 +40,8 @@ module Jekyll
|
|
|
97
40
|
# external-link icons for cross-domain links.
|
|
98
41
|
class LinkDecorator < Jekyll::Converters::Markdown
|
|
99
42
|
DEFAULT_LINK_CLASSES = 'link link-offset-3 link-offset-3-hover link-underline-primary ' \
|
|
100
|
-
'link-underline-opacity-0 link-underline-opacity-100-hover'
|
|
101
|
-
DEFAULT_ALERT_LINK_CLASSES = 'alert-link'
|
|
43
|
+
'link-underline-opacity-0 link-underline-opacity-100-hover focus-ring'
|
|
44
|
+
DEFAULT_ALERT_LINK_CLASSES = 'alert-link focus-ring'
|
|
102
45
|
|
|
103
46
|
# Bootstrap 5 link-utility "families" whose defaults must not silently
|
|
104
47
|
# override an author's own explicit class from the same family — see
|
|
@@ -111,9 +54,9 @@ module Jekyll
|
|
|
111
54
|
#
|
|
112
55
|
# Enumerated families (offset, both opacity families) lock to Bootstrap's
|
|
113
56
|
# fixed value set, so an out-of-range value like "link-opacity-42" isn't
|
|
114
|
-
# recognized as an override either. Color
|
|
115
|
-
# link-underline-{color}
|
|
116
|
-
# define arbitrary custom color names.
|
|
57
|
+
# recognized as an override either. Color and focus-ring families
|
|
58
|
+
# (link-{color}, link-underline-{color}, focus-ring(-{color})?) stay
|
|
59
|
+
# generic (\S+) since Bootstrap themes can define arbitrary custom color names.
|
|
117
60
|
LINK_CLASS_FAMILIES = [
|
|
118
61
|
{ # link-{color}, e.g. link-primary, link-body-emphasis (bare colored-link class;
|
|
119
62
|
# excludes link-underline-*, link-offset-*, and link-opacity-* families)
|
|
@@ -131,37 +74,34 @@ module Jekyll
|
|
|
131
74
|
{ # link-opacity-{10,25,50,75,100}, each with an optional -hover variant
|
|
132
75
|
# (controls link-text opacity, distinct from link-underline-opacity-*)
|
|
133
76
|
pattern: /\Alink-opacity-(?:10|25|50|75|100)(-hover)?\z/
|
|
77
|
+
},
|
|
78
|
+
{ # focus-ring and focus-ring-{color}, e.g. focus-ring-primary, focus-ring-success
|
|
79
|
+
# Bootstrap 5.3 focus-ring has no hover, offset, or opacity variants — only bare
|
|
80
|
+
# and color forms; color stays generic (\S+) for custom theme colors
|
|
81
|
+
pattern: /\Afocus-ring(-\S+)?\z/
|
|
134
82
|
}
|
|
135
83
|
].freeze
|
|
136
84
|
DEFAULT_EXTERNAL_LINK_ICON = true
|
|
137
85
|
DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS = [].freeze
|
|
138
86
|
DEFAULT_HEADING_ANCHOR = false
|
|
139
|
-
DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash
|
|
87
|
+
DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash'
|
|
140
88
|
|
|
141
89
|
def self.name
|
|
142
90
|
'LinkDecorator'
|
|
143
91
|
end
|
|
144
92
|
|
|
145
|
-
#
|
|
146
|
-
#
|
|
93
|
+
# Returns a URL's domain, stripped of scheme and path.
|
|
94
|
+
# "https://example.com/path?query=1#anchor" -> "example.com"
|
|
147
95
|
def extract_domain(url)
|
|
148
96
|
return '' if url.nil? || url.empty?
|
|
149
97
|
|
|
150
|
-
# Remove protocol (http:// or https://)
|
|
151
98
|
domain = url.sub(%r{^https?://}, '')
|
|
152
|
-
|
|
153
|
-
# Extract just the domain part (before /, ?, or #)
|
|
154
99
|
domain = domain.split(%r{[/?#]}).first
|
|
155
|
-
|
|
156
|
-
# Convert to lowercase for case-insensitive comparison
|
|
157
100
|
domain.downcase
|
|
158
101
|
end
|
|
159
102
|
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
# - Link starts with http:// or https://
|
|
163
|
-
# - Link domain is different from site.url domain
|
|
164
|
-
# - 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.
|
|
165
105
|
def is_external_different_domain_link?(link, site_domain)
|
|
166
106
|
return false unless absolute_different_domain?(link, site_domain)
|
|
167
107
|
return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? }
|
|
@@ -169,8 +109,9 @@ module Jekyll
|
|
|
169
109
|
true
|
|
170
110
|
end
|
|
171
111
|
|
|
172
|
-
#
|
|
173
|
-
#
|
|
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.
|
|
174
115
|
def absolute_different_domain?(link, site_domain)
|
|
175
116
|
href = link['href'].to_s
|
|
176
117
|
return false unless %r{^https?://}.match?(href)
|
|
@@ -182,51 +123,34 @@ module Jekyll
|
|
|
182
123
|
end
|
|
183
124
|
private :absolute_different_domain?
|
|
184
125
|
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
# - Link is absolute (starts with http:// or https://)
|
|
188
|
-
# - Link domain is different from site.url domain
|
|
189
|
-
# Returns false if:
|
|
190
|
-
# - Link is relative (starts with /, ../)
|
|
191
|
-
# - 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.
|
|
192
128
|
def cross_domain_external_link?(link, site_domain)
|
|
193
129
|
href = link['href'].to_s
|
|
194
|
-
|
|
195
|
-
# Relative URLs (starting with / or ../) are internal links
|
|
196
130
|
return false if %r{^(/|\.\.)}.match?(href)
|
|
197
131
|
|
|
198
132
|
absolute_different_domain?(link, site_domain)
|
|
199
133
|
end
|
|
200
134
|
|
|
201
|
-
#
|
|
202
|
-
# Only adds if target is not already set
|
|
135
|
+
# Sets target="_blank" on a cross-domain link, unless it already has a target.
|
|
203
136
|
def add_external_target_blank(link, site_domain)
|
|
204
|
-
# Skip if link already has a target attribute
|
|
205
137
|
return if link['target'].to_s.strip != ''
|
|
206
|
-
|
|
207
|
-
# Add target="_blank" if this is a cross-domain external link
|
|
208
138
|
return unless cross_domain_external_link?(link, site_domain)
|
|
209
139
|
|
|
210
140
|
link['target'] = '_blank'
|
|
211
141
|
end
|
|
212
142
|
|
|
213
|
-
#
|
|
214
|
-
#
|
|
143
|
+
# Adds rel="noopener noreferrer" to a target="_blank" link, keeping any
|
|
144
|
+
# rel values the author already set.
|
|
215
145
|
def add_external_target_protection(link)
|
|
216
146
|
target = link['target'].to_s
|
|
217
|
-
|
|
218
|
-
# Only add protection for target="_blank"
|
|
219
147
|
return unless target == '_blank'
|
|
220
148
|
|
|
221
|
-
# Get existing rel attribute (if any)
|
|
222
149
|
existing_rel = link['rel'].to_s.strip
|
|
223
150
|
rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/)
|
|
224
|
-
|
|
225
|
-
# Add required security values if not already present
|
|
226
151
|
rel_values << 'noopener' unless rel_values.include?('noopener')
|
|
227
152
|
rel_values << 'noreferrer' unless rel_values.include?('noreferrer')
|
|
228
153
|
|
|
229
|
-
# Set the updated rel attribute
|
|
230
154
|
link['rel'] = rel_values.join(' ')
|
|
231
155
|
end
|
|
232
156
|
|
|
@@ -264,8 +188,8 @@ module Jekyll
|
|
|
264
188
|
html
|
|
265
189
|
end
|
|
266
190
|
|
|
267
|
-
#
|
|
268
|
-
#
|
|
191
|
+
# Logs a notice when disabled; returns the given enabled flag unchanged.
|
|
192
|
+
# The caller (decorate_html) decides what "enabled" defaults to.
|
|
269
193
|
def plugin_enabled?(enabled)
|
|
270
194
|
unless enabled
|
|
271
195
|
Jekyll.logger.info 'LinkDecorator:',
|
|
@@ -274,17 +198,16 @@ module Jekyll
|
|
|
274
198
|
enabled
|
|
275
199
|
end
|
|
276
200
|
|
|
277
|
-
#
|
|
201
|
+
# Applies alert_classes to links inside an .alert box, default_classes
|
|
202
|
+
# to every other link.
|
|
278
203
|
def apply_link_styles(doc, config)
|
|
279
204
|
default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES)
|
|
280
205
|
alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES)
|
|
281
206
|
|
|
282
|
-
# Apply alert classes to links inside p.alert or blockquote.alert
|
|
283
207
|
doc.css('p.alert a:not(.btn), blockquote.alert a:not(.btn)').each do |link|
|
|
284
208
|
add_classes(link, alert_classes)
|
|
285
209
|
end
|
|
286
210
|
|
|
287
|
-
# Apply default classes to all other non-alert links
|
|
288
211
|
doc.css('a:not(.btn)').each do |link|
|
|
289
212
|
next if link.ancestors('p.alert, blockquote.alert').any?
|
|
290
213
|
|
|
@@ -305,12 +228,12 @@ module Jekyll
|
|
|
305
228
|
end
|
|
306
229
|
end
|
|
307
230
|
|
|
308
|
-
# Add CSS classes to a link element
|
|
309
231
|
def add_classes(link, classes_string)
|
|
310
232
|
classes_string.split.each { |cls| link.add_class(cls) }
|
|
311
233
|
end
|
|
312
234
|
|
|
313
|
-
#
|
|
235
|
+
# Adds target="_blank", the external-link icon, and rel security
|
|
236
|
+
# attributes to every link on the page, in that order.
|
|
314
237
|
def add_external_link_features(doc, config)
|
|
315
238
|
site_domain = extract_site_domain
|
|
316
239
|
return if site_domain.empty?
|
|
@@ -322,14 +245,14 @@ module Jekyll
|
|
|
322
245
|
end
|
|
323
246
|
end
|
|
324
247
|
|
|
325
|
-
# Extract site domain from Jekyll configuration
|
|
326
248
|
def extract_site_domain
|
|
327
249
|
site_url = @config['url'] || @config['baseurl'] || ''
|
|
328
250
|
extract_domain(site_url)
|
|
329
251
|
end
|
|
330
252
|
|
|
331
|
-
#
|
|
332
|
-
#
|
|
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.
|
|
333
256
|
def add_heading_anchors(doc)
|
|
334
257
|
enabled = @config.key?('with_heading_anchor') ? @config['with_heading_anchor'] : DEFAULT_HEADING_ANCHOR
|
|
335
258
|
return unless enabled
|
|
@@ -346,7 +269,7 @@ module Jekyll
|
|
|
346
269
|
id = resolve_heading_id(heading, id_counts)
|
|
347
270
|
|
|
348
271
|
anchor = Nokogiri::XML::Node.new('a', doc)
|
|
349
|
-
anchor['class'] = 'heading-anchor mx-1'
|
|
272
|
+
anchor['class'] = 'heading-anchor p-1 mx-1 focus-ring'
|
|
350
273
|
anchor['href'] = "##{id}"
|
|
351
274
|
anchor['data-copy-anchor'] = "##{id}"
|
|
352
275
|
anchor['aria-label'] = "Link to #{heading.text.strip}"
|
|
@@ -360,8 +283,9 @@ module Jekyll
|
|
|
360
283
|
end
|
|
361
284
|
end
|
|
362
285
|
|
|
363
|
-
#
|
|
364
|
-
# 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.
|
|
365
289
|
def resolve_heading_id(heading, id_counts)
|
|
366
290
|
existing = heading['id'].to_s.strip
|
|
367
291
|
return existing unless existing.empty?
|
|
@@ -374,8 +298,10 @@ module Jekyll
|
|
|
374
298
|
final_id
|
|
375
299
|
end
|
|
376
300
|
|
|
377
|
-
#
|
|
378
|
-
#
|
|
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.
|
|
379
305
|
def slugify(text)
|
|
380
306
|
text.strip
|
|
381
307
|
.downcase
|
|
@@ -387,7 +313,9 @@ module Jekyll
|
|
|
387
313
|
|
|
388
314
|
private :add_heading_anchors, :resolve_heading_id, :slugify
|
|
389
315
|
|
|
390
|
-
#
|
|
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.
|
|
391
319
|
def add_external_icon(link, site_domain, config, doc)
|
|
392
320
|
external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON)
|
|
393
321
|
|