MediaWiki:Common.js

De Oroza RO Wiki
Ir a la navegación Ir a la búsqueda

Nota: Después de publicar, quizás necesite actualizar la caché de su navegador para ver los cambios.

  • Firefox/Safari: Mantenga presionada la tecla Shift mientras pulsa el botón Actualizar, o presiona Ctrl+F5 o Ctrl+R (⌘+R en Mac)
  • Google Chrome: presione Ctrl+Shift+R (⌘+Shift+R en Mac)
  • Edge: mantenga presionada Ctrl mientras pulsa Actualizar, o presione Ctrl+F5
(function () {
  function safeNormalize(text) {
    text = String(text || '').toLowerCase().trim();

    try {
      return text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    } catch (e) {
      return text;
    }
  }

  function hasMark(cellText) {
    cellText = String(cellText || '').replace(/\s+/g, '').trim();

    return (
      cellText !== '' &&
      (
        cellText.indexOf('✔') !== -1 ||
        cellText.indexOf('✅') !== -1 ||
        cellText.indexOf('☑') !== -1
      )
    );
  }

  function getActiveCategoryCol(tableId) {
    var activeBtn = document.querySelector('.oroza-cat-btn.active[data-table="' + tableId + '"]');
    if (!activeBtn) {
      var firstBtn = document.querySelector('.oroza-cat-btn[data-table="' + tableId + '"]');
      if (!firstBtn) return null;
      firstBtn.classList.add('active');
      return firstBtn.getAttribute('data-col'); 
    }
    return activeBtn.getAttribute('data-col'); // Puede retornar "all" en string
  }

  function getSearchTextForRow(row, tableId) {
    var cells = row.getElementsByTagName('td');
    var text = row.getAttribute('data-search') || '';
    // Como añadimos "Prob.", ahora las columnas de información base son 3 en Slot 1 y 4 en Slot 2
    var visibleInfoCols = tableId === 'tableSlot1' ? 3 : 4;

    for (var i = 0; i < visibleInfoCols; i++) {
      if (cells[i]) {
        text += ' ' + (cells[i].textContent || cells[i].innerText || '');
      }
    }

    return safeNormalize(text);
  }

  function updateCategoryView(tableId, inputId) {
    var table = document.getElementById(tableId);
    var input = document.getElementById(inputId);

    if (!table || !table.tBodies.length || !table.tHead || !table.tHead.rows.length) return;

    var activeColRaw = getActiveCategoryCol(tableId);
    if (activeColRaw === null) return;

    var showAll = (activeColRaw === 'all');
    var activeCol = showAll ? -1 : parseInt(activeColRaw, 10);

    var searchText = input ? safeNormalize(input.value) : '';
    var rows = table.tBodies[0].getElementsByTagName('tr');
    var headerRow = table.tHead.rows[0];
    
    // Columnas que SIEMPRE se ven (Atributo, Prob, Rango, etc.)
    var staticCols = tableId === 'tableSlot1' ? 3 : 4;

    // Actualizar Header
    for (var h = 0; h < headerRow.cells.length; h++) {
      headerRow.cells[h].style.display = (showAll || h < staticCols || h === activeCol) ? '' : 'none';
    }

    // Actualizar Body
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var cells = row.getElementsByTagName('td');
      
      // Si estamos en "Todos", empezamos asumiendo que sí tiene categoría válida
      var matchCategory = showAll;
      var matchText = true;

      // Si se escogió una categoría específica, verificamos si tiene "✔"
      if (!showAll && cells[activeCol]) {
        matchCategory = hasMark(cells[activeCol].textContent || cells[activeCol].innerText);
      }

      // Verificación de búsqueda por texto
      if (searchText !== '') {
        var searchPool = getSearchTextForRow(row, tableId);
        if (searchPool.indexOf(searchText) === -1) {
          matchText = false;
        }
      }

      row.style.display = (matchCategory && matchText) ? '' : 'none';

      for (var c = 0; c < cells.length; c++) {
        cells[c].style.display = (showAll || c < staticCols || c === activeCol) ? '' : 'none';
      }
    }
  }

  function bindCategoryButtons(tableId, inputId) {
    var buttons = document.querySelectorAll('.oroza-cat-btn[data-table="' + tableId + '"]');

    for (var i = 0; i < buttons.length; i++) {
      if (buttons[i].dataset.orozaBound === '1') continue;

      buttons[i].dataset.orozaBound = '1';
      buttons[i].addEventListener('click', function () {
        var sameTableButtons = document.querySelectorAll('.oroza-cat-btn[data-table="' + tableId + '"]');
        for (var j = 0; j < sameTableButtons.length; j++) {
          sameTableButtons[j].classList.remove('active');
        }

        this.classList.add('active');
        updateCategoryView(tableId, inputId);
      });
    }
  }

  function bindSearchInput(tableId, inputId) {
    var input = document.getElementById(inputId);
    if (!input || input.dataset.orozaBound === '1') return;

    input.dataset.orozaBound = '1';
    input.addEventListener('input', function () {
      updateCategoryView(tableId, inputId);
    });
  }

  function initOrozaDropFilters() {
    if (document.getElementById('tableSlot1')) {
      bindCategoryButtons('tableSlot1', 'textSlot1');
      bindSearchInput('tableSlot1', 'textSlot1');
      updateCategoryView('tableSlot1', 'textSlot1');
    }

    if (document.getElementById('tableSlot2')) {
      bindCategoryButtons('tableSlot2', 'textSlot2');
      bindSearchInput('tableSlot2', 'textSlot2');
      updateCategoryView('tableSlot2', 'textSlot2');
    }
  }

  if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(function () {
      initOrozaDropFilters();
    });
  } else if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initOrozaDropFilters);
  } else {
    initOrozaDropFilters();
  }
})();


