jekyll-documents 0.3.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +50 -2
- data/README.md +53 -4
- data/_includes/documents_list.html +1 -1
- data/_includes/documents_search.html +8 -0
- data/_includes/latest_documents.html +1 -1
- data/_layouts/document.html +1 -1
- data/assets/js/documents-search.js +68 -90
- data/jekyll-documents.gemspec +6 -6
- data/lib/jekyll/documents/assets_generator.rb +48 -0
- data/lib/jekyll/documents/file_type_icons.rb +67 -38
- data/lib/jekyll/documents/generator.rb +2 -1
- data/lib/jekyll/documents/json_index_generator.rb +4 -1
- data/lib/jekyll/documents/layout_registrar.rb +60 -0
- data/lib/jekyll/documents/tags/doc_category.rb +117 -0
- data/lib/jekyll/documents/tags/doc_link.rb +131 -0
- data/lib/jekyll/documents/tags/document_icon.rb +68 -0
- data/lib/jekyll/documents/version.rb +1 -1
- data/lib/jekyll-documents.rb +5 -0
- metadata +25 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2e673dc309679ea3bc1ce25687bb9fbf52901f27b7b29517c63ef9337291e82
|
|
4
|
+
data.tar.gz: f89cef0e3d22c85665b516bd52da7f137b612c62c21f24e22ef33dc7a10170a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 956c33415793dd9e285f9e81874b612278c188d0dd39237a1bbdeea7964af1d784655b171d7efbf485b37dfa948c02ade3703b4e82e1b411a90d9c2049cb0f65
|
|
7
|
+
data.tar.gz: c6069125baef1f06acbd58124f5993881b2e595a60b02e470fb0a8a3a94a764f62b0bbd8ad53320b325d614590aa494fb469f83674145f1924db945e1afc0e46
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.3.3] - 2026-08-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `{% doc_link "title" %}` Liquid tag for linking to documents by partial title or slug match (case-insensitive); renders file icon, title, and human-readable file size; supports `text:"custom label"`, `icon:false`, and `size:false` options
|
|
9
|
+
- `{% doc_category "name" %}` Liquid tag for linking to category pages or listing documents by category; supports `text:"label"`, `list:true`, and `limit:N` options
|
|
10
|
+
- `file_size` attribute baked into each document's data at generation time
|
|
11
|
+
- `file_url` and `file_size` fields included in the generated `/documents.json` index
|
|
12
|
+
- 30 RSpec examples covering `DocLinkTag` and `DocCategoryTag` (exact/partial matching, slug matching, icon/size rendering, custom text, baseurl, HTML escaping, list mode, limit, sorting, empty collection handling)
|
|
13
|
+
|
|
14
|
+
## [0.3.2] - 2026-08-12
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- 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
|
|
18
|
+
- 7 RSpec examples for `LayoutRegistrar` covering copy, override, idempotency, and hook registration
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- 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
|
|
22
|
+
|
|
23
|
+
## [0.3.1] - 2026-08-03
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Added the context-aware `{% document_icon page %}` Liquid tag for simple icon rendering
|
|
27
|
+
- Added installed-gem system coverage for all supported document types and icon themes
|
|
28
|
+
- Added Playwright browser coverage for search interaction, links, icons, baseurl, and network errors
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Minimum Ruby version raised from `>= 3.2` to `>= 3.3`
|
|
32
|
+
- Updated RuboCop target Ruby version to 3.3
|
|
33
|
+
- Updated compatible development gems, including RuboCop, Reek, RSpec, and Playwright tooling
|
|
34
|
+
- Test and quality tasks now rebuild and force-install the gem before testing
|
|
35
|
+
- Added package-content checks to verify the built gem contains runtime templates, icons, JavaScript, and Liquid tags
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Corrected non-color icon mappings to reference packaged SVG assets
|
|
39
|
+
- Registered packaged icons and search JavaScript for copying during Jekyll builds
|
|
40
|
+
- Made document search respect the configured index path and site base URL
|
|
41
|
+
- Hardened document search index validation and disabled the search include when JSON indexing is disabled
|
|
42
|
+
- Escaped search-result URLs, icon URLs, titles, categories, and dates before rendering
|
|
43
|
+
- Added the missing date conversion required for Jekyll 4.4 document publishing
|
|
44
|
+
- Removed duplicate search scripts from the example site
|
|
45
|
+
- Made Reek failures fail the quality task consistently
|
|
46
|
+
|
|
47
|
+
### Verification
|
|
48
|
+
- 186 Ruby/system examples passing with 100% Ruby line coverage
|
|
49
|
+
- 3 Playwright browser tests passing
|
|
50
|
+
- 0 RuboCop offenses, 0 Reek warnings, and 0 Bundler Audit vulnerabilities
|
|
3
51
|
|
|
4
52
|
## [0.3.0] - 2026-07-23
|
|
5
53
|
|
|
@@ -13,8 +61,8 @@
|
|
|
13
61
|
- **System test for full build pipeline**: End-to-end test covering generator → JSON index → tag rendering
|
|
14
62
|
|
|
15
63
|
### Changed
|
|
16
|
-
- **Minimum Ruby version**: Bumped from `>= 2.7` to `>= 3.
|
|
17
|
-
- **RuboCop `TargetRubyVersion`**: Updated from `2.7` to `3.
|
|
64
|
+
- **Minimum Ruby version**: Bumped from `>= 2.7` to `>= 3.2` (zeitwerk 2.7+ — a Jekyll dependency — requires Ruby >= 3.2)
|
|
65
|
+
- **RuboCop `TargetRubyVersion`**: Updated from `2.7` to `3.2` to match gemspec
|
|
18
66
|
- **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
67
|
- **`category_list.html` icon set**: Now reads `icon_set` from first document's baked data instead of non-functional `site.documents.icon_set`
|
|
20
68
|
- **`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
|
+
[](https://github.com/gundestrup/jekyll-documents)
|
|
3
|
+
[](https://github.com/gundestrup/jekyll-documents/actions/workflows/test.yml)
|
|
4
|
+
[](https://codecov.io/gh/gundestrup/jekyll-documents)
|
|
5
|
+
[](https://rubygems.org/gems/jekyll-documents)
|
|
6
|
+
[](https://rubygems.org/gems/jekyll-documents)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](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
|
+
[](https://deepwiki.com/gundestrup/jekyll-documents)
|
|
17
|
+
|
|
7
18
|
## Quick Start
|
|
8
19
|
|
|
9
20
|
```ruby
|
|
@@ -52,18 +63,52 @@ 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
|
-
{
|
|
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
|
|
|
77
|
+
### Includes
|
|
78
|
+
|
|
61
79
|
```liquid
|
|
62
80
|
{% include latest_documents.html count=5 %}
|
|
63
81
|
{% include documents_list.html %}
|
|
64
82
|
{% include documents_search.html %}
|
|
65
83
|
```
|
|
66
84
|
|
|
85
|
+
### Liquid Tags
|
|
86
|
+
|
|
87
|
+
**Link to a document** by title or slug (partial match, case-insensitive):
|
|
88
|
+
|
|
89
|
+
```liquid
|
|
90
|
+
{% doc_link "Annual Report" %}
|
|
91
|
+
{% doc_link "annual" text:"Read the report" %}
|
|
92
|
+
{% doc_link "board-meeting" icon:false size:false %}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Renders an `<a>` tag with the file type icon, title, and human-readable file size.
|
|
96
|
+
Use `text:"..."` to override the link text, `icon:false` to hide the icon,
|
|
97
|
+
or `size:false` to hide the file size.
|
|
98
|
+
|
|
99
|
+
**Link to or list a category**:
|
|
100
|
+
|
|
101
|
+
```liquid
|
|
102
|
+
{% doc_category "reports" %}
|
|
103
|
+
{% doc_category "reports" text:"All reports" %}
|
|
104
|
+
{% doc_category "reports" list:true %}
|
|
105
|
+
{% doc_category "reports" list:true limit:5 %}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Link mode renders an `<a>` tag to the category page.
|
|
109
|
+
List mode renders a `<ul>` of all documents in the category, sorted by date descending.
|
|
110
|
+
Use `limit:N` to cap the number of items.
|
|
111
|
+
|
|
67
112
|
## Configuration
|
|
68
113
|
|
|
69
114
|
```yaml
|
|
@@ -78,15 +123,19 @@ See [configuration.rb](lib/jekyll/documents/configuration.rb) for all options.
|
|
|
78
123
|
## Development
|
|
79
124
|
|
|
80
125
|
```bash
|
|
81
|
-
rake #
|
|
82
|
-
rake
|
|
126
|
+
rake # Ruby quality checks
|
|
127
|
+
rake test # Rebuild/install gem + Ruby and browser tests
|
|
128
|
+
rake browser_test # Browser search tests (Node.js + Chromium required)
|
|
129
|
+
rake quick # Fast Ruby check
|
|
83
130
|
rake help # Show commands
|
|
84
131
|
```
|
|
85
132
|
|
|
86
|
-
**Quality**:
|
|
133
|
+
**Quality**: 100% coverage • 0 vulnerabilities • 0 offenses • 0 warnings
|
|
87
134
|
|
|
88
135
|
See [README.Development.md](README.Development.md) for details.
|
|
89
136
|
|
|
137
|
+
**DeepWiki**: [https://deepwiki.com/gundestrup/jekyll-documents](https://deepwiki.com/gundestrup/jekyll-documents)
|
|
138
|
+
|
|
90
139
|
## Release
|
|
91
140
|
|
|
92
141
|
```bash
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<ul class="documents-list">
|
|
3
3
|
{% for doc in docs %}
|
|
4
4
|
<li>
|
|
5
|
-
|
|
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
|
-
|
|
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>
|
data/_layouts/document.html
CHANGED
|
@@ -13,7 +13,7 @@ layout: default
|
|
|
13
13
|
|
|
14
14
|
<p>
|
|
15
15
|
<a href="{{ page.file_url | relative_url }}" class="document-download">
|
|
16
|
-
|
|
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
|
|
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 =
|
|
10
|
+
const indexUrl = window.__jekyllDocumentsIndexPath || "/documents.json";
|
|
13
11
|
let idx, docs = [];
|
|
14
12
|
|
|
15
|
-
function escapeHtml(
|
|
13
|
+
function escapeHtml(value) {
|
|
16
14
|
const div = document.createElement("div");
|
|
17
|
-
div.appendChild(document.createTextNode(
|
|
18
|
-
return div.innerHTML
|
|
15
|
+
div.appendChild(document.createTextNode(String(value)));
|
|
16
|
+
return div.innerHTML
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
return `<img src="${
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
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
|
|
99
|
-
|
|
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(
|
|
73
|
+
docs.forEach(document => this.add(document));
|
|
110
74
|
});
|
|
111
75
|
}
|
|
112
76
|
|
|
113
|
-
function doSearch(
|
|
114
|
-
if (!idx || !
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
135
|
-
|
|
136
|
-
|
|
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(
|
|
116
|
+
input.addEventListener("input", debounce(event => doSearch(event.target.value), 150));
|
|
139
117
|
})();
|
data/jekyll-documents.gemspec
CHANGED
|
@@ -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.
|
|
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.
|
|
39
|
-
spec.add_development_dependency "rspec", "~> 3.
|
|
40
|
-
spec.add_development_dependency "rubocop", "~> 1.
|
|
41
|
-
spec.add_development_dependency "rubocop-performance", "~> 1.
|
|
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" => "
|
|
42
|
-
"docx" => "
|
|
43
|
-
"doc" => "
|
|
44
|
-
"pptx" => "
|
|
45
|
-
"ppt" => "
|
|
46
|
-
"xlsx" => "
|
|
47
|
-
"xls" => "
|
|
48
|
-
"odt" => "
|
|
49
|
-
"ods" => "
|
|
50
|
-
"odp" => "
|
|
51
|
-
"txt" => "
|
|
52
|
-
"zip" => "
|
|
53
|
-
"mp3" => "
|
|
54
|
-
"mp4" => "
|
|
55
|
-
"jpg" => "
|
|
56
|
-
"jpeg" => "
|
|
57
|
-
"png" => "
|
|
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" => "
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
"
|
|
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') #=>
|
|
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
|
-
|
|
106
|
-
icons[
|
|
107
|
-
|
|
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,13 +58,14 @@ module Jekyll
|
|
|
58
58
|
data = doc.data
|
|
59
59
|
data["layout"] = @config["layout"]
|
|
60
60
|
data["title"] = title
|
|
61
|
-
data["date"] = date
|
|
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
|
|
65
65
|
data["file_type"] = file_type
|
|
66
66
|
data["icon_set"] = icon_set
|
|
67
67
|
data["icon_url"] = Jekyll::Documents::FileTypeIcons.icon_for(file_type, icon_set)
|
|
68
|
+
data["file_size"] = File.size(path)
|
|
68
69
|
data["slug"] = slug
|
|
69
70
|
data["permalink"] = @config["permalink"]
|
|
70
71
|
.gsub(":category", data["category"].to_s)
|
|
@@ -25,7 +25,10 @@ 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"],
|
|
30
|
+
"file_url" => data["file_url"],
|
|
31
|
+
"file_size" => data["file_size"]
|
|
29
32
|
}
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Documents
|
|
5
|
+
class DocCategoryTag < Liquid::Tag
|
|
6
|
+
public_class_method :new
|
|
7
|
+
|
|
8
|
+
include Jekyll::Filters::URLFilters
|
|
9
|
+
|
|
10
|
+
def initialize(tag_name, markup, tokens)
|
|
11
|
+
super
|
|
12
|
+
@category, @options = parse_markup(markup)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(context)
|
|
16
|
+
docs = documents_from(context)
|
|
17
|
+
return "" if docs.empty?
|
|
18
|
+
|
|
19
|
+
categories = available_categories(docs)
|
|
20
|
+
category = resolve_category(@category, categories)
|
|
21
|
+
return "" unless category
|
|
22
|
+
|
|
23
|
+
@context = context
|
|
24
|
+
return render_list(docs, category) if @options["list"] == "true"
|
|
25
|
+
|
|
26
|
+
render_link(category)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def documents_from(context)
|
|
32
|
+
site = context.registers[:site]
|
|
33
|
+
site.collections["documents"]&.docs || []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def available_categories(docs)
|
|
37
|
+
docs.map { |doc| doc.data["category"].to_s }.uniq.sort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_category(query, categories)
|
|
41
|
+
normalized = query.to_s.strip.downcase
|
|
42
|
+
return nil if normalized.empty?
|
|
43
|
+
|
|
44
|
+
categories.find { |cat| cat.downcase == normalized } ||
|
|
45
|
+
categories.find { |cat| cat.downcase.include?(normalized) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_link(category)
|
|
49
|
+
url = relative_url("/documents/#{category}/")
|
|
50
|
+
text = @options["text"] || category
|
|
51
|
+
%(<a href="#{escape_html(url)}">#{escape_html(text)}</a>)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_list(docs, category)
|
|
55
|
+
cat_docs = sorted_category_docs(docs, category)
|
|
56
|
+
limit = @options["limit"]&.to_i
|
|
57
|
+
cat_docs = cat_docs.first(limit) if limit&.positive?
|
|
58
|
+
|
|
59
|
+
out = %(<ul class="doc-category-list" data-category="#{escape_html(category)}">\n)
|
|
60
|
+
out << list_items(cat_docs)
|
|
61
|
+
out << "</ul>\n"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sorted_category_docs(docs, category)
|
|
65
|
+
docs.select { |doc| doc.data["category"].to_s == category }
|
|
66
|
+
.sort_by { |doc| doc.data["date"] || Time.at(0) }.reverse
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def list_items(cat_docs)
|
|
70
|
+
cat_docs.each_with_object(+"") do |doc, out|
|
|
71
|
+
title = escape_html(doc.data["title"])
|
|
72
|
+
url = escape_html(relative_url(doc.url))
|
|
73
|
+
date = (doc.data["date"] || Time.at(0)).strftime("%Y-%m-%d")
|
|
74
|
+
out << %(<li><a href="#{url}">#{title}</a> <small>(#{date})</small></li>\n)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_markup(markup)
|
|
79
|
+
text = markup.to_s
|
|
80
|
+
category, remainder = extract_category(text)
|
|
81
|
+
options = extract_options(remainder)
|
|
82
|
+
[category, options]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def extract_category(text)
|
|
86
|
+
quoted = text.match(/\A["']([^"']+)["']/)
|
|
87
|
+
return [quoted[1], text[quoted.end(0)..]] if quoted
|
|
88
|
+
|
|
89
|
+
bare = text.strip.match(/\A([^\s]+)/)
|
|
90
|
+
return [nil, ""] unless bare
|
|
91
|
+
|
|
92
|
+
[bare[1], text[bare.end(0)..]]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_options(text)
|
|
96
|
+
options = {}
|
|
97
|
+
text.to_s.scan(
|
|
98
|
+
/(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
|
|
99
|
+
) do |key, single, double, bare|
|
|
100
|
+
options[key] = single || double || bare
|
|
101
|
+
end
|
|
102
|
+
options
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def escape_html(value)
|
|
106
|
+
value.to_s.gsub(/[&<>"']/,
|
|
107
|
+
"&" => "&",
|
|
108
|
+
"<" => "<",
|
|
109
|
+
">" => ">",
|
|
110
|
+
'"' => """,
|
|
111
|
+
"'" => "'")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Liquid::Template.register_tag("doc_category", Jekyll::Documents::DocCategoryTag)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Documents
|
|
5
|
+
class DocLinkTag < Liquid::Tag
|
|
6
|
+
public_class_method :new
|
|
7
|
+
|
|
8
|
+
include Jekyll::Filters::URLFilters
|
|
9
|
+
|
|
10
|
+
SIZE_UNITS = %w[B KB MB GB].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(tag_name, markup, tokens)
|
|
13
|
+
super
|
|
14
|
+
@query, @options = parse_markup(markup)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render(context)
|
|
18
|
+
docs = documents_from(context)
|
|
19
|
+
return "" if docs.empty?
|
|
20
|
+
|
|
21
|
+
match = find_document(docs, @query)
|
|
22
|
+
return "" unless match
|
|
23
|
+
|
|
24
|
+
@context = context
|
|
25
|
+
url = relative_url(match.url)
|
|
26
|
+
text = @options["text"] || match.data["title"]
|
|
27
|
+
|
|
28
|
+
build_link(match, url, text)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def documents_from(context)
|
|
34
|
+
site = context.registers[:site]
|
|
35
|
+
site.collections["documents"]&.docs || []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def find_document(docs, query)
|
|
39
|
+
normalized = query.to_s.strip.downcase
|
|
40
|
+
return nil if normalized.empty?
|
|
41
|
+
|
|
42
|
+
docs.find { |doc| matches?(doc, normalized) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def matches?(doc, query)
|
|
46
|
+
title = doc.data["title"].to_s.downcase
|
|
47
|
+
slug = doc.data["slug"].to_s.downcase
|
|
48
|
+
title == query || slug == query ||
|
|
49
|
+
title.include?(query) || slug.include?(query)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_link(doc, url, text)
|
|
53
|
+
inner = +""
|
|
54
|
+
inner << icon_html(doc) if @options["icon"] != "false"
|
|
55
|
+
inner << %(<span class="doc-link-title">#{escape_html(text)}</span>)
|
|
56
|
+
inner << size_html(doc) if @options["size"] != "false"
|
|
57
|
+
%(<a href="#{escape_html(url)}" class="doc-link">#{inner}</a>)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def icon_html(doc)
|
|
61
|
+
icon_url = doc.data["icon_url"]
|
|
62
|
+
return "" unless icon_url
|
|
63
|
+
|
|
64
|
+
url = relative_url(icon_url)
|
|
65
|
+
file_type = doc.data["file_type"].to_s.upcase
|
|
66
|
+
"<img src=\"#{escape_html(url)}\" alt=\"#{file_type}\" class=\"file-icon doc-link-icon\" />"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def size_html(doc)
|
|
70
|
+
size = doc.data["file_size"]
|
|
71
|
+
return "" unless size
|
|
72
|
+
|
|
73
|
+
"<span class=\"doc-link-size has-text-grey is-size-7\">#{format_size(size)}</span>"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def format_size(bytes)
|
|
77
|
+
return "0 B" unless bytes&.positive?
|
|
78
|
+
|
|
79
|
+
value, unit = compute_size(bytes)
|
|
80
|
+
unit == "B" ? "#{value.to_i} B" : format("%<value>.1f %<unit>s", value:, unit:)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def compute_size(bytes)
|
|
84
|
+
value = bytes.to_f
|
|
85
|
+
SIZE_UNITS.each do |unit|
|
|
86
|
+
return [value, unit] if value < 1024 || unit == SIZE_UNITS.last
|
|
87
|
+
|
|
88
|
+
value /= 1024
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_markup(markup)
|
|
93
|
+
text = markup.to_s
|
|
94
|
+
query, remainder = extract_query(text)
|
|
95
|
+
options = extract_options(remainder)
|
|
96
|
+
[query, options]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def extract_query(text)
|
|
100
|
+
quoted = text.match(/\A["']([^"']+)["']/)
|
|
101
|
+
return [quoted[1], text[quoted.end(0)..]] if quoted
|
|
102
|
+
|
|
103
|
+
bare = text.strip.match(/\A([^\s]+)/)
|
|
104
|
+
return [nil, ""] unless bare
|
|
105
|
+
|
|
106
|
+
[bare[1], text[bare.end(0)..]]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract_options(text)
|
|
110
|
+
options = {}
|
|
111
|
+
text.to_s.scan(
|
|
112
|
+
/(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|([^\s]+))/
|
|
113
|
+
) do |key, single, double, bare|
|
|
114
|
+
options[key] = single || double || bare
|
|
115
|
+
end
|
|
116
|
+
options
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def escape_html(value)
|
|
120
|
+
value.to_s.gsub(/[&<>"']/,
|
|
121
|
+
"&" => "&",
|
|
122
|
+
"<" => "<",
|
|
123
|
+
">" => ">",
|
|
124
|
+
'"' => """,
|
|
125
|
+
"'" => "'")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Liquid::Template.register_tag("doc_link", Jekyll::Documents::DocLinkTag)
|
|
@@ -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
|
+
"&" => "&",
|
|
59
|
+
"<" => "<",
|
|
60
|
+
">" => ">",
|
|
61
|
+
'"' => """,
|
|
62
|
+
"'" => "'")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Liquid::Template.register_tag("document_icon", Jekyll::Documents::DocumentIconTag)
|
data/lib/jekyll-documents.rb
CHANGED
|
@@ -7,10 +7,15 @@ 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"
|
|
17
|
+
require_relative "jekyll/documents/tags/doc_link"
|
|
18
|
+
require_relative "jekyll/documents/tags/doc_category"
|
|
14
19
|
|
|
15
20
|
module Jekyll
|
|
16
21
|
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.
|
|
4
|
+
version: 0.3.3
|
|
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-
|
|
11
|
+
date: 2026-08-13 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
116
|
+
version: '1.26'
|
|
111
117
|
- !ruby/object:Gem::Dependency
|
|
112
118
|
name: simplecov
|
|
113
119
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -253,11 +259,16 @@ 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/doc_category.rb
|
|
270
|
+
- lib/jekyll/documents/tags/doc_link.rb
|
|
271
|
+
- lib/jekyll/documents/tags/document_icon.rb
|
|
261
272
|
- lib/jekyll/documents/tags/latest_documents.rb
|
|
262
273
|
- lib/jekyll/documents/utils.rb
|
|
263
274
|
- lib/jekyll/documents/version.rb
|
|
@@ -279,14 +290,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
279
290
|
requirements:
|
|
280
291
|
- - ">="
|
|
281
292
|
- !ruby/object:Gem::Version
|
|
282
|
-
version: '3.
|
|
293
|
+
version: '3.3'
|
|
283
294
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
284
295
|
requirements:
|
|
285
296
|
- - ">="
|
|
286
297
|
- !ruby/object:Gem::Version
|
|
287
298
|
version: '0'
|
|
288
299
|
requirements: []
|
|
289
|
-
rubygems_version: 3.
|
|
300
|
+
rubygems_version: 3.5.22
|
|
290
301
|
signing_key:
|
|
291
302
|
specification_version: 4
|
|
292
303
|
summary: Auto-generate Jekyll pages for documents (PDF/DOCX/...) with category/date/title
|