diff --git a/background/background.js b/background/background.js index 5db7baa..ff98c44 100644 --- a/background/background.js +++ b/background/background.js @@ -1,86 +1,230 @@ -const GOOGLE_SPEECH_URI = 'https://www.google.com/speech-api/v1/synthesize', - - DEFAULT_HISTORY_SETTING = { - enabled: true - }; +const DEFAULT_HISTORY_SETTING = { enabled: true }; browser.runtime.onMessage.addListener((request, sender, sendResponse) => { - const { word, lang } = request, - url = `https://www.google.com/search?hl=${lang}&q=define+${word}&gl=US`; - - fetch(url, { - method: 'GET' - }) - .then((response) => response.text()) - .then((text) => { - const document = new DOMParser().parseFromString(text, 'text/html'), - content = extractMeaning(document, { word, lang }); - + lookupWord(request.word, request.lang) + .then((content) => { sendResponse({ content }); - content && browser.storage.local.get().then((results) => { - let history = results.history || DEFAULT_HISTORY_SETTING; - - history.enabled && saveWord(content) - }); + if (content) { + browser.storage.local.get().then((results) => { + const history = results.history || DEFAULT_HISTORY_SETTING; + if (history.enabled) { + saveWord(content); + } + }); + } }) + .catch((error) => { + console.error("Dictionary lookup failed:", error); + sendResponse({ content: null }); + }); return true; }); -function extractMeaning (document, context) { - if (!document.querySelector("[data-dobid='hdw']")) { return null; } - - var word = document.querySelector("[data-dobid='hdw']").textContent, - definitionDiv = document.querySelector("div[data-dobid='dfn']"), - meaning = ""; - - if (definitionDiv) { - definitionDiv.querySelectorAll("span").forEach(function(span){ - if(!span.querySelector("sup")) - meaning = meaning + span.textContent; +async function lookupWord(rawWord, language) { + const word = (rawWord || "").trim(); + if (!word) { + return null; + } + + if (language === "de") { + return lookupGerman(word); + } + if (language === "fr") { + return lookupFrench(word); + } + if (language === "es") { + return lookupSpanish(word); + } + + return lookupEnglish(word); +} + +async function lookupEnglish(word) { + const url = `https://en.wiktionary.org/api/rest_v1/page/definition/${encodeURIComponent(word)}`; + const response = await fetch(url); + if (!response.ok) { + return null; + } + + const result = await response.json(); + const entries = result.en; + if (!entries || !entries.length) { + return null; + } + + const definitions = []; + entries.forEach((entry) => { + (entry.definitions || []).forEach((item) => { + if (item.definition && definitions.length < 3) { + definitions.push(htmlToText(item.definition)); + } }); + }); + + return createContent(word, definitions); +} + +async function lookupGerman(word) { + const parsed = await fetchWiktionaryPage("de", word); + if (!parsed) { + return null; } - meaning = meaning[0].toUpperCase() + meaning.substring(1); + const heading = Array.from(parsed.querySelectorAll("p")).find((node) => { + return node.textContent.trim().replace(/:$/, "") === "Bedeutungen"; + }); + const definitionList = heading && heading.nextElementSibling; + + if (!definitionList || definitionList.tagName !== "DL") { + return null; + } - var audio = document.querySelector("audio[jsname='QInZvb']"), - source = document.querySelector("audio[jsname='QInZvb'] source"), - audioSrc = source && source.getAttribute('src'); + const definitions = Array.from(definitionList.querySelectorAll(":scope > dd")) + .slice(0, 3) + .map(cleanDefinitionNode) + .filter(Boolean); - if (audioSrc) { - !audioSrc.includes("http") && (audioSrc = audioSrc.replace("//", "https://")); + return createContent(word, definitions); +} + +async function lookupFrench(word) { + const parsed = await fetchWiktionaryPage("fr", word); + if (!parsed) { + return null; } - else if (audio) { - let exactWord = word.replace(/ยท/g, ''), // We do not want syllable seperator to be present. - - queryString = new URLSearchParams({ - text: exactWord, - enc: 'mpeg', - lang: context.lang, - speed: '0.4', - client: 'lr-language-tts', - use_google_only_voices: 1 - }).toString(); - audioSrc = `${GOOGLE_SPEECH_URI}?${queryString}`; + const section = getLanguageSection(parsed, "fr"); + const definitionList = findFirstInSection(section, "ol:not(.references)"); + if (!definitionList) { + return null; } - return { word: word, meaning: meaning, audioSrc: audioSrc }; -}; + const definitions = Array.from(definitionList.children) + .filter((node) => node.tagName === "LI") + .slice(0, 3) + .map(cleanDefinitionNode) + .filter(Boolean); -function saveWord (content) { - let word = content.word, - meaning = content.meaning, - - storageItem = browser.storage.local.get('definitions'); + return createContent(word, definitions); +} - storageItem.then((results) => { - let definitions = results.definitions || {}; +async function lookupSpanish(word) { + const parsed = await fetchWiktionaryPage("es", word); + if (!parsed) { + return null; + } - definitions[word] = meaning; - browser.storage.local.set({ - definitions - }); - }) -} \ No newline at end of file + const section = getLanguageSection(parsed, "es"); + const definitions = []; + + section.some((element) => { + const lists = []; + if (element.matches && element.matches("dl")) { + lists.push(element); + } + lists.push(...element.querySelectorAll("dl")); + + lists.forEach((list) => { + const definition = list.querySelector(":scope > dd"); + if (definition && definitions.length < 3) { + definitions.push(cleanDefinitionNode(definition)); + } + }); + + return definitions.length >= 3; + }); + + return createContent(word, definitions.filter(Boolean)); +} + +async function fetchWiktionaryPage(language, word) { + const params = new URLSearchParams({ + action: "parse", + page: word, + prop: "text", + format: "json", + origin: "*" + }); + const url = `https://${language}.wiktionary.org/w/api.php?${params.toString()}`; + const response = await fetch(url); + + if (!response.ok) { + return null; + } + + const result = await response.json(); + if (!result.parse || !result.parse.text) { + return null; + } + + return new DOMParser().parseFromString(result.parse.text["*"], "text/html"); +} + +function getLanguageSection(parsed, languageId) { + const marker = parsed.getElementById(languageId); + const heading = marker && marker.closest("h2"); + const headingContainer = heading && heading.parentElement; + const elements = []; + + if (!headingContainer) { + return elements; + } + + let current = headingContainer.nextElementSibling; + while (current && !current.classList.contains("mw-heading2")) { + elements.push(current); + current = current.nextElementSibling; + } + + return elements; +} + +function findFirstInSection(section, selector) { + for (const element of section) { + if (element.matches && element.matches(selector)) { + return element; + } + + const match = element.querySelector(selector); + if (match) { + return match; + } + } + + return null; +} + +function cleanDefinitionNode(node) { + const copy = node.cloneNode(true); + copy.querySelectorAll("ul, ol, dl, sup, .example, .sources").forEach((child) => { + child.remove(); + }); + return copy.textContent.replace(/^\s*\[?\d+[\],.\s]*/, "").replace(/\s+/g, " ").trim(); +} + +function createContent(word, definitions) { + if (!definitions || !definitions.length) { + return null; + } + + return { + word, + meaning: definitions.join("; "), + audioSrc: null + }; +} + +function htmlToText(html) { + const parsed = new DOMParser().parseFromString(html, "text/html"); + return parsed.body.textContent.replace(/\s+/g, " ").trim(); +} + +function saveWord(content) { + browser.storage.local.get("definitions").then((results) => { + const definitions = results.definitions || {}; + definitions[content.word] = content.meaning; + browser.storage.local.set({ definitions }); + }); +} diff --git a/manifest.json b/manifest.json index d3bfd57..b709b21 100644 --- a/manifest.json +++ b/manifest.json @@ -3,10 +3,19 @@ "manifest_version": 2, "name": "Dictionary Anywhere", - "version": "1.2.0", + "version": "1.3.0", "description": "View definitions easily as you browse the web. Double-click any word to view its definition in a small pop-up bubble.", + "browser_specific_settings": { + "gecko": { + "id": "dictionary-anywhere-addon@addons.mozilla.org", + "data_collection_permissions": { + "required": ["none"] + } + } + }, + "icons": { "48": "icons/Dictionary-48.png", "64": "icons/Dictionary-64.png", @@ -40,6 +49,10 @@ "permissions": [ "storage", - "https://www.google.com/" + "https://www.google.com/", + "https://en.wiktionary.org/*", + "https://de.wiktionary.org/*", + "https://fr.wiktionary.org/*", + "https://es.wiktionary.org/*" ] }