/* [diction] nobody @ goeshard.org * GNU affero general public license v3 */ const definitionContainer = (() => { const container = document.createElement('div') container.id = 'diction-container' document.body.appendChild(container) return container })() let isDragging = false; const getDefinition = async (word) => { const handleRequest = async () => { const baseUrl = 'https://api.dictionaryapi.dev/api/v2/entries/en/' try { const response = await fetch(baseUrl + word); const data = await response.json(); if (response.ok) { localStorage.setItem(word, JSON.stringify(data)); useDefinition(data) } else if (response.status === 404) { localStorage.setItem(word, JSON.stringify("404")); handle404() } else { console.error(`Error ${response.status}:`, response.statusText); } } catch (error) { console.error('Error fetching data:', error); } } const handleCached = () => { setTimeout(() => { if (JSON.parse(cachedData) == "404") { handle404() } else { useDefinition(JSON.parse(cachedData)) } }, 0) } const handle404 = () => { const errorTable = generateTable({ word: word, phonetic: "Not Found" }, isError = true) definitionContainer.appendChild(errorTable) } const withRetry = async (callback) => { const maxTry = 5 const delay = 100 let currentTry = 0; function sleep(ms) { return new Promise(resolve => { setTimeout(() => { resolve() }, ms); }); } while (true) { try { callback(); break; } catch (error) { currentTry++; if (currentTry >= maxTry) { break; } } await sleep(delay); } } const cachedData = localStorage.getItem(word); if (cachedData) handleCached() else withRetry(handleRequest) } const useDefinition = (data) => { definitionContainer.innerHTML = "" data.forEach((word, index) => { word.words = { total: data.length, current: index + 1 } const phonetics = word.phonetics; let foundAudio = false; phonetics.forEach(p => { if (!foundAudio && p.audio.includes('mp3')) { word.audio = p.audio foundAudio = true; } }); const table = generateTable(word) definitionContainer.appendChild(table) preventSnapbackOnHeightChange() }) } const playPhonetic = (mp3) => { const audio = new Audio(mp3); audio.play(); } const removePunctuation = (s) => { const punctuation = /[.,\/#!$%\^&\*;:{}=\-_`~()]+/g; return s.replace(punctuation, "").replace(/^"|"$/g, "").replace(/\s{2,}/g, " "); }; function generateTable(entry, isError = false) { function initializeTable() { const table = document.createElement('table'); table.classList.add('diction'); return table } function createRow(headerText, contentElement) { const row = document.createElement('tr'); const header = document.createElement('th'); const data = document.createElement('td'); header.textContent = headerText; data.colSpan = 2; if (contentElement instanceof Element) { data.appendChild(contentElement); } else { data.textContent = contentElement; } row.appendChild(header); row.appendChild(data); return row; } function createWordRow(entry) { const row = document.createElement('tr'); const header = document.createElement('th'); header.textContent = 'Word'; const data = document.createElement('td'); const wordSpan = document.createElement('span'); wordSpan.classList.add('word'); wordSpan.textContent = entry.word; const phoneticItalic = document.createElement('i'); phoneticItalic.textContent = entry.phonetic; if (entry.audio) { const soundIcon = document.createElement('img'); soundIcon.src = 'https://files.catbox.moe/fg1yf5.svg'; soundIcon.addEventListener("click", () => playPhonetic(entry.audio)); phoneticItalic.insertBefore(soundIcon, null); } const paginContainer = document.createElement('div') paginContainer.id = 'pagin-container' const renderPaginationIcons = () => { if (entry.words.total > 1 && entry.words.current != 1) { const prevIcon = document.createElement('img') prevIcon.src = 'https://files.catbox.moe/gcaqne.svg' prevIcon.addEventListener("click", () => { changeDefinitionPage(entry.words.current - 2) }) paginContainer.appendChild(prevIcon) } if (entry.words.total > 1 && entry.words.current != 1 || entry.words.total > 1 && entry.words.current != entry.words.total) { const paginDisplay = document.createElement('span') paginDisplay.textContent = entry.words.current + " / " + entry.words.total paginContainer.appendChild(paginDisplay) } if (entry.words.total > 1 && entry.words.current != entry.words.total) { const nextIcon = document.createElement('img') nextIcon.src = 'https://files.catbox.moe/ll2iqy.svg' nextIcon.addEventListener("click", () => { changeDefinitionPage(entry.words.current) }) paginContainer.appendChild(nextIcon) } } if (!isError) renderPaginationIcons() phoneticItalic.appendChild(paginContainer) data.appendChild(wordSpan); data.appendChild(phoneticItalic); row.appendChild(header); row.appendChild(data); return row; } function createTypeRow(partOfSpeech) { const typeRow = document.createElement('tr'); const typeHeader = document.createElement('th'); typeHeader.textContent = 'Type'; const typeData = document.createElement('td'); typeData.colSpan = 2; typeData.classList.add('type'); const typeSpan1 = document.createElement('span'); const typeSpan2 = document.createElement('span'); typeSpan1.textContent = partOfSpeech; typeSpan2.textContent = '_'; typeData.appendChild(typeSpan1); typeData.appendChild(typeSpan2); typeRow.appendChild(typeHeader); typeRow.appendChild(typeData); return typeRow; } function createList(items) { const list = document.createElement('ol'); items.forEach(item => { const listItem = document.createElement('li'); listItem.textContent = item; list.appendChild(listItem); }); return list; } function license() { // Do not remove this. const ctd = document.createElement('td') ctd.classList.add('ctd') ctd.innerText = "diction by " const ca = document.createElement('a') ca.classList.add('ca') ca.innerText = "nobo" ca.href = "https://everyone.melonland.net/nobo" ctd.appendChild(ca) return ctd } const table = initializeTable() table.appendChild(createWordRow(entry)); if (isError) { return table } const spaceRow = createRow('', ''); table.appendChild(spaceRow); entry.meanings.forEach(meaning => { table.appendChild(createTypeRow(meaning.partOfSpeech)); const definitionsList = createList(meaning.definitions.map(def => def.definition)); table.appendChild(createRow('Definitions', definitionsList)); if (meaning.synonyms.length > 0) { table.appendChild(createRow('Synonyms', meaning.synonyms.join(', '))); } if (meaning.antonyms.length > 0) { table.appendChild(createRow('Antonyms', meaning.antonyms.join(', '))); } if (meaning.example) { table.appendChild(createRow('Example', meaning.example)); } }); table.appendChild(createRow('', license())) return table; } function changeDefinitionPage(page) { const pages = definitionContainer.querySelectorAll('table'); pages.forEach((table, index) => { if (index === page) { table.style.display = 'block'; } else { table.style.display = 'none'; } }); } function preventSnapbackOnHeightChange() { const currentMinHeight = parseInt(window.getComputedStyle(document.body).minHeight); const currentScrollHeight = Math.max( document.documentElement.scrollHeight, document.body.scrollHeight ); const threshold = 19; if (currentScrollHeight - currentMinHeight > threshold) { document.body.style.minHeight = currentScrollHeight + 'px'; } } document.querySelectorAll('p').forEach(function (element) { element.addEventListener('click', function (e) { if (!isDragging && !definitionContainer.hasChildNodes()) { const topPosition = e.clientY + window.scrollY + 20; definitionContainer.style.top = `${topPosition}px`; const selection = window.getSelection(); const range = selection.getRangeAt(0); const selectClickedWord = (r) => { const startNode = r.startContainer; let startOffset = r.startOffset; const endNode = r.endContainer; let endOffset = r.endOffset; while (startOffset > 0 && r.toString()[0] !== ' ') { r.setStart(startNode, (startOffset - 1)); startOffset--; } while (endOffset < endNode.length && r.toString()[r.toString().length - 1] !== ' ') { r.setEnd(endNode, (endOffset + 1)); endOffset++; } } const removeSpacesFromSelection = (s, r) => { // Leading and trailing spaces are included in selecttions // This function tries to remove them for display purposes only. (Firefox Only) let selectLength = r.endOffset - r.startOffset if (r.toString()[0] != ' ') selectLength++; const onFireFox = () => { for (let i = 0; i < selectLength - 1; i++) { s.modify("move", "left", "character") } for (let i = 0; i < selectLength - 2; i++) { s.modify("extend", "right", "character") } } if (navigator.userAgent.includes("Firefox")) onFireFox() } selectClickedWord(range) removeSpacesFromSelection(selection, range) const word = removePunctuation(range.toString().trim()); if (word.length > 0 && !word.includes(" ")) getDefinition(word); if (range.toString() == " ") selection.removeAllRanges() } isDragging = false }) }) document.addEventListener('click', (e) => { const closeDefinitionTable = () => { if (!definitionContainer.contains(e.target) && definitionContainer.hasChildNodes()) { definitionContainer.innerHTML = "" } } closeDefinitionTable() }); document.addEventListener('mousedown', function (e) { if (e.detail > 1) e.preventDefault(); // prevent multiple word selection isDragging = false; }); document.addEventListener('mousemove', function (e) { isDragging = true; });