| 1 | /** |
| 2 | * MacroBrowser - Dynamic documentation browser for macros. |
| 3 | * Similar to SlashCommandBrowser but for the macro system. |
| 4 | */ |
| 5 | |
| 6 | import { MacroRegistry, MacroCategory } from './MacroRegistry.js'; |
| 7 | import { performFuzzySearch } from '../../power-user.js'; |
| 8 | import { 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 | */ |
| 17 | const 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 | */ |
| 33 | export 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 | */ |
| 308 | export 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 | */ |
| 318 | function 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 | */ |
| 329 | export 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 | */ |
| 373 | export 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 | */ |
| 399 | export 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 | */ |
| 413 | export 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 | */ |
| 433 | function 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 | */ |
| 470 | export 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 | } |