jekyll-client-search 0.1.0 → 0.2.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: b07a5519cf45012ce0d198cedfe93e1e93e939ed9d9221386af87c10854f0cf4
4
- data.tar.gz: 630a3dd0f345e4e263c8228ac3a565610b435723860dac1125e86bf780d072b1
3
+ metadata.gz: 770d678df814030f4576214a39a7faca03f14d889cc284c5976486bc7c22f331
4
+ data.tar.gz: 2eaf824353f2b981ec1c538c3a84b938198960d1092110b6efceb181311ce2ea
5
5
  SHA512:
6
- metadata.gz: 0d53f70fa2e1f37facfebe56ebf3384f3d5c376672e08f93cc5a3a761b03a294843b0cf236cbda9ffa7a7ace87261683a525f3a5e12e9da61592946cc878024a
7
- data.tar.gz: 458dc19c9c0ad2b6c017057694b1af2103d918f16584205ec7c144a5e696fcb0d5609508fa414a7cb91facccf39a75dcd0c1c3e8c8a8e67bac8d1a09665419b5
6
+ metadata.gz: 44e285f7238df3e68523839fb80bdc3f67c25883e3311753b8b67d460a160dcb7e1b9c3c23c9066e04dc57b7d2e951924ebfc55ba205b4913e882b764e0fbff3
7
+ data.tar.gz: e9b48c426a74311b235b33eda93d119ba3864be6d64175640de6971521461226baf5f8c5e88e89a47889beea28feae22833d3ec49a8cf461064e8e7101fab1fc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.2.0 — 2026-08-27
6
+
7
+ ### Added
8
+ - Generic `passthrough_fields` config option: external plugins declare which document `data` fields to forward into the search index (e.g. `file_type`, `icon_url`, `icon_set` from jekyll-documents)
9
+ - Field renaming in `passthrough_fields`: entries can be strings (same name) or hashes (`{source => target}`) for integration with other search conventions
10
+ - `icon_field` config option (default: `"icon_url"`): tells the browser runtime which field to render as an `<img>` icon before the result title; set to `null` to disable
11
+ - Search index now includes `source` (collection label: `posts`, `pages`, etc.) — always present, set by the generator
12
+ - Result `<article>` elements carry `data-source`, `data-categories`, `data-tags`, plus `data-*` for all passthrough fields (snake_case → data-kebab-case) for CSS-based result customization
13
+
14
+ ### Fixed
15
+ - Allow whitespace in script/style end tags in `DocumentBuilder#clean` (`</script\s*>` instead of `</script>`) — CodeQL: bad HTML filtering regexp
16
+ - Fix polynomial ReDoS in `SearchTag` and `RelatedTag` syntax regexes — strip markup before matching, remove leading/trailing `\s*` patterns that caused O(n²) backtracking (CodeQL: polynomial regex on uncontrolled data)
17
+
18
+ ### Changed
19
+ - Simplified release workflow to tag-push trigger (`push: tags: v*`) — no manual `gh release create` needed
20
+ - Switched to RubyGems trusted publishing (`rubygems/release-gem@v1` with OIDC)
21
+ - Centralized version in `version.rb` as single source of truth — removed `version` field from `package.json`
22
+ - Added `rake version:bump` and `rake version:check_changelog` tasks
23
+ - Added CHANGELOG gate to release workflow (fails if entry missing for the version)
24
+ - Added `npm audit` and `npm outdated` (non-blocking) to CI
25
+ - Replaced pre-commit hook with rubocop-only (fast); added pre-push hook (rubocop + rspec)
26
+ - Dropped reek from quality stack (KISS — marginal value on a small gem)
27
+ - Aligned AGENTS.md to the open convention; added CLAUDE.md and .windsurfrules pointer files
28
+
3
29
  ## 0.1.0 — 2026-08-25
4
30
 
5
31
  - Add a pluggable search engine architecture with a base runtime and adapters.
