@jxsuite/search 0.3.0 → 0.4.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Kevin Shenk
3
+ Copyright (c) 2026 Avunu LLC and Jx Suite contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -183,6 +183,16 @@ function entryUrl(basePath, id, trailingSlash) {
183
183
  function jxTreeToText(children) {
184
184
  return collectText(children).join(" ").replaceAll(/\s+/g, " ").trim();
185
185
  }
186
+ var ENTITIES = {
187
+ "$": "$",
188
+ "&": "&",
189
+ ">": ">",
190
+ "&lt;": "<",
191
+ "&quot;": '"'
192
+ };
193
+ function innerHtmlText(html) {
194
+ return html.replaceAll(/<[^>]*>/g, " ").replaceAll(/&#36;|&amp;|&gt;|&lt;|&quot;/g, (entity) => ENTITIES[entity]);
195
+ }
186
196
  function collectText(children) {
187
197
  const parts = [];
188
198
  for (const node of children ?? []) {
@@ -195,6 +205,8 @@ function collectText(children) {
195
205
  }
196
206
  if (typeof node.textContent === "string") {
197
207
  parts.push(node.textContent);
208
+ } else if (typeof node.innerHTML === "string") {
209
+ parts.push(innerHtmlText(node.innerHTML));
198
210
  }
199
211
  if (Array.isArray(node.children)) {
200
212
  parts.push(...collectText(node.children));
@@ -286,7 +298,8 @@ var SearchIndex = {
286
298
  continue;
287
299
  }
288
300
  const locale = i18n === null ? undefined : canonicalizeLocale(entry._meta?.locale) ?? undefined;
289
- const url = entryUrl(`${localeUrlPrefix(locale, i18n)}${collection.basePath}`, entry.id, trailingSlash);
301
+ const stamped = entry._meta?.url;
302
+ const url = typeof stamped === "string" && stamped.startsWith("/") ? stamped : entryUrl(`${localeUrlPrefix(locale, i18n)}${collection.basePath}`, entry.id, trailingSlash);
290
303
  const title = typeof data.title === "string" ? data.title : entry.id;
291
304
  const description = typeof data.description === "string" ? data.description : "";
292
305
  const children = entry.$children;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/search",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Static site search for Jx: build-time index emission from content collections and a headless MiniSearch browser client",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "upgrade": "bun update --latest && bun install"
37
37
  },
38
38
  "dependencies": {
39
- "@jxsuite/schema": "^2.2.0",
39
+ "@jxsuite/schema": "^3.0.0",
40
40
  "minisearch": "^7.2.0"
41
41
  },
42
42
  "devDependencies": {
@@ -137,11 +137,21 @@ export const SearchIndex = {
137
137
  */
138
138
  const locale =
139
139
  i18n === null ? undefined : (canonicalizeLocale(entry._meta?.locale) ?? undefined);
140
- const url = entryUrl(
141
- `${localeUrlPrefix(locale, i18n)}${collection.basePath}`,
142
- entry.id,
143
- trailingSlash,
144
- );
140
+ /*
141
+ * An entry of a content type with a `route` carries its URL (`_meta.url`), stamped by the
142
+ * loader from the template its page and its links were generated from, so the index cannot
143
+ * point at a URL the site does not have. Only an entry with none is placed by the
144
+ * collection's `basePath` + id, the older, flat rule.
145
+ */
146
+ const stamped = entry._meta?.url;
147
+ const url =
148
+ typeof stamped === "string" && stamped.startsWith("/")
149
+ ? stamped
150
+ : entryUrl(
151
+ `${localeUrlPrefix(locale, i18n)}${collection.basePath}`,
152
+ entry.id,
153
+ trailingSlash,
154
+ );
145
155
  const title = typeof data.title === "string" ? data.title : entry.id;
146
156
  const description = typeof data.description === "string" ? data.description : "";
147
157
  const children = entry.$children as (JxElement | string)[] | undefined;
package/src/shared.ts CHANGED
@@ -84,6 +84,28 @@ export function jxTreeToText(children?: (JxElement | string)[]): string {
84
84
  return collectText(children).join(" ").replaceAll(/\s+/g, " ").trim();
85
85
  }
86
86
 
87
+ /** The five entities the parser's escaping and an author's markup commonly leave in `innerHTML`. */
88
+ const ENTITIES: Record<string, string> = {
89
+ "&#36;": "$",
90
+ "&amp;": "&",
91
+ "&gt;": ">",
92
+ "&lt;": "<",
93
+ "&quot;": '"',
94
+ };
95
+
96
+ /**
97
+ * The text of an `innerHTML` string: tags dropped, the common entities decoded.
98
+ *
99
+ * The content loader writes text that contains `${` as escaped `innerHTML` so the build does not
100
+ * read it as a template (parser.md §3.5), which means the words of a code sample live there and not
101
+ * in `textContent`. An index that skipped `innerHTML` would silently stop finding them.
102
+ */
103
+ function innerHtmlText(html: string): string {
104
+ return html
105
+ .replaceAll(/<[^>]*>/g, " ")
106
+ .replaceAll(/&#36;|&amp;|&gt;|&lt;|&quot;/g, (entity) => ENTITIES[entity]!);
107
+ }
108
+
87
109
  function collectText(children: (JxElement | string)[] | undefined): string[] {
88
110
  const parts: string[] = [];
89
111
  for (const node of children ?? []) {
@@ -96,6 +118,8 @@ function collectText(children: (JxElement | string)[] | undefined): string[] {
96
118
  }
97
119
  if (typeof node.textContent === "string") {
98
120
  parts.push(node.textContent);
121
+ } else if (typeof node.innerHTML === "string") {
122
+ parts.push(innerHtmlText(node.innerHTML));
99
123
  }
100
124
  if (Array.isArray(node.children)) {
101
125
  parts.push(...collectText(node.children as (JxElement | string)[]));