jekyll-documents 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +168 -1
- data/_includes/category_list.html +1 -1
- data/assets/css/documents.css +34 -0
- data/assets/js/documents-search.js +1 -1
- data/jekyll-documents.gemspec +7 -7
- data/lib/jekyll/documents/configuration.rb +5 -0
- data/lib/jekyll/documents/file_type_icons.rb +5 -3
- data/lib/jekyll/documents/generator.rb +91 -1
- data/lib/jekyll/documents/layout_registrar.rb +9 -0
- data/lib/jekyll/documents/tags/doc_link.rb +3 -1
- data/lib/jekyll/documents/tags/document_icon.rb +4 -3
- data/lib/jekyll/documents/text_extraction_manifest.rb +169 -0
- data/lib/jekyll/documents/version.rb +1 -1
- data/lib/jekyll-documents.rb +1 -0
- metadata +27 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 989b1372181a7ca00444d25b5fab29751a077d9818c41cca1fe8c14208050214
|
|
4
|
+
data.tar.gz: 8215c9234df6eba090b83a296acd0c058a73ae3bd16b0d11fd2ce69e539d788e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba352213f38733179f053f989cfe5f363dee453176f4d301629fa0d5ab34b5d4f3aad443e8f46bc9b4ee9106626d09cbc255544862b12ba05850259a5b8879e9
|
|
7
|
+
data.tar.gz: 911cb909b005316a55d1978127935cf1bbf6da093c5dd0a9b6ec9e66112a9aa88d094e82c318f8c7312450ec970592caf506a62b392b47e3f0f35380be5be9d5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Auto-inject `file_type`, `icon_url`, and `icon_set` into jekyll-client-search's `passthrough_fields` config when `documents` is in the search collections — zero extra config needed for icons in search results
|
|
9
|
+
- Documented field renaming in jekyll-client-search for integration with other search conventions
|
|
10
|
+
- Framework-agnostic CSS file (`assets/css/documents.css`) with icon scaling utility classes (`icon-x1` through `icon-x9`: 16px to 512px) — no dependency on Bulma, Bootstrap, or Tailwind
|
|
11
|
+
- Icons default to `1em` (line-height) so they scale with surrounding text
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Renamed icon CSS class from `file-icon` to `document-file-icon` to avoid collision with Bulma's `.file-icon` (which uses `display: flex` and breaks inline SVG icons onto a separate line)
|
|
15
|
+
- Inline `1em` sizing added to JS-rendered and folder icons as a fallback when the CSS file is not included
|
|
16
|
+
|
|
17
|
+
## [0.5.0] - 2026-08-27
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Text extraction from PDF, DOCX, XLSX, PPTX, ODT, ODS, and ODP files via the optional `plaintext` gem, enabling full-text search of document contents through `jekyll-client-search`
|
|
21
|
+
- `extract_text` configuration option to enable text extraction (disabled by default)
|
|
22
|
+
- `text_max_bytes` configuration option to control extracted text truncation (default 500KB)
|
|
23
|
+
- `text_cache_dir` configuration option for the persistent cache directory (default `.cache/jekyll-documents`)
|
|
24
|
+
- `TextExtractionManifest` class with SHA-256 content-based cache invalidation, atomic writes, sharded text file storage, and automatic cleanup of entries for deleted source files
|
|
25
|
+
- Cache directory excluded from Jekyll build output via a `:site, :after_init` hook
|
|
26
|
+
- 9 integration tests with real DOCX and ODT fixtures validating extraction, caching, manifest persistence, and cleanup
|
|
27
|
+
- `plaintext` as a development dependency for testing extraction
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Minimum Ruby version raised from 3.3 to 3.4 (tested on Ruby 3.4.10)
|
|
31
|
+
- RuboCop target version updated from 3.3 to 3.4
|
|
32
|
+
- Updated development dependencies: `rubocop` ~> 1.90, `rubocop-performance` ~> 1.27, `simplecov` ~> 1.1, `rake` ~> 13.4
|
|
33
|
+
- Updated Gemfile pins: `google-protobuf` ~> 4.36, `sass-embedded` ~> 1.103
|
|
34
|
+
- CI workflow matrix simplified to Ruby 3.4 only (dropped 3.3)
|
|
35
|
+
- `documentation_uri` in gemspec metadata now points to DeepWiki instead of duplicating the GitHub repo URL
|
|
36
|
+
- Removed redundant `homepage_uri` from gemspec metadata (already covered by `spec.homepage`)
|
|
37
|
+
- Suppressed ActiveSupport deprecation warnings from the `plaintext` gem (`String#mb_chars`, deprecated in Rails 8.2)
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- `CHANGELOG.md` file permissions corrected to be world-readable (was `600`, now `644`)
|
|
41
|
+
|
|
5
42
|
## [0.4.0] - 2026-08-26
|
|
6
43
|
|
|
7
44
|
### Added
|
data/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
Turn files in `assets/documents/` into browsable document pages.
|
|
11
11
|
|
|
12
|
-
**Requirements**: Ruby 3.
|
|
12
|
+
**Requirements**: Ruby 3.4+ • Jekyll 4.4+
|
|
13
13
|
|
|
14
14
|
**Features**: Auto-collection • File icons • Categories • Search
|
|
15
15
|
|
|
@@ -120,6 +120,173 @@ documents:
|
|
|
120
120
|
|
|
121
121
|
See [configuration.rb](lib/jekyll/documents/configuration.rb) for all options.
|
|
122
122
|
|
|
123
|
+
## Icon sizing
|
|
124
|
+
|
|
125
|
+
Icons default to `1em` (line-height) so they scale with surrounding text.
|
|
126
|
+
A framework-agnostic CSS file with fixed-size utility classes is included:
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<link rel="stylesheet" href="{{ '/assets/css/documents.css' | relative_url }}">
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Class | Size |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| (default) | `1em` — scales with line height |
|
|
135
|
+
| `icon-x1` | 16px |
|
|
136
|
+
| `icon-x2` | 32px |
|
|
137
|
+
| `icon-x3` | 48px |
|
|
138
|
+
| `icon-x4` | 64px |
|
|
139
|
+
| `icon-x5` | 96px |
|
|
140
|
+
| `icon-x6` | 128px |
|
|
141
|
+
| `icon-x7` | 150px |
|
|
142
|
+
| `icon-x8` | 256px |
|
|
143
|
+
| `icon-x9` | 512px |
|
|
144
|
+
|
|
145
|
+
Usage with the `document_icon` tag:
|
|
146
|
+
|
|
147
|
+
```liquid
|
|
148
|
+
{% document_icon page class:"document-file-icon icon-x2" %}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or with any `<img>`:
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<img src="..." class="document-file-icon icon-x3" />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The CSS uses `display: inline-block` and `vertical-align: middle` — no
|
|
158
|
+
dependency on Bulma, Bootstrap, Tailwind, or any other framework.
|
|
159
|
+
|
|
160
|
+
## Search Integration
|
|
161
|
+
|
|
162
|
+
Documents are compatible with [jekyll-client-search](https://github.com/gundestrup/jekyll-client-search)
|
|
163
|
+
for client-side search. Each document has `categories` (plural array) and
|
|
164
|
+
searchable `content` baked in at generation time, so search plugins can
|
|
165
|
+
index uploaded files alongside posts.
|
|
166
|
+
|
|
167
|
+
To include documents in the search index, add `documents` to the
|
|
168
|
+
`collections` list in `_config.yml`:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
client_search:
|
|
172
|
+
collections:
|
|
173
|
+
- posts
|
|
174
|
+
- documents
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Icons and file-type metadata in search results
|
|
178
|
+
|
|
179
|
+
jekyll-documents bakes `file_type`, `icon_url`, and `icon_set` into each
|
|
180
|
+
document's data at generation time. When jekyll-client-search is installed
|
|
181
|
+
and `documents` is in the search collections, these fields are
|
|
182
|
+
**auto-injected** into the search index — no extra configuration needed:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
client_search:
|
|
186
|
+
collections:
|
|
187
|
+
- posts
|
|
188
|
+
- documents
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This makes jekyll-client-search render:
|
|
192
|
+
|
|
193
|
+
- `data-file-type`, `data-icon-set` attributes on each result `<article>`
|
|
194
|
+
(for CSS-based badges and theme-aware styling)
|
|
195
|
+
- An `<img class="client-search-result-icon">` before the title, using the
|
|
196
|
+
icon from the configured `icon_set` (color, lines, minimal, or ultra-minimal)
|
|
197
|
+
|
|
198
|
+
The icon automatically matches the `icon_set` configured in your `documents`
|
|
199
|
+
section — no extra configuration needed for theme consistency.
|
|
200
|
+
|
|
201
|
+
**Field renaming** (for integration with other search conventions):
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
client_search:
|
|
205
|
+
passthrough_fields:
|
|
206
|
+
- file_type: doctype # rename in the search index
|
|
207
|
+
- icon_url: thumbnail
|
|
208
|
+
icon_field: thumbnail
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**CSS examples:**
|
|
212
|
+
|
|
213
|
+
```css
|
|
214
|
+
/* File-type badge */
|
|
215
|
+
.client-search-result[data-file-type="pdf"]::before {
|
|
216
|
+
content: "PDF";
|
|
217
|
+
background: #e74c3c; color: white;
|
|
218
|
+
padding: 0 0.3em; font-size: 0.7em; margin-right: 0.3em;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Theme-aware icon sizing */
|
|
222
|
+
.client-search-result[data-icon-set="color"] .client-search-result-icon { width: 2em; }
|
|
223
|
+
.client-search-result[data-icon-set="ultra-minimal"] .client-search-result-icon { width: 1em; }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
See the [jekyll-client-search README](https://github.com/gundestrup/jekyll-client-search#customizing-search-results-with-css)
|
|
227
|
+
for all CSS customization options.
|
|
228
|
+
|
|
229
|
+
## Text Extraction
|
|
230
|
+
|
|
231
|
+
Extract text from PDF/DOCX/XLSX/PPTX/ODT/ODS/ODP files so search engines
|
|
232
|
+
can index document contents, not just metadata.
|
|
233
|
+
|
|
234
|
+
### Setup
|
|
235
|
+
|
|
236
|
+
Add the optional [`plaintext`](https://github.com/planio-gmbh/plaintext) gem
|
|
237
|
+
to your Gemfile:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# Gemfile
|
|
241
|
+
gem "plaintext", group: :jekyll_plugins
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Enable extraction in `_config.yml`:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
documents:
|
|
248
|
+
extract_text: true
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### How it works
|
|
252
|
+
|
|
253
|
+
- Extracted text is stored in `doc.content`, which `jekyll-client-search`
|
|
254
|
+
indexes automatically — no extra configuration needed
|
|
255
|
+
- Text is cached in `.cache/jekyll-documents/` (in your site source,
|
|
256
|
+
not `.jekyll-cache/`), so it **survives `jekyll clean`**
|
|
257
|
+
- Cache invalidation uses **SHA-256 file digests** — only changed files
|
|
258
|
+
are re-extracted
|
|
259
|
+
- Stale cache entries for deleted files are cleaned up automatically
|
|
260
|
+
at the start of each build
|
|
261
|
+
- Falls back to metadata-only content (title, category, file type, date)
|
|
262
|
+
if the `plaintext` gem is missing or extraction fails
|
|
263
|
+
|
|
264
|
+
### Configuration
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
documents:
|
|
268
|
+
extract_text: true # Enable text extraction
|
|
269
|
+
text_max_bytes: 500000 # Truncate extracted text (default 500KB)
|
|
270
|
+
text_cache_dir: ".cache/jekyll-documents" # Cache directory in site source
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Add the cache directory to `.gitignore`:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
.cache/jekyll-documents/
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### CLI tool dependencies
|
|
280
|
+
|
|
281
|
+
The `plaintext` gem uses the `rubyzip` Ruby gem for Office formats (no CLI
|
|
282
|
+
tools needed). PDF extraction shells out to a system command:
|
|
283
|
+
|
|
284
|
+
| Format | Tool |
|
|
285
|
+
|--------|------|
|
|
286
|
+
| PDF | `pdftotext` (poppler-utils) |
|
|
287
|
+
| DOCX/PPTX/XLSX | rubyzip (Ruby gem, no CLI needed) |
|
|
288
|
+
| ODT/ODS/ODP | rubyzip (Ruby gem, no CLI needed) |
|
|
289
|
+
|
|
123
290
|
## Development
|
|
124
291
|
|
|
125
292
|
```bash
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<ul class="documents-category-list">
|
|
5
5
|
{% for cat in cats %}
|
|
6
6
|
<li>
|
|
7
|
-
<img src="{{ '/assets/icons/' | append: icon_set | append: '/folder-svgrepo-com.svg' | relative_url }}" 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" style="width:1em;height:1em;vertical-align:middle;" />
|
|
8
8
|
<span class="category-name">{{ cat }}</span>
|
|
9
9
|
<span class="category-count">({{ site.documents | where: "category", cat | size }})</span>
|
|
10
10
|
</li>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* jekyll-documents — icon sizing utilities
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic: no dependency on Bulma, Bootstrap, Tailwind, etc.
|
|
5
|
+
* Icons default to 1em (line-height) so they scale with surrounding text.
|
|
6
|
+
* Override with .icon-x{N} classes for fixed pixel sizes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* {% document_icon page class:"document-file-icon icon-x2" %}
|
|
10
|
+
* <img src="..." class="document-file-icon icon-x3" />
|
|
11
|
+
*
|
|
12
|
+
* Include this stylesheet in your site:
|
|
13
|
+
* <link rel="stylesheet" href="{{ '/assets/css/documents.css' | relative_url }}">
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* Default: icon scales with line height */
|
|
17
|
+
.document-file-icon,
|
|
18
|
+
.folder-icon {
|
|
19
|
+
width: 1em;
|
|
20
|
+
height: 1em;
|
|
21
|
+
vertical-align: middle;
|
|
22
|
+
display: inline-block;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Fixed pixel sizes */
|
|
26
|
+
.icon-x1 { width: 16px; height: 16px; }
|
|
27
|
+
.icon-x2 { width: 32px; height: 32px; }
|
|
28
|
+
.icon-x3 { width: 48px; height: 48px; }
|
|
29
|
+
.icon-x4 { width: 64px; height: 64px; }
|
|
30
|
+
.icon-x5 { width: 96px; height: 96px; }
|
|
31
|
+
.icon-x6 { width: 128px; height: 128px; }
|
|
32
|
+
.icon-x7 { width: 150px; height: 150px; }
|
|
33
|
+
.icon-x8 { width: 256px; height: 256px; }
|
|
34
|
+
.icon-x9 { width: 512px; height: 512px; }
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
if (!iconUrl) return "";
|
|
33
33
|
const altText = `${String(fileType || "file").toUpperCase()} file`;
|
|
34
34
|
const url = escapeHtml(withBaseurl(iconUrl));
|
|
35
|
-
return `<img src="${url}" alt="${escapeHtml(altText)}" class="file-icon" />`;
|
|
35
|
+
return `<img src="${url}" alt="${escapeHtml(altText)}" class="document-file-icon" style="width:1em;height:1em;vertical-align:middle;" />`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function render(matches) {
|
data/jekyll-documents.gemspec
CHANGED
|
@@ -20,12 +20,11 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
"source_code_uri" => "https://github.com/gundestrup/jekyll-documents",
|
|
21
21
|
"bug_tracker_uri" => "https://github.com/gundestrup/jekyll-documents/issues",
|
|
22
22
|
"changelog_uri" => "https://github.com/gundestrup/jekyll-documents/blob/main/CHANGELOG.md",
|
|
23
|
-
"documentation_uri" => "https://
|
|
24
|
-
"homepage_uri" => "https://github.com/gundestrup/jekyll-documents",
|
|
23
|
+
"documentation_uri" => "https://deepwiki.com/gundestrup/jekyll-documents",
|
|
25
24
|
"rubygems_mfa_required" => "true"
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
spec.required_ruby_version = ">= 3.
|
|
27
|
+
spec.required_ruby_version = ">= 3.4"
|
|
29
28
|
|
|
30
29
|
spec.files = Dir.glob("{lib,assets,_includes,_layouts}/**/*") +
|
|
31
30
|
["README.md", "CHANGELOG.md", "LICENSE", "jekyll-documents.gemspec"]
|
|
@@ -34,10 +33,11 @@ Gem::Specification.new do |spec|
|
|
|
34
33
|
spec.add_dependency "jekyll", ">= 4.4", "< 5.0"
|
|
35
34
|
|
|
36
35
|
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
|
37
|
-
spec.add_development_dependency "
|
|
36
|
+
spec.add_development_dependency "plaintext", "~> 0.3"
|
|
37
|
+
spec.add_development_dependency "rake", "~> 13.4"
|
|
38
38
|
spec.add_development_dependency "rspec", "~> 3.13"
|
|
39
|
-
spec.add_development_dependency "rubocop", "~> 1.
|
|
40
|
-
spec.add_development_dependency "rubocop-performance", "~> 1.
|
|
41
|
-
spec.add_development_dependency "simplecov", "~> 1.
|
|
39
|
+
spec.add_development_dependency "rubocop", "~> 1.90"
|
|
40
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.27"
|
|
41
|
+
spec.add_development_dependency "simplecov", "~> 1.1"
|
|
42
42
|
spec.add_development_dependency "yard", "~> 0.9"
|
|
43
43
|
end
|
|
@@ -24,6 +24,11 @@ module Jekyll
|
|
|
24
24
|
"json_index" => true,
|
|
25
25
|
"json_index_path" => "/documents.json",
|
|
26
26
|
|
|
27
|
+
# Text extraction for search indexing (requires optional 'plaintext' gem)
|
|
28
|
+
"extract_text" => false,
|
|
29
|
+
"text_max_bytes" => 500_000,
|
|
30
|
+
"text_cache_dir" => ".cache/jekyll-documents",
|
|
31
|
+
|
|
27
32
|
# Optional category mapping
|
|
28
33
|
"category_map" => {}
|
|
29
34
|
}.freeze
|
|
@@ -154,11 +154,13 @@ module Jekyll
|
|
|
154
154
|
# @return [String] HTML img tag
|
|
155
155
|
# @example
|
|
156
156
|
# file_type_icon_tag('pdf') #=> \
|
|
157
|
-
# '<img src="
|
|
158
|
-
def file_type_icon_tag(file_type, css_class: "file-icon", alt: nil, context: nil)
|
|
157
|
+
# '<img src="..." alt="PDF file" class="document-file-icon" ... />'
|
|
158
|
+
def file_type_icon_tag(file_type, css_class: "document-file-icon", alt: nil, context: nil)
|
|
159
159
|
url = file_type_icon(file_type, context)
|
|
160
160
|
alt_text = alt || "#{file_type.to_s.upcase} file"
|
|
161
|
-
|
|
161
|
+
style = "width:1em;height:1em;vertical-align:middle;"
|
|
162
|
+
"<img src=\"#{url}\" alt=\"#{alt_text}\" " \
|
|
163
|
+
"class=\"#{css_class}\" style=\"#{style}\" />"
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
private
|
|
@@ -10,6 +10,18 @@ module Jekyll
|
|
|
10
10
|
safe true
|
|
11
11
|
priority :normal
|
|
12
12
|
|
|
13
|
+
CONTENT_TYPES = {
|
|
14
|
+
".pdf" => "application/pdf",
|
|
15
|
+
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
16
|
+
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
17
|
+
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
18
|
+
".odt" => "application/vnd.oasis.opendocument.text",
|
|
19
|
+
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
|
|
20
|
+
".odp" => "application/vnd.oasis.opendocument.presentation"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
private_constant :CONTENT_TYPES
|
|
24
|
+
|
|
13
25
|
# Generates document collection from files in configured root directory
|
|
14
26
|
# @param site [Jekyll::Site] the Jekyll site instance
|
|
15
27
|
# @return [void]
|
|
@@ -25,6 +37,8 @@ module Jekyll
|
|
|
25
37
|
|
|
26
38
|
collection = ensure_collection(site, "documents")
|
|
27
39
|
|
|
40
|
+
current_paths = []
|
|
41
|
+
|
|
28
42
|
Dir.glob("#{root}/**/*").each do |path|
|
|
29
43
|
next unless File.file?(path)
|
|
30
44
|
|
|
@@ -36,6 +50,7 @@ module Jekyll
|
|
|
36
50
|
next unless @config["include_extensions"].include?(ext)
|
|
37
51
|
|
|
38
52
|
rel_path = path.delete_prefix("#{site.source}/")
|
|
53
|
+
current_paths << rel_path
|
|
39
54
|
category = infer_category_from(rel_path)
|
|
40
55
|
basename = File.basename(path, ext)
|
|
41
56
|
|
|
@@ -61,10 +76,31 @@ module Jekyll
|
|
|
61
76
|
bake_document_data(doc, file_info)
|
|
62
77
|
collection.docs << doc
|
|
63
78
|
end
|
|
79
|
+
|
|
80
|
+
cleanup_manifest(current_paths) if @config["extract_text"]
|
|
81
|
+
configure_client_search(site)
|
|
64
82
|
end
|
|
65
83
|
|
|
66
84
|
private
|
|
67
85
|
|
|
86
|
+
# Auto-injects passthrough_fields into client_search config when the
|
|
87
|
+
# documents collection is indexed. Only acts if client_search is already
|
|
88
|
+
# configured with +documents+ in its collections list — does nothing if
|
|
89
|
+
# jekyll-client-search is not installed or not used.
|
|
90
|
+
def configure_client_search(site)
|
|
91
|
+
search_config = site.config["client_search"]
|
|
92
|
+
return unless search_config.is_a?(Hash)
|
|
93
|
+
return unless Array(search_config["collections"]).include?("documents")
|
|
94
|
+
|
|
95
|
+
fields = search_config["passthrough_fields"] || []
|
|
96
|
+
existing = fields.flat_map { |f| f.is_a?(Hash) ? f.keys : [f.to_s] }
|
|
97
|
+
%w[file_type icon_url icon_set].each do |field|
|
|
98
|
+
fields << field unless existing.include?(field)
|
|
99
|
+
end
|
|
100
|
+
search_config["passthrough_fields"] = fields
|
|
101
|
+
search_config["icon_field"] = "icon_url" unless search_config.key?("icon_field")
|
|
102
|
+
end
|
|
103
|
+
|
|
68
104
|
# Ensures a collection exists and is configured for output
|
|
69
105
|
# @param site [Jekyll::Site] the Jekyll site instance
|
|
70
106
|
# @param label [String] the collection name
|
|
@@ -83,6 +119,55 @@ module Jekyll
|
|
|
83
119
|
"#{title} #{data['category']} #{file_type} #{date_str}"
|
|
84
120
|
end
|
|
85
121
|
|
|
122
|
+
def extract_file_content(info)
|
|
123
|
+
return nil unless load_plaintext
|
|
124
|
+
|
|
125
|
+
content_type = CONTENT_TYPES[info[:ext]]
|
|
126
|
+
return nil unless content_type
|
|
127
|
+
|
|
128
|
+
manifest = text_manifest
|
|
129
|
+
rel_path = info[:rel_path]
|
|
130
|
+
digest = ::Digest::SHA256.file(info[:path]).hexdigest
|
|
131
|
+
|
|
132
|
+
cached = manifest.get(rel_path, digest)
|
|
133
|
+
return cached if cached
|
|
134
|
+
|
|
135
|
+
# Suppress ActiveSupport deprecation warnings from the plaintext gem
|
|
136
|
+
# (it uses String#mb_chars, deprecated in Rails 8.2).
|
|
137
|
+
# We apply our own truncation below, so the gem's internal limit is redundant.
|
|
138
|
+
text = ActiveSupport::Deprecation._instance.silence do
|
|
139
|
+
::Plaintext::Resolver.new(File.open(info[:path]), content_type).text
|
|
140
|
+
end
|
|
141
|
+
text = text&.truncate(@config["text_max_bytes"]) if text
|
|
142
|
+
manifest.set(rel_path, digest, text) if text
|
|
143
|
+
text
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
::Jekyll.logger.warn "jekyll-documents",
|
|
146
|
+
"Text extraction failed for #{info[:path]}: #{e.message}"
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def load_plaintext
|
|
151
|
+
return true if defined?(::Plaintext)
|
|
152
|
+
|
|
153
|
+
require "plaintext"
|
|
154
|
+
true
|
|
155
|
+
rescue LoadError
|
|
156
|
+
::Jekyll.logger.warn "jekyll-documents",
|
|
157
|
+
"extract_text is enabled but the 'plaintext' gem is not installed. " \
|
|
158
|
+
"Run: gem install plaintext"
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def text_manifest
|
|
163
|
+
@text_manifest ||= TextExtractionManifest.new(@site, @config["text_cache_dir"])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def cleanup_manifest(current_rel_paths)
|
|
167
|
+
text_manifest.cleanup_deleted(current_rel_paths)
|
|
168
|
+
text_manifest.save
|
|
169
|
+
end
|
|
170
|
+
|
|
86
171
|
def bake_document_data(doc, info)
|
|
87
172
|
data = doc.data
|
|
88
173
|
category = remap_category(info[:category])
|
|
@@ -102,7 +187,12 @@ module Jekyll
|
|
|
102
187
|
data["permalink"] = @config["permalink"]
|
|
103
188
|
.gsub(":category", category.to_s)
|
|
104
189
|
.gsub(":slug", info[:slug])
|
|
105
|
-
doc.content =
|
|
190
|
+
doc.content = if @config["extract_text"]
|
|
191
|
+
extract_file_content(info) || searchable_content(info[:title], data,
|
|
192
|
+
info[:file_type])
|
|
193
|
+
else
|
|
194
|
+
searchable_content(info[:title], data, info[:file_type])
|
|
195
|
+
end
|
|
106
196
|
end
|
|
107
197
|
|
|
108
198
|
# Creates a virtual source path for the document
|
|
@@ -57,4 +57,13 @@ end
|
|
|
57
57
|
|
|
58
58
|
Jekyll::Hooks.register :site, :after_init do |site|
|
|
59
59
|
Jekyll::Documents::LayoutRegistrar.register(site)
|
|
60
|
+
|
|
61
|
+
# Exclude the text extraction cache directory from Jekyll output
|
|
62
|
+
config = Jekyll::Documents::Configuration.read(site)
|
|
63
|
+
if config["extract_text"] && config["text_cache_dir"]
|
|
64
|
+
site.config["exclude"] = Array(site.config["exclude"])
|
|
65
|
+
unless site.config["exclude"].include?(config["text_cache_dir"])
|
|
66
|
+
site.config["exclude"] << config["text_cache_dir"]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
60
69
|
end
|
|
@@ -63,7 +63,9 @@ module Jekyll
|
|
|
63
63
|
|
|
64
64
|
url = relative_url(icon_url)
|
|
65
65
|
file_type = doc.data["file_type"].to_s.upcase
|
|
66
|
-
"<img src=\"#{escape_html(url)}\" alt=\"#{file_type}\"
|
|
66
|
+
"<img src=\"#{escape_html(url)}\" alt=\"#{file_type}\" " \
|
|
67
|
+
"class=\"document-file-icon doc-link-icon\" " \
|
|
68
|
+
"style=\"width:1em;height:1em;vertical-align:middle;\" />"
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def size_html(doc)
|
|
@@ -21,10 +21,11 @@ module Jekyll
|
|
|
21
21
|
url = relative_url(icon_url)
|
|
22
22
|
file_type = document_value(document, "file_type").to_s
|
|
23
23
|
alt = @options["alt"] || "#{file_type.upcase} file"
|
|
24
|
-
css_class = @options["class"] || "file-icon"
|
|
24
|
+
css_class = @options["class"] || "document-file-icon"
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
style = "width:1em;height:1em;vertical-align:middle;"
|
|
27
|
+
"<img src=\"#{escape_html(url)}\" alt=\"#{escape_html(alt)}\" " \
|
|
28
|
+
"class=\"#{escape_html(css_class)}\" style=\"#{style}\" />"
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
private
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
module Jekyll
|
|
8
|
+
module Documents
|
|
9
|
+
# Manages the text extraction manifest — a persistent JSON file that tracks
|
|
10
|
+
# which documents have had their text extracted, keyed by SHA-256 digest.
|
|
11
|
+
#
|
|
12
|
+
# The manifest lives in <site.source>/<cache_dir>/text-extraction-manifest.json
|
|
13
|
+
# and extracted text is stored in separate files under <cache_dir>/text/
|
|
14
|
+
# (sharded by the first 2 hex chars of the digest to avoid huge directories).
|
|
15
|
+
#
|
|
16
|
+
# This design borrows from jekyll-imgflow's ManifestManager:
|
|
17
|
+
# - Atomic writes (temp file + rename)
|
|
18
|
+
# - SHA-256 content digests for cache invalidation
|
|
19
|
+
# - Cleanup of entries for deleted source files
|
|
20
|
+
# - Survives `jekyll clean` (stored in site source, not .jekyll-cache/)
|
|
21
|
+
class TextExtractionManifest
|
|
22
|
+
MANIFEST_FILENAME = "text-extraction-manifest.json"
|
|
23
|
+
TEXT_SUBDIR = "text"
|
|
24
|
+
|
|
25
|
+
attr_reader :manifest_path, :cache_dir
|
|
26
|
+
|
|
27
|
+
# @param site [Jekyll::Site] the Jekyll site instance
|
|
28
|
+
# @param cache_dir [String] relative path from site source for cache directory
|
|
29
|
+
def initialize(site, cache_dir)
|
|
30
|
+
@site = site
|
|
31
|
+
@cache_dir = cache_dir
|
|
32
|
+
@cache_root = File.join(site.source, cache_dir)
|
|
33
|
+
@manifest_path = File.join(@cache_root, MANIFEST_FILENAME)
|
|
34
|
+
@text_dir = File.join(@cache_root, TEXT_SUBDIR)
|
|
35
|
+
@manifest = load_manifest
|
|
36
|
+
@dirty = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get cached text for a document if the digest matches.
|
|
40
|
+
# @param rel_path [String] relative path of the source document
|
|
41
|
+
# @param digest [String] SHA-256 hex digest of the source file
|
|
42
|
+
# @return [String, nil] extracted text if cache hit, nil otherwise
|
|
43
|
+
def get(rel_path, digest)
|
|
44
|
+
entry = @manifest[rel_path]
|
|
45
|
+
return nil unless entry
|
|
46
|
+
return nil unless entry["digest"] == digest
|
|
47
|
+
|
|
48
|
+
text_file = text_file_path(entry["text_file"])
|
|
49
|
+
return nil unless File.file?(text_file)
|
|
50
|
+
|
|
51
|
+
File.read(text_file, encoding: "UTF-8")
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
::Jekyll.logger.warn "jekyll-documents",
|
|
54
|
+
"Manifest read failed for #{rel_path}: #{e.message}"
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Store extracted text for a document.
|
|
59
|
+
# @param rel_path [String] relative path of the source document
|
|
60
|
+
# @param digest [String] SHA-256 hex digest of the source file
|
|
61
|
+
# @param text [String] the extracted text
|
|
62
|
+
# @return [void]
|
|
63
|
+
def set(rel_path, digest, text)
|
|
64
|
+
text_file = write_text_file(digest, text)
|
|
65
|
+
@manifest[rel_path] = {
|
|
66
|
+
"digest" => digest,
|
|
67
|
+
"text_file" => text_file,
|
|
68
|
+
"extracted_at" => Time.now.to_i
|
|
69
|
+
}
|
|
70
|
+
@dirty = true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Remove manifest entries and their text files for source documents
|
|
74
|
+
# that no longer exist.
|
|
75
|
+
# @param current_rel_paths [Array<String>] relative paths of current source files
|
|
76
|
+
# @return [Integer] number of entries removed
|
|
77
|
+
def cleanup_deleted(current_rel_paths)
|
|
78
|
+
current_set = current_rel_paths.to_set
|
|
79
|
+
removed = 0
|
|
80
|
+
|
|
81
|
+
@manifest.each_key do |rel_path|
|
|
82
|
+
next if current_set.include?(rel_path)
|
|
83
|
+
|
|
84
|
+
entry = @manifest[rel_path]
|
|
85
|
+
delete_text_file(entry["text_file"]) if entry
|
|
86
|
+
@manifest.delete(rel_path)
|
|
87
|
+
removed += 1
|
|
88
|
+
@dirty = true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
removed
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Save the manifest to disk if it has changed.
|
|
95
|
+
# Uses atomic write (temp file + rename) to prevent corruption.
|
|
96
|
+
# @return [void]
|
|
97
|
+
def save
|
|
98
|
+
return unless @dirty
|
|
99
|
+
|
|
100
|
+
content = JSON.pretty_generate(@manifest)
|
|
101
|
+
return if File.exist?(@manifest_path) && File.binread(@manifest_path) == content
|
|
102
|
+
|
|
103
|
+
FileUtils.mkdir_p(@cache_root)
|
|
104
|
+
temporary_path = "#{@manifest_path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
|
|
105
|
+
File.open(temporary_path, "wb") do |file|
|
|
106
|
+
file.write(content)
|
|
107
|
+
file.flush
|
|
108
|
+
file.fsync
|
|
109
|
+
end
|
|
110
|
+
File.rename(temporary_path, @manifest_path)
|
|
111
|
+
ensure
|
|
112
|
+
FileUtils.rm_f(temporary_path) if defined?(temporary_path) && temporary_path
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if a document is in the manifest with a matching digest.
|
|
116
|
+
# @param rel_path [String] relative path of the source document
|
|
117
|
+
# @param digest [String] SHA-256 hex digest of the source file
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def cached?(rel_path, digest)
|
|
120
|
+
entry = @manifest[rel_path]
|
|
121
|
+
!!(entry && entry["digest"] == digest)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Number of entries in the manifest.
|
|
125
|
+
# @return [Integer]
|
|
126
|
+
def size
|
|
127
|
+
@manifest.size
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def load_manifest
|
|
133
|
+
return {} unless File.file?(@manifest_path)
|
|
134
|
+
|
|
135
|
+
data = JSON.parse(File.read(@manifest_path, encoding: "UTF-8"))
|
|
136
|
+
data.is_a?(Hash) ? data : {}
|
|
137
|
+
rescue JSON::ParserError => e
|
|
138
|
+
::Jekyll.logger.warn "jekyll-documents",
|
|
139
|
+
"Corrupt text extraction manifest, starting fresh: #{e.message}"
|
|
140
|
+
{}
|
|
141
|
+
rescue StandardError
|
|
142
|
+
{}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def text_file_path(text_file)
|
|
146
|
+
File.join(@text_dir, text_file)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def write_text_file(digest, text)
|
|
150
|
+
shard = digest[0, 2]
|
|
151
|
+
subdir = File.join(@text_dir, shard)
|
|
152
|
+
FileUtils.mkdir_p(subdir)
|
|
153
|
+
filename = "#{digest}.txt"
|
|
154
|
+
path = File.join(subdir, filename)
|
|
155
|
+
File.write(path, text, encoding: "UTF-8")
|
|
156
|
+
File.join(shard, filename)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delete_text_file(text_file)
|
|
160
|
+
return unless text_file
|
|
161
|
+
|
|
162
|
+
path = text_file_path(text_file)
|
|
163
|
+
File.delete(path) if File.file?(path)
|
|
164
|
+
rescue StandardError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/jekyll-documents.rb
CHANGED
|
@@ -7,6 +7,7 @@ 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/text_extraction_manifest"
|
|
10
11
|
require_relative "jekyll/documents/assets_generator"
|
|
11
12
|
require_relative "jekyll/documents/json_index_generator"
|
|
12
13
|
require_relative "jekyll/documents/layout_registrar"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-documents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svend Gundestrup
|
|
@@ -43,20 +43,34 @@ dependencies:
|
|
|
43
43
|
- - "~>"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '0.9'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: plaintext
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0.3'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0.3'
|
|
46
60
|
- !ruby/object:Gem::Dependency
|
|
47
61
|
name: rake
|
|
48
62
|
requirement: !ruby/object:Gem::Requirement
|
|
49
63
|
requirements:
|
|
50
64
|
- - "~>"
|
|
51
65
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: '13.
|
|
66
|
+
version: '13.4'
|
|
53
67
|
type: :development
|
|
54
68
|
prerelease: false
|
|
55
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
70
|
requirements:
|
|
57
71
|
- - "~>"
|
|
58
72
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: '13.
|
|
73
|
+
version: '13.4'
|
|
60
74
|
- !ruby/object:Gem::Dependency
|
|
61
75
|
name: rspec
|
|
62
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -77,42 +91,42 @@ dependencies:
|
|
|
77
91
|
requirements:
|
|
78
92
|
- - "~>"
|
|
79
93
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '1.
|
|
94
|
+
version: '1.90'
|
|
81
95
|
type: :development
|
|
82
96
|
prerelease: false
|
|
83
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
98
|
requirements:
|
|
85
99
|
- - "~>"
|
|
86
100
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '1.
|
|
101
|
+
version: '1.90'
|
|
88
102
|
- !ruby/object:Gem::Dependency
|
|
89
103
|
name: rubocop-performance
|
|
90
104
|
requirement: !ruby/object:Gem::Requirement
|
|
91
105
|
requirements:
|
|
92
106
|
- - "~>"
|
|
93
107
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '1.
|
|
108
|
+
version: '1.27'
|
|
95
109
|
type: :development
|
|
96
110
|
prerelease: false
|
|
97
111
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
112
|
requirements:
|
|
99
113
|
- - "~>"
|
|
100
114
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: '1.
|
|
115
|
+
version: '1.27'
|
|
102
116
|
- !ruby/object:Gem::Dependency
|
|
103
117
|
name: simplecov
|
|
104
118
|
requirement: !ruby/object:Gem::Requirement
|
|
105
119
|
requirements:
|
|
106
120
|
- - "~>"
|
|
107
121
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '1.
|
|
122
|
+
version: '1.1'
|
|
109
123
|
type: :development
|
|
110
124
|
prerelease: false
|
|
111
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
112
126
|
requirements:
|
|
113
127
|
- - "~>"
|
|
114
128
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: '1.
|
|
129
|
+
version: '1.1'
|
|
116
130
|
- !ruby/object:Gem::Dependency
|
|
117
131
|
name: yard
|
|
118
132
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -144,6 +158,7 @@ files:
|
|
|
144
158
|
- _includes/documents_search.html
|
|
145
159
|
- _includes/latest_documents.html
|
|
146
160
|
- _layouts/document.html
|
|
161
|
+
- assets/css/documents.css
|
|
147
162
|
- assets/icons/color/ai-document-svgrepo-com.svg
|
|
148
163
|
- assets/icons/color/attachment-document-svgrepo-com.svg
|
|
149
164
|
- assets/icons/color/audio-document-svgrepo-com.svg
|
|
@@ -255,6 +270,7 @@ files:
|
|
|
255
270
|
- lib/jekyll/documents/tags/doc_link.rb
|
|
256
271
|
- lib/jekyll/documents/tags/document_icon.rb
|
|
257
272
|
- lib/jekyll/documents/tags/latest_documents.rb
|
|
273
|
+
- lib/jekyll/documents/text_extraction_manifest.rb
|
|
258
274
|
- lib/jekyll/documents/utils.rb
|
|
259
275
|
- lib/jekyll/documents/version.rb
|
|
260
276
|
homepage: https://github.com/gundestrup/jekyll-documents
|
|
@@ -264,8 +280,7 @@ metadata:
|
|
|
264
280
|
source_code_uri: https://github.com/gundestrup/jekyll-documents
|
|
265
281
|
bug_tracker_uri: https://github.com/gundestrup/jekyll-documents/issues
|
|
266
282
|
changelog_uri: https://github.com/gundestrup/jekyll-documents/blob/main/CHANGELOG.md
|
|
267
|
-
documentation_uri: https://
|
|
268
|
-
homepage_uri: https://github.com/gundestrup/jekyll-documents
|
|
283
|
+
documentation_uri: https://deepwiki.com/gundestrup/jekyll-documents
|
|
269
284
|
rubygems_mfa_required: 'true'
|
|
270
285
|
rdoc_options: []
|
|
271
286
|
require_paths:
|
|
@@ -274,7 +289,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
274
289
|
requirements:
|
|
275
290
|
- - ">="
|
|
276
291
|
- !ruby/object:Gem::Version
|
|
277
|
-
version: '3.
|
|
292
|
+
version: '3.4'
|
|
278
293
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
279
294
|
requirements:
|
|
280
295
|
- - ">="
|