@canopy-iiif/app 0.8.4 → 0.8.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.
@@ -173,7 +173,7 @@ async function buildSearchPage() {
173
173
  }
174
174
  } catch (_) {}
175
175
  let html = htmlShell({ title: 'Search', body, cssHref: null, scriptHref: jsRel, headExtra });
176
- try { html = require('./common').applyBaseToHtml(html); } catch (_) {}
176
+ try { html = require('../common').applyBaseToHtml(html); } catch (_) {}
177
177
  await fsp.writeFile(outPath, html, 'utf8');
178
178
  console.log('Search: Built', path.relative(process.cwd(), outPath));
179
179
  } catch (e) {
@@ -185,14 +185,66 @@ async function buildSearchPage() {
185
185
  function toSafeString(val, defaultValue = '') {
186
186
  try { return String(val == null ? defaultValue : val); } catch (_) { return defaultValue; }
187
187
  }
188
- function sanitizeRecord(r) {
188
+
189
+ function sanitizeMetadataValues(list) {
190
+ const arr = Array.isArray(list) ? list : [];
191
+ const out = [];
192
+ const seen = new Set();
193
+ for (const val of arr) {
194
+ if (val && typeof val === 'object' && Array.isArray(val.values)) {
195
+ for (const v of val.values) {
196
+ const str = toSafeString(v, '').trim();
197
+ if (!str) continue;
198
+ const clipped = str.length > 500 ? str.slice(0, 500) + '…' : str;
199
+ if (seen.has(clipped)) continue;
200
+ seen.add(clipped);
201
+ out.push(clipped);
202
+ }
203
+ continue;
204
+ }
205
+ const str = toSafeString(val, '').trim();
206
+ if (!str) continue;
207
+ const clipped = str.length > 500 ? str.slice(0, 500) + '…' : str;
208
+ if (seen.has(clipped)) continue;
209
+ seen.add(clipped);
210
+ out.push(clipped);
211
+ }
212
+ return out;
213
+ }
214
+
215
+ function sanitizeRecordForIndex(r) {
189
216
  const title = toSafeString(r && r.title, '');
190
- const hrefRaw = toSafeString(r && r.href, '');
191
- const href = rootRelativeHref(hrefRaw);
192
217
  const type = toSafeString(r && r.type, 'page');
193
- const thumbnail = toSafeString(r && r.thumbnail, '');
194
218
  const safeTitle = title.length > 300 ? title.slice(0, 300) + '…' : title;
195
- const out = { title: safeTitle, href, type };
219
+ const out = { title: safeTitle, type };
220
+ const metadataSource =
221
+ (r && r.metadataValues) ||
222
+ (r && r.searchMetadataValues) ||
223
+ (r && r.search && r.search.metadata) ||
224
+ [];
225
+ const metadata = sanitizeMetadataValues(metadataSource);
226
+ if (metadata.length) out.metadata = metadata;
227
+ const summaryVal = toSafeString(
228
+ (r && r.summaryValue) ||
229
+ (r && r.searchSummary) ||
230
+ (r && r.search && r.search.summary),
231
+ ''
232
+ ).trim();
233
+ if (summaryVal) {
234
+ const clipped = summaryVal.length > 1000 ? summaryVal.slice(0, 1000) + '…' : summaryVal;
235
+ out.summary = clipped;
236
+ }
237
+ return out;
238
+ }
239
+
240
+ function sanitizeRecordForDisplay(r) {
241
+ const base = sanitizeRecordForIndex(r);
242
+ const out = { ...base };
243
+ if (out.metadata) delete out.metadata;
244
+ if (out.summary) out.summary = toSafeString(out.summary, '');
245
+ const hrefRaw = toSafeString(r && r.href, '');
246
+ out.href = rootRelativeHref(hrefRaw);
247
+ const thumbnail = toSafeString(r && r.thumbnail, '');
196
248
  if (thumbnail) out.thumbnail = thumbnail;
197
249
  // Preserve optional thumbnail dimensions for aspect ratio calculations in the UI
198
250
  try {
@@ -204,28 +256,76 @@ function sanitizeRecord(r) {
204
256
  return out;
205
257
  }
206
258
 
259
+ /**
260
+ * Write search datasets consumed by the runtime layers.
261
+ *
262
+ * Outputs:
263
+ * - search-index.json: compact payload (title/href/type/metadata + stable id) for FlexSearch
264
+ * - search-records.json: richer display data (thumbnail/dimensions + id) for UI rendering
265
+ */
207
266
  async function writeSearchIndex(records) {
208
267
  const apiDir = path.join(OUT_DIR, 'api');
209
268
  ensureDirSync(apiDir);
210
269
  const idxPath = path.join(apiDir, 'search-index.json');
211
270
  const list = Array.isArray(records) ? records : [];
212
- const safe = list.map(sanitizeRecord);
213
- const json = JSON.stringify(safe, null, 2);
214
- const approxBytes = Buffer.byteLength(json, 'utf8');
271
+ const indexRecords = list.map(sanitizeRecordForIndex);
272
+ const displayRecords = list.map(sanitizeRecordForDisplay);
273
+ for (let i = 0; i < indexRecords.length; i += 1) {
274
+ const id = String(i);
275
+ if (indexRecords[i]) indexRecords[i].id = id;
276
+ if (displayRecords[i]) displayRecords[i].id = id;
277
+ if (displayRecords[i] && !displayRecords[i].href) {
278
+ const original = list[i];
279
+ const href = original && original.href ? rootRelativeHref(String(original.href)) : '';
280
+ if (href) displayRecords[i].href = href;
281
+ }
282
+ }
283
+ const annotationsPath = path.join(apiDir, 'search-index-annotations.json');
284
+ const annotationRecords = [];
285
+ for (let i = 0; i < list.length; i += 1) {
286
+ const raw = list[i];
287
+ const annotationVal = toSafeString(
288
+ (raw && raw.annotationValue) ||
289
+ (raw && raw.searchAnnotation) ||
290
+ (raw && raw.search && raw.search.annotation),
291
+ ''
292
+ ).trim();
293
+ if (!annotationVal) continue;
294
+ annotationRecords.push({ id: String(i), annotation: annotationVal });
295
+ }
296
+
297
+ const indexJson = JSON.stringify(indexRecords);
298
+ const approxBytes = Buffer.byteLength(indexJson, 'utf8');
215
299
  if (approxBytes > 10 * 1024 * 1024) {
216
300
  console.warn('Search: index size is large (', Math.round(approxBytes / (1024 * 1024)), 'MB ). Consider narrowing sources.');
217
301
  }
218
- await fsp.writeFile(idxPath, json, 'utf8');
302
+ await fsp.writeFile(idxPath, indexJson, 'utf8');
303
+
304
+ const displayPath = path.join(apiDir, 'search-records.json');
305
+ const displayJson = JSON.stringify(displayRecords);
306
+ const displayBytes = Buffer.byteLength(displayJson, 'utf8');
307
+ await fsp.writeFile(displayPath, displayJson, 'utf8');
308
+ let annotationsBytes = 0;
309
+ if (annotationRecords.length) {
310
+ const annotationsJson = JSON.stringify(annotationRecords);
311
+ annotationsBytes = Buffer.byteLength(annotationsJson, 'utf8');
312
+ await fsp.writeFile(annotationsPath, annotationsJson, 'utf8');
313
+ } else {
314
+ try {
315
+ if (fs.existsSync(annotationsPath)) await fsp.unlink(annotationsPath);
316
+ } catch (_) {}
317
+ }
219
318
  // Also write a small metadata file with a stable version hash for cache-busting and IDB keying
220
319
  try {
221
- const version = crypto.createHash('sha256').update(json).digest('hex');
320
+ const version = crypto.createHash('sha256').update(indexJson).digest('hex');
222
321
  // Read optional search tabs order from canopy.yml
223
322
  let tabsOrder = [];
224
323
  try {
225
324
  const yaml = require('js-yaml');
226
- const cfgPath = require('./common').path.resolve(process.env.CANOPY_CONFIG || 'canopy.yml');
227
- if (require('./common').fs.existsSync(cfgPath)) {
228
- const raw = require('./common').fs.readFileSync(cfgPath, 'utf8');
325
+ const common = require('../common');
326
+ const cfgPath = common.path.resolve(process.env.CANOPY_CONFIG || 'canopy.yml');
327
+ if (common.fs.existsSync(cfgPath)) {
328
+ const raw = common.fs.readFileSync(cfgPath, 'utf8');
229
329
  const data = yaml.load(raw) || {};
230
330
  const searchCfg = data && data.search ? data.search : {};
231
331
  const tabs = searchCfg && searchCfg.tabs ? searchCfg.tabs : {};
@@ -235,17 +335,25 @@ async function writeSearchIndex(records) {
235
335
  } catch (_) {}
236
336
  const meta = {
237
337
  version,
238
- records: safe.length,
338
+ records: indexRecords.length,
239
339
  bytes: approxBytes,
240
340
  updatedAt: new Date().toISOString(),
241
341
  // Expose optional search config to the client runtime
242
- search: { tabs: { order: tabsOrder } },
342
+ search: {
343
+ tabs: { order: tabsOrder },
344
+ assets: {
345
+ display: { path: 'search-records.json', bytes: displayBytes },
346
+ ...(annotationRecords.length
347
+ ? { annotations: { path: 'search-index-annotations.json', bytes: annotationsBytes } }
348
+ : {}),
349
+ },
350
+ },
243
351
  };
244
352
  const metaPath = path.join(apiDir, 'index.json');
245
353
  await fsp.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf8');
246
354
  try {
247
355
  const { logLine } = require('./log');
248
- logLine(`✓ Search index version ${version.slice(0, 8)} (${safe.length} records)`, 'cyan');
356
+ logLine(`✓ Search index version ${version.slice(0, 8)} (${indexRecords.length} records)`, 'cyan');
249
357
  } catch (_) {}
250
358
  // Propagate version into IIIF cache index for a single, shared build identifier
251
359
  try {
@@ -263,7 +371,11 @@ async function writeSearchIndex(records) {
263
371
 
264
372
  // Compatibility: keep ensureResultTemplate as a no-op builder (template unused by React search)
265
373
  async function ensureResultTemplate() {
266
- try { const { path } = require('./common'); const p = path.join(OUT_DIR, 'search-result.html'); await fsp.writeFile(p, '', 'utf8'); } catch (_) {}
374
+ try {
375
+ const { path } = require('../common');
376
+ const p = path.join(OUT_DIR, 'search-result.html');
377
+ await fsp.writeFile(p, '', 'utf8');
378
+ } catch (_) {}
267
379
  }
268
380
 
269
381
  module.exports = { ensureSearchRuntime, ensureResultTemplate, buildSearchPage, writeSearchIndex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
@@ -14,6 +14,7 @@
14
14
  "./ui/styles/index.css": "./ui/styles/index.css",
15
15
  "./ui/canopy-iiif-plugin": "./ui/tailwind-canopy-iiif-plugin.js",
16
16
  "./ui/canopy-iiif-preset": "./ui/tailwind-canopy-iiif-preset.js",
17
+ "./ui/theme": "./ui/theme.js",
17
18
  "./lib/components/*": "./lib/components/*",
18
19
  "./head": "./lib/head.js",
19
20
  "./orchestrator": {
@@ -28,12 +29,14 @@
28
29
  "ui/styles/**",
29
30
  "ui/tailwind-canopy-iiif-plugin.js",
30
31
  "ui/tailwind-canopy-iiif-preset.js",
32
+ "ui/theme.js",
31
33
  "types/**"
32
34
  ],
33
35
  "publishConfig": {
34
36
  "access": "public"
35
37
  },
36
38
  "dependencies": {
39
+ "@radix-ui/colors": "^3.0.0",
37
40
  "@iiif/helpers": "^1.4.0",
38
41
  "@mdx-js/mdx": "^3.1.0",
39
42
  "@mdx-js/react": "^3.1.0",