| 330 | } | 335 | } |
| 331 | | 336 | |
| 332 | /** | 337 | /** |
| | 338 | * Builds an HTML string for a replacement, highlighting literal parts in green |
| | 339 | * and keeping back-referenced parts plain. |
| | 340 | * @param {RegExpMatchArray} match The match object from `matchAll`. |
| | 341 | * @param {string} pattern The replacement pattern string (e.g., "new text $1"). |
| | 342 | * @returns {string} The constructed HTML string. |
| | 343 | */ |
| | 344 | function buildReplacementHtml(match, pattern) { |
| | 345 | const container = document.createDocumentFragment(); |
| | 346 | let lastIndex = 0; |
| | 347 | const backrefRegex = /\$\$|\$&|\$`|\$'|\$(\d{1,2})/g; |
| | 348 | |
| | 349 | let reMatch; |
| | 350 | while ((reMatch = backrefRegex.exec(pattern)) !== null) { |
| | 351 | // Part of the pattern before the back-reference is a literal. |
| | 352 | const literalPart = pattern.substring(lastIndex, reMatch.index); |
| | 353 | if (literalPart) { |
| | 354 | const mark = document.createElement('mark'); |
| | 355 | mark.className = 'green_hl'; |
| | 356 | mark.innerText = literalPart; |
| | 357 | container.appendChild(mark); |
| | 358 | } |
| | 359 | |
| | 360 | const backref = reMatch[0]; |
| | 361 | if (backref === '$$') { |
| | 362 | container.appendChild(document.createTextNode('$')); |
| | 363 | } else if (backref === '$&') { |
| | 364 | const mark = document.createElement('mark'); |
| | 365 | mark.className = 'yellow_hl'; |
| | 366 | mark.innerText = match[0]; |
| | 367 | container.appendChild(mark); |
| | 368 | } else if (backref === '$`') { |
| | 369 | container.appendChild(document.createTextNode(match.input.substring(0, match.index))); |
| | 370 | } else if (backref === '$\'') { |
| | 371 | container.appendChild(document.createTextNode(match.input.substring(match.index + match[0].length))); |
| | 372 | } else { // It's a numbered capture group, $n. |
| | 373 | const groupIndex = parseInt(reMatch[1], 10); |
| | 374 | if (groupIndex > 0 && groupIndex < match.length && match[groupIndex] !== undefined) { |
| | 375 | const mark = document.createElement('mark'); |
| | 376 | mark.className = 'yellow_hl'; |
| | 377 | mark.innerText = match[groupIndex]; |
| | 378 | container.appendChild(mark); |
| | 379 | } else { |
| | 380 | // Not a valid group index, treat it as a literal. |
| | 381 | const mark = document.createElement('mark'); |
| | 382 | mark.className = 'green_hl'; |
| | 383 | mark.innerText = backref; |
| | 384 | container.appendChild(mark); |
| | 385 | } |
| | 386 | } |
| | 387 | lastIndex = backrefRegex.lastIndex; |
| | 388 | } |
| | 389 | |
| | 390 | // The final part of the pattern after the last back-reference. |
| | 391 | const finalLiteralPart = pattern.substring(lastIndex); |
| | 392 | if (finalLiteralPart) { |
| | 393 | const mark = document.createElement('mark'); |
| | 394 | mark.className = 'green_hl'; |
| | 395 | mark.innerText = finalLiteralPart; |
| | 396 | container.appendChild(mark); |
| | 397 | } |
| | 398 | |
| | 399 | // To get the HTML content, we need a temporary parent element. |
| | 400 | const tempDiv = document.createElement('div'); |
| | 401 | tempDiv.appendChild(container); |
| | 402 | return tempDiv.innerHTML; |
| | 403 | } |
| | 404 | |
| | 405 | function executeRegexScriptForDebugging(script, text) { |
| | 406 | let err; |
| | 407 | let originalRegex; |
| | 408 | |
| | 409 | try { |
| | 410 | originalRegex = regexFromString(script.findRegex); |
| | 411 | if (!originalRegex) throw new Error('Invalid regex string'); |
| | 412 | } catch (e) { |
| | 413 | err = `Compile error: ${e.message}`; |
| | 414 | return { output: text, highlightedOutput: text, error: err, charsCaptured: 0, charsAdded: 0, charsRemoved: 0 }; |
| | 415 | } |
| | 416 | |
| | 417 | const globalRegex = new RegExp(originalRegex.source, originalRegex.flags.includes('g') ? originalRegex.flags : originalRegex.flags + 'g'); |
| | 418 | const matches = [...text.matchAll(globalRegex)]; |
| | 419 | |
| | 420 | if (matches.length === 0) { |
| | 421 | return { output: text, highlightedOutput: escapeHtml(text), error: null, charsCaptured: 0, charsAdded: 0, charsRemoved: 0 }; |
| | 422 | } |
| | 423 | |
| | 424 | let outputText = ''; |
| | 425 | let highlightedOutput = ''; // This will now be our "diff view" |
| | 426 | let lastIndex = 0; |
| | 427 | let totalCharsCaptured = 0; |
| | 428 | let totalCharsAdded = 0; |
| | 429 | let totalCharsRemoved = 0; |
| | 430 | |
| | 431 | try { |
| | 432 | for (const match of matches) { |
| | 433 | const originalMatchText = match[0]; |
| | 434 | totalCharsCaptured += originalMatchText.length; |
| | 435 | |
| | 436 | // Append text between matches (this part is unchanged) |
| | 437 | const precedingText = text.substring(lastIndex, match.index); |
| | 438 | outputText += precedingText; |
| | 439 | highlightedOutput += escapeHtml(precedingText); |
| | 440 | |
| | 441 | // --- Start of new diff and statistics logic --- |
| | 442 | let charsAddedInMatch = 0; |
| | 443 | let charsKeptFromMatch = 0; |
| | 444 | const backrefRegex = /\$\$|\$&|\$`|\$'|\$(\d{1,2})/g; |
| | 445 | let lastPatternIndex = 0; |
| | 446 | let reMatch; |
| | 447 | let replacementForPlainText = ''; |
| | 448 | |
| | 449 | // This loop calculates the stats accurately |
| | 450 | while ((reMatch = backrefRegex.exec(script.replaceString)) !== null) { |
| | 451 | const literalPart = script.replaceString.substring(lastPatternIndex, reMatch.index); |
| | 452 | charsAddedInMatch += literalPart.length; |
| | 453 | replacementForPlainText += literalPart; |
| | 454 | const backref = reMatch[0]; |
| | 455 | if (backref === '$$') { |
| | 456 | replacementForPlainText += '$'; |
| | 457 | } else if (backref === '$&') { |
| | 458 | charsKeptFromMatch += (match[0] || '').length; replacementForPlainText += (match[0] || ''); |
| | 459 | } else if (backref === '$`') { |
| | 460 | const part = match.input.substring(0, match.index); charsKeptFromMatch += part.length; replacementForPlainText += part; |
| | 461 | } else if (backref === '$\'') { |
| | 462 | const part = match.input.substring(match.index + match[0].length); charsKeptFromMatch += part.length; replacementForPlainText += part; |
| | 463 | } else { |
| | 464 | const groupIndex = parseInt(reMatch[1], 10); |
| | 465 | if (groupIndex > 0 && groupIndex < match.length && match[groupIndex] !== undefined) { |
| | 466 | charsKeptFromMatch += match[groupIndex].length; |
| | 467 | replacementForPlainText += match[groupIndex]; |
| | 468 | } |
| | 469 | } |
| | 470 | lastPatternIndex = backrefRegex.lastIndex; |
| | 471 | } |
| | 472 | const finalLiteralPart = script.replaceString.substring(lastPatternIndex); |
| | 473 | charsAddedInMatch += finalLiteralPart.length; |
| | 474 | replacementForPlainText += finalLiteralPart; |
| | 475 | |
| | 476 | totalCharsAdded += charsAddedInMatch; |
| | 477 | totalCharsRemoved += (originalMatchText.length - charsKeptFromMatch); |
| | 478 | |
| | 479 | outputText += replacementForPlainText; |
| | 480 | // --- End of statistics logic --- |
| | 481 | |
| | 482 | // --- Build the new Diff View HTML --- |
| | 483 | // 1. Show the entire original match as "removed" (red strikethrough) |
| | 484 | highlightedOutput += `<mark class='red_hl'>${escapeHtml(originalMatchText)}</mark>`; |
| | 485 | // 2. Add an arrow to signify transformation |
| | 486 | highlightedOutput += ' → '; |
| | 487 | // 3. Build the replacement string with green (added) and yellow (kept) parts |
| | 488 | highlightedOutput += buildReplacementHtml(match, script.replaceString); |
| | 489 | |
| | 490 | lastIndex = match.index + originalMatchText.length; |
| | 491 | } |
| | 492 | |
| | 493 | // Append text after the last match |
| | 494 | const trailingText = text.substring(lastIndex); |
| | 495 | outputText += trailingText; |
| | 496 | highlightedOutput += escapeHtml(trailingText); |
| | 497 | |
| | 498 | } catch (e) { |
| | 499 | err = (err ? err + '; ' : '') + `Replace error: ${e.message}`; |
| | 500 | outputText = text; // Fallback |
| | 501 | highlightedOutput = escapeHtml(text); |
| | 502 | } |
| | 503 | |
| | 504 | return { |
| | 505 | output: outputText, |
| | 506 | highlightedOutput: highlightedOutput, |
| | 507 | error: err, |
| | 508 | charsCaptured: totalCharsCaptured, |
| | 509 | charsAdded: totalCharsAdded, |
| | 510 | charsRemoved: totalCharsRemoved, |
| | 511 | }; |
| | 512 | } |
| | 513 | |
| | 514 | function populateDebuggerRuleList(container) { |
| | 515 | const rulesContainer = container.find('#regex_debugger_rules'); |
| | 516 | const ruleTemplate = container.find('#regex_debugger_rule_template'); |
| | 517 | if (!rulesContainer.length || !ruleTemplate.length) { |
| | 518 | console.error('Regex Debugger: Could not find rule list or template in the DOM.'); |
| | 519 | return; |
| | 520 | } |
| | 521 | |
| | 522 | rulesContainer.empty(); |
| | 523 | |
| | 524 | const allScripts = getRegexScripts(); |
| | 525 | if (!allScripts || allScripts.length === 0) { |
| | 526 | rulesContainer.append('<div class="regex-debugger-no-rules">No regex rules found.</div>'); |
| | 527 | return; |
| | 528 | } |
| | 529 | |
| | 530 | const globalScriptIds = new Set((extension_settings.regex ?? []).map(s => s.id)); |
| | 531 | const globalScripts = []; |
| | 532 | const scopedScripts = []; |
| | 533 | |
| | 534 | allScripts.forEach(script => { |
| | 535 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy |
| | 536 | if (globalScriptIds.has(script.id)) { |
| | 537 | // @ts-ignore |
| | 538 | scriptCopy.isScoped = false; |
| | 539 | globalScripts.push(scriptCopy); |
| | 540 | } else { |
| | 541 | // @ts-ignore |
| | 542 | scriptCopy.isScoped = true; |
| | 543 | scopedScripts.push(scriptCopy); |
| | 544 | } |
| | 545 | }); |
| | 546 | |
| | 547 | container.data('allScripts', [...globalScripts, ...scopedScripts]); |
| | 548 | |
| | 549 | const renderRule = (script) => { |
| | 550 | if (!script.id) script.id = uuidv4(); |
| | 551 | const ruleElementContent = $(ruleTemplate.prop('content')).clone(); |
| | 552 | const ruleElement = ruleElementContent.find('.regex-debugger-rule'); |
| | 553 | |
| | 554 | ruleElement.attr('data-id', script.id); |
| | 555 | // @ts-ignore |
| | 556 | ruleElement.find('.rule-name').text(script.scriptName); |
| | 557 | ruleElement.find('.rule-regex').text(script.findRegex); |
| | 558 | // @ts-ignore |
| | 559 | ruleElement.find('.rule-scope').text(script.isScoped ? 'Scoped' : 'Global'); |
| | 560 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); |
| | 561 | // @ts-ignore |
| | 562 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScoped)); |
| | 563 | |
| | 564 | ruleElement.on('click', function (event) { |
| | 565 | if ($(event.target).is('input, .menu_button, .menu_button i')) { |
| | 566 | return; |
| | 567 | } |
| | 568 | const scriptId = $(this).data('id'); |
| | 569 | const stepElement = $(`#step-result-${scriptId}`); |
| | 570 | const container = $('#regex_debugger_steps_output'); |
| | 571 | |
| | 572 | if (stepElement.length && container.length) { |
| | 573 | // Replace scrollIntoView with scrollTop animation |
| | 574 | const targetTop = stepElement.position().top; |
| | 575 | const containerScrollTop = container.scrollTop(); |
| | 576 | const containerHeight = container.height(); |
| | 577 | |
| | 578 | // Center the element if possible |
| | 579 | let scrollTo = containerScrollTop + targetTop - (containerHeight / 2) + (stepElement.height() / 2); |
| | 580 | |
| | 581 | container.animate({ scrollTop: scrollTo }, 300); // 300ms smooth scroll |
| | 582 | |
| | 583 | stepElement.css('transition', 'background-color 0.5s').css('background-color', 'var(--highlight_color)'); |
| | 584 | setTimeout(() => stepElement.css('background-color', ''), 1000); |
| | 585 | } |
| | 586 | }); |
| | 587 | |
| | 588 | return ruleElementContent; |
| | 589 | }; |
| | 590 | |
| | 591 | if (globalScripts.length > 0) { |
| | 592 | rulesContainer.append('<div class="list-header regex-debugger-list-header">Global Rules</div>'); |
| | 593 | const globalList = $('<ul id="regex_debugger_rules_global" class="sortable-list"></ul>'); |
| | 594 | globalScripts.forEach(script => globalList.append(renderRule(script))); |
| | 595 | rulesContainer.append(globalList); |
| | 596 | } |
| | 597 | |
| | 598 | if (scopedScripts.length > 0) { |
| | 599 | rulesContainer.append('<div class="list-header regex-debugger-list-header">Scoped Rules</div>'); |
| | 600 | const scopedList = $('<ul id="regex_debugger_rules_scoped" class="sortable-list"></ul>'); |
| | 601 | scopedScripts.forEach(script => scopedList.append(renderRule(script))); |
| | 602 | rulesContainer.append(scopedList); |
| | 603 | } |
| | 604 | } |
| | 605 | |
| | 606 | /** |
| | 607 | * Opens the regex debugger. |
| | 608 | * @returns {Promise<void>} |
| | 609 | */ |
| | 610 | async function onRegexDebuggerOpenClick() { |
| | 611 | const templateContent = await renderExtensionTemplateAsync('regex', 'debugger'); |
| | 612 | const debuggerHtml = $('<div>').html(templateContent); |
| | 613 | |
| | 614 | const stepTemplate = debuggerHtml.find('#regex_debugger_step_template'); |
| | 615 | |
| | 616 | populateDebuggerRuleList(debuggerHtml); |
| | 617 | |
| | 618 | // @ts-ignore |
| | 619 | debuggerHtml.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
| | 620 | // @ts-ignore |
| | 621 | debuggerHtml.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); |
| | 622 | |
| | 623 | debuggerHtml.find('#regex_debugger_run_test').on('click', function () { |
| | 624 | const allScripts = debuggerHtml.data('allScripts'); |
| | 625 | const orderedRuleIds = [ |
| | 626 | ...$('#regex_debugger_rules_global').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
| | 627 | ...$('#regex_debugger_rules_scoped').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
| | 628 | ]; |
| | 629 | |
| | 630 | const rawInput = String($('#regex_debugger_raw_input').val()); |
| | 631 | const stepsOutput = $('#regex_debugger_steps_output'); |
| | 632 | const finalOutput = $('#regex_debugger_final_output'); |
| | 633 | |
| | 634 | if (!stepsOutput.length || !finalOutput.length) return; |
| | 635 | |
| | 636 | const displayMode = $('input[name="display_mode"]:checked').val(); |
| | 637 | stepsOutput.empty(); |
| | 638 | finalOutput.empty(); |
| | 639 | $('#regex_debugger_final_summary').remove(); |
| | 640 | |
| | 641 | if (!allScripts) return; |
| | 642 | let textForNextStep = rawInput; |
| | 643 | let totalCharsCaptured = 0; |
| | 644 | let totalCharsAdded = 0; |
| | 645 | let totalCharsRemoved = 0; |
| | 646 | |
| | 647 | orderedRuleIds.forEach(scriptId => { |
| | 648 | const ruleElement = $(`#regex_debugger_rules [data-id="${scriptId}"]`); |
| | 649 | if (!ruleElement.find('.rule-enabled').is(':checked')) return; |
| | 650 | |
| | 651 | const script = allScripts.find(s => s.id === scriptId); |
| | 652 | |
| | 653 | if (script) { |
| | 654 | const result = executeRegexScriptForDebugging(script, textForNextStep); |
| | 655 | totalCharsCaptured += result.charsCaptured; |
| | 656 | totalCharsAdded += result.charsAdded; |
| | 657 | totalCharsRemoved += result.charsRemoved; |
| | 658 | |
| | 659 | const stepElement = $(stepTemplate.prop('content')).clone(); |
| | 660 | // Set the ID on the TOP-LEVEL element that is being appended. |
| | 661 | stepElement.find('>:first-child').attr('id', `step-result-${script.id}`); |
| | 662 | const stepHeader = stepElement.find('.step-header'); |
| | 663 | stepHeader.find('strong').text(`After: ${script.scriptName}`); |
| | 664 | |
| | 665 | const metricsHtml = `<span class="step-metrics">Captured: ${result.charsCaptured}, Added: +${result.charsAdded}, Removed: -${result.charsRemoved}</span>`; |
| | 666 | stepHeader.append(metricsHtml); |
| | 667 | |
| | 668 | if (displayMode === 'highlight') { |
| | 669 | stepElement.find('.step-output').html(result.highlightedOutput); |
| | 670 | } else { |
| | 671 | stepElement.find('.step-output').text(result.output); |
| | 672 | } |
| | 673 | |
| | 674 | if (result.error) { |
| | 675 | stepHeader.append($(`<div class='warning_text text_rose-500'>${result.error}</div>`)); |
| | 676 | } |
| | 677 | |
| | 678 | stepsOutput.append(stepElement); |
| | 679 | textForNextStep = result.output; |
| | 680 | } |
| | 681 | }); |
| | 682 | |
| | 683 | const summaryHtml = ` |
| | 684 | <div id="regex_debugger_final_summary" class="regex-debugger-summary"> |
| | 685 | <strong>Total Captured:</strong> ${totalCharsCaptured} | <strong>Total Added:</strong> +${totalCharsAdded} | <strong>Total Removed:</strong> -${totalCharsRemoved} |
| | 686 | </div> |
| | 687 | `; |
| | 688 | finalOutput.before(summaryHtml); |
| | 689 | |
| | 690 | const renderMode = $('#regex_debugger_render_mode').val(); |
| | 691 | if (renderMode === 'message') { |
| | 692 | const formattedHtml = messageFormatting(textForNextStep, 'Debugger', true, false, null); |
| | 693 | const messageBlock = $('<div class="mes"><div class="mes_text"></div></div>'); |
| | 694 | messageBlock.find('.mes_text').html(formattedHtml); |
| | 695 | finalOutput.append(messageBlock); |
| | 696 | } else { |
| | 697 | finalOutput.text(textForNextStep); |
| | 698 | } |
| | 699 | }); |
| | 700 | |
| | 701 | debuggerHtml.find('#regex_debugger_save_order').on('click', async function () { |
| | 702 | const allKnownScripts = getRegexScripts(); |
| | 703 | const newGlobalScripts = $('#regex_debugger_rules_global').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
| | 704 | const newScopedScripts = $('#regex_debugger_rules_scoped').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
| | 705 | |
| | 706 | extension_settings.regex = newGlobalScripts; |
| | 707 | if (this_chid !== undefined) { |
| | 708 | await writeExtensionField(this_chid, 'regex_scripts', newScopedScripts); |
| | 709 | } |
| | 710 | |
| | 711 | saveSettingsDebounced(); |
| | 712 | await loadRegexScripts(); |
| | 713 | toastr.success(t`Regex script order saved!`); |
| | 714 | |
| | 715 | const currentPopupContent = $('div:has(> #regex_debugger_rules)'); |
| | 716 | populateDebuggerRuleList(currentPopupContent); |
| | 717 | // @ts-ignore |
| | 718 | currentPopupContent.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
| | 719 | // @ts-ignore |
| | 720 | currentPopupContent.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); |
| | 721 | }); |
| | 722 | |
| | 723 | debuggerHtml.find('#regex_debugger_expand_steps').on('click', function () { |
| | 724 | const popupContainer = $('<div class="expanded-regex-container"></div>'); |
| | 725 | const navPanel = $('<div class="expanded-regex-nav"><h4>Steps</h4></div>'); |
| | 726 | const contentPanel = $('<div class="expanded-regex-content"></div>'); |
| | 727 | |
| | 728 | const content = $('#regex_debugger_steps_output').clone().html(); |
| | 729 | contentPanel.html(content); |
| | 730 | |
| | 731 | $('#regex_debugger_rules .regex-debugger-rule').each(function () { |
| | 732 | const ruleElement = $(this); |
| | 733 | const scriptId = ruleElement.data('id'); |
| | 734 | const scriptName = ruleElement.find('.rule-name').text(); |
| | 735 | |
| | 736 | const link = $(`<a href="#">${escapeHtml(scriptName)}</a>`); |
| | 737 | link.data('target-id', `step-result-${scriptId}`); |
| | 738 | |
| | 739 | link.on('click', function (e) { |
| | 740 | e.preventDefault(); |
| | 741 | navPanel.find('a').removeClass('active'); |
| | 742 | $(this).addClass('active'); |
| | 743 | |
| | 744 | const targetId = $(this).data('target-id'); |
| | 745 | // The selector is now correct for the structure. |
| | 746 | const targetElement = contentPanel.find(`#${targetId}`); |
| | 747 | |
| | 748 | if (targetElement.length) { |
| | 749 | const scrollTo = contentPanel.scrollTop() + targetElement.position().top; |
| | 750 | contentPanel.animate({ scrollTop: scrollTo }, 300); |
| | 751 | |
| | 752 | targetElement.css('transition', 'background-color 0.5s').css('background-color', 'var(--highlight_color)'); |
| | 753 | setTimeout(() => targetElement.css('background-color', ''), 1000); |
| | 754 | } |
| | 755 | }); |
| | 756 | |
| | 757 | navPanel.append(link); |
| | 758 | }); |
| | 759 | |
| | 760 | popupContainer.append(navPanel).append(contentPanel); |
| | 761 | callGenericPopup(popupContainer, POPUP_TYPE.TEXT, 'Step-by-step Transformation', { wide: true, allowVerticalScrolling: false }); |
| | 762 | }); |
| | 763 | |
| | 764 | debuggerHtml.find('#regex_debugger_expand_final').on('click', function () { |
| | 765 | const content = $('#regex_debugger_final_output').html(); |
| | 766 | const popupContent = $('<div style="height: 70vh; overflow-y: auto;"></div>').html(content); |
| | 767 | callGenericPopup(popupContent, POPUP_TYPE.TEXT, 'Final Output', { wide: true, allowVerticalScrolling: true }); |
| | 768 | }); |
| | 769 | |
| | 770 | await callGenericPopup(debuggerHtml.children(), POPUP_TYPE.TEXT, '', { wide: true, allowVerticalScrolling: true }); |
| | 771 | } |
| | 772 | |
| | 773 | /** |
| 333 | * Updates the info block in the regex editor with hints regarding the find regex. | 774 | * Updates the info block in the regex editor with hints regarding the find regex. |
| 334 | * @param {JQuery<HTMLElement>} editorHtml The editor HTML | 775 | * @param {JQuery<HTMLElement>} editorHtml The editor HTML |
| 335 | */ | 776 | */ |