@canopy-iiif/app 0.10.19 → 0.10.21
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/lib/build/mdx.js +16 -0
- package/lib/build/search-index.js +5 -0
- package/lib/build/search.js +2 -0
- package/lib/search/search-app.jsx +74 -13
- package/lib/search/search.js +164 -15
- package/package.json +1 -1
- package/ui/dist/index.mjs +217 -70
- package/ui/dist/index.mjs.map +3 -3
- package/ui/styles/components/_article-card.scss +51 -0
- package/ui/styles/components/_card.scss +0 -33
- package/ui/styles/components/index.scss +1 -0
- package/ui/styles/index.css +50 -33
package/ui/dist/index.mjs
CHANGED
|
@@ -106,69 +106,186 @@ function Card({
|
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// ui/src/layout/
|
|
109
|
+
// ui/src/layout/ArticleCard.jsx
|
|
110
110
|
import React3, { useMemo } from "react";
|
|
111
111
|
function escapeRegExp(str = "") {
|
|
112
112
|
return String(str).replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
113
113
|
}
|
|
114
|
-
function buildSnippet({ text = "", query = "", maxLength =
|
|
114
|
+
function buildSnippet({ text = "", query = "", maxLength = 360 }) {
|
|
115
115
|
const clean = String(text || "").replace(/\s+/g, " ").trim();
|
|
116
116
|
if (!clean) return "";
|
|
117
|
+
const safeMax = Math.max(60, Number(maxLength) || 360);
|
|
117
118
|
const term = String(query || "").trim();
|
|
118
119
|
if (!term)
|
|
119
|
-
return clean.length >
|
|
120
|
+
return clean.length > safeMax ? clean.slice(0, safeMax).trimEnd() + "\u2026" : clean;
|
|
120
121
|
const lower = clean.toLowerCase();
|
|
121
122
|
const termLower = term.toLowerCase();
|
|
122
123
|
const idx = lower.indexOf(termLower);
|
|
123
|
-
if (idx === -1)
|
|
124
|
-
return clean.length >
|
|
124
|
+
if (idx === -1)
|
|
125
|
+
return clean.length > safeMax ? clean.slice(0, safeMax).trimEnd() + "\u2026" : clean;
|
|
126
|
+
const padding = Math.max(0, Math.floor((safeMax - term.length) / 2));
|
|
127
|
+
let start = Math.max(0, idx - padding);
|
|
128
|
+
let end = start + safeMax;
|
|
129
|
+
if (end > clean.length) {
|
|
130
|
+
end = clean.length;
|
|
131
|
+
start = Math.max(0, end - safeMax);
|
|
125
132
|
}
|
|
126
|
-
|
|
127
|
-
const start = Math.max(0, idx - context);
|
|
128
|
-
const end = Math.min(clean.length, idx + term.length + context);
|
|
129
|
-
let snippet = clean.slice(start, end);
|
|
133
|
+
let snippet = clean.slice(start, end).trim();
|
|
130
134
|
if (start > 0) snippet = "\u2026" + snippet;
|
|
131
135
|
if (end < clean.length) snippet = snippet + "\u2026";
|
|
132
136
|
return snippet;
|
|
133
137
|
}
|
|
134
|
-
function
|
|
135
|
-
if (!query) return
|
|
138
|
+
function highlightTextNode(text, query, keyPrefix = "") {
|
|
139
|
+
if (!query) return text;
|
|
136
140
|
const term = String(query).trim();
|
|
137
|
-
if (!term) return
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
+
if (!term) return text;
|
|
142
|
+
const regex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
|
143
|
+
const parts = String(text).split(regex);
|
|
141
144
|
const termLower = term.toLowerCase();
|
|
142
|
-
return parts.map(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
return parts.map((part, idx) => {
|
|
146
|
+
if (!part) return null;
|
|
147
|
+
if (part.toLowerCase() === termLower) {
|
|
148
|
+
return /* @__PURE__ */ React3.createElement("mark", { key: `${keyPrefix}-${idx}` }, part);
|
|
149
|
+
}
|
|
150
|
+
return /* @__PURE__ */ React3.createElement(React3.Fragment, { key: `${keyPrefix}-${idx}` }, part);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function tokenizeInlineMarkdown(input = "") {
|
|
154
|
+
const tokens = [];
|
|
155
|
+
let text = input;
|
|
156
|
+
while (text.length) {
|
|
157
|
+
if (text.startsWith("\n")) {
|
|
158
|
+
tokens.push({ type: "break" });
|
|
159
|
+
text = text.slice(1);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (text.startsWith("**")) {
|
|
163
|
+
const closing = text.indexOf("**", 2);
|
|
164
|
+
if (closing !== -1) {
|
|
165
|
+
const inner = text.slice(2, closing);
|
|
166
|
+
tokens.push({ type: "strong", children: tokenizeInlineMarkdown(inner) });
|
|
167
|
+
text = text.slice(closing + 2);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (text.startsWith("__")) {
|
|
172
|
+
const closing = text.indexOf("__", 2);
|
|
173
|
+
if (closing !== -1) {
|
|
174
|
+
const inner = text.slice(2, closing);
|
|
175
|
+
tokens.push({ type: "strong", children: tokenizeInlineMarkdown(inner) });
|
|
176
|
+
text = text.slice(closing + 2);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (text.startsWith("*")) {
|
|
181
|
+
if (!text.startsWith("**")) {
|
|
182
|
+
const closing = text.indexOf("*", 1);
|
|
183
|
+
if (closing !== -1) {
|
|
184
|
+
const inner = text.slice(1, closing);
|
|
185
|
+
tokens.push({ type: "em", children: tokenizeInlineMarkdown(inner) });
|
|
186
|
+
text = text.slice(closing + 1);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (text.startsWith("_")) {
|
|
192
|
+
if (!text.startsWith("__")) {
|
|
193
|
+
const closing = text.indexOf("_", 1);
|
|
194
|
+
if (closing !== -1) {
|
|
195
|
+
const inner = text.slice(1, closing);
|
|
196
|
+
tokens.push({ type: "em", children: tokenizeInlineMarkdown(inner) });
|
|
197
|
+
text = text.slice(closing + 1);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (text.startsWith("`")) {
|
|
203
|
+
const closing = text.indexOf("`", 1);
|
|
204
|
+
if (closing !== -1) {
|
|
205
|
+
const inner = text.slice(1, closing);
|
|
206
|
+
tokens.push({ type: "code", value: inner });
|
|
207
|
+
text = text.slice(closing + 1);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (text.startsWith("[")) {
|
|
212
|
+
const endLabel = text.indexOf("]");
|
|
213
|
+
const startHref = endLabel !== -1 ? text.indexOf("(", endLabel) : -1;
|
|
214
|
+
const endHref = startHref !== -1 ? text.indexOf(")", startHref) : -1;
|
|
215
|
+
if (endLabel !== -1 && startHref === endLabel + 1 && endHref !== -1) {
|
|
216
|
+
const label = text.slice(1, endLabel);
|
|
217
|
+
const href = text.slice(startHref + 1, endHref);
|
|
218
|
+
tokens.push({
|
|
219
|
+
type: "link",
|
|
220
|
+
href,
|
|
221
|
+
children: tokenizeInlineMarkdown(label)
|
|
222
|
+
});
|
|
223
|
+
text = text.slice(endHref + 1);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const specials = ["**", "__", "*", "_", "`", "[", "\n"];
|
|
228
|
+
const nextIndex = specials.map((needle) => needle === "\n" ? text.indexOf("\n") : text.indexOf(needle)).filter((idx) => idx > 0).reduce((min, idx) => min === -1 || idx < min ? idx : min, -1);
|
|
229
|
+
if (nextIndex === -1) {
|
|
230
|
+
tokens.push({ type: "text", value: text });
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
tokens.push({ type: "text", value: text.slice(0, nextIndex) });
|
|
234
|
+
text = text.slice(nextIndex);
|
|
235
|
+
}
|
|
236
|
+
return tokens;
|
|
237
|
+
}
|
|
238
|
+
function renderMarkdownTokens(tokens, query, keyPrefix = "token") {
|
|
239
|
+
return tokens.map((token, idx) => {
|
|
240
|
+
const key = `${keyPrefix}-${idx}`;
|
|
241
|
+
switch (token.type) {
|
|
242
|
+
case "strong":
|
|
243
|
+
return /* @__PURE__ */ React3.createElement("strong", { key }, renderMarkdownTokens(token.children || [], query, key));
|
|
244
|
+
case "em":
|
|
245
|
+
return /* @__PURE__ */ React3.createElement("em", { key }, renderMarkdownTokens(token.children || [], query, key));
|
|
246
|
+
case "code":
|
|
247
|
+
return /* @__PURE__ */ React3.createElement("code", { key }, token.value);
|
|
248
|
+
case "link":
|
|
249
|
+
return /* @__PURE__ */ React3.createElement("a", { key, href: token.href, target: "_blank", rel: "noreferrer" }, renderMarkdownTokens(token.children || [], query, key));
|
|
250
|
+
case "break":
|
|
251
|
+
return /* @__PURE__ */ React3.createElement("br", { key });
|
|
252
|
+
case "text":
|
|
253
|
+
default:
|
|
254
|
+
return /* @__PURE__ */ React3.createElement(React3.Fragment, { key }, highlightTextNode(token.value || "", query, key));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
145
257
|
}
|
|
146
|
-
function
|
|
258
|
+
function formatDisplayUrl(href = "") {
|
|
259
|
+
try {
|
|
260
|
+
const url = new URL(href, href.startsWith("http") ? void 0 : "http://example.com");
|
|
261
|
+
if (!href.startsWith("http")) return href;
|
|
262
|
+
const displayPath = url.pathname.replace(/\/$/, "");
|
|
263
|
+
return `${url.host}${displayPath}${url.search}`.replace(/\/$/, "");
|
|
264
|
+
} catch (_) {
|
|
265
|
+
return href;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function ArticleCard({
|
|
147
269
|
href = "#",
|
|
148
270
|
title = "Untitled",
|
|
149
271
|
annotation = "",
|
|
150
272
|
summary = "",
|
|
273
|
+
summaryMarkdown = "",
|
|
151
274
|
metadata = [],
|
|
152
275
|
query = ""
|
|
153
276
|
}) {
|
|
154
|
-
const snippetSource = annotation || summary;
|
|
277
|
+
const snippetSource = summaryMarkdown || annotation || summary;
|
|
155
278
|
const snippet = useMemo(
|
|
156
279
|
() => buildSnippet({ text: snippetSource, query }),
|
|
157
280
|
[snippetSource, query]
|
|
158
281
|
);
|
|
159
|
-
const
|
|
160
|
-
() =>
|
|
161
|
-
[snippet
|
|
282
|
+
const snippetTokens = useMemo(
|
|
283
|
+
() => tokenizeInlineMarkdown(snippet),
|
|
284
|
+
[snippet]
|
|
162
285
|
);
|
|
163
286
|
const metaList = Array.isArray(metadata) ? metadata.map((m) => String(m || "")).filter(Boolean) : [];
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{
|
|
167
|
-
key: `${item}-${idx}`,
|
|
168
|
-
className: "rounded-full border border-slate-200 bg-slate-50 px-2 py-1"
|
|
169
|
-
},
|
|
170
|
-
item
|
|
171
|
-
))) : null));
|
|
287
|
+
const displayUrl = useMemo(() => formatDisplayUrl(href), [href]);
|
|
288
|
+
return /* @__PURE__ */ React3.createElement("a", { href, className: "canopy-article-card" }, /* @__PURE__ */ React3.createElement("article", null, displayUrl ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__url" }, displayUrl) : null, /* @__PURE__ */ React3.createElement("h3", null, title), snippet ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__snippet" }, renderMarkdownTokens(snippetTokens, query)) : null, metaList.length ? /* @__PURE__ */ React3.createElement("ul", { className: "canopy-article-card__meta" }, metaList.slice(0, 3).map((item, idx) => /* @__PURE__ */ React3.createElement("li", { key: `${item}-${idx}` }, item))) : null));
|
|
172
289
|
}
|
|
173
290
|
|
|
174
291
|
// ui/src/layout/Grid.jsx
|
|
@@ -1221,71 +1338,103 @@ function MdxSearchTabs(props) {
|
|
|
1221
1338
|
|
|
1222
1339
|
// ui/src/search/SearchResults.jsx
|
|
1223
1340
|
import React24 from "react";
|
|
1341
|
+
function DefaultArticleTemplate({ record, query }) {
|
|
1342
|
+
if (!record) return null;
|
|
1343
|
+
const metadata = Array.isArray(record.metadata) ? record.metadata : [];
|
|
1344
|
+
return /* @__PURE__ */ React24.createElement(
|
|
1345
|
+
ArticleCard,
|
|
1346
|
+
{
|
|
1347
|
+
href: record.href,
|
|
1348
|
+
title: record.title || record.href || "Untitled",
|
|
1349
|
+
annotation: record.annotation,
|
|
1350
|
+
summary: record.summary || record.summaryValue || "",
|
|
1351
|
+
summaryMarkdown: record.summaryMarkdown || record.summary || record.summaryValue || "",
|
|
1352
|
+
metadata,
|
|
1353
|
+
query
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
function DefaultFigureTemplate({ record, thumbnailAspectRatio }) {
|
|
1358
|
+
if (!record) return null;
|
|
1359
|
+
const hasDims = Number.isFinite(Number(record.thumbnailWidth)) && Number(record.thumbnailWidth) > 0 && Number.isFinite(Number(record.thumbnailHeight)) && Number(record.thumbnailHeight) > 0;
|
|
1360
|
+
const aspect = Number.isFinite(Number(thumbnailAspectRatio)) && Number(thumbnailAspectRatio) > 0 ? Number(thumbnailAspectRatio) : hasDims ? Number(record.thumbnailWidth) / Number(record.thumbnailHeight) : void 0;
|
|
1361
|
+
return /* @__PURE__ */ React24.createElement(
|
|
1362
|
+
Card,
|
|
1363
|
+
{
|
|
1364
|
+
href: record.href,
|
|
1365
|
+
title: record.title || record.href,
|
|
1366
|
+
src: record.type === "work" ? record.thumbnail : void 0,
|
|
1367
|
+
imgWidth: record.thumbnailWidth,
|
|
1368
|
+
imgHeight: record.thumbnailHeight,
|
|
1369
|
+
aspectRatio: aspect
|
|
1370
|
+
}
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1224
1373
|
function SearchResults({
|
|
1225
1374
|
results = [],
|
|
1226
1375
|
type = "all",
|
|
1227
1376
|
layout = "grid",
|
|
1228
|
-
query = ""
|
|
1377
|
+
query = "",
|
|
1378
|
+
templates = {},
|
|
1379
|
+
variant = "auto"
|
|
1229
1380
|
}) {
|
|
1230
1381
|
if (!results.length) {
|
|
1231
1382
|
return /* @__PURE__ */ React24.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React24.createElement("em", null, "No results"));
|
|
1232
1383
|
}
|
|
1233
1384
|
const normalizedType = String(type || "all").toLowerCase();
|
|
1385
|
+
const normalizedVariant = variant === "figure" || variant === "article" ? variant : "auto";
|
|
1234
1386
|
const isAnnotationView = normalizedType === "annotation";
|
|
1387
|
+
const FigureTemplate = templates && templates.figure ? templates.figure : DefaultFigureTemplate;
|
|
1388
|
+
const ArticleTemplate = templates && templates.article ? templates.article : DefaultArticleTemplate;
|
|
1235
1389
|
if (isAnnotationView) {
|
|
1236
1390
|
return /* @__PURE__ */ React24.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
|
|
1237
1391
|
if (!r) return null;
|
|
1238
|
-
return
|
|
1392
|
+
return /* @__PURE__ */ React24.createElement(
|
|
1393
|
+
ArticleTemplate,
|
|
1394
|
+
{
|
|
1395
|
+
key: r.id || i,
|
|
1396
|
+
record: r,
|
|
1397
|
+
query,
|
|
1398
|
+
layout
|
|
1399
|
+
}
|
|
1400
|
+
);
|
|
1239
1401
|
}));
|
|
1240
1402
|
}
|
|
1241
|
-
const renderTextCard = (record, key) => {
|
|
1242
|
-
if (!record) return null;
|
|
1243
|
-
return /* @__PURE__ */ React24.createElement(
|
|
1244
|
-
TextCard,
|
|
1245
|
-
{
|
|
1246
|
-
key,
|
|
1247
|
-
href: record.href,
|
|
1248
|
-
title: record.title || record.href || "Untitled",
|
|
1249
|
-
annotation: record.annotation,
|
|
1250
|
-
summary: record.summary || record.summaryValue || "",
|
|
1251
|
-
metadata: Array.isArray(record.metadata) ? record.metadata : [],
|
|
1252
|
-
query
|
|
1253
|
-
}
|
|
1254
|
-
);
|
|
1255
|
-
};
|
|
1256
1403
|
const isWorkRecord = (record) => String(record && record.type).toLowerCase() === "work";
|
|
1257
|
-
const
|
|
1404
|
+
const shouldRenderAsArticle = (record) => {
|
|
1405
|
+
if (normalizedVariant === "article") return true;
|
|
1406
|
+
if (normalizedVariant === "figure") return false;
|
|
1407
|
+
return !isWorkRecord(record) || normalizedType !== "work";
|
|
1408
|
+
};
|
|
1258
1409
|
if (layout === "list") {
|
|
1259
|
-
return /* @__PURE__ */ React24.createElement("
|
|
1260
|
-
if (
|
|
1261
|
-
return /* @__PURE__ */ React24.createElement("
|
|
1410
|
+
return /* @__PURE__ */ React24.createElement("div", { id: "search-results", className: "space-y-6" }, results.map((r, i) => {
|
|
1411
|
+
if (shouldRenderAsArticle(r)) {
|
|
1412
|
+
return /* @__PURE__ */ React24.createElement("div", { key: i, className: `search-result ${r && r.type}` }, /* @__PURE__ */ React24.createElement(ArticleTemplate, { record: r, query, layout }));
|
|
1262
1413
|
}
|
|
1263
1414
|
const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
|
|
1264
1415
|
const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
|
|
1265
1416
|
return /* @__PURE__ */ React24.createElement(
|
|
1266
|
-
"
|
|
1417
|
+
"div",
|
|
1267
1418
|
{
|
|
1268
1419
|
key: i,
|
|
1269
1420
|
className: `search-result ${r.type}`,
|
|
1270
1421
|
"data-thumbnail-aspect-ratio": aspect
|
|
1271
1422
|
},
|
|
1272
1423
|
/* @__PURE__ */ React24.createElement(
|
|
1273
|
-
|
|
1424
|
+
FigureTemplate,
|
|
1274
1425
|
{
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
imgHeight: r.thumbnailHeight,
|
|
1280
|
-
aspectRatio: aspect
|
|
1426
|
+
record: r,
|
|
1427
|
+
query,
|
|
1428
|
+
layout,
|
|
1429
|
+
thumbnailAspectRatio: aspect
|
|
1281
1430
|
}
|
|
1282
1431
|
)
|
|
1283
1432
|
);
|
|
1284
1433
|
}));
|
|
1285
1434
|
}
|
|
1286
1435
|
return /* @__PURE__ */ React24.createElement("div", { id: "search-results" }, /* @__PURE__ */ React24.createElement(Grid, null, results.map((r, i) => {
|
|
1287
|
-
if (
|
|
1288
|
-
return /* @__PURE__ */ React24.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` },
|
|
1436
|
+
if (shouldRenderAsArticle(r)) {
|
|
1437
|
+
return /* @__PURE__ */ React24.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, /* @__PURE__ */ React24.createElement(ArticleTemplate, { record: r, query, layout }));
|
|
1289
1438
|
}
|
|
1290
1439
|
const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
|
|
1291
1440
|
const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
|
|
@@ -1297,14 +1446,12 @@ function SearchResults({
|
|
|
1297
1446
|
"data-thumbnail-aspect-ratio": aspect
|
|
1298
1447
|
},
|
|
1299
1448
|
/* @__PURE__ */ React24.createElement(
|
|
1300
|
-
|
|
1449
|
+
FigureTemplate,
|
|
1301
1450
|
{
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
imgHeight: r.thumbnailHeight,
|
|
1307
|
-
aspectRatio: aspect
|
|
1451
|
+
record: r,
|
|
1452
|
+
query,
|
|
1453
|
+
layout,
|
|
1454
|
+
thumbnailAspectRatio: aspect
|
|
1308
1455
|
}
|
|
1309
1456
|
)
|
|
1310
1457
|
);
|
|
@@ -1574,6 +1721,7 @@ function MarkdownTable({ className = "", ...rest }) {
|
|
|
1574
1721
|
return /* @__PURE__ */ React28.createElement("div", { className: "markdown-table__frame" }, /* @__PURE__ */ React28.createElement("table", { className: merged, ...rest }));
|
|
1575
1722
|
}
|
|
1576
1723
|
export {
|
|
1724
|
+
ArticleCard,
|
|
1577
1725
|
Button,
|
|
1578
1726
|
ButtonWrapper,
|
|
1579
1727
|
CanopyBrand,
|
|
@@ -1600,7 +1748,6 @@ export {
|
|
|
1600
1748
|
MdxSearchTabs as SearchTabs,
|
|
1601
1749
|
SearchTabs as SearchTabsUI,
|
|
1602
1750
|
Slider,
|
|
1603
|
-
TextCard,
|
|
1604
1751
|
Viewer
|
|
1605
1752
|
};
|
|
1606
1753
|
//# sourceMappingURL=index.mjs.map
|