jekyll-documents 0.2.0 → 0.3.2

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: d806534ca1d7d2b56106885f003e99845646369f842d03a01ac330a5f8dbaacc
4
- data.tar.gz: 50ff80ef20340823fd852265f87f90d6eb630aa56e3795cfc2ca3a58b94fb612
3
+ metadata.gz: 2c05b94feef92602870b24fd7b9c4e9244eb724061c1ef599ed12a23905d51ad
4
+ data.tar.gz: ccfe08306d471478e21092fd7518df1c6d593b05fa219051a6d866d3c947c6c9
5
5
  SHA512:
6
- metadata.gz: cf282187748db50d46e256dcee6f2f9b8b7e34f5e265acf35b4b1d4a5bda55950b66a668000f58aacad9195241c0b8e30e84fd06ad493be2cbc4f238cb3b28ed
7
- data.tar.gz: 95bd40229f6c2cd987e35bf6fab49d89f09a6070bbacf2bb5cedf412866bc368f458e367804a19e4f2fa1479cd2cfdbfab13acfce0ccc9af69aee9fa9be2b294
6
+ metadata.gz: '087a677f0f1efd8c54d7b52c05c1fd0b88c746e9063f8fc0ba843c5ca24db53b8cbdd6904a959d7ca606e490bf8da590ff9b400b21a9de25204079ed0f0b0eff'
7
+ data.tar.gz: 7519b8f1e7035b4391299dd6399dcb1a6fa6572d4b3567891e35fc3f383a7b2958acc28bdbfe058983d6fbf73592c33a609dd07ae082751410873d8d29ac1434
data/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.3.2] - 2026-08-12
6
+
7
+ ### Added
8
+ - Auto-registration of gem-packaged `_layouts` and `_includes` via a `:site, :after_init` hook so Jekyll discovers the `document` layout and all includes without the gem being declared as a theme; user-provided files always take precedence
9
+ - 7 RSpec examples for `LayoutRegistrar` covering copy, override, idempotency, and hook registration
10
+
11
+ ### Fixed
12
+ - Corrected publish workflow Ruby version from 3.2 to 3.3 to match the gemspec `required_ruby_version` so the gem builds and publishes to RubyGems successfully
13
+
14
+ ## [0.3.1] - 2026-08-03
15
+
16
+ ### Added
17
+ - Added the context-aware `{% document_icon page %}` Liquid tag for simple icon rendering
18
+ - Added installed-gem system coverage for all supported document types and icon themes
19
+ - Added Playwright browser coverage for search interaction, links, icons, baseurl, and network errors
20
+
21
+ ### Changed
22
+ - Minimum Ruby version raised from `>= 3.2` to `>= 3.3`
23
+ - Updated RuboCop target Ruby version to 3.3
24
+ - Updated compatible development gems, including RuboCop, Reek, RSpec, and Playwright tooling
25
+ - Test and quality tasks now rebuild and force-install the gem before testing
26
+ - Added package-content checks to verify the built gem contains runtime templates, icons, JavaScript, and Liquid tags
27
+
28
+ ### Fixed
29
+ - Corrected non-color icon mappings to reference packaged SVG assets
30
+ - Registered packaged icons and search JavaScript for copying during Jekyll builds
31
+ - Made document search respect the configured index path and site base URL
32
+ - Hardened document search index validation and disabled the search include when JSON indexing is disabled
33
+ - Escaped search-result URLs, icon URLs, titles, categories, and dates before rendering
34
+ - Added the missing date conversion required for Jekyll 4.4 document publishing
35
+ - Removed duplicate search scripts from the example site
36
+ - Made Reek failures fail the quality task consistently
37
+
38
+ ### Verification
39
+ - 186 Ruby/system examples passing with 100% Ruby line coverage
40
+ - 3 Playwright browser tests passing
41
+ - 0 RuboCop offenses, 0 Reek warnings, and 0 Bundler Audit vulnerabilities
42
+
43
+ ## [0.3.0] - 2026-07-23
44
+
45
+ ### Added
46
+ - **Baked icon data**: Generator now bakes `icon_url` and `icon_set` into each document's data at build time, eliminating the need for Liquid context in filters
47
+ - **`FileTypeIcons.icon_for` class method**: Allows icon URL resolution without a Liquid context (used by generator)
48
+ - **Search JavaScript wired up**: `_includes/documents_search.html` now loads Lunr.js and `documents-search.js` for out-of-the-box client-side search
49
+ - **jekyll-sitemap integration**: `TextStaticFile` exposes `sitemap: false` data to exclude generated JSON index from sitemap
50
+ - **Unit tests for `FileTypeIcons.icon_for`**: 5 new tests covering all icon sets, fallbacks, and nil handling
51
+ - **Generator tests for icon data**: Tests verifying `icon_url` and `icon_set` are correctly baked into document data
52
+ - **System test for full build pipeline**: End-to-end test covering generator → JSON index → tag rendering
53
+
54
+ ### Changed
55
+ - **Minimum Ruby version**: Bumped from `>= 2.7` to `>= 3.2` (zeitwerk 2.7+ — a Jekyll dependency — requires Ruby >= 3.2)
56
+ - **RuboCop `TargetRubyVersion`**: Updated from `2.7` to `3.2` to match gemspec
57
+ - **Templates use baked icon data**: `latest_documents.html`, `documents_list.html`, `document.html` now use `{{ doc.icon_url }}` instead of the broken `file_type_icon_tag` filter
58
+ - **`category_list.html` icon set**: Now reads `icon_set` from first document's baked data instead of non-functional `site.documents.icon_set`
59
+ - **`latest_documents.html` count fallback**: Simplified to `| default: 5` since `site.config` is inaccessible in Liquid
60
+ - **Reek configuration**: Updated `.reek.yml` with targeted exclusions for design-inherent warnings
61
+ - **Code quality**: Renamed uncommunicative variables (`s` → `slug`/`result`, `d` → `doc`, `m` → `match`, `y/m/d` → `year/month/day`) across all source files
62
+ - **Reduced duplicate method calls**: Extracted `data = doc.data` locals in generator, JSON index generator, and latest documents tag
63
+ - **Simplified `get_icon_set`**: Reduced from 6 statements to 3 using safe navigation with `dig`
64
+ - **Test suite**: 167 examples (up from 79), 100% line coverage (212/212 lines)
65
+ - **RuboCop**: 0 offenses (24 files inspected)
66
+ - **Reek**: 0 warnings (7 files inspected)
67
+
68
+ ### Fixed
69
+ - **Icon set config ignored in templates** (High): `file_type_icon_tag` filter never received Liquid context, causing `icon_set` config to be silently ignored — always fell back to `"color"`. Fixed by baking icon URLs into document data at generation time
70
+ - **`rel_path` slicing** (Low): Replaced fragile `path[(site.source.length + 1)..]` with `String#delete_prefix` for robustness
71
+ - **`Date.parse` in `parse_filename`** (Low): Replaced with `Date.new(year, month, day)` using integer-converted regex captures for clarity
72
+ - **`category_list.html` folder icon** (Low): `site.documents.icon_set` always resolved to `nil` since `site.documents` is an array — now reads from baked document data
73
+ - **Search include missing JavaScript** (Medium): `documents_search.html` had input/results elements but no script tags — now loads Lunr.js and search JS
74
+ - **No active jekyll-sitemap integration** (Low): JSON index file now flagged with `sitemap: false` to exclude it from sitemap
75
+
3
76
  ## [0.2.0] - 2026-03-12