data/README.developer.md CHANGED
@@ -20,6 +20,57 @@ Both test suites pass with only the committed Wikipedia fixtures and the
20
20
  committed baseline JSON artifacts. No network access or external services
21
21
  are required for the default test run.
22
22
 
23
+ ## Quality checks and git hooks
24
+
25
+ The project uses a focused quality stack: RuboCop (style), bundler-audit
26
+ (security), RSpec (Ruby tests), and `npm test` (JavaScript tests). This
27
+ covers the actionable ground without the maintenance burden of additional
28
+ code-smell tools on a small focused gem.
29
+
30
+ ### Rake tasks
31
+
32
+ | Task | What it runs |
33
+ | --- | --- |
34
+ | `rake` (default) | `rake quality` — all checks below |
35
+ | `rake quality` | rubocop + bundler-audit + rspec + npm test |
36
+ | `rake quick` | rubocop + rspec (fast pre-push subset) |
37
+ | `rake rubocop` | RuboCop style check |
38
+ | `rake rubocop_fix` | RuboCop auto-fix |
39
+ | `rake bundler_audit` | `bundle-audit check --update` security scan |
40
+ | `rake spec` | RSpec test suite |
41
+ | `rake npm_test` | JavaScript test suite (`npm test`) |
42
+ | `rake ci` | rspec + rubocop + syntax checks + npm test + gem build |
43
+
44
+ ### Git hooks
45
+
46
+ Git hooks are not committed to the repository. Install them locally after
47
+ cloning:
48
+
49
+ ```bash
50
+ bin/install-hooks.sh
51
+ ```
52
+
53
+ This installs two hooks:
54
+
55
+ | Hook | What it runs | When |
56
+ | --- | --- | --- |
57
+ | `pre-commit` | `rubocop` only (~2s) | Before each commit |
58
+ | `pre-push` | `rubocop + rspec` (~15s) | Before each push |
59
+
60
+ The pre-commit hook is intentionally fast (style only) to avoid bypassing
61
+ with `--no-verify`. The full test suite runs on pre-push and in CI. Skip
62
+ either with `git commit --no-verify` or `git push --no-verify`.
63
+
64
+ ### CI checks
65
+
66
+ The [CI workflow](.github/workflows/ci.yml) runs on every push and pull
67
+ request across Ruby 3.2/3.3/3.4 and Node 22/24:
68
+
69
+ - `bundle exec rake ci` (rspec, rubocop, syntax checks, npm test, gem build)
70
+ - `bundle exec bundle-audit check --update` (Ruby dependency security)
71
+ - `npm audit --audit-level=high` (JavaScript dependency security)
72
+ - `npm outdated` (non-blocking — warns about outdated npm packages)
73
+
23
74
  ## Build-time related articles
24
75
 
25
76
  The optional related-analysis pass writes `search-relations.json` separately
@@ -202,3 +253,152 @@ bundle exec rspec --tag ollama_integration
202
253
  The CI pipeline runs `bundle exec rspec` and `npm test` without arXiv
203
254
  fixtures or Ollama. The committed baseline JSON and semantic embeddings
204
255
  ensure all tests pass in CI without external dependencies.
