Dictionary recipes

Tested dictionary sources for Polypage · 8 recipes

Select a word while reading and choose Dictionary search: Polypage looks it up in the dictionaries you've added. Each one is a source - a search URL, and for card popups a short parse script that picks the useful parts of the dictionary's page and shows them over your book, so you never leave the page. The recipes below are ones we've tested; copy what you need.

Add a recipe in 1 minute
  1. Open the bookshelf → Settings (gear) → Dictionary → add a source.
  2. Click the copy icon next to the URL and paste it. Any name works.
  3. For card recipes: choose Card popup, open Parse script below, copy it and paste it. Turn on browser rendering when the recipe says On.
  4. Optional: set book languages so the dictionary only appears for books in that language.

Good to know

More in the manual: Dictionary sources.

Google Search

All languagesNew tabDefault

Searches the selected word or phrase on Google in a new tab. Works for every language; {lang} becomes your browser language. This is the default source that comes with Polypage.

NameGoogle
URL
https://www.google.com/search?q={query}&hl={lang}
Open asNew tab
Book languagesAll (leave empty)

Oxford Learner's Dictionaries

EnglishAudioExamplesCollocations

English learner's dictionary. Shows British and American pronunciation with audio, numbered meanings with examples, cross-references, extra examples, collocations and word origin. For inflected forms it lists "did you mean" suggestions.

Name옥스포드
URL
https://www.oxfordlearnersdictionaries.com/search/english/?q={query}
Open asCard popup
Browser renderingOff
Book languagesEnglish (en)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  // 활용형(예: "removed")은 페이지가 없고 "Did you mean" 추천만 나오는 경우가 있다.
  const didYouMean = doc.querySelector("#didyoumean");
  if (didYouMean) {
    const suggestions = Array.from(doc.querySelectorAll(".result-list .dym-link"))
      .slice(0, 6)
      .map((a) => esc(a.textContent.trim()));
    return `<div style="color:#888;">"${esc(word)}" 항목을 찾지 못했습니다.${
      suggestions.length ? `<br>혹시 이 단어를 찾으셨나요: ${suggestions.join(", ")}` : ""
    }</div>`;
  }

  const entries = doc.querySelectorAll("#entryContent .entry");
  if (entries.length === 0) {
    return `<div style="color:#888">"${esc(word)}"에 대한 항목을 찾지 못했습니다.</div>`;
  }

  const blocks = Array.from(entries).slice(0, 2).map((entry) => {
    const topWebtop = entry.querySelector(".webtop");
    const headword = esc(topWebtop?.querySelector(".headword")?.textContent?.trim()) || esc(word);
    const pos = esc(topWebtop?.querySelector(".pos")?.textContent?.trim());

    // 동사는 헤더 안에 "Verb Forms" 활용형 표가 숨어있고 그 표의 각 행마다 자기
    // 발음이 또 붙어있다. 헤더 바로 아래(직계 자식) .phonetics 하나만 잡아야
    // 활용형 발음까지 같이 끌려오지 않는다.
    const phoneticsEl = topWebtop?.querySelector(":scope > .phonetics");
    const phonRows = Array.from(phoneticsEl?.querySelectorAll(":scope > div") ?? []).map((phonEl) => {
      const label = phonEl.className.includes("phons_br") ? "영국" : phonEl.className.includes("phons_n_am") ? "미국" : "";
      const phon = esc(phonEl.querySelector(".phon")?.textContent?.trim());
      const src = phonEl.querySelector(".sound")?.getAttribute("data-src-mp3") || "";
      const audio = src
        ? `<audio controls preload="none" style="height:26px;width:170px;"><source src="${src}"></audio>`
        : "";
      return `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;margin-top:4px;"><span style="color:#555;">${label}</span><span style="color:#888;">${phon}</span>${audio}</div>`;
    }).join("");

    // ol 클래스명이 sense_single(뜻 1개)/senses_multiple(뜻 여러 개)로 갈리므로
    // 클래스명과 상관없이 entry 바로 아래 ol > li.sense만 잡는다(관용구 섹션의
    // ol은 .idioms 아래 더 깊이 있어서 여기 안 걸림).
    const senses = Array.from(entry.querySelectorAll(":scope > ol li.sense")).slice(0, 5);

    const senseItems = senses.map((sense) => {
      const def = esc(sense.querySelector(".def")?.textContent?.trim());

      const examples = Array.from(sense.querySelectorAll(":scope > ul.examples > li > .x"))
        .map((x) => `<div style="color:#888;font-size:12px;margin-top:2px;">– ${esc(x.textContent.trim())}</div>`)
        .join("");

      const xrefs = sense.querySelector(":scope > .xrefs");
      const xrefPrefix = esc(xrefs?.querySelector(".prefix")?.textContent?.trim());
      const xrefWords = xrefs ? Array.from(xrefs.querySelectorAll(".xh")).map((el) => esc(el.textContent.trim())) : [];
      const xrefHtml = xrefWords.length
        ? `<div style="color:#4a7dfc;font-size:12px;margin-top:6px;">${xrefPrefix} ${xrefWords.join(", ")}</div>`
        : "";

      // Extra Examples / Oxford Collocations Dictionary / Word Origin - 사이트에 있는
      // 순서 그대로, 있는 것만 붙인다(전부 선택 사항).
      const extraBox = sense.querySelector(':scope .unbox[unbox="extra_examples"]');
      const extraHtml = extraBox
        ? (() => {
            const title = esc(extraBox.querySelector(".box_title")?.textContent?.trim());
            const items = Array.from(extraBox.querySelectorAll(".examples .unx"))
              .map((el) => `<div style="color:#888;font-size:12px;margin-top:2px;">– ${esc(el.textContent.trim())}</div>`)
              .join("");
            return `<div style="margin-top:8px;"><div style="font-size:11px;font-weight:600;color:#555;">${title}</div>${items}</div>`;
          })()
        : "";

      const collocBox = sense.querySelector(':scope .unbox[unbox="snippet"]');
      const collocHtml = collocBox
        ? (() => {
            const title = esc(collocBox.querySelector(".box_title")?.textContent?.trim());
            const groups = Array.from(collocBox.querySelectorAll(".collocs_list")).map((ul) => {
              const groupLabel = esc(ul.previousElementSibling?.textContent?.trim());
              const words = Array.from(ul.querySelectorAll("li")).map((li) => esc(li.textContent.trim()));
              return `<div style="margin-top:2px;"><span style="color:#555;">${groupLabel}:</span> ${words.join(", ")}</div>`;
            }).join("");
            return `<div style="margin-top:8px;"><div style="font-size:11px;font-weight:600;color:#555;">${title}</div>${groups}</div>`;
          })()
        : "";

      const originBox = sense.querySelector(':scope .unbox[unbox="wordorigin"]');
      const originHtml = originBox
        ? (() => {
            const title = esc(originBox.querySelector(".box_title")?.textContent?.trim());
            const body = esc(originBox.querySelector(".body")?.textContent?.trim());
            return `<div style="margin-top:8px;"><div style="font-size:11px;font-weight:600;color:#555;">${title}</div><div style="color:#888;font-size:12px;margin-top:2px;">${body}</div></div>`;
          })()
        : "";

      return `<li style="margin-bottom:12px;">${def}${examples}${xrefHtml}${extraHtml}${collocHtml}${originHtml}</li>`;
    }).join("");

    return `
      <div style="margin-bottom:10px;">
        <div><span style="font-weight:700;font-size:16px;">${headword}</span><span style="color:#4a7dfc;font-style:italic;margin-left:6px;">${pos}</span></div>
        ${phonRows}
      </div>
      <ol style="margin:0 0 0 18px;padding:0;">${senseItems}</ol>
    `;
  }).join('<hr style="margin:10px 0;border-color:#eee;">');

  return blocks;
}