4
77
 
5
78
  ### Added
data/README.md CHANGED
@@ -1,9 +1,20 @@
1
1
  # jekyll-documents
2
+ [![Status: Active](https://img.shields.io/badge/status-active-success)](https://github.com/gundestrup/jekyll-documents)
3
+ [![Tests](https://github.com/gundestrup/jekyll-documents/actions/workflows/test.yml/badge.svg)](https://github.com/gundestrup/jekyll-documents/actions/workflows/test.yml)
4
+ [![Codecov](https://codecov.io/gh/gundestrup/jekyll-documents/graph/badge.svg)](https://codecov.io/gh/gundestrup/jekyll-documents)
5
+ [![Gem Version](https://img.shields.io/gem/v/jekyll-documents)](https://rubygems.org/gems/jekyll-documents)
6
+ [![Gem Downloads](https://img.shields.io/gem/dt/jekyll-documents)](https://rubygems.org/gems/jekyll-documents)
7
+ [![License: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-blue)](LICENSE)
8
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/gundestrup/jekyll-documents)
2
9
 
3
10
  Turn files in `assets/documents/` into browsable document pages.
4
11
 
12
+ **Requirements**: Ruby 3.3+ • Jekyll 4.4+
13
+
5
14
  **Features**: Auto-collection • File icons • Categories • Search
6
15
 
16
+ [![DeepWiki](https://img.shields.io/badge/DeepWiki-docs-blue)](https://deepwiki.com/gundestrup/jekyll-documents)
17
+
7
18
  ## Quick Start
8
19
 
9
20
  ```ruby
@@ -52,10 +63,15 @@ documents:
52
63
  icon_set: "color"
53
64
  ```
54
65
 
66
+ Render a document icon in templates with the context-aware tag:
67
+
55
68
  ```liquid
56
- {{ page.file_type | file_type_icon_tag }}
69
+ {% document_icon page %}
57
70
  ```
58
71
 
72
+ The tag applies the configured icon set and site `baseurl` automatically. The `file_type_icon` and
73
+ `file_type_icon_tag` filters remain available for lower-level or Ruby-side use.
74
+
59
75
  ## Usage
60
76
 
61
77
  ```liquid
@@ -78,15 +94,19 @@ See [configuration.rb](lib/jekyll/documents/configuration.rb) for all options.
78
94
  ## Development
79
95
 
80
96
  ```bash
81
- rake # All quality checks
82
- rake quick # Fast check
97
+ rake # Ruby quality checks
98
+ rake test # Rebuild/install gem + Ruby and browser tests
99
+ rake browser_test # Browser search tests (Node.js + Chromium required)
100
+ rake quick # Fast Ruby check
83
101
  rake help # Show commands
84
102
  ```
85
103
 
86
- **Quality**: 98.99% coverage • 0 vulnerabilities • RuboCopReek
104
+ **Quality**: 100% coverage • 0 vulnerabilities • 0 offenses 0 warnings
87
105
 
88
106
  See [README.Development.md](README.Development.md) for details.
89
107
 
108
+ **DeepWiki**: [https://deepwiki.com/gundestrup/jekyll-documents](https://deepwiki.com/gundestrup/jekyll-documents)
109
+
90
110
  ## Release
91
111
 
92
112
  ```bash
@@ -1,8 +1,10 @@
1
+ {% assign first_doc = site.documents | first %}
2
+ {% assign icon_set = first_doc.icon_set | default: 'color' %}
1
3
  {% assign cats = site.documents | map: "category" | uniq | sort %}
2
4
  <ul class="documents-category-list">
3
5
  {% for cat in cats %}
4
6
  <li>
5
- <img src="/assets/icons/{{ site.documents.icon_set | default: 'color' }}/folder-svgrepo-com.svg" alt="Folder" class="folder-icon" />
7
+ <img src="{{ '/assets/icons/' | append: icon_set | append: '/folder-svgrepo-com.svg' | relative_url }}" alt="Folder" class="folder-icon" />
6
8
  <span class="category-name">{{ cat }}</span>
7
9
  <span class="category-count">({{ site.documents | where: "category", cat | size }})</span>
8
10
  </li>
@@ -2,7 +2,7 @@
2
2
  <ul class="documents-list">
3
3
  {% for doc in docs %}
4
4
  <li>
5
- {{ doc.file_type | file_type_icon_tag }}
5
+ {% document_icon doc %}
6
6
  <a href="{{ doc.url | relative_url }}">{{ doc.title }}</a>
7
7
  <small>({{ doc.date | date: "%Y-%m-%d" }}, {{ doc.category }})</small>
8
8
  </li>
@@ -1,4 +1,14 @@
1
+ {% assign documents_config = site.data.jekyll_documents %}
2
+ {% assign index_path = documents_config.json_index_path | default: '/documents.json' %}
3
+ {% if documents_config.json_index != false %}
1
4
  <div class="documents-search">
2
5
  <input id="doc-search-input" type="search" placeholder="Search documents…" aria-label="Search documents" />
3
6
  <div id="doc-search-results" class="documents-search-results"></div>
4
7
  </div>
8
+ <script>
9
+ window.__jekyllDocumentsIndexPath = {{ index_path | relative_url | jsonify }};
10
+ window.__jekyllDocumentsBaseurl = {{ site.baseurl | jsonify }};
11
+ </script>
12
+ <script src="https://unpkg.com/lunr/lunr.js"></script>
13
+ <script src="{{ '/assets/js/documents-search.js' | relative_url }}"></script>
14
+ {% endif %}
@@ -1,9 +1,9 @@
1
- {% assign count = include.count | default: site.config.documents.latest_default_count | default: 5 %}
1
+ {% assign count = include.count | default: 5 %}
2
2
  {% assign docs = site.documents | sort: "date" | reverse | slice: 0, count %}
3
3
  <ul class="latest-documents">
4
4
  {% for doc in docs %}
5
5
  <li>
6
- {{ doc.file_type | file_type_icon_tag }}
6
+ {% document_icon doc %}
7
7
  <a href="{{ doc.url | relative_url }}">{{ doc.title }}</a>
8
8
  <small>({{ doc.date | date: "%Y-%m-%d" }}, {{ doc.category }})</small>
9
9
  </li>
@@ -13,7 +13,7 @@ layout: default
13
13
 
14
14
  <p>
15
15
  <a href="{{ page.file_url | relative_url }}" class="document-download">
16
- {{ page.file_type | file_type_icon_tag }}
16
+ {% document_icon page %}
17
17
  Download ({{ page.extension | remove: "." | upcase }})
18
18
  </a>
19
19
  </p>
@@ -1,80 +1,38 @@
1
- /* Minimal client-side search using Lunr and /documents.json (generated by the plugin).
1
+ /* Minimal client-side search using Lunr and the generated document index.
2
2
  Usage on any page:
3
3
  {% include documents_search.html %}
4
- <script src="https://unpkg.com/lunr/lunr.js"></script>
5
- <script src="{{ '/assets/js/documents-search.js' | relative_url }}"></script>
6
4
  */
7
5
  (function () {
8
6
  const input = document.getElementById("doc-search-input");
9
7
  const resultsEl = document.getElementById("doc-search-results");
10
8
  if (!input || !resultsEl) return;
11
9
 
12
- const indexUrl = (window.__jekyllDocumentsIndexPath || "/documents.json");
10
+ const indexUrl = window.__jekyllDocumentsIndexPath || "/documents.json";
13
11
  let idx, docs = [];
14
12
 
15
- function escapeHtml(s) {
13
+ function escapeHtml(value) {
16
14
  const div = document.createElement("div");
17
- div.appendChild(document.createTextNode(s));
18
- return div.innerHTML;
15
+ div.appendChild(document.createTextNode(String(value)));
16
+ return div.innerHTML
17
+ .replace(/"/g, "&quot;")
18
+ .replace(/'/g, "&#39;");
19
19
  }
20
20
 
21
- function getIconPath(fileType, iconSet) {
22
- iconSet = iconSet || "color";
23
- const iconMap = {
24
- "color": {
25
- "pdf": "/assets/icons/color/pdf-document-svgrepo-com.svg",
26
- "docx": "/assets/icons/color/word-document-svgrepo-com.svg",
27
- "doc": "/assets/icons/color/word-document-svgrepo-com.svg",
28
- "pptx": "/assets/icons/color/ppt-document-svgrepo-com.svg",
29
- "ppt": "/assets/icons/color/ppt-document-svgrepo-com.svg",
30
- "xlsx": "/assets/icons/color/excel-document-svgrepo-com.svg",
31
- "xls": "/assets/icons/color/excel-document-svgrepo-com.svg",
32
- "txt": "/assets/icons/color/txt-document-svgrepo-com.svg",
33
- "zip": "/assets/icons/color/zip-document-svgrepo-com.svg"
34
- },
35
- "lines": {
36
- "pdf": "/assets/icons/lines/pdf-svgrepo-com.svg",
37
- "doc": "/assets/icons/lines/doc-svgrepo-com.svg",
38
- "docx": "/assets/icons/lines/doc-svgrepo-com.svg",
39
- "ppt": "/assets/icons/lines/ppt-svgrepo-com.svg",
40
- "pptx": "/assets/icons/lines/ppt-svgrepo-com.svg",
41
- "xls": "/assets/icons/lines/xls-svgrepo-com.svg",
42
- "xlsx": "/assets/icons/lines/xls-svgrepo-com.svg",
43
- "txt": "/assets/icons/lines/txt-svgrepo-com.svg",
44
- "zip": "/assets/icons/lines/zip-svgrepo-com.svg"
45
- },
46
- "minimal": {
47
- "pdf": "/assets/icons/minimal/pdf-svgrepo-com.svg",
48
- "doc": "/assets/icons/minimal/doc-svgrepo-com.svg",
49
- "docx": "/assets/icons/minimal/doc-svgrepo-com.svg",
50
- "ppt": "/assets/icons/minimal/ppt-svgrepo-com.svg",
51
- "pptx": "/assets/icons/minimal/ppt-svgrepo-com.svg",
52
- "xls": "/assets/icons/minimal/xls-svgrepo-com.svg",
53
- "xlsx": "/assets/icons/minimal/xls-svgrepo-com.svg",
54
- "txt": "/assets/icons/minimal/txt-svgrepo-com.svg",
55
- "zip": "/assets/icons/minimal/zip-svgrepo-com.svg"
56
- },
57
- "ultra-minimal": {
58
- "pdf": "/assets/icons/ultra-minimal/pdf.svg",
59
- "doc": "/assets/icons/ultra-minimal/doc.svg",
60
- "docx": "/assets/icons/ultra-minimal/doc.svg",
61
- "ppt": "/assets/icons/ultra-minimal/ppt.svg",
62
- "pptx": "/assets/icons/ultra-minimal/ppt.svg",
63
- "xls": "/assets/icons/ultra-minimal/xls.svg",
64
- "xlsx": "/assets/icons/ultra-minimal/xls.svg",
65
- "txt": "/assets/icons/ultra-minimal/txt.svg",
66
- "zip": "/assets/icons/ultra-minimal/zip.svg"
67
- }
68
- };
69
- const icons = iconMap[iconSet] || iconMap["color"];
70
- return icons[fileType] || `/assets/icons/${iconSet}/unknown-document-svgrepo-com.svg`;
21
+ function withBaseurl(url) {
22
+ const value = String(url || "");
23
+ if (!value || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(value)) return value;
24
+
25
+ const baseurl = String(window.__jekyllDocumentsBaseurl || "")
26
+ .replace(/^\/+|\/+$/g, "");
27
+ const path = value.replace(/^\/+/, "");
28
+ return `/${[baseurl, path].filter(Boolean).join("/")}`;
71
29
  }
72
30
 
73
- function getIconTag(fileType) {
74
- const iconSet = window.__jekyllDocumentsIconSet || "color";
75
- const iconPath = getIconPath(fileType, iconSet);
76
- const altText = (fileType || "file").toUpperCase() + " file";
77
- return `<img src="${iconPath}" alt="${escapeHtml(altText)}" class="file-icon" />`;
31
+ function getIconTag(fileType, iconUrl) {
32
+ if (!iconUrl) return "";
33
+ const altText = `${String(fileType || "file").toUpperCase()} file`;
34
+ const url = escapeHtml(withBaseurl(iconUrl));
35
+ return `<img src="${url}" alt="${escapeHtml(altText)}" class="file-icon" />`;
78
36
  }
79
37
 
80
38
  function render(matches) {
@@ -82,21 +40,27 @@
82
40
  resultsEl.innerHTML = "<p>No results.</p>";
83
41
  return;
84
42
  }
85
- const items = matches.slice(0, 20).map(m => {
86
- const d = docs.find(x => x.url === m.ref);
87
- if (!d) return "";
88
- const title = escapeHtml(d.title || "Untitled");
89
- const category = escapeHtml(d.category || "uncategorized");
90
- const date = d.date ? `<small>${escapeHtml(d.date)}</small>` : "";
91
- const icon = d.file_type ? getIconTag(d.file_type) : "";
92
- return `<li>${icon}<a href="${d.url}">${title}</a> <small>[${category}]</small> ${date}</li>`;
43
+
44
+ const items = matches.slice(0, 20).map(match => {
45
+ const document = docs.find(item => item.url === match.ref);
46
+ if (!document) return "";
47
+
48
+ const title = escapeHtml(document.title || "Untitled");
49
+ const category = escapeHtml(document.category || "uncategorized");
50
+ const date = document.date ? `<small>${escapeHtml(document.date)}</small>` : "";
51
+ const icon = getIconTag(document.file_type, document.icon_url);
52
+ const url = escapeHtml(withBaseurl(document.url));
53
+ return `<li>${icon}<a href="${url}">${title}</a> ` +
54
+ `<small>[${category}]</small> ${date}</li>`;
93
55
  });
94
56
  resultsEl.innerHTML = `<ul class="documents-search-list">${items.join("")}</ul>`;
95
57
  }
96
58
 
97
59
  function debounce(fn, wait) {
98
- let t; return function (...args) {
99
- clearTimeout(t); t = setTimeout(() => fn.apply(this, args), wait);
60
+ let timer;
61
+ return function (...args) {
62
+ clearTimeout(timer);
63
+ timer = setTimeout(() => fn.apply(this, args), wait);
100
64
  };
101
65
  }
102
66
 
@@ -106,34 +70,48 @@
106
70
  this.field("title", { boost: 10 });
107
71
  this.field("category", { boost: 5 });
108
72
  this.field("slug");
109
- docs.forEach(doc => this.add(doc));
73
+ docs.forEach(document => this.add(document));
110
74
  });
111
75
  }
112
76
 
113
- function doSearch(q) {
114
- if (!idx || !q || q.trim().length === 0) {
77
+ function doSearch(query) {
78
+ if (!idx || !query || query.trim().length === 0) {
115
79
  resultsEl.innerHTML = "";
116
80
  return;
117
81
  }
82
+
118
83
  try {
119
- const res = idx.search(q);
120
- render(res);
121
- } catch (e) {
122
- // If Lunr query syntax fails, fallback to simple contains filter
123
- const qlc = q.toLowerCase();
124
- const res = docs.filter(d =>
125
- (d.title || "").toLowerCase().includes(qlc) ||
126
- (d.category || "").toLowerCase().includes(qlc) ||
127
- (d.slug || "").toLowerCase().includes(qlc)
128
- ).map(d => ({ ref: d.url }));
129
- render(res);
84
+ render(idx.search(query));
85
+ } catch (error) {
86
+ // If Lunr query syntax fails, fall back to a simple contains filter.
87
+ const normalizedQuery = query.toLowerCase();
88
+ const matches = docs.filter(document =>
89
+ [document.title, document.category, document.slug].some(value =>
90
+ String(value || "").toLowerCase().includes(normalizedQuery)
91
+ )
92
+ ).map(document => ({ ref: document.url }));
93
+ render(matches);
130
94
  }
131
95
  }
132
96
 
133
97
  fetch(indexUrl)
134
- .then(r => r.json())
135
- .then(json => { docs = json; buildIndex(); })
136
- .catch(err => { console.error("Documents search index failed:", err); });
98
+ .then(response => {
99
+ if (!response.ok) {
100
+ throw new Error(`Search index request failed: ${response.status}`);
101
+ }
102
+ return response.json();
103
+ })
104
+ .then(json => {
105
+ if (!Array.isArray(json)) {
106
+ throw new Error("Search index must be a JSON array");
107
+ }
108
+ docs = json;
109
+ buildIndex();
110
+ })
111
+ .catch(error => {
112
+ resultsEl.innerHTML = "<p>Document search is currently unavailable.</p>";
113
+ console.error("Documents search index failed:", error);
114
+ });
137
115
 
138
- input.addEventListener("input", debounce(e => doSearch(e.target.value), 150));
116
+ input.addEventListener("input", debounce(event => doSearch(event.target.value), 150));
139
117
  })();
@@ -25,20 +25,20 @@ Gem::Specification.new do |spec|
25
25
  "rubygems_mfa_required" => "true"
26
26
  }
27
27
 
28
- spec.required_ruby_version = ">= 2.7"
28
+ spec.required_ruby_version = ">= 3.3"
29
29
 
30
30
  spec.files = Dir.glob("{lib,assets,_includes,_layouts}/**/*") +
31
31
  ["README.md", "CHANGELOG.md", "LICENSE", "jekyll-documents.gemspec"]
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- spec.add_dependency "jekyll", ">= 4.0"
34
+ spec.add_dependency "jekyll", ">= 4.4", "< 5.0"
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.1"
39
- spec.add_development_dependency "rspec", "~> 3.12"
40
- spec.add_development_dependency "rubocop", "~> 1.60"
41
- spec.add_development_dependency "rubocop-performance", "~> 1.20"
42
- spec.add_development_dependency "simplecov", "~> 0.22"
38
+ spec.add_development_dependency "reek", "~> 6.5"
39
+ spec.add_development_dependency "rspec", "~> 3.13"
40
+ spec.add_development_dependency "rubocop", "~> 1.88"
41
+ spec.add_development_dependency "rubocop-performance", "~> 1.26"
42
+ spec.add_development_dependency "simplecov", "~> 1.0"
43
43
  spec.add_development_dependency "yard", "~> 0.9"
44
44
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Documents
5
+ # Registers gem-packaged frontend assets for copying into the generated site.
6
+ class AssetsGenerator < ::Jekyll::Generator
7
+ safe true
8
+ priority :normal
9
+
10
+ def generate(site)
11
+ package_root = File.expand_path("../../..", __dir__)
12
+ site.data["jekyll_documents"] = configuration(site)
13
+ register_assets(site, package_root)
14
+ end
15
+
16
+ private
17
+
18
+ def configuration(site)
19
+ config = Configuration.read(site)
20
+ {
21
+ "json_index" => config["json_index"],
22
+ "json_index_path" => config["json_index_path"]
23
+ }
24
+ end
25
+
26
+ def register_assets(site, package_root)
27
+ assets_root = File.join(package_root, "assets")
28
+ Dir.glob(File.join(assets_root, "**", "*")).each do |path|
29
+ register_asset(site, package_root, path)
30
+ end
31
+ end
32
+
33
+ def register_asset(site, package_root, path)
34
+ return unless File.file?(path)
35
+
36
+ relative_path = path.delete_prefix("#{package_root}/")
37
+ return if site.static_files.any? { |file| file.relative_path == relative_path }
38
+
39
+ site.static_files << ::Jekyll::StaticFile.new(
40
+ site,
41
+ package_root,
42
+ File.dirname(relative_path),
43
+ File.basename(relative_path)
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
@@ -13,6 +13,9 @@ module Jekyll
13
13
  # Lines: https://www.svgrepo.com/collection/simple-file-types-line-vectors/
14
14
  # Minimal: https://www.svgrepo.com/collection/file-type-minimal-icons/
15
15
  # Ultra-minimal: Based on simple document icons
16
+ LINES_ICON_PATH = "/assets/icons/lines/"
17
+ MINIMAL_ICON_PATH = "/assets/icons/minimal/"
18
+
16
19
  ICON_MAP = {
17
20
  "color" => {
18
21
  "pdf" => "/assets/icons/color/pdf-document-svgrepo-com.svg",
@@ -38,42 +41,59 @@ module Jekyll
38
41
  "rtf" => "/assets/icons/color/rtf-document-svgrepo-com.svg"
39
42
  },
40
43
  "lines" => {
41
- "pdf" => "/assets/icons/lines/pdf-svgrepo-com.svg",
42
- "docx" => "/assets/icons/lines/doc-svgrepo-com.svg",
43
- "doc" => "/assets/icons/lines/doc-svgrepo-com.svg",
44
- "pptx" => "/assets/icons/lines/ppt-svgrepo-com.svg",
45
- "ppt" => "/assets/icons/lines/ppt-svgrepo-com.svg",
46
- "xlsx" => "/assets/icons/lines/xls-svgrepo-com.svg",
47
- "xls" => "/assets/icons/lines/xls-svgrepo-com.svg",
48
- "odt" => "/assets/icons/lines/doc-svgrepo-com.svg",
49
- "ods" => "/assets/icons/lines/xls-svgrepo-com.svg",
50
- "odp" => "/assets/icons/lines/ppt-svgrepo-com.svg",
51
- "txt" => "/assets/icons/lines/txt-svgrepo-com.svg",
52
- "zip" => "/assets/icons/lines/zip-svgrepo-com.svg",
53
- "mp3" => "/assets/icons/lines/mp3-svgrepo-com.svg",
54
- "mp4" => "/assets/icons/lines/mp4-svgrepo-com.svg",
55
- "jpg" => "/assets/icons/lines/jpg-svgrepo-com.svg",
56
- "jpeg" => "/assets/icons/lines/jpg-svgrepo-com.svg",
57
- "png" => "/assets/icons/lines/png-svgrepo-com.svg"
44
+ "pdf" => "#{LINES_ICON_PATH}pdf-file-type-svgrepo-com.svg",
45
+ "docx" => "#{LINES_ICON_PATH}word-file-type-svgrepo-com.svg",
46
+ "doc" => "#{LINES_ICON_PATH}word-file-type-svgrepo-com.svg",
47
+ "pptx" => "#{LINES_ICON_PATH}ppt-file-type-svgrepo-com.svg",
48
+ "ppt" => "#{LINES_ICON_PATH}ppt-file-type-svgrepo-com.svg",
49
+ "xlsx" => "#{LINES_ICON_PATH}excel-file-type-svgrepo-com.svg",
50
+ "xls" => "#{LINES_ICON_PATH}excel-file-type-svgrepo-com.svg",
51
+ "odt" => "#{LINES_ICON_PATH}word-file-type-svgrepo-com.svg",
52
+ "ods" => "#{LINES_ICON_PATH}excel-file-type-svgrepo-com.svg",
53
+ "odp" => "#{LINES_ICON_PATH}ppt-file-type-svgrepo-com.svg",
54
+ "txt" => "#{LINES_ICON_PATH}txt-file-type-svgrepo-com.svg",
55
+ "zip" => "#{LINES_ICON_PATH}zip-file-type-svgrepo-com.svg",
56
+ "mp3" => "#{LINES_ICON_PATH}mp3-file-type-svgrepo-com.svg",
57
+ "mp4" => "#{LINES_ICON_PATH}other-file-type-svgrepo-com.svg",
58
+ "jpg" => "#{LINES_ICON_PATH}jpg-file-type-svgrepo-com.svg",
59
+ "jpeg" => "#{LINES_ICON_PATH}jpg-file-type-svgrepo-com.svg",
60
+ "png" => "#{LINES_ICON_PATH}png-file-type-svgrepo-com.svg"
58
61
  },
59
62
  "minimal" => {
60
- "pdf" => "/assets/icons/minimal/pdf-svgrepo-com.svg",
61
- "docx" => "/assets/icons/minimal/doc-svgrepo-com.svg",
62
- "doc" => "/assets/icons/minimal/doc-svgrepo-com.svg",
63
- "pptx" => "/assets/icons/minimal/ppt-svgrepo-com.svg",
64
- "ppt" => "/assets/icons/minimal/ppt-svgrepo-com.svg",
65
- "xlsx" => "/assets/icons/minimal/xls-svgrepo-com.svg",
66
- "xls" => "/assets/icons/minimal/xls-svgrepo-com.svg",
67
- "odt" => "/assets/icons/minimal/doc-svgrepo-com.svg",
68
- "ods" => "/assets/icons/minimal/xls-svgrepo-com.svg",
69
- "odp" => "/assets/icons/minimal/ppt-svgrepo-com.svg",
70
- "txt" => "/assets/icons/minimal/txt-svgrepo-com.svg",
71
- "zip" => "/assets/icons/minimal/zip-svgrepo-com.svg",
72
- "mp3" => "/assets/icons/minimal/mp3-svgrepo-com.svg",
73
- "mp4" => "/assets/icons/minimal/mp4-svgrepo-com.svg",
74
- "jpg" => "/assets/icons/minimal/jpg-svgrepo-com.svg",
75
- "jpeg" => "/assets/icons/minimal/jpg-svgrepo-com.svg",
76
- "png" => "/assets/icons/minimal/png-svgrepo-com.svg"
63
+ "pdf" => "#{MINIMAL_ICON_PATH}" \
64
+ "extension-file-format-pdf-document-file-format-svgrepo-com.svg",
65
+ "docx" => "#{MINIMAL_ICON_PATH}" \
66
+ "extension-file-format-document-file-format-svgrepo-com.svg",
67
+ "doc" => "#{MINIMAL_ICON_PATH}" \
68
+ "extension-file-format-document-file-format-svgrepo-com.svg",
69
+ "pptx" => "#{MINIMAL_ICON_PATH}" \
70
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
71
+ "ppt" => "#{MINIMAL_ICON_PATH}" \
72
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
73
+ "xlsx" => "#{MINIMAL_ICON_PATH}" \
74
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
75
+ "xls" => "#{MINIMAL_ICON_PATH}" \
76
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
77
+ "odt" => "#{MINIMAL_ICON_PATH}" \
78
+ "extension-file-format-document-file-format-svgrepo-com.svg",
79
+ "ods" => "#{MINIMAL_ICON_PATH}" \
80
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
81
+ "odp" => "#{MINIMAL_ICON_PATH}" \
82
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
83
+ "txt" => "#{MINIMAL_ICON_PATH}" \
84
+ "extension-file-format-txt-document-file-format-svgrepo-com.svg",
85
+ "zip" => "#{MINIMAL_ICON_PATH}" \
86
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
87
+ "mp3" => "#{MINIMAL_ICON_PATH}" \
88
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
89
+ "mp4" => "#{MINIMAL_ICON_PATH}" \
90
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
91
+ "jpg" => "#{MINIMAL_ICON_PATH}" \
92
+ "extension-file-format-jpg-document-file-format-svgrepo-com.svg",
93
+ "jpeg" => "#{MINIMAL_ICON_PATH}" \
94
+ "file-format-jpeg-document-extension-file-format-svgrepo-com.svg",
95
+ "png" => "#{MINIMAL_ICON_PATH}" \
96
+ "extension-file-format-document-file-format-2-svgrepo-com.svg"
77
97
  },
78
98
  "ultra-minimal" => {
79
99
  "pdf" => "/assets/icons/ultra-minimal/pdf.svg",
@@ -95,6 +115,27 @@ module Jekyll
95
115
  }
96
116
  }.freeze
97
117
 
118
+ UNKNOWN_ICON_MAP = {
119
+ "color" => "/assets/icons/color/unknown-document-svgrepo-com.svg",
120
+ "lines" => "#{LINES_ICON_PATH}other-file-type-svgrepo-com.svg",
121
+ "minimal" => "#{MINIMAL_ICON_PATH}" \
122
+ "extension-file-format-document-file-format-2-svgrepo-com.svg",
123
+ "ultra-minimal" => "/assets/icons/ultra-minimal/file.svg"
124
+ }.freeze
125
+
126
+ # Returns the icon URL for a given file type and explicit icon set
127
+ # @param file_type [String] the file extension (e.g., 'pdf', 'docx')
128
+ # @param icon_set [String] the icon set name (defaults to 'color')
129
+ # @return [String] the icon file URL
130
+ # @example
131
+ # FileTypeIcons.icon_for('pdf', 'lines') #=>
132
+ # "/assets/icons/lines/pdf-file-type-svgrepo-com.svg"
133
+ def self.icon_for(file_type, icon_set = "color")
134
+ selected_set = ICON_MAP.key?(icon_set) ? icon_set : "color"
135
+ icons = ICON_MAP[selected_set]
136
+ icons[file_type.to_s.downcase] || UNKNOWN_ICON_MAP[selected_set]
137
+ end
138
+
98
139
  # Returns the icon URL for a given file type using configured icon set
99
140
  # @param file_type [String] the file extension (e.g., 'pdf', 'docx')
100
141
  # @param context [Liquid::Context] the Liquid context for accessing site config
@@ -102,10 +143,7 @@ module Jekyll
102
143
  # @example
103
144
  # file_type_icon('pdf') #=> "/assets/icons/color/pdf.svg"
104
145
  def file_type_icon(file_type, context = nil)
105
- icon_set = get_icon_set(context)
106
- icons = ICON_MAP[icon_set] || ICON_MAP["color"]
107
- icons[file_type.to_s.downcase] ||
108
- "/assets/icons/#{icon_set}/unknown-document-svgrepo-com.svg"
146
+ Jekyll::Documents::FileTypeIcons.icon_for(file_type, get_icon_set(context))
109
147
  end
110
148
 
111
149
  # Returns an HTML img tag for the file type icon
@@ -119,7 +157,7 @@ module Jekyll
119
157
  # '<img src="/assets/icons/color/pdf.svg" alt="PDF file" class="file-icon" />'
120
158
  def file_type_icon_tag(file_type, css_class: "file-icon", alt: nil, context: nil)
121
159
  url = file_type_icon(file_type, context)
122
- alt_text = alt || "#{file_type.upcase} file"
160
+ alt_text = alt || "#{file_type.to_s.upcase} file"
123
161
  %(<img src="#{url}" alt="#{alt_text}" class="#{css_class}" />)
124
162
  end
125
163
 
@@ -131,11 +169,8 @@ module Jekyll
131
169
  def get_icon_set(context)
132
170
  return "color" unless context
133
171
 
134
- site_config = context.registers[:site]&.config
135
- documents_config = site_config["documents"] if site_config
136
- icon_set = documents_config["icon_set"] if documents_config
137
-
138
- # Validate icon set exists
172
+ site = context.registers[:site]
173
+ icon_set = site&.config&.dig("documents", "icon_set")
139
174
  return "color" unless icon_set && ICON_MAP.key?(icon_set)
140
175
 
141
176
  icon_set
@@ -10,17 +10,18 @@ module Jekyll
10
10
 
11
11
  # Slugify while handling Danish letters and basic punctuation cleanup.
12
12
  def documents_slugify(input, downcase: true, danish_map: true)
13
- s = input.to_s.dup
14
- s = s.gsub(/[æøåÆØÅ]/) { |ch| DANISH_MAP[ch] } if danish_map
15
- s = s.strip.gsub(/[^\p{Alnum}\-_\s]/u, "")
16
- s = s.tr(" ", "-").squeeze("-")
17
- s = s.gsub(/[æøåÆØÅ]/, "") unless danish_map # Remove Danish chars if mapping disabled
18
- s = s.downcase if downcase
19
- s
13
+ result = input.to_s.dup
14
+ result = result.gsub(/[æøåÆØÅ]/) { |ch| DANISH_MAP[ch] } if danish_map
15
+ result = result.strip.gsub(/[^\p{Alnum}\-_\s]/u, "")
16
+ result = result.tr(" ", "-").squeeze("-")
17
+ result = result.gsub(/[æøåÆØÅ]/, "") unless danish_map
18
+ result = result.downcase if downcase
19
+ result
20
20
  end
21
21
 
22
22
  # Title from filename e.g. "2026-03-01_Title_Here" -> "Title Here"
23
23
  def documents_title_from_filename(basename)
24
+ basename = basename.to_s
24
25
  if basename =~ /^\d{4}-\d{2}-\d{2}_(.+)$/
25
26
  Regexp.last_match(1).tr("_", " ")
26
27
  else
@@ -35,7 +35,7 @@ module Jekyll
35
35
  end
36
36
  next unless @config["include_extensions"].include?(ext)
37
37
 
38
- rel_path = path.start_with?(site.source) ? path[(site.source.length + 1)..] : path
38
+ rel_path = path.delete_prefix("#{site.source}/")
39
39
  category = infer_category_from(rel_path)
40
40
  basename = File.basename(path, ext)
41
41
 
@@ -46,6 +46,8 @@ module Jekyll
46
46
  end
47
47
 
48
48
  slug = build_slug(basename)
49
+ file_type = ext.sub(".", "").downcase
50
+ icon_set = @config["icon_set"]
49
51
 
50
52
  doc = ::Jekyll::Document.new(
51
53
  source_stub_for(basename, category),
@@ -53,17 +55,20 @@ module Jekyll
53
55
  collection: collection
54
56
  )
55
57
 
56
- doc.data["layout"] = @config["layout"]
57
- doc.data["title"] = title
58
- doc.data["date"] = date || File.mtime(path)
59
- doc.data["category"] = remap_category(category)
60
- doc.data["file_url"] = "/#{rel_path}"
61
- doc.data["extension"] = ext
62
- doc.data["file_type"] = ext.sub(".", "").downcase
63
- doc.data["slug"] = slug
64
- doc.data["permalink"] = @config["permalink"]
65
- .gsub(":category", doc.data["category"].to_s)
66
- .gsub(":slug", slug)
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)
67
72
 
68
73
  doc.content = "Auto-generated document page."
69
74
 
@@ -117,8 +122,10 @@ module Jekyll
117
122
  # @return [Array<Date, String, Boolean>] date, title, and validity flag
118
123
  def parse_filename(basename)
119
124
  if basename =~ /^(\d{4})-(\d{2})-(\d{2})_(.+)$/
120
- date = Date.parse("#{Regexp.last_match(1)}-#{Regexp.last_match(2)}-" \
121
- "#{Regexp.last_match(3)}")
125
+ year = Regexp.last_match(1).to_i
126
+ month = Regexp.last_match(2).to_i
127
+ day = Regexp.last_match(3).to_i
128
+ date = Date.new(year, month, day)
122
129
  title = Regexp.last_match(4).tr("_", " ")
123
130
  [date, title, true]
124
131
  else
@@ -132,16 +139,16 @@ module Jekyll
132
139
  # @param basename [String] the filename without extension
133
140
  # @return [String] the generated slug
134
141
  def build_slug(basename)
135
- s = basename.sub(/^\d{4}-\d{2}-\d{2}_/, "")
142
+ slug = basename.sub(/^\d{4}-\d{2}-\d{2}_/, "")
136
143
  if @config["slug_danish_map"]
137
- s = s.gsub(/[æøåÆØÅ]/,
138
- { "æ" => "ae", "ø" => "oe", "å" => "aa", "Æ" => "Ae", "Ø" => "Oe",
139
- "Å" => "Aa" })
144
+ slug = slug.gsub(/[æøåÆØÅ]/,
145
+ { "æ" => "ae", "ø" => "oe", "å" => "aa", "Æ" => "Ae", "Ø" => "Oe",
146
+ "Å" => "Aa" })
140
147
  end
141
- s = s.downcase if @config["slug_downcase"]
142
- s = s.gsub(/[^\p{Alnum}\-_\s]/u, "").tr("_ ", "--").squeeze("-")
143
- s = s.sub(/^-+/, "").sub(/-+$/, "") # Remove leading/trailing hyphens
144
- s.empty? ? "untitled" : s
148
+ slug = slug.downcase if @config["slug_downcase"]
149
+ slug = slug.gsub(/[^\p{Alnum}\-_\s]/u, "").tr("_ ", "--").squeeze("-")
150
+ slug = slug.sub(/^-+/, "").sub(/-+$/, "")
151
+ slug.empty? ? "untitled" : slug
145
152
  end
146
153
  end
147
154
  end
@@ -16,15 +16,17 @@ module Jekyll
16
16
  docs = site.collections["documents"]&.docs || []
17
17
  return if docs.empty?
18
18
 
19
- items = docs.map do |d|
19
+ items = docs.map do |doc|
20
+ data = doc.data
20
21
  {
21
- "url" => d.url,
22
- "title" => d.data["title"],
23
- "category" => d.data["category"],
24
- "date" => (d.data["date"] || Time.at(0)).strftime("%Y-%m-%d"),
25
- "slug" => d.data["slug"],
26
- "file_type" => d.data["file_type"],
27
- "extension" => d.data["extension"]
22
+ "url" => doc.url,
23
+ "title" => data["title"],
24
+ "category" => data["category"],
25
+ "date" => (data["date"] || Time.at(0)).strftime("%Y-%m-%d"),
26
+ "slug" => data["slug"],
27
+ "file_type" => data["file_type"],
28
+ "extension" => data["extension"],
29
+ "icon_url" => data["icon_url"]
28
30
  }
29
31
  end
30
32
 
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Jekyll
6
+ module Documents
7
+ # Registers gem-packaged _layouts and _includes with the site so Jekyll can
8
+ # discover them without the gem being declared as a theme.
9
+ #
10
+ # Jekyll only auto-discovers _layouts and _includes from theme gems (set via
11
+ # the +theme:+ config key). Plugin gems loaded via +group :jekyll_plugins+
12
+ # do not get their template directories added to Jekyll's lookup paths.
13
+ #
14
+ # This hook copies any gem-packaged layouts and includes into the site's
15
+ # source directories when the user has not provided their own. Files already
16
+ # present in the site take precedence (user overrides are never clobbered).
17
+ module LayoutRegistrar
18
+ GEM_ROOT = File.expand_path("../../..", __dir__)
19
+ TEMPLATE_DIRS = %w[_layouts _includes].freeze
20
+
21
+ class << self
22
+ # Copies gem-packaged template files into the site source.
23
+ # @param site [Jekyll::Site] the Jekyll site instance
24
+ # @return [void]
25
+ def register(site)
26
+ TEMPLATE_DIRS.each do |dir|
27
+ gem_dir = File.join(GEM_ROOT, dir)
28
+ next unless Dir.exist?(gem_dir)
29
+
30
+ copy_templates(gem_dir, File.join(site.source, dir))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Recursively copies files from +source_dir+ to +dest_dir+, skipping any
37
+ # file that already exists in the destination (user overrides win).
38
+ # @param source_dir [String] gem template directory
39
+ # @param dest_dir [String] site template directory
40
+ # @return [void]
41
+ def copy_templates(source_dir, dest_dir)
42
+ Dir.glob(File.join(source_dir, "**", "*")).each do |path|
43
+ next unless File.file?(path)
44
+
45
+ relative = path.delete_prefix("#{source_dir}/")
46
+ destination = File.join(dest_dir, relative)
47
+ next if File.exist?(destination)
48
+
49
+ FileUtils.mkdir_p(File.dirname(destination))
50
+ FileUtils.cp(path, destination)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ Jekyll::Hooks.register :site, :after_init do |site|
59
+ Jekyll::Documents::LayoutRegistrar.register(site)
60
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Documents
5
+ class DocumentIconTag < Liquid::Tag
6
+ public_class_method :new
7
+
8
+ include Jekyll::Filters::URLFilters
9
+
10
+ def initialize(tag_name, markup, tokens)
11
+ super
12
+ @document_expression, @options = parse_markup(markup)
13
+ end
14
+
15
+ def render(context)
16
+ document = context.find_variable(@document_expression)
17
+ return "" unless document
18
+
19
+ @context = context
20
+ icon_url = document_value(document, "icon_url") || fallback_icon(document, context)
21
+ url = relative_url(icon_url)
22
+ file_type = document_value(document, "file_type").to_s
23
+ alt = @options["alt"] || "#{file_type.upcase} file"
24
+ css_class = @options["class"] || "file-icon"
25
+
26
+ %(<img src="#{escape_html(url)}" alt="#{escape_html(alt)}" ) \
27
+ + %(class="#{escape_html(css_class)}" />)
28
+ end
29
+
30
+ private
31
+
32
+ def document_value(document, key)
33
+ return document.data[key] if document.respond_to?(:data)
34
+ return document[key] if document.respond_to?(:[])
35
+
36
+ nil
37
+ end
38
+
39
+ def fallback_icon(document, context)
40
+ file_type = document_value(document, "file_type")
41
+ icon_set = Configuration.read(context.registers[:site])["icon_set"]
42
+ FileTypeIcons.icon_for(file_type, icon_set)
43
+ end
44
+
45
+ def parse_markup(markup)
46
+ expression = markup.to_s.strip[/\A[^\s]+/]
47
+ options = {}
48
+ markup.to_s.scan(
49
+ /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
50
+ ) do |key, single, double, bare|
51
+ options[key] = single || double || bare
52
+ end
53
+ [expression, options]
54
+ end
55
+
56
+ def escape_html(value)
57
+ value.to_s.gsub(/[&<>"']/,
58
+ "&" => "&amp;",
59
+ "<" => "&lt;",
60
+ ">" => "&gt;",
61
+ '"' => "&quot;",
62
+ "'" => "&#39;")
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ Liquid::Template.register_tag("document_icon", Jekyll::Documents::DocumentIconTag)
@@ -17,14 +17,15 @@ module Jekyll
17
17
  category = @args["category"]
18
18
 
19
19
  docs = site.collections["documents"]&.docs || []
20
- docs = docs.select { |d| d.data["category"] == category } if category
21
- docs = docs.sort_by { |d| d.data["date"] || Time.at(0) }.reverse.first(count)
20
+ docs = docs.select { |doc| doc.data["category"] == category } if category
21
+ docs = docs.sort_by { |doc| doc.data["date"] || Time.at(0) }.reverse.first(count)
22
22
 
23
23
  out = +"<ul class=\"latest-documents\">\n"
24
- docs.each do |d|
25
- title = escape_html(d.data["title"])
26
- url = escape_html(d.url)
27
- date = (d.data["date"] || Time.at(0)).strftime("%Y-%m-%d")
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")
28
29
  out << %(<li><a href="#{url}">#{title}</a> <small>(#{date})</small></li>\n)
29
30
  end
30
31
  out << "</ul>\n"
@@ -50,11 +51,11 @@ module Jekyll
50
51
  args = {}
51
52
  # Safe: markup comes from Jekyll template authors (trusted), not end users
52
53
  # Runs only during static site generation, not on user requests
53
- markup.scan(/(\w+)\s*:\s*'([^']*)'|(\w+)\s*:\s*([^\s]+)/).each do |m|
54
- if m[0]
55
- args[m[0]] = m[1]
54
+ markup.scan(/(\w+)\s*:\s*'([^']*)'|(\w+)\s*:\s*([^\s]+)/).each do |match|
55
+ if match[0]
56
+ args[match[0]] = match[1]
56
57
  else
57
- args[m[2]] = m[3]
58
+ args[match[2]] = match[3]
58
59
  end
59
60
  end
60
61
  args
@@ -4,12 +4,15 @@ module Jekyll
4
4
  module Documents
5
5
  # A tiny StaticFile subclass for writing text files during build.
6
6
  class TextStaticFile < ::Jekyll::StaticFile
7
+ attr_reader :data
8
+
7
9
  def initialize(site, base, rel_dest, content)
8
10
  @site = site
9
11
  @base = base
10
12
  @dir = File.dirname(rel_dest)
11
13
  @name = File.basename(rel_dest)
12
14
  @content = content
15
+ @data = { "sitemap" => false }
13
16
  super(site, base, @dir, @name)
14
17
  end
15
18
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Documents
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.2"
6
6
  end
7
7
  end
@@ -7,10 +7,13 @@ require_relative "jekyll/documents/utils"
7
7
  require_relative "jekyll/documents/filters"
8
8
  require_relative "jekyll/documents/file_type_icons"
9
9
  require_relative "jekyll/documents/generator"
10
+ require_relative "jekyll/documents/assets_generator"
10
11
  require_relative "jekyll/documents/json_index_generator"
12
+ require_relative "jekyll/documents/layout_registrar"
11
13
 
12
14
  # Liquid tag(s)
13
15
  require_relative "jekyll/documents/tags/latest_documents"
16
+ require_relative "jekyll/documents/tags/document_icon"
14
17
 
15
18
  module Jekyll
16
19
  module Documents
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-documents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-30 00:00:00.000000000 Z
11
+ date: 2026-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.0'
19
+ version: '4.4'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '4.0'
29
+ version: '4.4'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: bundler-audit
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -58,70 +64,70 @@ dependencies:
58
64
  requirements:
59
65
  - - "~>"
60
66
  - !ruby/object:Gem::Version
61
- version: '6.1'
67
+ version: '6.5'
62
68
  type: :development
63
69
  prerelease: false
64
70
  version_requirements: !ruby/object:Gem::Requirement
65
71
  requirements:
66
72
  - - "~>"
67
73
  - !ruby/object:Gem::Version
68
- version: '6.1'
74
+ version: '6.5'
69
75
  - !ruby/object:Gem::Dependency
70
76
  name: rspec
71
77
  requirement: !ruby/object:Gem::Requirement
72
78
  requirements:
73
79
  - - "~>"
74
80
  - !ruby/object:Gem::Version
75
- version: '3.12'
81
+ version: '3.13'
76
82
  type: :development
77
83
  prerelease: false
78
84
  version_requirements: !ruby/object:Gem::Requirement
79
85
  requirements:
80
86
  - - "~>"
81
87
  - !ruby/object:Gem::Version
82
- version: '3.12'
88
+ version: '3.13'
83
89
  - !ruby/object:Gem::Dependency
84
90
  name: rubocop
85
91
  requirement: !ruby/object:Gem::Requirement
86
92
  requirements:
87
93
  - - "~>"
88
94
  - !ruby/object:Gem::Version
89
- version: '1.60'
95
+ version: '1.88'
90
96
  type: :development
91
97
  prerelease: false
92
98
  version_requirements: !ruby/object:Gem::Requirement
93
99
  requirements:
94
100
  - - "~>"
95
101
  - !ruby/object:Gem::Version
96
- version: '1.60'
102
+ version: '1.88'
97
103
  - !ruby/object:Gem::Dependency
98
104
  name: rubocop-performance
99
105
  requirement: !ruby/object:Gem::Requirement
100
106
  requirements:
101
107
  - - "~>"
102
108
  - !ruby/object:Gem::Version
103
- version: '1.20'
109
+ version: '1.26'
104
110
  type: :development
105
111
  prerelease: false
106
112
  version_requirements: !ruby/object:Gem::Requirement
107
113
  requirements:
108
114
  - - "~>"
109
115
  - !ruby/object:Gem::Version
110
- version: '1.20'
116
+ version: '1.26'
111
117
  - !ruby/object:Gem::Dependency
112
118
  name: simplecov
113
119
  requirement: !ruby/object:Gem::Requirement
114
120
  requirements:
115
121
  - - "~>"
116
122
  - !ruby/object:Gem::Version
117
- version: '0.22'
123
+ version: '1.0'
118
124
  type: :development
119
125
  prerelease: false
120
126
  version_requirements: !ruby/object:Gem::Requirement
121
127
  requirements:
122
128
  - - "~>"
123
129
  - !ruby/object:Gem::Version
124
- version: '0.22'
130
+ version: '1.0'
125
131
  - !ruby/object:Gem::Dependency
126
132
  name: yard
127
133
  requirement: !ruby/object:Gem::Requirement
@@ -253,11 +259,14 @@ files:
253
259
  - assets/js/documents-search.js
254
260
  - jekyll-documents.gemspec
255
261
  - lib/jekyll-documents.rb
262
+ - lib/jekyll/documents/assets_generator.rb
256
263
  - lib/jekyll/documents/configuration.rb
257
264
  - lib/jekyll/documents/file_type_icons.rb
258
265
  - lib/jekyll/documents/filters.rb
259
266
  - lib/jekyll/documents/generator.rb
260
267
  - lib/jekyll/documents/json_index_generator.rb
268
+ - lib/jekyll/documents/layout_registrar.rb
269
+ - lib/jekyll/documents/tags/document_icon.rb
261
270
  - lib/jekyll/documents/tags/latest_documents.rb
262
271
  - lib/jekyll/documents/utils.rb
263
272
  - lib/jekyll/documents/version.rb
@@ -279,14 +288,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
279
288
  requirements:
280
289
  - - ">="
281
290
  - !ruby/object:Gem::Version
282
- version: '2.7'
291
+ version: '3.3'
283
292
  required_rubygems_version: !ruby/object:Gem::Requirement
284
293
  requirements:
285
294
  - - ">="
286
295
  - !ruby/object:Gem::Version
287
296
  version: '0'
288
297
  requirements: []
289
- rubygems_version: 3.4.19
298
+ rubygems_version: 3.5.22
290
299
  signing_key:
291
300
  specification_version: 4
292
301
  summary: Auto-generate Jekyll pages for documents (PDF/DOCX/...) with category/date/title