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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be2955da22172cc24f7aed4e3dca0ac968bf131040a36db5fe2ba048a64bca90
4
- data.tar.gz: 2b3d0544fa7c86739e2d83cd9228ee24c64f6bc4ff7691211aefbc7f3533f837
3
+ metadata.gz: 34e9456980c65a0eab294a17100d6de0bcf522bc3652afcadc3c2521c8c9d48b
4
+ data.tar.gz: be4ff590ed8c3e251fb7c56a1e57a33e2650d60a63c44113bb2a4c48402fe906
5
5
  SHA512:
6
- metadata.gz: 1f2c7c21a5cdd8c5e996379467b2ceb38d69e368aa9f8bdae882a657366911fb1262f66a7c8ff549d848b1304fd90a8e56e9f37efe1b856f572fab550899d3d5
7
- data.tar.gz: fce8b514c14788e75405a026dcd6e7eabf25955a12ac16445380f995728cf6f2c65099ab42af18c51c8fb2c1361f3f3bb9cdfa1845a3642e4495e532640a7d16
6
+ metadata.gz: 4c456c2e5ff0b55a6d51bc777770bed926b6cfa4c8a1393fda110f5d0a8d3e0e49af5c5399e1a0bdf2f4c8f5632e19ec8490975722842a5657b9f6e737c4412a
7
+ data.tar.gz: 5ff47c4204e58ad1e49bd48cd146d4f51ccf44254f1244530b0fd39f8fa2b5817531cfa293409a3f6fbad7063bc3425599e2c1bf37d094b5dbb45bba5cc13dc8
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
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
- 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
+ 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
- DEFAULTS.merge(cfg)
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
- ext = File.extname(path).downcase
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
- rel_path = path.delete_prefix("#{site.source}/")
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
- category = infer_category_from(rel_path)
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(basename, category),
68
+ source_stub_for(source_path),
70
69
  site: site,
71
70
  collection: collection
72
71
  )
73
-
74
- file_info = { title: title, date: date, category: category,
75
- rel_path: rel_path, ext: ext, file_type: file_type,
76
- icon_set: icon_set, slug: slug, path: path }
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
- category = remap_category(info[:category])
209
- data["layout"] = @config["layout"]
210
- data["title"] = info[:title]
211
- data["date"] = info[:date] ? info[:date].to_time : File.mtime(info[:path])
212
- data["category"] = category
213
- data["categories"] = [category] if category
214
- data["file_url"] = "/#{info[:rel_path]}"
215
- data["extension"] = info[:ext]
216
- data["file_type"] = info[:file_type]
217
- data["icon_set"] = info[:icon_set]
218
- icon = FileTypeIcons.icon_for(info[:file_type], info[:icon_set])
219
- data["icon_url"] = icon
220
- data["file_size"] = File.size(info[:path])
221
- data["slug"] = info[:slug]
222
- data["permalink"] = @config["permalink"]
223
- .gsub(":category", category.to_s)
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 basename [String] the file basename
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(basename, category)
243
- File.join("_documents", "#{category}-#{basename}.md")
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
- category_dir = File.dirname(rel_path).sub(@config["root"].to_s, "")
253
- category_dir.split("/").reject(&:empty?).last || "uncategorized"
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 remapped category (lowercased)
259
+ # @return [String] the mapped display category or lowercased directory name
259
260
  def remap_category(cat)
260
261
  map = @config["category_map"] || {}
261
- (map[cat] || cat).to_s.downcase
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
- slug = basename.sub(/^\d{4}-\d{2}-\d{2}_/, "")
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? ? "untitled" : slug
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
- category = resolve_category(@category, categories)
21
- return "" unless category
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, category) if @options["list"] == "true"
78
+ return render_list(docs, matches) if @options["list"] == "true"
25
79
 
26
- render_link(category)
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.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) }
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(category)
49
- url = relative_url("/documents/#{category}/")
50
- text = @options["text"] || category
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, category)
55
- cat_docs = sorted_category_docs(docs, category)
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, category)
65
- docs.select { |doc| doc.data["category"].to_s == category }
66
- .sort_by { |doc| doc.data["date"] || Time.at(0) }.reverse
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Documents
5
- VERSION = "0.6.1"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
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.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup