Word Replacer Universal

Use it however, but it is fragile, unused features

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Word Replacer Universal
// @match        https://*/*
// @version      31.1
// @namespace    WordReplacer for all sites
// @description  Use it however, but it is fragile, unused features
// @grant        none
// ==/UserScript==

    (function () {
      'use strict';

      const STORAGE_KEY = 'wordReplacerKeys12345';

     
      let data = loadData();

      
    const mainButton = document.createElement('button');
    mainButton.textContent = '☰';
    Object.assign(mainButton.style, {
      position: 'fixed',
      bottom: '20px',    
      right: '20px',
      zIndex: '100001',    
      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;
      }
    }
    const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\",]/g, '\\$&');


    function isStartOfSentence(index, fullText) {
      if (index === 0) return true; 

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

      if (/^\s*$/.test(before)) return true;


      const trimmed = before.replace(/\s+$/, '');


      if (/[.!?…]["”’')\]]*$/.test(trimmed)) return true;


      if (/[\n\r]\s*$/.test(before)) return true;


      if (/["“”'‘(\[]\s*$/.test(before)) return true;


      if (/["“”]$/.test(before)) return true;


      if (/Chapter\s+\d+:\s*,?\s*$/.test(before)) return true;

      return false;
    }

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


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

  return quoteCount % 2 === 1;
}


      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) {

            return new RegExp(
                basePattern + `(?![\\s"“”'’,.-]+${escapeRegex(ignoreTerm)})`,
                flags
            );
        } else {
            return new RegExp(basePattern, flags);
        }
    }
    function applyReplacements(text, replacements) {
      let replacedText = text;
      const WILDCARD = '@';

    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();
        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];
        }


        if (quoteChars.includes(searchTerm.charAt(0))) {
          searchTerm = `[${quoteChars}]` + escapeRegex(searchTerm.slice(1));
        } else {
          searchTerm = escapeRegex(searchTerm);
        }


        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'), '.');

 
        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]|$)`;

    
        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*)`; 
          } 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]);


          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;
    }

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 targets = document.querySelectorAll('*');

    let replacedAny = false;

    targets.forEach(container => {
        const walker = document.createTreeWalker(
            container,
            NodeFilter.SHOW_TEXT,
            null,
            false
        );

        let node;
        while ((node = walker.nextNode())) {
            if (!node.nodeValue || !node.nodeValue.trim()) continue;

            let newText = applyReplacements(node.nodeValue, replacements);
            if (newText !== node.nodeValue) {
                node.nodeValue = newText;
                replacedAny = true;
            }
        }
    });

    if (replacedAny) console.log('Simple replacements done.');
    return replacedAny;
}



