commonmeta-ruby 3.7.2 → 3.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish.yml +23 -0
  3. data/Gemfile.lock +23 -23
  4. data/bin/commonmeta +1 -1
  5. data/docs/.gitignore +1 -0
  6. data/docs/_publish.yml +4 -0
  7. data/docs/_quarto.yml +46 -0
  8. data/docs/_site/favicon.ico +0 -0
  9. data/docs/_site/images/icon.png +0 -0
  10. data/docs/_site/index.html +947 -0
  11. data/docs/_site/readers/bibtex_reader.html +802 -0
  12. data/docs/_site/search.json +74 -0
  13. data/docs/_site/site_libs/bootstrap/bootstrap-dark.min.css +9 -0
  14. data/docs/_site/site_libs/bootstrap/bootstrap-icons.css +2078 -0
  15. data/docs/_site/site_libs/bootstrap/bootstrap-icons.woff +0 -0
  16. data/docs/_site/site_libs/bootstrap/bootstrap.min.css +9 -0
  17. data/docs/_site/site_libs/bootstrap/bootstrap.min.js +7 -0
  18. data/docs/_site/site_libs/clipboard/clipboard.min.js +7 -0
  19. data/docs/_site/site_libs/quarto-html/anchor.min.js +9 -0
  20. data/docs/_site/site_libs/quarto-html/popper.min.js +6 -0
  21. data/docs/_site/site_libs/quarto-html/quarto-syntax-highlighting-dark.css +179 -0
  22. data/docs/_site/site_libs/quarto-html/quarto-syntax-highlighting.css +179 -0
  23. data/docs/_site/site_libs/quarto-html/quarto.js +889 -0
  24. data/docs/_site/site_libs/quarto-html/tippy.css +1 -0
  25. data/docs/_site/site_libs/quarto-html/tippy.umd.min.js +2 -0
  26. data/docs/_site/site_libs/quarto-nav/headroom.min.js +7 -0
  27. data/docs/_site/site_libs/quarto-nav/quarto-nav.js +288 -0
  28. data/docs/_site/site_libs/quarto-search/autocomplete.umd.js +3 -0
  29. data/docs/_site/site_libs/quarto-search/fuse.min.js +9 -0
  30. data/docs/_site/site_libs/quarto-search/quarto-search.js +1241 -0
  31. data/docs/_site/utils/doi_utils.html +787 -0
  32. data/docs/_site/writers/bibtex_writer.html +803 -0
  33. data/docs/favicon.ico +0 -0
  34. data/docs/images/icon.png +0 -0
  35. data/docs/index.qmd +83 -0
  36. data/docs/readers/bibtex_reader.ipynb +37 -0
  37. data/docs/theme.scss +7 -0
  38. data/docs/utils/doi_utils.ipynb +28 -0
  39. data/docs/writers/bibtex_writer.ipynb +39 -0
  40. data/lib/commonmeta/cli.rb +5 -5
  41. data/lib/commonmeta/readers/json_feed_reader.rb +4 -4
  42. data/lib/commonmeta/version.rb +1 -1
  43. data/spec/cli_spec.rb +2 -2
  44. data/spec/fixtures/vcr_cassettes/Commonmeta_CLI/json_feed/json_feed_blog_slug.yml +71 -0
  45. data/spec/readers/json_feed_reader_spec.rb +2 -2
  46. metadata +40 -3