Cambridge Dictionary

EnglishAudioCEFR levelExamples

English dictionary with UK and US pronunciation audio, a CEFR level (A1–C2) on each meaning and example sentences - handy for judging how common a word is.

Name캠브릿지
URL
https://dictionary.cambridge.org/dictionary/english/{query}
Open asCard popup
Browser renderingOff
Book languagesEnglish (en)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  // 존재하지 않는 단어(또는 철자가 틀린 단어)는 캠브리지 사전이 항목 페이지 대신
  // 검색 홈(/dictionary/english/)으로 리다이렉트한다. 그 경우 entry-body 자체가
  // 없으므로 이걸로 "찾지 못함"을 판단한다.
  const entries = doc.querySelectorAll(".entry-body__el");
  if (entries.length === 0) {
    return `<div style="color:#888;">"${esc(word)}"에 대한 항목을 찾지 못했습니다.</div>`;
  }

  const blocks = Array.from(entries).slice(0, 2).map((entry) => {
    const headword = esc(entry.querySelector(".di-title .headword")?.textContent?.trim()) || esc(word);

    const posList = Array.from(entry.querySelectorAll(".posgram .pos"))
      .map((el) => esc(el.textContent.trim()))
      .join(", ");

    // 발음은 영국(.uk)/미국(.us) 두 블록으로 나뉘고, 각 블록 안에 IPA 텍스트와
    // mp3 오디오가 같이 들어있다.
    const pronBlocks = Array.from(entry.querySelectorAll(":scope > .pos-header > .uk, :scope > .pos-header > .us"))
      .map((regionEl) => {
        const label = regionEl.classList.contains("uk") ? "영국" : "미국";
        const ipa = esc(regionEl.querySelector(".ipa")?.textContent?.trim());
        const src = regionEl.querySelector("audio source[type='audio/mpeg']")?.getAttribute("src");
        const audioUrl = src ? (src.startsWith("http") ? src : `https://dictionary.cambridge.org${src}`) : "";
        const audio = audioUrl
          ? `<audio controls preload="none" style="height:26px;width:170px;"><source src="${audioUrl}"></audio>`
          : "";
        return `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;margin-top:4px;"><span style="color:#555;">${label}</span><span style="color:#888;">/${ipa}/</span>${audio}</div>`;
      })
      .join("");

    // 뜻(sense)마다 CEFR 레벨 배지(A1~C2)가 붙어있는 경우가 많아 학습자에게 유용하므로
    // 같이 보여준다. 없는 단어도 있어 optional로 처리.
    const senses = Array.from(entry.querySelectorAll(".pos-body .def-block")).slice(0, 5);
    const senseItems = senses.map((sense) => {
      const level = esc(sense.querySelector(".def-info .epp-xref")?.textContent?.trim());
      const def = esc(sense.querySelector(".def.ddef_d")?.textContent?.trim());
      const examples = Array.from(sense.parentElement?.querySelectorAll(":scope > .def-body .examp .eg") ?? [])
        .slice(0, 3)
        .map((eg) => `<div style="color:#888;font-size:12px;margin-top:2px;">– ${esc(eg.textContent.trim())}</div>`)
        .join("");
      const levelBadge = level
        ? `<span style="background:#eef2ff;color:#4a7dfc;font-size:11px;padding:1px 5px;border-radius:3px;margin-right:6px;">${level}</span>`
        : "";
      return `<li style="margin-bottom:12px;">${levelBadge}${def}${examples}</li>`;
    }).join("");

    return `
      <div style="margin-bottom:10px;">
        <div><span style="font-weight:700;font-size:16px;">${headword}</span><span style="color:#4a7dfc;font-style:italic;margin-left:6px;">${posList}</span></div>
        ${pronBlocks}
      </div>
      <ol style="margin:0 0 0 18px;padding:0;">${senseItems}</ol>
    `;
  }).join('<hr style="margin:10px 0;border-color:#eee;">');

  return blocks;
}

