jekyll-documents 0.6.1 → 0.7.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 +11 -1
- data/README.md +29 -5
- data/lib/jekyll/documents/configuration.rb +20 -2
- data/lib/jekyll/documents/generator.rb +110 -41
- data/lib/jekyll/documents/json_index_generator.rb +3 -0
- data/lib/jekyll/documents/tags/doc_category.rb +80 -21
- data/lib/jekyll/documents/tags/doc_link.rb +54 -15
- data/lib/jekyll/documents/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34e9456980c65a0eab294a17100d6de0bcf522bc3652afcadc3c2521c8c9d48b
|
|
4
|
+
data.tar.gz: be4ff590ed8c3e251fb7c56a1e57a33e2650d60a63c44113bb2a4c48402fe906
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c456c2e5ff0b55a6d51bc777770bed926b6cfa4c8a1393fda110f5d0a8d3e0e49af5c5399e1a0bdf2f4c8f5632e19ec8490975722842a5657b9f6e737c4412a
|
|
7
|
+
data.tar.gz: 5ff47c4204e58ad1e49bd48cd146d4f51ccf44254f1244530b0fd39f8fa2b5817531cfa293409a3f6fbad7063bc3425599e2c1bf37d094b5dbb45bba5cc13dc8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.7.0] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added stable `source_path`, `category_path`, and `category_slug` document metadata and JSON index fields
|
|
7
|
+
- Added exact `path:` resolution for document and category tags plus explicit category aggregation
|
|
8
|
+
- Added path/date permalink placeholders and full-path-first category mappings
|
|
9
|
+
- Added configurable `warn` or `strict` handling for unresolved and ambiguous tag references
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Ambiguous tags no longer silently select the first match
|
|
13
|
+
- Duplicate generated permalinks now abort the build and identify every conflicting source file
|
|
4
14
|
|
|
5
15
|
## [0.6.1] - 2026-09-03
|
|
6
16
|
|
data/README.md
CHANGED
|
@@ -90,24 +90,27 @@ The tag applies the configured icon set and site `baseurl` automatically. The `f
|
|
|
90
90
|
{% doc_link "Annual Report" %}
|
|
91
91
|
{% doc_link "annual" text:"Read the report" %}
|
|
92
92
|
{% doc_link "board-meeting" icon:false size:false %}
|
|
93
|
+
{% doc_link path:"Reports/2026-03-01_Annual_Report.pdf" %}
|
|
93
94
|
```
|
|
94
95
|
|
|
95
96
|
Renders an `<a>` tag with the file type icon, title, and human-readable file size.
|
|
96
97
|
Use `text:"..."` to override the link text, `icon:false` to hide the icon,
|
|
97
|
-
or `size:false` to hide the file size.
|
|
98
|
+
or `size:false` to hide the file size. A `path:` lookup is exact and case-sensitive after
|
|
99
|
+
normalizing `/` and `\` separators; use it when a title or slug matches multiple documents.
|
|
98
100
|
|
|
99
101
|
**Link to or list a category**:
|
|
100
102
|
|
|
101
103
|
```liquid
|
|
102
104
|
{% doc_category "reports" %}
|
|
103
105
|
{% doc_category "reports" text:"All reports" %}
|
|
104
|
-
{% doc_category "reports" list:true %}
|
|
105
106
|
{% doc_category "reports" list:true limit:5 %}
|
|
107
|
+
{% doc_category path:"Departments/Europe/Reports" list:true %}
|
|
108
|
+
{% doc_category "reports" aggregate:true list:true %}
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
Link mode renders an `<a>` tag to the category page.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
Link mode renders an `<a>` tag to the category page. List mode renders a `<ul>` sorted by date.
|
|
112
|
+
Use `path:` for an exact, case-sensitive category path, `limit:N` to cap the list, and
|
|
113
|
+
`aggregate:true list:true` to explicitly combine repeated short category names.
|
|
111
114
|
|
|
112
115
|
## Configuration
|
|
113
116
|
|
|
@@ -116,8 +119,29 @@ documents:
|
|
|
116
119
|
root: "assets/documents"
|
|
117
120
|
icon_set: "color"
|
|
118
121
|
strict_filename: true
|
|
122
|
+
resolution_mode: "warn"
|
|
119
123
|
```
|
|
120
124
|
|
|
125
|
+
`resolution_mode` controls ambiguous or unknown path references. The default `warn` mode logs
|
|
126
|
+
all matching candidates and renders nothing. Set it to `strict` to log the same diagnostic and
|
|
127
|
+
abort the build.
|
|
128
|
+
|
|
129
|
+
Generated documents expose `source_path`, `category_path`, and `category_slug`. Category mappings
|
|
130
|
+
check the complete `category_path` before falling back to its final directory component.
|
|
131
|
+
Permalinks support `:category`, `:category_path`, `:slug`, `:source_path`, `:date`, `:year`,
|
|
132
|
+
`:month`, and `:day`. For example:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
documents:
|
|
136
|
+
permalink: "/documents/:category_path/:date/:slug/"
|
|
137
|
+
category_map:
|
|
138
|
+
Departments/Europe/Research: "European Research"
|
|
139
|
+
Research: "Research"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The default permalink remains backward compatible. Any duplicate final permalink is fatal and
|
|
143
|
+
the build error lists every conflicting `source_path`.
|
|
144
|
+
|
|
121
145
|
See [configuration.rb](lib/jekyll/documents/configuration.rb) for all options.
|
|
122
146
|
|
|
123
147
|
## Icon sizing
|
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Jekyll
|
|
4
4
|
module Documents
|
|
5
|
+
module ResolutionReporter
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def report_resolution_issue(site, message)
|
|
9
|
+
method = Configuration.read(site)["resolution_mode"].to_s == "strict" ? :abort_with : :warn
|
|
10
|
+
::Jekyll.logger.public_send(method, "jekyll-documents", message)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
5
14
|
class Configuration
|
|
15
|
+
RESOLUTION_MODES = %w[warn strict].freeze
|
|
16
|
+
private_constant :RESOLUTION_MODES
|
|
17
|
+
|
|
6
18
|
DEFAULTS = {
|
|
7
19
|
"root" => "assets/documents",
|
|
8
20
|
"permalink" => "/documents/:category/:slug/",
|
|
@@ -19,6 +31,7 @@ module Jekyll
|
|
|
19
31
|
# Validation
|
|
20
32
|
"strict_filename" => true,
|
|
21
33
|
"strict_extensions" => true,
|
|
34
|
+
"resolution_mode" => "warn",
|
|
22
35
|
|
|
23
36
|
# JSON Index for Lunr
|
|
24
37
|
"json_index" => true,
|
|
@@ -34,8 +47,13 @@ module Jekyll
|
|
|
34
47
|
}.freeze
|
|
35
48
|
|
|
36
49
|
def self.read(site)
|
|
37
|
-
cfg = site.config["documents"] || {}
|
|
38
|
-
|
|
50
|
+
cfg = DEFAULTS.merge(site.config["documents"] || {})
|
|
51
|
+
mode = cfg["resolution_mode"].to_s
|
|
52
|
+
unless RESOLUTION_MODES.include?(mode)
|
|
53
|
+
::Jekyll.logger.abort_with "jekyll-documents",
|
|
54
|
+
"resolution_mode must be warn or strict, got #{mode.inspect}"
|
|
55
|
+
end
|
|
56
|
+
cfg
|
|
39
57
|
end
|
|
40
58
|
end
|
|
41
59
|
end
|
|
@@ -37,23 +37,26 @@ module Jekyll
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
collection = ensure_collection(site, "documents")
|
|
40
|
-
|
|
41
40
|
current_paths = []
|
|
41
|
+
generated_docs = []
|
|
42
42
|
|
|
43
43
|
Dir.glob("#{root}/**/*").each do |path|
|
|
44
44
|
next unless File.file?(path)
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
source_extension = File.extname(path)
|
|
47
|
+
ext = source_extension.downcase
|
|
47
48
|
if @config["strict_extensions"] && !@config["include_extensions"].include?(ext)
|
|
48
49
|
::Jekyll.logger.abort_with "jekyll-documents",
|
|
49
50
|
"Unsupported file type: #{path} (#{ext})"
|
|
50
51
|
end
|
|
51
52
|
next unless @config["include_extensions"].include?(ext)
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
source_path = source_path_for(path, root)
|
|
55
|
+
category_path = document_category_path(source_path)
|
|
56
|
+
category = remap_category(category_path)
|
|
57
|
+
rel_path = normalize_path(File.join(@config["root"], source_path))
|
|
54
58
|
current_paths << rel_path
|
|
55
|
-
|
|
56
|
-
basename = File.basename(path, ext)
|
|
59
|
+
basename = File.basename(path, source_extension)
|
|
57
60
|
|
|
58
61
|
date, title, valid = parse_filename(basename)
|
|
59
62
|
if !valid && @config["strict_filename"]
|
|
@@ -61,23 +64,23 @@ module Jekyll
|
|
|
61
64
|
"Filename must be 'YYYY-MM-DD_Title.ext' → #{rel_path}"
|
|
62
65
|
end
|
|
63
66
|
|
|
64
|
-
slug = build_slug(basename)
|
|
65
|
-
file_type = ext.sub(".", "").downcase
|
|
66
|
-
icon_set = @config["icon_set"]
|
|
67
|
-
|
|
68
67
|
doc = ::Jekyll::Document.new(
|
|
69
|
-
source_stub_for(
|
|
68
|
+
source_stub_for(source_path),
|
|
70
69
|
site: site,
|
|
71
70
|
collection: collection
|
|
72
71
|
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
file_info = {
|
|
73
|
+
title: title, date: date, category: category, category_path: category_path,
|
|
74
|
+
category_slug: slugify(category, "uncategorized"), source_path: source_path,
|
|
75
|
+
rel_path: rel_path, ext: ext, file_type: ext.delete_prefix("."),
|
|
76
|
+
icon_set: @config["icon_set"], slug: build_slug(basename), path: path
|
|
77
|
+
}
|
|
77
78
|
bake_document_data(doc, file_info)
|
|
78
79
|
collection.docs << doc
|
|
80
|
+
generated_docs << doc
|
|
79
81
|
end
|
|
80
82
|
|
|
83
|
+
detect_permalink_collisions(generated_docs)
|
|
81
84
|
cleanup_manifest(current_paths) if @config["extract_text"]
|
|
82
85
|
configure_client_search(site)
|
|
83
86
|
end
|
|
@@ -205,23 +208,22 @@ module Jekyll
|
|
|
205
208
|
|
|
206
209
|
def bake_document_data(doc, info)
|
|
207
210
|
data = doc.data
|
|
208
|
-
|
|
209
|
-
data["
|
|
210
|
-
data["
|
|
211
|
-
data["
|
|
212
|
-
data["
|
|
213
|
-
data["
|
|
214
|
-
data["
|
|
215
|
-
data["
|
|
216
|
-
data["
|
|
217
|
-
data["
|
|
218
|
-
|
|
219
|
-
data["
|
|
220
|
-
data["
|
|
221
|
-
data["
|
|
222
|
-
data["
|
|
223
|
-
|
|
224
|
-
.gsub(":slug", info[:slug])
|
|
211
|
+
data["layout"] = @config["layout"]
|
|
212
|
+
data["title"] = info[:title]
|
|
213
|
+
data["date"] = info[:date] ? info[:date].to_time : File.mtime(info[:path])
|
|
214
|
+
data["category"] = info[:category]
|
|
215
|
+
data["category_path"] = info[:category_path]
|
|
216
|
+
data["category_slug"] = info[:category_slug]
|
|
217
|
+
data["categories"] = [info[:category]] if info[:category]
|
|
218
|
+
data["source_path"] = info[:source_path]
|
|
219
|
+
data["file_url"] = "/#{info[:rel_path]}"
|
|
220
|
+
data["extension"] = info[:ext]
|
|
221
|
+
data["file_type"] = info[:file_type]
|
|
222
|
+
data["icon_set"] = info[:icon_set]
|
|
223
|
+
data["icon_url"] = FileTypeIcons.icon_for(info[:file_type], info[:icon_set])
|
|
224
|
+
data["file_size"] = File.size(info[:path])
|
|
225
|
+
data["slug"] = info[:slug]
|
|
226
|
+
data["permalink"] = expand_permalink(data)
|
|
225
227
|
metadata_content = searchable_content(info[:title], data, info[:file_type])
|
|
226
228
|
doc.content = if @config["extract_text"]
|
|
227
229
|
extracted_content = extract_file_content(info)
|
|
@@ -236,11 +238,10 @@ module Jekyll
|
|
|
236
238
|
end
|
|
237
239
|
|
|
238
240
|
# Creates a virtual source path for the document
|
|
239
|
-
# @param
|
|
240
|
-
# @param category [String] the document category
|
|
241
|
+
# @param source_path [String] the unique path below the documents root
|
|
241
242
|
# @return [String] virtual source path
|
|
242
|
-
def source_stub_for(
|
|
243
|
-
File.join("_documents", "#{
|
|
243
|
+
def source_stub_for(source_path)
|
|
244
|
+
File.join("_documents", "#{normalize_path(source_path)}.md")
|
|
244
245
|
end
|
|
245
246
|
|
|
246
247
|
# Infers category from the file's directory path
|
|
@@ -249,16 +250,80 @@ module Jekyll
|
|
|
249
250
|
def infer_category_from(rel_path)
|
|
250
251
|
return "uncategorized" unless @config["categories_from_path"]
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
source_path = source_path_for(rel_path, @config["root"])
|
|
254
|
+
category_path_for(source_path).split("/").last || "uncategorized"
|
|
254
255
|
end
|
|
255
256
|
|
|
256
257
|
# Remaps category name using category_map configuration
|
|
257
258
|
# @param cat [String] the original category
|
|
258
|
-
# @return [String] the
|
|
259
|
+
# @return [String] the mapped display category or lowercased directory name
|
|
259
260
|
def remap_category(cat)
|
|
260
261
|
map = @config["category_map"] || {}
|
|
261
|
-
|
|
262
|
+
leaf = cat.to_s.split("/").last || "uncategorized"
|
|
263
|
+
mapped = map[cat] || map[leaf]
|
|
264
|
+
mapped ? mapped.to_s : leaf.downcase
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def normalize_path(path)
|
|
268
|
+
path.to_s.tr("\\", "/").squeeze("/").delete_prefix("/").delete_suffix("/")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def source_path_for(path, root)
|
|
272
|
+
normalized_path = normalize_path(path)
|
|
273
|
+
normalized_root = normalize_path(root)
|
|
274
|
+
normalized_path.delete_prefix("#{normalized_root}/")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def category_path_for(source_path)
|
|
278
|
+
path = normalize_path(source_path)
|
|
279
|
+
path.include?("/") ? path.rpartition("/").first : "uncategorized"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def document_category_path(source_path)
|
|
283
|
+
@config["categories_from_path"] ? category_path_for(source_path) : "uncategorized"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def expand_permalink(data)
|
|
287
|
+
values = permalink_values(data)
|
|
288
|
+
@config["permalink"].gsub(/:([a-z_]+)/) do |placeholder|
|
|
289
|
+
values.fetch(Regexp.last_match(1), placeholder)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def permalink_values(data)
|
|
294
|
+
date = data["date"]
|
|
295
|
+
{
|
|
296
|
+
"category" => data["category_slug"],
|
|
297
|
+
"category_path" => slugify_path(data["category_path"]),
|
|
298
|
+
"slug" => data["slug"],
|
|
299
|
+
"date" => date.strftime("%Y-%m-%d"),
|
|
300
|
+
"year" => date.strftime("%Y"),
|
|
301
|
+
"month" => date.strftime("%m"),
|
|
302
|
+
"day" => date.strftime("%d"),
|
|
303
|
+
"source_path" => source_path_url(data["source_path"])
|
|
304
|
+
}
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def slugify_path(path)
|
|
308
|
+
normalize_path(path).split("/").map { |segment| slugify(segment, "untitled") }.join("/")
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def source_path_url(source_path)
|
|
312
|
+
extension = File.extname(source_path)
|
|
313
|
+
stem = source_path.delete_suffix(extension)
|
|
314
|
+
"#{slugify_path(stem)}#{extension.downcase}"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def detect_permalink_collisions(docs)
|
|
318
|
+
collisions = docs.group_by(&:url).select { |_url, matches| matches.size > 1 }
|
|
319
|
+
return if collisions.empty?
|
|
320
|
+
|
|
321
|
+
details = collisions.sort.map do |url, matches|
|
|
322
|
+
paths = matches.map { |doc| doc.data["source_path"] }.sort.join(", ")
|
|
323
|
+
"#{url}: #{paths}"
|
|
324
|
+
end
|
|
325
|
+
::Jekyll.logger.abort_with "jekyll-documents",
|
|
326
|
+
"Permalink collision detected:\n#{details.join("\n")}"
|
|
262
327
|
end
|
|
263
328
|
|
|
264
329
|
# Parses filename to extract date and title
|
|
@@ -283,7 +348,11 @@ module Jekyll
|
|
|
283
348
|
# @param basename [String] the filename without extension
|
|
284
349
|
# @return [String] the generated slug
|
|
285
350
|
def build_slug(basename)
|
|
286
|
-
|
|
351
|
+
slugify(basename.sub(/^\d{4}-\d{2}-\d{2}_/, ""), "untitled")
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def slugify(value, fallback)
|
|
355
|
+
slug = value.to_s
|
|
287
356
|
if @config["slug_danish_map"]
|
|
288
357
|
slug = slug.gsub(/[æøåÆØÅ]/,
|
|
289
358
|
{ "æ" => "ae", "ø" => "oe", "å" => "aa", "Æ" => "Ae", "Ø" => "Oe",
|
|
@@ -292,7 +361,7 @@ module Jekyll
|
|
|
292
361
|
slug = slug.downcase if @config["slug_downcase"]
|
|
293
362
|
slug = slug.gsub(/[^\p{Alnum}\-_\s]/u, "").tr("_ ", "--").squeeze("-")
|
|
294
363
|
slug = slug.sub(/^-+/, "").sub(/-+$/, "")
|
|
295
|
-
slug.empty? ?
|
|
364
|
+
slug.empty? ? fallback : slug
|
|
296
365
|
end
|
|
297
366
|
end
|
|
298
367
|
end
|
|
@@ -22,6 +22,9 @@ module Jekyll
|
|
|
22
22
|
"url" => doc.url,
|
|
23
23
|
"title" => data["title"],
|
|
24
24
|
"category" => data["category"],
|
|
25
|
+
"category_path" => data["category_path"],
|
|
26
|
+
"category_slug" => data["category_slug"],
|
|
27
|
+
"source_path" => data["source_path"],
|
|
25
28
|
"date" => (data["date"] || Time.at(0)).strftime("%Y-%m-%d"),
|
|
26
29
|
"slug" => data["slug"],
|
|
27
30
|
"file_type" => data["file_type"],
|
|
@@ -2,9 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
module Jekyll
|
|
4
4
|
module Documents
|
|
5
|
+
module CategoryResolver
|
|
6
|
+
include ResolutionReporter
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def resolve_category(query, options, categories, site)
|
|
11
|
+
return resolve_category_path(options["path"], categories, site) if options["path"]
|
|
12
|
+
|
|
13
|
+
normalized = query.to_s.strip.downcase
|
|
14
|
+
return nil if normalized.empty?
|
|
15
|
+
|
|
16
|
+
exact = categories.select do |category|
|
|
17
|
+
category_values(category).include?(normalized)
|
|
18
|
+
end
|
|
19
|
+
matches = exact.empty? ? partial_categories(categories, normalized) : exact
|
|
20
|
+
aggregate = options["aggregate"] == "true" && options["list"] == "true"
|
|
21
|
+
return matches if matches.one? || (aggregate && matches.any?)
|
|
22
|
+
return nil if matches.empty?
|
|
23
|
+
|
|
24
|
+
paths = matches.map { |category| category["path"] }.sort.join(", ")
|
|
25
|
+
message = "Ambiguous doc_category #{query.inspect}; matches: #{paths}. Rendering nothing."
|
|
26
|
+
report_resolution_issue(site, message)
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resolve_category_path(path, categories, site)
|
|
31
|
+
normalized = normalize_category_path(path)
|
|
32
|
+
matches = categories.select { |category| category["path"] == normalized }
|
|
33
|
+
return matches if matches.one?
|
|
34
|
+
|
|
35
|
+
issue = matches.empty? ? "Unknown" : "Ambiguous"
|
|
36
|
+
report_resolution_issue(site,
|
|
37
|
+
"#{issue} doc_category path #{path.inspect}: #{normalized}")
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def partial_categories(categories, query)
|
|
42
|
+
categories.select do |category|
|
|
43
|
+
category_values(category).any? { |value| value.include?(query) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def category_values(category)
|
|
48
|
+
leaf = category["path"].split("/").last.to_s.downcase
|
|
49
|
+
[category["category"], category["slug"], leaf].map { |value| value.to_s.downcase }.uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_category_path(path)
|
|
53
|
+
path.to_s.strip.tr("\\", "/").squeeze("/").delete_prefix("./").delete_prefix("/")
|
|
54
|
+
.delete_suffix("/")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
5
58
|
class DocCategoryTag < Liquid::Tag
|
|
6
59
|
public_class_method :new
|
|
7
60
|
|
|
61
|
+
include CategoryResolver
|
|
8
62
|
include Jekyll::Filters::URLFilters
|
|
9
63
|
|
|
10
64
|
def initialize(tag_name, markup, tokens)
|
|
@@ -17,13 +71,13 @@ module Jekyll
|
|
|
17
71
|
return "" if docs.empty?
|
|
18
72
|
|
|
19
73
|
categories = available_categories(docs)
|
|
20
|
-
|
|
21
|
-
return "" unless
|
|
74
|
+
matches = resolve_category(@category, @options, categories, context.registers[:site])
|
|
75
|
+
return "" unless matches
|
|
22
76
|
|
|
23
77
|
@context = context
|
|
24
|
-
return render_list(docs,
|
|
78
|
+
return render_list(docs, matches) if @options["list"] == "true"
|
|
25
79
|
|
|
26
|
-
render_link(
|
|
80
|
+
render_link(matches)
|
|
27
81
|
end
|
|
28
82
|
|
|
29
83
|
private
|
|
@@ -34,36 +88,39 @@ module Jekyll
|
|
|
34
88
|
end
|
|
35
89
|
|
|
36
90
|
def available_categories(docs)
|
|
37
|
-
docs.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
categories.find { |cat| cat.downcase == normalized } ||
|
|
45
|
-
categories.find { |cat| cat.downcase.include?(normalized) }
|
|
91
|
+
categories = docs.group_by { |doc| doc.data["category_path"] || doc.data["category"] }
|
|
92
|
+
.map do |path, matches|
|
|
93
|
+
data = matches.first.data
|
|
94
|
+
{ "path" => path.to_s, "category" => data["category"],
|
|
95
|
+
"slug" => data["category_slug"] || data["category"] }
|
|
96
|
+
end
|
|
97
|
+
categories.sort_by { |category| category["path"] }
|
|
46
98
|
end
|
|
47
99
|
|
|
48
|
-
def render_link(
|
|
49
|
-
|
|
50
|
-
|
|
100
|
+
def render_link(matches)
|
|
101
|
+
category = matches.first
|
|
102
|
+
url = relative_url("/documents/#{category['slug']}/")
|
|
103
|
+
text = @options["text"] || category["category"]
|
|
51
104
|
%(<a href="#{escape_html(url)}">#{escape_html(text)}</a>)
|
|
52
105
|
end
|
|
53
106
|
|
|
54
|
-
def render_list(docs,
|
|
55
|
-
|
|
107
|
+
def render_list(docs, matches)
|
|
108
|
+
paths = matches.map { |category| category["path"] }
|
|
109
|
+
cat_docs = sorted_category_docs(docs, paths)
|
|
56
110
|
limit = @options["limit"]&.to_i
|
|
57
111
|
cat_docs = cat_docs.first(limit) if limit&.positive?
|
|
58
112
|
|
|
113
|
+
category = paths.join(",")
|
|
59
114
|
out = %(<ul class="doc-category-list" data-category="#{escape_html(category)}">\n)
|
|
60
115
|
out << list_items(cat_docs)
|
|
61
116
|
out << "</ul>\n"
|
|
62
117
|
end
|
|
63
118
|
|
|
64
|
-
def sorted_category_docs(docs,
|
|
65
|
-
docs.select
|
|
66
|
-
|
|
119
|
+
def sorted_category_docs(docs, paths)
|
|
120
|
+
matches = docs.select do |doc|
|
|
121
|
+
paths.include?((doc.data["category_path"] || doc.data["category"]).to_s)
|
|
122
|
+
end
|
|
123
|
+
matches.sort_by { |doc| doc.data["date"] || Time.at(0) }.reverse
|
|
67
124
|
end
|
|
68
125
|
|
|
69
126
|
def list_items(cat_docs)
|
|
@@ -83,6 +140,8 @@ module Jekyll
|
|
|
83
140
|
end
|
|
84
141
|
|
|
85
142
|
def extract_category(text)
|
|
143
|
+
return [nil, text] if text.strip.match?(/\Apath\s*:/)
|
|
144
|
+
|
|
86
145
|
quoted = text.match(/\A["']([^"']+)["']/)
|
|
87
146
|
return [quoted[1], text[quoted.end(0)..]] if quoted
|
|
88
147
|
|
|
@@ -2,9 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
module Jekyll
|
|
4
4
|
module Documents
|
|
5
|
+
module DocumentResolver
|
|
6
|
+
include ResolutionReporter
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def find_document(docs, query, options, site)
|
|
11
|
+
return find_document_by_path(docs, options["path"], site) if options["path"]
|
|
12
|
+
|
|
13
|
+
normalized = query.to_s.strip.downcase
|
|
14
|
+
return nil if normalized.empty?
|
|
15
|
+
|
|
16
|
+
exact = docs.select { |doc| document_values(doc).include?(normalized) }
|
|
17
|
+
matches = if exact.empty?
|
|
18
|
+
docs.select do |doc|
|
|
19
|
+
document_values(doc).any? { |value| value.include?(normalized) }
|
|
20
|
+
end
|
|
21
|
+
else
|
|
22
|
+
exact
|
|
23
|
+
end
|
|
24
|
+
return matches.first if matches.one?
|
|
25
|
+
return nil if matches.empty?
|
|
26
|
+
|
|
27
|
+
paths = matches.map { |doc| doc.data["source_path"] || doc.path }.sort.join(", ")
|
|
28
|
+
message = "Ambiguous doc_link #{query.inspect}; matches: #{paths}. Rendering nothing."
|
|
29
|
+
report_resolution_issue(site, message)
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_document_by_path(docs, path, site)
|
|
34
|
+
normalized = normalize_identifier_path(path)
|
|
35
|
+
matches = docs.select { |doc| doc.data["source_path"].to_s == normalized }
|
|
36
|
+
return matches.first if matches.one?
|
|
37
|
+
|
|
38
|
+
issue = matches.empty? ? "Unknown" : "Ambiguous"
|
|
39
|
+
candidates = matches.map { |doc| doc.data["source_path"] }.sort.join(", ")
|
|
40
|
+
detail = candidates.empty? ? normalized : candidates
|
|
41
|
+
report_resolution_issue(site, "#{issue} doc_link path #{path.inspect}: #{detail}")
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def normalize_identifier_path(path)
|
|
46
|
+
path.to_s.strip.tr("\\", "/").squeeze("/").delete_prefix("./").delete_prefix("/")
|
|
47
|
+
.delete_suffix("/")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def document_values(doc)
|
|
51
|
+
[doc.data["title"], doc.data["slug"]].map { |value| value.to_s.downcase }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
5
55
|
class DocLinkTag < Liquid::Tag
|
|
6
56
|
public_class_method :new
|
|
7
57
|
|
|
58
|
+
include DocumentResolver
|
|
8
59
|
include Jekyll::Filters::URLFilters
|
|
9
60
|
|
|
10
61
|
SIZE_UNITS = %w[B KB MB GB].freeze
|
|
@@ -18,7 +69,7 @@ module Jekyll
|
|
|
18
69
|
docs = documents_from(context)
|
|
19
70
|
return "" if docs.empty?
|
|
20
71
|
|
|
21
|
-
match = find_document(docs, @query)
|
|
72
|
+
match = find_document(docs, @query, @options, context.registers[:site])
|
|
22
73
|
return "" unless match
|
|
23
74
|
|
|
24
75
|
@context = context
|
|
@@ -35,20 +86,6 @@ module Jekyll
|
|
|
35
86
|
site.collections["documents"]&.docs || []
|
|
36
87
|
end
|
|
37
88
|
|
|
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
89
|
def build_link(doc, url, text)
|
|
53
90
|
inner = +""
|
|
54
91
|
inner << icon_html(doc) if @options["icon"] != "false"
|
|
@@ -99,6 +136,8 @@ module Jekyll
|
|
|
99
136
|
end
|
|
100
137
|
|
|
101
138
|
def extract_query(text)
|
|
139
|
+
return [nil, text] if text.strip.match?(/\Apath\s*:/)
|
|
140
|
+
|
|
102
141
|
quoted = text.match(/\A["']([^"']+)["']/)
|
|
103
142
|
return [quoted[1], text[quoted.end(0)..]] if quoted
|
|
104
143
|
|