function runReplacementMultiple(times = 1, delay = 100) {
  let count = 0;

  function nextPass() {
    replaceTextInChapter();
    count++;

    if (count < times) {
      setTimeout(nextPass, delay);
    }
  }

  nextPass();
}




   
    (function () {
      let lastUrl = location.href;

      function checkUrlChange() {
        const currentUrl = location.href;
        if (currentUrl !== lastUrl) {
          lastUrl = currentUrl;
          runReplacementMultiple(1, 100); 
          removeExtraPencils();
          applyDataHashReplacements();
        }
      }


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


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


      window.addEventListener("popstate", () => window.dispatchEvent(new Event("locationchange")));



      window.addEventListener("locationchange", checkUrlChange);


      runReplacementMultiple(1, 100);
      applyDataHashReplacements(1,100);
    })();


    function openPopup() {
      if (popup) return;

    popup = document.createElement('div');
    Object.assign(popup.style, {
      position: 'fixed',
      bottom: '70px',      
      right: '10px',
        width: '100vw',     
       maxWidth: '370px',  
      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',    
      fontFamily: 'Arial, sans-serif',
      fontSize: '14px',
    });
    document.body.appendChild(popup);

   
      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);

    const infoBtn = document.createElement('button');
    infoBtn.textContent = 'Info';           
    infoBtn.style.marginLeft = '6px';          
    infoBtn.style.padding = '5px 10px';        
    infoBtn.style.alignSelf = 'flex-start';
    infoBtn.style.color = 'black';   
    styleButton(infoBtn);


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




    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>
            <li><strong>Raws:</strong> Match the raw text. You can copy the raw from popover, only works on the clickable terms.</li>
        </ul>
      </div>
    `;

    popup.appendChild(infoBox);

 
    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';  
        infoBox.style.padding = '10px';
      }
      e.stopPropagation();
    });


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

const invertBtn = document.createElement('button');
invertBtn.textContent = 'Invert';
invertBtn.style.marginLeft = '6px';
invertBtn.style.padding = '5px 10px';
invertBtn.style.alignSelf = 'flex-start';
invertBtn.style.color = '#000';
styleButton(invertBtn);
topBtnContainer.appendChild(invertBtn);


let isInverted = localStorage.getItem('replacementUIInverted') === 'true';

function applyInversion(state) {
  isInverted = state;
  localStorage.setItem('replacementUIInverted', String(state));

  const colour = isInverted ? '#fff' : '#000';
  const bg = isInverted ? '#000' : '#fff';

  if (!popup) return; 

  popup.style.backgroundColor = bg;
  popup.style.color = colour;infoBox && (infoBox.style.color = colour);
  popup.querySelectorAll('*').forEach(el => {
    const tag = el.tagName;
    if (tag === 'BUTTON' || tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
      el.style.backgroundColor = bg;
      el.style.color = colour;} else {
      el.style.color = colour; 
    }
  });


  topBtnContainer.querySelectorAll('button').forEach(btn => {
    btn.style.backgroundColor = bg;
    btn.style.color = colour;});
}
applyInversion(isInverted);


invertBtn.addEventListener('click', () => {
  applyInversion(!isInverted);
});




    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';

 
    const currentFlags = {
      ignoreCapital: false,
      startOfSentence: false,
      allInstances: false,
      preserveFirstCapital: false,
      global: false,
      noTrailingSpace: false,
      insideDialogueOnly: false,
      outsideDialogueOnly: false,
    };

    
    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'; 

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

      input.addEventListener('change', () => {
        currentFlags[flagKey] = input.checked;
  
      });

      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);


      const listUIContainer = document.createElement('div');
      listUIContainer.style.display = 'none';
      popup.appendChild(listUIContainer);

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

    
      const toggleFilter = document.createElement('select');
      ['Global'].forEach(optText => {
        const option = document.createElement('option');
        option.textContent = optText;
        toggleFilter.appendChild(option);
      });
      toggleFilter.style.width = '100%';
      toggleFilter.style.marginBottom = '10px';
      listUIContainer.appendChild(toggleFilter);

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



   
      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);

     
      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) {
      
        listUIContainer.style.display = 'block';
        rulesContainer.style.display = 'none';
        toggleListBtn.textContent = 'List';

        renderList(); 
      } else {
       
        listUIContainer.style.display = 'none';
        rulesContainer.style.display = 'flex';
        toggleListBtn.textContent = 'List';
      }
    });


     
    function getCurrentSeriesId() {
    
      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;
    }


      
      function renderList() {
        listContainer.innerHTML = '';

        const seriesId = getCurrentSeriesId();

        let keysToShow = [];

        if (toggleFilter.value === 'All') {
          if (seriesId) keysToShow = [`series-${seriesId}`];
          else keysToShow = [];
        } else if (toggleFilter.value === 'Global + Others') {
          keysToShow = Object.keys(data).filter(k => k !== `series-${seriesId}`);
        } else { 
          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'; 
      row.style.justifyContent = 'space-between'; 
      row.style.marginBottom = '6px';
      row.style.width = '100%';

    
      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'; 

      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);

     
      const controls = document.createElement('div');
      controls.style.display = 'flex';
      controls.style.alignItems = 'center';
      controls.style.flexShrink = '0'; 
      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);

   
      row.appendChild(textContainer);
      row.appendChild(controls);

      listContainer.appendChild(row);
    });
      }

    
        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;
            }
          }
        }

       
        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);

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

          
          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'));

         
          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'));






    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);
      });
    }


          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'));


          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'));

     
          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'));

  
          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);

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



     
      if (entry.noTrailingSpace) {
        f = f.trim();
      }
     
            const oldSeriesKey = entry.series ? `series-${entry.series}` : 'global';
            const newSeriesKey = seriesInput.value ? `series-${seriesInput.value}` : 'global';

            if (oldSeriesKey !== newSeriesKey) {
     
              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];
                }
              }
        
              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);
        }

  
        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';  
    inputContainer.style.alignItems = 'center'; 

    const fromInputNew = document.createElement('input');
    fromInputNew.placeholder = 'Find';
    fromInputNew.style.flex = '1';  
    fromInputNew.style.minWidth = '60px'; 
    inputContainer.appendChild(fromInputNew);
    const toInputNew = document.createElement('input');
    toInputNew.placeholder = 'Replace with';
    toInputNew.style.flex = '1';
    toInputNew.style.minWidth = '60px';
    inputContainer.appendChild(toInputNew);
    const replaceSuggestionBox = document.createElement('ul');
    Object.assign(replaceSuggestionBox.style, {
      position: 'absolute',
      zIndex: 9999,
      border: '1px solid #ccc',
      background: '#000',    
      color: '#fff',         
      listStyle: 'none',
      margin: 0,
      padding: 0,
      maxHeight: '120px',
      overflowY: 'auto',
      display: 'none',
      opacity: '1',       
    });
    inputContainer.appendChild(replaceSuggestionBox);

   
    function positionReplaceBox() {
   
      replaceSuggestionBox.style.display = 'block';

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

     
      replaceSuggestionBox.style.left = (toInputNew.offsetLeft) + 'px';
      replaceSuggestionBox.style.top = (toInputNew.offsetTop - replaceSuggestionBox.offsetHeight) + 'px';
    }


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

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

 
      const allTerms = Object.values(data)
        .flat()
        .map(entry => entry.to)
        .filter((v, i, self) => v && self.indexOf(v) === i); 

      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';  
      li.style.color = '#fff';      

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

     
      li.addEventListener('mouseover', () => {
        li.style.background = '#111'; 
      });
      li.addEventListener('mouseout', () => {
        li.style.background = '#000'; 
      });

      replaceSuggestionBox.appendChild(li);
    });


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


    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;

    const noTrailingSpaceChecked = document.querySelector('#noTrailingSpaceCheckboxId')?.checked;
    if (noTrailingSpaceChecked) {
      f = f.trim();
    }

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

  
      const seriesId = currentFlags.global ? '' : getCurrentSeriesId();
      const seriesKey = seriesId ? `series-${seriesId}` : 'global';

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

 
      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: '', 
      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);

     
        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);
        });

      
        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)) {

        for (const key in parsed) {
          if (!data[key]) data[key] = [];
    
          parsed[key].forEach(newEntry => data[key].push(newEntry));
        }
      } else if (Array.isArray(parsed)) {
   
        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();
        });

     
        searchInput.addEventListener('input', renderList);
        toggleFilter.addEventListener('change', renderList);

        renderList();

        document.body.appendChild(popup);
      }

    
      startReplaceLoop();

    })();