jekyll_outline 1.2.6 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 789f862a50e91feb22e61715574c1358dfac651dd0029617333032d89bd3920a
4
- data.tar.gz: 327437833f654e62df9e71540be12985eb98e518d4c87edd5f632aa4f5b9028a
3
+ metadata.gz: 88b2d41ecbd19787d53682b396bd2bbf9eabccf2ffa366026f4792079c3f827e
4
+ data.tar.gz: 44b1fbda200ea5c9a836538fc2f8e3565bef880b1dec9a77bedec513ef15f028
5
5
  SHA512:
6
- metadata.gz: ac54e627cf7a5e839dff378e2d85c2c1d89ae6cad12fcdd1fa728591b0d3ed597d93c0af78787ef27ca15a4a4212b6f02156897b988f98c27587d9d73f297c1b
7
- data.tar.gz: 4d6725aae2d9c428f9dcfbc2128918ca2bf43c626a26d235af7d183cb04d17252b8f1333ad125202e3c2fe8ef1a5880594bf68e1e52dfd3122e7d014116efc8b
6
+ metadata.gz: 5d432be906fbe832e4db55e2333c969b7b9beda06b7c391a5f43b6cd13771646cd285847d13510678adc548db642fa3f3eb3d7521737d9e0b1f48f28eaffd991
7
+ data.tar.gz: 056b5a89bdbb45d9f56016f1fd17be96c7d7a6e000c5cefbfecad28c496e540c60bcf712a024107aa34e789f59dbefdcd09d9a096077e7955ea852ae3176c41d
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- require:
1
+ plugins:
2
2
  # - rubocop-jekyll
3
3
  - rubocop-md
4
4
  - rubocop-performance
@@ -37,12 +37,18 @@ Metrics/BlockLength:
37
37
  - jekyll_plugin_support.gemspec
38
38
  Max: 30
39
39
 
40
+ Metrics/ClassLength:
41
+ Max: 150
42
+
40
43
  Metrics/CyclomaticComplexity:
41
44
  Max: 25
42
45
 
43
46
  Metrics/MethodLength:
44
47
  Max: 50
45
48
 
49
+ Metrics/ParameterLists:
50
+ Enabled: false
51
+
46
52
  Metrics/PerceivedComplexity:
47
53
  Max: 25
48
54
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.3.0 / 2025-06-04
4
+
5
+ * Now relies on `jekyll_plugin_support` v3.1.0.
6
+ * Fixed missing </div>s.
7
+ * Reimplemented using nested classes, concluding with a call `to_s`
8
+ instead of implementing crazy nesting tracking.
9
+ * The `fields` parameter was renamed to `pattern`, however for compatibility, both parameter names are accepted.
10
+ If both are provided, `pattern` has priority.
11
+
12
+
3
13
  ## 1.2.6 / 2025-01-03
4
14
 
5
15
  * Added `exclude_from_outline` optional front matter YAML element.
data/README.md CHANGED
@@ -41,6 +41,7 @@ This is the simplest possible outline, without images.
41
41
  800: Digging Deeper
42
42
  1900: Debugging
43
43
  2700: Production
44
+ {% endoutline %}
44
45
  ```
45
46
 
46
47
  ### [A/V Studio Technology](https://mslinn.com/av_studio/index.html)
@@ -189,7 +190,7 @@ The following examples are taken from [`demo/index.html`](demo/index.html).
189
190
  Sort by the `order` field:
190
191
 
191
192
  ```html
192
- {% outline attribution fields="<b> title </b> &ndash; <i> description </i>" stuff %}
193
+ {% outline attribution pattern="<b> title </b> &ndash; <i> description </i>" stuff %}
193
194
  000: A Topic 0..19
194
195
  020: A Topic 20..39
195
196
  040: A Topic 40..
@@ -199,7 +200,7 @@ Sort by the `order` field:
199
200
  Sort by `title` field:
200
201
 
201
202
  ```html
202
- {% outline attribution sort_by_title fields="<b> title </b> &ndash; <i> description </i>" stuff %}
203
+ {% outline attribution sort_by_title pattern="<b> title </b> &ndash; <i> description </i>" stuff %}
203
204
  000: B Topic 0..19
204
205
  020: B Topic 20..39
205
206
  040: B Topic 40..
@@ -230,21 +231,21 @@ By default, each displayed entry consists of a document title,
230
231
  wrapped within an &lt;a href&gt; HTML tag that links to the page for that entry,
231
232
  followed by an indication of whether the document is visible (a draft) or not.
232
233
 
233
- Entry can also include following fields:
234
+ Entry can also include following pattern:
234
235
  `draft`, `categories`, `description`, `date`, `last_modified` or `last_modified_at`, `layout`, `order`, `title`, `slug`,
235
236
  `ext`, and `tags`.
236
237
 
237
- Specify the fields like this:
238
+ Specify the pattern like this:
238
239
 
239
240
  ```html
240
- {% outline fields="title &ndash; <i> description </i>" %}
241
+ {% outline pattern="title &ndash; <i> description </i>" %}
241
242
  000: Topic 0..19
242
243
  020: Topic 20..39
243
244
  040: Topic 40..
244
245
  {% endoutline %}
