// ==UserScript== // @name Wiki Pali DPD // @namespace https://github.com/metanoia1989/wiki-pali-dpd // @version 0.1.0 // @description DPD 词典数据:变格表、复合词拆解、释义注入 // @author Adam Smith // @match https://*.wikipali.cc/* // @match https://*.wikipali.org/* // @icon https://pali-declension.mysticalpower.uk/favicon.svg // @require https://cdn.jsdelivr.net/npm/sql.js@1.11.0/dist/sql-wasm.js // @require https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @license MIT // ==/UserScript== var WikiPaliDPD = (() => { var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/storage/cache.js var cache_exports = {}; __export(cache_exports, { Cache: () => Cache }); var Cache; var init_cache = __esm({ "src/storage/cache.js"() { Cache = class { constructor(dbName = "WikiPaliDPD", storeName = "dbCache") { this.dbName = dbName; this.storeName = storeName; this._db = null; } async _open() { if (this._db) return this._db; return new Promise((resolve, reject) => { const req = indexedDB.open(this.dbName, 1); req.onupgradeneeded = () => { req.result.createObjectStore(this.storeName); }; req.onsuccess = () => { this._db = req.result; resolve(this._db); }; req.onerror = () => reject(req.error); }); } async get(key) { const db = await this._open(); return new Promise((resolve) => { const tx = db.transaction(this.storeName, "readonly"); const req = tx.objectStore(this.storeName).get(key); req.onsuccess = () => resolve(req.result || null); req.onerror = () => resolve(null); }); } async set(key, value) { const db = await this._open(); return new Promise((resolve, reject) => { const tx = db.transaction(this.storeName, "readwrite"); tx.objectStore(this.storeName).put(value, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async delete(key) { const db = await this._open(); return new Promise((resolve, reject) => { const tx = db.transaction(this.storeName, "readwrite"); tx.objectStore(this.storeName).delete(key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } }; } }); // src/db/loader.js var loader_exports = {}; __export(loader_exports, { Loader: () => Loader }); var Loader; var init_loader = __esm({ "src/db/loader.js"() { Loader = class { constructor(url) { this.url = url; } async load(onProgress) { const resp = await fetch(this.url); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const total = Number(resp.headers.get("Content-Length")) || 0; const reader = resp.body.getReader(); const chunks = []; let received = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); received += value.length; if (total && onProgress) { onProgress(Math.round(received / total * 100)); } } const compressed = new Uint8Array(received); let offset = 0; for (const chunk of chunks) { compressed.set(chunk, offset); offset += chunk.length; } const decompressed = pako.ungzip(compressed); return decompressed.buffer; } }; } }); // src/db/query.js var query_exports = {}; __export(query_exports, { Query: () => Query }); var Query; var init_query = __esm({ "src/db/query.js"() { Query = class { constructor(dbBuffer, SQL) { this.ready = this._init(dbBuffer, SQL); } async _init(dbBuffer, SQL) { this.db = new SQL.Database(new Uint8Array(dbBuffer)); this._stmtCache = {}; } /** * Look up a word in the lookup table. * @param {string} word - The word to query. * @returns {object|null} Row with {lookup_key, headwords, deconstructor, grammar, spelling, see} */ lookupWord(word) { if (!this.db) return null; const stmt = this._prepare( "lookupWord", `SELECT lookup_key, headwords, deconstructor, grammar, spelling, see FROM lookup WHERE lookup_key = ?` ); stmt.bind([word]); if (stmt.step()) { const row = stmt.getAsObject(); stmt.reset(); return row; } stmt.reset(); return null; } /** * Get headword info by id. * @param {number} id - headword ID. * @returns {object|null} */ getHeadword(id) { if (!this.db) return null; const stmt = this._prepare( "getHeadword", `SELECT id, lemma_1, pos, stem, pattern, meaning_1, meaning_lit, inflections FROM headwords WHERE id = ?` ); stmt.bind([id]); if (stmt.step()) { const row = stmt.getAsObject(); stmt.reset(); return row; } stmt.reset(); return null; } /** * Get inflection template by pattern name. * @param {string} pattern - Template name, e.g. "masc__a". * @returns {object|null} {pattern, like, data} */ getTemplate(pattern) { if (!this.db) return null; const stmt = this._prepare( "getTemplate", `SELECT pattern, like, data FROM inflection_templates WHERE pattern = ?` ); stmt.bind([pattern]); if (stmt.step()) { const row = stmt.getAsObject(); stmt.reset(); return row; } stmt.reset(); return null; } /** * Get all templates as a Map for fast lookup. * @returns {Map} */ getAllTemplates() { if (!this.db) return /* @__PURE__ */ new Map(); const stmt = this._prepare( "getAllTemplates", `SELECT pattern, like, data FROM inflection_templates` ); const map = /* @__PURE__ */ new Map(); while (stmt.step()) { const row = stmt.getAsObject(); map.set(row.pattern, row); } stmt.reset(); return map; } /** * Look up root info. * @param {string} root * @returns {object|null} */ getRoot(root) { if (!this.db) return null; const stmt = this._prepare( "getRoot", `SELECT root, root_meaning FROM roots WHERE root = ?` ); stmt.bind([root]); if (stmt.step()) { const row = stmt.getAsObject(); stmt.reset(); return row; } stmt.reset(); return null; } _prepare(key, sql) { if (!this._stmtCache[key]) { this._stmtCache[key] = this.db.prepare(sql); } return this._stmtCache[key]; } }; } }); // src/ui/injector.js var injector_exports = {}; __export(injector_exports, { Injector: () => Injector }); var Injector; var init_injector = __esm({ "src/ui/injector.js"() { Injector = class { constructor(query, Panel2, history) { this.query = query; this.Panel = Panel2; this.history = history; this._observer = null; this._containerObserver = null; this._lastWord = ""; this._panelInstance = null; this._resultContainer = null; } start() { this._observer = new MutationObserver(() => this._findContainer()); this._observer.observe(document.body, { childList: true, subtree: true }); this._findContainer(); } stop() { if (this._observer) this._observer.disconnect(); if (this._containerObserver) this._containerObserver.disconnect(); this._removePanel(); } _findContainer() { const el = document.querySelector("div#rc-tabs-0-panel-result"); if (el && el !== this._resultContainer) { this._resultContainer = el; if (this._containerObserver) this._containerObserver.disconnect(); this._containerObserver = new MutationObserver( () => this._onResultChange() ); this._containerObserver.observe(el, { childList: true, subtree: true }); this._onResultChange(); } } _onResultChange() { if (!this._resultContainer) return; if (!this._resultContainer.children.length) return; const input = document.querySelector("input#rc_select_0"); if (!input) return; const word = input.value.trim().toLowerCase(); if (!word || word === this._lastWord) return; this._lastWord = word; requestAnimationFrame(() => this._lookup(word)); } _removePanel() { if (this._panelInstance) { this._panelInstance.remove(); this._panelInstance = null; } } async _lookup(word) { this._removePanel(); const lookupRow = this.query.lookupWord(word); if (!lookupRow || !lookupRow.headwords) return; let headwordIds; try { headwordIds = JSON.parse(lookupRow.headwords); } catch { return; } if (!headwordIds || headwordIds.length === 0) return; const headword = this.query.getHeadword(headwordIds[0]); if (!headword) return; let deconstruction = null; if (lookupRow.deconstructor) { try { deconstruction = JSON.parse(lookupRow.deconstructor); } catch { } } const panel = new this.Panel( word, headword, lookupRow, deconstruction, this.query ); if (this._resultContainer) { panel.injectBefore(this._resultContainer); this._panelInstance = panel; } this.history.add({ word, headword: headword.lemma_1, timestamp: Date.now() }); } }; } }); // src/inflection/renderer.js var Renderer; var init_renderer = __esm({ "src/inflection/renderer.js"() { Renderer = class { /** * @param {Map} templates - Map of pattern -> {pattern, like, data} */ constructor(templates) { this.templates = templates; } /** * Render inflection table HTML for a given stem and pattern. * @param {string} stem - The word stem (e.g. "loka"). * @param {string} pattern - Template name (e.g. "masc__a"). * @returns {string} HTML table string, or empty string if template not found. */ render(stem, pattern) { const tmpl = this._resolvePattern(pattern); if (!tmpl) return ""; const tableData = JSON.parse(tmpl.data); let html = ''; for (let rowIdx = 0; rowIdx < tableData.length; rowIdx++) { const row = tableData[rowIdx]; html += ""; for (let colIdx = 0; colIdx < row.length; colIdx++) { const cell = row[colIdx]; if (rowIdx === 0) { if (colIdx === 0) { html += ''; } else if (colIdx % 2 === 1) { html += ``; } } else if (colIdx === 0) { html += ``; } else if (colIdx % 2 === 1) { const forms = cell.map((suffix) => { const cleaned = this._cleanStem(stem); return cleaned + suffix; }); html += ``; } } html += ""; } html += "
${this._escape(cell[0] || "")}${this._escape(cell[0] || "")}${forms.map((f) => this._escape(f)).join("
")}
"; return html; } /** * Resolve pattern, following the 'like' chain. */ _resolvePattern(pattern) { let tmpl = this.templates.get(pattern); if (!tmpl) return null; let depth = 0; while (tmpl.like && !tmpl.like.startsWith("irreg") && depth < 5) { const next = this.templates.get(tmpl.like); if (!next || next === tmpl) break; if (next.data) return next; tmpl = next; depth++; } return tmpl; } _cleanStem(stem) { return stem.replace(/[!*]/g, ""); } _escape(str) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; return String(str).replace(/[&<>"']/g, (ch) => map[ch]); } }; } }); // src/ui/panel.js var panel_exports = {}; __export(panel_exports, { Panel: () => Panel }); var Panel; var init_panel = __esm({ "src/ui/panel.js"() { init_renderer(); Panel = class { constructor(word, headword, lookupRow, deconstruction, query) { this.word = word; this.headword = headword; this.lookupRow = lookupRow; this.deconstruction = deconstruction; this.query = query; this._el = null; this._expanded = false; } remove() { if (this._el) { this._el.remove(); this._el = null; } } // ── 紧凑工具条 ────────────────────────────────────────────── _compactHTML() { const hw = this.headword; return `
${this._e(hw.lemma_1)} ${this._e(hw.pos || "")} ${this._e(hw.meaning_1 || "")} \u25B6 DPD
${this._styleHTML()}`; } // ── 展开内容 ───────────────────────────────────────────────── _expandHTML() { const hw = this.headword; const parts = []; const meaning = [hw.lemma_1, hw.pos]; if (hw.meaning_1) meaning.push("\u2014 " + hw.meaning_1); if (hw.meaning_lit) meaning.push("(lit. " + hw.meaning_lit + ")"); parts.push(`
${this._e(meaning.join(" "))}
`); if (hw.stem && hw.pattern) { const renderer = new Renderer(this.query.getAllTemplates()); const tableHtml = renderer.render(hw.stem, hw.pattern); if (tableHtml) { parts.push(`
\u53D8\u683C\u8868 ${tableHtml}
`); } } if (this.deconstruction && this.deconstruction.length > 0) { parts.push(`
\u590D\u5408\u8BCD\uFF1A${this.deconstruction.map((d) => `${this._e(d)}`).join(" + ")}
`); } if (this.lookupRow.grammar) { parts.push(`
${this._e(this.lookupRow.grammar)}
`); } return parts.join("\n"); } // ── 点击切换 ───────────────────────────────────────────────── _bindToggle() { const bar = this._el.querySelector(".dpd-bar"); const body = this._el.querySelector(".dpd-body"); const toggle = this._el.querySelector(".dpd-bar-toggle"); bar.addEventListener("click", () => { this._expanded = !this._expanded; if (this._expanded) { body.innerHTML = this._expandHTML(); body.style.display = "block"; toggle.textContent = "\u25BC DPD"; } else { body.style.display = "none"; toggle.textContent = "\u25B6 DPD"; } }); } injectBefore(referenceEl) { this._el = document.createElement("div"); this._el.className = "dpd-toolbar"; this._el.innerHTML = this._compactHTML(); referenceEl.parentNode.insertBefore(this._el, referenceEl); this._bindToggle(); } // ── 样式 ───────────────────────────────────────────────────── _styleHTML() { return ``; } _e(str) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; return String(str).replace(/[&<>"']/g, (ch) => map[ch]); } }; } }); // src/ui/settings.js var settings_exports = {}; __export(settings_exports, { Settings: () => Settings }); var Settings; var init_settings = __esm({ "src/ui/settings.js"() { Settings = class { static show(history) { const html = `

Wiki Pali DPD Settings

Version: ${self.__DPD_META__.version}

Cache: ${GM_getValue("dpd_db_version") ? "Loaded" : "Empty"}

`; const container = document.createElement("div"); container.innerHTML = html; document.body.appendChild(container); document.getElementById("dpd-settings-close").onclick = () => container.remove(); document.getElementById("dpd-clear-cache").onclick = async () => { const { Cache: Cache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports)); const cache = new Cache2(); await cache.delete("dpd_web_db"); GM_deleteValue("dpd_db_version"); alert("Cache cleared. Reload to re-download."); container.remove(); }; } }; } }); // src/storage/history.js var history_exports = {}; __export(history_exports, { History: () => History }); var History; var init_history = __esm({ "src/storage/history.js"() { History = class { constructor(maxEntries = 100) { this.maxEntries = maxEntries; this._key = "dpd_query_history"; } get() { try { return JSON.parse(localStorage.getItem(this._key) || "[]"); } catch { return []; } } add(entry) { const list = this.get(); list.unshift(entry); if (list.length > this.maxEntries) list.length = this.maxEntries; localStorage.setItem(this._key, JSON.stringify(list)); } clear() { localStorage.removeItem(this._key); } show() { const list = this.get(); if (list.length === 0) { alert("No query history yet."); return; } const html = `

Query History

${list.map((e) => ` `).join("")}
Word Lemma Time
${e.word} ${e.headword} ${new Date(e.timestamp).toLocaleString()}
`; const container = document.createElement("div"); container.innerHTML = html; document.body.appendChild(container); document.getElementById("dpd-history-close").onclick = () => container.remove(); } }; } }); // src/main.js self.__DPD_META__ = { name: "Wiki Pali DPD", version: "0.1.0" }; self.__DPD_MAIN__ = (async () => { const { Cache: Cache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports)); const { Loader: Loader2 } = await Promise.resolve().then(() => (init_loader(), loader_exports)); const { Query: Query2 } = await Promise.resolve().then(() => (init_query(), query_exports)); const { Injector: Injector2 } = await Promise.resolve().then(() => (init_injector(), injector_exports)); const { Panel: Panel2 } = await Promise.resolve().then(() => (init_panel(), panel_exports)); const { Settings: Settings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports)); const { History: History2 } = await Promise.resolve().then(() => (init_history(), history_exports)); const DB_CACHE_KEY = "dpd_web_db"; const DATA_URL = GM_getValue("dpd_data_url", ""); const cache = new Cache2(); const history = new History2(); async function ensureDb() { let dbBuffer2 = await cache.get(DB_CACHE_KEY); if (dbBuffer2) return dbBuffer2; const banner = _initBanner(); document.body.appendChild(banner); const result = await new Promise((resolve) => { banner.querySelector(".dpd-init-yes").onclick = async () => { banner.querySelector(".dpd-init-msg").textContent = "\u4E0B\u8F7D\u8BCD\u5178\u6570\u636E 0%"; banner.querySelector(".dpd-init-btns").remove(); const barWrap = document.createElement("div"); barWrap.className = "dpd-progress-wrap"; barWrap.innerHTML = `
`; banner.querySelector(".dpd-init-body").appendChild(barWrap); const fill = barWrap.querySelector(".dpd-progress-fill"); let buffer; if (!DATA_URL) { banner.querySelector(".dpd-init-msg").textContent = "\u8BCD\u5178\u6570\u636E URL \u672A\u914D\u7F6E\uFF0C\u8BF7\u5728 GM_setValue \u4E2D\u8BBE\u7F6E dpd_data_url"; return; } try { const loader = new Loader2(DATA_URL); buffer = await loader.load((pct) => { fill.style.width = pct + "%"; banner.querySelector(".dpd-init-msg").textContent = `\u4E0B\u8F7D\u8BCD\u5178\u6570\u636E ${pct}%`; }); } catch (err) { banner.querySelector(".dpd-init-msg").textContent = "\u4E0B\u8F7D\u5931\u8D25: " + err.message; return; } await cache.set(DB_CACHE_KEY, buffer); banner.remove(); resolve(buffer); }; banner.querySelector(".dpd-init-no").onclick = () => { banner.remove(); resolve(null); }; }); return result; } const dbBuffer = await ensureDb(); if (!dbBuffer) { console.log("[DPD] \u7528\u6237\u8DF3\u8FC7\u521D\u59CB\u5316"); return; } const SQL = await initSqlJs({ locateFile: () => "https://cdn.jsdelivr.net/npm/sql.js@1.11.0/dist/sql-wasm.wasm" }); const query = new Query2(dbBuffer, SQL); await query.ready; GM_registerMenuCommand("\u2699\uFE0F \u8BBE\u7F6E", () => Settings2.show()); GM_registerMenuCommand("\u{1F4DC} \u67E5\u8BE2\u5386\u53F2", () => history.show()); const injector = new Injector2(query, Panel2, history); injector.start(); self.__DPD = { query, cache, history, injector }; })(); function _initBanner() { const div = document.createElement("div"); div.id = "dpd-init-banner"; div.innerHTML = `
\u52A0\u8F7D DPD \u5DF4\u5229\u8BED\u8BCD\u5178\u6570\u636E\uFF1F
\u9996\u6B21\u4F7F\u7528\u9700\u4E0B\u8F7D ~11MB \u8BCD\u5178\u6570\u636E\uFF0C\u4E4B\u540E\u79BB\u7EBF\u53EF\u7528
`; return div; } })();