House of Web

Change text to reflect the style of 'House of Leaves'

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         House of Web
// @namespace    https://github.com/agoramachina/House-of-Web
// @version      2.0.1
// @description  Change text to reflect the style of 'House of Leaves'
// @author       agoramachina (forked from DetectiveR's House of Chrome extension)
// @match        *://*/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
  'use strict';

  // =============================================================================
  // SETTINGS
  // =============================================================================

  // Set to false to only match whole words (e.g., "house" but not "household")
  // Set to true to match words inside other words (e.g., "house" in "household")
  const includeSubstrings = true;

  // =============================================================================
  // HOUSE - translations across languages (19 languages)
  // =============================================================================
  const houseWords = [
    // Latin script - European
    'house',        // English
    'maison',       // French
    'haus',         // German
    'casa',         // Spanish / Italian / Portuguese
    'domus',        // Latin
    'huis',         // Dutch
    'hus',          // Swedish / Norwegian / Danish

    // Cyrillic script
    'Дом',          // Russian
    'будинок',      // Ukrainian
    'къща',         // Bulgarian

    // Greek script
    'σπίτι',        // Greek

    // CJK (Chinese, Japanese, Korean)
    '家',           // Japanese / Chinese
    '집',           // Korean

    // Other scripts
    'בית',          // Hebrew
    'بيت',          // Arabic
    'വീട്',         // Malayalam

    // Constructed languages
    'juH qach',     // Klingon (tlhIngan Hol) - "home building"
    '-kelek',       // Vulcan (Golic Vulcan)
    'zdani',        // Lojban
  ];

  // =============================================================================
  // MINOTAUR - translations across languages (19 languages)
  // =============================================================================
  const minotaurWords = [
    // Latin script - European (most derive from Greek Μινώταυρος)
    'minotaur',     // English
    'minotaure',    // French
    'minotauro',    // Spanish / Italian / Portuguese
    'minotauros',   // Greek (romanized) / Turkish
    'minotaurus',   // Latin / German / Dutch

    // Cyrillic script
    'Минотавр',     // Russian
    'Мінотавр',     // Ukrainian
    'Минотавър',    // Bulgarian

    // Greek script
    'Μινώταυρος',   // Greek (native)

    // CJK (Chinese, Japanese, Korean)
    'ミノタウロス',       // Japanese (Minotaurosu)
    'ミーノータウロス',   // Japanese (alternate: Mīnōtaurosu)
    '미노타우로스',      // Korean
    '弥诺陶洛斯',       // Chinese (Simplified)

    // Other scripts
    'מינוטאור',     // Hebrew
    'مينوتور',      // Arabic
    'മിനോട്ടോർ',  // Malayalam

    // Constructed languages
    'veqlargh',     // Klingon - Fek'lhr (mythological demon/beast)
    'stislak',      // Vulcan
    "cizda'u",      // Lojban (monster/strange creature)
  ];

  // =============================================================================
  // STYLES
  // =============================================================================
  const houseStyle = 'color:#0047bb; font-family:Courier, monospace';
  const minotaurStyle = 'color:red; font-family:Courier, monospace; text-decoration: line-through;';

  // =============================================================================
  // HIGHLIGHTING LOGIC
  // =============================================================================
  function highlightWordInTextNodes(rootNode, words, style, className, useSubstrings) {
    const pattern = words.join('|');
    // Use word boundaries when substrings are disabled
    const regexPattern = useSubstrings ? pattern : `\\b(${pattern})\\b`;
    const regex = new RegExp(regexPattern, 'gi');
    const skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'];

    function isInsideHighlight(node) {
      while (node) {
        if (
          node.nodeType === Node.ELEMENT_NODE &&
          (node.classList.contains(className) || node.getAttribute('data-highlighted') === 'true')
        ) {
          return true;
        }
        node = node.parentNode;
      }
      return false;
    }

    function walk(node) {
      if (skipTags.includes(node.nodeName)) return;

      if (node.nodeType === Node.TEXT_NODE) {
        if (isInsideHighlight(node)) return;
        // Reset regex lastIndex to avoid issues with global flag
        regex.lastIndex = 0;
        if (!regex.test(node.nodeValue)) return;

        const spanWrapper = document.createElement('span');
        regex.lastIndex = 0;
        spanWrapper.innerHTML = node.nodeValue.replace(regex, match =>
          `<span class="${className}" style="${style}" data-highlighted="true">${match}</span>`
        );

        while (spanWrapper.firstChild) {
          node.parentNode.insertBefore(spanWrapper.firstChild, node);
        }
        node.parentNode.removeChild(node);
      } else if (!isInsideHighlight(node)) {
        for (let i = node.childNodes.length - 1; i >= 0; i--) {
          walk(node.childNodes[i]);
        }
      }
    }

    walk(rootNode);
  }

  function applyHighlighting(rootNode) {
    highlightWordInTextNodes(rootNode, houseWords, houseStyle, 'highlighted-house', includeSubstrings);
    highlightWordInTextNodes(rootNode, minotaurWords, minotaurStyle, 'highlighted-minotaur', includeSubstrings);
  }

  // =============================================================================
  // MUTATION OBSERVER (for dynamic content)
  // =============================================================================
  let scheduled = false;
  let pendingNodes = new Set();

  const observer = new MutationObserver((mutations) => {
    // Collect only the added nodes, skip if they're our own highlights
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE) {
          // Skip nodes we created (our highlight spans)
          if (node.getAttribute && node.getAttribute('data-highlighted') === 'true') {
            continue;
          }
          pendingNodes.add(node);
        } else if (node.nodeType === Node.TEXT_NODE) {
          // For text nodes, we'll process the parent
          if (node.parentNode && !(node.parentNode.getAttribute && node.parentNode.getAttribute('data-highlighted'))) {
            pendingNodes.add(node.parentNode);
          }
        }
      }
    }

    if (pendingNodes.size === 0) return;
    if (scheduled) return;
    scheduled = true;

    setTimeout(() => {
      // Disconnect observer while we make changes to avoid feedback loop
      observer.disconnect();

      for (const node of pendingNodes) {
        // Make sure node is still in the document
        if (document.contains(node)) {
          applyHighlighting(node);
        }
      }
      pendingNodes.clear();
      scheduled = false;

      // Reconnect observer
      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    }, 250);
  });

  // =============================================================================
  // INITIALIZE
  // =============================================================================
  applyHighlighting(document.body);

  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });

})();