jekyll_href 1.0.12 → 1.0.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33ad179cd8d6770d3828246bdc51cdbe398f7fd3f50b0101481e2dacd597191e
4
- data.tar.gz: 543a944799bdb811fd84f3afb4ce0b8239e2008208feb0612360307bd1843c69
3
+ metadata.gz: e77bc04770e6fae8eeb1a595be3e3faf81fe6c77c023a9a711a3a97d51b3a828
4
+ data.tar.gz: c9ff34ea2c4611fefb8f36e99471ad55a490b60d68c255fbcc06ce3791acc90e
5
5
  SHA512:
6
- metadata.gz: be0fd539b076cedb893cee10a528f4a3c71d2ec01e84c16a26455ec4c4aacded321bc11f96ac8ff5d6647b2cdf3692aba9bb843e419d67c54aab9a47edb63ff9
7
- data.tar.gz: f2d3abce3b08922eefa404905d1f61f319150662f891dd03171e716479d16e068e4e2a02b9975b5f609716f942876ee9b7d542d14236dba141fb4fcaa6e3ce66
6
+ metadata.gz: 9727eed971f210d18ab6826a297b5563bf899ab1c14bbd9a988fa31405d1521357ca6fa48894ab3dfacfda11371f025c83908ed5ef1519ce7fb04ba9f25605d9
7
+ data.tar.gz: e82fbe117198467601a940fbc24fd9a2a40ec802b263e43324cfaaca7467f8b704f16ad1dc6214a02154ba88f04006de7f9830c08a2857c49a8614515f016f4e
data/.rubocop.yml CHANGED
@@ -1,6 +1,6 @@
1
- require: rubocop-jekyll
2
- inherit_gem:
3
- rubocop-jekyll: .rubocop.yml
1
+ #require: rubocop-jekyll
2
+ #inherit_gem:
3
+ # rubocop-jekyll: .rubocop.yml
4
4
 
5
5
  AllCops:
6
6
  Exclude:
@@ -9,8 +9,11 @@ AllCops:
9
9
  NewCops: enable
10
10
  TargetRubyVersion: 2.6
11
11
 
12
- Gemspec/RequireMFA:
13
- Enabled: false
12
+ # Gemspec/RequireMFA:
13
+ # Enabled: false
14
+
15
+ Layout/HashAlignment:
16
+ EnforcedHashRocketStyle: table
14
17
 
15
18
  Layout/LineLength:
16
19
  Max: 150
@@ -32,3 +35,6 @@ Style/StringLiterals:
32
35
 
33
36
  Style/StringLiteralsInInterpolation:
34
37
  Enabled: false
38
+
39
+ Style/TrailingCommaInHashLiteral:
40
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 1.0.14 / 2023-01-09
2
+ * Added `blank` parameter.
3
+
4
+ ## 1.0.13 / 2022-12-14
5
+ * Links with embedded spaces are now supported when the new 'url' named parameter is used. For example, instead of specifying:
6
+ ```
7
+ {% href http://link.com with space.html some text %}
8
+ ```
9
+ Instead specify (single and double quotes are equally valid):
10
+ ```
11
+ {% href url="http://link.com with space.html" some text %}
12
+ ```
13
+ * URLs can now contain environment variable references. For example, if $domain and $uri are environment variables:
14
+ ```
15
+ {% href url="http://$domain.html" some text %}
16
+ {% href url="$uri" some text %}
17
+ ```
18
+
1
19
  ## 1.0.12 / 2022-08-09
2
20
  * No longer abends if `plugin-vars` is not present in `_config.yml`
3
21
 
data/README.md CHANGED
@@ -3,24 +3,134 @@
3
3
  ===========
4
4
 
5
5
  `Jekyll_href` is a Jekyll plugin that provides a new Liquid tag: `href`.
6
- The Liquid tag generates and `<a href>` HTML tag, by default containing `target="_blank"` and `rel=nofollow`.
7
- To suppress the `nofollow` attribute, preface the link with the word `follow`.
8
- To suppress the `target` attribute, preface the link with the word `notarget`.
9
- Also provides a convenient way to generate formatted and clickable URIs.
6
+ It provides a convenient way to generate formatted and clickable URIs.
7
+ The Liquid tag generates an `a href` HTML tag, which by default contains `target="_blank"` and `rel=nofollow`.
8
+
9
+ If the url starts with `http`, or the `match` keyword is specified:
10
+ - The url will open in a new tab or window.
11
+ - The url will include `rel=nofollow` for SEO purposes.
12
+
13
+ CAUTION: if linked text contains a single or double quote you will see the error message: `Liquid Exception: Unmatched quote`.
14
+ Instead, use &apos; (`&apos;`), &quot; (`&quot;`), &lsquo; (`&lsquo;`), &rsquo; (`&rsquo;`), &ldquo; (`&ldquo;`), and &rdquo; (`&rdquo;`)
15
+
16
+ In `_config.yml`, if a section called `plugin-vars` exists,
17
+ then its name/value pairs are available for substitution.
18
+ ```yaml
19
+ plugin-vars:
20
+ django-github: 'https://github.com/django/django/blob/3.1.7'
21
+ django-oscar-github: 'https://github.com/django-oscar/django-oscar/blob/3.0.2'
22
+ ```
10
23
 
11
24
 