245
246
  ```
246
247
 
247
- Words in the `fields` argument that are not recognized as a field are transcribed into the output.
248
+ Words in the `pattern` argument that are not recognized as a field are transcribed into the output.
248
249
 
249
250
  In the above example, notice that the HTML is space delimited from the field names.
250
251
  The parser is simple and stupid: each token is matched against the known keywords.
@@ -35,7 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
36
36
  spec.version = JekyllOutlineVersion::VERSION
37
37
 
38
- spec.add_dependency 'jekyll', '>= 3.5.0'
39
- spec.add_dependency 'jekyll_draft', '>= 2.0.2'
40
- spec.add_dependency 'jekyll_plugin_support', '>= 1.0.2'
38
+ spec.add_dependency 'jekyll', '>= 4.4.0'
39
+ spec.add_dependency 'jekyll_draft', '>= 3.0.0'
40
+ spec.add_dependency 'jekyll_plugin_support', '>= 3.1.0'
41
41
  end
@@ -1,3 +1,3 @@
1
1
  module JekyllOutlineVersion
2
- VERSION = '1.2.6'.freeze
2
+ VERSION = '1.3.0'.freeze
3
3
  end
data/lib/outline_tag.rb CHANGED
@@ -1,79 +1,51 @@
1
- # @author Copyright 2022 {https://www.mslinn.com Michael Slinn}
2
-
3
1
  require 'jekyll_draft'
4
2
  require 'jekyll_plugin_logger'
5
3
  require 'jekyll_plugin_support'
6
- require 'yaml'
7
4
  require_relative 'jekyll_outline/version'
5
+ require_relative 'structure/outline'
6
+ require_relative 'structure/yaml_parser'
8
7
 
9
- # This class generates output of the form, where NNN is an order value:
10
- # <div class="outer_posts">
11
- # <h3 class="post_title clear" id="title_0">Django / Oscar Evaluation</h3>
12
- # <div id="posts_wrapper_0" class="clearfix">
13
- # <div id="posts_0" class="posts">
14
- # <span>2021-02-11</span> <span><a href="/collection/page1.html">Title 1</a></span>
15
- # <span>2023-12-09</span> <span><a href="/collection/page2.html">Title 2</a></span>
16
- # </div>
17
- # <h3 class="post_title clear" id="title_NNN">Notes</h3>
18
- # <div id="posts_wrapper_NNN" class="clearfix">
19
- # <div id="posts_400" class="posts">
20
- # <span>2021-04-14</span> <span><a href="/collection/page3.html">Title 3</a></span>
21
- # <span>2021-03-29</span> <span><a href="/collection/page4.html">Title 4</a></span>
22
- # </div>
23
- # </div>
24
- # <div id="jps_attribute_570007" class="jps_attribute">
25
- # <div>
26
- # <a href="https://www.mslinn.com/jekyll_plugins/jekyll_outline.html" target="_blank" rel="nofollow">
27
- # Generated by the jekyll_outline v1.2.1 Jekyll plugin, written by Mike Slinn 2024-01-09.
28
- # </a>
29
- # </div>
30
- # </div>
31
- # </div>
32
- # </div>
33
- #
34
- # Subclasses, such as jekyll_toc.rb, might generate other output.
35
-
8
+ # See spec/outline_spec for an example of HTML output.
36
9
  module JekyllSupport
37
10
  PLUGIN_NAME = 'outline'.freeze
38
- OutlineError = JekyllSupport.define_error
39
-
40
- # Interleaves with docs
41
- # Duck type compatible with Jekyll doc
42
- class Header
43
- attr_accessor :order, :title
44
-
45
- def initialize(yaml)
46
- @order = yaml[0]
47
- @published = true
48
- @title = yaml[1]
49
- end
50
11
 
51
- def to_s
52
- " <h3 class='post_title clear' id=\"title_#{@order}\">#{@title}</h3>"
53
- end
54
- end
12
+ OutlineError = JekyllSupport.define_error
55
13
 
56
- class OutlineTag < JekyllBlock # rubocop:disable Metrics/ClassLength
14
+ class OutlineTag < JekyllBlock
57
15
  include JekyllOutlineVersion
58
16
 
59
- FIXNUM_MAX = (2**((0.size * 8) - 2)) - 1
60
-
61
17
  def render_impl(text)
62
- headers = make_headers(super) # Process the block content.
18
+ block_content = super # Process the block content.
63
19
 
64
- @helper.gem_file __FILE__
20
+ @helper.gem_file __FILE__ # For attribution
65
21
 
66
22
  @die_on_outline_error = @tag_config['die_on_outline_error'] == true if @tag_config
67
23
  @pry_on_outline_error = @tag_config['pry_on_outline_error'] == true if @tag_config
68
24
 
69
- @fields = @helper.parameter_specified?('fields')&.split || ['title']
70
- @sort_by = @helper.parameter_specified?('sort_by_title') ? 'title' : 'order'
71
- @collection_name = @helper.remaining_markup
72
- raise OutlineError, 'collection_name was not specified' unless @collection_name
73
-
74
- @docs = obtain_docs(@collection_name)
75
- collection = headers + @docs
76
- render_outline collection
25
+ pattern = @helper.parameter_specified?('pattern')&.split ||
26
+ @helper.parameter_specified?('fields')&.split ||
27
+ ['title']
28
+ sort_by = @helper.parameter_specified?('sort_by_title') ? :title : :order
29
+ collection_name = @helper.remaining_markup
30
+ raise OutlineError, 'collection_name was not specified' unless collection_name
31
+
32
+ outline_options = OutlineOptions.new(
33
+ attribution: @attribution,
34
+ collection_name: collection_name,
35
+ enable_attribution: @attribution,
36
+ pattern: pattern,
37
+ sort_by: sort_by
38
+ )
39
+ yaml_parser = YamlParser.new outline_options, block_content
40
+ outline = Outline.new(outline_options: outline_options)
41
+ outline.add_sections yaml_parser.sections
42
+
43
+ abort "#{collection_name} is not a valid collection." unless @site.collections&.key?(collection_name)
44
+ docs = @site
45
+ .collections[collection_name]
46
+ .docs
47
+ outline.add_entries(collection_apages(docs))
48
+ outline.to_s
77
49
  rescue OutlineError => e # jekyll_plugin_support handles StandardError
78
50
  @logger.error { JekyllPluginHelper.remove_html_tags e.logger_message }
79
51
  binding.pry if @pry_on_outline_error # rubocop:disable Lint/Debugger
@@ -82,175 +54,16 @@ module JekyllSupport
82
54
  e.html_message
83
55
  end
84
56
 
85
- # Overload this for a subclass
86
- def render_outline(collection)
87
- <<~HEREDOC
88
- <div class="outer_posts">
89
- #{make_entries collection}
90
- </div>
91
- #{@helper.attribute if @helper.attribution}
92
- HEREDOC
93
- end
94
-
95
- def open_head; end
96
-
97
57
  private
98
58
 
99
- def header?(variable)
100
- variable.instance_of?(Header)
101
- end
102
-
103
- def make_headers(content)
104
- content = remove_leading_zeros remove_leading_spaces content
105
- yaml = YAML.safe_load content
106
- yaml.map { |entry| Header.new entry }
107
- rescue NoMethodError => e
108
- raise OutlineError, <<~END_MSG
109
- Invalid YAML within {% outline %} tag. The offending content was:
110
-
111
- <pre>#{content}</pre>
112
- END_MSG
113
- rescue Psych::SyntaxError => e
114
- msg = <<~END_MSG
115
- Invalid YAML found within {% outline %} tag:<br>
116
- <pre>#{e.message}</pre>
117
- END_MSG
118
- @logger.error { e.message }
119
- raise OutlineError, msg
120
- end
121
-
122
- # @section_state can have values: :head, :in_body
123
- # @param collection Array of Jekyll::Document and JekyllSupport::Header
124
- # @return Array of String
125
- def make_entries(collection)
126
- sorted = if @sort_by == 'order'
127
- collection.sort_by(&obtain_order)
128
- else
129
- collection.sort_by(&obtain_field)
130
- end
131
- pruned = remove_empty_headers sorted
132
- @section_state = :head
133
- @section_id = 0
134
- result = pruned.map do |entry|
135
- handle entry
136
- end
137
- result << " </div>\n </div>" if @section_state == :in_body # Modify this for TOC
138
- result&.join("\n")
139
- end
140
-
141
- KNOWN_FIELDS = %w[draft categories description date last_modified_at layout order title slug ext tags excerpt].freeze
142
-
143
- def handle(entry)
144
- if entry.instance_of? Header
145
- @header_order = entry.order
146
- section_end = " </div>\n" if @section_state == :in_body
147
- @section_state = :head
148
- entry = section_end + entry.to_s if section_end
149
- entry
150
- else
151
- if @section_state == :head
152
- section_start = <<~ENDTEXT # Modify this for TOC
153
- <div id="posts_wrapper_#{@header_order}" class='clearfix'>
154
- <div id="posts_#{@header_order}" class='posts'>
155
- ENDTEXT
156
- end
157
- @section_state = :in_body
158
- date = entry.data['last_modified_at'] # "%Y-%m-%d"
159
- draft = Jekyll::Draft.draft_html(entry)
160
- visible_line = handle_entry entry
161
- result = " <span>#{date}</span> <span><a href='#{entry.url}'>#{visible_line.strip}</a>#{draft}</span>" # Modify this for TOC
162
- result = section_start + result if section_start
163
- result
164
- end
165
- end
166
-
167
- def handle_entry(entry)
168
- result = ''
169
- @fields.each do |field|
170
- if KNOWN_FIELDS.include? field
171
- if entry.data.key? field
172
- result += "#{entry.data[field]} "
173
- else
174
- @logger.warn { "#{field} is a known field, but it was not present in entry #{entry}" }
175
- end
176
- else
177
- result += "#{field} "
178
- end
179
- end
180
- result
181
- end
182
-
183
- # Find the given document
184
- def obtain_doc(doc_name)
185
- abort "#{@collection_name} is not a valid collection." unless @site.collections.key? @collection_name
186
- @site
187
- .collections[@collection_name]
188
- .docs
189
- .reject { |doc| doc.data['exclude_from_outline'] }
190
- .find { |doc| doc.url.match(/#{doc_name}(.\w*)?$/) }
191
- end
192
-
193
- # Ignores files whose name starts with `index`, and those with the following in their front matter:
194
- # exclude_from_outline: true
195
- def obtain_docs(collection_name)
196
- abort "#{@collection_name} is not a valid collection." unless @site.collections.key? @collection_name
197
- @site
198
- .collections[collection_name]
199
- .docs
59
+ # Returns an APage for each document in the collection with the given named.
60
+ # Ignores files whose name starts with `index`,
61
+ # and those with the following in their front matter:
62
+ # exclude_from_outline: true
63
+ def collection_apages(pages)
64
+ pages
200
65
  .reject { |doc| doc.url.match(/index(.\w*)?$/) || doc.data['exclude_from_outline'] }
201
- end
202
-
203
- # Sort entries within the outline tag which do not have the property specified by @sort_by at the end
204
- def obtain_field
205
- sort_by = @sort_by.to_s
206
- proc do |entry|
207
- if entry.respond_to? :data # page
208
- entry.data.key?(sort_by) ? entry.data[sort_by] || 'zzz' : 'zzz'
209
- else # heading
210
- entry.respond_to?(sort_by) ? entry.send(sort_by) || 'zzz' : 'zzz'
211
- end
212
- end
213
- end
214
-
215
- # Sort entries within the outline tag which do not have an order property at the end
216
- def obtain_order
217
- proc do |entry|
218
- if entry.respond_to? :data # page
219
- entry.data.key?('order') ? entry.data['order'] || FIXNUM_MAX : FIXNUM_MAX
220
- else # heading
221
- entry.order || FIXNUM_MAX
222
- end
223
- end
224
- end
225
-
226
- def remove_empty_headers(array)
227
- i = 0
228
- while i < array.length - 1
229
- if header?(array[i]) && header?(array[i + 1])
230
- array.delete_at(i)
231
- else
232
- i += 1
233
- end
234
- end
235
-
236
- array.delete_at(array.length - 1) if header?(array.last)
237
- array
238
- end
239
-
240
- def remove_leading_spaces(multiline)
241
- multiline
242
- .strip
243
- .split("\n")
244
- .map { |x| x.gsub(/\A\s+/, '') }
245
- .join("\n")
246
- end
247
-
248
- def remove_leading_zeros(multiline)
249
- multiline
250
- .strip
251
- .split("\n")
252
- .map { |x| x.gsub(/(?<= |\A)0+(?=\d)/, '') }
253
- .join("\n")
66
+ .map { |x| ::JekyllSupport::APage.new(x, 'collection') if x }
254
67
  end
255
68
 
256
69
  JekyllPluginHelper.register(self, PLUGIN_NAME)
@@ -0,0 +1,46 @@
1
+ # Enriches JekyllSupport.APage
2
+ module JekyllSupport
3
+ KNOWN_FIELDS = %w[draft categories description date last_modified_at layout order title slug ext tags excerpt].freeze
4
+
5
+ # Overrides definition from `jekyll_plugin_support`
6
+ class APage
7
+ def render_outline_apage(pattern)
8
+ <<~END_ENTRY
9
+ <span>#{@last_modified.strftime('%Y-%m-%d')}</span>
10
+ <span><a href='#{@url}'>#{render_entry_details pattern}</a>#{@draft}</span>
11
+ END_ENTRY
12
+ end
13
+
14
+ # @param pattern can either be a String or [String]
15
+ # Renders a section entry as a string
16
+ # Recognized tokens are looked up, otherwise they are incorporated into the output
17
+ # Currently spaces are the only valid delimiters; HTML tags should be tokenized even when not delimited by spaces
18
+ def render_entry_details(pattern)
19
+ result = ''
20
+ fields = case pattern
21
+ when String
22
+ pattern.split
23
+ when Array
24
+ pattern
25
+ else
26
+ @logger.error { "Pattern is neither a String nor an Array (#{pattern})" }
27
+ end
28
+ fields.each do |field|
29
+ if KNOWN_FIELDS.include? field
30
+ if respond_to? field
31
+ value = send field
32
+ result += "#{value} "
33
+ elsif data.key?(field.to_sym) || data.key?(field.to_s)
34
+ value = data[field.to_sym] || data[field.to_s]
35
+ result += "#{value} "
36
+ else
37
+ @logger.warn { "'#{field}' is a known field, but it was not present in apage with url '#{@url}'." }
38
+ end
39
+ else
40
+ result += "#{field} "
41
+ end
42
+ end
43
+ result
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,156 @@
1
+ require 'jekyll_plugin_support'
2
+ require_relative 'section'
3
+
4
+ module JekyllSupport
5
+ # @param attribution sets the attribution message
6
+ # @param enable_attribution causes the attribution message to be displayed if truthy
7
+ # @param collection_name Name of the Jekyll collection the outline is organizing
8
+ # @param pattern String containing keyswords and literals; interpreted and displayed when an APage is rendered as a topic entry
9
+ # @param sort_by Either has value :order or :title
10
+ class OutlineOptions
11
+ attr_accessor :attribution, :enable_attribution, :collection_name, :logger, :pattern, :sort_by
12
+
13
+ def initialize(
14
+ collection_name: '_posts',
15
+ attribution: '',
16
+ enable_attribution: false,
17
+ logger: PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config),
18
+ pattern: '<b> title </b> &ndash; <i> description </i>',
19
+ sort_by: :order
20
+ )
21
+ @attribution = attribution
22
+ @enable_attribution = enable_attribution
23
+ @collection_name = collection_name
24
+ @logger = logger
25
+ @pattern = pattern
26
+ @sort_by = sort_by
27
+ end
28
+ end
29
+
30
+ class Outline
31
+ attr_reader :logger, :options, :sections
32
+
33
+ # Sort all entries first so they are iteratable according to the desired order.
34
+ # This presorts the entries for each section.
35
+ #
36
+ # If options[:sort_by] == :order then place each APage into it's appropriate section.
37
+ # Otherwise place all entries into one section.
38
+ #
39
+ # options[:pattern] defaults to ['title'], but might be something like
40
+ # ["<b>", "title", "</b>", "&ndash;", "<i>", "description", "</i>"]
41
+ def initialize(outline_options: OutlineOptions.new)
42
+ @add_sections_called = false
43
+ @options = outline_options
44
+
45
+ @logger = @options.logger
46
+ @sections = @options.sort_by == :order ? [] : [Section.new(@options, [0, ''])]
47
+ rescue StandardError => e
48
+ error_short_trace @logger, e
49
+ end
50
+
51
+ def add_entries(apages)
52
+ sorted_apages = make_entries sort apages
53
+ sorted_apages.each { |apage| add_apage apage }
54
+ self
55
+ end
56
+
57
+ def add_section(section)
58
+ return unless @options.sort_by == :order
59
+
60
+ @sections << section
61
+ self
62
+ end
63
+
64
+ def add_sections(sections)
65
+ sections.each { |x| add_section x }
66
+ @add_sections_called = true
67
+ self
68
+ end
69
+
70
+ def make_entries(docs)
71
+ docs.map do |doc|
72
+ draft = Jekyll::Draft.draft_html doc
73
+ JekyllSupport.apage_from(
74
+ date: doc.date,
75
+ description: doc.description,
76
+ draft: draft,
77
+ last_modified: doc.last_modified,
78
+ order: doc.order,
79
+ title: doc.title,
80
+ url: doc.url
81
+ )
82
+ end
83
+ end
84
+
85
+ # @param apages [APage]
86
+ # @return muliline String
87
+ def sort(apages)
88
+ if @options.sort_by == :order
89
+ apages.sort_by(&:order)
90
+ else
91
+ apages.sort_by { |apage| sort_property_value apage }
92
+ end
93
+ end
94
+
95
+ def to_s
96
+ return '' unless @sections&.count&.positive?
97
+
98
+ result = []
99
+ result << "<div class='outer_posts'>"
100
+ result << (@sections.map { |section| " #{section}" })
101
+ result << '</div>'
102
+ result << @options.attribution if @options.enable_attribution
103
+ result.join "\n"
104
+ end
105
+
106
+ private
107
+
108
+ def add_apage(apage)
109
+ unless apage
110
+ raise ::OutlineError, 'add_apage called with nil apage'
111
+ puts
112
+ end
113
+ raise ::OutlineError, 'add_apage called without first calling add_sections' unless @add_sections_called
114
+
115
+ section = section_for apage
116
+ section.add_child apage
117
+ end
118
+
119
+ def default_sort_value(sort_by)
120
+ case sort_by
121
+ when :date, :last_modified, :last_modified_at
122
+ Date.today
123
+ else
124
+ ''
125
+ end
126
+ end
127
+
128
+ # Obtain sort property value from APage instance, or return a default value
129
+ def sort_property_value(apage)
130
+ sort_by = @options.sort_by.to_s
131
+ if apage.data.key?(sort_by)
132
+ apage.data[sort_by] || default_sort_value(sort_by)
133
+ else
134
+ default_sort_value(sort_by)
135
+ end
136
+ end
137
+
138
+ # Only called when entries are organized into multiple sections
139
+ # @param apage must have a property called `order`
140
+ def section_for(apage)
141
+ return @sections.first if @sections.count == 1
142
+
143
+ last = @sections.length - 1
144
+ (0..last).each do |i|
145
+ return @sections.last if i == last
146
+
147
+ page_order = apage.order
148
+ this_section = @sections[i]
149
+ next_section = @sections[i + 1]
150
+ return this_section if (page_order >= this_section.order) && (page_order < next_section.order)
151
+ end
152
+ @sections.last
153
+ # raise OutlineError, "No Section found for APage #{apage}"
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,38 @@
1
+ require_relative 'a_page_enrichment'
2
+
3
+ module JekyllSupport
4
+ class Section
5
+ attr_accessor :children, :title, :order
6
+
7
+ def initialize(outline_options, parameter_array)
8
+ @outline_options = outline_options
9
+ @order = parameter_array[0].to_i
10
+ @title = parameter_array[1]
11
+ @children = []
12
+ end
13
+
14
+ def add_child(child)
15
+ @children << child
16
+ end
17
+
18
+ def to_s
19
+ return '' if @children.count.zero?
20
+
21
+ unless @children.first.instance_of?(JekyllSupport::APage)
22
+ raise "First child of Section was a #{@children.first.class}, not an APage"
23
+ end
24
+ apages = @children
25
+ .map { |x| x.render_outline_apage @outline_options.pattern }
26
+ .join("\n ")
27
+
28
+ <<~END_SECTION
29
+ <h3 class='post_title clear' id="title_#{@order}">#{@title}</h3>
30
+ <div id='posts_wrapper_#{@order}' class='clearfix'>
31
+ <div id="posts_#{@order}" class='posts'>
32
+ #{apages}
33
+ </div>
34
+ </div>
35
+ END_SECTION
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,58 @@
1
+ require 'yaml'
2
+ require_relative 'section'
3
+
4
+ class OutlineError < StandardError; end
5
+
6
+ module JekyllSupport
7
+ class YamlParser
8
+ attr_reader :sections
9
+
10
+ # @return array of empty Sections
11
+ def initialize(outline_options, content = '')
12
+ @logger = outline_options.logger
13
+ @sections = if content && !content.strip.empty?
14
+ parse_sections outline_options, content
15
+ else
16
+ []
17
+ end
18
+ end
19
+
20
+ # @return Array of Sections that do not contain children
21
+ def parse_sections(outline_options, content)
22
+ content = remove_leading_zeros remove_leading_spaces content
23
+ yaml = YAML.safe_load content
24
+ yaml.map { |entry| Section.new outline_options, entry }
25
+ rescue NoMethodError => e
26
+ raise OutlineError, <<~END_MSG
27
+ Invalid YAML within {% outline %} tag. The offending content was:
28
+
29
+ <pre>#{content}</pre>
30
+ END_MSG
31
+ rescue Psych::SyntaxError => e
32
+ msg = <<~END_MSG
33
+ Invalid YAML found within {% outline %} tag:<br>
34
+ <pre>#{e.message}</pre>
35
+ END_MSG
36
+ @logger.error { e.message }
37
+ raise OutlineError, msg
38
+ end
39
+
40
+ private
41
+
42
+ def remove_leading_spaces(multiline)
43
+ multiline
44
+ .strip
45
+ .split("\n")
46
+ .map { |x| x.gsub(/\A\s+/, '') }
47
+ .join("\n")
48
+ end
49
+
50
+ def remove_leading_zeros(multiline)
51
+ multiline
52
+ .strip
53
+ .split("\n")
54
+ .map { |x| x.gsub(/(?<= |\A)0+(?=\d)/, '') }
55
+ .join("\n")
56
+ end
57
+ end
58
+ end
data/spec/outline_spec.rb CHANGED
@@ -1,10 +1,152 @@
1
- require 'jekyll'
2
- require_relative '../lib/jekyll_outline'
1
+ require 'jekyll_plugin_support'
2
+ require 'rspec/match_ignoring_whitespace'
3
+ require_relative 'spec_helper'
4
+ require_relative '../lib/structure/outline'
3
5
 
4
- RSpec.describe(OutlineTag) do
5
- include Jekyll
6
+ RSpec.describe(JekyllSupport) do
7
+ outline_options = JekyllSupport::OutlineOptions.new(pattern: '<b> title </b> &ndash; <i> description </i>')
8
+ section1 = described_class::Section.new(outline_options, [0, 'Section 1'])
9
+ section2 = described_class::Section.new(outline_options, [3, 'Section 2'])
6
10
 
7
- it 'never works first time', skip: 'Just a placeholder' do
8
- expect(true).to be_truthy
11
+ apages = [
12
+ described_class.apage_from(
13
+ collection_name: '_posts',
14
+ date: '2023-01-01',
15
+ description: 'This is the entry1 description.',
16
+ draft: true,
17
+ last_modified: '2023-01-01',
18
+ order: 1,
19
+ title: 'Entry 1',
20
+ url: 'https://example.com/entry1'
21
+ ),
22
+ described_class.apage_from(
23
+ collection_name: '_posts',
24
+ date: '2023-01-02',
25
+ description: 'This is the entry2 description.',
26
+ last_modified: '2023-01-02',
27
+ order: 2,
28
+ title: 'Entry 2',
29
+ url: 'https://example.com/entry2'
30
+ ),
31
+ described_class.apage_from(
32
+ collection_name: '_posts',
33
+ date: '2023-10-03',
34
+ description: 'This is the entry3 description.',
35
+ last_modified: '2024-10-03',
36
+ order: 3,
37
+ title: 'Entry 3',
38
+ url: 'https://example.com/entry3'
39
+ ),
40
+ described_class.apage_from(
41
+ collection_name: '_posts',
42
+ date: '2023-10-04',
43
+ description: 'This is the entry4 description.',
44
+ draft: true,
45
+ last_modified: '2024-10-04',
46
+ order: 4,
47
+ title: 'Entry 4',
48
+ url: 'https://example.com/entry4'
49
+ ),
50
+ described_class.apage_from(
51
+ collection_name: '_posts',
52
+ date: '2023-10-05',
53
+ description: 'This is the entry5 description.',
54
+ last_modified: '2024-10-05',
55
+ order: 5,
56
+ title: 'Entry 5',
57
+ url: 'https://example.com/entry5'
58
+ ),
59
+ described_class.apage_from(
60
+ collection_name: '_posts',
61
+ date: '2023-10-06',
62
+ description: 'This is the entry6 description.',
63
+ last_modified: '2024-10-06',
64
+ order: 6,
65
+ title: 'Entry 6',
66
+ url: 'https://example.com/entry6'
67
+ )
68
+ ]
69
+
70
+ outline = described_class::Outline.new
71
+ outline.add_sections [section1, section2]
72
+ outline.add_entries apages
73
+
74
+ _attribution = <<~END_ATT
75
+ <div id="jps_attribute_570007" class="jps_attribute">
76
+ <div>
77
+ <a href="https://www.mslinn.com/jekyll_plugins/jekyll_outline.html" target="_blank" rel="nofollow">
78
+ Generated by the jekyll_outline v1.2.1 Jekyll plugin, written by Mike Slinn 2024-01-09.
79
+ </a>
80
+ </div>
81
+ </div>
82
+ END_ATT
83
+
84
+ expected_section1 = <<~END_EXPECTED
85
+ <h3 class='post_title clear' id="title_0">Section 1</h3>
86
+ <div id='posts_wrapper_0' class='clearfix'>
87
+ <div id="posts_0" class='posts'>
88
+ <span>2023-01-01</span>
89
+ <span><a href='https://example.com/entry1'><b> Entry 1 </b> &ndash; <i> This is the entry1 description. </i> </a> <i class='jekyll_draft'>Draft</i></span>
90
+
91
+ <span>2023-01-02</span>
92
+ <span><a href='https://example.com/entry2'><b> Entry 2 </b> &ndash; <i> This is the entry2 description. </i> </a></span>
93
+ </div>
94
+ </div>
95
+ END_EXPECTED
96
+
97
+ expected_section2 = <<~END_EXPECTED
98
+ <h3 class='post_title clear' id="title_3">Section 2</h3>
99
+ <div id='posts_wrapper_3' class='clearfix'>
100
+ <div id="posts_3" class='posts'>
101
+ <span>2024-10-03</span>
102
+ <span><a href='https://example.com/entry3'><b> Entry 3 </b> &ndash; <i> This is the entry3 description. </i> </a></span>
103
+
104
+ <span>2024-10-04</span>
105
+ <span><a href='https://example.com/entry4'><b> Entry 4 </b> &ndash; <i> This is the entry4 description. </i> </a> <i class='jekyll_draft'>Draft</i></span>
106
+
107
+ <span>2024-10-05</span>
108
+ <span><a href='https://example.com/entry5'><b> Entry 5 </b> &ndash; <i> This is the entry5 description. </i> </a></span>
109
+
110
+ <span>2024-10-06</span>
111
+ <span><a href='https://example.com/entry6'><b> Entry 6 </b> &ndash; <i> This is the entry6 description. </i> </a></span>
112
+ </div>
113
+ </div>
114
+ END_EXPECTED
115
+
116
+ it 'verifies initial values' do
117
+ expect(outline.options.sort_by).to eq(:order)
118
+ end
119
+
120
+ it 'sorts by :order' do
121
+ expect(outline.sort(apages)).to eq(apages)
122
+ end
123
+
124
+ it 'checks html shape' do
125
+ expect(outline.sections.count).to eq(2)
126
+ expect(outline.sections[0].children.count).to eq(2)
127
+ expect(outline.sections[1].children.count).to eq(4)
128
+ end
129
+
130
+ it 'verifies html for first section' do
131
+ actual = outline.sections[0].to_s
132
+ expect(expected_section1).to match_ignoring_whitespace(actual)
133
+ end
134
+
135
+ it 'verifies html for second section' do
136
+ actual = outline.sections[1].to_s
137
+ expect(expected_section2).to match_ignoring_whitespace(actual)
138
+ end
139
+
140
+ it 'verifies generated html' do
141
+ actual = outline.to_s
142
+
143
+ expected = <<~END_EXPECTED
144
+ <div class='outer_posts'>
145
+ #{expected_section1}
146
+ #{expected_section2}
147
+ </div>
148
+ END_EXPECTED
149
+
150
+ expect(expected).to match_ignoring_whitespace(actual)
9
151
  end
10
152
  end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,23 @@
1
1
  require 'jekyll'
2
- require_relative '../lib/jekyll_outline'
3
2
 
4
- RSpec.configure do |config|
5
- config.filter_run_when_matching focus: true
6
- config.order = 'random'
3
+ # For testing Jekyll plugins based on jekyll_plugin_support:
4
+ require 'jekyll_plugin_logger'
5
+ require 'jekyll_plugin_support'
7
6
 
7
+ RSpec.configure do |config|
8
8
  # See https://relishapp.com/rspec/rspec-core/docs/command-line/only-failures
9
9
  config.example_status_persistence_file_path = 'spec/status_persistence.txt'
10
+
11
+ # See https://rspec.info/features/3-12/rspec-core/filtering/filter-run-when-matching/
12
+ # and https://github.com/rspec/rspec/issues/221
13
+ config.filter_run_when_matching :focus
14
+
15
+ # Other values: :progress, :html, :json, CustomFormatterClass
16
+ config.formatter = :documentation
17
+
18
+ # See https://rspec.info/features/3-12/rspec-core/command-line/order/
19
+ config.order = :defined
20
+
21
+ # See https://www.rubydoc.info/github/rspec/rspec-core/RSpec%2FCore%2FConfiguration:pending_failure_output
22
+ config.pending_failure_output = :skip
10
23
  end
@@ -1,3 +1,3 @@
1
- example_id | status | run_time |
2
- --------------------------- | ------- | --------------- |
3
- ./spec/outline_spec.rb[1:1] | pending | 0.00001 seconds |
1
+ example_id | status | run_time |
2
+ ---------------------------------- | ------ | --------------- |
3
+ ./spec/jekyll_outline_spec.rb[1:1] | passed | 0.00023 seconds |
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require 'rspec/match_ignoring_whitespace'
3
+ require_relative '../lib/structure/outline'
4
+ require_relative '../lib/structure/yaml_parser'
5
+
6
+ module JekyllSupport
7
+ logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
8
+ outline_options = OutlineOptions.new
9
+
10
+ # includes leading zeros and leading spaces, which are invalid in YAML
11
+ yaml_parser_big = YamlParser.new outline_options, <<~END_DATA
12
+ 0: Production Infrastructure
13
+ 0015000: Audio
14
+ 20000: Video
15
+ 30000: RME
16
+ 40000: OBS Studio
17
+ 50000: Pro Tools
18
+ 55000: Ableton Live &amp; Push
19
+ 60000: Other Music Software
20
+ 70000: MIDI Hardware &amp; Software
21
+ 80000: Davinci Resolve
22
+ 100000: Computer Analysis
23
+ 200000: Music Theory
24
+ 300000: Business
25
+ 400000: General
26
+ END_DATA
27
+
28
+ RSpec.describe(YamlParser) do
29
+ it 'handles no section headings' do
30
+ yaml_parser = described_class.new outline_options
31
+ sections = yaml_parser.sections
32
+ expect(sections.count).to equal(0)
33
+
34
+ yaml_parser = described_class.new outline_options, ''
35
+ sections = yaml_parser.sections
36
+ expect(sections.count).to equal(0)
37
+ end
38
+
39
+ it 'finds only 1 section heading' do
40
+ yaml_parser = described_class.new outline_options, '0: General'
41
+ sections = yaml_parser.sections
42
+ expect(sections.count).to equal(1)
43
+ # expect(sections.first)
44
+
45
+ yaml_parser = described_class.new outline_options, <<~END_DATA
46
+ 0: General
47
+ END_DATA
48
+ sections = yaml_parser.sections
49
+ expect(sections.count).to equal(1)
50
+
51
+ # Handle leading space (this is invalid YAML) and the lack of an end of line character
52
+ yaml_parser = described_class.new outline_options, ' 0: General'
53
+ sections = yaml_parser.sections
54
+ expect(sections.count).to equal(1)
55
+ end
56
+
57
+ it 'finds matching sections' do
58
+ sections = yaml_parser_big.sections
59
+ expect(sections.count).to equal(14)
60
+
61
+ section1 = sections.first
62
+ expect(section1.order).to eq(0)
63
+ expect(section1.title).to eq('Production Infrastructure')
64
+
65
+ outline = Outline.new.add_sections sections
66
+
67
+ apage0 = JekyllSupport.apage_from(date: Time.now, logger: logger, order: 0)
68
+ actual_section = outline.send :section_for, apage0
69
+ expect(actual_section.order).to eq(0)
70
+
71
+ apage1000 = JekyllSupport.apage_from(date: Time.now, logger: logger, order: 1000)
72
+ actual_section = outline.send :section_for, apage1000
73
+ expect(actual_section.order).to eq(0)
74
+
75
+ apage16000 = JekyllSupport.apage_from(date: Time.now, logger: logger, order: 16_000)
76
+ actual_section = outline.send :section_for, apage16000
77
+ expect(actual_section.order).to eq(15_000)
78
+
79
+ apage25000 = JekyllSupport.apage_from(date: Time.now, logger: logger, order: 25_000)
80
+ actual_section = outline.send :section_for, apage25000
81
+ expect(actual_section.order).to eq(20_000)
82
+ end
83
+ end
84
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll_outline
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.6
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Slinn
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: jekyll
@@ -15,42 +15,42 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 3.5.0
18
+ version: 4.4.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 3.5.0
25
+ version: 4.4.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: jekyll_draft
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 2.0.2
32
+ version: 3.0.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 2.0.2
39
+ version: 3.0.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: jekyll_plugin_support
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 1.0.2
46
+ version: 3.1.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 1.0.2
53
+ version: 3.1.0
54
54
  description: 'Jekyll tag plugin that creates a clickable table of contents.
55
55
 
56
56
  '
@@ -71,9 +71,14 @@ files:
71
71
  - lib/jekyll_outline/version.rb
72
72
  - lib/outline_js.rb
73
73
  - lib/outline_tag.rb
74
+ - lib/structure/a_page_enrichment.rb
75
+ - lib/structure/outline.rb
76
+ - lib/structure/section.rb
77
+ - lib/structure/yaml_parser.rb
74
78
  - spec/outline_spec.rb
75
79
  - spec/spec_helper.rb
76
80
  - spec/status_persistence.txt
81
+ - spec/yaml_parser_spec.rb
77
82
  homepage: https://www.mslinn.com/jekyll_plugins/jekyll_outline.html
78
83
  licenses:
79
84
  - MIT
@@ -101,11 +106,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
106
  - !ruby/object:Gem::Version
102
107
  version: '0'
103
108
  requirements: []
104
- rubygems_version: 3.6.3
109
+ rubygems_version: 3.6.9
105
110
  specification_version: 4
106
111
  summary: Jekyll tag plugin that creates a clickable table of contents.
107
112
  test_files:
108
113
  - spec/outline_spec.rb
109
114
  - spec/spec_helper.rb
110
115
  - spec/status_persistence.txt
116
+ - spec/yaml_parser_spec.rb
111
117
  ...