Diferencia entre revisiones de «MediaWiki:Common.js»
Ir a la navegación
Ir a la búsqueda
Sin resumen de edición |
Sin resumen de edición |
||
| Línea 307: | Línea 307: | ||
} | } | ||
})(); | })(); | ||
/* ========================================================= | /* ========================================================= | ||
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 () { | (function () { | ||
| Línea 321: | Línea 323: | ||
var SELECTOR = '[data-oroza-global-search]'; | var SELECTOR = '[data-oroza-global-search]'; | ||
var MIN_QUERY_LENGTH = 2; | var MIN_QUERY_LENGTH = 2; | ||
var SMART_LIMIT = 10; | |||
var API_LIMIT = 8; | var API_LIMIT = 8; | ||
var MENU_LIMIT = | var MENU_LIMIT = 3; | ||
var DEBOUNCE_MS = | 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) { | function normalizeText(value) { | ||
| Línea 329: | Línea 339: | ||
.toLowerCase() | .toLowerCase() | ||
.replace(/_/g, ' ') | .replace(/_/g, ' ') | ||
.replace(/ /gi, ' ') | |||
.replace(/[\u2010-\u2015]/g, '-') | |||
.replace(/[^a-z0-9@+#áéíóúüñçàèìòùâêîôûäëïöü\s-]/gi, ' ') | |||
.replace(/\s+/g, ' ') | .replace(/\s+/g, ' ') | ||
.trim(); | .trim(); | ||
| Línea 345: | Línea 358: | ||
.replace(/\s+/g, ' ') | .replace(/\s+/g, ' ') | ||
.trim(); | .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; | |||
} | } | ||
| Línea 355: | Línea 466: | ||
var marker = '/wiki/index.php/'; | var marker = '/wiki/index.php/'; | ||
var markerIndex = url.pathname.indexOf(marker); | var markerIndex = url.pathname.indexOf(marker); | ||
if (markerIndex !== -1) | if (markerIndex !== -1) title = url.pathname.slice(markerIndex + marker.length); | ||
} | } | ||
| Línea 371: | Línea 480: | ||
return isEnglish ? { | return isEnglish ? { | ||
placeholder: 'Search guides, commands, systems | placeholder: 'Search guides, commands, items, systems and content...', | ||
ariaLabel: 'Search | ariaLabel: 'Search inside every Oroza RO Wiki guide', | ||
searchButton: 'Search', | searchButton: 'Search', | ||
loading: 'Searching the | 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', | noResultsTitle: 'No results found', | ||
noResultsText: 'Try a shorter | noResultsText: 'Try another spelling, a shorter phrase, or a related Ragnarok term.', | ||
categoryFallback: 'Wiki | categoryFallback: 'Wiki guide', | ||
sectionPrefix: 'Section', | sectionPrefix: 'Section', | ||
insideGuide: 'Inside guide', | |||
titleMatch: 'Guide title', | |||
aliasMatch: 'Related term', | |||
resultSingular: 'result', | resultSingular: 'result', | ||
resultPlural: 'results', | resultPlural: 'results', | ||
viewAll: 'View | viewAll: 'View native search', | ||
suggestion: 'Did you mean', | suggestion: 'Did you mean', | ||
apiErrorTitle: ' | indexedGuides: 'guides indexed', | ||
apiErrorText: ' | 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, sistemas | placeholder: 'Buscar guías, comandos, objetos, sistemas y contenido...', | ||
ariaLabel: 'Buscar | ariaLabel: 'Buscar dentro de todas las guías de la Wiki de Oroza RO', | ||
searchButton: 'Buscar', | searchButton: 'Buscar', | ||
loading: 'Buscando en | 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', | noResultsTitle: 'No encontramos resultados', | ||
noResultsText: 'Prueba | noResultsText: 'Prueba otra escritura, una frase más corta o un término relacionado de Ragnarok.', | ||
categoryFallback: ' | categoryFallback: 'Guía de la Wiki', | ||
sectionPrefix: 'Sección', | sectionPrefix: 'Sección', | ||
insideGuide: 'Dentro de la guía', | |||
titleMatch: 'Título de la guía', | |||
aliasMatch: 'Término relacionado', | |||
resultSingular: 'resultado', | resultSingular: 'resultado', | ||
resultPlural: 'resultados', | resultPlural: 'resultados', | ||
viewAll: 'Ver | viewAll: 'Ver búsqueda nativa', | ||
suggestion: 'Quizá quisiste decir', | suggestion: 'Quizá quisiste decir', | ||
apiErrorTitle: 'La búsqueda | indexedGuides: 'guías indexadas', | ||
apiErrorText: ' | 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)) | |||
}; | }; | ||
} | } | ||
| Línea 414: | Línea 663: | ||
var category = (titleNode.textContent || '').replace(/\s+/g, ' ').trim(); | var category = (titleNode.textContent || '').replace(/\s+/g, ' ').trim(); | ||
var list = titleNode.nextElementSibling; | var list = titleNode.nextElementSibling; | ||
if (!list || String(list.tagName).toLowerCase() !== 'ul') return; | 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 title = decodePageName(link.getAttribute('href')); | ||
var labelNode = link.querySelector('.menu-text'); | var labelNode = link.querySelector('.menu-text'); | ||
| Línea 425: | Línea 672: | ||
label = String(label || '').replace(/›/g, '').replace(/\s+/g, ' ').trim(); | label = String(label || '').replace(/›/g, '').replace(/\s+/g, ' ').trim(); | ||
title = title || label; | title = title || label; | ||
if (!title || !link.href) return; | if (!title || !link.href) return; | ||
| Línea 445: | Línea 691: | ||
if (!normalizedQuery) return []; | if (!normalizedQuery) return []; | ||
return entries | 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) { | function buildCategoryMap(entries) { | ||
var map = Object.create(null); | var map = Object.create(null); | ||
entries.forEach(function (entry) { | entries.forEach(function (entry) { | ||
map[normalizeText(entry.title)] = entry.category; | map[normalizeText(entry.title)] = entry.category; | ||
map[normalizeText(entry.label)] = 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; | |||
}); | }); | ||
return | 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); | |||
} | } | ||
| Línea 485: | Línea 1032: | ||
function initSearch(root) { | function initSearch(root) { | ||
var widget = root.querySelector ? root.querySelector(SELECTOR) : null; | var widget = root.querySelector ? root.querySelector(SELECTOR) : null; | ||
if (!widget || widget.dataset. | if (!widget || widget.dataset.orozaSmartReady === '2') return; | ||
widget.dataset.orozaSmartReady = '2'; | |||
widget.dataset. | |||
var form = widget.querySelector('.oroza-global-search-form'); | var form = widget.querySelector('.oroza-global-search-form'); | ||
| Línea 494: | Línea 1040: | ||
var clearButton = widget.querySelector('[data-oroza-search-clear]'); | var clearButton = widget.querySelector('[data-oroza-search-clear]'); | ||
var submitText = widget.querySelector('.oroza-global-search-submit span:last-child'); | var submitText = widget.querySelector('.oroza-global-search-submit span:last-child'); | ||
var helper = widget.querySelector('.oroza-global-search-helper'); | |||
if (!form || !input || !results) return; | if (!form || !input || !results || !clearButton) return; | ||
var labels = getLocale(); | var labels = getLocale(); | ||
var isEnglish = window.location.hostname === 'en.orozaro.com'; | |||
var synonymMap = getSynonymMap(isEnglish); | |||
var menuEntries = collectMenuEntries(); | var menuEntries = collectMenuEntries(); | ||
var categoryMap = buildCategoryMap(menuEntries); | var categoryMap = buildCategoryMap(menuEntries); | ||
var | var apiCache = Object.create(null); | ||
var debounceTimer = null; | var debounceTimer = null; | ||
var requestSequence = 0; | var requestSequence = 0; | ||
| Línea 509: | Línea 1058: | ||
input.setAttribute('aria-label', labels.ariaLabel); | input.setAttribute('aria-label', labels.ariaLabel); | ||
if (submitText) submitText.textContent = labels.searchButton; | 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) { | function getNativeSearchUrl(query) { | ||
| Línea 545: | Línea 1099: | ||
var main = createElement('div', 'oroza-search-result-main'); | var main = createElement('div', 'oroza-search-result-main'); | ||
main.appendChild(createElement('span', 'oroza-search-result-title', item.label || item.title)); | 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) { | if (item.snippet) { | ||
| Línea 557: | Línea 1115: | ||
function addSuggestion(suggestion) { | function addSuggestion(suggestion) { | ||
if (!suggestion) return; | if (!suggestion) return; | ||
var box = createElement('div', 'oroza-search-suggestion'); | var box = createElement('div', 'oroza-search-suggestion'); | ||
box.appendChild(document.createTextNode(labels.suggestion + ': ')); | box.appendChild(document.createTextNode(labels.suggestion + ': ')); | ||
var button = createElement('button', '', suggestion); | var button = createElement('button', '', suggestion); | ||
button.type = 'button'; | button.type = 'button'; | ||
| Línea 568: | Línea 1124: | ||
input.focus(); | input.focus(); | ||
}); | }); | ||
box.appendChild(button); | box.appendChild(button); | ||
results.appendChild(box); | results.appendChild(box); | ||
} | } | ||
function addFooter(query, totalHits) { | function addFooter(query, totalHits, indexState) { | ||
var footer = createElement('div', 'oroza-search-footer'); | var footer = createElement('div', 'oroza-search-footer'); | ||
var left = createElement('span', 'oroza-search-footer-count'); | |||
var countText = ''; | var countText = ''; | ||
| Línea 581: | Línea 1137: | ||
} | } | ||
footer.appendChild(createElement('span', '', | left.textContent = countText; | ||
footer.appendChild(left); | |||
if (indexState) { | |||
footer.appendChild(createElement('span', 'oroza-search-index-state', indexState)); | |||
} | |||
var link = createElement('a', '', labels.viewAll + ' →'); | var link = createElement('a', '', labels.viewAll + ' →'); | ||
| Línea 591: | Línea 1152: | ||
function renderLoading(menuMatches) { | function renderLoading(menuMatches) { | ||
while (results.firstChild) results.removeChild(results.firstChild); | while (results.firstChild) results.removeChild(results.firstChild); | ||
if (menuMatches.length) { | if (menuMatches.length) { | ||
addGroupTitle(labels.menuGroup); | addGroupTitle(labels.menuGroup); | ||
| Línea 600: | Línea 1160: | ||
href: item.href, | href: item.href, | ||
category: item.category, | category: item.category, | ||
location: labels.titleMatch, | |||
snippet: '' | snippet: '' | ||
}); | }); | ||
}); | }); | ||
} | } | ||
var status = createElement('div', 'oroza-search-status'); | var status = createElement('div', 'oroza-search-status'); | ||
status.appendChild(createElement('span', 'oroza-search-spinner')); | status.appendChild(createElement('span', 'oroza-search-spinner')); | ||
| Línea 612: | Línea 1172: | ||
} | } | ||
function | function renderState(query, state) { | ||
while (results.firstChild) results.removeChild(results.firstChild); | 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); | var key = normalizeText(item.title); | ||
if (!key || seen[key]) return; | if (!key || seen[key]) return; | ||
seen[key] = true; | seen[key] = true; | ||
apiResults.push(item); | |||
}); | }); | ||
(state.menu || []).forEach(function (item) { | |||
var key = normalizeText(item.title); | var key = normalizeText(item.title); | ||
if (!key || seen[key]) return; | if (!key || seen[key]) return; | ||
seen[key] = true; | seen[key] = true; | ||
menuMatches.push(item); | |||
}); | }); | ||
if ( | if (smartResults.length) { | ||
addGroupTitle(labels.smartGroup); | |||
smartResults.forEach(addResultItem); | |||
} | |||
if (apiResults.length) { | |||
addGroupTitle(labels.apiGroup); | |||
apiResults.forEach(addResultItem); | |||
} | } | ||
if ( | if (menuMatches.length) { | ||
addGroupTitle(labels.menuGroup); | addGroupTitle(labels.menuGroup); | ||
menuMatches.forEach(function (item) { | |||
addResultItem({ | addResultItem({ | ||
title: item.title, | title: item.title, | ||
| Línea 699: | Línea 1219: | ||
href: item.href, | href: item.href, | ||
category: item.category, | category: item.category, | ||
location: labels.titleMatch, | |||
snippet: '' | snippet: '' | ||
}); | }); | ||
| Línea 704: | Línea 1225: | ||
} | } | ||
if ( | 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); | |||
} | } | ||
addSuggestion( | if (state.smartLoading) { | ||
addFooter(query, | 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); | setOpen(true); | ||
} | } | ||
| Línea 726: | Línea 1271: | ||
var section = stripHtml(item.sectiontitle || ''); | var section = stripHtml(item.sectiontitle || ''); | ||
var snippet = stripHtml(item.snippet || ''); | var snippet = stripHtml(item.snippet || ''); | ||
var location = labels.insideGuide; | |||
if (section) | if (section) location += ' · ' + labels.sectionPrefix + ': ' + section; | ||
return { | return { | ||
| Línea 736: | Línea 1280: | ||
href: (window.mw && mw.util) ? mw.util.getUrl(item.title) : '/wiki/index.php?title=' + encodeURIComponent(item.title.replace(/ /g, '_')), | href: (window.mw && mw.util) ? mw.util.getUrl(item.title) : '/wiki/index.php?title=' + encodeURIComponent(item.title.replace(/ /g, '_')), | ||
category: category, | category: category, | ||
location: location, | |||
snippet: snippet | snippet: snippet | ||
}; | }; | ||
| Línea 742: | Línea 1287: | ||
} | } | ||
function | function searchNative(query) { | ||
var 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', | action: 'query', | ||
list: 'search', | list: 'search', | ||
srsearch: query, | srsearch: query, | ||
srwhat: 'text', | |||
srnamespace: 0, | srnamespace: 0, | ||
srlimit: API_LIMIT, | srlimit: API_LIMIT, | ||
| Línea 777: | Línea 1304: | ||
formatversion: 2, | formatversion: 2, | ||
utf8: 1 | 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: [], | ||
if ( | 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); | |||
}); | }); | ||
} | } | ||
| Línea 796: | Línea 1366: | ||
var links = getResultLinks(); | var links = getResultLinks(); | ||
if (!links.length) return; | if (!links.length) return; | ||
if (index < 0) index = links.length - 1; | if (index < 0) index = links.length - 1; | ||
if (index >= links.length) index = 0; | if (index >= links.length) index = 0; | ||
activeIndex = index; | |||
activeIndex | 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 () { | input.addEventListener('input', function () { | ||
var query = input.value; | var query = input.value.trim(); | ||
clearButton.hidden = !query; | clearButton.hidden = !query; | ||
activeIndex = -1; | activeIndex = -1; | ||
window.clearTimeout(debounceTimer); | window.clearTimeout(debounceTimer); | ||
if | if (query.length < MIN_QUERY_LENGTH) { | ||
requestSequence++; | requestSequence++; | ||
clearResults(); | clearResults(); | ||
| Línea 830: | Línea 1397: | ||
input.addEventListener('focus', function () { | input.addEventListener('focus', function () { | ||
if (input.value.trim().length >= MIN_QUERY_LENGTH && results. | if (input.value.trim().length >= MIN_QUERY_LENGTH && results.hidden) { | ||
runSearch(input.value); | |||
} | } | ||
}); | }); | ||
| Línea 878: | Línea 1445: | ||
function boot(root) { | function boot(root) { | ||
root = root || document; | root = root || document; | ||
var widgets = root.querySelectorAll ? root.querySelectorAll(SELECTOR) : []; | var widgets = root.querySelectorAll ? root.querySelectorAll(SELECTOR) : []; | ||
Array.prototype.forEach.call(widgets, function (widget) { | Array.prototype.forEach.call(widgets, function (widget) { | ||
| Línea 885: | Línea 1451: | ||
} | } | ||
function | function bootWithModules(root) { | ||
if (window.mw && mw.loader && mw.loader.using) { | if (window.mw && mw.loader && mw.loader.using) { | ||
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () { | mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () { | ||
| Línea 896: | Línea 1462: | ||
} | } | ||
} | } | ||
window.OrozaWikiSearch = { | |||
rebuild: function () { | |||
clearIndexCache(); | |||
return true; | |||
}, | |||
clearCache: clearIndexCache | |||
}; | |||
if (document.readyState === 'loading') { | if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', function () { | document.addEventListener('DOMContentLoaded', function () { | ||
bootWithModules(document); | |||
}); | }); | ||
} else { | } else { | ||
bootWithModules(document); | |||
} | } | ||
| Línea 908: | Línea 1482: | ||
mw.hook('wikipage.content').add(function ($content) { | mw.hook('wikipage.content').add(function ($content) { | ||
var node = $content && $content[0] ? $content[0] : document; | var node = $content && $content[0] ? $content[0] : document; | ||
bootWithModules(node); | |||
bootWithModules(document); | |||
}); | }); | ||
} | } | ||
})(); | })(); | ||
Revisión del 11:06 10 jul 2026
(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(/ /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);
});
}
})();