jekyll-client-search 0.3.3 → 0.3.5
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 +64 -0
- data/LICENSE +654 -15
- data/README.developer.md +23 -13
- data/README.md +19 -8
- data/assets/client-search-base.js +2 -2
- data/assets/client-search-dropdown.js +2 -2
- data/assets/client-search-related.js +1 -1
- data/assets/includes/related-articles.html +4 -2
- data/assets/query-embedders/transformers-worker.js +3 -0
- data/lib/jekyll/client_search/dropdown_tag.rb +24 -41
- data/lib/jekyll/client_search/related_tag.rb +23 -17
- data/lib/jekyll/client_search/script_tags.rb +41 -0
- data/lib/jekyll/client_search/search_tag.rb +20 -34
- data/lib/jekyll/client_search/version.rb +1 -1
- data/lib/jekyll/client_search.rb +1 -0
- metadata +3 -2
data/README.developer.md
CHANGED
|
@@ -8,13 +8,12 @@ See [README.md](README.md) for user-facing configuration and usage docs.
|
|
|
8
8
|
## Quick start
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
rbenv install
|
|
12
|
-
rbenv local 3.4.10
|
|
11
|
+
rbenv install # reads .ruby-version
|
|
13
12
|
bundle install
|
|
14
13
|
npm ci
|
|
15
|
-
bundle exec rspec # Ruby tests
|
|
14
|
+
bundle exec rspec # Ruby tests
|
|
16
15
|
OLLAMA_INTEGRATION=1 bundle exec rspec # all tests including Ollama integration
|
|
17
|
-
npm test # JavaScript tests
|
|
16
|
+
npm test # JavaScript tests
|
|
18
17
|
```
|
|
19
18
|
|
|
20
19
|
Both test suites pass with only the committed Wikipedia fixtures and the
|
|
@@ -29,16 +28,22 @@ The project uses a focused quality stack: RuboCop (style), bundler-audit
|
|
|
29
28
|
maintenance burden of additional code-smell tools on a small focused gem.
|
|
30
29
|
|
|
31
30
|
External automated code review services:
|
|
31
|
+
|
|
32
32
|
- [CodeQL](https://codeql.github.com) — GitHub native security scanning
|
|
33
33
|
(Ruby + JavaScript), runs on every push.
|
|
34
34
|
- [Semgrep](https://semgrep.dev) — CI security scan using the Semgrep Pro
|
|
35
|
-
engine with the organization's full policy
|
|
35
|
+
engine with the organization's full policy ruleset. Runs as a CI
|
|
36
36
|
job (`semgrep ci`) and in the pre-commit hook (custom rules only). See
|
|
37
37
|
`.semgrep.yml` for local configuration and usage instructions.
|
|
38
38
|
- [CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search) —
|
|
39
39
|
automated code review (RuboCop, Brakeman, bundler-audit, duplication).
|
|
40
40
|
- [Dependabot](https://docs.github.com/en/code-security/dependabot) — weekly
|
|
41
41
|
GitHub Actions version updates (SHA-pinned actions updated automatically).
|
|
42
|
+
- [Codecov](https://codecov.io/gh/gundestrup/jekyll-client-search) — coverage
|
|
43
|
+
reporting. CI uploads Cobertura XML generated by `simplecov-cobertura` from
|
|
44
|
+
the Ruby 3.4 / Node 24 matrix job; status check thresholds live in
|
|
45
|
+
`codecov.yml`. Uploads authenticate via OIDC (`id-token: write`), with the
|
|
46
|
+
`CODECOV_TOKEN` secret as a fallback.
|
|
42
47
|
|
|
43
48
|
### Semgrep usage
|
|
44
49
|
|
|
@@ -85,8 +90,9 @@ comments. Dependabot updates the SHAs and version comments automatically.
|
|
|
85
90
|
|
|
86
91
|
### Git hooks
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
Hooks are tracked in `bin/hooks/`; `bin/install-hooks.sh` points git at
|
|
94
|
+
them via `core.hooksPath` (no copying — tracked hooks can't drift from
|
|
95
|
+
installed ones). Run once after cloning:
|
|
90
96
|
|
|
91
97
|
```bash
|
|
92
98
|
bin/install-hooks.sh
|
|
@@ -106,11 +112,11 @@ CI. Skip either with `git commit --no-verify` or `git push --no-verify`.
|
|
|
106
112
|
### CI checks
|
|
107
113
|
|
|
108
114
|
The [CI workflow](.github/workflows/ci.yml) runs on every push and pull
|
|
109
|
-
request across Ruby 3.
|
|
115
|
+
request across Ruby 3.3/3.4 and Node 22/24:
|
|
110
116
|
|
|
111
117
|
- `bundle exec rake ci` (rspec, rubocop, syntax checks, npm test, gem build)
|
|
112
118
|
- `bundle exec bundle-audit check --update` (Ruby dependency security)
|
|
113
|
-
- `npm
|
|
119
|
+
- `npm run audit` (JavaScript dependency security gate — see `scripts/npm-audit.mjs`)
|
|
114
120
|
- `npm outdated` (non-blocking — warns about outdated npm packages)
|
|
115
121
|
- `semgrep ci` (security scan with custom rules from `.semgrep.yml`)
|
|
116
122
|
|
|
@@ -163,7 +169,7 @@ test files are `spec/related_analyzer_spec.rb`, `spec/related_tag_spec.rb`,
|
|
|
163
169
|
## What is committed
|
|
164
170
|
|
|
165
171
|
| Artifact | Committed? | Why |
|
|
166
|
-
|
|
172
|
+
| --- | --- | --- |
|
|
167
173
|
| Wikipedia fixture posts (40) | Yes | CC BY-SA 3.0 allows redistribution with attribution |
|
|
168
174
|
| arXiv fixture posts (40) | **No** | Mixed/restrictive licenses — see below |
|
|
169
175
|
| Baseline search-index JSON | Yes | Generated test artifact (indexed data, not raw text) |
|
|
@@ -193,6 +199,7 @@ ruby spec/fixtures/download_arxiv.rb
|
|
|
193
199
|
```
|
|
194
200
|
|
|
195
201
|
This script:
|
|
202
|
+
|
|
196
203
|
- Fetches 40 recent arXiv papers from 5 CS/AI subfields
|
|
197
204
|
- Downloads each PDF and extracts text with `pdftotext`
|
|
198
205
|
- Records the actual per-paper license in each post's frontmatter
|
|
@@ -244,6 +251,7 @@ To run the full integration tests that call a real Ollama server:
|
|
|
244
251
|
```
|
|
245
252
|
|
|
246
253
|
These tests:
|
|
254
|
+
|
|
247
255
|
- Build the full 80-post index with real Ollama embeddings
|
|
248
256
|
- Verify embedding dimensions and validity
|
|
249
257
|
- Test semantic search quality with concept queries
|
|
@@ -272,7 +280,7 @@ ruby spec/fixtures/generate_semantic_gold.rb
|
|
|
272
280
|
## Test categories
|
|
273
281
|
|
|
274
282
|
| Tag | Description | Requires arXiv? | Requires Ollama? |
|
|
275
|
-
|
|
283
|
+
| --- | --- | --- | --- |
|
|
276
284
|
| `:unit` | Ruby unit tests | No | No |
|
|
277
285
|
| `:system` | Ruby system tests (build fixture site) | Partially | No |
|
|
278
286
|
| `:integration` | Ruby integration tests | No | No |
|
|
@@ -298,7 +306,9 @@ always return an asset).
|
|
|
298
306
|
|
|
299
307
|
The coverage report is generated at `coverage/index.html` after each RSpec
|
|
300
308
|
run. Raw data is in `coverage/.resultset.json` (SimpleCov 1.0+ format with
|
|
301
|
-
`lines` and `branches` keys per file).
|
|
309
|
+
`lines` and `branches` keys per file). In CI (`ENV["CI"]`), SimpleCov emits
|
|
310
|
+
Cobertura XML at `coverage/coverage.xml` instead, which is uploaded to
|
|
311
|
+
Codecov by the `codecov/codecov-action` step in `ci.yml`.
|
|
302
312
|
|
|
303
313
|
To check remaining uncovered lines and branches after a run:
|
|
304
314
|
|
|
@@ -492,7 +502,7 @@ The [`release.yml`](.github/workflows/release.yml) workflow, triggered by
|
|
|
492
502
|
pushing a `v*` tag:
|
|
493
503
|
|
|
494
504
|
1. Checks out the repository at the release tag (`persist-credentials: false`).
|
|
495
|
-
2. Sets up Ruby
|
|
505
|
+
2. Sets up Ruby from `.ruby-version`.
|
|
496
506
|
3. Verifies the tag name matches `v<gem version>` (fails on mismatch).
|
|
497
507
|
4. Verifies `CHANGELOG.md` has a `## X.Y.Z` entry for the version
|
|
498
508
|
(`rake version:check_changelog` — fails if missing).
|
data/README.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# jekyll-client-search
|
|
2
2
|
|
|
3
|
+
[](https://codecov.io/gh/gundestrup/jekyll-client-search)
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|

|
|
4
7
|
|
|
5
8
|
[](https://github.com/gundestrup/jekyll-client-search/actions/workflows/ci.yml)
|
|
6
|
-
[](https://rubygems.org/gems/jekyll-client-search)
|
|
10
|
+
[](https://www.ruby-lang.org/)
|
|
7
11
|
[](https://jekyllrb.com/)
|
|
8
12
|
[](LICENSE)
|
|
9
|
-
[](https://deepwiki.com/gundestrup/jekyll-client-search)
|
|
10
14
|
[](#testing-strategy)
|
|
11
15
|
[](https://semgrep.dev)
|
|
12
16
|
[](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search)
|
|
17
|
+
[](https://sonarcloud.io/project/overview?id=gundestrup_jekyll-client-search)
|
|
18
|
+
[](https://codecov.io/gh/gundestrup/jekyll-client-search)
|
|
13
19
|
|
|
14
20
|
A Jekyll plugin that generates a JSON document index for client-side search.
|
|
15
21
|
Supports lexical search ([MiniSearch](https://lucaong.github.io/minisearch/),
|
|
@@ -602,14 +608,19 @@ Jekyll build time using a local [Ollama](https://ollama.ai/) server and the
|
|
|
602
608
|
### Setup
|
|
603
609
|
|
|
604
610
|
1. Install [Ollama](https://ollama.ai/) and pull an embedding model:
|
|
611
|
+
|
|
605
612
|
```bash
|
|
606
613
|
ollama pull embeddinggemma:300m
|
|
607
614
|
```
|
|
615
|
+
|
|
608
616
|
2. Add `ollama-ruby` to your site's Gemfile (optional dependency):
|
|
617
|
+
|
|
609
618
|
```ruby
|
|
610
619
|
gem "ollama-ruby", "~> 1.23"
|
|
611
620
|
```
|
|
621
|
+
|
|
612
622
|
3. Enable embeddings in `_config.yml`:
|
|
623
|
+
|
|
613
624
|
```yaml
|
|
614
625
|
client_search:
|
|
615
626
|
engine: semantic
|
|
@@ -1011,18 +1022,18 @@ Recommended policy:
|
|
|
1011
1022
|
|
|
1012
1023
|
## License
|
|
1013
1024
|
|
|
1025
|
+
Copyright (C) 2026 Svend Gundestrup.
|
|
1026
|
+
|
|
1014
1027
|
This project is licensed under the GNU Affero General Public License v3.0 or
|
|
1015
1028
|
later (`AGPL-3.0-or-later`). See [LICENSE](LICENSE).
|
|
1016
1029
|
|
|
1017
1030
|
## Development
|
|
1018
1031
|
|
|
1019
|
-
Development uses Ruby
|
|
1020
|
-
and newer.
|
|
1021
|
-
Ruby 3.2, 3.3, and 3.4.
|
|
1032
|
+
Development uses the Ruby version pinned in `.ruby-version` (via rbenv),
|
|
1033
|
+
while the gem supports Ruby 3.3 and newer. CI tests Ruby 3.3 and 3.4.
|
|
1022
1034
|
|
|
1023
1035
|
```bash
|
|
1024
|
-
rbenv install
|
|
1025
|
-
rbenv local 3.4.10
|
|
1036
|
+
rbenv install # reads .ruby-version
|
|
1026
1037
|
bundle install
|
|
1027
1038
|
npm ci
|
|
1028
1039
|
bundle exec rake ci
|
|
@@ -1152,7 +1163,7 @@ Additional automated code quality and security services:
|
|
|
1152
1163
|
- [CodeQL](https://codeql.github.com) — GitHub native code scanning for
|
|
1153
1164
|
security vulnerabilities (Ruby + JavaScript).
|
|
1154
1165
|
- [Semgrep](https://semgrep.dev) — CI security scan using the Semgrep Pro
|
|
1155
|
-
engine with the organization's full policy
|
|
1166
|
+
engine with the organization's full policy ruleset. Custom ReDoS
|
|
1156
1167
|
detection rules are defined in `.semgrep.yml`. Findings are triaged on the
|
|
1157
1168
|
[Semgrep AppSec Platform](https://semgrep.dev) dashboard. False positives
|
|
1158
1169
|
and acceptable risks are annotated inline with `# nosemgrep: rule.id`.
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
throw new TypeError("Search index must be an array");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
var documents = data.map(normalize).filter(Boolean);
|
|
69
|
+
var documents = data.map(function (entry) { return normalize(entry); }).filter(Boolean);
|
|
70
70
|
documentsById = new Map(documents.map(function (entry) {
|
|
71
71
|
return [entry.id, entry];
|
|
72
72
|
}));
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
}
|
|
233
233
|
results.replaceChildren.apply(
|
|
234
234
|
results,
|
|
235
|
-
matches.map(resultElement).filter(Boolean)
|
|
235
|
+
matches.map(function (match) { return resultElement(match); }).filter(Boolean)
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
if (!Array.isArray(data)) {
|
|
68
68
|
throw new TypeError("Search index must be an array");
|
|
69
69
|
}
|
|
70
|
-
var documents = data.map(normalize).filter(Boolean);
|
|
70
|
+
var documents = data.map(function (entry) { return normalize(entry); }).filter(Boolean);
|
|
71
71
|
var adapter = window.ClientSearchAdapters &&
|
|
72
72
|
(window.ClientSearchAdapters[config.engine] ||
|
|
73
73
|
window.ClientSearchAdapters.minisearch);
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
this.input = root.querySelector("[data-cs-dropdown-input]");
|
|
178
178
|
this.results = root.querySelector("[data-cs-dropdown-results]");
|
|
179
179
|
this.config = config;
|
|
180
|
-
this.maxItems = parseInt(this.results.dataset.maxItems, 10) || config.maxItems || 5;
|
|
180
|
+
this.maxItems = Number.parseInt(this.results.dataset.maxItems, 10) || config.maxItems || 5;
|
|
181
181
|
this.selectedIndex = -1;
|
|
182
182
|
this.currentItems = [];
|
|
183
183
|
this.debounceTimer = null;
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
if (container.dataset.relatedMax) {
|
|
156
|
-
options.maxItems = parseInt(container.dataset.relatedMax, 10) || 0;
|
|
156
|
+
options.maxItems = Number.parseInt(container.dataset.relatedMax, 10) || 0;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
return fetch(options.relationsUrl, { headers: { Accept: "application/json" } })
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
# scripts: true (default) to load runtime scripts, false to skip
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
{%
|
|
19
|
-
|
|
18
|
+
{% comment %}Bracket access: bare include.sort falls back to Hash#sort on
|
|
19
|
+
non-empty param hashes, so the default would silently stop applying.{% endcomment %}
|
|
20
|
+
{% assign related_sort = include["sort"] | default: "relevance" %}
|
|
21
|
+
{% assign include_scripts = include["scripts"] | default: true %}
|
|
20
22
|
{% if include_scripts == false or include_scripts == "false" %}
|
|
21
23
|
{% assign include_scripts = false %}
|
|
22
24
|
{% endif %}
|
|
@@ -106,6 +106,9 @@ async function embed(query) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
self.addEventListener("message", function (event) {
|
|
109
|
+
if (event.origin !== self.location.origin) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
109
112
|
var message = event.data || {};
|
|
110
113
|
activeConfig = activeConfig || message.config || {};
|
|
111
114
|
latestRequestId = message.id;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "cgi"
|
|
4
|
-
|
|
5
3
|
module Jekyll
|
|
6
4
|
module ClientSearch
|
|
7
5
|
# Liquid tag that renders a compact live-search dropdown suitable for
|
|
@@ -15,27 +13,22 @@ module Jekyll
|
|
|
15
13
|
#
|
|
16
14
|
# When client_search is disabled the tag renders nothing.
|
|
17
15
|
class DropdownTag < Liquid::Tag
|
|
16
|
+
include ScriptTags
|
|
17
|
+
|
|
18
18
|
SYNTAX = /\A(max:(\d+))?\s*(scripts_only|no_scripts)?\z/
|
|
19
19
|
|
|
20
20
|
def initialize(tag_name, markup, tokens)
|
|
21
21
|
super
|
|
22
|
+
# No validation here: `{% comment %}` still parses nested tags, so
|
|
23
|
+
# `initialize` runs on commented-out tags. Validate in `render` —
|
|
24
|
+
# a bad tag inside a comment must not break the build.
|
|
22
25
|
@markup = markup.to_s.strip
|
|
23
|
-
unless (match = @markup.match(SYNTAX))
|
|
24
|
-
raise Liquid::SyntaxError,
|
|
25
|
-
"search_dropdown: invalid syntax. Use {% search_dropdown %}, " \
|
|
26
|
-
"{% search_dropdown max:10 %}, {% search_dropdown scripts_only %}, " \
|
|
27
|
-
"or {% search_dropdown no_scripts %}"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
@max_items = match[2].to_i if match[2]
|
|
31
|
-
raise Liquid::SyntaxError, "search_dropdown: max must be greater than zero" if @max_items && @max_items < 1
|
|
32
|
-
|
|
33
|
-
@mode = match[3] || "full"
|
|
34
26
|
@input_id = "cs-dropdown-input-#{object_id}"
|
|
35
27
|
@results_id = "cs-dropdown-results-#{object_id}"
|
|
36
28
|
end
|
|
37
29
|
|
|
38
30
|
def render(context)
|
|
31
|
+
validate_markup!
|
|
39
32
|
site = context.registers[:site]
|
|
40
33
|
return "" unless enabled?(site)
|
|
41
34
|
|
|
@@ -47,6 +40,23 @@ module Jekyll
|
|
|
47
40
|
|
|
48
41
|
private
|
|
49
42
|
|
|
43
|
+
def validate_markup!
|
|
44
|
+
return if @validated
|
|
45
|
+
|
|
46
|
+
@validated = true
|
|
47
|
+
unless (match = @markup.match(SYNTAX))
|
|
48
|
+
raise Liquid::SyntaxError,
|
|
49
|
+
"search_dropdown: invalid syntax. Use {% search_dropdown %}, " \
|
|
50
|
+
"{% search_dropdown max:10 %}, {% search_dropdown scripts_only %}, " \
|
|
51
|
+
"or {% search_dropdown no_scripts %}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@max_items = match[2].to_i if match[2]
|
|
55
|
+
raise Liquid::SyntaxError, "search_dropdown: max must be greater than zero" if @max_items && @max_items < 1
|
|
56
|
+
|
|
57
|
+
@mode = match[3] || "full"
|
|
58
|
+
end
|
|
59
|
+
|
|
50
60
|
def enabled?(site)
|
|
51
61
|
return false if site.nil?
|
|
52
62
|
|
|
@@ -62,7 +72,7 @@ module Jekyll
|
|
|
62
72
|
form_html = build_form(max)
|
|
63
73
|
return form_html if @mode == "no_scripts"
|
|
64
74
|
|
|
65
|
-
scripts =
|
|
75
|
+
scripts = script_tags(configuration, prefix, "client-search-dropdown.js")
|
|
66
76
|
return scripts if @mode == "scripts_only"
|
|
67
77
|
|
|
68
78
|
"#{form_html}\n#{scripts}"
|
|
@@ -87,33 +97,6 @@ module Jekyll
|
|
|
87
97
|
</div>
|
|
88
98
|
HTML
|
|
89
99
|
end
|
|
90
|
-
|
|
91
|
-
def build_scripts(configuration, prefix)
|
|
92
|
-
scripts = []
|
|
93
|
-
engine = engine_script(configuration)
|
|
94
|
-
scripts << engine if engine
|
|
95
|
-
scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
|
|
96
|
-
scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
|
|
97
|
-
scripts << "<script src=\"#{prefix}/assets/client-search-dropdown.js\"></script>"
|
|
98
|
-
scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
|
|
99
|
-
scripts.map { |script| " #{script}" }.join("\n")
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def engine_script(configuration)
|
|
103
|
-
url = configuration.engine_url
|
|
104
|
-
return nil unless url
|
|
105
|
-
|
|
106
|
-
attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
|
|
107
|
-
if configuration.engine_crossorigin
|
|
108
|
-
crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
|
|
109
|
-
attrs << "crossorigin=\"#{crossorigin}\""
|
|
110
|
-
end
|
|
111
|
-
if configuration.engine_sri
|
|
112
|
-
integrity = CGI.escapeHTML(configuration.engine_sri)
|
|
113
|
-
attrs << "integrity=\"#{integrity}\""
|
|
114
|
-
end
|
|
115
|
-
"<script #{attrs.join(' ')}></script>"
|
|
116
|
-
end
|
|
117
100
|
end
|
|
118
101
|
end
|
|
119
102
|
end
|
|
@@ -18,11 +18,34 @@ module Jekyll
|
|
|
18
18
|
|
|
19
19
|
def initialize(tag_name, markup, tokens)
|
|
20
20
|
super
|
|
21
|
+
# No validation here: `{% comment %}` still parses nested tags, so
|
|
22
|
+
# `initialize` runs on commented-out tags. Validate in `render` —
|
|
23
|
+
# a bad tag inside a comment must not break the build.
|
|
21
24
|
@markup = markup.to_s.strip
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render(context)
|
|
22
28
|
parse_tokens(@markup)
|
|
29
|
+
site = context.registers[:site]
|
|
30
|
+
return "" if site.nil?
|
|
31
|
+
|
|
32
|
+
config = site.config.fetch("client_search", {})
|
|
33
|
+
return "" if config == false
|
|
34
|
+
return "" unless related_enabled?(config)
|
|
35
|
+
|
|
36
|
+
asset_prefix = asset_prefix(site)
|
|
37
|
+
sort_attr = @sort ? " data-related-sort=\"#{@sort}\"" : ""
|
|
38
|
+
max_attr = resolve_max_attr(config)
|
|
39
|
+
scripts = build_scripts(asset_prefix)
|
|
40
|
+
build_html(sort_attr, max_attr, scripts)
|
|
23
41
|
end
|
|
24
42
|
|
|
43
|
+
private
|
|
44
|
+
|
|
25
45
|
def parse_tokens(markup)
|
|
46
|
+
return if @parsed
|
|
47
|
+
|
|
48
|
+
@parsed = true
|
|
26
49
|
@sort = nil
|
|
27
50
|
@max_items = nil
|
|
28
51
|
@include_scripts = true
|
|
@@ -49,23 +72,6 @@ module Jekyll
|
|
|
49
72
|
"or {% related_articles no_scripts %}"
|
|
50
73
|
end
|
|
51
74
|
|
|
52
|
-
def render(context)
|
|
53
|
-
site = context.registers[:site]
|
|
54
|
-
return "" if site.nil?
|
|
55
|
-
|
|
56
|
-
config = site.config.fetch("client_search", {})
|
|
57
|
-
return "" if config == false
|
|
58
|
-
return "" unless related_enabled?(config)
|
|
59
|
-
|
|
60
|
-
asset_prefix = asset_prefix(site)
|
|
61
|
-
sort_attr = @sort ? " data-related-sort=\"#{@sort}\"" : ""
|
|
62
|
-
max_attr = resolve_max_attr(config)
|
|
63
|
-
scripts = build_scripts(asset_prefix)
|
|
64
|
-
build_html(sort_attr, max_attr, scripts)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
75
|
def related_enabled?(config)
|
|
70
76
|
related = config.fetch("related", {})
|
|
71
77
|
related["enabled"] == true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module ClientSearch
|
|
7
|
+
# Shared <script> tag assembly for the client-search Liquid tags so each
|
|
8
|
+
# tag only declares its own asset file.
|
|
9
|
+
module ScriptTags
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def script_tags(configuration, prefix, tag_asset, extra: [])
|
|
13
|
+
scripts = []
|
|
14
|
+
engine = engine_script(configuration)
|
|
15
|
+
scripts << engine if engine
|
|
16
|
+
scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
|
|
17
|
+
scripts.concat(extra)
|
|
18
|
+
scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
|
|
19
|
+
scripts << "<script src=\"#{prefix}/assets/#{tag_asset}\"></script>"
|
|
20
|
+
scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
|
|
21
|
+
scripts.map { |script| " #{script}" }.join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def engine_script(configuration)
|
|
25
|
+
url = configuration.engine_url
|
|
26
|
+
return nil unless url
|
|
27
|
+
|
|
28
|
+
attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
|
|
29
|
+
if configuration.engine_crossorigin
|
|
30
|
+
crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
|
|
31
|
+
attrs << "crossorigin=\"#{crossorigin}\""
|
|
32
|
+
end
|
|
33
|
+
if configuration.engine_sri
|
|
34
|
+
integrity = CGI.escapeHTML(configuration.engine_sri)
|
|
35
|
+
attrs << "integrity=\"#{integrity}\""
|
|
36
|
+
end
|
|
37
|
+
"<script #{attrs.join(' ')}></script>"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "cgi"
|
|
4
|
-
|
|
5
3
|
module Jekyll
|
|
6
4
|
module ClientSearch
|
|
7
5
|
# Liquid tag that renders the search form, status/results containers,
|
|
@@ -14,21 +12,20 @@ module Jekyll
|
|
|
14
12
|
#
|
|
15
13
|
# When client_search is disabled the tag renders nothing.
|
|
16
14
|
class SearchTag < Liquid::Tag
|
|
15
|
+
include ScriptTags
|
|
16
|
+
|
|
17
17
|
SYNTAX = /\A(scripts_only|no_scripts)?\z/
|
|
18
18
|
|
|
19
19
|
def initialize(tag_name, markup, tokens)
|
|
20
20
|
super
|
|
21
|
+
# No validation here: `{% comment %}` still parses nested tags, so
|
|
22
|
+
# `initialize` runs on commented-out tags. Validate in `render` —
|
|
23
|
+
# a bad tag inside a comment must not break the build.
|
|
21
24
|
@markup = markup.to_s.strip
|
|
22
|
-
unless (match = @markup.match(SYNTAX))
|
|
23
|
-
raise Liquid::SyntaxError,
|
|
24
|
-
"search_form: invalid syntax. Use {% search_form %}, " \
|
|
25
|
-
"{% search_form scripts_only %}, or {% search_form no_scripts %}"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
@mode = match[1] || "full"
|
|
29
25
|
end
|
|
30
26
|
|
|
31
27
|
def render(context)
|
|
28
|
+
validate_markup!
|
|
32
29
|
site = context.registers[:site]
|
|
33
30
|
return "" if site.nil?
|
|
34
31
|
|
|
@@ -51,6 +48,18 @@ module Jekyll
|
|
|
51
48
|
|
|
52
49
|
private
|
|
53
50
|
|
|
51
|
+
def validate_markup!
|
|
52
|
+
return if @mode
|
|
53
|
+
|
|
54
|
+
unless (match = @markup.match(SYNTAX))
|
|
55
|
+
raise Liquid::SyntaxError,
|
|
56
|
+
"search_form: invalid syntax. Use {% search_form %}, " \
|
|
57
|
+
"{% search_form scripts_only %}, or {% search_form no_scripts %}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@mode = match[1] || "full"
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
def build_form
|
|
55
64
|
<<~HTML
|
|
56
65
|
<form id="search-form" role="search">
|
|
@@ -64,31 +73,8 @@ module Jekyll
|
|
|
64
73
|
end
|
|
65
74
|
|
|
66
75
|
def build_scripts(configuration, prefix)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
scripts << engine if engine
|
|
70
|
-
scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
|
|
71
|
-
scripts.concat(embedder_scripts(configuration, prefix))
|
|
72
|
-
scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
|
|
73
|
-
scripts << "<script src=\"#{prefix}/assets/client-search-base.js\"></script>"
|
|
74
|
-
scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
|
|
75
|
-
scripts.map { |script| " #{script}" }.join("\n")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def engine_script(configuration)
|
|
79
|
-
url = configuration.engine_url
|
|
80
|
-
return nil unless url
|
|
81
|
-
|
|
82
|
-
attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
|
|
83
|
-
if configuration.engine_crossorigin
|
|
84
|
-
crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
|
|
85
|
-
attrs << "crossorigin=\"#{crossorigin}\""
|
|
86
|
-
end
|
|
87
|
-
if configuration.engine_sri
|
|
88
|
-
integrity = CGI.escapeHTML(configuration.engine_sri)
|
|
89
|
-
attrs << "integrity=\"#{integrity}\""
|
|
90
|
-
end
|
|
91
|
-
"<script #{attrs.join(' ')}></script>"
|
|
76
|
+
script_tags(configuration, prefix, "client-search-base.js",
|
|
77
|
+
extra: embedder_scripts(configuration, prefix))
|
|
92
78
|
end
|
|
93
79
|
|
|
94
80
|
def embedder_scripts(configuration, prefix)
|
data/lib/jekyll/client_search.rb
CHANGED
|
@@ -23,5 +23,6 @@ require_relative "client_search/index_cache"
|
|
|
23
23
|
require_relative "client_search/ollama_embedding_adapter"
|
|
24
24
|
require_relative "client_search/generator"
|
|
25
25
|
require_relative "client_search/related_tag"
|
|
26
|
+
require_relative "client_search/script_tags"
|
|
26
27
|
require_relative "client_search/dropdown_tag"
|
|
27
28
|
require_relative "client_search/search_tag"
|
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.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svend Gundestrup
|
|
@@ -75,6 +75,7 @@ files:
|
|
|
75
75
|
- lib/jekyll/client_search/related_page.rb
|
|
76
76
|
- lib/jekyll/client_search/related_tag.rb
|
|
77
77
|
- lib/jekyll/client_search/runtime_config_page.rb
|
|
78
|
+
- lib/jekyll/client_search/script_tags.rb
|
|
78
79
|
- lib/jekyll/client_search/search_index_page.rb
|
|
79
80
|
- lib/jekyll/client_search/search_tag.rb
|
|
80
81
|
- lib/jekyll/client_search/tasks.rb
|
|
@@ -95,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
95
96
|
requirements:
|
|
96
97
|
- - ">="
|
|
97
98
|
- !ruby/object:Gem::Version
|
|
98
|
-
version: 3.
|
|
99
|
+
version: 3.3.0
|
|
99
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
101
|
requirements:
|
|
101
102
|
- - ">="
|