Blame Raw
· · · 686 lines (25.0 KB)
0 contributors
1/**
2 * MacroBrowser - Dynamic documentation browser for macros.
3 * Similar to SlashCommandBrowser but for the macro system.
4 */
5
6import { MacroRegistry, MacroCategory } from './MacroRegistry.js';
7import { performFuzzySearch } from '../../power-user.js';
8import { escapeRegex } from '/scripts/utils.js';
9
10/** @typedef {import('./MacroRegistry.js').MacroDefinition} MacroDefinition */
11/** @typedef {import('./MacroRegistry.js').MacroValueType} MacroValueType */
12
13/**
14 * Category display names and order for documentation.
15 * @type {Record<string, { label: string, order: number }>}
16 */
17const CATEGORY_CONFIG = {
18 [MacroCategory.NAMES]: { label: 'Names & Participants', order: 1 },
19 [MacroCategory.UTILITY]: { label: 'Utilities', order: 2 },
20 [MacroCategory.RANDOM]: { label: 'Randomization', order: 3 },
21 [MacroCategory.TIME]: { label: 'Date & Time', order: 4 },
22 [MacroCategory.VARIABLE]: { label: 'Variables', order: 5 },
23 [MacroCategory.STATE]: { label: 'Runtime State', order: 6 },
24 [MacroCategory.CHARACTER]: { label: 'Character Card & Persona Fields', order: 7 },
25 [MacroCategory.CHAT]: { label: 'Chat History & Messages', order: 8 },
26 [MacroCategory.PROMPTS]: { label: 'Prompt Templates', order: 9 },
27 [MacroCategory.MISC]: { label: 'Miscellaneous', order: 10 },
28};
29
30/**
31 * MacroBrowser class for displaying searchable macro documentation.
32 */
33export class MacroBrowser {
34 /** @type {Map<string, MacroDefinition[]>} */
35 macrosByCategory = new Map();
36
37 /** @type {HTMLElement} */
38 dom;
39
40 /** @type {HTMLInputElement} */
41 searchInput;
42
43 /** @type {HTMLElement} */
44 detailsPanel;
45
46 /** @type {Map<string, HTMLElement>} */
47 itemMap = new Map();
48
49 /** @type {boolean} */
50 isSorted = false;
51
52 /**
53 * Groups macros by category in registration order.
54 * Excludes hidden aliases from the list.
55 */
56 #loadMacros() {
57 this.macrosByCategory.clear();
58 // Exclude hidden aliases - they won't show in the list
59 const allMacros = MacroRegistry.getAllMacros({ excludeHiddenAliases: true });
60
61 for (const macro of allMacros) {
62 const category = macro.category || MacroCategory.MISC;
63 if (!this.macrosByCategory.has(category)) {
64 this.macrosByCategory.set(category, []);
65 }
66 this.macrosByCategory.get(category).push(macro);
67 }
68 }
69
70 /**
71 * Sorts macros within each category alphabetically.
72 */
73 #sortMacros() {
74 for (const [, macros] of this.macrosByCategory) {
75 macros.sort((a, b) => a.name.localeCompare(b.name));
76 }
77 }
78
79 /**
80 * Gets categories sorted by their configured order.
81 * @returns {string[]}
82 */
83 #getSortedCategories() {
84 return Array.from(this.macrosByCategory.keys())
85 .sort((a, b) => getCategoryConfig(a).order - getCategoryConfig(b).order);
86 }
87
88 /**
89 * Renders the browser into a parent element.
90 * @param {HTMLElement} parent
91 * @returns {HTMLElement}
92 */
93 renderInto(parent) {
94 this.#loadMacros();
95
96 const root = document.createElement('div');
97 root.classList.add('macroBrowser');
98 this.dom = root;
99
100 // Search bar and sort button
101 const toolbar = document.createElement('div');
102 toolbar.classList.add('macro-toolbar');
103
104 const searchLabel = document.createElement('label');
105 searchLabel.classList.add('macro-search-label');
106 searchLabel.textContent = 'Search: ';
107
108 const searchInput = document.createElement('input');
109 searchInput.type = 'search';
110 searchInput.classList.add('macro-search-input', 'text_pole');
111 searchInput.placeholder = 'Search macros by name or description...';
112 searchInput.addEventListener('input', () => this.#handleSearch(searchInput.value));
113 this.searchInput = searchInput;
114 searchLabel.appendChild(searchInput);
115 toolbar.appendChild(searchLabel);
116
117 const sortBtn = document.createElement('button');
118 sortBtn.classList.add('macro-sort-btn', 'menu_button');
119 sortBtn.innerHTML = '<i class="fa-solid fa-arrow-down-a-z"></i> Sort A-Z';
120 sortBtn.title = 'Sort macros alphabetically within each category';
121 sortBtn.addEventListener('click', () => this.#toggleSort());
122 toolbar.appendChild(sortBtn);
123
124 root.appendChild(toolbar);
125
126 // Container for list and details
127 const container = document.createElement('div');
128 container.classList.add('macro-container');
129
130 // Macro list
131 const listPanel = document.createElement('div');
132 listPanel.classList.add('macro-list-panel');
133 this.#renderList(listPanel);
134 container.appendChild(listPanel);
135
136 // Details panel
137 const detailsPanel = document.createElement('div');
138 detailsPanel.classList.add('macro-details-panel');
139 detailsPanel.innerHTML = '<div class="macro-details-placeholder">Select a macro to view details</div>';
140 this.detailsPanel = detailsPanel;
141 container.appendChild(detailsPanel);
142
143 root.appendChild(container);
144 parent.appendChild(root);
145
146 return root;
147 }
148
149 /**
150 * Renders the macro list grouped by category.
151 * @param {HTMLElement} listPanel
152 */
153 #renderList(listPanel) {
154 listPanel.innerHTML = '';
155 this.itemMap.clear();
156
157 for (const category of this.#getSortedCategories()) {
158 const macros = this.macrosByCategory.get(category);
159 if (!macros || macros.length === 0) continue;
160
161 // Category header
162 const categoryHeader = document.createElement('div');
163 categoryHeader.classList.add('macro-category-header');
164 categoryHeader.textContent = getCategoryConfig(category).label;
165 categoryHeader.dataset.category = category;
166 listPanel.appendChild(categoryHeader);
167
168 // Macro items
169 for (const macro of macros) {
170 const item = renderMacroItem(macro);
171 item.addEventListener('click', () => this.#showDetails(macro, item));
172 this.itemMap.set(macro.name, item);
173 listPanel.appendChild(item);
174 }
175 }
176 }
177
178 /**
179 * Shows details for a selected macro.
180 * @param {MacroDefinition} macro
181 * @param {HTMLElement} item
182 */
183 #showDetails(macro, item) {
184 // Clear previous selection
185 this.dom.querySelectorAll('.macro-item.selected').forEach(el => el.classList.remove('selected'));
186 item.classList.add('selected');
187
188 // Render details
189 this.detailsPanel.innerHTML = '';
190 this.detailsPanel.appendChild(renderMacroDetails(macro));
191 }
192
193 /**
194 * Handles search input using fuzzy search.
195 * @param {string} query
196 */
197 #handleSearch(query) {
198 query = query.trim();
199
200 // Clear details on search
201 this.detailsPanel.innerHTML = '<div class="macro-details-placeholder">Select a macro to view details</div>';
202 this.dom.querySelectorAll('.macro-item.selected').forEach(el => el.classList.remove('selected'));
203
204 // If empty query, show all
205 if (!query) {
206 for (const item of this.itemMap.values()) {
207 item.classList.remove('isFiltered');
208 }
209 this.dom.querySelectorAll('.macro-category-header').forEach(h => h.classList.remove('isFiltered'));
210 return;
211 }
212
213 // Trim query of braces, as we don't have them in the macro names of the search definitions
214 query = query.replace(/[{}]/g, '');
215
216 // Build searchable data array from all macros
217 const allMacros = MacroRegistry.getAllMacros();
218 const searchData = allMacros.map(macro => ({
219 name: macro.name,
220 aliases: macro.aliases?.map(a => a.alias).join(' '),
221 description: macro.description || '',
222 category: getCategoryConfig(macro.category).label,
223 argNames: macro.unnamedArgDefs.map(d => d.name).join(' '),
224 argDescriptions: macro.unnamedArgDefs.map(d => d.description || '').join(' '),
225 }));
226
227 // Fuzzy search with weighted keys
228 const keys = [
229 { name: 'name', weight: 10 },
230 { name: 'aliases', weight: 1 }, // No need to rank those high, if they are important (visible) they have their own entry
231 { name: 'description', weight: 5 },
232 { name: 'category', weight: 3 },
233 { name: 'argNames', weight: 2 },
234 { name: 'argDescriptions', weight: 1 },
235 ];
236
237 const results = performFuzzySearch('macro-browser', searchData, keys, query);
238 const matchedNames = new Set(results.map(r => r.item.name));
239
240 // Filter items based on fuzzy results
241 for (const [name, item] of this.itemMap) {
242 item.classList.toggle('isFiltered', !matchedNames.has(name));
243 }
244
245 // Hide empty category headers
246 this.dom.querySelectorAll('.macro-category-header').forEach(header => {
247 if (!(header instanceof HTMLElement)) return;
248 const category = header.dataset.category;
249 const hasVisible = Array.from(this.itemMap.values())
250 .filter(item => item.dataset.macroName)
251 .some(item => {
252 const macro = MacroRegistry.getMacro(item.dataset.macroName);
253 return macro?.category === category && !item.classList.contains('isFiltered');
254 });
255 header.classList.toggle('isFiltered', !hasVisible);
256 });
257 }
258
259 /**
260 * Toggles alphabetical sorting.
261 */
262 #toggleSort() {
263 this.isSorted = !this.isSorted;
264
265 if (this.isSorted) {
266 this.#sortMacros();
267 } else {
268 this.#loadMacros(); // Reload to restore registration order
269 }
270
271 const listPanel = this.dom.querySelector('.macro-list-panel');
272 if (!(listPanel instanceof HTMLElement)) return;
273
274 this.#renderList(listPanel);
275 // Re-apply current search filter
276 if (this.searchInput?.value) {
277 this.#handleSearch(this.searchInput.value);
278 }
279
280 // Update button state
281 const sortBtn = this.dom.querySelector('.macro-sort-btn');
282 sortBtn?.classList.toggle('active', this.isSorted);
283 }
284
285 /**
286 * Handles keyboard shortcuts.
287 * @param {KeyboardEvent} evt
288 */
289 #handleKeyDown(evt) {
290 if (!evt.shiftKey && !evt.altKey && evt.ctrlKey && evt.key.toLowerCase() === 'f') {
291 if (!this.dom.closest('body')) return;
292 if (this.dom.closest('.mes') && !this.dom.closest('.last_mes')) return;
293 evt.preventDefault();
294 evt.stopPropagation();
295 evt.stopImmediatePropagation();
296 this.searchInput?.focus();
297 }
298 }
299}
300
301/**
302 * Gets the macro help content.
303 * If experimental_macro_engine is enabled, returns a placeholder for the browser.
304 * Otherwise returns the static template content.
305 *
306 * @returns {string} HTML string for help content
307 */
308export function getMacrosHelp() {
309 // Return a placeholder that will be replaced with the browser
310 return '<div class="macroHelp"><i class="fa-solid fa-spinner fa-spin"></i> Loading macro documentation...</div>';
311}
312
313/**
314 * Gets display config for a category.
315 * @param {string} category
316 * @returns {{ label: string, order: number }}
317 */
318function getCategoryConfig(category) {
319 return CATEGORY_CONFIG[category] ?? { label: category, order: 100 };
320}
321
322/**
323 * Formats a macro signature with its arguments.
324 * Uses displayOverride if available, otherwise auto-generates from args.
325 * Optional args are shown in [brackets].
326 * @param {MacroDefinition} macro
327 * @returns {string}
328 */
329export function formatMacroSignature(macro) {
330 // Use displayOverride if provided
331 if (macro.displayOverride) {
332 if (macro.aliasOf) {
333 // Replace all occurrences of the macro name with the alias for this list
334 const escapedMainName = escapeRegex(macro.aliasOf);
335 return macro.displayOverride.replace(new RegExp(`(?<=[\\b{\\s])${escapedMainName}(?=[\\b}:\\s])`, 'g'), `${macro.name}`);
336 }
337 return macro.displayOverride;
338 }
339
340 const parts = [macro.name];
341
342 // Add all unnamed args (required + optional)
343 for (let i = 0; i < macro.unnamedArgDefs.length; i++) {
344 const argDef = macro.unnamedArgDefs[i];
345 const argName = argDef?.sampleValue || argDef?.name || `arg${i + 1}`;
346 // Wrap optional args in brackets
347 parts.push(argDef?.optional ? `[${argName}]` : argName);
348 }
349
350 // Add list args indicator
351 if (macro.list) {
352 const hasMin = macro.list.min > 0;
353 const hasMax = macro.list.max !== null;
354 if (hasMin && hasMax && macro.list.min === macro.list.max) {
355 // Fixed number of list items
356 for (let i = 0; i < macro.list.min; i++) {
357 parts.push(`item${i + 1}`);
358 }
359 } else {
360 // Variable list
361 parts.push('item1', 'item2', '...');
362 }
363 }
364
365 return `{{${parts.join('::')}}}`;
366}
367
368/**
369 * Creates a DOM element for a macro's source indicator (extension/third-party icons).
370 * @param {MacroDefinition} macro
371 * @returns {HTMLElement}
372 */
373export function createSourceIndicator(macro) {
374 const src = document.createElement('span');
375 src.classList.add('macro-source', 'fa-solid');
376
377 if (macro.source.isExtension) {
378 src.classList.add('isExtension', 'fa-cubes');
379 src.classList.add(macro.source.isThirdParty ? 'isThirdParty' : 'isCore');
380 } else {
381 src.classList.add('isCore', 'fa-star-of-life');
382 }
383
384 const titleParts = [
385 macro.source.isExtension ? 'Extension' : 'Core',
386 macro.source.isThirdParty ? 'Third Party' : (macro.source.isExtension ? 'Built-in' : null),
387 macro.source.name,
388 ].filter(Boolean);
389 src.title = titleParts.join('\n');
390
391 return src;
392}
393
394/**
395 * Creates a DOM element for alias indicator icon.
396 * @param {MacroDefinition} macro
397 * @returns {HTMLElement|null}
398 */
399export function createAliasIndicator(macro) {
400 if (!macro.aliasOf) return null;
401
402 const icon = document.createElement('span');
403 icon.classList.add('macro-alias-indicator', 'fa-solid', 'fa-arrow-turn-up');
404 icon.title = `Alias of {{${macro.aliasOf}}}`;
405 return icon;
406}
407
408/**
409 * Creates a type badge element. Supports single type or array of types.
410 * @param {MacroValueType|MacroValueType[]} type - Single type or array of accepted types.
411 * @returns {HTMLElement}
412 */
413export function createTypeBadge(type) {
414 const badge = document.createElement('span');
415 badge.classList.add('macro-arg-type');
416
417 if (Array.isArray(type)) {
418 badge.textContent = type.join(' | ');
419 badge.title = `Accepts: ${type.join(', ')}`;
420 } else {
421 badge.textContent = type;
422 }
423
424 return badge;
425}
426
427/**
428 * Renders a single macro item for the list.
429 * Order: [signature] [description (shrinks)] [alias icon?] [source icon]
430 * @param {MacroDefinition} macro
431 * @returns {HTMLElement}
432 */
433function renderMacroItem(macro) {
434 const item = document.createElement('div');
435 item.classList.add('macro-item');
436 if (macro.aliasOf) item.classList.add('isAlias');
437 item.dataset.macroName = macro.name;
438
439 // Signature (fixed width, truncates if too long)
440 const signature = document.createElement('code');
441 signature.classList.add('macro-signature');
442 signature.textContent = formatMacroSignature(macro);
443 item.appendChild(signature);
444
445 // Description preview (shrinks to fit, truncates)
446 const desc = document.createElement('span');
447 desc.classList.add('macro-desc-preview');
448 desc.textContent = macro.description || '<no description>';
449 item.appendChild(desc);
450
451 // Alias indicator (if this is an alias entry)
452 const aliasIcon = createAliasIndicator(macro);
453 if (aliasIcon) item.appendChild(aliasIcon);
454
455 // Source indicator (fixed, stays at right edge)
456 item.appendChild(createSourceIndicator(macro));
457
458 return item;
459}
460
461/**
462 * Renders detailed information for a macro.
463 * Can optionally highlight the current argument being typed.
464 * @param {MacroDefinition} macro
465 * @param {Object} [options]
466 * @param {number} [options.currentArgIndex=-1] - Index of argument to highlight (-1 for none).
467 * @param {boolean} [options.showCategory=true] - Whether to show category badge.
468 * @returns {HTMLElement}
469 */
470export function renderMacroDetails(macro, options = {}) {
471 const { currentArgIndex = -1, showCategory = true } = options;
472 const details = document.createElement('div');
473 details.classList.add('macro-details');
474
475 // Header with name and source
476 const header = document.createElement('div');
477 header.classList.add('macro-details-header');
478
479 const nameEl = document.createElement('code');
480 nameEl.classList.add('macro-details-name');
481 nameEl.textContent = formatMacroSignature(macro);
482 header.appendChild(nameEl);
483
484 header.appendChild(createSourceIndicator(macro));
485 details.appendChild(header);
486
487 // Category badge (optional)
488 if (showCategory) {
489 const categoryBadge = document.createElement('span');
490 categoryBadge.classList.add('macro-category-badge');
491 categoryBadge.textContent = getCategoryConfig(macro.category).label;
492 details.appendChild(categoryBadge);
493 }
494
495 // If this is an alias, show what it's an alias of
496 if (macro.aliasOf) {
497 const aliasOfSection = document.createElement('div');
498 aliasOfSection.classList.add('macro-alias-of');
499 aliasOfSection.innerHTML = `<i class="fa-solid fa-arrow-turn-up"></i> Alias of <code>{{${macro.aliasOf}}}</code>`;
500 details.appendChild(aliasOfSection);
501 }
502
503 // Description
504 const descSection = document.createElement('div');
505 descSection.classList.add('macro-details-section');
506 const descLabel = document.createElement('div');
507 descLabel.classList.add('macro-details-label');
508 descLabel.textContent = 'Description';
509 descSection.appendChild(descLabel);
510 const descText = document.createElement('div');
511 descText.classList.add('macro-details-text');
512 descText.textContent = macro.description || '<no description>';
513 descSection.appendChild(descText);
514 details.appendChild(descSection);
515
516 // Arguments section (if any)
517 if (macro.unnamedArgDefs.length > 0 || macro.list) {
518 const argsSection = document.createElement('div');
519 argsSection.classList.add('macro-details-section');
520 const argsLabel = document.createElement('div');
521 argsLabel.classList.add('macro-details-label');
522 argsLabel.textContent = 'Arguments';
523 argsSection.appendChild(argsLabel);
524
525 const argsList = document.createElement('ul');
526 argsList.classList.add('macro-args-list');
527
528 // Unnamed args (required + optional)
529 for (let i = 0; i < macro.unnamedArgDefs.length; i++) {
530 const argDef = macro.unnamedArgDefs[i];
531 const argItem = document.createElement('li');
532 argItem.classList.add('macro-arg-item');
533 if (argDef?.optional) argItem.classList.add('isOptional');
534 if (currentArgIndex === i) argItem.classList.add('current');
535
536 const argName = document.createElement('code');
537 argName.classList.add('macro-arg-name');
538 argName.textContent = argDef?.name || `arg${i + 1}`;
539 argItem.appendChild(argName);
540
541 argItem.appendChild(createTypeBadge(argDef.type ?? 'string'));
542
543 const argRequiredLabel = document.createElement('span');
544 argRequiredLabel.classList.add(argDef?.optional ? 'macro-arg-optional' : 'macro-arg-required');
545 if (argDef?.optional && argDef.defaultValue !== undefined) {
546 argRequiredLabel.textContent = `(optional, default: ${argDef.defaultValue === '' ? '<empty string>' : argDef.defaultValue})`;
547 } else {
548 argRequiredLabel.textContent = argDef?.optional ? '(optional)' : '(required)';
549 }
550 argItem.appendChild(argRequiredLabel);
551
552 if (argDef?.description) {
553 const argDesc = document.createElement('span');
554 argDesc.classList.add('macro-arg-desc');
555 argDesc.textContent = ` — ${argDef.description}`;
556 argItem.appendChild(argDesc);
557 }
558
559 if (argDef?.sampleValue) {
560 const sample = document.createElement('span');
561 sample.classList.add('macro-arg-sample');
562 sample.textContent = ` (e.g. ${argDef.sampleValue})`;
563 argItem.appendChild(sample);
564 }
565
566 argsList.appendChild(argItem);
567 }
568
569 // List args
570 if (macro.list) {
571 const listItem = document.createElement('li');
572 listItem.classList.add('macro-arg-item', 'macro-arg-list');
573 if (currentArgIndex >= macro.maxArgs) listItem.classList.add('current');
574
575 const listName = document.createElement('code');
576 listName.classList.add('macro-arg-name');
577 listName.textContent = 'item1::item2::...';
578 listItem.appendChild(listName);
579
580 const listInfo = document.createElement('span');
581 listInfo.classList.add('macro-arg-list-info');
582
583 const minMax = [];
584 if (macro.list.min > 0) minMax.push(`min: ${macro.list.min}`);
585 if (macro.list.max !== null) minMax.push(`max: ${macro.list.max}`);
586
587 if (minMax.length > 0) {
588 listInfo.textContent = ` (list, ${minMax.join(', ')})`;
589 } else {
590 listInfo.textContent = ' (variable-length list)';
591 }
592 listItem.appendChild(listInfo);
593
594 argsList.appendChild(listItem);
595 }
596
597 argsSection.appendChild(argsList);
598 details.appendChild(argsSection);
599 }
600
601 // Returns section (always show - at minimum shows the type)
602 {
603 const returnsSection = document.createElement('div');
604 returnsSection.classList.add('macro-details-section');
605 const returnsLabel = document.createElement('div');
606 returnsLabel.classList.add('macro-details-label');
607 returnsLabel.textContent = 'Returns';
608 returnsSection.appendChild(returnsLabel);
609
610 const returnsContent = document.createElement('div');
611 returnsContent.classList.add('macro-returns-content');
612
613 // Add return type badge
614 const returnTypeBadge = createTypeBadge(macro.returnType);
615 returnsContent.appendChild(returnTypeBadge);
616
617 // Add description text if provided
618 if (macro.returns) {
619 const returnsText = document.createElement('span');
620 returnsText.classList.add('macro-details-text');
621 returnsText.textContent = macro.returns;
622 returnsContent.appendChild(returnsText);
623 }
624
625 returnsSection.appendChild(returnsContent);
626 details.appendChild(returnsSection);
627 }
628
629 // Example usage section (if any)
630 if (macro.exampleUsage && macro.exampleUsage.length > 0) {
631 const exampleSection = document.createElement('div');
632 exampleSection.classList.add('macro-details-section');
633 const exampleLabel = document.createElement('div');
634 exampleLabel.classList.add('macro-details-label');
635 exampleLabel.textContent = 'Example Usage';
636 exampleSection.appendChild(exampleLabel);
637
638 const exampleList = document.createElement('ul');
639 exampleList.classList.add('macro-example-list');
640 for (const example of macro.exampleUsage) {
641 const li = document.createElement('li');
642 const code = document.createElement('code');
643 code.textContent = example;
644 li.appendChild(code);
645 exampleList.appendChild(li);
646 }
647 exampleSection.appendChild(exampleList);
648 details.appendChild(exampleSection);
649 }
650
651 // Aliases section (if this macro has aliases)
652 if (macro.aliases && macro.aliases.length > 0) {
653 const aliasSection = document.createElement('div');
654 aliasSection.classList.add('macro-details-section');
655 const aliasLabel = document.createElement('div');
656 aliasLabel.classList.add('macro-details-label');
657 aliasLabel.textContent = 'Aliases';
658 aliasSection.appendChild(aliasLabel);
659
660 const aliasList = document.createElement('ul');
661 aliasList.classList.add('macro-alias-list');
662 for (const { alias, visible } of macro.aliases) {
663 const li = document.createElement('li');
664 li.classList.add('macro-alias-item');
665 if (!visible) li.classList.add('isHidden');
666
667 const code = document.createElement('code');
668 code.textContent = `{{${alias}}}`;
669 li.appendChild(code);
670
671 if (!visible) {
672 const hiddenBadge = document.createElement('span');
673 hiddenBadge.classList.add('macro-alias-hidden-badge');
674 hiddenBadge.textContent = '(deprecated)';
675 hiddenBadge.title = 'This alias is deprecated and will not be shown in documentation or autocomplete';
676 li.appendChild(hiddenBadge);
677 }
678
679 aliasList.appendChild(li);
680 }
681 aliasSection.appendChild(aliasList);
682 details.appendChild(aliasSection);
683 }
684
685 return details;
686}