commonmeta-ruby 3.7.1 → 3.7.3

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish.yml +23 -0
  3. data/Gemfile.lock +26 -26
  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 +8 -8
  41. data/lib/commonmeta/readers/json_feed_reader.rb +6 -6
  42. data/lib/commonmeta/utils.rb +2 -2
  43. data/lib/commonmeta/version.rb +1 -1
  44. data/spec/cli_spec.rb +5 -5
  45. data/spec/fixtures/vcr_cassettes/Commonmeta_CLI/json_feed/json_feed_blog_slug.yml +71 -0
  46. data/spec/fixtures/vcr_cassettes/Commonmeta_CLI/json_feed/json_feed_updated.yml +186 -0
  47. data/spec/fixtures/vcr_cassettes/Commonmeta_Metadata/get_json_feed/updated_posts.yml +186 -0
  48. data/spec/readers/json_feed_reader_spec.rb +5 -5
  49. metadata +42 -5
  50. data/spec/fixtures/vcr_cassettes/Commonmeta_CLI/json_feed/json_feed_not_indexed.yml +0 -37
  51. data/spec/fixtures/vcr_cassettes/Commonmeta_Metadata/get_json_feed/not_indexed_posts.yml +0 -37
@@ -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
+ }