256
+
257
+ ## Releasing
258
+
259
+ Releases are published to RubyGems.org via the
260
+ [`release.yml`](.github/workflows/release.yml) GitHub Actions workflow,
261
+ which triggers automatically when a GitHub Release is published. The
262
+ workflow uses RubyGems trusted publishing (OIDC) — no API key is stored
263
+ in the repository.
264
+
265
+ ### Prerequisites (one-time setup)
266
+
267
+ Trusted publishing must be configured on rubygems.org for the gem before
268
+ the first release:
269
+
270
+ 1. Log into <https://rubygems.org> and open the gem's page.
271
+ 2. Go to **Settings → Trusted Publishers → Add trusted publisher**.
272
+ 3. Enter:
273
+ - **Repository**: `gundestrup/jekyll-client-search`
274
+ - **Workflow filename**: `release.yml`
275
+ - **Environment**: `release`
276
+
277
+ If the gem has never been published before, rubygems.org may require a
278
+ one-time manual `gem push` with an API key to create the gem name before
279
+ a trusted publisher can be attached. After that, all subsequent releases
280
+ use trusted publishing automatically.
281
+
282
+ ### Version bumping
283
+
284
+ The version lives in [`lib/jekyll/client_search/version.rb`](lib/jekyll/client_search/version.rb)
285
+ and follows [Semantic Versioning](https://semver.org/). The gemspec reads
286
+ from this file — it is the **single source of truth**. No other file
287
+ stores the version (`package.json` is `private: true` and has no version
288
+ field). Use the rake task to bump it:
289
+
290
+ ```bash
291
+ bundle exec rake "version:bump[patch]" # 0.1.0 -> 0.1.1 (bug fixes)
292
+ bundle exec rake "version:bump[minor]" # 0.1.0 -> 0.2.0 (new features, backwards compatible)
293
+ bundle exec rake "version:bump[major]" # 0.1.0 -> 1.0.0 (incompatible API changes)
294
+ ```
295
+
296
+ The rake task only edits `version.rb`. It does not commit, tag, or update
297
+ the changelog — those are manual steps in the release checklist below.
298
+
299
+ You can verify that the CHANGELOG has an entry for the current version:
300
+
301
+ ```bash
302
+ bundle exec rake version:check_changelog
303
+ ```
304
+
305
+ This check also runs in the release workflow — the publish will fail if
306
+ `CHANGELOG.md` has no `## X.Y.Z` entry matching the version being released.
307
+
308
+ ### Release checklist
309
+
310
+ 1. **Ensure the working tree is clean** and on `main`:
311
+
312
+ ```bash
313
+ git status
314
+ ```
315
+
316
+ 2. **Ensure all 80 fixture posts are present** (needed for Ollama
317
+ integration tests and baseline regeneration):
318
+
319
+ ```bash
320
+ ruby spec/fixtures/download_arxiv.rb
321
+ ```
322
+
323
+ Requires `pdftotext` (`brew install poppler` on macOS). Skip if the
324
+ arXiv posts are already present and unchanged.
325
+
326
+ 3. **Run the full test suite locally**, including Ollama integration tests:
327
+
328
+ ```bash
329
+ bundle exec rspec
330
+ OLLAMA_INTEGRATION=1 bundle exec rspec spec/ollama_integration_spec.rb
331
+ OLLAMA_INTEGRATION=1 bundle exec rspec spec/llm_injection_spec.rb
332
+ bundle exec rubocop
333
+ npm test
334
+ npm run lint
335
+ gem build jekyll-client-search.gemspec
336
+ ```
337
+
338
+ The Ollama integration tests require a running [Ollama](https://ollama.ai/)
339
+ server with the `embeddinggemma:300m` model pulled (`ollama pull
340
+ embeddinggemma:300m`). Do not publish a release until the Ollama
341
+ integration tests pass locally — they are skipped by default and will
342
+ not block CI, so they must be run manually before each release.
343
+
344
+ 4. **Bump the version** (see above) and update the `## Unreleased` or new
345
+ `## X.Y.Z — YYYY-MM-DD` section at the top of
346
+ [`CHANGELOG.md`](CHANGELOG.md) with a user-facing summary of changes.
347
+
348
+ 5. **Commit the version bump and changelog**:
349
+
350
+ ```bash
351
+ git add lib/jekyll/client_search/version.rb CHANGELOG.md
352
+ git commit -m "Release X.Y.Z: <short summary>"
353
+ ```
354
+
355
+ 6. **Tag the release** with an annotated tag:
356
+
357
+ ```bash
358
+ git tag -a vX.Y.Z -m "Release X.Y.Z
359
+
360
+ <one-line summary of notable changes>"
361
+ ```
362
+
363
+ 7. **Push `main` and the tag** to GitHub — pushing the tag triggers the
364
+ release workflow automatically:
365
+
366
+ ```bash
367
+ git push origin main
368
+ git push origin vX.Y.Z
369
+ ```
370
+
371
+ No manual `gh release create` is needed. The workflow builds the gem,
372
+ attaches it to a GitHub release, and publishes to RubyGems.
373
+
374
+ 8. **Watch the workflow** and verify the gem appears on RubyGems:
375
+
376
+ ```bash
377
+ gh run watch --workflow=release.yml
378
+ gem list jekyll-client-search --remote --exact
379
+ ```
380
+
381
+ 9. **Verify the integration site** builds cleanly against the published
382
+ gem (update the path dependency to the released version in
383
+ `../gundestrup.dk` and run `bundle exec jekyll build`).
384
+
385
+ ### What the release workflow does
386
+
387
+ The [`release.yml`](.github/workflows/release.yml) workflow, triggered by
388
+ pushing a `v*` tag:
389
+
390
+ 1. Checks out the repository at the release tag (`persist-credentials: false`).
391
+ 2. Sets up Ruby 3.4.10.
392
+ 3. Verifies the tag name matches `v<gem version>` (fails on mismatch).
393
+ 4. Verifies `CHANGELOG.md` has a `## X.Y.Z` entry for the version
394
+ (`rake version:check_changelog` — fails if missing).
395
+ 5. Builds the gem (`gem build`).
396
+ 6. Creates a GitHub Release and attaches the `.gem` file as a downloadable
397
+ asset (`softprops/action-gh-release@v2`).
398
+ 7. Publishes the gem to RubyGems.org using trusted publishing
399
+ (`rubygems/release-gem@v1` with OIDC).
400
+
401
+ CI already validates tests on every push — the release workflow does not
402
+ re-run the test suite. It only builds and publishes. The workflow runs in
403
+ the `release` environment, which must match the environment name
404
+ configured on the trusted publisher.
data/README.md CHANGED
@@ -27,6 +27,7 @@ and vector similarity.
27
27
  - [The search form](#the-search-form)
28
28
  - [Rake tasks](#rake-tasks)
29
29
  - [Configuration reference](#configuration-reference)
30
+ - [Indexing custom collections](#indexing-custom-collections)
30
31
  - [Architecture](#architecture)
31
32
  - [Embeddings and incremental indexing](#embeddings-and-incremental-indexing)
32
33
  - [Related articles reference](#related-articles-reference)
@@ -215,6 +216,89 @@ client_search:
215
216
  For manual `<script>` setup (engine-specific HTML), see
216
217
  [Browser usage reference](#browser-usage-reference).
217
218
 
219
+ ### Customizing search results with CSS
220
+
221
+ Each search result is rendered as an `<article>` with data attributes that
222
+ expose the document's metadata for CSS-based styling:
223
+
224
+ | Attribute | Value | Source |
225
+ | --- | --- | --- |
226
+ | `data-source` | Collection label (`posts`, `pages`, `documents`, …) | Generator (always present) |
227
+ | `data-categories` | Space-separated categories | Front matter |
228
+ | `data-tags` | Space-separated tags | Front matter |
229
+ | `data-*` | Any passthrough field (e.g. `data-file-type`, `data-icon-set`) | Config-driven |
230
+
231
+ **Passthrough fields:** External plugins can expose their data in search
232
+ results by listing field names in `passthrough_fields`. Each listed field is
233
+ forwarded from the document's front matter / data into the search index and
234
+ rendered as a `data-*` attribute on the result `<article>` (field names are
235
+ converted from `snake_case` to `data-kebab-case`).
236
+
237
+ **Field renaming:** Entries can be strings (same name) or hashes (rename
238
+ `{source => target}`) for integration with other search conventions:
239
+
240
+ ```yaml
241
+ client_search:
242
+ passthrough_fields:
243
+ - file_type: doctype # index as "doctype" instead of "file_type"
244
+ - icon_url: thumbnail # index as "thumbnail" instead of "icon_url"
245
+ - icon_set # stays as "icon_set"
246
+ icon_field: thumbnail # must match the target name
247
+ ```
248
+
249
+ **Auto-injection:** When jekyll-documents is installed and `documents` is
250
+ in the search collections, `file_type`, `icon_url`, and `icon_set` are
251
+ auto-injected into `passthrough_fields` — no manual config needed.
252
+
253
+ **Icon rendering:** When the configured `icon_field` (default: `"icon_url"`)
254
+ is present in a result, an `<img class="client-search-result-icon">` is
255
+ rendered before the title. Set `icon_field: null` to disable.
256
+
257
+ **Default icon sizing:** Icons are sized to `1em` (matching the heading's
258
+ font size) with `vertical-align: middle` and a small right margin — they
259
+ auto-scale with the text. The inline styles are self-contained (no CSS
260
+ dependency), so they work with any framework (Bulma, Bootstrap, Tailwind).
261
+
262
+ Override with CSS:
263
+
264
+ ```css
265
+ /* Make icons larger */
266
+ .client-search-result-icon { width: 1.5em; height: 1.5em; }
267
+
268
+ /* Fixed pixel sizes (if jekyll-documents' documents.css is included) */
269
+ .client-search-result-icon.icon-x2 { width: 32px; height: 32px; }
270
+
271
+ /* Theme-aware sizing using jekyll-documents' icon_set */
272
+ .client-search-result[data-icon-set="color"] .client-search-result-icon { width: 1.5em; }
273
+ .client-search-result[data-icon-set="ultra-minimal"] .client-search-result-icon { width: 1em; }
274
+ ```
275
+
276
+ **CSS examples:**
277
+
278
+ ```css
279
+ /* Show a different border color for posts vs pages */
280
+ .client-search-result[data-source="posts"] { border-left: 3px solid #3273dc; }
281
+ .client-search-result[data-source="pages"] { border-left: 3px solid #48c774; }
282
+
283
+ /* Add a badge showing the file type via CSS (no icon_url needed) */
284
+ .client-search-result[data-file-type="pdf"]::before {
285
+ content: "PDF";
286
+ background: #ff0000;
287
+ color: white;
288
+ padding: 0 0.3em;
289
+ font-size: 0.7em;
290
+ margin-right: 0.3em;
291
+ }
292
+
293
+ /* Show categories as tags */
294
+ .client-search-result[data-categories~="travel"] .title::after {
295
+ content: " ✈";
296
+ }
297
+ ```
298
+
299
+ This enables per-source icons, file-type badges, category indicators, and
300
+ tag-based styling — all through CSS, with no JavaScript changes needed.
301
+
218
302
  ## Rake tasks
219
303
 
220
304
  The gem ships reference layouts and includes that you can copy into your
@@ -343,6 +427,47 @@ Each document in the index contains:
343
427
  }
344
428
  ```
345
429
 
430
+ ## Indexing custom collections
431
+
432
+ By default only `posts` are indexed. Add any Jekyll collection to the
433
+ `collections` list to include it in the search index:
434
+
435
+ ```yaml
436
+ client_search:
437
+ collections:
438
+ - posts
439
+ - documents
440
+ ```
441
+
442
+ Each document in the index contains `id`, `title`, `url`, `excerpt`,
443
+ `content`, `categories`, `tags`, and normalized date fields. Collections
444
+ from other plugins work as long as their documents expose these fields.
445
+
446
+ ### Integration with jekyll-documents
447
+
448
+ [jekyll-documents](https://github.com/gundestrup/jekyll-documents) turns
449
+ files in `assets/documents/` into a browsable `documents` collection with
450
+ titles, categories, and searchable content baked in at build time. To
451
+ include uploaded documents in search results alongside posts:
452
+
453
+ ```yaml
454
+ # _config.yml
455
+ plugins:
456
+ - jekyll-documents
457
+ - jekyll-client-search
458
+
459
+ client_search:
460
+ collections:
461
+ - posts
462
+ - documents
463
+ related:
464
+ enabled: true
465
+ ```
466
+
467
+ No additional configuration is needed — `jekyll-documents` bakes
468
+ `categories` (plural array) and searchable `content` into each document
469
+ so the `DocumentBuilder` can index them directly.
470
+
346
471
  ## Architecture
347
472
 
348
473
  The plugin uses a base runtime + adapter architecture where the base owns
@@ -58,6 +58,12 @@
58
58
  return;
59
59
  }
60
60
 
61
+ var coreFields = [
62
+ "id", "title", "url", "excerpt", "content",
63
+ "categories", "tags", "categoriesText", "tagsText",
64
+ "date", "date_timestamp", "embedding"
65
+ ];
66
+
61
67
  function normalize(entry) {
62
68
  var id = entry.id || entry.url;
63
69
  if (!id) {
@@ -78,9 +84,20 @@
78
84
  categoriesText: categories.join(" "),
79
85
  tagsText: tags.join(" ")
80
86
  };
87
+ if (entry.source) {
88
+ normalized.source = entry.source;
89
+ }
81
90
  if (Array.isArray(entry.embedding) && entry.embedding.length > 0) {
82
91
  normalized.embedding = entry.embedding;
83
92
  }
93
+ Object.keys(entry).forEach(function (key) {
94
+ if (coreFields.indexOf(key) === -1 && key !== "source" && !(key in normalized)) {
95
+ var value = entry[key];
96
+ if (value !== null && value !== undefined && value !== "") {
97
+ normalized[key] = value;
98
+ }
99
+ }
100
+ });
84
101
  return normalized;
85
102
  }
86
103
 
@@ -143,17 +160,52 @@
143
160
  var url = safeUrl(entry.url);
144
161
 
145
162
  article.className = "box client-search-result";
163
+ if (entry.source) {
164
+ article.dataset.source = entry.source;
165
+ }
166
+ if (entry.categories.length) {
167
+ article.dataset.categories = entry.categories.join(" ");
168
+ }
169
+ if (entry.tags.length) {
170
+ article.dataset.tags = entry.tags.join(" ");
171
+ }
172
+ Object.keys(entry).forEach(function (key) {
173
+ if (coreFields.indexOf(key) !== -1 || key === "source") {
174
+ return;
175
+ }
176
+ var value = entry[key];
177
+ if (typeof value === "string" || typeof value === "number") {
178
+ article.dataset[toCamelCase(key)] = String(value);
179
+ }
180
+ });
146
181
  heading.className = "title is-4";
147
182
  titleLink.href = url;
148
183
  titleLink.textContent = entry.title;
184
+ heading.appendChild(titleLink);
185
+ var iconField = options.iconField;
186
+ if (iconField && entry[iconField]) {
187
+ var icon = document.createElement("img");
188
+ icon.className = "client-search-result-icon";
189
+ icon.src = safeUrl(entry[iconField]);
190
+ icon.alt = entry.file_type || entry.source || "";
191
+ icon.loading = "lazy";
192
+ icon.style.width = "1em";
193
+ icon.style.height = "1em";
194
+ icon.style.verticalAlign = "middle";
195
+ icon.style.marginRight = "0.3em";
196
+ heading.insertBefore(icon, titleLink);
197
+ }
149
198
  excerpt.textContent = entry.excerpt;
150
199
  readMore.href = url;
151
200
  readMore.textContent = "Read more";
152
- heading.appendChild(titleLink);
153
201
  article.append(heading, excerpt, readMore);
154
202
  return article;
155
203
  }
156
204
 
205
+ function toCamelCase(key) {
206
+ return key.replace(/_([a-z])/g, function (_, char) { return char.toUpperCase(); });
207
+ }
208
+
157
209
  function sortMatches(matches) {
158
210
  var sortControl = document.querySelector(options.sortControl);
159
211
  var sortOrder = sortControl ? sortControl.value : options.sort;
@@ -23,6 +23,8 @@ module Jekyll
23
23
  "collections" => ["posts"],
24
24
  "include_pages" => false,
25
25
  "copy_runtime" => true,
26
+ "passthrough_fields" => [],
27
+ "icon_field" => "icon_url",
26
28
  "embedding" => {
27
29
  "enabled" => false,
28
30
  "model" => "embeddinggemma:300m",
@@ -111,11 +113,7 @@ module Jekyll
111
113
  end
112
114
 
113
115
  def collections
114
- Array(@values["collections"])
115
- .compact
116
- .map(&:to_s)
117
- .reject(&:empty?)
118
- .uniq
116
+ Array(@values["collections"]).compact.map(&:to_s).reject(&:empty?).uniq
119
117
  end
120
118
 
121
119
  def include_pages?
@@ -126,6 +124,31 @@ module Jekyll
126
124
  @values["copy_runtime"] != false
127
125
  end
128
126
 
127
+ def passthrough_fields
128
+ Array(@values["passthrough_fields"]).compact.each_with_object([]) do |e, f|
129
+ e.is_a?(Hash) ? add_hash_fields(e, f) : add_string_field(e, f)
130
+ end.uniq
131
+ end
132
+
133
+ def add_hash_fields(hash, fields)
134
+ hash.each do |s, t|
135
+ fields << [s.to_s, t.to_s] unless s.to_s.empty? || t.to_s.empty?
136
+ end
137
+ end
138
+
139
+ def add_string_field(entry, fields)
140
+ fields << [entry.to_s, entry.to_s] unless entry.to_s.empty?
141
+ end
142
+
143
+ def icon_field
144
+ v = @values["icon_field"]
145
+ v.nil? || v == false ? nil : v.to_s
146
+ end
147
+
148
+ def runtime_icon_field
149
+ icon_field if passthrough_fields.map(&:last).include?(icon_field)
150
+ end
151
+
129
152
  private
130
153
 
131
154
  def validate_query_embedder!
@@ -5,11 +5,21 @@ module Jekyll
5
5
  # Normalizes Jekyll documents and pages into the flat hash shape that the
6
6
  # search index JSON emits.
7
7
  class DocumentBuilder
8
- def from_document(document)
8
+ def from_document(document, source: nil, passthrough_fields: [])
9
9
  url = document.url.to_s
10
10
  return if url.empty?
11
11
 
12
12
  data = document.data
13
+ core_fields(document, data, url)
14
+ .compact
15
+ .merge("source" => source)
16
+ .merge(passthrough(data, passthrough_fields))
17
+ .compact
18
+ end
19
+
20
+ private
21
+
22
+ def core_fields(document, data, url)
13
23
  {
14
24
  "id" => url,
15
25
  "title" => clean(data["title"] || "Untitled"),
@@ -20,10 +30,18 @@ module Jekyll
20
30
  "tags" => normalize_list(data["tags"]),
21
31
  "date" => normalized_date(document),
22
32
  "date_timestamp" => normalized_timestamp(document)
23
- }.compact
33
+ }
24
34
  end
25
35
 
26
- private
36
+ def passthrough(data, fields)
37
+ fields.each_with_object({}) do |(source, target), result|
38
+ value = data[source]
39
+ next if value.nil?
40
+ next if value.is_a?(String) && value.empty?
41
+
42
+ result[target] = value
43
+ end
44
+ end
27
45
 
28
46
  def excerpt_for(document)
29
47
  document.excerpt if document.respond_to?(:excerpt)
@@ -49,8 +67,8 @@ module Jekyll
49
67
 
50
68
  def clean(value)
51
69
  cleaned = value.to_s
52
- .gsub(%r{<script\b[^>]*>.*?</script>}mi, " ")
53
- .gsub(%r{<style\b[^>]*>.*?</style>}mi, " ")
70
+ .gsub(%r{<script\b[^>]*>.*?</script\s*>}mi, " ")
71
+ .gsub(%r{<style\b[^>]*>.*?</style\s*>}mi, " ")
54
72
  .gsub(/\{%.*?%\}/m, " ")
55
73
  .gsub(/\{\{.*?\}\}/m, " ")
56
74
  .gsub(/!\[[^\]]*\]\([^)]*\)/, " ")
@@ -90,19 +90,23 @@ module Jekyll
90
90
  end
91
91
 
92
92
  def collection_documents(site, configuration, builder)
93
+ fields = configuration.passthrough_fields
93
94
  configuration.collections.flat_map do |label|
94
95
  collection = label == "posts" ? site.posts : site.collections[label]
95
96
  next [] unless collection
96
97
 
97
- collection.docs.filter_map { |document| builder.from_document(document) }
98
+ collection.docs.filter_map do |document|
99
+ builder.from_document(document, source: label, passthrough_fields: fields)
100
+ end
98
101
  end
99
102
  end
100
103
 
101
104
  def page_documents(site, builder, configuration)
105
+ fields = configuration.passthrough_fields
102
106
  site.pages
103
107
  .reject { |page| page.url == "/#{configuration.output}" }
104
108
  .select { |page| page.data["title"] }
105
- .filter_map { |page| builder.from_document(page) }
109
+ .filter_map { |page| builder.from_document(page, source: "pages", passthrough_fields: fields) }
106
110
  end
107
111
 
108
112
  def add_runtime_asset(site, configuration)
@@ -12,11 +12,11 @@ module Jekyll
12
12
  # When +related.enabled+ is false the tag renders nothing, so it is safe
13
13
  # to leave in a layout even when the feature is off.
14
14
  class RelatedTag < Liquid::Tag
15
- SYNTAX = /\A\s*(sort:(\w+))?\s*(no_scripts)?\s*\z/
15
+ SYNTAX = /\A(sort:(\w+))?\s*(no_scripts)?\z/
16
16
 
17
17
  def initialize(tag_name, markup, tokens)
18
18
  super
19
- @markup = markup.to_s
19
+ @markup = markup.to_s.strip
20
20
  unless (match = @markup.match(SYNTAX))
21
21
  raise Liquid::SyntaxError,
22
22
  "related_articles: invalid syntax. Use {% related_articles %}, " \
@@ -12,6 +12,7 @@ module Jekyll
12
12
  "liveSearch" => configuration.live_search_config
13
13
  }
14
14
  defaults["relatedUrl"] = index_url(site, configuration.related_output) if configuration.related_enabled?
15
+ defaults["iconField"] = configuration.runtime_icon_field if configuration.runtime_icon_field
15
16
  json = JSON.generate(defaults)
16
17
  self.content = "window.clientSearchConfig = (function (generated, existing) {" \
17
18
  "var liveSearch = Object.assign({}, generated.liveSearch, existing.liveSearch || {});" \
@@ -12,11 +12,11 @@ module Jekyll
12
12
  #
13
13
  # When client_search is disabled the tag renders nothing.
14
14
  class SearchTag < Liquid::Tag
15
- SYNTAX = /\A\s*(scripts_only|no_scripts)?\s*\z/
15
+ SYNTAX = /\A(scripts_only|no_scripts)?\z/
16
16
 
17
17
  def initialize(tag_name, markup, tokens)
18
18
  super
19
- @markup = markup.to_s
19
+ @markup = markup.to_s.strip
20
20
  unless (match = @markup.match(SYNTAX))
21
21
  raise Liquid::SyntaxError,
22
22
  "search_form: invalid syntax. Use {% search_form %}, " \
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module ClientSearch
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-client-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup