jekyll-documents 0.3.2 → 0.4.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: 2c05b94feef92602870b24fd7b9c4e9244eb724061c1ef599ed12a23905d51ad
4
- data.tar.gz: ccfe08306d471478e21092fd7518df1c6d593b05fa219051a6d866d3c947c6c9
3
+ metadata.gz: 76640be0741d32137905f8281bc4e83e4e895971a9ad39e7f360bccf06ba5eef
4
+ data.tar.gz: 93ac35f0606b84b474ef6aff260e21c7dab78ce5e4a8f7f80b4b97493755ae8d
5
5
  SHA512:
6
- metadata.gz: '087a677f0f1efd8c54d7b52c05c1fd0b88c746e9063f8fc0ba843c5ca24db53b8cbdd6904a959d7ca606e490bf8da590ff9b400b21a9de25204079ed0f0b0eff'
7
- data.tar.gz: 7519b8f1e7035b4391299dd6399dcb1a6fa6572d4b3567891e35fc3f383a7b2958acc28bdbfe058983d6fbf73592c33a609dd07ae082751410873d8d29ac1434
6
+ metadata.gz: db8fa1ad5fd90e910238d8b3b41427d8eeb8451ba70ca095cd610e26b56db88ae564e32856b6890c8c1e8af7b4a294fa8a9a9e7219fdd157bc7b0d80aa0c4181
7
+ data.tar.gz: a19a1da88694fd8282f3040a3bdad01ca5627667a116a984d16d916a875a346bb156bb371374b6b0ae241b6cbe2a97c8ecfc8cb40a1728960243ce1359e60e97
data/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.0] - 2026-08-26
6
+
7
+ ### Added
8
+ - `categories` array baked into each document's data (in addition to the existing singular `category`) for compatibility with search plugins like `jekyll-client-search` that expect the plural Jekyll convention
9
+ - Searchable content string (title, category, file type, date) set as document content so client-side search engines can index uploaded documents
10
+
11
+ ### Changed
12
+ - Refactored `Generator#generate` by extracting `bake_document_data` and `searchable_content` helper methods to keep method length within RuboCop limits
13
+
14
+ ### Changed
15
+ - Simplified release workflow to tag-push trigger (`push: tags: v*`) — no manual `gh release create` needed
16
+ - Switched to RubyGems trusted publishing (`rubygems/release-gem@v1` with OIDC)
17
+ - Centralized version in `version.rb` as single source of truth — removed `version` field from `package.json` (was drifted to 0.3.1)
18
+ - Added `rake version:bump` and `rake version:check_changelog` tasks
19
+ - Added CHANGELOG gate to release workflow (fails if entry missing for the version)
20
+ - Added `npm audit` and `npm outdated` (non-blocking) to CI
21
+ - Replaced `setup_hooks.sh` with `bin/install-hooks.sh` (pre-commit: rubocop only, pre-push: rubocop + rspec)
22
+ - Dropped reek from gemspec, Rakefile, and deleted `.reek.yml` (KISS)
23
+ - Deleted `bump_version.sh`, `release.sh`, `rollback.sh` — replaced by rake tasks and release workflow
24
+ - Renamed `AI_INSTRUCTIONS.md` to `AGENTS.md` (agents.md open convention)
25
+ - Updated `CLAUDE.md` and `.windsurfrules` to point to `AGENTS.md`
26
+
27
+ ## [0.3.3] - 2026-08-12
28
+
29
+ ### Added
30
+ - `{% doc_link "title" %}` Liquid tag for linking to documents by partial title or slug match (case-insensitive); renders file icon, title, and human-readable file size; supports `text:"custom label"`, `icon:false`, and `size:false` options
31
+ - `{% doc_category "name" %}` Liquid tag for linking to category pages or listing documents by category; supports `text:"label"`, `list:true`, and `limit:N` options
32
+ - `file_size` attribute baked into each document's data at generation time
33
+ - `file_url` and `file_size` fields included in the generated `/documents.json` index
34
+ - 30 RSpec examples covering `DocLinkTag` and `DocCategoryTag` (exact/partial matching, slug matching, icon/size rendering, custom text, baseurl, HTML escaping, list mode, limit, sorting, empty collection handling)
35
+
5
36
  ## [0.3.2] - 2026-08-12