/* =========================================================
   RESALTAR MENÚ LATERAL ACTIVO (VERSIÓN NATIVA MEDIAWIKI)
========================================================= */
function highlightActiveSidebarLink() {
  // 1. Obtenemos el nombre exacto de la página actual desde el motor de MediaWiki
  var currentPage = mw.config.get('wgPageName');
  if (!currentPage) return;

  // Limpiamos el nombre de la página actual (todo a minúsculas y espacios a guiones bajos)
  currentPage = currentPage.replace(/ /g, '_').toLowerCase();

  var sidebarLinks = document.querySelectorAll('.oroza-sidebar li a');

  for (var i = 0; i < sidebarLinks.length; i++) {
    var linkHref = sidebarLinks[i].getAttribute('href');
    if (!linkHref) continue;

    // 2. Extraemos el nombre de la página del enlace del menú
    var linkPage = "";
    if (linkHref.indexOf('title=') !== -1) {
      linkPage = linkHref.split('title=')[1].split('&')[0];
    } else {
      linkPage = linkHref.split('/').pop(); // Toma lo último después del slash (ej. "Como-instalarlo")
    }

    if (!linkPage) continue;

    // 3. Decodificamos caracteres especiales (como acentos o espacios %20)
    try {
      linkPage = decodeURIComponent(linkPage);
    } catch(e) {}

    // Limpiamos el enlace de la misma forma para que la comparación sea exacta
    linkPage = linkPage.replace(/ /g, '_').toLowerCase();

    // 4. Comparamos
    if (currentPage === linkPage) {
      sidebarLinks[i].classList.add('active');
    } else {
      sidebarLinks[i].classList.remove('active'); // Limpia los demás por si acaso
    }
  }
}

// Integración con MediaWiki
if (window.mw && mw.hook) {
  mw.hook('wikipage.content').add(highlightActiveSidebarLink);
} else if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', highlightActiveSidebarLink);
} else {
  highlightActiveSidebarLink();
}