12
- ### Syntax:
25
+ ## Syntax 1 (requires `url` does not have embedded spaces):
13
26
  ```
14
- {% href [match | [follow] [notarget]] [url] text to display %}
27
+ {% href [match | [follow] [blank|notarget]] url text to display %}
15
28
  ```
16
- Note that the url should not be enclosed in quotes.
17
- Also please note that the square brackets denote optional parameters, and should not be typed.
29
+ 1. The url must be a single token, without embedded spaces.
30
+ 2. The url need not be enclosed in quotes.
31
+ 3. The square brackets denote optional keyword parameters, and should not be typed.
18
32
 
19
- `match` will attempt to match the url fragment (specified as a regex) to a URL in any collection.
20
- If multiple documents have matching URL an error is thrown.
33
+
34
+ ## Syntax 2 (always works):
35
+ This syntax is recommended when the URL contains a colon (:).
36
+ ```
37
+ {% href [match | [follow] [blank|notarget]]
38
+ url="http://link.com with space.html" some text %}
39
+
40
+ {% href [match | [follow] [blank|notarget]]
41
+ url='http://link.com with space.html' some text %}
42
+ ```
43
+ 1. Each of the above examples contain an embedded newline, which is legal.
44
+ 2. The url must be enclosed by either single or double quotes.
45
+ 3. The square brackets denote optional keyword parameters, and should not be typed.
46
+
47
+
48
+ ## Syntax 3 (implicit URL):
49
+ ```
50
+ {% href [match | [follow] [blank|notarget]] www.domain.com %}
51
+ ```
52
+ The URI provided, for example `www.domain.com`,
53
+ is used to form the URL by prepending `https://`,
54
+ in this case the result would be `https://www.domain.com`.
55
+ The displayed URI is enclosed in `<code></code>`,
56
+ so the resulting text is `<code>www.domain.com</code>`.
57
+
58
+
59
+ ## Environment Variable Expansion
60
+ URLs can contain environment variable references.
61
+ For example, if `$domain`, `$uri` and `$USER` are environment variables:
62
+ ```
63
+ {% href http://$domain.html some text %}
64
+
65
+ {% href url="$uri" some text %}
66
+
67
+ {% href https://mslinn.html <code>USER=$USER</code> %}
68
+ ```
69
+
70
+ ## Optional Parameters
71
+ ### `blank`
72
+ The `target='_blank'` attribute is not normally generated for relative links.
73
+ To enforce the generation of this attribute, preface the link with the word `blank`.
74
+ The `blank` and `notarget` parameters are mutually exclusive.
75
+ If both are specified, `blank` prevails.
76
+
77
+ ### `follow`
78
+ To suppress the `nofollow` attribute, preface the link with the word `follow`.
79
+
80
+
81
+ ### `notarget`
82
+ To suppress the `target` attribute, preface the link with the word `notarget`.
83
+ The `blank` and `notarget` parameters are mutually exclusive.
84
+ If both are specified, `blank` prevails.
21
85
 
22
86
 