Longman Dictionary of Contemporary English

EnglishAudioGrammar patternsCollocations

English dictionary with UK and US audio, simple definitions, examples, grammar patterns (e.g. student at) and collocations. Suggestions for inflected forms are clickable.

Name롱맨
URL
https://www.ldoceonline.com/dictionary/{query}
Open asCard popup
Browser renderingOff
Book languagesEnglish (en)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  // 존재하지 않는 단어(활용형 등)는 항목 페이지 대신 철자 확인(spellcheck)
  // 페이지로 리다이렉트되고, 그 페이지엔 .dictentry가 없다. 대신 "Did you mean"
  // 추천 목록(.didyoumean)이 있을 때가 많아서, 있으면 같이 보여준다(예:
  // "succeeded" 검색 시 succeed/succeeds/succeeding 등을 추천).
  const entries = doc.querySelectorAll(".dictentry .ldoceEntry.Entry");
  if (entries.length === 0) {
    // 추천 단어는 클릭하면 그 단어로 바로 다시 검색되도록
    // <a class="dict-suggest-word" data-word="...">로 만든다(카드 팝업 공통
    // 코드 dictionary-card.ts가 이 클래스를 보고 클릭을 처리한다).
    const suggestions = Array.from(doc.querySelectorAll(".didyoumean a"))
      .slice(0, 8)
      .map((a) => esc(a.textContent.trim()))
      .filter(Boolean)
      .map((w) => `<a href="#" class="dict-suggest-word" data-word="${w}">${w}</a>`);
    return `<div style="color:#888;">"${esc(word)}"에 대한 항목을 찾지 못했습니다.${
      suggestions.length ? `<br>혹시 이 단어를 찾으셨나요: ${suggestions.join(", ")}` : ""
    }</div>`;
  }

  const blocks = Array.from(entries).slice(0, 2).map((entry) => {
    const headEl = entry.querySelector(":scope > .Head");
    const headword = esc(headEl?.querySelector(".HWD")?.textContent?.trim()) || esc(word);

    // .POS가 여러 개일 수 있다(예: "interjection" 하나, ", noun" 하나) - 앞의
    // 쉼표만 떼고 이어붙인다.
    const posList = Array.from(headEl?.querySelectorAll(":scope > .POS") ?? [])
      .map((el) => esc(el.textContent.trim().replace(/^,\s*/, "")))
      .filter(Boolean)
      .join(", ");

    const pronEl = headEl?.querySelector(":scope > .PronCodes");
    const pron = pronEl ? esc(pronEl.textContent.replace(/\s+/g, " ").trim()) : "";

    // 발음은 <audio> 태그가 아니라 data-src-mp3 속성이 붙은 아이콘(span)이라,
    // 우리가 직접 <audio> 태그로 만들어준다(brefile=영국, amefile=미국).
    const audioBlocks = Array.from(headEl?.querySelectorAll(":scope > span[data-src-mp3]") ?? [])
      .map((el) => {
        const label = el.classList.contains("brefile") ? "영국" : el.classList.contains("amefile") ? "미국" : "";
        const src = el.getAttribute("data-src-mp3");
        if (!src) return "";
        return `<span style="display:inline-flex;align-items:center;gap:4px;margin-right:10px;"><span style="color:#555;font-size:12px;">${label}</span><audio controls preload="none" style="height:30px;width:240px;"><source src="${src}"></audio></span>`;
      })
      .join("");

    const senses = Array.from(entry.querySelectorAll(":scope > .Sense")).slice(0, 5);
    const senseItems = senses.map((sense) => {
      const def = esc(sense.querySelector(":scope > .DEF")?.textContent?.trim());
      if (!def) return "";
      // EXAMPLE 안에도 재생 아이콘(data-src-mp3 span)이 같이 들어있어서, 복제한
      // 뒤 그것만 지우고 남은 텍스트를 예문으로 쓴다.
      function renderExample(exampleEl) {
        if (!exampleEl) return "";
        const clone = exampleEl.cloneNode(true);
        clone.querySelectorAll("span[data-src-mp3]").forEach((s) => s.remove());
        const text = clone.textContent.trim();
        return text ? `<div style="color:#888;font-size:12px;margin-top:2px;">– ${esc(text)}</div>` : "";
      }

      const examples = Array.from(sense.querySelectorAll(":scope > .EXAMPLE")).slice(0, 3).map(renderExample).join("");

      // .GramExa: 전치사/문형 패턴 + 예문(예: "student at" - "a first-year student
      // at the University of Oslo"). .ColloExa: 연어 + 뜻풀이/예문(예: "law/science/
      // medical etc student"). 둘 다 Sense 바로 아래 붙어있고, 실제 사이트에서
      // 정의 밑에 나오는 핵심 내용이라 빠뜨리면 내용이 너무 빈약해진다. 페이지
      // 맨 아래 별도 COLLOCATIONS 박스(.Tail .ColloBox)는 카드에 넣기엔 너무 길어
      // 의도적으로 뺐다 - 필요하면 "새 탭에서 원본 보기"로 확인.
      const gramExas = Array.from(sense.querySelectorAll(":scope > .GramExa")).map((g) => {
        const pattern = esc(g.querySelector(".PROPFORMPREP")?.textContent?.trim());
        if (!pattern) return "";
        return `<div style="margin-top:6px;"><span style="font-weight:600;">${pattern}</span>${renderExample(g.querySelector(".EXAMPLE"))}</div>`;
      }).join("");

      const colloExas = Array.from(sense.querySelectorAll(":scope > .ColloExa")).map((c) => {
        const phrase = esc(c.querySelector(".COLLO")?.textContent?.trim());
        if (!phrase) return "";
        const gloss = c.querySelector(".GLOSS")?.textContent?.trim();
        const glossHtml = gloss ? ` <span style="color:#888;font-size:12px;">${esc(gloss)}</span>` : "";
        return `<div style="margin-top:6px;"><span style="font-weight:600;">${phrase}</span>${glossHtml}${renderExample(c.querySelector(".EXAMPLE"))}</div>`;
      }).join("");

      return `<li style="margin-bottom:12px;">${def}${examples}${gramExas}${colloExas}</li>`;
    }).join("");

    return `
      <div style="margin-bottom:8px;">
        <div><span style="font-weight:700;font-size:16px;">${headword}</span><span style="color:#4a7dfc;font-style:italic;margin-left:6px;">${posList}</span></div>
        <div style="color:#888;font-size:13px;margin-top:2px;">${pron}</div>
        <div style="margin-top:4px;">${audioBlocks}</div>
      </div>
      <ol style="margin:0 0 0 18px;padding:0;">${senseItems}</ol>
    `;
  }).join('<hr style="margin:10px 0;border-color:#eee;">');

  return blocks;
}

