jekyll-documents 0.3.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: 3865d4ab22aa0cf85e53ad31d222e4e4d025f7f53fb1271a949d326ee8ad60a3
4
- data.tar.gz: 483721501a072d99ade0088e958b52e02583b4d13de7ee26ecd74bc7141b9c87
3
+ metadata.gz: 2c05b94feef92602870b24fd7b9c4e9244eb724061c1ef599ed12a23905d51ad
4
+ data.tar.gz: ccfe08306d471478e21092fd7518df1c6d593b05fa219051a6d866d3c947c6c9
5
5
  SHA512:
6
- metadata.gz: c923ebb2b6e801ab1449b76ed999e8408d234ddb03c03c5b6bf21086c54a49a6fbc7d74b258049248ae53c159c81814d7f23ee44e1f590d9c89f49814bf56630
7
- data.tar.gz: 639afd3c1abce1196a31b7fb7e34acc197d681f5dc863c53b480f7d127b104db0e27c9adb02c88889dcc62977f4d86162bfade3a077ea269f811e0f24a6da3da
6
+ metadata.gz: '087a677f0f1efd8c54d7b52c05c1fd0b88c746e9063f8fc0ba843c5ca24db53b8cbdd6904a959d7ca606e490bf8da590ff9b400b21a9de25204079ed0f0b0eff'
7
+ data.tar.gz: 7519b8f1e7035b4391299dd6399dcb1a6fa6572d4b3567891e35fc3f383a7b2958acc28bdbfe058983d6fbf73592c33a609dd07ae082751410873d8d29ac1434
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
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
3
42
 
4
43
  ## [0.3.0] - 2026-07-23
5
44
 
@@ -13,8 +52,8 @@
13
52
  - **System test for full build pipeline**: End-to-end test covering generator → JSON index → tag rendering
14
53
 
15
54
  ### Changed
16
- - **Minimum Ruby version**: Bumped from `>= 2.7` to `>= 3.1` (Ruby 2.7 EOL March 2023, 3.0 EOL April 2024)
17
- - **RuboCop `TargetRubyVersion`**: Updated from `2.7` to `3.1` to match gemspec
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
18
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
19
58
  - **`category_list.html` icon set**: Now reads `icon_set` from first document's baked data instead of non-functional `site.documents.icon_set`
20
59
  - **`latest_documents.html` count fallback**: Simplified to `| default: 5` since `site.config` is inaccessible in Liquid
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
@@ -2,7 +2,7 @@
2
2
  <ul class="documents-list">
3
3
  {% for doc in docs %}
4
4
  <li>
5
- <img src="{{ doc.icon_url | relative_url }}" alt="{{ doc.file_type | upcase }} file" class="file-icon" />
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,6 +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>
5
12
  <script src="https://unpkg.com/lunr/lunr.js"></script>
6
13
  <script src="{{ '/assets/js/documents-search.js' | relative_url }}"></script>
14
+ {% endif %}
@@ -3,7 +3,7 @@
3
3
  <ul class="latest-documents">
4
4
  {% for doc in docs %}
5
5
  <li>
6
- <img src="{{ doc.icon_url | relative_url }}" alt="{{ doc.file_type | upcase }} file" class="file-icon" />
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
- <img src="{{ page.icon_url | relative_url }}" alt="{{ page.file_type | upcase }} file" class="file-icon" />
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 = ">= 3.1"
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"
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
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,16 +115,25 @@ 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
+
98
126
  # Returns the icon URL for a given file type and explicit icon set
99
127
  # @param file_type [String] the file extension (e.g., 'pdf', 'docx')
100
128
  # @param icon_set [String] the icon set name (defaults to 'color')
101
129
  # @return [String] the icon file URL
102
130
  # @example
103
- # FileTypeIcons.icon_for('pdf', 'lines') #=> "/assets/icons/lines/pdf-svgrepo-com.svg"
131
+ # FileTypeIcons.icon_for('pdf', 'lines') #=>
132
+ # "/assets/icons/lines/pdf-file-type-svgrepo-com.svg"
104
133
  def self.icon_for(file_type, icon_set = "color")
105
- icons = ICON_MAP[icon_set] || ICON_MAP["color"]
106
- icons[file_type.to_s.downcase] ||
107
- "/assets/icons/#{icon_set}/unknown-document-svgrepo-com.svg"
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]
108
137
  end
109
138
 
110
139
  # Returns the icon URL for a given file type using configured icon set
@@ -58,7 +58,7 @@ module Jekyll
58
58
  data = doc.data
59
59
  data["layout"] = @config["layout"]
60
60
  data["title"] = title
61
- data["date"] = date || File.mtime(path)
61
+ data["date"] = date ? date.to_time : File.mtime(path)
62
62
  data["category"] = remap_category(category)
63
63
  data["file_url"] = "/#{rel_path}"
64
64
  data["extension"] = ext
@@ -25,7 +25,8 @@ module Jekyll
25
25
  "date" => (data["date"] || Time.at(0)).strftime("%Y-%m-%d"),
26
26
  "slug" => data["slug"],
27
27
  "file_type" => data["file_type"],
28
- "extension" => data["extension"]
28
+ "extension" => data["extension"],
29
+ "icon_url" => data["icon_url"]
29
30
  }
30
31
  end
31
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Documents
5
- VERSION = "0.3.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.3.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,56 +64,56 @@ 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
@@ -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: '3.1'
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