@canopy-iiif/app 0.9.0 → 0.9.2

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 CHANGED
@@ -139,6 +139,67 @@ async function loadUiComponents() {
139
139
  return UI_COMPONENTS;
140
140
  }
141
141
 
142
+ function slugifyHeading(text) {
143
+ try {
144
+ return String(text || '')
145
+ .toLowerCase()
146
+ .trim()
147
+ .replace(/[^a-z0-9\s-]/g, '')
148
+ .replace(/\s+/g, '-');
149
+ } catch (_) {
150
+ return '';
151
+ }
152
+ }
153
+
154
+ function extractHeadings(mdxSource) {
155
+ const { content } = parseFrontmatter(String(mdxSource || ''));
156
+ const cleaned = content.replace(/```[\s\S]*?```/g, '');
157
+ const headingRegex = /^ {0,3}(#{1,6})\s+(.+?)\s*$/gm;
158
+ const seen = new Map();
159
+ const headings = [];
160
+ let match;
161
+ while ((match = headingRegex.exec(cleaned))) {
162
+ const hashes = match[1] || '';
163
+ const depth = hashes.length;
164
+ let raw = match[2] || '';
165
+ let explicitId = null;
166
+ const idMatch = raw.match(/\s*\{#([A-Za-z0-9_-]+)\}\s*$/);
167
+ if (idMatch) {
168
+ explicitId = idMatch[1];
169
+ raw = raw.slice(0, raw.length - idMatch[0].length);
170
+ }
171
+ const title = raw.replace(/\<[^>]*\>/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').trim();
172
+ if (!title) continue;
173
+ const baseId = explicitId || slugifyHeading(title) || `section-${headings.length + 1}`;
174
+ const count = seen.get(baseId) || 0;
175
+ seen.set(baseId, count + 1);
176
+ const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
177
+ headings.push({
178
+ id,
179
+ title,
180
+ depth,
181
+ });
182
+ }
183
+ return headings;
184
+ }
185
+
186
+ function extractPlainText(mdxSource) {
187
+ let { content } = parseFrontmatter(String(mdxSource || ''));
188
+ if (!content) return '';
189
+ content = content.replace(/```[\s\S]*?```/g, ' ');
190
+ content = content.replace(/`{1,3}([^`]+)`{1,3}/g, '$1');
191
+ content = content.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ');
192
+ content = content.replace(/\[[^\]]*\]\([^)]*\)/g, '$1');
193
+ content = content.replace(/<[^>]+>/g, ' ');
194
+ content = content.replace(/\{#([^}]+)\}/g, ' ');
195
+ content = content.replace(/\{\/[A-Za-z0-9_.-]+\}/g, ' ');
196
+ content = content.replace(/\{[^{}]*\}/g, ' ');
197
+ content = content.replace(/[#>*~_\-]+/g, ' ');
198
+ content = content.replace(/\n+/g, ' ');
199
+ content = content.replace(/\s+/g, ' ').trim();
200
+ return content;
201
+ }
202
+
142
203
  function extractTitle(mdxSource) {
143
204
  const { data, content } = parseFrontmatter(String(mdxSource || ""));
144
205
  if (data && typeof data.title === "string" && data.title.trim()) {
@@ -271,6 +332,84 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
271
332
  const mod = await import(pathToFileURL(tmpFile).href + bust);
272
333
  const MDXContent = mod.default || mod.MDXContent || mod;
273
334
  const components = await loadUiComponents();
335
+ const rawHeadings = Array.isArray(extraProps && extraProps.page && extraProps.page.headings)
336
+ ? extraProps.page.headings
337
+ .map((heading) => (heading ? { ...heading } : heading))
338
+ .filter(Boolean)
339
+ : [];
340
+ const headingQueue = rawHeadings.slice();
341
+ const headingIdCounts = new Map();
342
+ headingQueue.forEach((heading) => {
343
+ if (heading && heading.id) {
344
+ const key = String(heading.id);
345
+ headingIdCounts.set(key, (headingIdCounts.get(key) || 0) + 1);
346
+ }
347
+ });
348
+
349
+ function reserveHeadingId(base) {
350
+ const fallback = base || 'section';
351
+ let candidate = fallback;
352
+ let attempt = 1;
353
+ while (headingIdCounts.has(candidate)) {
354
+ attempt += 1;
355
+ candidate = `${fallback}-${attempt}`;
356
+ }
357
+ headingIdCounts.set(candidate, 1);
358
+ return candidate;
359
+ }
360
+
361
+ function extractTextFromChildren(children) {
362
+ if (children == null) return '';
363
+ if (typeof children === 'string' || typeof children === 'number') return String(children);
364
+ if (Array.isArray(children)) return children.map((child) => extractTextFromChildren(child)).join('');
365
+ if (React.isValidElement(children)) return extractTextFromChildren(children.props && children.props.children);
366
+ return '';
367
+ }
368
+
369
+ function takeHeading(level, children) {
370
+ if (!headingQueue.length) return null;
371
+ const idx = headingQueue.findIndex((item) => {
372
+ if (!item || typeof item !== 'object') return false;
373
+ const depth = typeof item.depth === 'number' ? item.depth : item.level;
374
+ return depth === level;
375
+ });
376
+ if (idx === -1) return null;
377
+ const [heading] = headingQueue.splice(idx, 1);
378
+ if (!heading.id) {
379
+ const text = heading.title || extractTextFromChildren(children);
380
+ const baseId = slugifyHeading(text);
381
+ heading.id = reserveHeadingId(baseId);
382
+ }
383
+ if (!heading.title) {
384
+ heading.title = extractTextFromChildren(children);
385
+ }
386
+ return heading;
387
+ }
388
+
389
+ function createHeadingComponent(level) {
390
+ const tag = `h${level}`;
391
+ const Base = components && components[tag] ? components[tag] : tag;
392
+ return function HeadingComponent(props) {
393
+ const heading = takeHeading(level, props && props.children);
394
+ const id = props && props.id ? props.id : heading && heading.id;
395
+ const finalProps = id ? { ...props, id } : props;
396
+ return React.createElement(Base, finalProps, props && props.children);
397
+ };
398
+ }
399
+
400
+ const levelsPresent = Array.from(
401
+ new Set(
402
+ headingQueue
403
+ .map((heading) => (heading ? heading.depth || heading.level : null))
404
+ .filter((level) => typeof level === 'number' && level >= 1 && level <= 6)
405
+ )
406
+ );
407
+ const headingComponents = levelsPresent.length
408
+ ? levelsPresent.reduce((acc, level) => {
409
+ acc[`h${level}`] = createHeadingComponent(level);
410
+ return acc;
411
+ }, {})
412
+ : {};
274
413
  const MDXProvider = await getMdxProvider();
275
414
  // Base path support for anchors
276
415
  const Anchor = function A(props) {
@@ -294,7 +433,7 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
294
433
  ? React.createElement(PageContext.Provider, { value: contextValue }, withLayout)
295
434
  : withLayout;
296
435
  const withApp = React.createElement(app.App, null, withContext);
297
- const compMap = { ...components, a: Anchor };
436
+ const compMap = { ...components, ...headingComponents, a: Anchor };
298
437
  const page = MDXProvider
299
438
  ? React.createElement(MDXProvider, { components: compMap }, withApp)
300
439
  : withApp;
@@ -811,6 +950,8 @@ async function ensureHeroRuntime() {
811
950
 
812
951
  module.exports = {
813
952
  extractTitle,
953
+ extractHeadings,
954
+ extractPlainText,
814
955
  isReservedFile,
815
956
  parseFrontmatter,
816
957
  compileMdxFile,
@@ -50,6 +50,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
50
50
  const pageInfo = navigation.getPageInfo(normalizedRel);
51
51
  const navData = navigation.buildNavigationForFile(normalizedRel);
52
52
  const mergedProps = { ...(extraProps || {}) };
53
+ const headings = mdx.extractHeadings(source);
53
54
  if (pageInfo) {
54
55
  mergedProps.page = mergedProps.page
55
56
  ? { ...pageInfo, ...mergedProps.page }
@@ -58,6 +59,11 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
58
59
  if (navData && !mergedProps.navigation) {
59
60
  mergedProps.navigation = navData;
60
61
  }
62
+ if (headings && headings.length) {
63
+ mergedProps.page = mergedProps.page
64
+ ? { ...mergedProps.page, headings }
65
+ : { headings };
66
+ }
61
67
  const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, mergedProps);
62
68
  const needsHydrateViewer =
63
69
  body.includes('data-canopy-viewer') || body.includes('data-canopy-scroll');
@@ -6,11 +6,16 @@ function pagesToRecords(pageRecords) {
6
6
  const list = Array.isArray(pageRecords) ? pageRecords : [];
7
7
  return list
8
8
  .filter((p) => p && p.href && p.searchInclude)
9
- .map((p) => ({
10
- title: p.title || p.href,
11
- href: rootRelativeHref(p.href),
12
- type: p.searchType || 'page',
13
- }));
9
+ .map((p) => {
10
+ const summary = typeof p.searchSummary === 'string' ? p.searchSummary.trim() : '';
11
+ const record = {
12
+ title: p.title || p.href,
13
+ href: rootRelativeHref(p.href),
14
+ type: p.searchType || 'page',
15
+ };
16
+ if (summary) record.summaryValue = summary;
17
+ return record;
18
+ });
14
19
  }
15
20
 
16
21
  function maybeMockRecords() {
@@ -184,6 +184,8 @@ async function collectMdxPageRecords() {
184
184
  const rel = path.relative(CONTENT_DIR, p).replace(/\.mdx$/i, '.html');
185
185
  if (base !== 'sitemap.mdx') {
186
186
  const href = rootRelativeHref(rel.split(path.sep).join('/'));
187
+ const plainText = mdx.extractPlainText(src);
188
+ const summary = plainText || '';
187
189
  const underSearch = /^search\//i.test(href) || href.toLowerCase() === 'search.html';
188
190
  let include = !underSearch;
189
191
  let resolvedType = null;
@@ -201,7 +203,13 @@ async function collectMdxPageRecords() {
201
203
  resolvedType = 'page';
202
204
  }
203
205
  const trimmedType = resolvedType && String(resolvedType).trim();
204
- pages.push({ title, href, searchInclude: include && !!trimmedType, searchType: trimmedType || undefined });
206
+ pages.push({
207
+ title,
208
+ href,
209
+ searchInclude: include && !!trimmedType,
210
+ searchType: trimmedType || undefined,
211
+ searchSummary: summary,
212
+ });
205
213
  }
206
214
  }
207
215
  }
@@ -231,8 +231,7 @@ function sanitizeRecordForIndex(r) {
231
231
  ''
232
232
  ).trim();
233
233
  if (summaryVal) {
234
- const clipped = summaryVal.length > 1000 ? summaryVal.slice(0, 1000) + '…' : summaryVal;
235
- out.summary = clipped;
234
+ out.summary = summaryVal;
236
235
  }
237
236
  return out;
238
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
@@ -19,6 +19,7 @@
19
19
  "./head": "./lib/head.js",
20
20
  "./orchestrator": {
21
21
  "types": "./types/orchestrator.d.ts",
22
+ "import": "./lib/orchestrator.js",
22
23
  "require": "./lib/orchestrator.js",
23
24
  "default": "./lib/orchestrator.js"
24
25
  }
package/ui/dist/index.mjs CHANGED
@@ -104,7 +104,7 @@ function Card({
104
104
  );
105
105
  }
106
106
 
107
- // ui/src/layout/AnnotationCard.jsx
107
+ // ui/src/layout/TextCard.jsx
108
108
  import React3, { useMemo } from "react";
109
109
  function escapeRegExp(str = "") {
110
110
  return String(str).replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
@@ -141,7 +141,7 @@ function highlightSnippet(snippet, query) {
141
141
  (part, idx) => part.toLowerCase() === termLower ? /* @__PURE__ */ React3.createElement("mark", { key: idx }, part) : /* @__PURE__ */ React3.createElement(React3.Fragment, { key: idx }, part)
142
142
  );
143
143
  }
144
- function AnnotationCard({
144
+ function TextCard({
145
145
  href = "#",
146
146
  title = "Untitled",
147
147
  annotation = "",
@@ -428,26 +428,36 @@ function SearchResults({
428
428
  if (!results.length) {
429
429
  return /* @__PURE__ */ React12.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React12.createElement("em", null, "No results"));
430
430
  }
431
- const isAnnotationView = String(type).toLowerCase() === "annotation";
431
+ const normalizedType = String(type || "all").toLowerCase();
432
+ const isAnnotationView = normalizedType === "annotation";
432
433
  if (isAnnotationView) {
433
434
  return /* @__PURE__ */ React12.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
434
- if (!r || !r.annotation) return null;
435
- return /* @__PURE__ */ React12.createElement(
436
- AnnotationCard,
437
- {
438
- key: r.id || i,
439
- href: r.href,
440
- title: r.title || r.href || "Untitled",
441
- annotation: r.annotation,
442
- summary: r.summary,
443
- metadata: Array.isArray(r.metadata) ? r.metadata : [],
444
- query
445
- }
446
- );
435
+ if (!r) return null;
436
+ return renderTextCard(r, r.id || i);
447
437
  }));
448
438
  }
439
+ const renderTextCard = (record, key) => {
440
+ if (!record) return null;
441
+ return /* @__PURE__ */ React12.createElement(
442
+ TextCard,
443
+ {
444
+ key,
445
+ href: record.href,
446
+ title: record.title || record.href || "Untitled",
447
+ annotation: record.annotation,
448
+ summary: record.summary || record.summaryValue || "",
449
+ metadata: Array.isArray(record.metadata) ? record.metadata : [],
450
+ query
451
+ }
452
+ );
453
+ };
454
+ const isWorkRecord = (record) => String(record && record.type).toLowerCase() === "work";
455
+ const shouldRenderAsTextCard = (record) => !isWorkRecord(record) || normalizedType !== "work";
449
456
  if (layout === "list") {
450
457
  return /* @__PURE__ */ React12.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
458
+ if (shouldRenderAsTextCard(r)) {
459
+ return /* @__PURE__ */ React12.createElement("li", { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
460
+ }
451
461
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
452
462
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
453
463
  return /* @__PURE__ */ React12.createElement(
@@ -472,6 +482,9 @@ function SearchResults({
472
482
  }));
473
483
  }
474
484
  return /* @__PURE__ */ React12.createElement("div", { id: "search-results" }, /* @__PURE__ */ React12.createElement(Grid, null, results.map((r, i) => {
485
+ if (shouldRenderAsTextCard(r)) {
486
+ return /* @__PURE__ */ React12.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
487
+ }
475
488
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
476
489
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
477
490
  return /* @__PURE__ */ React12.createElement(
@@ -957,7 +970,6 @@ function SearchPanel(props = {}) {
957
970
  return /* @__PURE__ */ React19.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React19.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React19.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React19.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React19.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
958
971
  }
959
972
  export {
960
- AnnotationCard,
961
973
  Card,
962
974
  Grid,
963
975
  GridItem,
@@ -975,6 +987,7 @@ export {
975
987
  MdxSearchTabs as SearchTabs,
976
988
  SearchTabs as SearchTabsUI,
977
989
  Slider,
990
+ TextCard,
978
991
  Viewer
979
992
  };
980
993
  //# sourceMappingURL=index.mjs.map