« get me outta code hell

precanon - Homestuck^2: Preceeding Canon - (re)presentation of HS2 stylized upon Prequel
summary refs log tree commit diff
path: root/search.js
diff options
context:
space:
mode:
Diffstat (limited to 'search.js')
-rw-r--r--search.js289
1 files changed, 289 insertions, 0 deletions
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));