Naver English–Korean Dictionary

English → KoreanExamplesAudio

English to Korean. Meanings grouped by part of speech, example sentences with Korean translations, and pronunciation audio when the site provides a recording.

Name네이버
URL
https://en.dict.naver.com/#/search?query={query}
Open asCard popup
Browser renderingOn
Book languagesEnglish (en)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  // 네이버는 단어에 따라 뜻풀이 DOM 구조가 아예 다르다(실사용 확인: "apple"과
  // "regiment"가 다름). "학습사전"에 있는 단어는 품사별로 .mean_tray로 묶여서
  // 나오고(그 안에 .mean_desc .cont .mean이 정의), 그 외 다수 단어는 .mean_tray
  // 없이 .mean_item이 바로 나열되며 품사+정의가 .mean 안에 한 번에 들어있다.
  // 두 구조 다 찾아서 하나의 목록으로 합친다.
  //
  // 난이도 탭(초급/중급/전체)마다 같은 뜻풀이가 .mean_tray로 중복 존재하고,
  // 지금 안 보이는 탭 것들은 style="display: none;"이 그대로 박혀있어서 그것만
  // 걸러내면 현재 화면에 보이는 것과 같은 내용만 남는다.
  const visibleTrays = Array.from(doc.querySelectorAll(".mean_tray")).filter(
    (t) => !/display:\s*none/i.test(t.getAttribute("style") || ""),
  );
  const entries = visibleTrays.length
    ? visibleTrays.flatMap((tray) => {
        const pos = tray.querySelector(".part_speech")?.textContent?.trim();
        return Array.from(tray.querySelectorAll(".mean_item")).map((item) => ({ item, pos }));
      })
    : // .mean_tray가 없는 단어는 뜻풀이(.mean_item, 번호 있음)와 "복합어·숙어"
      // 등 관련 구문(.mean_item이지만 번호 없음)이 같은 <ul>에 형제로 섞여 있다
      // (실사용 확인: "regiment"). 직계 자식으로 .num(번호) 있는 것만 진짜 뜻풀이.
      Array.from(doc.querySelectorAll(".mean_item"))
        .filter((item) => item.querySelector(":scope > .num"))
        .map((item) => ({ item, pos: null }));

  if (entries.length === 0) {
    return `<div style="color:#888;">"${esc(word)}"에 대한 항목을 찾지 못했습니다.</div>`;
  }

  const headword = esc(doc.querySelector(".origin .highlight")?.textContent?.trim()) || esc(word);

  // 발음은 미국식/영국식 둘 다 IPA 텍스트는 있지만, 재생 버튼은 실제 mp3
  // 파일(purl 속성)이 있는 것도 있고 자체 TTS(vcode/encode)로만 되는 것도 있다 -
  // TTS는 우리 쪽에서 재현할 방법이 없어 실제 파일(purl)이 있는 것만 <audio>로
  // 만들고, 없으면 텍스트만 보여준다.
  // .listen_list는 표제어 자신의 발음 말고도 "복합어·숙어"(parachute regiment
  // 등) 각각의 발음까지 페이지 안에 여러 개 더 있다(실사용 확인: "regiment"에서
  // 5개나 나옴). 문서에서 제일 먼저 나오는 것(표제어 자신)만 쓴다.
  const pronBlocks = Array.from(doc.querySelector(".listen_list")?.querySelectorAll(".listen_item") ?? [])
    .map((li) => {
      const label = esc(li.childNodes[0]?.textContent?.trim());
      const ipa = esc(li.querySelector(".listen_pronunce_mark")?.textContent?.trim());
      const purl = li.querySelector(".btn_listen.play")?.getAttribute("purl");
      if (!label && !ipa) return "";
      const audio = purl
        ? `<audio controls preload="none" style="height:30px;width:200px;"><source src="${purl}"></audio>`
        : "";
      return `<div style="display:flex;align-items:center;gap:6px;margin-top:4px;"><span style="color:#555;">${label}</span><span style="color:#888;">${ipa}</span>${audio}</div>`;
    })
    .join("");

  const senseItems = entries
    .slice(0, 8)
    .map(({ item, pos }) => {
      let posText = esc(pos);
      let defText;
      const descMean = item.querySelector(".mean_desc .cont .mean");
      if (descMean) {
        defText = esc(descMean.textContent.replace(/\s+/g, " ").trim());
      } else {
        const meanEl = item.querySelector(":scope > .mean");
        if (!meanEl) return "";
        if (!posText) posText = esc(meanEl.querySelector(".word_class")?.textContent?.trim());
        const clone = meanEl.cloneNode(true);
        clone.querySelectorAll(".word_class").forEach((el) => el.remove());
        defText = esc(clone.textContent.replace(/\s+/g, " ").trim());
      }
      if (!defText) return "";

      const example = item.querySelector(".example_item");
      const origin = esc(example?.querySelector(".origin .text")?.textContent?.trim());
      const translate = esc(example?.querySelector(".translate .text")?.textContent?.trim());
      const exampleHtml = origin
        ? `<div style="color:#888;font-size:12px;margin-top:2px;">– ${origin}${translate ? ` (${translate})` : ""}</div>`
        : "";
      const posPrefix = posText
        ? `<span style="color:#4a7dfc;font-weight:600;margin-right:4px;">${posText}</span>`
        : "";
      return `<li style="margin-bottom:10px;">${posPrefix}${defText}${exampleHtml}</li>`;
    })
    .join("");

  return `
    <div style="font-weight:700;font-size:16px;">${headword}</div>
    ${pronBlocks}
    <ol style="margin:8px 0 0 18px;padding:0;">${senseItems}</ol>
  `;
}

