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

    })();