From 89cadbf468519782bc9c2396d6f3f7d75703d846 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Nov 2020 10:51:09 -0400 Subject: initial commit --- search.js | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 search.js (limited to 'search.js') diff --git a/search.js b/search.js new file mode 100644 index 0000000..61cc67f --- /dev/null +++ b/search.js @@ -0,0 +1,289 @@ +const characterList = [ + {id: 'aradia', name: 'Aradia'}, + {id: 'alt-callie', name: '(Alt.) Calliope'}, + {id: 'dave', name: 'Dave'}, + {id: 'dirk', name: 'Dirk'}, + {id: 'harry', name: 'Harry Anderson'}, + {id: 'jade', name: 'Jade'}, + {id: 'jake', name: 'Jake'}, + {id: 'jane', name: 'Jane'}, + {id: 'john', name: 'John'}, + {id: 'kanaya', name: 'Kanaya'}, + {id: 'karkat', name: 'Karkat'}, + {id: 'rose', name: 'Rose'}, + {id: 'roxy', name: 'Roxy'}, + { + id: 'sollux', name: 'Sollux', + convert: text => (text + .replace(/0/g, 'o')) + }, + {id: 'tavros', name: 'Tavros (Crocker)'}, + { + id: 'terezi', name: 'Terezi', + convert: text => (text + .replace(/4/g, 'A') + .replace(/1/g, 'I') + .replace(/3/g, 'E')) + }, + { + id: 'vriska', name: 'Vriska (Serket)', + convert: text => (text + .replace(/8/g, 'b')) + }, + { + id: 'vrissy', name: 'Vrissy (ML)', + convert: text => (text + .replace(/8/g, 'b')) + }, + {id: 'yiffy', name: 'Yiffy'} +]; + +const parser = new DOMParser(); + +function fetchPage(url) { + return fetch(url) + .then(res => res.text()) + .then(html => parser.parseFromString(html, 'text/html')); +} + +function eachEl(doc, query, cb) { + return Array.from(doc.querySelectorAll(query)).map(cb); +} + +function fillEl(el, newChildren) { + while (el.firstChild) { + el.firstChild.remove(); + } + for (const child of newChildren) { + el.appendChild(child); + } +} + +function makeOption(value, label) { + const opt = document.createElement('option'); + opt.value = value; + opt.appendChild(document.createTextNode(label)); + return opt; +} + +async function fetchData() { + const indexDoc = await fetchPage("index.html"); + + const chaptersP = Promise.all(eachEl(indexDoc, '#chapters a', async a => { + const chapterDoc = await fetchPage(a.href); + const id = new URL(a.href).pathname; + const name = a.innerText; + return { + id, name, + doc: chapterDoc, + texts: eachEl(chapterDoc, 'p:not(.command-line), .command div', el => { + let text = el.innerText; + const type = ( + el.closest('.chat') ? 'chat' : + el.closest('.command') ? 'command' : + 'prose' + ); + + const contents = document.createDocumentFragment(); + contents.appendChild(el.cloneNode(true)); + + let extraProps = {}; + + switch (type) { + case 'chat': { + text = (text.match(/(?<=: ).*/) || [''])[0]; + let character = el.className; + for (const [ elClass, parentQuery, replace ] of [ + ['jade', '.dead-jade', 'alt-callie'], + ['ag', '.ag-vrissy', 'vrissy'], + ['gg', '.gg-tavros', 'tavros'], + ['tg', '.tg-harry', 'harry'] + ]) { + if (character === elClass && el.closest(parentQuery)) { + character = replace; + break; + } + } + extraProps = {character}; + break; + } + } + return {el, contents, text, ...extraProps}; + }) + }; + })); + + return { + chapters: await chaptersP + }; +} + +function convertCharacterString(string, characterId) { + const character = characterList.find(char => char.id === characterId); + if (character && character.convert) { + return character.convert(string); + } else { + return string; + } +} + +const transformString = string => string + .split(' ') + .map(word => word + .toLowerCase() + .replace(/['"!?.,]/g, '') + ) + .filter(Boolean); + +function runTextSearch(form, data) { + const filteredChapters = ( + form.elements['do-chapter'].checked + ? [data.chapters.find(chapter => chapter.id === form.elements['chapter'].value)] + : data.chapters + ); + const filterString = form.elements['text'].value; + let filterFunc = () => true; + if (filterString) { + const last = filterFunc; + let transformText; + if (form.elements['do-convert'].checked) { + transformText = text => { + return transformString(convertCharacterString(text.text, text.character)); + }; + } else { + transformText = text => transformString(text.text); + } + const compareWords = transformString(filterString); + filterFunc = text => { + const entryWords = transformText(text); + for (const word of compareWords) { + if (!entryWords.includes(word)) { + return false; + } + } + return last(text); + }; + } + if (form.elements['do-character'].checked) { + const last = filterFunc; + const character = form.elements['character'].value; + filterFunc = text => { + return (text.character === character) && last(text); + }; + } + const results = []; + for (const chapter of filteredChapters) { + for (const text of chapter.texts) { + if (filterFunc(text)) { + results.push(text); + } + } + } + displayTextResults(form, filterString, results); +} + +function displayTextResults(form, filterString, results) { + const container = document.getElementById('text-results'); + container.style.visibility = 'visible'; + + const compareWords = transformString(filterString); + + paginateResults(container, results, text => { + const div = document.createElement('div'); + const p = document.createElement('p'); + let transformWord = word => transformString(word)[0]; + if (text.character) { + p.classList.add(text.character); + if (form.elements['do-convert'].checked) { + transformWord = word => transformString(convertCharacterString(word, text.character))[0]; + } + } else { + p.className = text.contents.children[0].className; + } + let chunk = ''; + const pushChunk = () => { + if (chunk) { + p.appendChild(document.createTextNode(chunk)); + chunk = ''; + } + }; + const words = text.text.split(' '); + for (let i = 0; i < words.length; i++) { + if (i > 0) { + chunk += ' '; + } + const word = words[i]; + const transformed = transformWord(word); + console.log(transformed); + if (compareWords.includes(transformed)) { + pushChunk(); + const b = document.createElement('b'); + b.appendChild(document.createTextNode(word)); + p.appendChild(b); + } else { + chunk += word; + } + } + pushChunk(); + div.appendChild(p); + return div; + }); +} + +function paginateResults(container, results, makeResultRow) { + const heading = container.querySelector('.results-heading'); + const list = container.querySelector('.results-list'); + + const headingNodes = [ + document.createTextNode(`${results.length} result${results.length === 1 ? '' : 's'}`) + ]; + + const pageSize = 100; + if (results.length > pageSize) { + const pageAnchors = []; + for (let i = 0; i < results.length; i += pageSize) { + const start = i; + const end = Math.min(i + pageSize, results.length); + const a = document.createElement('a'); + a.appendChild(document.createTextNode(`${start + 1}\u2013${end}`)); + a.href = '#'; + a.addEventListener('click', event => { + event.preventDefault(); + for (const a of pageAnchors) { + a.href = '#'; + } + a.removeAttribute('href'); + fillEl(list, results.slice(start, end).map(makeResultRow)); + }); + pageAnchors.push(a); + if (start === 0) { + headingNodes.push(document.createTextNode(': ')); + } + headingNodes.push(a); + if (end < results.length) { + headingNodes.push(document.createTextNode(', ')); + } + } + pageAnchors[0].click(); + } else { + fillEl(list, results.map(makeResultRow)); + } + + fillEl(heading, headingNodes); +} + +async function main() { + const data = await fetchData(); + const textForm = document.forms.text; + const panelForm = document.forms.panel; + + fillEl(textForm.elements['character'], characterList.map(({ id, name }) => makeOption(id, name))); + fillEl(textForm.elements['chapter'], data.chapters.map(({ id, name }) => makeOption(id, name))); + + textForm.addEventListener('submit', event => { + event.preventDefault(); + runTextSearch(textForm, data); + }); +} + +main().catch(err => console.error(err)); -- cgit 1.3.0-6-gf8a5