Daum English–Korean Dictionary

English → KoreanAudioExamples

English to Korean. US and UK pronunciation audio, meanings by part of speech and example sentences in English and Korean.

Name다음
URL
https://dic.daum.net/search.do?q={query}&dic=eng
Open asCard popup
Browser renderingOn
Book languagesEnglish (en)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  const headwordEl = doc.querySelector(".txt_cleanword");
  if (!headwordEl) {
    return `<div style="color:#888;">"${esc(word)}"에 대한 항목을 찾지 못했습니다.</div>`;
  }
  const headword = esc(headwordEl.textContent.trim()) || esc(word);

  // 발음 - 미국식/영국식 각각 IPA + 실제 mp3(data-url)가 있다.
  const pronBlocks = Array.from(doc.querySelectorAll(".word_nation"))
    .map((nation) => {
      const label = esc(nation.querySelector(".txt_nation")?.childNodes?.[0]?.textContent?.trim());
      const ipa = esc(nation.querySelector(".txt_pronounce")?.textContent?.trim());
      const audioUrl = nation.querySelector(".btn_voice.btn_listen[data-url]")?.getAttribute("data-url");
      if (!label && !ipa) return "";
      const audio = audioUrl
        ? `<audio controls preload="none" style="height:30px;width:200px;"><source src="${audioUrl}"></audio>`
        : "";
      return `<div style="display:flex;align-items:center;gap:6px;margin-top:4px;"><span style="color:#555;">${label}</span><span style="color:#888;">${ipa}</span>${audio}</div>`;
    })
    .join("");

  // .fold_ex 하나가 품사 하나(감탄사/명사/동사 등)에 대응한다. 그 안의 .wrap_ex가
  // 뜻풀이인데, 번호(.num_item)와 뜻(.desc_item)이 항상 같은 줄에 안 붙어있는
  // 렌더링 특성이 있어서 번호는 무시하고 뜻 텍스트가 있는 것만 순서대로 모아
  // <ol>로 자동 번호를 매긴다. 예문의 재생 버튼(TTS, 별도 인증 필요)은 지우고
  // 텍스트만(영어 - 한국어 순) 붙인다. "더보기"로 접혀있는 예문(style="display:
  // none")은 뺀다.
  const groups = Array.from(doc.querySelectorAll(".fold_ex"))
    .slice(0, 4)
    .map((group) => {
      const pos = esc(group.querySelector(".tit_ex")?.textContent?.trim());
      const defs = Array.from(group.querySelectorAll(":scope > .wrap_ex"))
        .map((w) => esc(w.querySelector(".desc_item")?.textContent?.trim()))
        .filter(Boolean)
        .slice(0, 5);
      if (defs.length === 0) return "";

      const examples = Array.from(group.querySelectorAll(":scope > .item_example > li"))
        .filter((li) => !/display:\s*none/i.test(li.getAttribute("style") || ""))
        .slice(0, 2)
        .map((li) => {
          const parts = Array.from(li.querySelectorAll(".desc_ex"))
            .map((d) => {
              const clone = d.cloneNode(true);
              clone.querySelectorAll("a.btn_voice").forEach((a) => a.remove());
              return esc(clone.textContent.trim());
            })
            .filter(Boolean);
          return parts.length
            ? `<div style="color:#888;font-size:12px;margin-top:2px;">– ${parts.join(" · ")}</div>`
            : "";
        })
        .join("");

      const defItems = defs.map((def) => `<li style="margin-bottom:8px;">${def}</li>`).join("");
      return `<div style="margin-top:8px;"><span style="color:#4a7dfc;font-weight:600;">${pos}</span><ol style="margin:4px 0 0 18px;padding:0;">${defItems}</ol>${examples}</div>`;
    })
    .join("");

  return `
    <div style="font-weight:700;font-size:16px;">${headword}</div>
    ${pronBlocks}
    ${groups}
  `;
}

