jekyll-client-search 0.3.2 → 0.3.4

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: d8289b785b9591674a7c171eea4ac3a3500765d79dd6679793a5c693879d7ed1
4
- data.tar.gz: 2cc203750200af25356f75ca4e272747f39763f7abcfd9bedc9eff2ee9332b24
3
+ metadata.gz: 74262c7813a4c946b9a4c9c7d7a19aa1d866dedfdf82781131e5edbe9aabbdef
4
+ data.tar.gz: 6ebbd6018cfc419d586d50d60d7d346c5a6c0a7b442275caf55fc22d3420a797
5
5
  SHA512:
6
- metadata.gz: 9ffd9295cb6e23c001c8c88acbdeb5fa8252a47dd646c58ef5f09b4a0233fe88b026cea4c5f8a4b0cc9b03e2907377e6b070af3066c2b266752ad1e49163beaf
7
- data.tar.gz: 90261e2e563cf3649913211bcded60669fe55abec5a9e5caf1fa049e4342874d4a33e2ff058b72c35817ed96fed92b88a10c94731c2f7c7467940413f4444211
6
+ metadata.gz: 402221144b4bbf95e1daf759430c6a59fad8025d650f4f84624146ed44951de2e34755a1e335f478aeabe2fbec693f2b95c74c64aad1ff19e54b3a1e30213ee8
7
+ data.tar.gz: 84d405d332fdd45895885b43dc408a46574b97f26f2b1742513ad97f9910e0ed96f6bb4ddff0c4cf0148751b2a0348b97b5adaa41aab57ba5c724c07f0827d5e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.4 — 2026-09-11
6
+
7
+ ### Added
8
+ - SonarQube Cloud badge in README.md (project key: `gundestrup_jekyll-client-search`)
9
+
10
+ ### Fixed
11
+ - Verify `event.origin` in transformers-worker.js `message` listener to prevent cross-origin message injection (SonarQube: `javascript:S2819`, CRITICAL)
12
+ - Add `--ignore-scripts` to `npm ci` in CI to prevent lifecycle script execution during dependency installation (SonarQube: `githubactions:S6505`)
13
+ - Pin Semgrep to `1.176.0` with `--only-binary :all:` in CI to prevent setup script execution and lock resolved versions (SonarQube: `githubactions:S8541`, `githubactions:S8544`)
14
+ - Wrap `normalize` and `resultElement` callbacks in anonymous functions in `.map()` calls to avoid passing unexpected iterator arguments (SonarQube: `javascript:S7727`)
15
+ - Wrap `search` callback in anonymous function in `.forEach()` call in benchmark (SonarQube: `javascript:S7727`)
16
+ - Replace `parseInt` with `Number.parseInt` in dropdown and related runtimes (SonarQube: `javascript:S7773`)
17
+
18
+ ## 0.3.3 — 2026-09-11
19
+
20
+ ### Refactored
21
+ - Extract duplicated `normalize` function from `client-search-base.js` and `client-search-dropdown.js` into new shared module `assets/client-search-shared.js` (exposes `window.ClientSearchShared.normalize` and `window.ClientSearchShared.coreFields`) — eliminates CodeFactor duplicate-code finding and ensures consistent normalization across both runtimes
22
+
5
23
  ## 0.3.2 — 2026-09-10
6
24
 
7
25
  ### Added