6
37
 
7
38
  ### Added
data/README.md CHANGED
@@ -74,12 +74,41 @@ The tag applies the configured icon set and site `baseurl` automatically. The `f
74
74
 
75
75
  ## Usage
76
76
 
77
+ ### Includes
78
+
77
79
  ```liquid
78
80
  {% include latest_documents.html count=5 %}
79
81
  {% include documents_list.html %}
80
82
  {% include documents_search.html %}
81
83
  ```
82
84
 
85
+ ### Liquid Tags
86
+
87
+ **Link to a document** by title or slug (partial match, case-insensitive):
88
+
89
+ ```liquid
90
+ {% doc_link "Annual Report" %}
91
+ {% doc_link "annual" text:"Read the report" %}
92
+ {% doc_link "board-meeting" icon:false size:false %}
93
+ ```
94
+
95
+ Renders an `<a>` tag with the file type icon, title, and human-readable file size.
96
+ Use `text:"..."` to override the link text, `icon:false` to hide the icon,
97
+ or `size:false` to hide the file size.
98
+
99
+ **Link to or list a category**:
100
+
101
+ ```liquid
102
+ {% doc_category "reports" %}
103
+ {% doc_category "reports" text:"All reports" %}
104
+ {% doc_category "reports" list:true %}
105
+ {% doc_category "reports" list:true limit:5 %}
106
+ ```
107
+
108
+ Link mode renders an `<a>` tag to the category page.
109
+ List mode renders a `<ul>` of all documents in the category, sorted by date descending.
110
+ Use `limit:N` to cap the number of items.
111
+
83
112
  ## Configuration
84
113
 
85
114
  ```yaml
@@ -110,8 +139,13 @@ See [README.Development.md](README.Development.md) for details.
110
139
  ## Release
111
140
 
112
141
  ```bash
113
- ./bump_version.sh patch
114
- ./release.sh
142
+ bundle exec rake "version:bump[patch]"
143
+ # Edit CHANGELOG.md
144
+ git add lib/jekyll/documents/version.rb CHANGELOG.md
145
+ git commit -m "Release X.Y.Z: summary"
146
+ git tag -a vX.Y.Z -m "Release X.Y.Z"
147
+ git push origin main
148
+ git push origin vX.Y.Z
115
149
  ```
116
150
 
117
151
  ## License
@@ -35,7 +35,6 @@ Gem::Specification.new do |spec|
35
35
 
36
36
  spec.add_development_dependency "bundler-audit", "~> 0.9"
37
37
  spec.add_development_dependency "rake", "~> 13.0"
38
- spec.add_development_dependency "reek", "~> 6.5"
39
38
  spec.add_development_dependency "rspec", "~> 3.13"
40
39
  spec.add_development_dependency "rubocop", "~> 1.88"
41
40
  spec.add_development_dependency "rubocop-performance", "~> 1.26"
@@ -55,23 +55,10 @@ module Jekyll
55
55
  collection: collection
56
56
  )
57
57
 
58
- data = doc.data
59
- data["layout"] = @config["layout"]
60
- data["title"] = title
61
- data["date"] = date ? date.to_time : File.mtime(path)
62
- data["category"] = remap_category(category)
63
- data["file_url"] = "/#{rel_path}"
64
- data["extension"] = ext
65
- data["file_type"] = file_type
66
- data["icon_set"] = icon_set
67
- data["icon_url"] = Jekyll::Documents::FileTypeIcons.icon_for(file_type, icon_set)
68
- data["slug"] = slug
69
- data["permalink"] = @config["permalink"]
70
- .gsub(":category", data["category"].to_s)
71
- .gsub(":slug", slug)
72
-
73
- doc.content = "Auto-generated document page."
74
-
58
+ file_info = { title: title, date: date, category: category,
59
+ rel_path: rel_path, ext: ext, file_type: file_type,
60
+ icon_set: icon_set, slug: slug, path: path }
61
+ bake_document_data(doc, file_info)
75
62
  collection.docs << doc
76
63
  end
77
64
  end
@@ -91,6 +78,33 @@ module Jekyll
91
78
  site.collections[label]
