jekyll-link-decorator 1.6.0 → 1.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 +4 -4
- data/lib/jekyll-link-decorator/version.rb +1 -1
- data/lib/jekyll-link-decorator.rb +53 -129
- 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: 0a4872962803af2990a40727305d6c2addb9ea7d3df92d614d00371a0aaf77ad
|
|
4
|
+
data.tar.gz: 47250445c0c9163963082deb6ee986022dc44e4db2ecd79d2b8367c1ecc3000b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8328e80c3b9ba63802c5aef9133054731c19112e078ced348ff5f30fa8da47c6c02531d4bb140bd016ae7e9c376921a00561df16eaa2695a3f9cb000496d5b5
|
|
7
|
+
data.tar.gz: ef76d4e238557f5ad72114c1701519f1323af626149f8730bc873f9d6213f3a8c5624afd29357dc021aa5c507db29a80d7f56e3d0b93e29f114b8431accd9b0a
|
|
@@ -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'
|
|
@@ -96,7 +39,7 @@ module Jekyll
|
|
|
96
39
|
# box) via Nokogiri CSS selectors, excluding buttons. Also adds
|
|
97
40
|
# external-link icons for cross-domain links.
|
|
98
41
|
class LinkDecorator < Jekyll::Converters::Markdown
|
|
99
|
-
DEFAULT_LINK_CLASSES = 'link link-offset-
|
|
42
|
+
DEFAULT_LINK_CLASSES = 'link link-offset-2 link-offset-2-hover link-underline ' \
|
|
100
43
|
'link-underline-opacity-0 link-underline-opacity-100-hover focus-ring'
|
|
101
44
|
DEFAULT_ALERT_LINK_CLASSES = 'alert-link focus-ring'
|
|
102
45
|
|
|
@@ -119,8 +62,9 @@ module Jekyll
|
|
|
119
62
|
# excludes link-underline-*, link-offset-*, and link-opacity-* families)
|
|
120
63
|
pattern: /\Alink-(?!underline\b|offset-|opacity-)\S+\z/
|
|
121
64
|
},
|
|
122
|
-
{ # link-underline-{color}, e.g. link-underline-success
|
|
123
|
-
|
|
65
|
+
{ # link-underline and link-underline-{color}, e.g. link-underline-success
|
|
66
|
+
# (excludes -opacity-*); the bare class takes the link's own text color
|
|
67
|
+
pattern: /\Alink-underline(?:-(?!opacity)\S+)?\z/
|
|
124
68
|
},
|
|
125
69
|
{ # link-offset-{1,2,3}, each with an optional -hover variant
|
|
126
70
|
pattern: /\Alink-offset-[123](-hover)?\z/
|
|
@@ -141,32 +85,24 @@ module Jekyll
|
|
|
141
85
|
DEFAULT_EXTERNAL_LINK_ICON = true
|
|
142
86
|
DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS = [].freeze
|
|
143
87
|
DEFAULT_HEADING_ANCHOR = false
|
|
144
|
-
DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash
|
|
88
|
+
DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash'
|
|
145
89
|
|
|
146
90
|
def self.name
|
|
147
91
|
'LinkDecorator'
|
|
148
92
|
end
|
|
149
93
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
94
|
+
# Returns a URL's domain, stripped of scheme and path.
|
|
95
|
+
# "https://example.com/path?query=1#anchor" -> "example.com"
|
|
152
96
|
def extract_domain(url)
|
|
153
97
|
return '' if url.nil? || url.empty?
|
|
154
98
|
|
|
155
|
-
# Remove protocol (http:// or https://)
|
|
156
99
|
domain = url.sub(%r{^https?://}, '')
|
|
157
|
-
|
|
158
|
-
# Extract just the domain part (before /, ?, or #)
|
|
159
100
|
domain = domain.split(%r{[/?#]}).first
|
|
160
|
-
|
|
161
|
-
# Convert to lowercase for case-insensitive comparison
|
|
162
101
|
domain.downcase
|
|
163
102
|
end
|
|
164
103
|
|
|
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
|
|
104
|
+
# True when this link needs the external-link icon: an absolute,
|
|
105
|
+
# cross-domain link that doesn't already have one.
|
|
170
106
|
def is_external_different_domain_link?(link, site_domain)
|
|
171
107
|
return false unless absolute_different_domain?(link, site_domain)
|
|
172
108
|
return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? }
|
|
@@ -174,8 +110,9 @@ module Jekyll
|
|
|
174
110
|
true
|
|
175
111
|
end
|
|
176
112
|
|
|
177
|
-
#
|
|
178
|
-
#
|
|
113
|
+
# True when href is an absolute http(s) URL on a domain other than
|
|
114
|
+
# site_domain. Shared by is_external_different_domain_link? and
|
|
115
|
+
# cross_domain_external_link? below.
|
|
179
116
|
def absolute_different_domain?(link, site_domain)
|
|
180
117
|
href = link['href'].to_s
|
|
181
118
|
return false unless %r{^https?://}.match?(href)
|
|
@@ -187,51 +124,34 @@ module Jekyll
|
|
|
187
124
|
end
|
|
188
125
|
private :absolute_different_domain?
|
|
189
126
|
|
|
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
|
|
127
|
+
# True when this link needs target="_blank": absolute and cross-domain.
|
|
128
|
+
# Relative URLs (/, ../) are always treated as internal.
|
|
197
129
|
def cross_domain_external_link?(link, site_domain)
|
|
198
130
|
href = link['href'].to_s
|
|
199
|
-
|
|
200
|
-
# Relative URLs (starting with / or ../) are internal links
|
|
201
131
|
return false if %r{^(/|\.\.)}.match?(href)
|
|
202
132
|
|
|
203
133
|
absolute_different_domain?(link, site_domain)
|
|
204
134
|
end
|
|
205
135
|
|
|
206
|
-
#
|
|
207
|
-
# Only adds if target is not already set
|
|
136
|
+
# Sets target="_blank" on a cross-domain link, unless it already has a target.
|
|
208
137
|
def add_external_target_blank(link, site_domain)
|
|
209
|
-
# Skip if link already has a target attribute
|
|
210
138
|
return if link['target'].to_s.strip != ''
|
|
211
|
-
|
|
212
|
-
# Add target="_blank" if this is a cross-domain external link
|
|
213
139
|
return unless cross_domain_external_link?(link, site_domain)
|
|
214
140
|
|
|
215
141
|
link['target'] = '_blank'
|
|
216
142
|
end
|
|
217
143
|
|
|
218
|
-
#
|
|
219
|
-
#
|
|
144
|
+
# Adds rel="noopener noreferrer" to a target="_blank" link, keeping any
|
|
145
|
+
# rel values the author already set.
|
|
220
146
|
def add_external_target_protection(link)
|
|
221
147
|
target = link['target'].to_s
|
|
222
|
-
|
|
223
|
-
# Only add protection for target="_blank"
|
|
224
148
|
return unless target == '_blank'
|
|
225
149
|
|
|
226
|
-
# Get existing rel attribute (if any)
|
|
227
150
|
existing_rel = link['rel'].to_s.strip
|
|
228
151
|
rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/)
|
|
229
|
-
|
|
230
|
-
# Add required security values if not already present
|
|
231
152
|
rel_values << 'noopener' unless rel_values.include?('noopener')
|
|
232
153
|
rel_values << 'noreferrer' unless rel_values.include?('noreferrer')
|
|
233
154
|
|
|
234
|
-
# Set the updated rel attribute
|
|
235
155
|
link['rel'] = rel_values.join(' ')
|
|
236
156
|
end
|
|
237
157
|
|
|
@@ -269,8 +189,8 @@ module Jekyll
|
|
|
269
189
|
html
|
|
270
190
|
end
|
|
271
191
|
|
|
272
|
-
#
|
|
273
|
-
#
|
|
192
|
+
# Logs a notice when disabled; returns the given enabled flag unchanged.
|
|
193
|
+
# The caller (decorate_html) decides what "enabled" defaults to.
|
|
274
194
|
def plugin_enabled?(enabled)
|
|
275
195
|
unless enabled
|
|
276
196
|
Jekyll.logger.info 'LinkDecorator:',
|
|
@@ -279,17 +199,16 @@ module Jekyll
|
|
|
279
199
|
enabled
|
|
280
200
|
end
|
|
281
201
|
|
|
282
|
-
#
|
|
202
|
+
# Applies alert_classes to links inside an .alert box, default_classes
|
|
203
|
+
# to every other link.
|
|
283
204
|
def apply_link_styles(doc, config)
|
|
284
205
|
default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES)
|
|
285
206
|
alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES)
|
|
286
207
|
|
|
287
|
-
# Apply alert classes to links inside p.alert or blockquote.alert
|
|
288
208
|
doc.css('p.alert a:not(.btn), blockquote.alert a:not(.btn)').each do |link|
|
|
289
209
|
add_classes(link, alert_classes)
|
|
290
210
|
end
|
|
291
211
|
|
|
292
|
-
# Apply default classes to all other non-alert links
|
|
293
212
|
doc.css('a:not(.btn)').each do |link|
|
|
294
213
|
next if link.ancestors('p.alert, blockquote.alert').any?
|
|
295
214
|
|
|
@@ -310,12 +229,12 @@ module Jekyll
|
|
|
310
229
|
end
|
|
311
230
|
end
|
|
312
231
|
|
|
313
|
-
# Add CSS classes to a link element
|
|
314
232
|
def add_classes(link, classes_string)
|
|
315
233
|
classes_string.split.each { |cls| link.add_class(cls) }
|
|
316
234
|
end
|
|
317
235
|
|
|
318
|
-
#
|
|
236
|
+
# Adds target="_blank", the external-link icon, and rel security
|
|
237
|
+
# attributes to every link on the page, in that order.
|
|
319
238
|
def add_external_link_features(doc, config)
|
|
320
239
|
site_domain = extract_site_domain
|
|
321
240
|
return if site_domain.empty?
|
|
@@ -327,14 +246,14 @@ module Jekyll
|
|
|
327
246
|
end
|
|
328
247
|
end
|
|
329
248
|
|
|
330
|
-
# Extract site domain from Jekyll configuration
|
|
331
249
|
def extract_site_domain
|
|
332
250
|
site_url = @config['url'] || @config['baseurl'] || ''
|
|
333
251
|
extract_domain(site_url)
|
|
334
252
|
end
|
|
335
253
|
|
|
336
|
-
#
|
|
337
|
-
#
|
|
254
|
+
# Injects a permalink anchor into each heading (h1-h6). The anchor's
|
|
255
|
+
# glyph defaults to a plain "#"; with_heading_anchor_data can swap it
|
|
256
|
+
# for a Font Awesome icon or any other class list.
|
|
338
257
|
def add_heading_anchors(doc)
|
|
339
258
|
enabled = @config.key?('with_heading_anchor') ? @config['with_heading_anchor'] : DEFAULT_HEADING_ANCHOR
|
|
340
259
|
return unless enabled
|
|
@@ -351,7 +270,7 @@ module Jekyll
|
|
|
351
270
|
id = resolve_heading_id(heading, id_counts)
|
|
352
271
|
|
|
353
272
|
anchor = Nokogiri::XML::Node.new('a', doc)
|
|
354
|
-
anchor['class'] = 'heading-anchor mx-1'
|
|
273
|
+
anchor['class'] = 'heading-anchor p-1 mx-1 focus-ring'
|
|
355
274
|
anchor['href'] = "##{id}"
|
|
356
275
|
anchor['data-copy-anchor'] = "##{id}"
|
|
357
276
|
anchor['aria-label'] = "Link to #{heading.text.strip}"
|
|
@@ -365,8 +284,9 @@ module Jekyll
|
|
|
365
284
|
end
|
|
366
285
|
end
|
|
367
286
|
|
|
368
|
-
#
|
|
369
|
-
# id_counts tracks
|
|
287
|
+
# Returns the heading's existing id, or generates one from its text and
|
|
288
|
+
# assigns it. id_counts tracks slugs already used, so repeated headings
|
|
289
|
+
# get unique "-1", "-2", ... suffixes.
|
|
370
290
|
def resolve_heading_id(heading, id_counts)
|
|
371
291
|
existing = heading['id'].to_s.strip
|
|
372
292
|
return existing unless existing.empty?
|
|
@@ -379,8 +299,10 @@ module Jekyll
|
|
|
379
299
|
final_id
|
|
380
300
|
end
|
|
381
301
|
|
|
382
|
-
#
|
|
383
|
-
#
|
|
302
|
+
# Converts heading text to a URL-safe slug, loosely mirroring the slug
|
|
303
|
+
# rules in jekyll-theme-centos-base's link.html.liquid (same characters
|
|
304
|
+
# stripped and replaced), plus collapsing repeated/edge hyphens so
|
|
305
|
+
# headings with unusual punctuation still get a clean id.
|
|
384
306
|
def slugify(text)
|
|
385
307
|
text.strip
|
|
386
308
|
.downcase
|
|
@@ -392,7 +314,9 @@ module Jekyll
|
|
|
392
314
|
|
|
393
315
|
private :add_heading_anchors, :resolve_heading_id, :slugify
|
|
394
316
|
|
|
395
|
-
#
|
|
317
|
+
# Appends the external-link icon to a cross-domain link, unless the
|
|
318
|
+
# icon is disabled, the link already has an icon-bearing tag excluded
|
|
319
|
+
# via external_link_icon_excluded_tags, or one is already present.
|
|
396
320
|
def add_external_icon(link, site_domain, config, doc)
|
|
397
321
|
external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON)
|
|
398
322
|
|