data/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  [![Coverage](https://img.shields.io/badge/coverage-100%25%20branches-brightgreen.svg)](#testing-strategy)
11
11
  [![Semgrep](https://img.shields.io/badge/semgrep-scanning-brightgreen.svg)](https://semgrep.dev)
12
12
  [![CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search/badge)](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search)
13
+ [![SonarQube](https://sonarcloud.io/api/project_badges/measure?project=gundestrup_jekyll-client-search&metric=alert_status)](https://sonarcloud.io/project/overview?id=gundestrup_jekyll-client-search)
13
14
 
14
15
  A Jekyll plugin that generates a JSON document index for client-side search.
15
16
  Supports lexical search ([MiniSearch](https://lucaong.github.io/minisearch/),
@@ -58,55 +58,15 @@
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
-
67
- function normalize(entry) {
68
- var id = entry.id || entry.url;
69
- if (!id) {
70
- return null;
71
- }
72
- var categories = Array.isArray(entry.categories) ? entry.categories : [];
73
- var tags = Array.isArray(entry.tags) ? entry.tags : [];
74
- var normalized = {
75
- id: id,
76
- title: entry.title || "Untitled",
77
- url: entry.url || "#",
78
- excerpt: entry.excerpt || "",
79
- content: entry.content || "",
80
- date: entry.date || "",
81
- date_timestamp: Number(entry.date_timestamp) || 0,
82
- categories: categories,
83
- tags: tags,
84
- categoriesText: categories.join(" "),
85
- tagsText: tags.join(" ")
86
- };
87
- if (entry.source) {
88
- normalized.source = entry.source;
89
- }
90
- if (Array.isArray(entry.embedding) && entry.embedding.length > 0) {
91
- normalized.embedding = entry.embedding;
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
- });
101
- return normalized;
102
- }
61
+ var normalize = window.ClientSearchShared.normalize;
62
+ var coreFields = window.ClientSearchShared.coreFields;
103
63
 
104
64
  function buildIndex(data) {
105
65
  if (!Array.isArray(data)) {
106
66
  throw new TypeError("Search index must be an array");
107
67
  }
108
68
 
109
- var documents = data.map(normalize).filter(Boolean);
69
+ var documents = data.map(function (entry) { return normalize(entry); }).filter(Boolean);
110
70
  documentsById = new Map(documents.map(function (entry) {
111
71
  return [entry.id, entry];
112
72
  }));
@@ -272,7 +232,7 @@
272
232
  }
273
233
  results.replaceChildren.apply(
274
234
  results,
275
- matches.map(resultElement).filter(Boolean)
235
+ matches.map(function (match) { return resultElement(match); }).filter(Boolean)
276
236
  );
277
237
  }
278
238
 
@@ -41,39 +41,7 @@
41
41
  return key.replace(/_([a-z])/g, function (_, char) { return char.toUpperCase(); });
42
42
  }
43
43
 
44
- function normalize(entry) {
45
- var id = entry.id || entry.url;
46
- if (!id) {
47
- return null;
48
- }
49
- var categories = Array.isArray(entry.categories) ? entry.categories : [];
50
- var tags = Array.isArray(entry.tags) ? entry.tags : [];
51
- var normalized = {
52
- id: id,
53
- title: entry.title || "Untitled",
54
- url: entry.url || "#",
55
- excerpt: entry.excerpt || "",
56
- content: entry.content || "",
57
- date: entry.date || "",
58
- date_timestamp: Number(entry.date_timestamp) || 0,
59
- categories: categories,
60
- tags: tags,
61
- categoriesText: categories.join(" "),
62
- tagsText: tags.join(" ")
63
- };
64
- if (entry.source) {
65
- normalized.source = entry.source;
66
- }
67
- Object.keys(entry).forEach(function (key) {
68
- if (!(key in normalized) && key !== "embedding") {
69
- var value = entry[key];
70
- if (value !== null && value !== undefined && value !== "") {
71
- normalized[key] = value;
72
- }
73
- }
74
- });
75
- return normalized;
76
- }
44
+ var normalize = window.ClientSearchShared.normalize;
77
45
 
78
46
  function loadIndex(config) {
79
47
  if (sharedIndex && sharedDocuments) {
@@ -99,7 +67,7 @@
99
67
  if (!Array.isArray(data)) {
100
68
  throw new TypeError("Search index must be an array");
101
69
  }
102
- var documents = data.map(normalize).filter(Boolean);
70
+ var documents = data.map(function (entry) { return normalize(entry); }).filter(Boolean);
103
71
  var adapter = window.ClientSearchAdapters &&
104
72
  (window.ClientSearchAdapters[config.engine] ||
105
73
  window.ClientSearchAdapters.minisearch);
@@ -209,7 +177,7 @@
209
177
  this.input = root.querySelector("[data-cs-dropdown-input]");
210
178
  this.results = root.querySelector("[data-cs-dropdown-results]");
211
179
  this.config = config;
212
- this.maxItems = parseInt(this.results.dataset.maxItems, 10) || config.maxItems || 5;
180
+ this.maxItems = Number.parseInt(this.results.dataset.maxItems, 10) || config.maxItems || 5;
213
181
  this.selectedIndex = -1;
214
182
  this.currentItems = [];
215
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" } })
@@ -0,0 +1,57 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ /**
5
+ * ClientSearch shared utilities — used by the base runtime and the
6
+ * dropdown runtime to normalize search index entries into a uniform
7
+ * document format.
8
+ *
9
+ * Loaded before client-search-base.js and client-search-dropdown.js.
10
+ * Exposes window.ClientSearchShared.normalize(entry).
11
+ */
12
+
13
+ var CORE_FIELDS = [
14
+ "id", "title", "url", "excerpt", "content",
15
+ "categories", "tags", "categoriesText", "tagsText",
16
+ "date", "date_timestamp", "embedding"
17
+ ];
18
+
19
+ function normalize(entry) {
20
+ var id = entry.id || entry.url;
21
+ if (!id) {
22
+ return null;
23
+ }
24
+ var categories = Array.isArray(entry.categories) ? entry.categories : [];
25
+ var tags = Array.isArray(entry.tags) ? entry.tags : [];
26
+ var normalized = {
27
+ id: id,
28
+ title: entry.title || "Untitled",
29
+ url: entry.url || "#",
30
+ excerpt: entry.excerpt || "",
31
+ content: entry.content || "",
32
+ date: entry.date || "",
33
+ date_timestamp: Number(entry.date_timestamp) || 0,
34
+ categories: categories,
35
+ tags: tags,
36
+ categoriesText: categories.join(" "),
37
+ tagsText: tags.join(" ")
38
+ };
39
+ if (entry.source) {
40
+ normalized.source = entry.source;
41
+ }
42
+ if (Array.isArray(entry.embedding) && entry.embedding.length > 0) {
43
+ normalized.embedding = entry.embedding;
44
+ }
45
+ Object.keys(entry).forEach(function (key) {
46
+ if (CORE_FIELDS.indexOf(key) === -1 && key !== "source" && !(key in normalized)) {
47
+ var value = entry[key];
48
+ if (value !== null && value !== undefined && value !== "") {
49
+ normalized[key] = value;
50
+ }
51
+ }
52
+ });
53
+ return normalized;
54
+ }
55
+
56
+ window.ClientSearchShared = { normalize: normalize, coreFields: CORE_FIELDS };
57
+ })();
@@ -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;
@@ -105,7 +105,7 @@ module Jekyll
105
105
  end
106
106
 
107
107
  def runtime_assets
108
- assets = ["assets/client-search-base.js", "assets/adapters/#{engine}.js"]
108
+ assets = ["assets/client-search-shared.js", "assets/client-search-base.js", "assets/adapters/#{engine}.js"]
109
109
  assets.concat(query_embedder_assets) if engine == "semantic" && embedding_enabled?
110
110
  assets << "assets/client-search-related.js" if related_enabled?
111
111
  assets << "assets/client-search-dropdown.js" if dropdown_enabled?
@@ -90,24 +90,30 @@ module Jekyll
90
90
 
91
91
  def build_scripts(configuration, prefix)
92
92
  scripts = []
93
- engine = configuration.engine_url
94
- if engine
95
- attrs = ["src=\"#{CGI.escapeHTML(engine)}\""]
96
- if configuration.engine_crossorigin
97
- crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
98
- attrs << "crossorigin=\"#{crossorigin}\""
99
- end
100
- if configuration.engine_sri
101
- integrity = CGI.escapeHTML(configuration.engine_sri)
102
- attrs << "integrity=\"#{integrity}\""
103
- end
104
- scripts << "<script #{attrs.join(' ')}></script>"
105
- end
93
+ engine = engine_script(configuration)
94
+ scripts << engine if engine
106
95
  scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
96
+ scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
107
97
  scripts << "<script src=\"#{prefix}/assets/client-search-dropdown.js\"></script>"
108
98
  scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
109
99
  scripts.map { |script| " #{script}" }.join("\n")
110
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
111
117
  end
112
118
  end
113
119
  end
@@ -69,6 +69,7 @@ module Jekyll
69
69
  scripts << engine if engine
70
70
  scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
71
71
  scripts.concat(embedder_scripts(configuration, prefix))
72
+ scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
72
73
  scripts << "<script src=\"#{prefix}/assets/client-search-base.js\"></script>"
73
74
  scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
74
75
  scripts.map { |script| " #{script}" }.join("\n")
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module ClientSearch
5
- VERSION = "0.3.2"
5
+ VERSION = "0.3.4"
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.3.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
@@ -48,6 +48,7 @@ files:
48
48
  - assets/client-search-base.js
49
49
  - assets/client-search-dropdown.js
50
50
  - assets/client-search-related.js
51
+ - assets/client-search-shared.js
51
52
  - assets/includes/related-articles.html
52
53
  - assets/layouts/post-with-related.html
53
54
  - assets/query-embedders/ollama-api.js