92
79
  end
93
80
 
81
+ def searchable_content(title, data, file_type)
82
+ date_str = data["date"].strftime("%Y-%m-%d")
83
+ "#{title} #{data['category']} #{file_type} #{date_str}"
84
+ end
85
+
86
+ def bake_document_data(doc, info)
87
+ data = doc.data
88
+ category = remap_category(info[:category])
89
+ data["layout"] = @config["layout"]
90
+ data["title"] = info[:title]
91
+ data["date"] = info[:date] ? info[:date].to_time : File.mtime(info[:path])
92
+ data["category"] = category
93
+ data["categories"] = [category] if category
94
+ data["file_url"] = "/#{info[:rel_path]}"
95
+ data["extension"] = info[:ext]
96
+ data["file_type"] = info[:file_type]
97
+ data["icon_set"] = info[:icon_set]
98
+ icon = FileTypeIcons.icon_for(info[:file_type], info[:icon_set])
99
+ data["icon_url"] = icon
100
+ data["file_size"] = File.size(info[:path])
101
+ data["slug"] = info[:slug]
102
+ data["permalink"] = @config["permalink"]
103
+ .gsub(":category", category.to_s)
104
+ .gsub(":slug", info[:slug])
105
+ doc.content = searchable_content(info[:title], data, info[:file_type])
106
+ end
107
+
94
108
  # Creates a virtual source path for the document
95
109
  # @param basename [String] the file basename
96
110
  # @param category [String] the document category
@@ -26,7 +26,9 @@ module Jekyll
26
26
  "slug" => data["slug"],
27
27
  "file_type" => data["file_type"],
28
28
  "extension" => data["extension"],
29
- "icon_url" => data["icon_url"]
29
+ "icon_url" => data["icon_url"],
30
+ "file_url" => data["file_url"],
31
+ "file_size" => data["file_size"]
30
32
  }
31
33
  end
