310X-Word Replacer WTRLAB

Use it however, but the notes feature is fragile

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
    // @name         310X-Word Replacer WTRLAB
    // @match        https://wtr-lab.com/en/*
    // @version      31.3
    // @namespace    WordReplace310XXXX
    // @description  Use it however, but the notes feature is fragile
    // @grant        none
    // ==/UserScript==

    (function () {
      'use strict';

      const STORAGE_KEY = 'wordReplacerPairsV3';

      // Load data or initialize empty object
      let data = loadData();

      // Main UI button
    const mainButton = document.createElement('button');
    mainButton.textContent = 'Word Replacer';
    Object.assign(mainButton.style, {
      position: 'fixed',
      bottom: '20px',       // always 20px from bottom
      right: '20px',
      zIndex: '100001',     // above everything except popup panel
      padding: '8px 14px',
      fontSize: '16px',
      backgroundColor: '#333',
      color: '#fff',
      border: 'none',
      borderRadius: '6px',
      cursor: 'pointer',
    });
    document.body.appendChild(mainButton);

      let popup = null;

      mainButton.addEventListener('click', () => {
        if (popup) {
          closePopup();
        } else {
          openPopup();
          replaceTextInChapter();
        }
      });

      function loadData() {
        const raw = localStorage.getItem(STORAGE_KEY);
        if (!raw) return {};
        try {
          return JSON.parse(raw);
        } catch {
          return {};
        }
      }

      function saveData(obj) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
      }
    function closePopup() {
      if (popup) {
        popup.remove();
        popup = null;
      }
    }
      // Escape regex helper
    const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\",]/g, '\\$&');



    // Check if index is start of sentence
    function isStartOfSentence(index, fullText) {
      if (index === 0) return true; // start of paragraph/text

      const before = fullText.slice(0, index);

      // If the slice is only whitespace, treat as start
      if (/^\s*$/.test(before)) return true;

      // Trim trailing spaces for checks of punctuation/endings
      const trimmed = before.replace(/\s+$/, '');

      // 1) sentence-ending punctuation (., !, ?, …) possibly followed by closing quotes/brackets:
      if (/[.!?…]["”’')\]]*$/.test(trimmed)) return true;

      // 2) newline right before (dialogue line breaks etc.)
      if (/[\n\r]\s*$/.test(before)) return true;

      // 3) opening quote/paren immediately before the match OR opening quote then space
      if (/["“”'‘(\[]\s*$/.test(before)) return true;

      // 4) after any **double quotation mark** (straight or curly) **without space**
      if (/["“”]$/.test(before)) return true;

      // 5) after "Chapter XX:" optionally followed by a comma and space
      if (/Chapter\s+\d+:\s*,?\s*$/.test(before)) return true;

      return false;
    }

    function isInsideDialogueAtIndex(text, index) {
  const quoteChars = `"'""''`;

  // Count quotes before this index
  let quoteCount = 0;
  for (let i = 0; i < index; i++) {
    if (quoteChars.includes(text[i])) {
      quoteCount++;
    }
  }

  return quoteCount % 2 === 1;
}


      // Preserve first capital helper
      function applyPreserveCapital(orig, replacement) {
        if (!orig) return replacement;
        if (orig[0] >= 'A' && orig[0] <= 'Z') {
          return replacement.charAt(0).toUpperCase() + replacement.slice(1);
        }
        return replacement;
      }

    function buildIgnoreRegex(from, ignoreTerm, entry, wildcardSymbol) {
        const flags = entry.ignoreCapital ? 'gi' : 'g';
        let basePattern = escapeRegex(from).replace(new RegExp(`\\${wildcardSymbol}`, 'g'), '.');

        if (entry.noTrailingSpace) {
            basePattern = basePattern.trim();
        }

        if (ignoreTerm) {
            // Negative lookahead for the ignore phrase (allowing spaces or punctuation between)
            return new RegExp(
                basePattern + `(?![\\s"“”'’,.-]+${escapeRegex(ignoreTerm)})`,
                flags
            );
        } else {
            return new RegExp(basePattern, flags);
        }
    }
      // The core replacement function, applies all enabled replacements with flags
    function applyReplacements(text, replacements) {
      let replacedText = text;
      const WILDCARD = '@';
      // Expanded boundary characters
    const punctuationRegex = /^[\W_'"“”‘’„,;:!?~()\[\]{}<>【】「」『』()《》〈〉—–-]|[\W_'"“”‘’„,;:!?~()\[\]{}<>【】「」『』()《》〈〉—–-]$/;
      const quoteChars = `"'“”‘’`;

      for (const entry of replacements) {
        if (!entry.from || !entry.to) continue;

        const flags = entry.ignoreCapital ? 'gi' : 'g';
        let searchTerm = entry.from;
        if (entry.noTrailingSpace) searchTerm = searchTerm.trimEnd();

        // Handle optional ignore term |term| (before or after)
        let ignoreTerm = null;
        const prefixMatch = searchTerm.match(/^\|(.*?)\|\s*(.+)$/);
        const suffixMatch = searchTerm.match(/^(.*?)\s*\|(.*?)\|$/);
        if (prefixMatch) {
          ignoreTerm = { type: 'before', value: prefixMatch[1] };
          searchTerm = prefixMatch[2];
        } else if (suffixMatch) {
          ignoreTerm = { type: 'after', value: suffixMatch[2] };
          searchTerm = suffixMatch[1];
        }

        // PATCH: allow any quote char at start if searchTerm starts with a quote
        if (quoteChars.includes(searchTerm.charAt(0))) {
          searchTerm = `[${quoteChars}]` + escapeRegex(searchTerm.slice(1));
        } else {
          searchTerm = escapeRegex(searchTerm);
        }

        // Placeholder ^ handling
        const caretCountFrom = (entry.from.match(/\^/g) || []).length;
        const caretCountTo = (entry.to.match(/\^/g) || []).length;
        const usePlaceholder = caretCountFrom === 1 && caretCountTo === 1;
        let base = usePlaceholder ? searchTerm.replace('\\^', '([^\\s])') : searchTerm.replace(new RegExp(`\\${WILDCARD}`, 'g'), '.');

        // Determine boundaries
        const firstChar = entry.from.charAt(0);
        const lastChar = entry.from.charAt(entry.from.length - 1);
        const skipBoundaries = punctuationRegex.test(firstChar) || punctuationRegex.test(lastChar);

    let patternStr = (entry.allInstances || skipBoundaries)
      ? base
      : `(?<=^|[^A-Za-z0-9])${base}(?=[^A-Za-z0-9]|$)`;

        // Apply ignore-term properly
        if (ignoreTerm && ignoreTerm.value) {
          const escapedIgnore = escapeRegex(ignoreTerm.value);
          if (ignoreTerm.type === 'before') {
            patternStr = `(?<!${escapedIgnore})${patternStr}`;
          } else if (ignoreTerm.type === 'after') {
            patternStr = `${patternStr}(?!${escapedIgnore}\\s*)`; // allow trailing spaces
          } else {
            patternStr = `(?<!${escapedIgnore})${patternStr}(?!${escapedIgnore}\\s*)`;
          }
        }

        const regex = new RegExp(patternStr, flags);

        let newText = '';
        let lastIndex = 0;
        let match;

        while ((match = regex.exec(replacedText)) !== null) {
          const idx = match.index;
          const insideDialogue = isInsideDialogueAtIndex(replacedText, idx);
          if ((entry.insideDialogueOnly && !insideDialogue) || (entry.outsideDialogueOnly && insideDialogue)) continue;

          newText += replacedText.slice(lastIndex, idx);

          let replacementBase = entry.noTrailingSpace ? entry.to.trimEnd() : entry.to;
          if (usePlaceholder && match[1] !== undefined) replacementBase = replacementBase.replace('^', match[1]);

          // START OF SENTENCE capitalization check
          const startSentence = entry.startOfSentence && isStartOfSentence(idx, replacedText);
          const finalReplacement = startSentence
            ? (entry.preserveFirstCapital
                ? applyPreserveCapital(match[0], replacementBase)
                : replacementBase.charAt(0).toUpperCase() + replacementBase.slice(1))
            : (entry.preserveFirstCapital
                ? applyPreserveCapital(match[0], replacementBase)
                : replacementBase);

          newText += finalReplacement;
          lastIndex = idx + match[0].length;
        }

        if (lastIndex < replacedText.length) newText += replacedText.slice(lastIndex);
        replacedText = newText;
      }

      return replacedText;
    }

      // The main replaceTextInChapter, preserves old proportional slicing and paragraph-limited replacement
    function replaceTextInChapter() {
      const seriesId = (() => {
        const urlMatch = location.href.match(/\/novel\/(\d+)\//i);
        if (urlMatch) return urlMatch[1];
        const crumb = document.querySelector('.breadcrumb li.breadcrumb-item a[href*="/novel/"]');
        if (crumb) {
          const crumbMatch = crumb.href.match(/\/novel\/(\d+)\//i);
          if (crumbMatch) return crumbMatch[1];
        }
        return null;
      })();

      let replacements = [];
      for (const key in data) {
        if (key === 'global' || (seriesId && key === `series-${seriesId}`)) {
          replacements = replacements.concat(data[key].filter(e => e.enabled));
        }
      }

      if (replacements.length === 0) return false;

      const paragraphs = document.querySelectorAll(
        'div.chapter-body p[data-line], h3.chapter-title, div.post-content *'
      );

      let replacedAny = false;

      paragraphs.forEach(p => {
        const textNodes = [];
        const originalLengths = [];
        let originalText = '';

        const walker = document.createTreeWalker(p, NodeFilter.SHOW_TEXT, null, false);
        while (walker.nextNode()) {
          const node = walker.currentNode;
          if (!node.nodeValue) continue;
          textNodes.push(node);
          originalLengths.push(node.nodeValue.length);
          originalText += node.nodeValue;
        }

        if (!originalText) return;

        // --- Step 1: Apply all replacements on flat text ---
        let replacedText = applyReplacements(originalText, replacements);

        // --- Step 2: Proportional slicing back into text nodes ---
        const totalOriginalLength = originalText.length;
        const totalReplacedLength = replacedText.length;
        let currentIndex = 0;

        textNodes.forEach((node, i) => {
          const proportion = originalLengths[i] / totalOriginalLength;
          let sliceLength = Math.round(proportion * totalReplacedLength);
          if (i === textNodes.length - 1) sliceLength = totalReplacedLength - currentIndex;
          node.nodeValue = replacedText.slice(currentIndex, currentIndex + sliceLength);
          currentIndex += sliceLength;
        });

        // --- Step 3: Wrap note entries in <span> ---
        textNodes.forEach(node => {
          let nodeVal = node.nodeValue;
          replacements.forEach(entry => {
            if (entry.note && entry.note.trim()) {
              let idx = 0;
              while ((idx = nodeVal.indexOf(entry.to, idx)) !== -1) {
                const parent = node.parentNode;
                if (!parent) break;

                const span = document.createElement('span');
                span.className = 'text-patch system_term';
                span.dataset.note = entry.note;
                span.textContent = entry.to;

                const before = nodeVal.slice(0, idx);
                const after = nodeVal.slice(idx + entry.to.length);

                if (before) parent.insertBefore(document.createTextNode(before), node);
                parent.insertBefore(span, node);
                nodeVal = after;
                idx = 0; // restart in remaining text
                node.nodeValue = nodeVal;
                if (!nodeVal) {
                  parent.removeChild(node);
                  break;
                }
              }
            }
          });
        });

        replacedAny = true;
      });

      if (replacedAny) console.log('Replacements done on chapter paragraphs.');
      return replacedAny;
    }


    function wrapNotesInParagraph(paragraph, replacements) {
      const html = paragraph.innerHTML;

      let newHTML = html;

      replacements.forEach(rep => {
        if (rep.note && rep.note.trim()) {
          // Regex escape
          const fromEscaped = rep.from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
          // Wrap the entire matching text in a span with data-note
          const regex = new RegExp(`(${fromEscaped})`, 'gi');
          newHTML = newHTML.replace(regex, `<span class="text-patch system_term" data-note="${rep.note}">$1</span>`);
        }
      });

      paragraph.innerHTML = newHTML;
    }

    function runReplacementMultiple(times = 1, delay = 100) {
      let count = 0;
    // ---------------------
    // Single global note modal
    // ---------------------

    function initInlinePopoverButtons() {
      const seriesId = window.seriesId || 'default-series';
      const shownotesRaw = localStorage.getItem('shownotes') || '';
      const shownotesSet = new Set(shownotesRaw.split(',').filter(Boolean));
      const notesVisible = shownotesSet.has(seriesId); // only visible if enabled for this series

      document.querySelectorAll('span.text-patch.system_term[data-note]').forEach(span => {
        // Skip if we already initialized a button for this span
        if (span.dataset.hasPencil === 'true') return;

        // Mark as having a pencil
        span.dataset.hasPencil = 'true';

        // Create tiny pencil button
        const btn = document.createElement('button');
        btn.textContent = '✎';
        btn.title = 'Show Note';
        Object.assign(btn.style, {
          fontSize: '10px',
          padding: '0 2px',
          marginLeft: '4px',
          cursor: 'pointer',
          lineHeight: '1',
          verticalAlign: 'middle',
          border: 'none',
          background: 'transparent',
          display: notesVisible ? 'inline' : 'none', // invisible by default
        });

        // Insert after the span
        span.insertAdjacentElement('afterend', btn);

        // Create popover
        const pop = document.createElement('div');
        pop.className = 'user-popover';
        Object.assign(pop.style, {
          position: 'absolute',
          zIndex: 9999,
          maxWidth: '280px',
          background: 'black',
          color: 'white',
          border: '1px solid #ccc',
          borderRadius: '4px',
          boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
          padding: '8px',
          display: 'none',
        });

        const desc = document.createElement('div');
        desc.className = 'patch-desc';
        desc.textContent = 'Note: ' + span.dataset.note;
        pop.appendChild(desc);
        document.body.appendChild(pop);

        // Store reference on span
        span._popover = pop;

        // Button click toggles popover
        btn.addEventListener('click', e => {
          e.stopPropagation();
          document.querySelectorAll('.user-popover').forEach(p => {
            if (p !== pop) p.style.display = 'none';
          });

          desc.textContent = 'Note: ' + span.dataset.note;

          const rect = span.getBoundingClientRect();
          // Position below the span
          pop.style.top = `${window.scrollY + rect.bottom + 5}px`; // 5px gap below
          pop.style.left = `${window.scrollX + rect.left}px`;

          // Toggle visibility
          pop.style.display = pop.style.display === 'block' ? 'none' : 'block';
        });
      });

      // Click outside -> hide all popovers
      document.addEventListener('click', () => {
        document.querySelectorAll('.user-popover').forEach(p => (p.style.display = 'none'));
      });
    }


    function removeExtraPencils() {
      document.querySelectorAll('span.text-patch.system_term[data-note]').forEach(span => {
        // find all button siblings immediately after this span
        const siblings = [];
        let next = span.nextElementSibling;

        while (next && next.tagName === 'BUTTON') {
          if (next.textContent.trim() === '✎') siblings.push(next);
          next = next.nextElementSibling;
        }

        // keep only the first ✎, remove the rest
        siblings.slice(1).forEach(btn => btn.remove());
      });
    }

    function nextPass() {
      const replaced = replaceTextInChapter();
      initInlinePopoverButtons();
      count++;
      if (count < times) {
        setTimeout(nextPass, delay);
      } else {
        // ✅ all passes finished, wait a bit then clean up
        setTimeout(removeExtraPencils, 150);
      }
    }

    nextPass(); // start first pass immediately
    }


    setTimeout(() => runReplacementMultiple(1, 100), 2000);



    // Reactive URL detection (History API hook)
    (function () {
      let lastUrl = location.href;

      function checkUrlChange() {
        const currentUrl = location.href;
        if (currentUrl !== lastUrl) {
          lastUrl = currentUrl;
          runReplacementMultiple(1, 100); // multi-pass on SPA navigation
          removeExtraPencils();
        }
      }

      // Wrap pushState
      const originalPushState = history.pushState;
      history.pushState = function () {
        originalPushState.apply(this, arguments);
        window.dispatchEvent(new Event("locationchange"));
      };

      // Wrap replaceState
      const originalReplaceState = history.replaceState;
      history.replaceState = function () {
        originalReplaceState.apply(this, arguments);
        window.dispatchEvent(new Event("locationchange"));
      };

      // Listen to Back/Forward
      window.addEventListener("popstate", () => window.dispatchEvent(new Event("locationchange")));


      // Unified listener
      window.addEventListener("locationchange", checkUrlChange);

      // ✅ Forcefully run once immediately on reload
      runReplacementMultiple(1, 100);
    })();

      // ===============
      // UI popup code
      // ===============

    function openPopup() {
      if (popup) return; // Prevent multiple

    popup = document.createElement('div');
    Object.assign(popup.style, {
      position: 'fixed',
      bottom: '70px',       // push above the main button
      right: '10px',
        width: '100vw',       // scale to viewport
       maxWidth: '370px',   // cap at your original
      height: 'auto',
      maxHeight: 'none',
      backgroundColor: '#fff',
      border: '1px solid #aaa',
      padding: '15px',
      boxShadow: '0 0 15px rgba(0,0,0,0.2)',
      overflow: 'visible',
      zIndex: '100000',     // below the button so button stays clickable
      fontFamily: 'Arial, sans-serif',
      fontSize: '14px',
    });
    document.body.appendChild(popup);

      // Toggle button for the list
      const toggleListBtn = document.createElement('button');
      toggleListBtn.textContent = 'List';
      toggleListBtn.style.marginBottom = '1px';
      toggleListBtn.style.display = 'block';
      toggleListBtn.style.color = 'black'
      styleButton(toggleListBtn);
      popup.appendChild(toggleListBtn);
    // Info button
    const infoBtn = document.createElement('button');
    infoBtn.textContent = 'Info';              // Label it properly
    infoBtn.style.marginLeft = '6px';          // space from the list button
    infoBtn.style.padding = '5px 10px';        // optional: smaller padding if needed
    infoBtn.style.alignSelf = 'flex-start';
    infoBtn.style.color = 'black';    // <-- shift up to top of flex container
    styleButton(infoBtn);

    // Container for buttons
    const topBtnContainer = document.createElement('div');
    topBtnContainer.style.display = 'flex';
    topBtnContainer.style.alignItems = 'center';
    topBtnContainer.appendChild(toggleListBtn);
    topBtnContainer.appendChild(infoBtn);
    popup.appendChild(topBtnContainer);


      const openRawsBtn = document.createElement('button');
openRawsBtn.textContent = 'Raws';
styleButton(openRawsBtn); // re-use your styleButton helper
openRawsBtn.style.marginLeft = '6px';
topBtnContainer.appendChild(openRawsBtn);


function openRawsModal() {
    // check if already exists
    if (document.querySelector('#rawsModal')) return;

    const modal = document.createElement('div');
    modal.id = 'rawsModal';
    Object.assign(modal.style, {
        position: 'fixed',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        background: '#fff',
        border: '1px solid #ccc',
        padding: '20px',
        zIndex: '100001', // above popup
        width: '320px',
        maxHeight: '70%',
        overflowY: 'auto',
        boxShadow: '0 0 12px rgba(0,0,0,0.3)',
        borderRadius: '6px',
    });

    document.body.appendChild(modal);

    const closeBtn = document.createElement('button');
    closeBtn.textContent = 'Close';
    closeBtn.style.float = 'right';
    closeBtn.style.cursor = 'pointer';
    closeBtn.onclick = () => modal.remove();
    modal.appendChild(closeBtn);

    const title = document.createElement('h3');
    title.textContent = 'Data-Hash Replacements';
    modal.appendChild(title);

    const listContainer = document.createElement('div');
    modal.appendChild(listContainer);

    const addContainer = document.createElement('div');
    addContainer.style.marginTop = '10px';
    modal.appendChild(addContainer);

    const dataHashInput = document.createElement('input');
    dataHashInput.placeholder = 'Data Hash';
    dataHashInput.style.width = 'calc(50% - 6px)';
    dataHashInput.style.marginRight = '6px';
    addContainer.appendChild(dataHashInput);

    const replacementInput = document.createElement('input');
    replacementInput.placeholder = 'Replacement';
    replacementInput.style.width = 'calc(50% - 6px)';
    addContainer.appendChild(replacementInput);

    const addBtn = document.createElement('button');
    addBtn.textContent = 'Add';
    addBtn.style.marginTop = '6px';
    addContainer.appendChild(addBtn);

    // load saved replacements
    let replacements = JSON.parse(localStorage.getItem('dataHashReplacements') || '[]');

    function saveAndRender() {
        localStorage.setItem('dataHashReplacements', JSON.stringify(replacements));
        renderList();
        runReplacementMultiple(2, 50);
    }

    function renderList() {
        listContainer.innerHTML = '';
        replacements.forEach((r, idx) => {
            const div = document.createElement('div');
            div.style.display = 'flex';
            div.style.justifyContent = 'space-between';
            div.style.marginBottom = '4px';

            const span = document.createElement('span');
            span.textContent = `${r.dataHash} → ${r.to}`;
            div.appendChild(span);

            const del = document.createElement('button');
            del.textContent = 'Delete';
            del.onclick = () => {
                replacements.splice(idx, 1);
                saveAndRender();
            };
            div.appendChild(del);

            listContainer.appendChild(div);
        });
    }

    addBtn.onclick = () => {
        const dh = dataHashInput.value.trim();
        const rp = replacementInput.value.trim();
        if (!dh || !rp) return;
        replacements.push({ dataHash: dh, to: rp });
        saveAndRender();
        dataHashInput.value = '';
        replacementInput.value = '';
    };

    function applyDataHashReplacements() {
        if (!replacements.length) return;
        const spans = document.querySelectorAll('span[data-hash]');
        spans.forEach(span => {
            const dhEntry = replacements.find(r => r.dataHash === span.getAttribute('data-hash'));
            if (dhEntry) span.textContent = dhEntry.to;
        });
    }

    function runReplacementMultiple(times = 2, delay = 50) {
        let count = 0;
        function nextPass() {
            applyDataHashReplacements();
            count++;
            if (count < times) setTimeout(nextPass, delay);
        }
        nextPass();
    }

    renderList();
    runReplacementMultiple(2, 50);
}
openRawsBtn.onclick = openRawsModal;

    // Info box (hidden by default)
    const infoBox = document.createElement('div');
    Object.assign(infoBox.style, {
      maxHeight: '0',
      overflow: 'hidden',
      backgroundColor: '#fff',
      color: 'black',
      border: '1px solid #000',
      padding: '0 10px',
      marginTop: '10px',
      fontSize: '13px',
      maxHeightWhenOpen: '200px',
      lineHeight: '1.4',
        overflowY: 'auto',
      transition: 'max-height 0.3s ease, padding 0.3s ease',
    });
    infoBox.innerHTML = `
      <div style="padding:10px 0;">
        <strong>Replacement System Info:</strong>
        <ul style="margin:5px 0; padding-left:18px;">
          <li><strong>Ignore Capital:</strong> Match case-insensitively.</li>
          <li><strong>Start of Sentence:</strong> Only capitalize if the word starts a sentence.</li>
          <li><strong>Fuzzy Match:</strong> Ignore boundaries, match anywhere.</li>
          <li><strong>Preserve Capital:</strong> Keep first letter capitalized if original was capitalized.</li>
          <li><strong>No Trailing Space:</strong> Trim trailing space in replacement.</li>
          <li><strong>Inside Dialogue Only:</strong> Replace only inside quotation marks.</li>
          <li><strong>Outside Dialogue Only:</strong> Replace only outside quotation marks.</li>
          <li><strong>Global:</strong> Makes the entry apply to all novels.</li>
          <li><strong>|ignore this|:</strong> Use before or after a word to ignore specific matches. Example: <code>|ignore |term</code> or <code>term| ignore|</code>. Spaces must be inside the <code>||</code>.</li>
          <li><strong>@ wildcard:</strong> Any character substitution. Example: <code>fr@t</code> replaces fret, frat, frit, etc.</li>
          <li><strong>^ special placeholder:</strong> Use <code>^</code> in Find like <code>Th^t</code> and in Replace like <code>Br^</code>. The character at <code>^</code> in Find will be preserved in the replacement.</li>
          <li><strong>Edit Entries:</strong> Use 'Show List', tap an entry to make edits and change the series ID. By default, it will be applied only to whatever novel you're on currently. If you entered a term while in Library, it will default to an empty series ID, which is global.</li>
    	  <li>The Show Note requires you have go to the term editor in the Show List, but this is a fragile feature, I don't recommend it.<li>
        </ul>
      </div>
    `;

    popup.appendChild(infoBox);

    // Info button click
    infoBtn.addEventListener('click', (e) => {
      if (infoBox.style.maxHeight && infoBox.style.maxHeight !== '0px') {
        infoBox.style.maxHeight = '0';
        infoBox.style.padding = '0 10px';
      } else {
        infoBox.style.maxHeight = '200px';  // fixed max height
        infoBox.style.padding = '10px';
      }
      e.stopPropagation();
    });

    // Close info box if clicking outside
    document.addEventListener('click', (e) => {
      if (!infoBox.contains(e.target) && e.target !== infoBtn) {
        infoBox.style.maxHeight = '0';
        infoBox.style.padding = '0 10px';
      }
    });

    // Create invert colors button
    const invertBtn = document.createElement('button');
    invertBtn.textContent = 'Invert';
    invertBtn.style.marginLeft = '6px';       // spacing from previous button
    invertBtn.style.padding = '5px 10px';
    invertBtn.style.alignSelf = 'flex-start';
    invertBtn.style.color = 'black';
    styleButton(invertBtn);
    topBtnContainer.appendChild(invertBtn);

    // Track state, restore from localStorage
    let isInverted = localStorage.getItem('replacementUIInverted') === 'true';

    // Function to apply inversion to popup and infoBox
    function applyInversion(state) {
      isInverted = state;
      localStorage.setItem('replacementUIInverted', state); // persist across reloads

      if (isInverted) {
        popup.style.backgroundColor = '#000';
        popup.style.color = '#fff';

        infoBox.style.backgroundColor = '#000';
        infoBox.style.color = '#fff';

        Array.from(popup.querySelectorAll('*')).forEach(el => {
          if (['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
            el.style.backgroundColor = '#222';
            el.style.color = '#fff';
          }
          if (el.tagName === 'BUTTON') {
            el.style.backgroundColor = '#333';
            el.style.color = '#fff';
          }
        });
      } else {
        popup.style.backgroundColor = '#fff';
        popup.style.color = '#000';

        infoBox.style.backgroundColor = '#fff';
        infoBox.style.color = '#000';

        Array.from(popup.querySelectorAll('*')).forEach(el => {
          if (!['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(el.tagName)) {
            el.style.backgroundColor = '';
            el.style.color = '';
          } else {
            // revert form elements and buttons to default
            if (el.tagName === 'BUTTON') {
              el.style.backgroundColor = '#eee';
              el.style.color = 'black';
            }
            if (['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
              el.style.backgroundColor = '#fff';
              el.style.color = '#000';
            }
          }
        });
      }
    }

    // Apply last saved state when popup opens
    applyInversion(isInverted);

    // Toggle inversion on click
    invertBtn.addEventListener('click', () => {
      applyInversion(!isInverted);
    });
    // Toggle inversion on click
    invertBtn.addEventListener('click', () => {
      applyInversion(!isInverted);
    });
    // === Show/Hide Notes Button ===
    const notesToggleBtn = document.createElement('button');
    notesToggleBtn.textContent = 'Show Note'; // initial text
    notesToggleBtn.style.marginLeft = '4px';  // spacing from previous button
      notesToggleBtn.style.padding = '2px 4px';

    // Apply consistent button style
    styleButton(notesToggleBtn);

    // Append next to Invert Colors
    topBtnContainer.appendChild(notesToggleBtn);

    const seriesId = window.seriesId || 'default-series';
    const shownotesRaw = localStorage.getItem('shownotes') || '';
    const shownotesSet = new Set(shownotesRaw.split(',').filter(Boolean));
    let notesInitiallyVisible = shownotesSet.has(seriesId);

    // Helper: show/hide ✎ only for this series
    function updatePencils(show) {
      document.querySelectorAll('span.text-patch.system_term[data-note]').forEach(span => {
        const btn = span.nextElementSibling;
        if (!btn || btn.textContent.trim() !== '✎') return;

        const noteSeries = span.dataset.series || 'default-series';
        if (noteSeries !== seriesId) return;

        btn.style.display = show ? 'inline-block' : 'none';
      });
    }

    // Initialize visibility
    updatePencils(notesInitiallyVisible);
    notesToggleBtn.textContent = notesInitiallyVisible ? 'Hide Note' : 'Show Note';

    // Click handler
    notesToggleBtn.addEventListener('click', () => {
      const showing = notesToggleBtn.textContent === 'Hide Note';
      const newShow = !showing;

      updatePencils(newShow);
      notesToggleBtn.textContent = newShow ? 'Hide Note' : 'Show Note';

      if (newShow) shownotesSet.add(seriesId);
      else shownotesSet.delete(seriesId);

      localStorage.setItem('shownotes', Array.from(shownotesSet).join(','));
    });

    // Participate in inversion
    function applyInversion(state) {
      isInverted = state;
      localStorage.setItem('replacementUIInverted', state);

      const color = isInverted ? '#fff' : '#000';
      const bg = isInverted ? '#333' : '#eee';

      Array.from(topBtnContainer.querySelectorAll('button')).forEach(btn => {
        btn.style.color = color;
        btn.style.backgroundColor = bg;
      });

      popup.style.backgroundColor = isInverted ? '#000' : '#fff';
      popup.style.color = color;
      infoBox.style.backgroundColor = isInverted ? '#000' : '#fff';
      infoBox.style.color = color;
    }

    // Apply last saved state
    applyInversion(isInverted);

    // Append button next to Invert Colors
    topBtnContainer.appendChild(notesToggleBtn);



    const rulesContainer = document.createElement('div');
    rulesContainer.style.display = 'flex';
    rulesContainer.style.flexWrap = 'wrap';
    rulesContainer.style.gap = '10px';
    rulesContainer.style.alignItems = 'center';
    rulesContainer.style.marginBottom = '10px';

    // Current flags state for the checkboxes
    const currentFlags = {
      ignoreCapital: false,
      startOfSentence: false,
      allInstances: false,
      preserveFirstCapital: false,
      global: false,
      noTrailingSpace: false,
      // New dialogue flags
      insideDialogueOnly: false,
      outsideDialogueOnly: false,
    };

    // Helper to create checkbox + label
    function createCheckbox(flagKey, labelText) {
      const label = document.createElement('label');
      label.style.userSelect = 'none';
      label.style.fontSize = '13px';
      label.style.display = 'flex';
      label.style.alignItems = 'center';
      label.style.gap = '4px';
      label.style.whiteSpace = 'nowrap';
      label.style.flex = '0 1 auto'; // shrink if needed, don't force 100%

      const input = document.createElement('input');
      input.type = 'checkbox';
      input.checked = currentFlags[flagKey];
      input.style.cursor = 'pointer';

      input.addEventListener('change', () => {
        currentFlags[flagKey] = input.checked;
        // If global toggled, optionally do something like disable series input if you have one
      });

      label.appendChild(input);
      label.appendChild(document.createTextNode(labelText));
      return label;
    }

    rulesContainer.appendChild(createCheckbox('ignoreCapital', 'Ignore Capital'));
    rulesContainer.appendChild(createCheckbox('startOfSentence', 'Start of Sentence'));
    rulesContainer.appendChild(createCheckbox('allInstances', 'Fuzzy Match'));
    rulesContainer.appendChild(createCheckbox('preserveFirstCapital', 'Preserve Capital'));
    rulesContainer.appendChild(createCheckbox('global', 'Global'));
    rulesContainer.appendChild(createCheckbox('noTrailingSpace', 'No Trailing Space'));
    rulesContainer.appendChild(createCheckbox('insideDialogueOnly', 'Edit Inside Dialogue'));
    rulesContainer.appendChild(createCheckbox('outsideDialogueOnly', 'Edit Outside Dialogue'));


    popup.appendChild(rulesContainer);

      // Container that will hold search, filter, and list
      const listUIContainer = document.createElement('div');
      listUIContainer.style.display = 'none'; // hidden by default
      popup.appendChild(listUIContainer);

      // Search input
      const searchInput = document.createElement('input');
      searchInput.type = 'search';
      searchInput.placeholder = 'Search terms...';
      searchInput.style.width = '100%';
      searchInput.style.marginBottom = '10px';
      listUIContainer.appendChild(searchInput);

      // Filter select
      const toggleFilter = document.createElement('select');
      ['Current Series', 'Global + Others', 'All'].forEach(optText => {
        const option = document.createElement('option');
        option.textContent = optText;
        toggleFilter.appendChild(option);
      });
      toggleFilter.style.width = '100%';
      toggleFilter.style.marginBottom = '10px';
      listUIContainer.appendChild(toggleFilter);

      // Buttons container
      const btnContainer = document.createElement('div');
      btnContainer.style.marginBottom = '10px';
      btnContainer.style.textAlign = 'right';

      const exportBtn = document.createElement('button');
      exportBtn.textContent = 'Export';
      styleButton(exportBtn);
      btnContainer.appendChild(exportBtn);
      exportBtn.style.marginRight = '4px';
      exportBtn.style.padding = '2px 4px';


      const importBtn = document.createElement('button');
      importBtn.textContent = 'Import';
      styleButton(importBtn);
      importBtn.style.marginLeft = '4px';
      btnContainer.appendChild(importBtn);
      importBtn.style.padding = '2px 4px';

    // --- Create current-series-only buttons ---
    const exportCurrentBtn = document.createElement('button');
    exportCurrentBtn.textContent = 'Export Current';
    styleButton(exportCurrentBtn);
    exportCurrentBtn.style.marginRight = '4px'; // spacing to the right
    exportCurrentBtn.style.padding = '2px 4px';


    const importCurrentBtn = document.createElement('button');
    importCurrentBtn.textContent = 'Import Current';
    styleButton(importCurrentBtn);
    importCurrentBtn.style.marginRight = '4px'; // spacing to the right
    importCurrentBtn.style.padding = '2px 4px';


    // --- Append current-series buttons to the existing container ---
    // Ensure they appear to the left of the original buttons
    btnContainer.insertBefore(exportCurrentBtn, exportBtn);
    btnContainer.insertBefore(importCurrentBtn, exportBtn);

    // --- Event listeners for current-series buttons ---
    exportCurrentBtn.addEventListener('click', function() {
      const seriesId = getCurrentSeriesId();
      if (!seriesId) {
        alert('No current series selected!');
        return;
      }

      // Only export entries whose series matches current series ID
      const exportData = [];
      for (const key in data) {
        (data[key] || []).forEach(entry => {
          if (entry.series === seriesId) {
            exportData.push(entry);
          }
        });
      }

      if (exportData.length === 0) {
        alert('No entries found for the current series.');
        return;
      }

      const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');
      a.href = url;
      a.download = `word-replacer-series-${seriesId}.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    });

    importCurrentBtn.addEventListener('click', function() {
      const seriesId = getCurrentSeriesId();
      if (!seriesId) {
        alert('No current series selected!');
        return;
      }

      const seriesKey = `series-${seriesId}`;
      if (!data[seriesKey]) data[seriesKey] = [];

      const inputFile = document.createElement('input');
      inputFile.type = 'file';
      inputFile.accept = '.json,.txt';

      inputFile.addEventListener('change', (e) => {
        if (!e.target.files.length) return;
        const file = e.target.files[0];
        const reader = new FileReader();

        reader.onload = (event) => {
          try {
            const parsed = JSON.parse(event.target.result);
            if (!Array.isArray(parsed)) {
              alert('Invalid format: must be an array of replacement entries.');
              return;
            }

            // Assign all imported entries to current series
            parsed.forEach(entry => {
              if (!entry.from || !entry.to) return;
              data[seriesKey].push({
                ...entry,
                series: seriesId,   // force current series
                enabled: true,
              });
            });

            saveData(data);
            renderList();
            replaceTextInChapter();
          } catch (err) {
            alert('Import failed: ' + err.message);
          }
        };

        reader.readAsText(file);
      });

      inputFile.click();
    });


      listUIContainer.appendChild(btnContainer);

      // List container
      const listContainer = document.createElement('div');
      listContainer.style.maxHeight = '260px';
      listContainer.style.overflowY = 'auto';
      listContainer.style.borderTop = '1px solid #ddd';
      listContainer.style.paddingTop = '8px';
      listUIContainer.appendChild(listContainer);

      // Style buttons helper
      function styleButton(btn) {
        btn.style.padding = '5px 12px';
        btn.style.fontSize = '13px';
        btn.style.cursor = 'pointer';
        btn.style.border = '1px solid #888';
        btn.style.borderRadius = '4px';
        btn.style.backgroundColor = '#eee';
        btn.style.userSelect = 'none';
      }
    toggleListBtn.addEventListener('click', () => {
      const isShowing = listUIContainer.style.display !== 'none';

      if (!isShowing) {
        // Show the list, hide the rules
        listUIContainer.style.display = 'block';
        rulesContainer.style.display = 'none';
        toggleListBtn.textContent = 'Hide List';

        renderList(); // refresh list when showing
      } else {
        // Hide the list, show the rules
        listUIContainer.style.display = 'none';
        rulesContainer.style.display = 'flex';
        toggleListBtn.textContent = 'Show List';
      }
    });


      // Get current series id helper
    function getCurrentSeriesId() {
      // Match new URL structure: /novel/{id}/
      const urlMatch = location.href.match(/\/novel\/(\d+)\//i);
      if (urlMatch) return urlMatch[1];

      // Fallback: check breadcrumb links
      const crumb = document.querySelector('.breadcrumb li.breadcrumb-item a[href*="/novel/"]');
      if (crumb) {
        const crumbMatch = crumb.href.match(/\/novel\/(\d+)\//i);
        if (crumbMatch) return crumbMatch[1];
      }

      return null;
    }


      // Render list of replacements
      function renderList() {
        listContainer.innerHTML = '';

        const seriesId = getCurrentSeriesId();

        let keysToShow = [];

        if (toggleFilter.value === 'Current Series') {
          if (seriesId) keysToShow = [`series-${seriesId}`];
          else keysToShow = [];
        } else if (toggleFilter.value === 'Global + Others') {
          keysToShow = Object.keys(data).filter(k => k !== `series-${seriesId}`);
        } else { // All
          keysToShow = Object.keys(data);
        }

        let allEntries = [];
        keysToShow.forEach(key => {
          if (data[key]) allEntries = allEntries.concat(data[key]);
        });

        const searchLower = searchInput.value.trim().toLowerCase();
    if (searchLower) {
      allEntries = allEntries.filter(e =>
        (e.from && e.from.toLowerCase().includes(searchLower)) ||
        (e.to && e.to.toLowerCase().includes(searchLower))
      );
    }

        if (allEntries.length === 0) {
          const emptyMsg = document.createElement('div');
          emptyMsg.textContent = 'No terms found.';
          emptyMsg.style.fontStyle = 'italic';
          listContainer.appendChild(emptyMsg);
          return;
        }

    allEntries.forEach((entry) => {
      const row = document.createElement('div');
      row.style.display = 'flex';
      row.style.alignItems = 'flex-start'; // align top when text wraps
      row.style.justifyContent = 'space-between'; // space between left text and right controls
      row.style.marginBottom = '6px';
      row.style.width = '100%';

      // ---- Left side (text) ----
      const textContainer = document.createElement('div');
      textContainer.style.display = 'flex';
      textContainer.style.flexDirection = 'row';
      textContainer.style.flexWrap = 'wrap';
      textContainer.style.flexGrow = '1';
      textContainer.style.minWidth = '0'; // needed for wrapping

      const fromSpan = document.createElement('span');
      fromSpan.textContent = entry.from;
      fromSpan.style.cursor = 'pointer';
      fromSpan.style.userSelect = 'none';
      fromSpan.style.color = '#007bff';
      fromSpan.style.wordBreak = 'break-word';
      fromSpan.style.overflowWrap = 'anywhere';
      fromSpan.addEventListener('click', () => {
        openEditDialog(entry);
      });

      const toSpan = document.createElement('span');
      toSpan.textContent = ' → ' + entry.to;
      toSpan.style.marginLeft = '8px';
      toSpan.style.wordBreak = 'break-word';
      toSpan.style.overflowWrap = 'anywhere';

      textContainer.appendChild(fromSpan);
      textContainer.appendChild(toSpan);

      // ---- Right side (controls) ----
      const controls = document.createElement('div');
      controls.style.display = 'flex';
      controls.style.alignItems = 'center';
      controls.style.flexShrink = '0'; // don’t shrink controls
      controls.style.marginLeft = '12px';

      const enabledCheckbox = document.createElement('input');
      enabledCheckbox.type = 'checkbox';
      enabledCheckbox.checked = entry.enabled ?? true;
      enabledCheckbox.title = 'Enable / Disable this replacement';
      enabledCheckbox.style.marginRight = '8px';
      enabledCheckbox.addEventListener('change', () => {
        entry.enabled = enabledCheckbox.checked;
        saveData(data);
        replaceTextInChapter();
      });

      const deleteBtn = document.createElement('button');
      deleteBtn.textContent = '✕';
      styleButton(deleteBtn);
      deleteBtn.title = 'Delete this replacement';
      deleteBtn.addEventListener('click', () => {
        deleteEntry(entry);
      });

      controls.appendChild(enabledCheckbox);
      controls.appendChild(deleteBtn);

      // ---- Combine ----
      row.appendChild(textContainer);
      row.appendChild(controls);

      listContainer.appendChild(row);
    });
      }

        // Delete entry
        function deleteEntry(entry) {
          for (const key in data) {
            const arr = data[key];
            const idx = arr.findIndex(e => e.from === entry.from);
            if (idx >= 0) {
              arr.splice(idx, 1);
              if (arr.length === 0 && key !== 'global') {
                delete data[key];
              }
              saveData(data);
              renderList();
              replaceTextInChapter();
              break;
            }
          }
        }

        // Edit dialog popup
        function openEditDialog(entry) {
          const modalBg = document.createElement('div');
          Object.assign(modalBg.style, {
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            backgroundColor: 'rgba(0,0,0,0.5)',
            zIndex: 100001,
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
          });

    const modal = document.createElement('div');
    Object.assign(modal.style, {
      backgroundColor: isInverted ? '#000' : 'white',
      color: isInverted ? '#fff' : '#000',
      padding: '20px',
      borderRadius: '8px',
      width: '320px',
      boxShadow: '0 0 15px rgba(0,0,0,0.3)',
      fontSize: '14px',
    });

          modalBg.appendChild(modal);

          // Title
          const title = document.createElement('h3');
          title.textContent = 'Edit Replacement';
          title.style.marginTop = '0';
          modal.appendChild(title);

          // From input
          const fromLabel = document.createElement('label');
          fromLabel.textContent = 'Find: ';
          const fromInput = document.createElement('input');
          fromInput.type = 'text';
          fromInput.value = entry.from;
          fromInput.style.width = '100%';
          fromInput.required = true;
          fromLabel.appendChild(fromInput);
          modal.appendChild(fromLabel);

          modal.appendChild(document.createElement('br'));

          // To input
          const toLabel = document.createElement('label');
          toLabel.textContent = 'Replace with: ';
          const toInput = document.createElement('input');
          toInput.type = 'text';
          toInput.value = entry.to;
          toInput.style.width = '100%';
          toLabel.appendChild(toInput);
          modal.appendChild(toLabel);

          modal.appendChild(document.createElement('br'));

    	  const noteBtn = document.createElement('button');
          noteBtn.textContent = entry.note ? 'Edit Note' : 'Add Note';
          noteBtn.style.marginTop = '8px';
          noteBtn.style.display = 'block';

          modal.appendChild(noteBtn);
    noteBtn.addEventListener('click', () => openNoteModal(entry, noteBtn));


    function openNoteModal(entry, buttonRef) {
      const noteModalBg = document.createElement('div');
      Object.assign(noteModalBg.style, {
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100vw',
        height: '100vh',
        backgroundColor: 'rgba(0,0,0,0.4)',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        zIndex: 999999,
      });

      const noteModal = document.createElement('div');
      Object.assign(noteModal.style, {
        backgroundColor: 'black',
        color: 'black',
        padding: '20px',
        borderRadius: '8px',
        width: '280px',
        boxShadow: '0 0 15px rgba(0,0,0,0.3)',
        fontSize: '14px',
      });

      noteModalBg.appendChild(noteModal);

      const noteTitle = document.createElement('h3');
      noteTitle.textContent = 'Add Note';
      noteTitle.style.marginTop = '0';
      noteModal.appendChild(noteTitle);
      noteTitle.style.color = 'white';

      const noteInput = document.createElement('textarea');
      noteInput.rows = 3;
      noteInput.maxLength = 30;
      noteInput.value = entry.note || '';
      noteInput.style.width = '100%';
      noteInput.placeholder = 'Enter a short note';
      noteModal.appendChild(noteInput);

      const noteSave = document.createElement('button');
      noteSave.textContent = 'Save';
      noteSave.style.marginRight = '10px';

      const noteCancel = document.createElement('button');
      noteCancel.textContent = 'Cancel';

      noteModal.appendChild(noteSave);
      noteModal.appendChild(noteCancel);

      document.body.appendChild(noteModalBg);

      noteSave.addEventListener('click', () => {
        entry.note = noteInput.value.trim().slice(0, 30);
        if (buttonRef) buttonRef.textContent = entry.note ? 'Edit Note' : 'Add Note';
        document.body.removeChild(noteModalBg);
      });

      noteCancel.addEventListener('click', () => {
        document.body.removeChild(noteModalBg);
      });
    }




          // Enabled checkbox
          const enabledLabel = document.createElement('label');
          const enabledInput = document.createElement('input');
          enabledInput.type = 'checkbox';
          enabledInput.checked = entry.enabled ?? true;
          enabledLabel.appendChild(enabledInput);
          enabledLabel.append(' Enabled');
          enabledLabel.style.userSelect = 'none';
          modal.appendChild(enabledLabel);

          modal.appendChild(document.createElement('br'));

          // Flags container
          const flags = [
            { key: 'ignoreCapital', label: 'Ignore Capitalization' },
            { key: 'startOfSentence', label: 'Match Whether Start of Sentence' },
            { key: 'allInstances', label: 'Fuzzy Match' },
            { key: 'preserveFirstCapital', label: 'Preserve First Capital Letter' },
            { key: 'noTrailingSpace', label: 'No Trailing Space' },
            { key: 'insideDialogueOnly', label: 'Edit Only Inside Dialogue' },
            { key: 'outsideDialogueOnly', label: 'Edit Only Outside Dialogue' },
          ];

          flags.forEach(f => {
            const flagLabel = document.createElement('label');
            const flagInput = document.createElement('input');
            flagInput.type = 'checkbox';
            flagInput.checked = entry[f.key] ?? false;
            flagLabel.appendChild(flagInput);
            flagLabel.append(' ' + f.label);
            flagLabel.style.display = 'block';
            flagLabel.style.userSelect = 'none';
            modal.appendChild(flagLabel);

            flagInput.addEventListener('change', () => {
              entry[f.key] = flagInput.checked;
            });
          });

          modal.appendChild(document.createElement('br'));

          // Series input (optional)
          const seriesLabel = document.createElement('label');
          seriesLabel.textContent = 'Series ID (empty = global): ';
          const seriesInput = document.createElement('input');
          seriesInput.type = 'text';
          seriesInput.value = entry.series || '';
          seriesInput.style.width = '100%';
          seriesLabel.appendChild(seriesInput);
          modal.appendChild(seriesLabel);

          modal.appendChild(document.createElement('br'));

          // Buttons
          const btnSave = document.createElement('button');
          btnSave.textContent = 'Save';
          btnSave.style.marginRight = '10px';

          const btnCancel = document.createElement('button');
          btnCancel.textContent = 'Cancel';

          modal.appendChild(btnSave);
          modal.appendChild(btnCancel);

          // Save handler
    btnSave.addEventListener('click', () => {
      let f = fromInput.value;
      const t = toInput.value;



      // Only trim the search word if this entry has noTrailingSpace enabled
      if (entry.noTrailingSpace) {
        f = f.trim();
      }
            // Update entry properties
            // If series changed, move to correct group
            const oldSeriesKey = entry.series ? `series-${entry.series}` : 'global';
            const newSeriesKey = seriesInput.value ? `series-${seriesInput.value}` : 'global';

            if (oldSeriesKey !== newSeriesKey) {
              // Remove from old array
              if (data[oldSeriesKey]) {
                const idx = data[oldSeriesKey].indexOf(entry);
                if (idx >= 0) data[oldSeriesKey].splice(idx, 1);
                if (data[oldSeriesKey].length === 0 && oldSeriesKey !== 'global') {
                  delete data[oldSeriesKey];
                }
              }
              // Add to new array
              if (!data[newSeriesKey]) data[newSeriesKey] = [];
              data[newSeriesKey].push(entry);
              entry.series = seriesInput.value.trim();
            }

            entry.from = f;
            entry.to = t;
            entry.enabled = enabledInput.checked;

            saveData(data);
            renderList();
            replaceTextInChapter();
            closeEditModal();
          });

          btnCancel.addEventListener('click', () => {
            closeEditModal();
          });

          function closeEditModal() {
            modalBg.remove();
          }

          document.body.appendChild(modalBg);
        }

        // Add new entry controls at bottom
        const addNewLabel = document.createElement('div');
        addNewLabel.textContent = 'Add New Replacement:';
        addNewLabel.style.marginTop = '15px';
        addNewLabel.style.fontWeight = 'bold';
        popup.appendChild(addNewLabel);

    const inputContainer = document.createElement('div');
    inputContainer.style.display = 'flex';
    inputContainer.style.gap = '6px';
    inputContainer.style.marginTop = '6px';
    inputContainer.style.flexWrap = 'nowrap';  // keep inputs + button on one line
    inputContainer.style.alignItems = 'center'; // vertical alignment

    const fromInputNew = document.createElement('input');
    fromInputNew.placeholder = 'Find';
    fromInputNew.style.flex = '1';   // take available width
    fromInputNew.style.minWidth = '60px'; // prevent shrinking too much
    inputContainer.appendChild(fromInputNew);
    const toInputNew = document.createElement('input');
    toInputNew.placeholder = 'Replace with';
    toInputNew.style.flex = '1';
    toInputNew.style.minWidth = '60px';
    inputContainer.appendChild(toInputNew);
        // Autocomplete suggestion box for "Replace with"
    const replaceSuggestionBox = document.createElement('ul');
    Object.assign(replaceSuggestionBox.style, {
      position: 'absolute',
      zIndex: 9999,
      border: '1px solid #ccc',
      background: '#000',     // solid black background
      color: '#fff',          // white text
      listStyle: 'none',
      margin: 0,
      padding: 0,
      maxHeight: '120px',
      overflowY: 'auto',
      display: 'none',
      opacity: '1',           // fully opaque
    });
    inputContainer.appendChild(replaceSuggestionBox);

    // Helper to position suggestion box above the input
    function positionReplaceBox() {
      // Make it visible temporarily to measure its height
      replaceSuggestionBox.style.display = 'block';

      const rect = toInputNew.getBoundingClientRect();
      const containerRect = inputContainer.getBoundingClientRect();

      // Place the bottom of the box at the top of the input
      replaceSuggestionBox.style.left = (toInputNew.offsetLeft) + 'px';
      replaceSuggestionBox.style.top = (toInputNew.offsetTop - replaceSuggestionBox.offsetHeight) + 'px';
    }


    // Input listener
    toInputNew.addEventListener('input', () => {
      const val = toInputNew.value.trim().toLowerCase();
      replaceSuggestionBox.innerHTML = '';

      if (val.length < 2) {
        replaceSuggestionBox.style.display = 'none';
        return;
      }

      // Pull all "to" values from data across all series + global
      const allTerms = Object.values(data)
        .flat()
        .map(entry => entry.to)
        .filter((v, i, self) => v && self.indexOf(v) === i); // unique & non-empty

      const matches = allTerms.filter(term => term.toLowerCase().includes(val));

      if (!matches.length) {
        replaceSuggestionBox.style.display = 'none';
        return;
      }

    matches.forEach(term => {
      const li = document.createElement('li');
      li.textContent = term;
      li.style.padding = '4px 6px';
      li.style.cursor = 'pointer';
      li.style.background = '#000';  // default black background
      li.style.color = '#fff';       // default white text

      li.addEventListener('mousedown', (e) => {
        e.preventDefault(); // prevent blur
        toInputNew.value = term;
        replaceSuggestionBox.style.display = 'none';
      });

      // Remove the previous hover effects
      li.addEventListener('mouseover', () => {
        li.style.background = '#111'; // slightly lighter black on hover
      });
      li.addEventListener('mouseout', () => {
        li.style.background = '#000'; // back to solid black
      });

      replaceSuggestionBox.appendChild(li);
    });


      positionReplaceBox();
      replaceSuggestionBox.style.display = 'block';
    });

    // Hide suggestions when clicking outside
    document.addEventListener('click', (e) => {
      if (!inputContainer.contains(e.target)) {
        replaceSuggestionBox.style.display = 'none';
      }
    });


        const addBtn = document.createElement('button');
        addBtn.textContent = 'Add';
        styleButton(addBtn);

    addBtn.addEventListener('click', () => {
    let f = fromInputNew.value;
    const t = toInputNew.value;

    // Check the actual checkbox state at creation time
    const noTrailingSpaceChecked = document.querySelector('#noTrailingSpaceCheckboxId')?.checked;
    if (noTrailingSpaceChecked) {
      f = f.trim();
    }

      if (!f) {
        alert('Find term cannot be empty');
        return;
      }

      // Use global checkbox to decide seriesId: empty means global
      const seriesId = currentFlags.global ? '' : getCurrentSeriesId();
      const seriesKey = seriesId ? `series-${seriesId}` : 'global';

      if (!data[seriesKey]) data[seriesKey] = [];

      // Avoid duplicates in that series/global bucket
      if (data[seriesKey].some(e => e.from.toLowerCase() === f.toLowerCase())) {
        alert('This find term already exists in this series/global.');
        return;
      }

    data[seriesKey].push({
      from: f,
      to: t,
      note: '', // <-- new
      enabled: true,
      ignoreCapital: currentFlags.ignoreCapital,
      startOfSentence: currentFlags.startOfSentence,
      allInstances: currentFlags.allInstances,
      preserveFirstCapital: currentFlags.preserveFirstCapital,
      series: seriesId || '',
      noTrailingSpace: currentFlags.noTrailingSpace,
      insideDialogueOnly: currentFlags.insideDialogueOnly,
      outsideDialogueOnly: currentFlags.outsideDialogueOnly,
    });

      saveData(data);
      fromInputNew.value = '';
      toInputNew.value = '';
      renderList();
      replaceTextInChapter();
    });


        inputContainer.appendChild(fromInputNew);
        inputContainer.appendChild(toInputNew);
        inputContainer.appendChild(addBtn);
        popup.appendChild(inputContainer);

        // Export button
        exportBtn.addEventListener('click', () => {
          const dataStr = JSON.stringify(data, null, 2);
          const blob = new Blob([dataStr], { type: 'application/json' });
          const url = URL.createObjectURL(blob);

          const a = document.createElement('a');
          a.href = url;
          a.download = 'word-replacer-data.json';
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);

          URL.revokeObjectURL(url);
        });

        // Import button
        importBtn.addEventListener('click', () => {
          const inputFile = document.createElement('input');
          inputFile.type = 'file';
          inputFile.accept = '.json,.txt';

          inputFile.addEventListener('change', (e) => {
            if (!e.target.files.length) return;
            const file = e.target.files[0];
            const reader = new FileReader();

    function importData(parsed) {
      if (typeof parsed === 'object' && !Array.isArray(parsed)) {
        // Assume full structured data (keys like 'global', 'series-...')
        // Merge imported data with existing data (append pairs)
        for (const key in parsed) {
          if (!data[key]) data[key] = [];
          // Append new entries (avoid duplicates if needed)
          parsed[key].forEach(newEntry => data[key].push(newEntry));
        }
      } else if (Array.isArray(parsed)) {
        // Array of pairs (old simple format)
        if (!data.global) data.global = [];
        const newPairs = parsed.map(pair => {
          if (!Array.isArray(pair) || pair.length < 2) return null;
          return {
            from: pair[0],
            to: pair[1],
            enabled: true,
            startOfSentence: false,
            ignoreCapital: false,
            allInstances: false,
            preserveFirstCapital: false,
            global: true,
            seriesId: ''
          };
        }).filter(Boolean);
        data.global.push(...newPairs);
      } else {
        alert('Import failed: unsupported format.');
        return;
      }
      saveData(data);
      renderList();
      replaceTextInChapter();
    }

    reader.onload = (e) => {
      try {
        const parsed = JSON.parse(e.target.result);
        importData(parsed);
        alert('Import successful!');
      } catch (err) {
        alert('Invalid JSON: ' + err.message);
      }
    };
            reader.readAsText(file);
          });

          inputFile.click();
        });

        // Search and filter event handlers
        searchInput.addEventListener('input', renderList);
        toggleFilter.addEventListener('change', renderList);

        renderList();

        document.body.appendChild(popup);
      }

    })();