Daum Japanese–Korean Dictionary

Japanese → KoreanReadingsAudio

Japanese to Korean. The exact match first, then related words - each with its reading, kanji spelling, audio and short meanings.

Name다음-일본어
URL
https://dic.daum.net/search.do?q={query}&dic=jp
Open asCard popup
Browser renderingOn
Book languagesJapanese (ja)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  // 검색어와 정확히 일치하는 표제어(.cleanword_type)와, 그 외 후보 단어들
  // (.search_type)이 순서대로 나온다. 둘 다 같은 모양(표제어 + .list_search
  // 뜻풀이 목록)이라 같이 모아서 처리한다.
  const groups = Array.from(doc.querySelectorAll(".cleanword_type, .search_type"));
  if (groups.length === 0) {
    return `<div style="color:#888;">"${esc(word)}"에 대한 항목을 찾지 못했습니다.</div>`;
  }

  const blocks = groups
    .slice(0, 5)
    .map((group) => {
      // 표제어는 .tit_cleansch/.tit_searchword(<strong>) 전체가 아니라 그 안의
      // 링크(.txt_cleansch/.txt_searchword)만 잡아야 한다 - <strong> 전체를
      // 잡으면 같이 들어있는 발음 듣기 버튼의 "듣기" 글자까지 텍스트로 딸려
      // 나온다(실사용 확인: "きょう 興 듣기"처럼 표시됨).
      const headword = esc(
        group.querySelector(".txt_cleansch, .txt_searchword")?.textContent?.replace(/\s+/g, " ").trim(),
      );
      // 한자 표기가 따로 있으면(가나 표제어 밑에 작게) 같이 보여준다.
      const kanji = esc(group.querySelector(".sub_txt")?.textContent?.trim());
      // 발음 - .desc_listen 안의 링크에 실제 mp3 URL이 data-url 속성으로 있다.
      const audioUrl = group.querySelector(".desc_listen a[data-url]")?.getAttribute("data-url");
      const audio = audioUrl
        ? `<audio controls preload="none" style="height:26px;width:180px;margin-left:8px;"><source src="${audioUrl}"></audio>`
        : "";
      const meanings = Array.from(group.querySelectorAll(".list_search .txt_search"))
        .slice(0, 4)
        .map((el) => esc(el.textContent.replace(/\s+/g, " ").trim()))
        .filter(Boolean);
      if (!headword || meanings.length === 0) return "";
      const meaningHtml = meanings.map((m) => `<li style="margin-bottom:4px;">${m}</li>`).join("");
      return `
        <div style="margin-top:8px;display:flex;align-items:center;flex-wrap:wrap;">
          <span style="font-weight:700;font-size:16px;">${headword}</span>
          ${kanji ? `<span style="color:#888;font-size:12px;margin-left:6px;">${kanji}</span>` : ""}
          ${audio}
        </div>
        <ol style="margin:4px 0 0 18px;padding:0;">${meaningHtml}</ol>
      `;
    })
    .join('<hr style="margin:10px 0;border-color:#eee;">');

  return blocks;
}