32
34
 
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Documents
5
+ class DocCategoryTag < Liquid::Tag
6
+ public_class_method :new
7
+
8
+ include Jekyll::Filters::URLFilters
9
+
10
+ def initialize(tag_name, markup, tokens)
11
+ super
12
+ @category, @options = parse_markup(markup)
13
+ end
14
+
15
+ def render(context)
16
+ docs = documents_from(context)
17
+ return "" if docs.empty?
18
+
19
+ categories = available_categories(docs)
20
+ category = resolve_category(@category, categories)
21
+ return "" unless category
22
+
23
+ @context = context
24
+ return render_list(docs, category) if @options["list"] == "true"
25
+
26
+ render_link(category)
27
+ end
28
+
29
+ private
30
+
31
+ def documents_from(context)
32
+ site = context.registers[:site]
33
+ site.collections["documents"]&.docs || []
34
+ end
35
+
36
+ def available_categories(docs)
37
+ docs.map { |doc| doc.data["category"].to_s }.uniq.sort
38
+ end
39
+
40
+ def resolve_category(query, categories)
41
+ normalized = query.to_s.strip.downcase
42
+ return nil if normalized.empty?
43
+
44
+ categories.find { |cat| cat.downcase == normalized } ||
45
+ categories.find { |cat| cat.downcase.include?(normalized) }
46
+ end
47
+
48
+ def render_link(category)
49
+ url = relative_url("/documents/#{category}/")
50
+ text = @options["text"] || category
51
+ %(<a href="#{escape_html(url)}">#{escape_html(text)}</a>)
52
+ end
53
+
54
+ def render_list(docs, category)
55
+ cat_docs = sorted_category_docs(docs, category)
56
+ limit = @options["limit"]&.to_i
57
+ cat_docs = cat_docs.first(limit) if limit&.positive?
58
+
59
+ out = %(<ul class="doc-category-list" data-category="#{escape_html(category)}">\n)
60
+ out << list_items(cat_docs)
61
+ out << "</ul>\n"
62
+ end
63
+
64
+ def sorted_category_docs(docs, category)
65
+ docs.select { |doc| doc.data["category"].to_s == category }
66
+ .sort_by { |doc| doc.data["date"] || Time.at(0) }.reverse
67
+ end
68
+
69
+ def list_items(cat_docs)
70
+ cat_docs.each_with_object(+"") do |doc, out|
71
+ title = escape_html(doc.data["title"])
72
+ url = escape_html(relative_url(doc.url))
73
+ date = (doc.data["date"] || Time.at(0)).strftime("%Y-%m-%d")
74
+ out << %(<li><a href="#{url}">#{title}</a> <small>(#{date})</small></li>\n)
75
+ end
76
+ end
77
+
78
+ def parse_markup(markup)
79
+ text = markup.to_s
80
+ category, remainder = extract_category(text)
81
+ options = extract_options(remainder)
82
+ [category, options]
83
+ end
84
+
85
+ def extract_category(text)
86
+ quoted = text.match(/\A["']([^"']+)["']/)
87
+ return [quoted[1], text[quoted.end(0)..]] if quoted
88
+
89
+ bare = text.strip.match(/\A([^\s]+)/)
90
+ return [nil, ""] unless bare
91
+
92
+ [bare[1], text[bare.end(0)..]]
93
+ end
94
+
95
+ def extract_options(text)
96
+ options = {}
97
+ text.to_s.scan(
98
+ /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
99
+ ) do |key, single, double, bare|
100
+ options[key] = single || double || bare
101
+ end
102
+ options
103
+ end
104
+
105
+ def escape_html(value)
106
+ value.to_s.gsub(/[&<>"']/,
107
+ "&" => "&amp;",
108
+ "<" => "&lt;",
109
+ ">" => "&gt;",
110
+ '"' => "&quot;",
111
+ "'" => "&#39;")
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ Liquid::Template.register_tag("doc_category", Jekyll::Documents::DocCategoryTag)
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Documents
5
+ class DocLinkTag < Liquid::Tag
6
+ public_class_method :new
7
+
8
+ include Jekyll::Filters::URLFilters
9
+
10
+ SIZE_UNITS = %w[B KB MB GB].freeze
11
+
12
+ def initialize(tag_name, markup, tokens)
13
+ super
14
+ @query, @options = parse_markup(markup)
15
+ end
16
+
17
+ def render(context)
18
+ docs = documents_from(context)
19
+ return "" if docs.empty?
20
+
21
+ match = find_document(docs, @query)
22
+ return "" unless match
23
+
24
+ @context = context
25
+ url = relative_url(match.url)
26
+ text = @options["text"] || match.data["title"]
27
+
28
+ build_link(match, url, text)
29
+ end
30
+
31
+ private
32
+
33
+ def documents_from(context)
34
+ site = context.registers[:site]
35
+ site.collections["documents"]&.docs || []
36
+ end
37
+
38
+ def find_document(docs, query)
39
+ normalized = query.to_s.strip.downcase
40
+ return nil if normalized.empty?
41
+
42
+ docs.find { |doc| matches?(doc, normalized) }
43
+ end
44
+
45
+ def matches?(doc, query)
46
+ title = doc.data["title"].to_s.downcase
47
+ slug = doc.data["slug"].to_s.downcase
48
+ title == query || slug == query ||
49
+ title.include?(query) || slug.include?(query)
50
+ end
51
+
52
+ def build_link(doc, url, text)
53
+ inner = +""
54
+ inner << icon_html(doc) if @options["icon"] != "false"
55
+ inner << %(<span class="doc-link-title">#{escape_html(text)}</span>)
56
+ inner << size_html(doc) if @options["size"] != "false"
57
+ %(<a href="#{escape_html(url)}" class="doc-link">#{inner}</a>)
58
+ end
59
+
60
+ def icon_html(doc)
61
+ icon_url = doc.data["icon_url"]
62
+ return "" unless icon_url
63
+
64
+ url = relative_url(icon_url)
65
+ file_type = doc.data["file_type"].to_s.upcase
66
+ "<img src=\"#{escape_html(url)}\" alt=\"#{file_type}\" class=\"file-icon doc-link-icon\" />"
67
+ end
68
+
69
+ def size_html(doc)
70
+ size = doc.data["file_size"]
71
+ return "" unless size
72
+
73
+ "<span class=\"doc-link-size has-text-grey is-size-7\">#{format_size(size)}</span>"
74
+ end
75
+
76
+ def format_size(bytes)
77
+ return "0 B" unless bytes&.positive?
78
+
79
+ value, unit = compute_size(bytes)
80
+ unit == "B" ? "#{value.to_i} B" : format("%<value>.1f %<unit>s", value:, unit:)
81
+ end
82
+
83
+ def compute_size(bytes)
84
+ value = bytes.to_f
85
+ SIZE_UNITS.each do |unit|
86
+ return [value, unit] if value < 1024 || unit == SIZE_UNITS.last
87
+
88
+ value /= 1024
89
+ end
90
+ end
91
+
92
+ def parse_markup(markup)
93
+ text = markup.to_s
94
+ query, remainder = extract_query(text)
95
+ options = extract_options(remainder)
96
+ [query, options]
97
+ end
98
+
99
+ def extract_query(text)
100
+ quoted = text.match(/\A["']([^"']+)["']/)
101
+ return [quoted[1], text[quoted.end(0)..]] if quoted
102
+
103
+ bare = text.strip.match(/\A([^\s]+)/)
104
+ return [nil, ""] unless bare
105
+
106
+ [bare[1], text[bare.end(0)..]]
107
+ end
108
+
109
+ def extract_options(text)
110
+ options = {}
111
+ text.to_s.scan(
112
+ /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
113
+ ) do |key, single, double, bare|
114
+ options[key] = single || double || bare
115
+ end
116
+ options
117
+ end
118
+
119
+ def escape_html(value)
120
+ value.to_s.gsub(/[&<>"']/,
121
+ "&" => "&amp;",
122
+ "<" => "&lt;",
123
+ ">" => "&gt;",
124
+ '"' => "&quot;",
125
+ "'" => "&#39;")
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ Liquid::Template.register_tag("doc_link", Jekyll::Documents::DocLinkTag)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Documents
5
- VERSION = "0.3.2"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -14,6 +14,8 @@ require_relative "jekyll/documents/layout_registrar"
14
14
  # Liquid tag(s)
15
15
  require_relative "jekyll/documents/tags/latest_documents"
16
16
  require_relative "jekyll/documents/tags/document_icon"
17
+ require_relative "jekyll/documents/tags/doc_link"
18
+ require_relative "jekyll/documents/tags/doc_category"
17
19
 
18
20
  module Jekyll
19
21
  module Documents
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-documents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-08-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: jekyll
@@ -58,20 +57,6 @@ dependencies:
58
57
  - - "~>"
59
58
  - !ruby/object:Gem::Version
60
59
  version: '13.0'
61
- - !ruby/object:Gem::Dependency
62
- name: reek
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '6.5'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '6.5'
75
60
  - !ruby/object:Gem::Dependency
76
61
  name: rspec
77
62
  requirement: !ruby/object:Gem::Requirement
@@ -266,6 +251,8 @@ files:
266
251
  - lib/jekyll/documents/generator.rb
267
252
  - lib/jekyll/documents/json_index_generator.rb
268
253
  - lib/jekyll/documents/layout_registrar.rb
254
+ - lib/jekyll/documents/tags/doc_category.rb
255
+ - lib/jekyll/documents/tags/doc_link.rb
269
256
  - lib/jekyll/documents/tags/document_icon.rb
270
257
  - lib/jekyll/documents/tags/latest_documents.rb
271
258
  - lib/jekyll/documents/utils.rb
@@ -280,7 +267,6 @@ metadata:
280
267
  documentation_uri: https://github.com/gundestrup/jekyll-documents
281
268
  homepage_uri: https://github.com/gundestrup/jekyll-documents
282
269
  rubygems_mfa_required: 'true'
283
- post_install_message:
284
270
  rdoc_options: []
285
271
  require_paths:
286
272
  - lib
@@ -295,8 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
295
281
  - !ruby/object:Gem::Version
296
282
  version: '0'
297
283
  requirements: []
298
- rubygems_version: 3.5.22
299
- signing_key:
284
+ rubygems_version: 3.6.9
300
285
  specification_version: 4
301
286
  summary: Auto-generate Jekyll pages for documents (PDF/DOCX/...) with category/date/title
302
287
  parsing.