jekyll-documents 0.7.1 → 0.7.3

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: aa2578d6c2919197cbd6a80f56d4b93a55e3a41c6bc9801b6344094460a5e764
4
- data.tar.gz: 2905501bf1a5d4d7050852dbb7293d179cd9abd6a11218a7649f155ded19109e
3
+ metadata.gz: aa6c0e2569d3772c02fe174700001a8290d7b313e7b068781d21627172d9e785
4
+ data.tar.gz: 407cc042bf5b2de1fe1b01cf9c9043bd961c811de7d0e6ee2dc896b1ab29e1dc
5
5
  SHA512:
6
- metadata.gz: 899348532a7c8df6f816ed8795070c63881999f7f01b060332ae80f4b14ff183864f5543a1b63b20b461a1650cef16642ab4739148e9af763c590f55b416d357
7
- data.tar.gz: e94844d7dd1614560fbbe0f8fe11d46a88f99e461aa778fc61cfc4765dfa26188d96986eebc46e693054ec3c6c4e3eb2a0661053c88278436cd1826000f3ed28
6
+ metadata.gz: d9016ce9608b013d8b0b6ac4d3756512d99a17cb55d5c2d2d67e096ccb5175e07ca40a17c68629dd09c684b5291fcad0ea1b876d74e7e58b0fedff0a7d1b97c7
7
+ data.tar.gz: f13021dccbed117f29e6c3f2c3d70f9ba9c499f3008ba30083f69c710ba3b30753a72009e3b84fa7fed64c3c73e8a85d20630cbe26de46623a38fb7766cd3986
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.3] - 2026-09-10
4
+
5
+ ### Added
6
+ - Added Semgrep CE security scanning with a custom ReDoS detection rule for pre-commit and CI
7
+ - Added CodeFactor code quality analysis badge and integration
8
+ - Added SEMGREP_APP_TOKEN integration for Cloud dashboard sync from CI
9
+ - Added `rake version:pre_release` task for pre-release security gate with FORCE=1 override
10
+
11
+ ### Changed
12
+ - Updated CI to Node.js 26 and current Node 24-based GitHub Actions runtimes
13
+ - Pinned all GitHub Actions to commit SHAs to prevent supply-chain attacks
14
+ - Added cooldown period to Dependabot configuration
15
+ - Pinned Lunr.js CDN to version 2.3.9 with SRI integrity hash
16
+ - Bumped actions/checkout to v7.0.1 (SHA-pinned)
17
+ - Reduced method complexity across 6 flagged CodeFactor findings:
18
+ - `generator.rb`: Extracted `process_file` and `merge_passthrough_fields` from `generate` and `configure_client_search`
19
+ - `doc_link.rb`: Extracted `match_documents` and `report_ambiguous` from `find_document`
20
+ - `doc_category.rb`: Extracted `match_categories` and `report_ambiguous_category` from `resolve_category`
21
+ - `latest_documents.rb`: Extracted `render_list` and `list_item` from `render`
22
+ - `options_parser.rb`: Extracted `read_value` from `parse_options`
23
+
24
+ ### Fixed
25
+ - Replaced dangerous subshell in system spec with Open3.capture2
26
+
27
+ ## [0.7.2] - 2026-09-09
28
+
29
+ ### Fixed
30
+ - Replaced polynomial-time Liquid tag option regexes with a shared linear-time parser
31
+
3
32
  ## [0.7.1] - 2026-09-09
4
33
 