Naver Japanese–Korean Dictionary

Japanese → KoreanReadingsAudioExamples

Japanese to Korean. Kanji with several readings are shown reading by reading, each with its own pronunciation, meanings and example sentences with translations.

Name네이버-일본어
URL
https://ja.dict.naver.com/#/search?query={query}
Open asCard popup
Browser renderingOn
Book languagesJapanese (ja)
Parse script
function extract(html, word) {
  const doc = new DOMParser().parseFromString(html, "text/html");

  function esc(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  function extractDef(item) {
    const descMean = item.querySelector(".mean_desc .cont .mean");
    if (descMean) return esc(descMean.textContent.replace(/\s+/g, " ").trim());
    const meanEl = item.querySelector(":scope > .mean");
    if (!meanEl) return "";
    const clone = meanEl.cloneNode(true);
    clone.querySelectorAll(".word_class").forEach((el) => el.remove());
    return esc(clone.textContent.replace(/\s+/g, " ").trim());
  }

  function extractExample(item) {
    const example = item.querySelector(".example_item");
    const src = esc(example?.querySelector(".origin .text")?.textContent?.trim());
    const translate = esc(example?.querySelector(".translate .text")?.textContent?.trim());
    return src
      ? `<div style="color:#888;font-size:12px;margin-top:2px;">– ${src}${translate ? ` (${translate})` : ""}</div>`
      : "";
  }

  function renderSenses(items) {
    return items
      .slice(0, 8)
      .map((item) => {
        const def = extractDef(item);
        return def ? `<li style="margin-bottom:8px;">${def}${extractExample(item)}</li>` : "";
      })
      .join("");
  }

  // 네이버는 단어에 따라 뜻풀이 DOM 구조가 아예 다르다(영어판 dic/네이버.md와
  // 동일한 이유/로직 - 실사용 확인: "こんにちは"와 "界"가 다름). "학습사전"에
  // 있는 단어는 품사별로 .mean_tray로 묶여서 나온다 - 이 경우는 표제어 하나에
  // 뜻풀이 목록 하나뿐이라 기존처럼 처리한다.
  const firstComponent = doc.querySelector(".component_keyword");
  const visibleTrays = firstComponent
    ? Array.from(firstComponent.querySelectorAll(".mean_tray")).filter(
        (t) => !/display:\s*none/i.test(t.getAttribute("style") || ""),
      )
    : [];

  // 발음은 실제 mp3 파일(purl 속성)이 있는 것도 있고 자체 TTS로만 되는 것도
  // 있다 - TTS는 재현할 방법이 없어 실제 파일이 있는 것만 <audio>로 만든다.
  // 표제어(또는 읽기)마다 발음이 다르므로 항상 해당 .origin 범위 안에서만
  // 찾는다 - 문서 전체에서 처음 찾은 것만 쓰면 다른 읽기/구문의 발음이 섞여
  // 나온다. 구문 항목은 .listen_item은 있어도 실제 IPA/음성 없이 빈 라벨만
  // 있는 경우가 많아 그런 건 걸러낸다.
  // purl 속성은 항상 mp3 하나짜리 URL이 아니라 "여성음성URL|남성음성URL"처럼
  // 파이프(|)로 여러 개가 합쳐져 오는 경우가 있다(실사용 확인: 일부 일본어
  // 단어) - 그대로 <source src="...">에 넣으면 통짜로 잘못된 URL이 되어
  // 재생이 안 된다. 첫 번째 것만 잘라 쓴다.
  function firstAudioUrl(purl) {
    return purl ? purl.split("|")[0] : null;
  }

  function renderPronFor(originEl) {
    if (!originEl) return "";
    const listenItems = originEl.querySelectorAll(".listen_list .listen_item");
    if (listenItems.length) {
      return Array.from(listenItems)
        .map((li) => {
          const label = esc(li.childNodes[0]?.textContent?.trim());
          const ipa = esc(li.querySelector(".listen_pronunce_mark")?.textContent?.trim());
          const purl = firstAudioUrl(li.querySelector(".btn_listen.play")?.getAttribute("purl"));
          if (!ipa && !purl) return "";
          const audio = purl
            ? `<audio controls preload="none" style="height:30px;width:200px;"><source src="${purl}"></audio>`
            : "";
          return `<div style="display:flex;align-items:center;gap:6px;margin-top:4px;"><span style="color:#555;">${label}</span><span style="color:#888;">${ipa}</span>${audio}</div>`;
        })
        .join("");
    }
    // 일본어판은 .listen_list 래퍼 없이 .origin 바로 아래 .unit_listen
    // 버튼만 있다(실사용 확인: "水" → みず/すい/수/みずとあぶら 각 읽기가
    // 전부 다른 발음/무발음).
    const purl = firstAudioUrl(originEl.querySelector(".unit_listen .btn_listen.play")?.getAttribute("purl"));
    return purl
      ? `<div style="margin-top:4px;"><audio controls preload="none" style="height:30px;width:200px;"><source src="${purl}"></audio></div>`
      : "";
  }

  if (visibleTrays.length) {
    const headword = esc(doc.querySelector(".origin .highlight")?.textContent?.trim()) || esc(word);
    const body = visibleTrays
      .slice(0, 4)
      .map((tray) => {
        const pos = esc(tray.querySelector(".part_speech")?.textContent?.trim());
        const senseHtml = renderSenses(Array.from(tray.querySelectorAll(".mean_item")));
        if (!senseHtml) return "";
        return `<div style="margin-top:8px;"><span style="color:#4a7dfc;font-weight:600;">${pos}</span><ol style="margin:4px 0 0 18px;padding:0;">${senseHtml}</ol></div>`;
      })
      .join("");
    return `<div style="font-weight:700;font-size:16px;">${headword}</div>${renderPronFor(firstComponent?.querySelector(".origin"))}${body}`;
  }

  // 한자에 읽는 법이 여러 개면(예: "界" → さかい/-かい/-계/くがい) .mean_tray
  // 없이 .row 하나가 "읽기 하나"에 대응한다(.row > .origin(읽기+한자 주석) +
  // .word_class(품사) + .mean_list(뜻풀이)). 이걸 구분 안 하고 페이지 전체의
  // .mean_item을 한 목록으로 합치면 서로 다른 읽기의 뜻풀이가 번호만 이어진
  // 채 뒤섞여 나온다(실사용 확인: "界" 검색 시 さかい/-かい/-계 뜻이 한
  // 목록으로 합쳐짐). 검색어와 같은 한자를 쓰는 첫 번째 항목
  // (.component_keyword) 안의 .row들만 각각 별도 블록으로 렌더링한다.
  const rows = firstComponent
    ? Array.from(firstComponent.querySelectorAll(".row")).filter((row) => row.querySelector(":scope > .mean_list"))
    : [];

  if (rows.length === 0) {
    return `<div style="color:#888;">"${esc(word)}"에 대한 항목을 찾지 못했습니다.</div>`;
  }

  const blocks = rows
    .slice(0, 6)
    .map((row) => {
      const originEl = row.querySelector(":scope > .origin");
      const reading = esc(originEl?.querySelector("a")?.textContent?.trim()) || esc(word);
      const kanji = esc(originEl?.querySelector(".text")?.textContent?.trim());
      const pos = esc(row.querySelector(":scope > .word_class")?.textContent?.trim());
      // 뜻이 하나뿐인 읽기는 번호(.num) 자체가 안 붙는다(실사용 확인: "界"의
      // "-かい"/"-계") - row 범위로 이미 좁혔으니 번호 유무로 다시 거를
      // 필요가 없다.
      const items = Array.from(row.querySelectorAll(":scope > .mean_list .mean_item"));
      const senseHtml = renderSenses(items);
      if (!senseHtml) return "";
      return `
        <div style="margin-top:8px;">
          <span style="font-weight:700;font-size:16px;">${reading}</span>
          ${kanji ? `<span style="color:#888;font-size:12px;margin-left:6px;">${kanji}</span>` : ""}
          ${pos ? `<span style="color:#4a7dfc;margin-left:6px;">${pos}</span>` : ""}
        </div>
        ${renderPronFor(originEl)}
        <ol style="margin:4px 0 0 18px;padding:0;">${senseHtml}</ol>
      `;
    })
    .join('<hr style="margin:10px 0;border-color:#eee;">');

  return blocks;
}