/* =========================================================
   OROZA PET ATTACK REBALANCE V9 - BUSCADOR SIMPLE
   Pegar en MediaWiki:Common.js
========================================================= */
(function () {
  function normalize(text) {
    return (text || '')
      .toString()
      .toLowerCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/_/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function initOrozaPetSearchV9(root) {
    root = root || document;

    var input = root.querySelector ? root.querySelector('#orozaPetSearchV6') : document.getElementById('orozaPetSearchV6');
    if (!input || input.dataset.ready === 'v9') return;
    input.dataset.ready = 'v9';

    var container = input.closest('.oroza-pet-v6') || document;
    var cards = Array.prototype.slice.call(container.querySelectorAll('.oroza-pet-v6-card'));
    var sections = Array.prototype.slice.call(container.querySelectorAll('[data-pet-section]'));
    var result = container.querySelector('#orozaPetSearchV6Result');
    var clearBtn = container.querySelector('[data-pet-clear]');

    function applySearch() {
      var q = normalize(input.value);
      var shown = 0;

      cards.forEach(function (card) {
        var haystack = normalize((card.getAttribute('data-pet-search') || '') + ' ' + card.textContent);
        var match = !q || haystack.indexOf(q) !== -1;

        card.classList.toggle('is-hidden', !match);
        if (match) shown++;
      });

      sections.forEach(function (section) {
        var visibleCards = section.querySelectorAll('.oroza-pet-v6-card:not(.is-hidden)').length;
        var shouldHide = visibleCards === 0 && q;
        section.classList.toggle('is-hidden', shouldHide);
        if (!shouldHide && q) section.open = true;
      });

      if (result) {
        if (shown === 0) {
          result.textContent = 'No se encontraron mascotas con ese texto.';
        } else if (q) {
          result.textContent = 'Resultados visibles: ' + shown + '. Abre la tarjeta para revisar la skill y el nivel de cada Pokémon.';
        } else {
          result.textContent = 'Mostrando todas las mascotas.';
        }
      }
    }

    input.addEventListener('input', applySearch);

    if (clearBtn) {
      clearBtn.addEventListener('click', function () {
        input.value = '';
        applySearch();
        input.focus();
      });
    }

    applySearch();
  }

  function boot() {
    initOrozaPetSearchV9(document);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }

  if (window.mw && window.mw.hook) {
    window.mw.hook('wikipage.content').add(function ($content) {
      var node = $content && $content[0] ? $content[0] : document;
      initOrozaPetSearchV9(node);
    });
  }
})();

/* =========================================================
   OROZA RO SMART WIKI SEARCH V2
   ---------------------------------------------------------
   - Searches inside the complete text of every main Wiki page
   - Detects headings/sections and shows the matching fragment
   - Combines a local smart index with MediaWiki native search
   - Adds aliases/synonyms for common Ragnarok Online terms
   - Uses cache to avoid downloading the Wiki on every search
   - Keyboard navigation, mobile support and native fallback
========================================================= */
(function () {
  'use strict';

  var SELECTOR = '[data-oroza-global-search]';
  var MIN_QUERY_LENGTH = 2;
  var SMART_LIMIT = 10;
  var API_LIMIT = 8;
  var MENU_LIMIT = 3;
  var DEBOUNCE_MS = 240;
  var INDEX_TTL_MS = 2 * 60 * 60 * 1000;
  var MAX_INDEX_PAGES = 400;
  var MAX_PAGE_TEXT = 60000;
  var CACHE_VERSION = 2;

  var indexPromise = null;
  var memoryIndex = null;

  function normalizeText(value) {
    var text = String(value || '')
      .toLowerCase()
      .replace(/_/g, ' ')
      .replace(/&nbsp;/gi, ' ')
      .replace(/[\u2010-\u2015]/g, '-')
      .replace(/[^a-z0-9@+#áéíóúüñçàèìòùâêîôûäëïöü\s-]/gi, ' ')
      .replace(/\s+/g, ' ')
      .trim();

    try {
      text = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    } catch (e) {}

    return text;
  }

  function stripHtml(value) {
    var node = document.createElement('div');
    node.innerHTML = String(value || '');
    return (node.textContent || node.innerText || '')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function cleanWikiText(value) {
    var text = String(value || '');

    text = text
      .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
      .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
      .replace(/<ref\b[^>]*>[\s\S]*?<\/ref>/gi, ' ')
      .replace(/<ref\b[^>]*\/\s*>/gi, ' ')
      .replace(/<!--([\s\S]*?)-->/g, ' ')
      .replace(/\{\|[\s\S]*?\|\}/g, function (table) {
        return table.replace(/[|!{}]/g, ' ');
      })
      .replace(/\[\[Category:[^\]]+\]\]/gi, ' ')
      .replace(/\[\[File:[^\]]+\]\]/gi, ' ')
      .replace(/\[\[Image:[^\]]+\]\]/gi, ' ')
      .replace(/\[\[[^\]|]+\|([^\]]+)\]\]/g, '$1')
      .replace(/\[\[([^\]]+)\]\]/g, '$1')
      .replace(/\[(?:https?:)?\/\/[^\s\]]+\s+([^\]]+)\]/g, '$1')
      .replace(/\[(?:https?:)?\/\/[^\]]+\]/g, ' ')
      .replace(/\{\{[^{}]*\}\}/g, ' ')
      .replace(/\{\{[^{}]*\}\}/g, ' ')
      .replace(/<[^>]+>/g, ' ')
      .replace(/'{2,5}/g, '')
      .replace(/={2,6}/g, ' ')
      .replace(/__[^_]+__/g, ' ')
      .replace(/[|{}]/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();

    return text;
  }

  function extractKeywords(wikitext) {
    var keywords = [];
    var pattern = /<!--\s*OROZA-KEYWORDS\s*:\s*([\s\S]*?)-->/gi;
    var match;

    while ((match = pattern.exec(String(wikitext || '')))) {
      match[1].split(/[,;|\n]/).forEach(function (item) {
        item = item.trim();
        if (item) keywords.push(item);
      });
    }

    return keywords;
  }

  function extractCategories(wikitext) {
    var categories = [];
    var pattern = /\[\[Category\s*:\s*([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
    var match;

    while ((match = pattern.exec(String(wikitext || '')))) {
      var category = cleanWikiText(match[1]);
      if (category && categories.indexOf(category) === -1) categories.push(category);
    }

    return categories;
  }

  function splitIntoSections(wikitext) {
    var source = String(wikitext || '');
    var lines = source.split(/\r?\n/);
    var sections = [];
    var currentTitle = '';
    var currentLines = [];

    function flush() {
      var plain = cleanWikiText(currentLines.join('\n'));
      if (plain) {
        sections.push({
          title: cleanWikiText(currentTitle),
          text: plain.slice(0, MAX_PAGE_TEXT)
        });
      }
      currentLines = [];
    }

    lines.forEach(function (line) {
      var heading = line.match(/^\s*(={2,6})\s*(.*?)\s*\1\s*$/);
      if (heading) {
        flush();
        currentTitle = heading[2];
      } else {
        currentLines.push(line);
      }
    });

    flush();

    if (!sections.length) {
      var plain = cleanWikiText(source);
      if (plain) sections.push({ title: '', text: plain.slice(0, MAX_PAGE_TEXT) });
    }

    return sections;
  }

  function decodePageName(href) {
    try {
      var url = new URL(href, window.location.href);
      var title = url.searchParams.get('title');

      if (!title) {
        var marker = '/wiki/index.php/';
        var markerIndex = url.pathname.indexOf(marker);
        if (markerIndex !== -1) title = url.pathname.slice(markerIndex + marker.length);
      }

      if (!title) return '';
      return decodeURIComponent(title).replace(/_/g, ' ').trim();
    } catch (e) {
      return '';
    }
  }

  function getLocale() {
    var isEnglish = window.location.hostname === 'en.orozaro.com';

    return isEnglish ? {
      placeholder: 'Search guides, commands, items, systems and content...',
      ariaLabel: 'Search inside every Oroza RO Wiki guide',
      searchButton: 'Search',
      loading: 'Searching titles, sections and guide content...',
      smartLoading: 'Building the smart guide index for the first search...',
      smartGroup: 'Matches found inside guides',
      apiGroup: 'Other Wiki results',
      menuGroup: 'Quick links',
      noResultsTitle: 'No results found',
      noResultsText: 'Try another spelling, a shorter phrase, or a related Ragnarok term.',
      categoryFallback: 'Wiki guide',
      sectionPrefix: 'Section',
      insideGuide: 'Inside guide',
      titleMatch: 'Guide title',
      aliasMatch: 'Related term',
      resultSingular: 'result',
      resultPlural: 'results',
      viewAll: 'View native search',
      suggestion: 'Did you mean',
      indexedGuides: 'guides indexed',
      indexUnavailable: 'Smart index unavailable; showing native results.',
      apiErrorTitle: 'Native search is temporarily unavailable',
      apiErrorText: 'Smart guide matches are still available. Press Enter to open MediaWiki search.',
      introduction: 'Introduction'
    } : {
      placeholder: 'Buscar guías, comandos, objetos, sistemas y contenido...',
      ariaLabel: 'Buscar dentro de todas las guías de la Wiki de Oroza RO',
      searchButton: 'Buscar',
      loading: 'Buscando en títulos, secciones y contenido de las guías...',
      smartLoading: 'Creando el índice inteligente de las guías por primera vez...',
      smartGroup: 'Coincidencias encontradas dentro de las guías',
      apiGroup: 'Otros resultados de la Wiki',
      menuGroup: 'Accesos rápidos',
      noResultsTitle: 'No encontramos resultados',
      noResultsText: 'Prueba otra escritura, una frase más corta o un término relacionado de Ragnarok.',
      categoryFallback: 'Guía de la Wiki',
      sectionPrefix: 'Sección',
      insideGuide: 'Dentro de la guía',
      titleMatch: 'Título de la guía',
      aliasMatch: 'Término relacionado',
      resultSingular: 'resultado',
      resultPlural: 'resultados',
      viewAll: 'Ver búsqueda nativa',
      suggestion: 'Quizá quisiste decir',
      indexedGuides: 'guías indexadas',
      indexUnavailable: 'Índice inteligente no disponible; se muestran resultados nativos.',
      apiErrorTitle: 'La búsqueda nativa no está disponible temporalmente',
      apiErrorText: 'Las coincidencias del índice inteligente siguen disponibles. Presiona Enter para abrir la búsqueda de MediaWiki.',
      introduction: 'Introducción'
    };
  }

  function getSynonymMap(isEnglish) {
    var shared = {
      '@aa': ['auto attack', 'auto ataque', 'autoatk', 'automatic attack'],
      'aa': ['auto attack', 'auto ataque', 'autoatk'],
      'bg': ['battleground', 'battlegrounds', 'campo de batalla'],
      'woe': ['war of emperium', 'guild war', 'guerra de emperium'],
      'mvp': ['boss', 'jefe', 'monster boss'],
      'npc': ['non player character', 'personaje no jugador'],
      'exp': ['experience', 'experiencia'],
      'zeny': ['money', 'currency', 'dinero', 'moneda']
    };

    var english = {
      'beginner': ['starter', 'new player', 'newbie', 'first steps', 'getting started'],
      'newbie': ['beginner', 'starter', 'new player', 'first steps'],
      'starter': ['beginner', 'new player', 'newbie', 'first steps', '@starter'],
      'job': ['class', 'profession', 'job change'],
      'class': ['job', 'profession', 'job change'],
      'card': ['cards', 'monster card'],
      'cards': ['card', 'monster cards'],
      'hat': ['headgear', 'head gear', 'costume'],
      'headgear': ['hat', 'head gear', 'costume'],
      'refine': ['refinement', 'upgrade', 'safe refine'],
      'refinement': ['refine', 'upgrade', 'safe refine'],
      'enchant': ['enchantment', 'bonus', 'random option'],
      'quest': ['mission', 'task'],
      'mission': ['quest', 'task'],
      'party': ['group', 'even share'],
      'group': ['party', 'even share'],
      'drop': ['loot', 'reward', 'item drop'],
      'loot': ['drop', 'reward'],
      'pet': ['pokemon', 'companion', 'homunculus'],
      'pokemon': ['pet', 'companion'],
      'command': ['commands', '@command', 'chat command'],
      'donation': ['donate', 'cash points', 'support server'],
      'event': ['events', 'activity'],
      'skill': ['skills', 'ability'],
      'equipment': ['gear', 'weapon', 'armor'],
      'gear': ['equipment', 'weapon', 'armor']
    };

    var spanish = {
      'principiante': ['starter', 'jugador nuevo', 'novato', 'primeros pasos', 'inicio'],
      'novato': ['principiante', 'starter', 'jugador nuevo', 'primeros pasos'],
      'nuevo': ['principiante', 'starter', 'jugador nuevo', 'primeros pasos'],
      'starter': ['principiante', 'jugador nuevo', 'novato', 'primeros pasos', '@starter'],
      'job': ['clase', 'profesión', 'cambio de job'],
      'clase': ['job', 'profesión', 'cambio de clase'],
      'carta': ['cartas', 'card', 'monster card'],
      'cartas': ['carta', 'cards', 'monster cards'],
      'hat': ['sombrero', 'headgear', 'costume'],
      'sombrero': ['hat', 'headgear', 'costume'],
      'refinar': ['refinamiento', 'mejorar equipo', 'refine'],
      'refinamiento': ['refinar', 'mejorar equipo', 'safe refine'],
      'encantar': ['encantamiento', 'bonus', 'opción aleatoria'],
      'encantamiento': ['encantar', 'bonus', 'random option'],
      'quest': ['misión', 'mision', 'tarea'],
      'misión': ['quest', 'mision', 'tarea'],
      'mision': ['quest', 'misión', 'tarea'],
      'party': ['grupo', 'even share', 'compartir experiencia'],
      'grupo': ['party', 'even share', 'compartir experiencia'],
      'drop': ['loot', 'recompensa', 'objeto'],
      'loot': ['drop', 'recompensa'],
      'mascota': ['pokemon', 'pet', 'homúnculo', 'homunculo'],
      'pokemon': ['mascota', 'pet', 'compañero'],
      'comando': ['comandos', '@comando', 'chat command'],
      'donación': ['donar', 'cash points', 'apoyar servidor'],
      'donacion': ['donar', 'cash points', 'apoyar servidor'],
      'evento': ['eventos', 'actividad'],
      'skill': ['skills', 'habilidad'],
      'equipo': ['gear', 'arma', 'armadura']
    };

    var result = {};
    Object.keys(shared).forEach(function (key) { result[normalizeText(key)] = shared[key]; });
    Object.keys(isEnglish ? english : spanish).forEach(function (key) {
      result[normalizeText(key)] = (isEnglish ? english : spanish)[key];
    });
    return result;
  }

  function tokenize(value) {
    var stopWords = {
      'the': 1, 'a': 1, 'an': 1, 'and': 1, 'or': 1, 'of': 1, 'to': 1, 'in': 1, 'for': 1,
      'el': 1, 'la': 1, 'los': 1, 'las': 1, 'un': 1, 'una': 1, 'y': 1, 'o': 1, 'de': 1,
      'del': 1, 'en': 1, 'para': 1, 'como': 1, 'how': 1, 'what': 1, 'que': 1
    };

    return normalizeText(value).split(' ').filter(function (token) {
      return token && (token.length >= 2 || token.charAt(0) === '@') && !stopWords[token];
    });
  }

  function unique(values) {
    var seen = Object.create(null);
    return values.filter(function (value) {
      var key = normalizeText(value);
      if (!key || seen[key]) return false;
      seen[key] = true;
      return true;
    });
  }

  function expandQuery(query, synonymMap) {
    var originalTokens = tokenize(query);
    var expandedPhrases = [];

    originalTokens.forEach(function (token) {
      var synonyms = synonymMap[token] || [];
      synonyms.forEach(function (synonym) {
        expandedPhrases.push(synonym);
      });
    });

    return {
      phrase: normalizeText(query),
      originalTokens: unique(originalTokens),
      expandedTokens: unique(expandedPhrases.reduce(function (all, phrase) {
        return all.concat(tokenize(phrase));
      }, [])),
      expandedPhrases: unique(expandedPhrases.map(normalizeText))
    };
  }

  function collectMenuEntries() {
    var entries = [];
    var titles = document.querySelectorAll('.oroza-sidebar .menu-title');

    Array.prototype.forEach.call(titles, function (titleNode) {
      var category = (titleNode.textContent || '').replace(/\s+/g, ' ').trim();
      var list = titleNode.nextElementSibling;
      if (!list || String(list.tagName).toLowerCase() !== 'ul') return;

      Array.prototype.forEach.call(list.querySelectorAll('a[href]'), function (link) {
        var title = decodePageName(link.getAttribute('href'));
        var labelNode = link.querySelector('.menu-text');
        var label = labelNode ? labelNode.textContent : link.textContent;

        label = String(label || '').replace(/›/g, '').replace(/\s+/g, ' ').trim();
        title = title || label;
        if (!title || !link.href) return;

        entries.push({
          title: title,
          label: label || title,
          category: category,
          href: link.href,
          normalized: normalizeText(title + ' ' + label + ' ' + category)
        });
      });
    });

    return entries;
  }

  function findMenuMatches(entries, query) {
    var normalizedQuery = normalizeText(query);
    if (!normalizedQuery) return [];

    return entries.map(function (entry) {
      var normalizedTitle = normalizeText(entry.title + ' ' + entry.label);
      var score = 0;
      if (normalizedTitle === normalizedQuery) score = 100;
      else if (normalizedTitle.indexOf(normalizedQuery) === 0) score = 80;
      else if (normalizedTitle.indexOf(normalizedQuery) !== -1) score = 60;
      else if (entry.normalized.indexOf(normalizedQuery) !== -1) score = 40;
      return { entry: entry, score: score };
    }).filter(function (item) {
      return item.score > 0;
    }).sort(function (a, b) {
      return b.score - a.score || a.entry.label.localeCompare(b.entry.label);
    }).slice(0, MENU_LIMIT).map(function (item) {
      return item.entry;
    });
  }

  function buildCategoryMap(entries) {
    var map = Object.create(null);
    entries.forEach(function (entry) {
      map[normalizeText(entry.title)] = entry.category;
      map[normalizeText(entry.label)] = entry.category;
    });
    return map;
  }

  function getCacheKey() {
    return 'oroza-smart-wiki-index-v' + CACHE_VERSION + ':' + window.location.hostname;
  }

  function readIndexCache() {
    try {
      var raw = window.localStorage.getItem(getCacheKey());
      if (!raw) return null;
      var payload = JSON.parse(raw);
      if (!payload || !Array.isArray(payload.pages) || !payload.createdAt) return null;
      if (Date.now() - payload.createdAt > INDEX_TTL_MS) return null;
      return payload.pages;
    } catch (e) {
      return null;
    }
  }

  function writeIndexCache(pages) {
    try {
      window.localStorage.setItem(getCacheKey(), JSON.stringify({
        createdAt: Date.now(),
        pages: pages
      }));
    } catch (e) {}
  }

  function clearIndexCache() {
    try { window.localStorage.removeItem(getCacheKey()); } catch (e) {}
    memoryIndex = null;
    indexPromise = null;
  }

  function getRevisionContent(page) {
    var revision = page && page.revisions && page.revisions[0];
    if (!revision) return '';
    if (revision.slots && revision.slots.main) {
      return revision.slots.main.content || revision.slots.main['*'] || '';
    }
    return revision.content || revision['*'] || '';
  }

  function createIndexedPage(page, categoryMap) {
    var wikitext = getRevisionContent(page);
    var title = page.title || '';
    var sections = splitIntoSections(wikitext);
    var keywords = extractKeywords(wikitext);
    var categories = extractCategories(wikitext);
    var category = categoryMap[normalizeText(title)] || categories[0] || '';

    return {
      title: title,
      normalizedTitle: normalizeText(title),
      url: (window.mw && mw.util) ? mw.util.getUrl(title) : '/wiki/index.php?title=' + encodeURIComponent(title.replace(/ /g, '_')),
      category: category,
      keywords: keywords,
      normalizedKeywords: normalizeText(keywords.join(' ')),
      sections: sections.map(function (section) {
        return {
          title: section.title,
          normalizedTitle: normalizeText(section.title),
          text: section.text,
          normalizedText: normalizeText(section.text)
        };
      })
    };
  }

  function buildSmartIndex(api, categoryMap) {
    var pages = [];
    var continueParams = {};

    function fetchBatch() {
      var params = {
        action: 'query',
        generator: 'allpages',
        gapnamespace: 0,
        gapfilterredir: 'nonredirects',
        gaplimit: 'max',
        prop: 'revisions',
        rvprop: 'content',
        rvslots: 'main',
        rvlimit: 1,
        formatversion: 2,
        utf8: 1
      };

      Object.keys(continueParams).forEach(function (key) {
        params[key] = continueParams[key];
      });

      return api.get(params).then(function (data) {
        var batch = data && data.query && Array.isArray(data.query.pages) ? data.query.pages : [];

        batch.forEach(function (page) {
          if (pages.length >= MAX_INDEX_PAGES || page.missing) return;
          pages.push(createIndexedPage(page, categoryMap));
        });

        if (data && data.continue && pages.length < MAX_INDEX_PAGES) {
          continueParams = data.continue;
          return fetchBatch();
        }

        writeIndexCache(pages);
        memoryIndex = pages;
        return pages;
      });
    }

    return fetchBatch();
  }

  function ensureSmartIndex(api, categoryMap) {
    if (memoryIndex) return Promise.resolve(memoryIndex);

    var cached = readIndexCache();
    if (cached) {
      memoryIndex = cached;
      return Promise.resolve(cached);
    }

    if (!api) return Promise.reject(new Error('MediaWiki API unavailable'));
    if (!indexPromise) {
      indexPromise = buildSmartIndex(api, categoryMap).catch(function (error) {
        indexPromise = null;
        throw error;
      });
    }
    return indexPromise;
  }

  function countOccurrences(text, token) {
    if (!token) return 0;
    var count = 0;
    var position = 0;
    while ((position = text.indexOf(token, position)) !== -1) {
      count++;
      position += Math.max(token.length, 1);
      if (count >= 8) break;
    }
    return count;
  }

  function makeSnippet(text, queryData, maxLength) {
    var plain = String(text || '').replace(/\s+/g, ' ').trim();
    if (!plain) return '';

    var lower = plain.toLowerCase();
    var candidates = [String(queryData.raw || '').toLowerCase()]
      .concat(queryData.originalTokens || [])
      .concat(queryData.expandedTokens || []);
    var position = -1;

    candidates.some(function (candidate) {
      if (!candidate) return false;
      position = lower.indexOf(candidate.toLowerCase());
      return position !== -1;
    });

    if (position < 0) position = 0;
    var radius = Math.floor((maxLength || 220) / 2);
    var start = Math.max(0, position - radius);
    var end = Math.min(plain.length, start + (maxLength || 220));

    if (end - start < (maxLength || 220) && start > 0) {
      start = Math.max(0, end - (maxLength || 220));
    }

    var snippet = plain.slice(start, end).trim();
    if (start > 0) snippet = '…' + snippet;
    if (end < plain.length) snippet += '…';
    return snippet;
  }

  function scoreSection(page, section, queryData) {
    var title = page.normalizedTitle;
    var sectionTitle = section.normalizedTitle;
    var text = section.normalizedText;
    var keywords = page.normalizedKeywords;
    var phrase = queryData.phrase;
    var originalTokens = queryData.originalTokens;
    var expandedTokens = queryData.expandedTokens;
    var score = 0;
    var matchType = '';

    if (title === phrase) {
      score += 320;
      matchType = 'title';
    } else if (title.indexOf(phrase) === 0) {
      score += 260;
      matchType = 'title';
    } else if (title.indexOf(phrase) !== -1) {
      score += 220;
      matchType = 'title';
    }

    if (keywords && phrase && keywords.indexOf(phrase) !== -1) {
      score += 190;
      if (!matchType) matchType = 'alias';
    }

    if (sectionTitle === phrase) {
      score += 185;
      if (!matchType) matchType = 'section';
    } else if (sectionTitle && sectionTitle.indexOf(phrase) !== -1) {
      score += 160;
      if (!matchType) matchType = 'section';
    }

    if (phrase && text.indexOf(phrase) !== -1) {
      score += 135;
      if (!matchType) matchType = 'content';
    }

    var originalMatches = 0;
    var expandedMatches = 0;
    var frequency = 0;

    originalTokens.forEach(function (token) {
      var inTitle = title.indexOf(token) !== -1;
      var inSection = sectionTitle.indexOf(token) !== -1;
      var inKeywords = keywords.indexOf(token) !== -1;
      var occurrences = countOccurrences(text, token);

      if (inTitle || inSection || inKeywords || occurrences) originalMatches++;
      if (inTitle) score += 24;
      if (inSection) score += 19;
      if (inKeywords) score += 17;
      if (occurrences) score += Math.min(occurrences, 5) * 7;
      frequency += occurrences;
    });

    expandedTokens.forEach(function (token) {
      if (title.indexOf(token) !== -1 || sectionTitle.indexOf(token) !== -1 || keywords.indexOf(token) !== -1 || text.indexOf(token) !== -1) {
        expandedMatches++;
      }
    });

    var coverage = originalTokens.length ? originalMatches / originalTokens.length : 0;
    if (originalTokens.length > 1 && originalMatches === originalTokens.length) score += 65;
    score += Math.round(coverage * 50);
    score += Math.min(expandedMatches, 4) * 8;
    score += Math.min(frequency, 8) * 2;

    var qualifies = false;
    if (phrase && (title.indexOf(phrase) !== -1 || sectionTitle.indexOf(phrase) !== -1 || keywords.indexOf(phrase) !== -1 || text.indexOf(phrase) !== -1)) {
      qualifies = true;
    } else if (originalTokens.length === 1) {
      qualifies = originalMatches === 1 || expandedMatches > 0;
    } else if (originalTokens.length > 1) {
      qualifies = coverage >= 0.67 || (coverage >= 0.5 && expandedMatches > 0);
    }

    return {
      qualifies: qualifies,
      score: score,
      matchType: matchType || (expandedMatches ? 'alias' : 'content')
    };
  }

  function searchSmartIndex(index, rawQuery, labels, synonymMap) {
    var queryData = expandQuery(rawQuery, synonymMap);
    queryData.raw = rawQuery;
    var results = [];

    (index || []).forEach(function (page) {
      var best = null;

      (page.sections || []).forEach(function (section) {
        var scored = scoreSection(page, section, queryData);
        if (!scored.qualifies) return;
        if (!best || scored.score > best.score) {
          best = {
            score: scored.score,
            matchType: scored.matchType,
            section: section
          };
        }
      });

      if (!best) return;

      var location = labels.insideGuide;
      if (best.matchType === 'title') location = labels.titleMatch;
      else if (best.matchType === 'alias') location = labels.aliasMatch;

      if (best.section.title) {
        location += ' · ' + labels.sectionPrefix + ': ' + best.section.title;
      }

      results.push({
        title: page.title,
        label: page.title,
        href: page.url,
        category: page.category || labels.categoryFallback,
        location: location,
        snippet: makeSnippet(best.section.text, queryData, 240),
        score: best.score
      });
    });

    return results.sort(function (a, b) {
      return b.score - a.score || a.title.localeCompare(b.title);
    }).slice(0, SMART_LIMIT);
  }

  function createElement(tag, className, text) {
    var element = document.createElement(tag);
    if (className) element.className = className;
    if (typeof text !== 'undefined') element.textContent = text;
    return element;
  }

  function initSearch(root) {
    var widget = root.querySelector ? root.querySelector(SELECTOR) : null;
    if (!widget || widget.dataset.orozaSmartReady === '2') return;
    widget.dataset.orozaSmartReady = '2';

    var form = widget.querySelector('.oroza-global-search-form');
    var input = widget.querySelector('.oroza-global-search-input');
    var results = widget.querySelector('.oroza-global-search-results');
    var clearButton = widget.querySelector('[data-oroza-search-clear]');
    var submitText = widget.querySelector('.oroza-global-search-submit span:last-child');
    var helper = widget.querySelector('.oroza-global-search-helper');

    if (!form || !input || !results || !clearButton) return;

    var labels = getLocale();
    var isEnglish = window.location.hostname === 'en.orozaro.com';
    var synonymMap = getSynonymMap(isEnglish);
    var menuEntries = collectMenuEntries();
    var categoryMap = buildCategoryMap(menuEntries);
    var apiCache = Object.create(null);
    var debounceTimer = null;
    var requestSequence = 0;
    var activeIndex = -1;
    var api = (window.mw && mw.Api) ? new mw.Api() : null;

    input.placeholder = labels.placeholder;
    input.setAttribute('aria-label', labels.ariaLabel);
    if (submitText) submitText.textContent = labels.searchButton;
    if (helper) {
      helper.innerHTML = isEnglish
        ? 'Searches inside every guide, section and paragraph. Use <kbd>↑</kbd> <kbd>↓</kbd> and <kbd>Enter</kbd>.'
        : 'Busca dentro de cada guía, sección y párrafo. Usa <kbd>↑</kbd> <kbd>↓</kbd> y <kbd>Enter</kbd>.';
    }

    function getNativeSearchUrl(query) {
      try {
        var url = new URL(form.action, window.location.href);
        url.searchParams.set('title', 'Special:Search');
        url.searchParams.set('search', query);
        url.searchParams.set('fulltext', '1');
        return url.toString();
      } catch (e) {
        return '/wiki/index.php?title=Special:Search&fulltext=1&search=' + encodeURIComponent(query);
      }
    }

    function setOpen(isOpen) {
      results.hidden = !isOpen;
      input.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
      if (!isOpen) activeIndex = -1;
    }

    function clearResults() {
      while (results.firstChild) results.removeChild(results.firstChild);
      setOpen(false);
    }

    function addGroupTitle(text) {
      results.appendChild(createElement('div', 'oroza-search-group-title', text));
    }

    function addResultItem(item) {
      var link = createElement('a', 'oroza-search-result-link');
      link.href = item.href;
      link.setAttribute('role', 'option');
      link.setAttribute('aria-selected', 'false');

      var main = createElement('div', 'oroza-search-result-main');
      main.appendChild(createElement('span', 'oroza-search-result-title', item.label || item.title));

      if (item.location) {
        main.appendChild(createElement('span', 'oroza-search-result-location', item.location));
      }

      if (item.snippet) {
        main.appendChild(createElement('p', 'oroza-search-result-snippet', item.snippet));
      }

      link.appendChild(main);
      link.appendChild(createElement('span', 'oroza-search-result-category', item.category || labels.categoryFallback));
      results.appendChild(link);
    }

    function addSuggestion(suggestion) {
      if (!suggestion) return;
      var box = createElement('div', 'oroza-search-suggestion');
      box.appendChild(document.createTextNode(labels.suggestion + ': '));
      var button = createElement('button', '', suggestion);
      button.type = 'button';
      button.addEventListener('click', function () {
        input.value = suggestion;
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.focus();
      });
      box.appendChild(button);
      results.appendChild(box);
    }

    function addFooter(query, totalHits, indexState) {
      var footer = createElement('div', 'oroza-search-footer');
      var left = createElement('span', 'oroza-search-footer-count');
      var countText = '';

      if (typeof totalHits === 'number') {
        countText = totalHits + ' ' + (totalHits === 1 ? labels.resultSingular : labels.resultPlural);
      }

      left.textContent = countText;
      footer.appendChild(left);

      if (indexState) {
        footer.appendChild(createElement('span', 'oroza-search-index-state', indexState));
      }

      var link = createElement('a', '', labels.viewAll + ' →');
      link.href = getNativeSearchUrl(query);
      footer.appendChild(link);
      results.appendChild(footer);
    }

    function renderLoading(menuMatches) {
      while (results.firstChild) results.removeChild(results.firstChild);
      if (menuMatches.length) {
        addGroupTitle(labels.menuGroup);
        menuMatches.forEach(function (item) {
          addResultItem({
            title: item.title,
            label: item.label,
            href: item.href,
            category: item.category,
            location: labels.titleMatch,
            snippet: ''
          });
        });
      }
      var status = createElement('div', 'oroza-search-status');
      status.appendChild(createElement('span', 'oroza-search-spinner'));
      status.appendChild(createElement('span', '', labels.loading));
      results.appendChild(status);
      setOpen(true);
    }

    function renderState(query, state) {
      while (results.firstChild) results.removeChild(results.firstChild);

      var seen = Object.create(null);
      var smartResults = [];
      var apiResults = [];
      var menuMatches = [];

      (state.smart || []).forEach(function (item) {
        var key = normalizeText(item.title);
        if (!key || seen[key]) return;
        seen[key] = true;
        smartResults.push(item);
      });

      (state.api || []).forEach(function (item) {
        var key = normalizeText(item.title);
        if (!key || seen[key]) return;
        seen[key] = true;
        apiResults.push(item);
      });

      (state.menu || []).forEach(function (item) {
        var key = normalizeText(item.title);
        if (!key || seen[key]) return;
        seen[key] = true;
        menuMatches.push(item);
      });

      if (smartResults.length) {
        addGroupTitle(labels.smartGroup);
        smartResults.forEach(addResultItem);
      }

      if (apiResults.length) {
        addGroupTitle(labels.apiGroup);
        apiResults.forEach(addResultItem);
      }

      if (menuMatches.length) {
        addGroupTitle(labels.menuGroup);
        menuMatches.forEach(function (item) {
          addResultItem({
            title: item.title,
            label: item.label,
            href: item.href,
            category: item.category,
            location: labels.titleMatch,
            snippet: ''
          });
        });
      }

      if (!smartResults.length && !apiResults.length && !menuMatches.length) {
        var empty = createElement('div', 'oroza-search-empty');
        empty.appendChild(createElement('span', 'oroza-search-empty-icon', '🔍'));
        empty.appendChild(createElement('p', 'oroza-search-empty-title', labels.noResultsTitle));
        empty.appendChild(createElement('p', 'oroza-search-empty-text', labels.noResultsText));
        results.appendChild(empty);
      }

      if (state.smartLoading) {
        var loading = createElement('div', 'oroza-search-smart-loading');
        loading.appendChild(createElement('span', 'oroza-search-spinner'));
        loading.appendChild(createElement('span', '', labels.smartLoading));
        results.appendChild(loading);
      }

      if (state.apiFailed && !apiResults.length) {
        var warning = createElement('div', 'oroza-search-inline-warning');
        warning.textContent = labels.apiErrorTitle + '. ' + labels.apiErrorText;
        results.appendChild(warning);
      }

      addSuggestion(state.suggestion);

      var total = typeof state.totalHits === 'number'
        ? Math.max(state.totalHits, smartResults.length)
        : smartResults.length + apiResults.length + menuMatches.length;
      var indexState = '';
      if (typeof state.indexCount === 'number') indexState = state.indexCount + ' ' + labels.indexedGuides;
      else if (state.smartFailed) indexState = labels.indexUnavailable;

      addFooter(query, total, indexState);
      setOpen(true);
    }

    function transformApiData(data) {
      var query = data && data.query ? data.query : {};
      var searchInfo = query.searchinfo || {};
      var items = Array.isArray(query.search) ? query.search : [];

      return {
        totalHits: typeof searchInfo.totalhits === 'number' ? searchInfo.totalhits : items.length,
        suggestion: searchInfo.suggestion || '',
        items: items.map(function (item) {
          var category = categoryMap[normalizeText(item.title)] || labels.categoryFallback;
          var section = stripHtml(item.sectiontitle || '');
          var snippet = stripHtml(item.snippet || '');
          var location = labels.insideGuide;

          if (section) location += ' · ' + labels.sectionPrefix + ': ' + section;

          return {
            title: item.title,
            label: item.title,
            href: (window.mw && mw.util) ? mw.util.getUrl(item.title) : '/wiki/index.php?title=' + encodeURIComponent(item.title.replace(/ /g, '_')),
            category: category,
            location: location,
            snippet: snippet
          };
        })
      };
    }

    function searchNative(query) {
      var key = normalizeText(query);
      if (apiCache[key]) return Promise.resolve(apiCache[key]);
      if (!api) return Promise.reject(new Error('MediaWiki API unavailable'));

      return api.get({
        action: 'query',
        list: 'search',
        srsearch: query,
        srwhat: 'text',
        srnamespace: 0,
        srlimit: API_LIMIT,
        srinfo: 'totalhits|suggestion|rewrittenquery',
        srprop: 'snippet|titlesnippet|sectiontitle|redirecttitle',
        srenablerewrites: 1,
        formatversion: 2,
        utf8: 1
      }).then(function (data) {
        var payload = transformApiData(data);
        apiCache[key] = payload;
        return payload;
      });
    }

    function runSearch(rawQuery) {
      var query = String(rawQuery || '').trim();
      if (query.length < MIN_QUERY_LENGTH) {
        clearResults();
        return;
      }

      var currentRequest = ++requestSequence;
      var state = {
        menu: findMenuMatches(menuEntries, query),
        smart: [],
        api: [],
        smartLoading: true,
        smartFailed: false,
        apiFailed: false,
        suggestion: '',
        totalHits: null,
        indexCount: null
      };

      renderLoading(state.menu);

      searchNative(query).then(function (payload) {
        if (currentRequest !== requestSequence) return;
        state.api = payload.items;
        state.suggestion = payload.suggestion;
        state.totalHits = payload.totalHits;
        renderState(query, state);
      }).catch(function () {
        if (currentRequest !== requestSequence) return;
        state.apiFailed = true;
        renderState(query, state);
      });

      ensureSmartIndex(api, categoryMap).then(function (index) {
        if (currentRequest !== requestSequence) return;
        state.smart = searchSmartIndex(index, query, labels, synonymMap);
        state.smartLoading = false;
        state.indexCount = index.length;
        renderState(query, state);
      }).catch(function () {
        if (currentRequest !== requestSequence) return;
        state.smartLoading = false;
        state.smartFailed = true;
        renderState(query, state);
      });
    }

    function getResultLinks() {
      return Array.prototype.slice.call(results.querySelectorAll('.oroza-search-result-link'));
    }

    function setActiveResult(index) {
      var links = getResultLinks();
      if (!links.length) return;

      if (index < 0) index = links.length - 1;
      if (index >= links.length) index = 0;
      activeIndex = index;

      links.forEach(function (link, itemIndex) {
        var active = itemIndex === activeIndex;
        link.classList.toggle('is-active', active);
        link.setAttribute('aria-selected', active ? 'true' : 'false');
        if (active) link.scrollIntoView({ block: 'nearest' });
      });
    }

    input.addEventListener('input', function () {
      var query = input.value.trim();
      clearButton.hidden = !query;
      activeIndex = -1;
      window.clearTimeout(debounceTimer);

      if (query.length < MIN_QUERY_LENGTH) {
        requestSequence++;
        clearResults();
        return;
      }

      debounceTimer = window.setTimeout(function () {
        runSearch(query);
      }, DEBOUNCE_MS);
    });

    input.addEventListener('focus', function () {
      if (input.value.trim().length >= MIN_QUERY_LENGTH && results.hidden) {
        runSearch(input.value);
      }
    });

    input.addEventListener('keydown', function (event) {
      if (event.key === 'ArrowDown') {
        event.preventDefault();
        if (results.hidden) runSearch(input.value);
        setActiveResult(activeIndex + 1);
      } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        setActiveResult(activeIndex - 1);
      } else if (event.key === 'Enter' && activeIndex >= 0) {
        var links = getResultLinks();
        if (links[activeIndex]) {
          event.preventDefault();
          window.location.href = links[activeIndex].href;
        }
      } else if (event.key === 'Escape') {
        setOpen(false);
        input.blur();
      }
    });

    clearButton.addEventListener('click', function () {
      input.value = '';
      clearButton.hidden = true;
      requestSequence++;
      window.clearTimeout(debounceTimer);
      clearResults();
      input.focus();
    });

    document.addEventListener('click', function (event) {
      if (!widget.contains(event.target)) setOpen(false);
    });

    form.addEventListener('submit', function (event) {
      if (!input.value.trim()) {
        event.preventDefault();
        input.focus();
      }
    });
  }

  function boot(root) {
    root = root || document;
    var widgets = root.querySelectorAll ? root.querySelectorAll(SELECTOR) : [];
    Array.prototype.forEach.call(widgets, function (widget) {
      initSearch(widget.parentNode || document);
    });
  }

  function bootWithModules(root) {
    if (window.mw && mw.loader && mw.loader.using) {
      mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
        boot(root);
      }, function () {
        boot(root);
      });
    } else {
      boot(root);
    }
  }

  window.OrozaWikiSearch = {
    rebuild: function () {
      clearIndexCache();
      return true;
    },
    clearCache: clearIndexCache
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', function () {
      bootWithModules(document);
    });
  } else {
    bootWithModules(document);
  }

  if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(function ($content) {
      var node = $content && $content[0] ? $content[0] : document;
      bootWithModules(node);
      bootWithModules(document);
    });
  }
})();