23
- ### Additional Information
87
+ ### `match`
88
+ `match` will attempt to match the url fragment (specified as a regex) to a URL in any collection.
89
+ If multiple documents have matching URL an error is thrown.
90
+ The `match` option looks through the pages collection for a URL with containing the provided substring.
91
+ `Match` implies `follow` and `notarget`.
92
+
93
+
94
+ ## Examples
95
+ 1. Generates `nofollow` and `target` attributes:
96
+ ```
97
+ {% href https://mslinn.com The Awesome %}
98
+ ```
99
+
100
+ 2. Does not generate `nofollow` or `target` attributes.
101
+ ```
102
+ {% href follow notarget https://mslinn.com The Awesome %}
103
+ ```
104
+
105
+ 3. Does not generate `nofollow` attribute.
106
+ ```
107
+ {% href follow https://mslinn.com The Awesome %}
108
+ ```
109
+
110
+ 4. Does not generate `target` attribute.
111
+ ```
112
+ {% href notarget https://mslinn.com The Awesome %}
113
+ ```
114
+
115
+ 5. Matches page with URL containing abc.
116
+ ```
117
+ {% href match abc The Awesome %}
118
+ ```
119
+
120
+ 6. Matches page with URL containing abc.
121
+ ```
122
+ {% href match abc.html#tag The Awesome %}
123
+ ```
124
+
125
+ 7. Substitute name/value pair for the `django-github` variable defined above:
126
+ ```
127
+ {% href {{django-github}}/django/core/management/__init__.py#L398-L401
128
+ <code>django.core.management.execute_from_command_line</code> %}
129
+ ```
130
+ Substitutions are only made to the URL, not to the linked text.
131
+
132
+
133
+ ## Additional Information
24
134
  More information is available on my web site about [my Jekyll plugins](https://www.mslinn.com/blog/2020/10/03/jekyll-plugins.html).
25
135
 
26
136
 
@@ -42,9 +152,10 @@ Or install it yourself as:
42
152
 
43
153
  $ gem install jekyll_href
44
154
 
45
- ## Usage
46
155
 
47
- ### Defaults
156
+ ## Generated HTML
157
+
158
+ ### Without Keywords
48
159
  ```
49
160
  {% href https://mslinn.com The Awesome %}
50
161
  ```
@@ -56,7 +167,7 @@ Expands to this:
56
167
 
57
168
  Which renders as this: <a href='https://mslinn.com' target='_blank' rel='nofollow'>The Awesome</a>
58
169
 
59
- ### `follow`
170
+ ### With `follow`
60
171
  ```
61
172
  {% href follow https://mslinn.com The Awesome %}
62
173
  ```
@@ -67,7 +178,7 @@ Expands to this:
67
178
  ```
68
179
 
69
180
 
70
- ### `notarget`
181
+ ### With `notarget`
71
182
  ```
72
183
  {% href notarget https://mslinn.com The Awesome %}
73
184
  ```
@@ -78,7 +189,7 @@ Expands to this:
78
189
  ```
79
190
 
80
191
 
81
- ### `follow notarget`
192
+ ### With `follow notarget`
82
193
  ```
83
194
  {% href follow notarget https://mslinn.com The Awesome %}
84
195
  ```
@@ -88,7 +199,7 @@ Expands to this:
88
199
  <a href='https://mslinn.com'>The Awesome</a>
89
200
  ```
90
201
 
91
- ### `match`
202
+ ### With `match`
92
203
  Looks for a post with a matching URL.
93
204
  ```
94
205
  {% href match setting-up-django-oscar.html tutorial site %}
@@ -109,6 +220,7 @@ Expands to this:
109
220
  ```
110
221
  Which renders as: [`mslinn.com`](https://mslinn.com)
111
222
 
223
+
112
224
  ## Development
113
225
 
114
226
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -123,6 +235,21 @@ To install this gem onto your local machine, run:
123
235
  $ bundle exec rake install
124
236
  ```
125
237
 
238
+ ## Test
239
+ A test web site is provided in the `demo` directory.
240
+ 1. Set breakpoints.
241
+
242
+ 2. Initiate a debug session from the command line:
243
+ ```shell
244
+ $ bin/attach demo
245
+ ```
246
+
247
+ 3. Once the `Fast Debugger` signon appears, launch the test configuration called `Attach rdebug-ide`.
248
+
249
+ 4. View the generated website at [`http://localhost:4444`](http://localhost:4444)
250
+
251
+
252
+ ## Release
126
253
  To release a new version,
127
254
  1. Update the version number in `version.rb`.
128
255
  2. Commit all changes to git; if you don't the next step might fail with an unexplainable error message.
data/jekyll_href.gemspec CHANGED
@@ -36,6 +36,8 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'jekyll', '>= 3.5.0'
37
37
  spec.add_dependency 'jekyll_all_collections'
38
38
  spec.add_dependency 'jekyll_plugin_logger'
39
+ spec.add_dependency 'key-value-parser'
40
+ spec.add_dependency 'shellwords'
39
41
 
40
42
  # spec.add_development_dependency 'debase'
41
43
  # spec.add_development_dependency 'rubocop-jekyll'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllHrefVersion
4
- VERSION = "1.0.12"
4
+ VERSION = "1.0.14"
5
5
  end
data/lib/jekyll_href.rb CHANGED
@@ -1,136 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "jekyll_plugin_logger"
4
3
  require "jekyll_all_collections"
4
+ require "jekyll_plugin_logger"
5
5
  require "liquid"
6
6
  require_relative "jekyll_href/version"
7
+ require_relative './jekyll_tag_helper2'
7
8
 
8
9
  # @author Copyright 2020 Michael Slinn
9
10
  # @license SPDX-License-Identifier: Apache-2.0
10
- #
11
11
  # Generates an href.
12
- # Note that the url should not be enclosed in quotes.
13
- #
14
- # If the link starts with 'http' or `match` is specified:
15
- # The link will open in a new tab or window
16
- # The link will include `rel=nofollow` for SEO purposes.
17
- #
18
- # To suppress the `nofollow` attribute, preface the link with the word `follow`.
19
- # To suppress the `target` attribute, preface the link with the word `notarget`.
20
- #
21
- # The `match` option looks through the pages collection for a URL with containing the provided substring.
22
- # Match implies follow and notarget.
23
- #
24
- # If a section called plugin-vars exists then its name/value pairs are available for substitution.
25
- # plugin-vars:
26
- # django-github: 'https://github.com/django/django/blob/3.1.7'
27
- # django-oscar-github: 'https://github.com/django-oscar/django-oscar/blob/3.0.2'
28
- #
29
- #
30
- # @example General form
31
- # {% href [follow] [notarget] [match] url text to display %}
32
- #
33
- # @example Generates `nofollow` and `target` attributes.
34
- # {% href https://mslinn.com The Awesome %}
35
- #
36
- # @example Does not generate `nofollow` or `target` attributes.
37
- # {% href follow notarget https://mslinn.com The Awesome %}
38
- #
39
- # @example Does not generate `nofollow` attribute.
40
- # {% href follow https://mslinn.com The Awesome %}
41
- #
42
- # @example Does not generate `target` attribute.
43
- # {% href notarget https://mslinn.com The Awesome %}
44
- #
45
- # @example Matches page with URL containing abc.
46
- # {% href match abc The Awesome %}
47
- # @example Matches page with URL containing abc.
48
- # {% href match abc.html#tag The Awesome %}
49
- #
50
- # @example Substitute name/value pair for the django-github variable:
51
- # {% href {{django-github}}/django/core/management/__init__.py#L398-L401
52
- # <code>django.core.management.execute_from_command_line</code> %}
53
-
54
- class ExternalHref < Liquid::Tag
12
+
13
+ # Implements href Jekyll tag
14
+ class ExternalHref < Liquid::Tag # rubocop:disable Metrics/ClassLength
15
+ attr_reader :follow, :helper, :line_number, :match, :page, :path, :site, :text, :target, :url
16
+ attr_accessor :link
55
17
 
56
18
  # @param tag_name [String] is the name of the tag, which we already know.
57
- # @param command_line [Hash, String, Liquid::Tag::Parser] the arguments from the web page.
58
- # @param _parse_context [Liquid::ParseContext] tokenized command line
19
+ # @param markup [String] the arguments from the web page.
20
+ # @param _tokens [Liquid::ParseContext] tokenized command line
21
+ # By default it has two keys: :locale and :line_numbers, the first is a Liquid::I18n object, and the second,
22
+ # a boolean parameter that determines if error messages should display the line number the error occurred.
23
+ # This argument is used mostly to display localized error messages on Liquid built-in Tags and Filters.
24
+ # See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
59
25
  # @return [void]
60
- def initialize(tag_name, command_line, _parse_context)
26
+ def initialize(tag_name, markup, _tokens)
61
27
  super
28
+ markup = '' if markup.nil?
29
+ markup.strip!
62
30
 
63
31
  @logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
64
- @match = false
65
- @tokens = command_line.strip.split
66
- @follow = get_value("follow", " rel='nofollow'")
67
- @target = get_value("notarget", " target='_blank'")
68
-
69
- match_index = @tokens.index("match")
70
- if match_index
71
- @tokens.delete_at(match_index)
72
- @follow = ""
73
- @match = true
74
- @target = ""
75
- end
76
-
77
- finalize @tokens
32
+ @helper = JekyllTagHelper2.new(tag_name, markup, @logger)
78
33
  end
79
34
 
80
35
  # Method prescribed by the Jekyll plugin lifecycle.
81
36
  # @param liquid_context [Liquid::Context]
82
37
  # @return [String]
83
38
  def render(liquid_context)
84
- @site = liquid_context.registers[:site]
85
- JekyllAllCollections::maybe_compute_all_collections(@site)
86
-
87
- match(liquid_context) if @match
88
- link = replace_vars(liquid_context, @link)
89
- @target = @follow = "" if link.start_with? "mailto:"
90
- @logger.debug { "@link=#{@link}; link=#{link}" }
91
- "<a href='#{link}'#{@target}#{@follow}>#{@text}</a>"
39
+ super
40
+ globals_initial(liquid_context)
41
+ linkk = compute_linkk
42
+ linkk = replace_vars(linkk)
43
+ @link_save = linkk
44
+ @helper_save = @helper.clone
45
+ globals_update(@helper.argv, linkk) # Sets @link and @text, might clear @follow and @target
46
+ handle_match if @match
47
+ "<a href='#{@link}'#{@target}#{@follow}>#{@text}</a>"
92
48
  end
93
49
 
94
50
  private
95
51
 
96
- def finalize(tokens)
97
- @link = tokens.shift
98
-
99
- @text = tokens.join(" ").strip
100
- if @text.empty?
101
- @text = "<code>#{@link}</code>"
102
- @link = "https://#{@link}"
52
+ def compute_linkk # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
53
+ # Does not look at or compute @link
54
+ linkk = @url
55
+ if linkk.nil? || !linkk
56
+ linkk = @helper.argv&.shift
57
+ @helper.params&.shift
58
+ @helper.keys_values&.delete(linkk)
59
+ dump_linkk_relations(linkk) if linkk.nil?
60
+ elsif @url.to_s.empty?
61
+ dump_linkk_relations(linkk)
103
62
  end
63
+ linkk
64
+ end
104
65
 
105
- unless @link.start_with? "http"
106
- @follow = ""
107
- @target = ""
108
- end
66
+ def dump_linkk_relations(linkk) # rubocop:disable Metrics/MethodLength
67
+ msg = <<~END_MESSAGE
68
+ jekyll_href error: no url was provided on #{@path}:#{@line_number}.
69
+ @helper.markup=#{@helper.markup}
70
+ @helper.argv='#{@helper.argv}'
71
+ linkk='#{linkk}'
72
+ @match='#{@match}'
73
+ @url='#{@url}'
74
+ @follow='#{@follow}
75
+ @target='#{@target}'
76
+ END_MESSAGE
77
+ abort msg.red
109
78
  end
110
79
 
111
- def get_value(token, default_value)
112
- value = default_value
113
- target_index = @tokens.index(token)
114
- if target_index
115
- @tokens.delete_at(target_index)
116
- value = ""
80
+ def globals_initial(liquid_context) # rubocop:disable Metrics/MethodLength
81
+ # Sets @follow, @helper, @match, @page, @path, @site, @target, @url
82
+ @helper.liquid_context = liquid_context
83
+
84
+ @page = liquid_context.registers[:page]
85
+ @path = @page['path']
86
+ @site = liquid_context.registers[:site]
87
+ JekyllAllCollections.maybe_compute_all_collections(@site)
88
+
89
+ @follow = @helper.parameter_specified?('follow') ? '' : " rel='nofollow'"
90
+ @match = @helper.parameter_specified?('match')
91
+ @blank = @helper.parameter_specified?('blank')
92
+ @target = @blank ? " target='_blank'" : nil
93
+ @target ||= @helper.parameter_specified?('notarget') ? '' : " target='_blank'"
94
+ @url = @helper.parameter_specified?('url')
95
+ end
96
+
97
+ def globals_update(tokens, linkk) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
98
+ # Might set @follow, @linkk, @target, and @text
99
+ if linkk.start_with? 'mailto:'
100
+ @link = linkk
101
+ @target = @follow = ''
102
+ @text = @helper.argv.join(' ')
103
+ if @text.empty?
104
+ text = linkk.delete_prefix('mailto:')
105
+ @text = "<code>#{text}</code>"
106
+ end
107
+ return
108
+ else
109
+ @text = tokens.join(" ").strip
110
+ if @text.to_s.empty?
111
+ @text = "<code>#{linkk}</code>"
112
+ @link = "https://#{linkk}"
113
+ else
114
+ @link = linkk
115
+ end
117
116
  end
118
- value
117
+
118
+ return if @link.start_with? "http"
119
+
120
+ @follow = ''
121
+ @target = '' unless @blank
119
122
  end
120
123
 
121
- def match(liquid_context) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
122
- config = @site.config['href']
123
- die_if_nomatch = !config.nil? && config['nomatch'] && config['nomatch']=='fatal'
124
+ def handle_match
125
+ match_post
126
+ @follow = ''
127
+ @target = '' unless @blank
128
+ end
124
129
 
130
+ def match_post # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
131
+ # Might set @link and @text
132
+ config = @site.config['href']
133
+ die_if_nomatch = !config.nil? && config['nomatch'] && config['nomatch'] == 'fatal'
125
134
  path, fragment = @link.split('#')
126
135
 
127
- @logger.debug {
136
+ @logger.debug do
128
137
  <<~END_DEBUG
129
138
  @link=#{@link}
130
139
  @site.posts.docs[0].url = #{@site.posts.docs[0].url}
131
140
  @site.posts.docs[0].path = #{@site.posts.docs[0].path}
132
141
  END_DEBUG
133
- }
142
+ end
134
143
 
135
144
  all_urls = @site.all_collections.map(&:url)
136
145
  url_matches = all_urls.select { |url| url.include? path }
@@ -143,20 +152,22 @@ class ExternalHref < Liquid::Tag
143
152
  @link = url_matches.first
144
153
  @link = "#{@link}\##{fragment}" if fragment
145
154
  else
146
- abort "Error: More than one url matched '#{path}': #{ url_matches.join(", ")}"
155
+ abort "Error: More than one url matched '#{path}': #{url_matches.join(", ")}"
147
156
  end
148
157
  end
149
158
 
150
- def replace_vars(liquid_context, link)
159
+ def replace_vars(text)
160
+ # Replace names in plugin-vars with values
151
161
  variables = @site.config['plugin-vars']
152
- return link unless variables
162
+ return text unless variables
153
163
 
154
164
  variables.each do |name, value|
155
- link = link.gsub "{{#{name}}}", value
165
+ text = text.gsub "{{#{name}}}", value
156
166
  end
157
- link
167
+ @logger.debug { "@link=#{@link}" }
168
+ text
158
169
  end
159
170
  end
160
171
 
161
- PluginMetaLogger.instance.info { "Loaded jeykll_href v#{JekyllHrefVersion::VERSION} plugin." }
172
+ PluginMetaLogger.instance.info { "Loaded jekyll_href v#{JekyllHrefVersion::VERSION} plugin." }
162
173
  Liquid::Template.register_tag('href', ExternalHref)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+ require 'key_value_parser'
5
+
6
+ # Parses arguments and options
7
+ class JekyllTagHelper2
8
+ attr_reader :argv, :keys_values, :liquid_context, :logger, :markup, :params, :tag_name
9
+
10
+ # Expand a environment variable reference
11
+ def self.expand_env(str, die_if_undefined: false)
12
+ str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
13
+ envar = Regexp.last_match(1)
14
+ raise HrefError, "jekyll_href error: #{envar} is undefined".red, [] \
15
+ if !ENV.key?(envar) && die_if_undefined # Suppress stack trace
16
+
17
+ ENV[envar]
18
+ end
19
+ end
20
+
21
+ # strip leading and trailing quotes if present
22
+ def self.remove_quotes(string)
23
+ string.strip.gsub(/\A'|\A"|'\Z|"\Z/, '').strip if string
24
+ end
25
+
26
+ def initialize(tag_name, markup, logger)
27
+ # @keys_values was a Hash[Symbol, String|Boolean] but now it is Hash[String, String|Boolean]
28
+ @tag_name = tag_name
29
+ @markup = markup # Useful for debugging
30
+ @argv = Shellwords.split(JekyllTagHelper2.expand_env(markup))
31
+ @keys_values = KeyValueParser \
32
+ .new({}, { array_values: false, normalize_keys: false, separator: /=/ }) \
33
+ .parse(@argv)
34
+ @logger = logger
35
+ @logger.debug { "@keys_values='#{@keys_values}'" }
36
+ end
37
+
38
+ def delete_parameter(key)
39
+ return if @keys_values.empty?
40
+
41
+ @params.delete(key)
42
+ @argv.delete_if { |x| x == key or x.start_with?("#{key}=") }
43
+ @keys_values.delete(key)
44
+ end
45
+
46
+ # @return if parameter was specified, removes it from the available tokens and returns value
47
+ def parameter_specified?(name)
48
+ return false if @keys_values.empty?
49
+
50
+ key = name
51
+ key = name.to_sym if @keys_values.first.first.instance_of?(Symbol)
52
+ value = @keys_values[key]
53
+ delete_parameter(name)
54
+ value
55
+ end
56
+
57
+ PREDEFINED_SCOPE_KEYS = %i[include page].freeze
58
+
59
+ # Finds variables defined in an invoking include, or maybe somewhere else
60
+ # @return variable value or nil
61
+ def dereference_include_variable(name)
62
+ @liquid_context.scopes.each do |scope|
63
+ next if PREDEFINED_SCOPE_KEYS.include? scope.keys.first
64
+
65
+ value = scope[name]
66
+ return value if value
67
+ end
68
+ nil
69
+ end
70
+
71
+ # @return value of variable, or the empty string
72
+ def dereference_variable(name)
73
+ value = @liquid_context[name] # Finds variables named like 'include.my_variable', found in @liquid_context.scopes.first
74
+ value ||= @page[name] if @page # Finds variables named like 'page.my_variable'
75
+ value ||= dereference_include_variable(name)
76
+ value ||= ''
77
+ value
78
+ end
79
+
80
+ # Sets @params by replacing any Liquid variable names with their values
81
+ def liquid_context=(context)
82
+ @liquid_context = context
83
+ @params = @keys_values.map { |k, _v| lookup_variable(k) }
84
+ end
85
+
86
+ def lookup_variable(symbol)
87
+ string = symbol.to_s
88
+ return string unless string.start_with?('{{') && string.end_with?('}}')
89
+
90
+ dereference_variable(string.delete_prefix('{{').delete_suffix('}}'))
91
+ end
92
+
93
+ def page
94
+ @liquid_context.registers[:page]
95
+ end
96
+ end
data/spec/href_spec.rb ADDED
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jekyll'
4
+ require 'jekyll_plugin_logger'
5
+ require 'yaml'
6
+ require_relative '../lib/jekyll_href'
7
+
8
+ Registers = Struct.new(:page, :site)
9
+
10
+ # Mock for Collections
11
+ class Collections
12
+ def values
13
+ []
14
+ end
15
+ end
16
+
17
+ # Mock for Site
18
+ class SiteMock
19
+ attr_reader :config
20
+
21
+ def initialize
22
+ @config = YAML.safe_load(File.read('_config.yml'))
23
+ end
24
+
25
+ def collections
26
+ Collections.new
27
+ end
28
+ end
29
+
30
+ # Mock for Liquid::ParseContent
31
+ class TestParseContext < Liquid::ParseContext
32
+ attr_reader :line_number, :registers
33
+
34
+ def initialize
35
+ super
36
+ @line_number = 123
37
+
38
+ @registers = Registers.new(
39
+ { 'path' => 'https://feeds.soundcloud.com/users/soundcloud:users:7143896/sounds.rss' },
40
+ SiteMock.new
41
+ )
42
+ end
43
+ end
44
+
45
+ # Lets get this party started
46
+ class MyTest # rubocop:disable Metrics/ClassLength
47
+ Dir.chdir 'demo'
48
+
49
+ RSpec.describe ExternalHref do
50
+ let(:logger) do
51
+ PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
52
+ end
53
+
54
+ let(:parse_context) { TestParseContext.new }
55
+
56
+ let(:helper) do
57
+ JekyllTagHelper2.new(
58
+ 'href',
59
+ 'mailto:j.smith@super-fake-merger.com',
60
+ logger
61
+ )
62
+ end
63
+
64
+ it "Obtains internal link with blank" do
65
+ href = ExternalHref.send(
66
+ :new,
67
+ 'href',
68
+ 'blank ./path/page.html internal link text'.dup,
69
+ parse_context
70
+ )
71
+ href.send(:globals_initial, parse_context)
72
+ linkk = href.send(:compute_linkk)
73
+ href.send(:globals_update, href.helper.argv, linkk)
74
+ expect(href.follow).to eq('')
75
+ expect(href.link).to eq('./path/page.html')
76
+ expect(href.target).to eq(" target='_blank'")
77
+ expect(href.text).to eq('internal link text')
78
+ end
79
+
80
+ it "Obtains external link with text" do
81
+ href = ExternalHref.send(
82
+ :new,
83
+ 'href',
84
+ 'https://feeds.soundcloud.com/users/soundcloud:users:7143896/sounds.rss SoundCloud RSS Feed'.dup,
85
+ parse_context
86
+ )
87
+ href.send(:globals_initial, parse_context)
88
+ linkk = href.send(:compute_linkk)
89
+ href.send(:globals_update, href.helper.argv, linkk)
90
+ expect(href.follow).to eq(" rel='nofollow'")
91
+ expect(href.link).to eq('https://feeds.soundcloud.com/users/soundcloud:users:7143896/sounds.rss')
92
+ expect(href.target).to eq(" target='_blank'")
93
+ expect(href.text).to eq('SoundCloud RSS Feed')
94
+ end
95
+
96
+ it "Obtains external link using url parameter with text" do
97
+ href = ExternalHref.send(
98
+ :new,
99
+ 'href',
100
+ 'url="https://feeds.soundcloud.com/users/soundcloud:users:7143896/sounds.rss" SoundCloud RSS Feed'.dup,
101
+ parse_context
102
+ )
103
+ href.send(:globals_initial, parse_context)
104
+ linkk = href.send(:compute_linkk)
105
+ href.send(:globals_update, href.helper.argv, linkk)
106
+ expect(href.follow).to eq(" rel='nofollow'")
107
+ expect(href.link).to eq('https://feeds.soundcloud.com/users/soundcloud:users:7143896/sounds.rss')
108
+ expect(href.target).to eq(" target='_blank'")
109
+ expect(href.text).to eq('SoundCloud RSS Feed')
110
+ end
111
+
112
+ it "Obtains external link without scheme or text" do
113
+ href = ExternalHref.send(
114
+ :new,
115
+ 'href',
116
+ 'super-fake-merger.com'.dup,
117
+ parse_context
118
+ )
119
+ href.send(:globals_initial, parse_context)
120
+ linkk = href.send(:compute_linkk)
121
+ href.send(:globals_update, href.helper.argv, linkk)
122
+ expect(href.follow).to eq(" rel='nofollow'")
123
+ expect(href.link).to eq('https://super-fake-merger.com')
124
+ expect(href.target).to eq(" target='_blank'")
125
+ expect(href.text).to eq('<code>super-fake-merger.com</code>')
126
+ end
127
+
128
+ it "Expands YAML hash with link text" do
129
+ href = ExternalHref.send(
130
+ :new,
131
+ 'href',
132
+ '{{github}}/diasks2/confidential_info_redactor <code>confidential_info_redactor</code>'.dup,
133
+ parse_context
134
+ )
135
+ href.send(:globals_initial, parse_context)
136
+ linkk = href.send(:compute_linkk)
137
+ linkk = href.send(:replace_vars, linkk)
138
+ href.send(:globals_update, href.helper.argv, linkk)
139
+ expect(href.follow).to eq(" rel='nofollow'")
140
+ expect(href.link).to eq('https://github.com/diasks2/confidential_info_redactor')
141
+ expect(href.target).to eq(" target='_blank'")
142
+ expect(href.text).to eq('<code>confidential_info_redactor</code>')
143
+ end
144
+
145
+ it "Obtains external link with follow" do
146
+ href = ExternalHref.send(
147
+ :new,
148
+ 'href',
149
+ 'follow https://www.mslinn.com Awesome'.dup,
150
+ parse_context
151
+ )
152
+ href.send(:globals_initial, parse_context)
153
+ linkk = href.send(:compute_linkk)
154
+ href.send(:globals_update, href.helper.argv, linkk)
155
+ expect(href.follow).to eq('')
156
+ expect(href.link).to eq('https://www.mslinn.com')
157
+ expect(href.target).to eq(" target='_blank'")
158
+ expect(href.text).to eq('Awesome')
159
+ end
160
+
161
+ it "Obtains external link with follow and notarget" do
162
+ href = ExternalHref.send(
163
+ :new,
164
+ 'href',
165
+ 'follow notarget https://www.mslinn.com Awesome'.dup,
166
+ parse_context
167
+ )
168
+ href.send(:globals_initial, parse_context)
169
+ linkk = href.send(:compute_linkk)
170
+ href.send(:globals_update, href.helper.argv, linkk)
171
+ expect(href.follow).to eq('')
172
+ expect(href.link).to eq('https://www.mslinn.com')
173
+ expect(href.target).to eq('')
174
+ expect(href.text).to eq('Awesome')
175
+ end
176
+
177
+ it "Obtains external link with blank" do
178
+ href = ExternalHref.send(
179
+ :new,
180
+ 'href',
181
+ 'blank https://www.mslinn.com Awesome'.dup,
182
+ parse_context
183
+ )
184
+ href.send(:globals_initial, parse_context)
185
+ linkk = href.send(:compute_linkk)
186
+ href.send(:globals_update, href.helper.argv, linkk)
187
+ expect(href.follow).to eq(" rel='nofollow'")
188
+ expect(href.link).to eq('https://www.mslinn.com')
189
+ expect(href.target).to eq(" target='_blank'")
190
+ expect(href.text).to eq('Awesome')
191
+ end
192
+
193
+ it "Implicitly computes external link from text" do
194
+ href = ExternalHref.send(
195
+ :new,
196
+ 'href',
197
+ 'www.mslinn.com'.dup,
198
+ parse_context
199
+ )
200
+ href.send(:globals_initial, parse_context)
201
+ linkk = href.send(:compute_linkk)
202
+ href.send(:globals_update, href.helper.argv, linkk)
203
+ expect(href.follow).to eq(" rel='nofollow'")
204
+ expect(href.link).to eq('https://www.mslinn.com')
205
+ expect(href.target).to eq(" target='_blank'")
206
+ expect(href.text).to eq('<code>www.mslinn.com</code>')
207
+ end
208
+
209
+ it "Implicitly computes external link from text with follow and notarget" do
210
+ href = ExternalHref.send(
211
+ :new,
212
+ 'href',
213
+ 'follow notarget www.mslinn.com'.dup,
214
+ parse_context
215
+ )
216
+ href.send(:globals_initial, parse_context)
217
+ linkk = href.send(:compute_linkk)
218
+ href.send(:globals_update, href.helper.argv, linkk)
219
+ expect(href.follow).to eq('')
220
+ expect(href.link).to eq('https://www.mslinn.com')
221
+ expect(href.target).to eq('')
222
+ expect(href.text).to eq('<code>www.mslinn.com</code>')
223
+ end
224
+
225
+ it "Implicitly computes external link from text with blank" do
226
+ href = ExternalHref.send(
227
+ :new,
228
+ 'href',
229
+ 'follow blank www.mslinn.com'.dup,
230
+ parse_context
231
+ )
232
+ href.send(:globals_initial, parse_context)
233
+ linkk = href.send(:compute_linkk)
234
+ href.send(:globals_update, href.helper.argv, linkk)
235
+ expect(href.follow).to eq('')
236
+ expect(href.link).to eq('https://www.mslinn.com')
237
+ expect(href.target).to eq(" target='_blank'")
238
+ expect(href.text).to eq('<code>www.mslinn.com</code>')
239
+ end
240
+
241
+ it "Obtains mailto without text" do
242
+ href = ExternalHref.send(
243
+ :new,
244
+ 'href',
245
+ 'mailto:mslinn@mslinn.com'.dup,
246
+ parse_context
247
+ )
248
+ href.send(:globals_initial, parse_context)
249
+ linkk = href.send(:compute_linkk)
250
+ href.send(:globals_update, href.helper.argv, linkk)
251
+ expect(href.follow).to eq('')
252
+ expect(href.link).to eq('mailto:mslinn@mslinn.com')
253
+ expect(href.target).to eq('')
254
+ expect(href.text).to eq('<code>mslinn@mslinn.com</code>')
255
+ end
256
+
257
+ it "Obtains mailto with text" do
258
+ href = ExternalHref.send(
259
+ :new,
260
+ 'href',
261
+ 'mailto:mslinn@mslinn.com Mike Slinn'.dup,
262
+ parse_context
263
+ )
264
+ href.send(:globals_initial, parse_context)
265
+ linkk = href.send(:compute_linkk)
266
+ href.send(:globals_update, href.helper.argv, linkk)
267
+ expect(href.follow).to eq('')
268
+ expect(href.link).to eq('mailto:mslinn@mslinn.com')
269
+ expect(href.target).to eq('')
270
+ expect(href.text).to eq('Mike Slinn')
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid"
4
+ require "fileutils"
5
+ require_relative "../lib/jekyll_href"
6
+
7
+ RSpec.configure do |config|
8
+ config.filter_run :focus
9
+ config.order = "random"
10
+ config.run_all_when_everything_filtered = true
11
+
12
+ # See https://relishapp.com/rspec/rspec-core/docs/command-line/only-failures
13
+ config.example_status_persistence_file_path = "../spec/status_persistence.txt"
14
+
15
+ config.filter_run_when_matching focus: true
16
+ end
@@ -0,0 +1,15 @@
1
+ example_id | status | run_time |
2
+ ----------------------------------------------------------------- | ------ | --------------- |
3
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:1] | passed | 0.00817 seconds |
4
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:2] | passed | 0.00349 seconds |
5
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:3] | passed | 0.0033 seconds |
6
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:4] | passed | 0.00307 seconds |
7
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:5] | passed | 0.00315 seconds |
8
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:6] | passed | 0.0032 seconds |
9
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:7] | passed | 0.0035 seconds |
10
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:8] | passed | 0.00323 seconds |
11
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:9] | passed | 0.0032 seconds |
12
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:10] | passed | 0.00307 seconds |
13
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:11] | passed | 0.00317 seconds |
14
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:12] | passed | 0.00357 seconds |
15
+ /mnt/f/work/jekyll/my_plugins/jekyll_href/spec/href_spec.rb[1:13] | passed | 0.0035 seconds |
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll_href
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.12
4
+ version: 1.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Slinn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-09 00:00:00.000000000 Z
11
+ date: 2023-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: key-value-parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: shellwords
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  description: 'Generates an ''a href'' tag, possibly with target=''_blank'' and rel=''nofollow''.
56
84
 
57
85
  '
@@ -69,6 +97,10 @@ files:
69
97
  - jekyll_href.gemspec
70
98
  - lib/jekyll_href.rb
71
99
  - lib/jekyll_href/version.rb
100
+ - lib/jekyll_tag_helper2.rb
101
+ - spec/href_spec.rb
102
+ - spec/spec_helper.rb
103
+ - spec/status_persistence.txt
72
104
  homepage: https://www.mslinn.com/blog/2020/10/03/jekyll-plugins.html#href
73
105
  licenses:
74
106
  - MIT
@@ -100,5 +132,8 @@ rubygems_version: 3.3.3
100
132
  signing_key:
101
133
  specification_version: 4
102
134
  summary: Generates an 'a href' tag, possibly with target='_blank' and rel='nofollow'.
103
- test_files: []
135
+ test_files:
136
+ - spec/href_spec.rb
137
+ - spec/spec_helper.rb
138
+ - spec/status_persistence.txt
104
139
  ...