@@ -0,0 +1,1241 @@
1
+ const kQueryArg = "q";
2
+ const kResultsArg = "show-results";
3
+
4
+ // If items don't provide a URL, then both the navigator and the onSelect
5
+ // function aren't called (and therefore, the default implementation is used)
6
+ //
7
+ // We're using this sentinel URL to signal to those handlers that this
8
+ // item is a more item (along with the type) and can be handled appropriately
9
+ const kItemTypeMoreHref = "0767FDFD-0422-4E5A-BC8A-3BE11E5BBA05";
10
+
11
+ window.document.addEventListener("DOMContentLoaded", function (_event) {
12
+ // Ensure that search is available on this page. If it isn't,
13
+ // should return early and not do anything
14
+ var searchEl = window.document.getElementById("quarto-search");
15
+ if (!searchEl) return;
16
+
17
+ const { autocomplete } = window["@algolia/autocomplete-js"];
18
+
19
+ let quartoSearchOptions = {};
20
+ let language = {};
21
+ const searchOptionEl = window.document.getElementById(
22
+ "quarto-search-options"
23
+ );
24
+ if (searchOptionEl) {
25
+ const jsonStr = searchOptionEl.textContent;
26
+ quartoSearchOptions = JSON.parse(jsonStr);
27
+ language = quartoSearchOptions.language;
28
+ }
29
+
30
+ // note the search mode
31
+ if (quartoSearchOptions.type === "overlay") {
32
+ searchEl.classList.add("type-overlay");
33
+ } else {
34
+ searchEl.classList.add("type-textbox");
35
+ }
36
+
37
+ // Used to determine highlighting behavior for this page
38
+ // A `q` query param is expected when the user follows a search
39
+ // to this page
40
+ const currentUrl = new URL(window.location);
41
+ const query = currentUrl.searchParams.get(kQueryArg);
42
+ const showSearchResults = currentUrl.searchParams.get(kResultsArg);
43
+ const mainEl = window.document.querySelector("main");
44
+
45
+ // highlight matches on the page
46
+ if (query && mainEl) {
47
+ // perform any highlighting
48
+ highlight(escapeRegExp(query), mainEl);
49
+
50
+ // fix up the URL to remove the q query param
51
+ const replacementUrl = new URL(window.location);
52
+ replacementUrl.searchParams.delete(kQueryArg);
53
+ window.history.replaceState({}, "", replacementUrl);
54
+ }
55
+
56
+ // function to clear highlighting on the page when the search query changes
57
+ // (e.g. if the user edits the query or clears it)
58
+ let highlighting = true;
59
+ const resetHighlighting = (searchTerm) => {
60
+ if (mainEl && highlighting && query && searchTerm !== query) {
61
+ clearHighlight(query, mainEl);
62
+ highlighting = false;
63
+ }
64
+ };
65
+
66
+ // Clear search highlighting when the user scrolls sufficiently
67
+ const resetFn = () => {
68
+ resetHighlighting("");
69
+ window.removeEventListener("quarto-hrChanged", resetFn);
70
+ window.removeEventListener("quarto-sectionChanged", resetFn);
71
+ };
72
+
73
+ // Register this event after the initial scrolling and settling of events
74
+ // on the page
75
+ window.addEventListener("quarto-hrChanged", resetFn);
76
+ window.addEventListener("quarto-sectionChanged", resetFn);
77
+
78
+ // Responsively switch to overlay mode if the search is present on the navbar
79
+ // Note that switching the sidebar to overlay mode requires more coordinate (not just
80
+ // the media query since we generate different HTML for sidebar overlays than we do
81
+ // for sidebar input UI)
82
+ const detachedMediaQuery =
83
+ quartoSearchOptions.type === "overlay" ? "all" : "(max-width: 991px)";
84
+
85
+ // If configured, include the analytics client to send insights
86
+ const plugins = configurePlugins(quartoSearchOptions);
87
+
88
+ let lastState = null;
89
+ const { setIsOpen, setQuery, setCollections } = autocomplete({
90
+ container: searchEl,
91
+ detachedMediaQuery: detachedMediaQuery,
92
+ defaultActiveItemId: 0,
93
+ panelContainer: "#quarto-search-results",
94
+ panelPlacement: quartoSearchOptions["panel-placement"],
95
+ debug: false,
96
+ openOnFocus: true,
97
+ plugins,
98
+ classNames: {
99
+ form: "d-flex",
100
+ },
101
+ translations: {
102
+ clearButtonTitle: language["search-clear-button-title"],
103
+ detachedCancelButtonText: language["search-detached-cancel-button-title"],
104
+ submitButtonTitle: language["search-submit-button-title"],
105
+ },
106
+ initialState: {
107
+ query,
108
+ },
109
+ getItemUrl({ item }) {
110
+ return item.href;
111
+ },
112
+ onStateChange({ state }) {
113
+ // If this is a file URL, note that
114
+
115
+ // Perhaps reset highlighting
116
+ resetHighlighting(state.query);
117
+
118
+ // If the panel just opened, ensure the panel is positioned properly
119
+ if (state.isOpen) {
120
+ if (lastState && !lastState.isOpen) {
121
+ setTimeout(() => {
122
+ positionPanel(quartoSearchOptions["panel-placement"]);
123
+ }, 150);
124
+ }
125
+ }
126
+
127
+ // Perhaps show the copy link
128
+ showCopyLink(state.query, quartoSearchOptions);
129
+
130
+ lastState = state;
131
+ },
132
+ reshape({ sources, state }) {
133
+ return sources.map((source) => {
134
+ try {
135
+ const items = source.getItems();
136
+
137
+ // Validate the items
138
+ validateItems(items);
139
+
140
+ // group the items by document
141
+ const groupedItems = new Map();
142
+ items.forEach((item) => {
143
+ const hrefParts = item.href.split("#");
144
+ const baseHref = hrefParts[0];
145
+ const isDocumentItem = hrefParts.length === 1;
146
+
147
+ const items = groupedItems.get(baseHref);
148
+ if (!items) {
149
+ groupedItems.set(baseHref, [item]);
150
+ } else {
151
+ // If the href for this item matches the document
152
+ // exactly, place this item first as it is the item that represents
153
+ // the document itself
154
+ if (isDocumentItem) {
155
+ items.unshift(item);
156
+ } else {
157
+ items.push(item);
158
+ }
159
+ groupedItems.set(baseHref, items);
160
+ }
161
+ });
162
+
163
+ const reshapedItems = [];
164
+ let count = 1;
165
+ for (const [_key, value] of groupedItems) {
166
+ const firstItem = value[0];
167
+ reshapedItems.push({
168
+ ...firstItem,
169
+ type: kItemTypeDoc,
170
+ });
171
+
172
+ const collapseMatches = quartoSearchOptions["collapse-after"];
173
+ const collapseCount =
174
+ typeof collapseMatches === "number" ? collapseMatches : 1;
175
+
176
+ if (value.length > 1) {
177
+ const target = `search-more-${count}`;
178
+ const isExpanded =
179
+ state.context.expanded &&
180
+ state.context.expanded.includes(target);
181
+
182
+ const remainingCount = value.length - collapseCount;
183
+
184
+ for (let i = 1; i < value.length; i++) {
185
+ if (collapseMatches && i === collapseCount) {
186
+ reshapedItems.push({
187
+ target,
188
+ title: isExpanded
189
+ ? language["search-hide-matches-text"]
190
+ : remainingCount === 1
191
+ ? `${remainingCount} ${language["search-more-match-text"]}`
192
+ : `${remainingCount} ${language["search-more-matches-text"]}`,
193
+ type: kItemTypeMore,
194
+ href: kItemTypeMoreHref,
195
+ });
196
+ }
197
+
198
+ if (isExpanded || !collapseMatches || i < collapseCount) {
199
+ reshapedItems.push({
200
+ ...value[i],
201
+ type: kItemTypeItem,
202
+ target,
203
+ });
204
+ }
205
+ }
206
+ }
207
+ count += 1;
208
+ }
209
+
210
+ return {
211
+ ...source,
212
+ getItems() {
213
+ return reshapedItems;
214
+ },
215
+ };
216
+ } catch (error) {
217
+ // Some form of error occurred
218
+ return {
219
+ ...source,
220
+ getItems() {
221
+ return [
222
+ {
223
+ title: error.name || "An Error Occurred While Searching",
224
+ text:
225
+ error.message ||
226
+ "An unknown error occurred while attempting to perform the requested search.",
227
+ type: kItemTypeError,
228
+ },
229
+ ];
230
+ },
231
+ };
232
+ }
233
+ });
234
+ },
235
+ navigator: {
236
+ navigate({ itemUrl }) {
237
+ if (itemUrl !== offsetURL(kItemTypeMoreHref)) {
238
+ window.location.assign(itemUrl);
239
+ }
240
+ },
241
+ navigateNewTab({ itemUrl }) {
242
+ if (itemUrl !== offsetURL(kItemTypeMoreHref)) {
243
+ const windowReference = window.open(itemUrl, "_blank", "noopener");
244
+ if (windowReference) {
245
+ windowReference.focus();
246
+ }
247
+ }
248
+ },
249
+ navigateNewWindow({ itemUrl }) {
250
+ if (itemUrl !== offsetURL(kItemTypeMoreHref)) {
251
+ window.open(itemUrl, "_blank", "noopener");
252
+ }
253
+ },
254
+ },
255
+ getSources({ state, setContext, setActiveItemId, refresh }) {
256
+ return [
257
+ {
258
+ sourceId: "documents",
259
+ getItemUrl({ item }) {
260
+ if (item.href) {
261
+ return offsetURL(item.href);
262
+ } else {
263
+ return undefined;
264
+ }
265
+ },
266
+ onSelect({
267
+ item,
268
+ state,
269
+ setContext,
270
+ setIsOpen,
271
+ setActiveItemId,
272
+ refresh,
273
+ }) {
274
+ if (item.type === kItemTypeMore) {
275
+ toggleExpanded(item, state, setContext, setActiveItemId, refresh);
276
+
277
+ // Toggle more
278
+ setIsOpen(true);
279
+ }
280
+ },
281
+ getItems({ query }) {
282
+ if (query === null || query === "") {
283
+ return [];
284
+ }
285
+
286
+ const limit = quartoSearchOptions.limit;
287
+ if (quartoSearchOptions.algolia) {
288
+ return algoliaSearch(query, limit, quartoSearchOptions.algolia);
289
+ } else {
290
+ // Fuse search options
291
+ const fuseSearchOptions = {
292
+ isCaseSensitive: false,
293
+ shouldSort: true,
294
+ minMatchCharLength: 2,
295
+ limit: limit,
296
+ };
297
+
298
+ return readSearchData().then(function (fuse) {
299
+ return fuseSearch(query, fuse, fuseSearchOptions);
300
+ });
301
+ }
302
+ },
303
+ templates: {
304
+ noResults({ createElement }) {
305
+ const hasQuery = lastState.query;
306
+
307
+ return createElement(
308
+ "div",
309
+ {
310
+ class: `quarto-search-no-results${
311
+ hasQuery ? "" : " no-query"
312
+ }`,
313
+ },
314
+ language["search-no-results-text"]
315
+ );
316
+ },
317
+ header({ items, createElement }) {
318
+ // count the documents
319
+ const count = items.filter((item) => {
320
+ return item.type === kItemTypeDoc;
321
+ }).length;
322
+
323
+ if (count > 0) {
324
+ return createElement(
325
+ "div",
326
+ { class: "search-result-header" },
327
+ `${count} ${language["search-matching-documents-text"]}`
328
+ );
329
+ } else {
330
+ return createElement(
331
+ "div",
332
+ { class: "search-result-header-no-results" },
333
+ ``
334
+ );
335
+ }
336
+ },
337
+ footer({ _items, createElement }) {
338
+ if (
339
+ quartoSearchOptions.algolia &&
340
+ quartoSearchOptions.algolia["show-logo"]
341
+ ) {
342
+ const libDir = quartoSearchOptions.algolia["libDir"];
343
+ const logo = createElement("img", {
344
+ src: offsetURL(
345
+ `${libDir}/quarto-search/search-by-algolia.svg`
346
+ ),
347
+ class: "algolia-search-logo",
348
+ });
349
+ return createElement(
350
+ "a",
351
+ { href: "http://www.algolia.com/" },
352
+ logo
353
+ );
354
+ }
355
+ },
356
+
357
+ item({ item, createElement }) {
358
+ return renderItem(
359
+ item,
360
+ createElement,
361
+ state,
362
+ setActiveItemId,
363
+ setContext,
364
+ refresh,
365
+ quartoSearchOptions
366
+ );
367
+ },
368
+ },
369
+ },
370
+ ];
371
+ },
372
+ });
373
+
374
+ window.quartoOpenSearch = () => {
375
+ setIsOpen(false);
376
+ setIsOpen(true);
377
+ focusSearchInput();
378
+ };
379
+
380
+ document.addEventListener("keyup", (event) => {
381
+ const { key } = event;
382
+ const kbds = quartoSearchOptions["keyboard-shortcut"];
383
+ const focusedEl = document.activeElement;
384
+
385
+ const isFormElFocused = [
386
+ "input",
387
+ "select",
388
+ "textarea",
389
+ "button",
390
+ "option",
391
+ ].find((tag) => {
392
+ return focusedEl.tagName.toLowerCase() === tag;
393
+ });
394
+
395
+ if (kbds && kbds.includes(key) && !isFormElFocused) {
396
+ event.preventDefault();
397
+ window.quartoOpenSearch();
398
+ }
399
+ });
400
+
401
+ // Remove the labeleledby attribute since it is pointing
402
+ // to a non-existent label
403
+ if (quartoSearchOptions.type === "overlay") {
404
+ const inputEl = window.document.querySelector(
405
+ "#quarto-search .aa-Autocomplete"
406
+ );
407
+ if (inputEl) {
408
+ inputEl.removeAttribute("aria-labelledby");
409
+ }
410
+ }
411
+
412
+ function throttle(func, wait) {
413
+ let waiting = false;
414
+ return function () {
415
+ if (!waiting) {
416
+ func.apply(this, arguments);
417
+ waiting = true;
418
+ setTimeout(function () {
419
+ waiting = false;
420
+ }, wait);
421
+ }
422
+ };
423
+ }
424
+
425
+ // If the main document scrolls dismiss the search results
426
+ // (otherwise, since they're floating in the document they can scroll with the document)
427
+ window.document.body.onscroll = throttle(() => {
428
+ // Only do this if we're not detached
429
+ // Bug #7117
430
+ // This will happen when the keyboard is shown on ios (resulting in a scroll)
431
+ // which then closed the search UI
432
+ if (!window.matchMedia(detachedMediaQuery).matches) {
433
+ setIsOpen(false);
434
+ }
435
+ }, 50);
436
+
437
+ if (showSearchResults) {
438
+ setIsOpen(true);
439
+ focusSearchInput();
440
+ }
441
+ });
442
+
443
+ function configurePlugins(quartoSearchOptions) {
444
+ const autocompletePlugins = [];
445
+ const algoliaOptions = quartoSearchOptions.algolia;
446
+ if (
447
+ algoliaOptions &&
448
+ algoliaOptions["analytics-events"] &&
449
+ algoliaOptions["search-only-api-key"] &&
450
+ algoliaOptions["application-id"]
451
+ ) {
452
+ const apiKey = algoliaOptions["search-only-api-key"];
453
+ const appId = algoliaOptions["application-id"];
454
+
455
+ // Aloglia insights may not be loaded because they require cookie consent
456
+ // Use deferred loading so events will start being recorded when/if consent
457
+ // is granted.
458
+ const algoliaInsightsDeferredPlugin = deferredLoadPlugin(() => {
459
+ if (
460
+ window.aa &&
461
+ window["@algolia/autocomplete-plugin-algolia-insights"]
462
+ ) {
463
+ window.aa("init", {
464
+ appId,
465
+ apiKey,
466
+ useCookie: true,
467
+ });
468
+
469
+ const { createAlgoliaInsightsPlugin } =
470
+ window["@algolia/autocomplete-plugin-algolia-insights"];
471
+ // Register the insights client
472
+ const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({
473
+ insightsClient: window.aa,
474
+ onItemsChange({ insights, insightsEvents }) {
475
+ const events = insightsEvents.flatMap((event) => {
476
+ // This API limits the number of items per event to 20
477
+ const chunkSize = 20;
478
+ const itemChunks = [];
479
+ const eventItems = event.items;
480
+ for (let i = 0; i < eventItems.length; i += chunkSize) {
481
+ itemChunks.push(eventItems.slice(i, i + chunkSize));
482
+ }
483
+ // Split the items into multiple events that can be sent
484
+ const events = itemChunks.map((items) => {
485
+ return {
486
+ ...event,
487
+ items,
488
+ };
489
+ });
490
+ return events;
491
+ });
492
+
493
+ for (const event of events) {
494
+ insights.viewedObjectIDs(event);
495
+ }
496
+ },
497
+ });
498
+ return algoliaInsightsPlugin;
499
+ }
500
+ });
501
+
502
+ // Add the plugin
503
+ autocompletePlugins.push(algoliaInsightsDeferredPlugin);
504
+ return autocompletePlugins;
505
+ }
506
+ }
507
+
508
+ // For plugins that may not load immediately, create a wrapper
509
+ // plugin and forward events and plugin data once the plugin
510
+ // is initialized. This is useful for cases like cookie consent
511
+ // which may prevent the analytics insights event plugin from initializing
512
+ // immediately.
513
+ function deferredLoadPlugin(createPlugin) {
514
+ let plugin = undefined;
515
+ let subscribeObj = undefined;
516
+ const wrappedPlugin = () => {
517
+ if (!plugin && subscribeObj) {
518
+ plugin = createPlugin();
519
+ if (plugin && plugin.subscribe) {
520
+ plugin.subscribe(subscribeObj);
521
+ }
522
+ }
523
+ return plugin;
524
+ };
525
+
526
+ return {
527
+ subscribe: (obj) => {
528
+ subscribeObj = obj;
529
+ },
530
+ onStateChange: (obj) => {
531
+ const plugin = wrappedPlugin();
532
+ if (plugin && plugin.onStateChange) {
533
+ plugin.onStateChange(obj);
534
+ }
535
+ },
536
+ onSubmit: (obj) => {
537
+ const plugin = wrappedPlugin();
538
+ if (plugin && plugin.onSubmit) {
539
+ plugin.onSubmit(obj);
540
+ }
541
+ },
542
+ onReset: (obj) => {
543
+ const plugin = wrappedPlugin();
544
+ if (plugin && plugin.onReset) {
545
+ plugin.onReset(obj);
546
+ }
547
+ },
548
+ getSources: (obj) => {
549
+ const plugin = wrappedPlugin();
550
+ if (plugin && plugin.getSources) {
551
+ return plugin.getSources(obj);
552
+ } else {
553
+ return Promise.resolve([]);
554
+ }
555
+ },
556
+ data: (obj) => {
557
+ const plugin = wrappedPlugin();
558
+ if (plugin && plugin.data) {
559
+ plugin.data(obj);
560
+ }
561
+ },
562
+ };
563
+ }
564
+
565
+ function validateItems(items) {
566
+ // Validate the first item
567
+ if (items.length > 0) {
568
+ const item = items[0];
569
+ const missingFields = [];
570
+ if (item.href == undefined) {
571
+ missingFields.push("href");
572
+ }
573
+ if (!item.title == undefined) {
574
+ missingFields.push("title");
575
+ }
576
+ if (!item.text == undefined) {
577
+ missingFields.push("text");
578
+ }
579
+
580
+ if (missingFields.length === 1) {
581
+ throw {
582
+ name: `Error: Search index is missing the <code>${missingFields[0]}</code> field.`,
583
+ message: `The items being returned for this search do not include all the required fields. Please ensure that your index items include the <code>${missingFields[0]}</code> field or use <code>index-fields</code> in your <code>_quarto.yml</code> file to specify the field names.`,
584
+ };
585
+ } else if (missingFields.length > 1) {
586
+ const missingFieldList = missingFields
587
+ .map((field) => {
588
+ return `<code>${field}</code>`;
589
+ })
590
+ .join(", ");
591
+
592
+ throw {
593
+ name: `Error: Search index is missing the following fields: ${missingFieldList}.`,
594
+ message: `The items being returned for this search do not include all the required fields. Please ensure that your index items includes the following fields: ${missingFieldList}, or use <code>index-fields</code> in your <code>_quarto.yml</code> file to specify the field names.`,
595
+ };
596
+ }
597
+ }
598
+ }
599
+
600
+ let lastQuery = null;
601
+ function showCopyLink(query, options) {
602
+ const language = options.language;
603
+ lastQuery = query;
604
+ // Insert share icon
605
+ const inputSuffixEl = window.document.body.querySelector(
606
+ ".aa-Form .aa-InputWrapperSuffix"
607
+ );
608
+
609
+ if (inputSuffixEl) {
610
+ let copyButtonEl = window.document.body.querySelector(
611
+ ".aa-Form .aa-InputWrapperSuffix .aa-CopyButton"
612
+ );
613
+
614
+ if (copyButtonEl === null) {
615
+ copyButtonEl = window.document.createElement("button");
616
+ copyButtonEl.setAttribute("class", "aa-CopyButton");
617
+ copyButtonEl.setAttribute("type", "button");
618
+ copyButtonEl.setAttribute("title", language["search-copy-link-title"]);
619
+ copyButtonEl.onmousedown = (e) => {
620
+ e.preventDefault();
621
+ e.stopPropagation();
622
+ };
623
+
624
+ const linkIcon = "bi-clipboard";
625
+ const checkIcon = "bi-check2";
626
+
627
+ const shareIconEl = window.document.createElement("i");
628
+ shareIconEl.setAttribute("class", `bi ${linkIcon}`);
629
+ copyButtonEl.appendChild(shareIconEl);
630
+ inputSuffixEl.prepend(copyButtonEl);
631
+
632
+ const clipboard = new window.ClipboardJS(".aa-CopyButton", {
633
+ text: function (_trigger) {
634
+ const copyUrl = new URL(window.location);
635
+ copyUrl.searchParams.set(kQueryArg, lastQuery);
636
+ copyUrl.searchParams.set(kResultsArg, "1");
637
+ return copyUrl.toString();
638
+ },
639
+ });
640
+ clipboard.on("success", function (e) {
641
+ // Focus the input
642
+
643
+ // button target
644
+ const button = e.trigger;
645
+ const icon = button.querySelector("i.bi");
646
+
647
+ // flash "checked"
648
+ icon.classList.add(checkIcon);
649
+ icon.classList.remove(linkIcon);
650
+ setTimeout(function () {
651
+ icon.classList.remove(checkIcon);
652
+ icon.classList.add(linkIcon);
653
+ }, 1000);
654
+ });
655
+ }
656
+
657
+ // If there is a query, show the link icon
658
+ if (copyButtonEl) {
659
+ if (lastQuery && options["copy-button"]) {
660
+ copyButtonEl.style.display = "flex";
661
+ } else {
662
+ copyButtonEl.style.display = "none";
663
+ }
664
+ }
665
+ }
666
+ }
667
+
668
+ /* Search Index Handling */
669
+ // create the index
670
+ var fuseIndex = undefined;
671
+ var shownWarning = false;
672
+ async function readSearchData() {
673
+ // Initialize the search index on demand
674
+ if (fuseIndex === undefined) {
675
+ if (window.location.protocol === "file:" && !shownWarning) {
676
+ window.alert(
677
+ "Search requires JavaScript features disabled when running in file://... URLs. In order to use search, please run this document in a web server."
678
+ );
679
+ shownWarning = true;
680
+ return;
681
+ }
682
+ // create fuse index
683
+ const options = {
684
+ keys: [
685
+ { name: "title", weight: 20 },
686
+ { name: "section", weight: 20 },
687
+ { name: "text", weight: 10 },
688
+ ],
689
+ ignoreLocation: true,
690
+ threshold: 0.1,
691
+ };
692
+ const fuse = new window.Fuse([], options);
693
+
694
+ // fetch the main search.json
695
+ const response = await fetch(offsetURL("search.json"));
696
+ if (response.status == 200) {
697
+ return response.json().then(function (searchDocs) {
698
+ searchDocs.forEach(function (searchDoc) {
699
+ fuse.add(searchDoc);
700
+ });
701
+ fuseIndex = fuse;
702
+ return fuseIndex;
703
+ });
704
+ } else {
705
+ return Promise.reject(
706
+ new Error(
707
+ "Unexpected status from search index request: " + response.status
708
+ )
709
+ );
710
+ }
711
+ }
712
+
713
+ return fuseIndex;
714
+ }
715
+
716
+ function inputElement() {
717
+ return window.document.body.querySelector(".aa-Form .aa-Input");
718
+ }
719
+
720
+ function focusSearchInput() {
721
+ setTimeout(() => {
722
+ const inputEl = inputElement();
723
+ if (inputEl) {
724
+ inputEl.focus();
725
+ }
726
+ }, 50);
727
+ }
728
+
729
+ /* Panels */
730
+ const kItemTypeDoc = "document";
731
+ const kItemTypeMore = "document-more";
732
+ const kItemTypeItem = "document-item";
733
+ const kItemTypeError = "error";
734
+
735
+ function renderItem(
736
+ item,
737
+ createElement,
738
+ state,
739
+ setActiveItemId,
740
+ setContext,
741
+ refresh,
742
+ quartoSearchOptions
743
+ ) {
744
+ switch (item.type) {
745
+ case kItemTypeDoc:
746
+ return createDocumentCard(
747
+ createElement,
748
+ "file-richtext",
749
+ item.title,
750
+ item.section,
751
+ item.text,
752
+ item.href,
753
+ item.crumbs,
754
+ quartoSearchOptions
755
+ );
756
+ case kItemTypeMore:
757
+ return createMoreCard(
758
+ createElement,
759
+ item,
760
+ state,
761
+ setActiveItemId,
762
+ setContext,
763
+ refresh
764
+ );
765
+ case kItemTypeItem:
766
+ return createSectionCard(
767
+ createElement,
768
+ item.section,
769
+ item.text,
770
+ item.href
771
+ );
772
+ case kItemTypeError:
773
+ return createErrorCard(createElement, item.title, item.text);
774
+ default:
775
+ return undefined;
776
+ }
777
+ }
778
+
779
+ function createDocumentCard(
780
+ createElement,
781
+ icon,
782
+ title,
783
+ section,
784
+ text,
785
+ href,
786
+ crumbs,
787
+ quartoSearchOptions
788
+ ) {
789
+ const iconEl = createElement("i", {
790
+ class: `bi bi-${icon} search-result-icon`,
791
+ });
792
+ const titleEl = createElement("p", { class: "search-result-title" }, title);
793
+ const titleContents = [iconEl, titleEl];
794
+ const showParent = quartoSearchOptions["show-item-context"];
795
+ if (crumbs && showParent) {
796
+ let crumbsOut = undefined;
797
+ const crumbClz = ["search-result-crumbs"];
798
+ if (showParent === "root") {
799
+ crumbsOut = crumbs.length > 1 ? crumbs[0] : undefined;
800
+ } else if (showParent === "parent") {
801
+ crumbsOut = crumbs.length > 1 ? crumbs[crumbs.length - 2] : undefined;
802
+ } else {
803
+ crumbsOut = crumbs.length > 1 ? crumbs.join(" > ") : undefined;
804
+ crumbClz.push("search-result-crumbs-wrap");
805
+ }
806
+
807
+ const crumbEl = createElement(
808
+ "p",
809
+ { class: crumbClz.join(" ") },
810
+ crumbsOut
811
+ );
812
+ titleContents.push(crumbEl);
813
+ }
814
+
815
+ const titleContainerEl = createElement(
816
+ "div",
817
+ { class: "search-result-title-container" },
818
+ titleContents
819
+ );
820
+
821
+ const textEls = [];
822
+ if (section) {
823
+ const sectionEl = createElement(
824
+ "p",
825
+ { class: "search-result-section" },
826
+ section
827
+ );
828
+ textEls.push(sectionEl);
829
+ }
830
+ const descEl = createElement("p", {
831
+ class: "search-result-text",
832
+ dangerouslySetInnerHTML: {
833
+ __html: text,
834
+ },
835
+ });
836
+ textEls.push(descEl);
837
+
838
+ const textContainerEl = createElement(
839
+ "div",
840
+ { class: "search-result-text-container" },
841
+ textEls
842
+ );
843
+
844
+ const containerEl = createElement(
845
+ "div",
846
+ {
847
+ class: "search-result-container",
848
+ },
849
+ [titleContainerEl, textContainerEl]
850
+ );
851
+
852
+ const linkEl = createElement(
853
+ "a",
854
+ {
855
+ href: offsetURL(href),
856
+ class: "search-result-link",
857
+ },
858
+ containerEl
859
+ );
860
+
861
+ const classes = ["search-result-doc", "search-item"];
862
+ if (!section) {
863
+ classes.push("document-selectable");
864
+ }
865
+
866
+ return createElement(
867
+ "div",
868
+ {
869
+ class: classes.join(" "),
870
+ },
871
+ linkEl
872
+ );
873
+ }
874
+
875
+ function createMoreCard(
876
+ createElement,
877
+ item,
878
+ state,
879
+ setActiveItemId,
880
+ setContext,
881
+ refresh
882
+ ) {
883
+ const moreCardEl = createElement(
884
+ "div",
885
+ {
886
+ class: "search-result-more search-item",
887
+ onClick: (e) => {
888
+ // Handle expanding the sections by adding the expanded
889
+ // section to the list of expanded sections
890
+ toggleExpanded(item, state, setContext, setActiveItemId, refresh);
891
+ e.stopPropagation();
892
+ },
893
+ },
894
+ item.title
895
+ );
896
+
897
+ return moreCardEl;
898
+ }
899
+
900
+ function toggleExpanded(item, state, setContext, setActiveItemId, refresh) {
901
+ const expanded = state.context.expanded || [];
902
+ if (expanded.includes(item.target)) {
903
+ setContext({
904
+ expanded: expanded.filter((target) => target !== item.target),
905
+ });
906
+ } else {
907
+ setContext({ expanded: [...expanded, item.target] });
908
+ }
909
+
910
+ refresh();
911
+ setActiveItemId(item.__autocomplete_id);
912
+ }
913
+
914
+ function createSectionCard(createElement, section, text, href) {
915
+ const sectionEl = createSection(createElement, section, text, href);
916
+ return createElement(
917
+ "div",
918
+ {
919
+ class: "search-result-doc-section search-item",
920
+ },
921
+ sectionEl
922
+ );
923
+ }
924
+
925
+ function createSection(createElement, title, text, href) {
926
+ const descEl = createElement("p", {
927
+ class: "search-result-text",
928
+ dangerouslySetInnerHTML: {
929
+ __html: text,
930
+ },
931
+ });
932
+
933
+ const titleEl = createElement("p", { class: "search-result-section" }, title);
934
+ const linkEl = createElement(
935
+ "a",
936
+ {
937
+ href: offsetURL(href),
938
+ class: "search-result-link",
939
+ },
940
+ [titleEl, descEl]
941
+ );
942
+ return linkEl;
943
+ }
944
+
945
+ function createErrorCard(createElement, title, text) {
946
+ const descEl = createElement("p", {
947
+ class: "search-error-text",
948
+ dangerouslySetInnerHTML: {
949
+ __html: text,
950
+ },
951
+ });
952
+
953
+ const titleEl = createElement("p", {
954
+ class: "search-error-title",
955
+ dangerouslySetInnerHTML: {
956
+ __html: `<i class="bi bi-exclamation-circle search-error-icon"></i> ${title}`,
957
+ },
958
+ });
959
+ const errorEl = createElement("div", { class: "search-error" }, [
960
+ titleEl,
961
+ descEl,
962
+ ]);
963
+ return errorEl;
964
+ }
965
+
966
+ function positionPanel(pos) {
967
+ const panelEl = window.document.querySelector(
968
+ "#quarto-search-results .aa-Panel"
969
+ );
970
+ const inputEl = window.document.querySelector(
971
+ "#quarto-search .aa-Autocomplete"
972
+ );
973
+
974
+ if (panelEl && inputEl) {
975
+ panelEl.style.top = `${Math.round(panelEl.offsetTop)}px`;
976
+ if (pos === "start") {
977
+ panelEl.style.left = `${Math.round(inputEl.left)}px`;
978
+ } else {
979
+ panelEl.style.right = `${Math.round(inputEl.offsetRight)}px`;
980
+ }
981
+ }
982
+ }
983
+
984
+ /* Highlighting */
985
+ // highlighting functions
986
+ function highlightMatch(query, text) {
987
+ if (text) {
988
+ const start = text.toLowerCase().indexOf(query.toLowerCase());
989
+ if (start !== -1) {
990
+ const startMark = "<mark class='search-match'>";
991
+ const endMark = "</mark>";
992
+
993
+ const end = start + query.length;
994
+ text =
995
+ text.slice(0, start) +
996
+ startMark +
997
+ text.slice(start, end) +
998
+ endMark +
999
+ text.slice(end);
1000
+ const startInfo = clipStart(text, start);
1001
+ const endInfo = clipEnd(
1002
+ text,
1003
+ startInfo.position + startMark.length + endMark.length
1004
+ );
1005
+ text =
1006
+ startInfo.prefix +
1007
+ text.slice(startInfo.position, endInfo.position) +
1008
+ endInfo.suffix;
1009
+
1010
+ return text;
1011
+ } else {
1012
+ return text;
1013
+ }
1014
+ } else {
1015
+ return text;
1016
+ }
1017
+ }
1018
+
1019
+ function clipStart(text, pos) {
1020
+ const clipStart = pos - 50;
1021
+ if (clipStart < 0) {
1022
+ // This will just return the start of the string
1023
+ return {
1024
+ position: 0,
1025
+ prefix: "",
1026
+ };
1027
+ } else {
1028
+ // We're clipping before the start of the string, walk backwards to the first space.
1029
+ const spacePos = findSpace(text, pos, -1);
1030
+ return {
1031
+ position: spacePos.position,
1032
+ prefix: "",
1033
+ };
1034
+ }
1035
+ }
1036
+
1037
+ function clipEnd(text, pos) {
1038
+ const clipEnd = pos + 200;
1039
+ if (clipEnd > text.length) {
1040
+ return {
1041
+ position: text.length,
1042
+ suffix: "",
1043
+ };
1044
+ } else {
1045
+ const spacePos = findSpace(text, clipEnd, 1);
1046
+ return {
1047
+ position: spacePos.position,
1048
+ suffix: spacePos.clipped ? "…" : "",
1049
+ };
1050
+ }
1051
+ }
1052
+
1053
+ function findSpace(text, start, step) {
1054
+ let stepPos = start;
1055
+ while (stepPos > -1 && stepPos < text.length) {
1056
+ const char = text[stepPos];
1057
+ if (char === " " || char === "," || char === ":") {
1058
+ return {
1059
+ position: step === 1 ? stepPos : stepPos - step,
1060
+ clipped: stepPos > 1 && stepPos < text.length,
1061
+ };
1062
+ }
1063
+ stepPos = stepPos + step;
1064
+ }
1065
+
1066
+ return {
1067
+ position: stepPos - step,
1068
+ clipped: false,
1069
+ };
1070
+ }
1071
+
1072
+ // removes highlighting as implemented by the mark tag
1073
+ function clearHighlight(searchterm, el) {
1074
+ const childNodes = el.childNodes;
1075
+ for (let i = childNodes.length - 1; i >= 0; i--) {
1076
+ const node = childNodes[i];
1077
+ if (node.nodeType === Node.ELEMENT_NODE) {
1078
+ if (
1079
+ node.tagName === "MARK" &&
1080
+ node.innerText.toLowerCase() === searchterm.toLowerCase()
1081
+ ) {
1082
+ el.replaceChild(document.createTextNode(node.innerText), node);
1083
+ } else {
1084
+ clearHighlight(searchterm, node);
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ function escapeRegExp(string) {
1091
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
1092
+ }
1093
+
1094
+ // highlight matches
1095
+ function highlight(term, el) {
1096
+ const termRegex = new RegExp(term, "ig");
1097
+ const childNodes = el.childNodes;
1098
+
1099
+ // walk back to front avoid mutating elements in front of us
1100
+ for (let i = childNodes.length - 1; i >= 0; i--) {
1101
+ const node = childNodes[i];
1102
+
1103
+ if (node.nodeType === Node.TEXT_NODE) {
1104
+ // Search text nodes for text to highlight
1105
+ const text = node.nodeValue;
1106
+
1107
+ let startIndex = 0;
1108
+ let matchIndex = text.search(termRegex);
1109
+ if (matchIndex > -1) {
1110
+ const markFragment = document.createDocumentFragment();
1111
+ while (matchIndex > -1) {
1112
+ const prefix = text.slice(startIndex, matchIndex);
1113
+ markFragment.appendChild(document.createTextNode(prefix));
1114
+
1115
+ const mark = document.createElement("mark");
1116
+ mark.appendChild(
1117
+ document.createTextNode(
1118
+ text.slice(matchIndex, matchIndex + term.length)
1119
+ )
1120
+ );
1121
+ markFragment.appendChild(mark);
1122
+
1123
+ startIndex = matchIndex + term.length;
1124
+ matchIndex = text.slice(startIndex).search(new RegExp(term, "ig"));
1125
+ if (matchIndex > -1) {
1126
+ matchIndex = startIndex + matchIndex;
1127
+ }
1128
+ }
1129
+ if (startIndex < text.length) {
1130
+ markFragment.appendChild(
1131
+ document.createTextNode(text.slice(startIndex, text.length))
1132
+ );
1133
+ }
1134
+
1135
+ el.replaceChild(markFragment, node);
1136
+ }
1137
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
1138
+ // recurse through elements
1139
+ highlight(term, node);
1140
+ }
1141
+ }
1142
+ }
1143
+
1144
+ /* Link Handling */
1145
+ // get the offset from this page for a given site root relative url
1146
+ function offsetURL(url) {
1147
+ var offset = getMeta("quarto:offset");
1148
+ return offset ? offset + url : url;
1149
+ }
1150
+
1151
+ // read a meta tag value
1152
+ function getMeta(metaName) {
1153
+ var metas = window.document.getElementsByTagName("meta");
1154
+ for (let i = 0; i < metas.length; i++) {
1155
+ if (metas[i].getAttribute("name") === metaName) {
1156
+ return metas[i].getAttribute("content");
1157
+ }
1158
+ }
1159
+ return "";
1160
+ }
1161
+
1162
+ function algoliaSearch(query, limit, algoliaOptions) {
1163
+ const { getAlgoliaResults } = window["@algolia/autocomplete-preset-algolia"];
1164
+
1165
+ const applicationId = algoliaOptions["application-id"];
1166
+ const searchOnlyApiKey = algoliaOptions["search-only-api-key"];
1167
+ const indexName = algoliaOptions["index-name"];
1168
+ const indexFields = algoliaOptions["index-fields"];
1169
+ const searchClient = window.algoliasearch(applicationId, searchOnlyApiKey);
1170
+ const searchParams = algoliaOptions["params"];
1171
+ const searchAnalytics = !!algoliaOptions["analytics-events"];
1172
+
1173
+ return getAlgoliaResults({
1174
+ searchClient,
1175
+ queries: [
1176
+ {
1177
+ indexName: indexName,
1178
+ query,
1179
+ params: {
1180
+ hitsPerPage: limit,
1181
+ clickAnalytics: searchAnalytics,
1182
+ ...searchParams,
1183
+ },
1184
+ },
1185
+ ],
1186
+ transformResponse: (response) => {
1187
+ if (!indexFields) {
1188
+ return response.hits.map((hit) => {
1189
+ return hit.map((item) => {
1190
+ return {
1191
+ ...item,
1192
+ text: highlightMatch(query, item.text),
1193
+ };
1194
+ });
1195
+ });
1196
+ } else {
1197
+ const remappedHits = response.hits.map((hit) => {
1198
+ return hit.map((item) => {
1199
+ const newItem = { ...item };
1200
+ ["href", "section", "title", "text", "crumbs"].forEach(
1201
+ (keyName) => {
1202
+ const mappedName = indexFields[keyName];
1203
+ if (
1204
+ mappedName &&
1205
+ item[mappedName] !== undefined &&
1206
+ mappedName !== keyName
1207
+ ) {
1208
+ newItem[keyName] = item[mappedName];
1209
+ delete newItem[mappedName];
1210
+ }
1211
+ }
1212
+ );
1213
+ newItem.text = highlightMatch(query, newItem.text);
1214
+ return newItem;
1215
+ });
1216
+ });
1217
+ return remappedHits;
1218
+ }
1219
+ },
1220
+ });
1221
+ }
1222
+
1223
+ function fuseSearch(query, fuse, fuseOptions) {
1224
+ return fuse.search(query, fuseOptions).map((result) => {
1225
+ const addParam = (url, name, value) => {
1226
+ const anchorParts = url.split("#");
1227
+ const baseUrl = anchorParts[0];
1228
+ const sep = baseUrl.search("\\?") > 0 ? "&" : "?";
1229
+ anchorParts[0] = baseUrl + sep + name + "=" + value;
1230
+ return anchorParts.join("#");
1231
+ };
1232
+
1233
+ return {
1234
+ title: result.item.title,
1235
+ section: result.item.section,
1236
+ href: addParam(result.item.href, kQueryArg, query),
1237
+ text: highlightMatch(query, result.item.text),
1238
+ crumbs: result.item.crumbs,
1239
+ };
1240
+ });
1241
+ }