5
34
  ### Changed
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![Gem Downloads](https://img.shields.io/gem/dt/jekyll-documents)](https://rubygems.org/gems/jekyll-documents)
7
7
  [![License: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-blue)](LICENSE)
8
8
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/gundestrup/jekyll-documents)
9
+ [![CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-documents/badge)](https://www.codefactor.io/repository/github/gundestrup/jekyll-documents)
10
+ [![Semgrep CE](https://img.shields.io/badge/Semgrep_CE-security-success)](https://github.com/gundestrup/jekyll-documents/security/code-scanning)
9
11
 
10
12
  Turn files in `assets/documents/` into browsable document pages.
11
13
 
@@ -9,6 +9,8 @@
9
9
  window.__jekyllDocumentsIndexPath = {{ index_path | relative_url | jsonify }};
10
10
  window.__jekyllDocumentsBaseurl = {{ site.baseurl | jsonify }};
11
11
  </script>
12
- <script src="https://unpkg.com/lunr/lunr.js"></script>
12
+ <script src="https://unpkg.com/lunr@2.3.9/lunr.js"
13
+ integrity="sha384-Mrc0XsDxndfJgV5jLnEeThch4j9dNEXAE1vbWeyzmERxSvF8siQfuEczzgUelQQH"
14
+ crossorigin="anonymous"></script>
13
15
  <script src="{{ '/assets/js/documents-search.js' | relative_url }}"></script>
14
16
  {% endif %}
@@ -43,39 +43,10 @@ module Jekyll
43
43
  Dir.glob("#{root}/**/*").each do |path|
44
44
  next unless File.file?(path)
45
45
 
46
- source_extension = File.extname(path)
47
- ext = source_extension.downcase
48
- if @config["strict_extensions"] && !@config["include_extensions"].include?(ext)
49
- ::Jekyll.logger.abort_with "jekyll-documents",
50
- "Unsupported file type: #{path} (#{ext})"
51
- end
52
- next unless @config["include_extensions"].include?(ext)
46
+ doc, rel_path = process_file(path, root, collection)
47
+ next unless doc
53
48
 
54
- source_path = source_path_for(path, root)
55
- category_path = document_category_path(source_path)
56
- category = remap_category(category_path)
57
- rel_path = normalize_path(File.join(@config["root"], source_path))
58
49
  current_paths << rel_path
59
- basename = File.basename(path, source_extension)
60
-
61
- date, title, valid = parse_filename(basename)
62
- if !valid && @config["strict_filename"]
63
- ::Jekyll.logger.abort_with "jekyll-documents",
64
- "Filename must be 'YYYY-MM-DD_Title.ext' → #{rel_path}"
65
- end
66
-
67
- doc = ::Jekyll::Document.new(
68
- source_stub_for(source_path),
69
- site: site,
70
- collection: collection
71
- )
72
- file_info = {
73
- title: title, date: date, category: category, category_path: category_path,
74
- category_slug: slugify(category, "uncategorized"), source_path: source_path,
75
- rel_path: rel_path, ext: ext, file_type: ext.delete_prefix("."),
76
- icon_set: @config["icon_set"], slug: build_slug(basename), path: path
77
- }
78
- bake_document_data(doc, file_info)
79
50
  collection.docs << doc
80
51
  generated_docs << doc
81
52
  end
@@ -87,6 +58,42 @@ module Jekyll
87
58
 
88
59
  private
89
60
 
61
+ def process_file(path, root, collection)
62
+ source_extension = File.extname(path)
63
+ ext = source_extension.downcase
64
+ if @config["strict_extensions"] && !@config["include_extensions"].include?(ext)
65
+ ::Jekyll.logger.abort_with "jekyll-documents",
66
+ "Unsupported file type: #{path} (#{ext})"
67
+ end
68
+ return [nil, nil] unless @config["include_extensions"].include?(ext)
69
+
70
+ source_path = source_path_for(path, root)
71
+ rel_path = normalize_path(File.join(@config["root"], source_path))
72
+ basename = File.basename(path, source_extension)
73
+
74
+ date, title, valid = parse_filename(basename)
75
+ if !valid && @config["strict_filename"]
76
+ ::Jekyll.logger.abort_with "jekyll-documents",
77
+ "Filename must be 'YYYY-MM-DD_Title.ext' → #{rel_path}"
78
+ end
79
+
80
+ category_path = document_category_path(source_path)
81
+ category = remap_category(category_path)
82
+ doc = ::Jekyll::Document.new(
83
+ source_stub_for(source_path),
84
+ site: @site,
85
+ collection: collection
86
+ )
87
+ file_info = {
88
+ title: title, date: date, category: category, category_path: category_path,
89
+ category_slug: slugify(category, "uncategorized"), source_path: source_path,
90
+ rel_path: rel_path, ext: ext, file_type: ext.delete_prefix("."),
91
+ icon_set: @config["icon_set"], slug: build_slug(basename), path: path
92
+ }
93
+ bake_document_data(doc, file_info)
94
+ [doc, rel_path]
95
+ end
96
+
90
97
  # Auto-injects passthrough_fields into client_search config when the
91
98
  # documents collection is indexed. Only acts if client_search is already
92
99
  # configured with +documents+ in its collections list — does nothing if
@@ -96,13 +103,17 @@ module Jekyll
96
103
  return unless search_config.is_a?(Hash)
97
104
  return unless Array(search_config["collections"]).include?("documents")
98
105
 
106
+ merge_passthrough_fields(search_config)
107
+ search_config["icon_field"] = "icon_url" unless search_config.key?("icon_field")
108
+ end
109
+
110
+ def merge_passthrough_fields(search_config)
99
111
  fields = search_config["passthrough_fields"] || []
100
112
  existing = fields.flat_map { |f| f.is_a?(Hash) ? f.keys : [f.to_s] }
101
113
  %w[file_type icon_url icon_set].each do |field|
102
114
  fields << field unless existing.include?(field)
103
115
  end
104
116
  search_config["passthrough_fields"] = fields
105
- search_config["icon_field"] = "icon_url" unless search_config.key?("icon_field")
106
117
  end
107
118
 
108
119
  # Ensures a collection exists and is configured for output
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Jekyll
6
+ module Documents
7
+ # Linear-time parser for Liquid tag markup key:value pairs.
8
+ # Replaces polynomial-redos-vulnerable regex scanning with
9
+ # anchored scans that advance through the input once.
10
+ module OptionsParser
11
+ module_function
12
+
13
+ def parse_options(markup)
14
+ scanner = StringScanner.new(markup.to_s)
15
+ options = {}
16
+
17
+ until scanner.eos?
18
+ scanner.skip(/\s+/)
19
+ break if scanner.eos?
20
+
21
+ key = scanner.scan(/\w+/)
22
+ unless key
23
+ scanner.getch
24
+ next
25
+ end
26
+
27
+ scanner.skip(/\s*/)
28
+ next unless scanner.scan(":")
29
+
30
+ value = read_value(scanner)
31
+ options[key] = value unless value.nil?
32
+ end
33
+
34
+ options
35
+ end
36
+
37
+ def read_value(scanner)
38
+ scanner.skip(/\s*/)
39
+ if scanner.scan(/'([^']*)'/) || scanner.scan(/"([^"]*)"/)
40
+ scanner[1]
41
+ else
42
+ scanner.scan(/\S+/)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -13,18 +13,24 @@ module Jekyll
13
13
  normalized = query.to_s.strip.downcase
14
14
  return nil if normalized.empty?
15
15
 
16
- exact = categories.select do |category|
17
- category_values(category).include?(normalized)
18
- end
19
- matches = exact.empty? ? partial_categories(categories, normalized) : exact
16
+ matches = match_categories(categories, normalized)
20
17
  aggregate = options["aggregate"] == "true" && options["list"] == "true"
21
18
  return matches if matches.one? || (aggregate && matches.any?)
22
19
  return nil if matches.empty?
23
20
 
21
+ report_ambiguous_category(site, query, matches)
22
+ nil
23
+ end
24
+
25
+ def match_categories(categories, normalized)
26
+ exact = categories.select { |cat| category_values(cat).include?(normalized) }
27
+ exact.empty? ? partial_categories(categories, normalized) : exact
28
+ end
29
+
30
+ def report_ambiguous_category(site, query, matches)
24
31
  paths = matches.map { |category| category["path"] }.sort.join(", ")
25
32
  message = "Ambiguous doc_category #{query.inspect}; matches: #{paths}. Rendering nothing."
26
33
  report_resolution_issue(site, message)
27
- nil
28
34
  end
29
35
 
30
36
  def resolve_category_path(path, categories, site)
@@ -152,13 +158,7 @@ module Jekyll
152
158
  end
153
159
 
154
160
  def extract_options(text)
155
- options = {}
156
- text.to_s.scan(
157
- /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
158
- ) do |key, single, double, bare|
159
- options[key] = single || double || bare
160
- end
161
- options
161
+ OptionsParser.parse_options(text)
162
162
  end
163
163
 
164
164
  def escape_html(value)
@@ -13,21 +13,27 @@ module Jekyll
13
13
  normalized = query.to_s.strip.downcase
14
14
  return nil if normalized.empty?
15
15
 
16
- exact = docs.select { |doc| document_values(doc).include?(normalized) }
17
- matches = if exact.empty?
18
- docs.select do |doc|
19
- document_values(doc).any? { |value| value.include?(normalized) }
20
- end
21
- else
22
- exact
23
- end
16
+ matches = match_documents(docs, normalized)
24
17
  return matches.first if matches.one?
25
18
  return nil if matches.empty?
26
19
 
20
+ report_ambiguous(site, query, matches)
21
+ nil
22
+ end
23
+
24
+ def match_documents(docs, normalized)
25
+ exact = docs.select { |doc| document_values(doc).include?(normalized) }
26
+ return exact unless exact.empty?
27
+
28
+ docs.select do |doc|
29
+ document_values(doc).any? { |value| value.include?(normalized) }
30
+ end
31
+ end
32
+
33
+ def report_ambiguous(site, query, matches)
27
34
  paths = matches.map { |doc| doc.data["source_path"] || doc.path }.sort.join(", ")
28
35
  message = "Ambiguous doc_link #{query.inspect}; matches: #{paths}. Rendering nothing."
29
36
  report_resolution_issue(site, message)
30
- nil
31
37
  end
32
38
 
33
39
  def find_document_by_path(docs, path, site)
@@ -148,13 +154,7 @@ module Jekyll
148
154
  end
149
155
 
150
156
  def extract_options(text)
151
- options = {}
152
- text.to_s.scan(
153
- /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
154
- ) do |key, single, double, bare|
155
- options[key] = single || double || bare
156
- end
157
- options
157
+ OptionsParser.parse_options(text)
158
158
  end
159
159
 
160
160
  def escape_html(value)
@@ -45,13 +45,7 @@ module Jekyll
45
45
 
46
46
  def parse_markup(markup)
47
47
  expression = markup.to_s.strip[/\A[^\s]+/]
48
- options = {}
49
- markup.to_s.scan(
50
- /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
51
- ) do |key, single, double, bare|
52
- options[key] = single || double || bare
53
- end
54
- [expression, options]
48
+ [expression, OptionsParser.parse_options(markup)]
55
49
  end
56
50
 
57
51
  def escape_html(value)
@@ -20,19 +20,25 @@ module Jekyll
20
20
  docs = docs.select { |doc| doc.data["category"] == category } if category
21
21
  docs = docs.sort_by { |doc| doc.data["date"] || Time.at(0) }.reverse.first(count)
22
22
 
23
+ render_list(docs)
24
+ end
25
+
26
+ private
27
+
28
+ def render_list(docs)
23
29
  out = +"<ul class=\"latest-documents\">\n"
24
- docs.each do |doc|
25
- data = doc.data
26
- title = escape_html(data["title"])
27
- url = escape_html(doc.url)
28
- date = (data["date"] || Time.at(0)).strftime("%Y-%m-%d")
29
- out << %(<li><a href="#{url}">#{title}</a> <small>(#{date})</small></li>\n)
30
- end
30
+ docs.each { |doc| out << list_item(doc) }
31
31
  out << "</ul>\n"
32
32
  out
33
33
  end
34
34
 
35
- private
35
+ def list_item(doc)
36
+ data = doc.data
37
+ title = escape_html(data["title"])
38
+ url = escape_html(doc.url)
39
+ date = (data["date"] || Time.at(0)).strftime("%Y-%m-%d")
40
+ %(<li><a href="#{url}">#{title}</a> <small>(#{date})</small></li>\n)
41
+ end
36
42
 
37
43
  def escape_html(text)
38
44
  return "" unless text
@@ -46,19 +52,9 @@ module Jekyll
46
52
  })
47
53
  end
48
54
 
55
+ # supports: count:5 category:'referat'
49
56
  def parse_args(markup)
50
- # supports: count:5 category:'referat'
51
- args = {}
52
- # Safe: markup comes from Jekyll template authors (trusted), not end users
53
- # Runs only during static site generation, not on user requests
54
- markup.scan(/(\w+)\s*:\s*'([^']*)'|(\w+)\s*:\s*([^\s]+)/).each do |match|
55
- if match[0]
56
- args[match[0]] = match[1]
57
- else
58
- args[match[2]] = match[3]
59
- end
60
- end
61
- args
57
+ OptionsParser.parse_options(markup)
62
58
  end
63
59
  end
64
60
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Documents
5
- VERSION = "0.7.1"
5
+ VERSION = "0.7.3"
6
6
  end
7
7
  end
@@ -11,6 +11,7 @@ require_relative "jekyll/documents/text_extraction_manifest"
11
11
  require_relative "jekyll/documents/assets_generator"
12
12
  require_relative "jekyll/documents/json_index_generator"
13
13
  require_relative "jekyll/documents/layout_registrar"
14
+ require_relative "jekyll/documents/options_parser"
14
15
 
15
16
  # Liquid tag(s)
16
17
  require_relative "jekyll/documents/tags/latest_documents"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-documents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
@@ -154,6 +154,7 @@ files:
154
154
  - lib/jekyll/documents/generator.rb
155
155
  - lib/jekyll/documents/json_index_generator.rb
156
156
  - lib/jekyll/documents/layout_registrar.rb
157
+ - lib/jekyll/documents/options_parser.rb
157
158
  - lib/jekyll/documents/tags/doc_category.rb
158
159
  - lib/jekyll/documents/tags/doc_link.rb
159
160
  - lib/jekyll/documents/tags/document_icon.rb