-
Notifications
You must be signed in to change notification settings - Fork 19
/
jekyll-target-blank.rb
336 lines (295 loc) · 10.1 KB
/
jekyll-target-blank.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# frozen_string_literal: true
require "jekyll"
require "nokogiri"
require "uri"
module Jekyll
class TargetBlank
BODY_START_TAG = "<body"
OPENING_BODY_TAG_REGEX = %r!<body([^<>]*)>\s*!.freeze
class << self
# Public: Processes the content and updated the external links
# by adding target="_blank" and rel="noopener noreferrer" attributes.
#
# content - the document or page to be processes.
def process(content)
@site_url = content.site.config["url"]
@config = content.site.config
@target_blank_config = class_config
@requires_specified_css_class = false
@required_css_class_name = nil
@should_add_css_classes = false
@css_classes_to_add = nil
@should_add_noopener = true
@should_add_noreferrrer = true
@should_add_extra_rel_attribute_values = false
@extra_rel_attribute_values = nil
return unless content.output.include?("<a")
initialise
content.output = if content.output.include? BODY_START_TAG
process_html(content)
else
process_anchor_tags(content.output)
end
end
# Public: Determines if the document should be processed.
#
# doc - the document being processed.
def document_processable?(doc)
(doc.is_a?(Jekyll::Page) || doc.write?) &&
doc.output_ext == ".html" || (doc.permalink&.end_with?("/"))
end
private
def initialise
requires_css_class_name
configure_adding_additional_css_classes
add_default_rel_attributes?
add_extra_rel_attributes?
end
# Private: Processes html content which has a body opening tag.
#
# content - html to be processes.
def process_html(content)
head, opener, tail = content.output.partition(OPENING_BODY_TAG_REGEX)
body_content, *rest = tail.partition("</body>")
processed_markup = process_anchor_tags(body_content)
content.output = String.new(head) << opener << processed_markup << rest.join
end
# Private: Processes the anchor tags and adds the target
# attribute if the link is external and depending on the config settings.
#
# html = the html which includes the anchor tags.
def process_anchor_tags(html)
content = Nokogiri::HTML::DocumentFragment.parse(html)
anchors = content.css("a[href]")
anchors.each do |item|
if processable_link?(item)
add_target_blank_attribute(item)
add_rel_attributes(item)
add_css_classes_if_required(item)
end
next
end
content.to_html
end
# Private: Determines of the link should be processed.
#
# link = Nokogiri node.
def processable_link?(link)
if not_mailto_link?(link["href"]) && external?(link["href"])
if @requires_specified_css_class
return false unless includes_specified_css_class?(link)
end
true
end
end
# Private: Handles adding the target attribute of the config
# requires a specifies class.
def requires_css_class_name
if css_class_name_specified_in_config?
@requires_specified_css_class = true
@required_css_class_name = specified_class_name_from_config
end
end
# Private: Configures any additional CSS classes
# if needed.
def configure_adding_additional_css_classes
if should_add_css_classes?
@should_add_css_classes = true
@css_classes_to_add = css_classes_to_add_from_config.to_s
end
end
# Private: Handles the default rel attribute values
def add_default_rel_attributes?
@should_add_noopener = false if should_not_include_noopener?
@should_add_noreferrrer = false if should_not_include_noreferrer?
end
# Private: Sets any extra rel attribute values
# if required.
def add_extra_rel_attributes?
if should_add_extra_rel_attribute_values?
@should_add_extra_rel_attribute_values = true
@extra_rel_attribute_values = extra_rel_attribute_values_to_add
end
end
# Private: adds the cs classes if set in config.
#
# link = Nokogiri node.
def add_css_classes_if_required(link)
if @should_add_css_classes
existing_classes = get_existing_css_classes(link)
existing_classes = " " + existing_classes unless existing_classes.to_s.empty?
link["class"] = @css_classes_to_add + existing_classes
end
end
# Private: Adds a target="_blank" to the link.
#
# link = Nokogiri node.
def add_target_blank_attribute(link)
link["target"] = "_blank"
end
# Private: Adds the rel attribute and values to the link.
#
# link = Nokogiri node.
def add_rel_attributes(link)
rel = link["rel"] || ""
rel = add_noopener_to_rel(rel)
if @should_add_noreferrrer
rel += " " unless rel.empty?
rel += "noreferrer"
end
if @should_add_extra_rel_attribute_values
rel += " " unless rel.empty?
rel += @extra_rel_attribute_values
end
link["rel"] = rel unless rel.empty?
end
# Private: Adds noopener attribute.
#
# rel = string
def add_noopener_to_rel(rel)
if @should_add_noopener
rel += " " unless rel.empty?
rel += "noopener"
end
rel
end
# Private: Checks if the link is a mailto url.
#
# link - a url.
def not_mailto_link?(link)
true unless link.to_s.start_with?("mailto:")
end
# Private: Checks if the links points to a host
# other than that set in Jekyll's configuration.
#
# link - a url.
def external?(link)
if link&.match?(URI.regexp(%w(http https)))
URI.parse(link).host != URI.parse(@site_url).host
end
end
# Private: Checks if a css class name is specified in config
def css_class_name_specified_in_config?
target_blank_config = @target_blank_config
case target_blank_config
when nil, NilClass
false
else
target_blank_config.fetch("css_class", false)
end
end
# Private: Checks if the link contains the same css class name
# as specified in config.
#
# link - the url under test.
def includes_specified_css_class?(link)
link_classes = get_existing_css_classes(link)
if link_classes
link_classes = link_classes.split(" ")
contained = false
link_classes.each do |name|
contained = true unless name != @required_css_class_name
end
return contained
end
false
end
# Private: Gets the the css classes of the link.
#
# link - an anchor tag.
def get_existing_css_classes(link)
link["class"].to_s
end
# Private: Checks if the link contains the class attribute.
#
# link - an anchor tag.
def link_has_class_attribute?(link)
link.include?("class=")
end
# Private: Fetches the specified css class name
# from config.
def specified_class_name_from_config
target_blank_config = @target_blank_config
target_blank_config.fetch("css_class")
end
# Private: Checks if it should add additional CSS classes.
def should_add_css_classes?
config = @target_blank_config
case config
when nil, NilClass
false
else
config.fetch("add_css_classes", false)
end
end
# Private: Checks if any addional rel attribute values
# should be added.
def should_add_extra_rel_attribute_values?
config = @target_blank_config
case config
when nil, NilClass
false
else
config.fetch("rel", false)
end
end
# Private: Gets any additional rel attribute values
# values to add from config.
def extra_rel_attribute_values_to_add
config = @target_blank_config
config.fetch("rel")
end
# Private: Gets the CSS classes to be added to the link from
# config.
def css_classes_to_add_from_config
config = @target_blank_config
config.fetch("add_css_classes")
end
# Private: Determines if the noopener rel attribute value should be added
# based on the specified config values.
#
# Returns true if noopener is false in config.
def should_not_include_noopener?
config = @target_blank_config
case config
when nil, NilClass
false
else
noopener = config.fetch("noopener", true)
if noopener == false
return true
else
return false
end
end
end
# Private: Determines if the noreferrer rel attribute value should be added
# based on the specified config values.
#
# Returns true if noreferrer is false in config.
def should_not_include_noreferrer?
config = @target_blank_config
case config
when nil, NilClass
false
else
noreferrer = config.fetch("noreferrer", true)
if noreferrer == false
return true
else
return false
end
end
end
# Private: Gets the relative config values
# if they exist.
def class_config
@target_blank_config = @config.fetch("target-blank", nil)
end
end
end
end
# Hooks into Jekyll's post_render event.
Jekyll::Hooks.register [:pages, :documents], :post_render do |doc|
Jekyll::TargetBlank.process(doc) if Jekyll::TargetBlank.document_processable?(doc)
end