bryanmanio_obsidian-lexophile/main.js

4405 lines
1.2 MiB
JavaScript
Raw Normal View History

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
var __toBinary = /* @__PURE__ */ (() => {
var table = new Uint8Array(128);
for (var i = 0; i < 64; i++)
table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;
return (base64) => {
var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0);
for (var i2 = 0, j = 0; i2 < n; ) {
var c0 = table[base64.charCodeAt(i2++)], c1 = table[base64.charCodeAt(i2++)];
var c2 = table[base64.charCodeAt(i2++)], c3 = table[base64.charCodeAt(i2++)];
bytes[j++] = c0 << 2 | c1 >> 4;
bytes[j++] = c1 << 4 | c2 >> 2;
bytes[j++] = c2 << 6 | c3;
}
return bytes;
};
})();
// node_modules/sql.js/dist/sql-wasm-browser.js
var require_sql_wasm_browser = __commonJS({
"node_modules/sql.js/dist/sql-wasm-browser.js"(exports, module2) {
var initSqlJsPromise = void 0;
var initSqlJs2 = function(moduleConfig) {
if (initSqlJsPromise) {
return initSqlJsPromise;
}
initSqlJsPromise = new Promise(function(resolveModule, reject) {
var _a, _b;
var Module = typeof moduleConfig !== "undefined" ? moduleConfig : {};
var originalOnAbortFunction = Module["onAbort"];
Module["onAbort"] = function(errorThatCausedAbort) {
reject(new Error(errorThatCausedAbort));
if (originalOnAbortFunction) {
originalOnAbortFunction(errorThatCausedAbort);
}
};
Module["postRun"] = Module["postRun"] || [];
Module["postRun"].push(function() {
resolveModule(Module);
});
module2 = void 0;
var k;
k || (k = typeof Module != "undefined" ? Module : {});
var aa = !!globalThis.window, ba = !!globalThis.WorkerGlobalScope;
k.onRuntimeInitialized = function() {
function a(f, l) {
switch (typeof l) {
case "boolean":
$b(f, l ? 1 : 0);
break;
case "number":
ac(f, l);
break;
case "string":
bc(f, l, -1, -1);
break;
case "object":
if (null === l)
eb(f);
else if (null != l.length) {
var n = ca(l.length);
m.set(l, n);
cc(f, n, l.length, -1);
da(n);
} else
ra(f, "Wrong API use : tried to return a value of an unknown type (" + l + ").", -1);
break;
default:
eb(f);
}
}
function b(f, l) {
for (var n = [], p = 0; p < f; p += 1) {
var u = r(l + 4 * p, "i32"), v = dc(u);
if (1 === v || 2 === v)
u = ec(u);
else if (3 === v)
u = fc(u);
else if (4 === v) {
v = u;
u = gc(v);
v = hc(v);
for (var K = new Uint8Array(u), I = 0; I < u; I += 1)
K[I] = m[v + I];
u = K;
} else
u = null;
n.push(u);
}
return n;
}
function c(f, l) {
this.Qa = f;
this.db = l;
this.Oa = 1;
this.yb = [];
}
function d(f, l) {
this.db = l;
this.ob = ea(f);
if (null === this.ob)
throw Error("Unable to allocate memory for the SQL string");
this.ub = this.ob;
this.gb = this.Fb = null;
}
function e(f) {
this.filename = "dbfile_" + (4294967295 * Math.random() >>> 0);
if (null != f) {
var l = this.filename, n = "/", p = l;
n && (n = "string" == typeof n ? n : fa(n), p = l ? ha(n + "/" + l) : n);
l = ia(true, true);
p = ja(
p,
l
);
if (f) {
if ("string" == typeof f) {
n = Array(f.length);
for (var u = 0, v = f.length; u < v; ++u)
n[u] = f.charCodeAt(u);
f = n;
}
ka(p, l | 146);
n = la(p, 577);
ma(n, f, 0, f.length, 0);
na(n);
ka(p, l);
}
}
this.handleError(q(this.filename, g));
this.db = r(g, "i32");
hb(this.db);
this.pb = {};
this.Sa = {};
}
var g = y(4), h = k.cwrap, q = h("sqlite3_open", "number", ["string", "number"]), w = h("sqlite3_close_v2", "number", ["number"]), t = h("sqlite3_exec", "number", ["number", "string", "number", "number", "number"]), x = h("sqlite3_changes", "number", ["number"]), D = h(
"sqlite3_prepare_v2",
"number",
["number", "string", "number", "number", "number"]
), ib = h("sqlite3_sql", "string", ["number"]), jc = h("sqlite3_normalized_sql", "string", ["number"]), jb = h("sqlite3_prepare_v2", "number", ["number", "number", "number", "number", "number"]), kc = h("sqlite3_bind_text", "number", ["number", "number", "number", "number", "number"]), kb = h("sqlite3_bind_blob", "number", ["number", "number", "number", "number", "number"]), lc = h("sqlite3_bind_double", "number", ["number", "number", "number"]), mc = h("sqlite3_bind_int", "number", [
"number",
"number",
"number"
]), nc = h("sqlite3_bind_parameter_index", "number", ["number", "string"]), oc = h("sqlite3_step", "number", ["number"]), pc = h("sqlite3_errmsg", "string", ["number"]), qc = h("sqlite3_column_count", "number", ["number"]), rc = h("sqlite3_data_count", "number", ["number"]), sc = h("sqlite3_column_double", "number", ["number", "number"]), lb = h("sqlite3_column_text", "string", ["number", "number"]), tc = h("sqlite3_column_blob", "number", ["number", "number"]), uc = h("sqlite3_column_bytes", "number", ["number", "number"]), vc = h(
"sqlite3_column_type",
"number",
["number", "number"]
), wc = h("sqlite3_column_name", "string", ["number", "number"]), xc = h("sqlite3_reset", "number", ["number"]), yc = h("sqlite3_clear_bindings", "number", ["number"]), zc = h("sqlite3_finalize", "number", ["number"]), mb = h("sqlite3_create_function_v2", "number", "number string number number number number number number number".split(" ")), dc = h("sqlite3_value_type", "number", ["number"]), gc = h("sqlite3_value_bytes", "number", ["number"]), fc = h("sqlite3_value_text", "string", ["number"]), hc = h(
"sqlite3_value_blob",
"number",
["number"]
), ec = h("sqlite3_value_double", "number", ["number"]), ac = h("sqlite3_result_double", "", ["number", "number"]), eb = h("sqlite3_result_null", "", ["number"]), bc = h("sqlite3_result_text", "", ["number", "string", "number", "number"]), cc = h("sqlite3_result_blob", "", ["number", "number", "number", "number"]), $b = h("sqlite3_result_int", "", ["number", "number"]), ra = h("sqlite3_result_error", "", ["number", "string", "number"]), nb = h("sqlite3_aggregate_context", "number", ["number", "number"]), hb = h(
"RegisterExtensionFunctions",
"number",
["number"]
), ob = h("sqlite3_update_hook", "number", ["number", "number", "number"]);
c.prototype.bind = function(f) {
if (!this.Qa)
throw "Statement closed";
this.reset();
return Array.isArray(f) ? this.Wb(f) : null != f && "object" === typeof f ? this.Xb(f) : true;
};
c.prototype.step = function() {
if (!this.Qa)
throw "Statement closed";
this.Oa = 1;
var f = oc(this.Qa);
switch (f) {
case 100:
return true;
case 101:
return false;
default:
throw this.db.handleError(f);
}
};
c.prototype.Pb = function(f) {
null == f && (f = this.Oa, this.Oa += 1);
return sc(this.Qa, f);
};
c.prototype.hc = function(f) {
null == f && (f = this.Oa, this.Oa += 1);
f = lb(this.Qa, f);
if ("function" !== typeof BigInt)
throw Error("BigInt is not supported");
return BigInt(f);
};
c.prototype.mc = function(f) {
null == f && (f = this.Oa, this.Oa += 1);
return lb(this.Qa, f);
};
c.prototype.getBlob = function(f) {
null == f && (f = this.Oa, this.Oa += 1);
var l = uc(this.Qa, f);
f = tc(this.Qa, f);
for (var n = new Uint8Array(l), p = 0; p < l; p += 1)
n[p] = m[f + p];
return n;
};
c.prototype.get = function(f, l) {
l = l || {};
null != f && this.bind(f) && this.step();
f = [];
for (var n = rc(this.Qa), p = 0; p < n; p += 1)
switch (vc(this.Qa, p)) {
case 1:
var u = l.useBigInt ? this.hc(p) : this.Pb(p);
f.push(u);
break;
case 2:
f.push(this.Pb(p));
break;
case 3:
f.push(this.mc(p));
break;
case 4:
f.push(this.getBlob(p));
break;
default:
f.push(null);
}
return f;
};
c.prototype.Db = function() {
for (var f = [], l = qc(this.Qa), n = 0; n < l; n += 1)
f.push(wc(this.Qa, n));
return f;
};
c.prototype.Ob = function(f, l) {
f = this.get(f, l);
l = this.Db();
for (var n = {}, p = 0; p < l.length; p += 1)
n[l[p]] = f[p];
return n;
};
c.prototype.lc = function() {
return ib(this.Qa);
};
c.prototype.ic = function() {
return jc(this.Qa);
};
c.prototype.Jb = function(f) {
null != f && this.bind(f);
this.step();
return this.reset();
};
c.prototype.Lb = function(f, l) {
null == l && (l = this.Oa, this.Oa += 1);
f = ea(f);
this.yb.push(f);
this.db.handleError(kc(this.Qa, l, f, -1, 0));
};
c.prototype.Vb = function(f, l) {
null == l && (l = this.Oa, this.Oa += 1);
var n = ca(f.length);
m.set(f, n);
this.yb.push(n);
this.db.handleError(kb(this.Qa, l, n, f.length, 0));
};
c.prototype.Kb = function(f, l) {
null == l && (l = this.Oa, this.Oa += 1);
this.db.handleError((f === (f | 0) ? mc : lc)(
this.Qa,
l,
f
));
};
c.prototype.Yb = function(f) {
null == f && (f = this.Oa, this.Oa += 1);
kb(this.Qa, f, 0, 0, 0);
};
c.prototype.Mb = function(f, l) {
null == l && (l = this.Oa, this.Oa += 1);
switch (typeof f) {
case "string":
this.Lb(f, l);
return;
case "number":
this.Kb(f, l);
return;
case "bigint":
this.Lb(f.toString(), l);
return;
case "boolean":
this.Kb(f + 0, l);
return;
case "object":
if (null === f) {
this.Yb(l);
return;
}
if (null != f.length) {
this.Vb(f, l);
return;
}
}
throw "Wrong API use : tried to bind a value of an unknown type (" + f + ").";
};
c.prototype.Xb = function(f) {
var l = this;
Object.keys(f).forEach(function(n) {
var p = nc(l.Qa, n);
0 !== p && l.Mb(f[n], p);
});
return true;
};
c.prototype.Wb = function(f) {
for (var l = 0; l < f.length; l += 1)
this.Mb(f[l], l + 1);
return true;
};
c.prototype.reset = function() {
this.Cb();
return 0 === yc(this.Qa) && 0 === xc(this.Qa);
};
c.prototype.Cb = function() {
for (var f; void 0 !== (f = this.yb.pop()); )
da(f);
};
c.prototype.cb = function() {
this.Cb();
var f = 0 === zc(this.Qa);
delete this.db.pb[this.Qa];
this.Qa = 0;
return f;
};
d.prototype.next = function() {
if (null === this.ob)
return { done: true };
null !== this.gb && (this.gb.cb(), this.gb = null);
if (!this.db.db)
throw this.Ab(), Error("Database closed");
var f = oa(), l = y(4);
pa(g);
pa(l);
try {
this.db.handleError(jb(this.db.db, this.ub, -1, g, l));
this.ub = r(l, "i32");
var n = r(g, "i32");
if (0 === n)
return this.Ab(), { done: true };
this.gb = new c(n, this.db);
this.db.pb[n] = this.gb;
return { value: this.gb, done: false };
} catch (p) {
throw this.Fb = z(this.ub), this.Ab(), p;
} finally {
qa(f);
}
};
d.prototype.Ab = function() {
da(this.ob);
this.ob = null;
};
d.prototype.jc = function() {
return null !== this.Fb ? this.Fb : z(this.ub);
};
"function" === typeof Symbol && "symbol" === typeof Symbol.iterator && (d.prototype[Symbol.iterator] = function() {
return this;
});
e.prototype.Jb = function(f, l) {
if (!this.db)
throw "Database closed";
if (l) {
f = this.Gb(f, l);
try {
f.step();
} finally {
f.cb();
}
} else
this.handleError(t(this.db, f, 0, 0, g));
return this;
};
e.prototype.exec = function(f, l, n) {
if (!this.db)
throw "Database closed";
var p = null, u = null, v = null;
try {
v = u = ea(f);
var K = y(4);
for (f = []; 0 !== r(v, "i8"); ) {
pa(g);
pa(K);
this.handleError(jb(this.db, v, -1, g, K));
var I = r(g, "i32");
v = r(
K,
"i32"
);
if (0 !== I) {
var H = null;
p = new c(I, this);
for (null != l && p.bind(l); p.step(); )
null === H && (H = { columns: p.Db(), values: [] }, f.push(H)), H.values.push(p.get(null, n));
p.cb();
}
}
return f;
} catch (L) {
throw p && p.cb(), L;
} finally {
u && da(u);
}
};
e.prototype.ec = function(f, l, n, p, u) {
"function" === typeof l && (p = n, n = l, l = void 0);
f = this.Gb(f, l);
try {
for (; f.step(); )
n(f.Ob(null, u));
} finally {
f.cb();
}
if ("function" === typeof p)
return p();
};
e.prototype.Gb = function(f, l) {
pa(g);
this.handleError(D(this.db, f, -1, g, 0));
f = r(g, "i32");
if (0 === f)
throw "Nothing to prepare";
var n = new c(f, this);
null != l && n.bind(l);
return this.pb[f] = n;
};
e.prototype.pc = function(f) {
return new d(f, this);
};
e.prototype.fc = function() {
Object.values(this.pb).forEach(function(l) {
l.cb();
});
Object.values(this.Sa).forEach(A);
this.Sa = {};
this.handleError(w(this.db));
var f = sa(this.filename);
this.handleError(q(this.filename, g));
this.db = r(g, "i32");
hb(this.db);
return f;
};
e.prototype.close = function() {
null !== this.db && (Object.values(this.pb).forEach(function(f) {
f.cb();
}), Object.values(this.Sa).forEach(A), this.Sa = {}, this.fb && (A(this.fb), this.fb = void 0), this.handleError(w(this.db)), ta("/" + this.filename), this.db = null);
};
e.prototype.handleError = function(f) {
if (0 === f)
return null;
f = pc(this.db);
throw Error(f);
};
e.prototype.kc = function() {
return x(this.db);
};
e.prototype.bc = function(f, l) {
Object.prototype.hasOwnProperty.call(this.Sa, f) && (A(this.Sa[f]), delete this.Sa[f]);
var n = ua(function(p, u, v) {
u = b(u, v);
try {
var K = l.apply(null, u);
} catch (I) {
ra(p, I, -1);
return;
}
a(p, K);
}, "viii");
this.Sa[f] = n;
this.handleError(mb(
this.db,
f,
l.length,
1,
0,
n,
0,
0,
0
));
return this;
};
e.prototype.ac = function(f, l) {
var n = l.init || function() {
return null;
}, p = l.finalize || function(H) {
return H;
}, u = l.step;
if (!u)
throw "An aggregate function must have a step function in " + f;
var v = {};
Object.hasOwnProperty.call(this.Sa, f) && (A(this.Sa[f]), delete this.Sa[f]);
l = f + "__finalize";
Object.hasOwnProperty.call(this.Sa, l) && (A(this.Sa[l]), delete this.Sa[l]);
var K = ua(function(H, L, Ka) {
var V = nb(H, 1);
Object.hasOwnProperty.call(v, V) || (v[V] = n());
L = b(L, Ka);
L = [v[V]].concat(L);
try {
v[V] = u.apply(
null,
L
);
} catch (Bc) {
delete v[V], ra(H, Bc, -1);
}
}, "viii"), I = ua(function(H) {
var L = nb(H, 1);
try {
var Ka = p(v[L]);
} catch (V) {
delete v[L];
ra(H, V, -1);
return;
}
a(H, Ka);
delete v[L];
}, "vi");
this.Sa[f] = K;
this.Sa[l] = I;
this.handleError(mb(this.db, f, u.length - 1, 1, 0, 0, K, I, 0));
return this;
};
e.prototype.vc = function(f) {
this.fb && (ob(this.db, 0, 0), A(this.fb), this.fb = void 0);
if (!f)
return this;
this.fb = ua(function(l, n, p, u, v) {
switch (n) {
case 18:
l = "insert";
break;
case 23:
l = "update";
break;
case 9:
l = "delete";
break;
default:
throw "unknown operationCode in updateHook callback: " + n;
}
p = z(p);
u = z(u);
if (v > Number.MAX_SAFE_INTEGER)
throw "rowId too big to fit inside a Number";
f(l, p, u, Number(v));
}, "viiiij");
ob(this.db, this.fb, 0);
return this;
};
c.prototype.bind = c.prototype.bind;
c.prototype.step = c.prototype.step;
c.prototype.get = c.prototype.get;
c.prototype.getColumnNames = c.prototype.Db;
c.prototype.getAsObject = c.prototype.Ob;
c.prototype.getSQL = c.prototype.lc;
c.prototype.getNormalizedSQL = c.prototype.ic;
c.prototype.run = c.prototype.Jb;
c.prototype.reset = c.prototype.reset;
c.prototype.freemem = c.prototype.Cb;
c.prototype.free = c.prototype.cb;
d.prototype.next = d.prototype.next;
d.prototype.getRemainingSQL = d.prototype.jc;
e.prototype.run = e.prototype.Jb;
e.prototype.exec = e.prototype.exec;
e.prototype.each = e.prototype.ec;
e.prototype.prepare = e.prototype.Gb;
e.prototype.iterateStatements = e.prototype.pc;
e.prototype["export"] = e.prototype.fc;
e.prototype.close = e.prototype.close;
e.prototype.handleError = e.prototype.handleError;
e.prototype.getRowsModified = e.prototype.kc;
e.prototype.create_function = e.prototype.bc;
e.prototype.create_aggregate = e.prototype.ac;
e.prototype.updateHook = e.prototype.vc;
k.Database = e;
};
var va = "./this.program", wa = (_b = (_a = globalThis.document) == null ? void 0 : _a.currentScript) == null ? void 0 : _b.src;
ba && (wa = self.location.href);
var xa = "", ya, za;
if (aa || ba) {
try {
xa = new URL(".", wa).href;
} catch (e) {
}
ba && (za = (a) => {
var b = new XMLHttpRequest();
b.open("GET", a, false);
b.responseType = "arraybuffer";
b.send(null);
return new Uint8Array(b.response);
});
ya = async (a) => {
a = await fetch(a, { credentials: "same-origin" });
if (a.ok)
return a.arrayBuffer();
throw Error(a.status + " : " + a.url);
};
}
var Aa = console.log.bind(console), B = console.error.bind(console), Ba, Ca = false, Da, m, C, Ea, E, F, Fa, Ga, G;
function Ha() {
var a = Ia.buffer;
m = new Int8Array(a);
Ea = new Int16Array(a);
C = new Uint8Array(a);
new Uint16Array(a);
E = new Int32Array(a);
F = new Uint32Array(a);
Fa = new Float32Array(a);
Ga = new Float64Array(a);
G = new BigInt64Array(a);
new BigUint64Array(a);
}
function Ja(a) {
var _a2;
(_a2 = k.onAbort) == null ? void 0 : _a2.call(k, a);
a = "Aborted(" + a + ")";
B(a);
Ca = true;
throw new WebAssembly.RuntimeError(a + ". Build with -sASSERTIONS for more info.");
}
var La;
async function Ma(a) {
if (!Ba)
try {
var b = await ya(a);
return new Uint8Array(b);
} catch (e) {
}
if (a == La && Ba)
a = new Uint8Array(Ba);
else if (za)
a = za(a);
else
throw "both async and sync fetching of the wasm failed";
return a;
}
async function Na(a, b) {
try {
var c = await Ma(a);
return await WebAssembly.instantiate(c, b);
} catch (d) {
B(`failed to asynchronously prepare wasm: ${d}`), Ja(d);
}
}
async function Oa(a) {
var b = La;
if (!Ba)
try {
var c = fetch(b, { credentials: "same-origin" });
return await WebAssembly.instantiateStreaming(c, a);
} catch (d) {
B(`wasm streaming compile failed: ${d}`), B("falling back to ArrayBuffer instantiation");
}
return Na(b, a);
}
class Pa {
constructor(a) {
__publicField(this, "name", "ExitStatus");
this.message = `Program terminated with exit(${a})`;
this.status = a;
}
}
var Qa = (a) => {
for (; 0 < a.length; )
a.shift()(k);
}, Ra = [], Sa = [], Ta = () => {
var a = k.preRun.shift();
Sa.push(a);
}, J = 0, Ua = null;
function r(a, b = "i8") {
b.endsWith("*") && (b = "*");
switch (b) {
case "i1":
return m[a];
case "i8":
return m[a];
case "i16":
return Ea[a >> 1];
case "i32":
return E[a >> 2];
case "i64":
return G[a >> 3];
case "float":
return Fa[a >> 2];
case "double":
return Ga[a >> 3];
case "*":
return F[a >> 2];
default:
Ja(`invalid type for getValue: ${b}`);
}
}
var Va = true;
function pa(a) {
var b = "i32";
b.endsWith("*") && (b = "*");
switch (b) {
case "i1":
m[a] = 0;
break;
case "i8":
m[a] = 0;
break;
case "i16":
Ea[a >> 1] = 0;
break;
case "i32":
E[a >> 2] = 0;
break;
case "i64":
G[a >> 3] = BigInt(0);
break;
case "float":
Fa[a >> 2] = 0;
break;
case "double":
Ga[a >> 3] = 0;
break;
case "*":
F[a >> 2] = 0;
break;
default:
Ja(`invalid type for setValue: ${b}`);
}
}
var Wa = new TextDecoder(), Xa = (a, b, c, d) => {
c = b + c;
if (d)
return c;
for (; a[b] && !(b >= c); )
++b;
return b;
}, z = (a, b, c) => a ? Wa.decode(C.subarray(a, Xa(C, a, b, c))) : "", Ya = (a, b) => {
for (var c = 0, d = a.length - 1; 0 <= d; d--) {
var e = a[d];
"." === e ? a.splice(d, 1) : ".." === e ? (a.splice(d, 1), c++) : c && (a.splice(d, 1), c--);
}
if (b)
for (; c; c--)
a.unshift("..");
return a;
}, ha = (a) => {
var b = "/" === a.charAt(0), c = "/" === a.slice(-1);
(a = Ya(a.split("/").filter((d) => !!d), !b).join("/")) || b || (a = ".");
a && c && (a += "/");
return (b ? "/" : "") + a;
}, Za = (a) => {
var b = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);
a = b[0];
b = b[1];
if (!a && !b)
return ".";
b && (b = b.slice(0, -1));
return a + b;
}, $a = (a) => a && a.match(/([^\/]+|\/)\/*$/)[1], ab = () => (a) => crypto.getRandomValues(a), bb = (a) => {
(bb = ab())(a);
}, cb = (...a) => {
for (var b = "", c = false, d = a.length - 1; -1 <= d && !c; d--) {
c = 0 <= d ? a[d] : "/";
if ("string" != typeof c)
throw new TypeError("Arguments to path.resolve must be strings");
if (!c)
return "";
b = c + "/" + b;
c = "/" === c.charAt(0);
}
b = Ya(b.split("/").filter((e) => !!e), !c).join("/");
return (c ? "/" : "") + b || ".";
}, db = (a) => {
var b = Xa(a, 0);
return Wa.decode(a.buffer ? a.subarray(0, b) : new Uint8Array(a.slice(0, b)));
}, fb = [], gb = (a) => {
for (var b = 0, c = 0; c < a.length; ++c) {
var d = a.charCodeAt(c);
127 >= d ? b++ : 2047 >= d ? b += 2 : 55296 <= d && 57343 >= d ? (b += 4, ++c) : b += 3;
}
return b;
}, M = (a, b, c, d) => {
if (!(0 < d))
return 0;
var e = c;
d = c + d - 1;
for (var g = 0; g < a.length; ++g) {
var h = a.codePointAt(g);
if (127 >= h) {
if (c >= d)
break;
b[c++] = h;
} else if (2047 >= h) {
if (c + 1 >= d)
break;
b[c++] = 192 | h >> 6;
b[c++] = 128 | h & 63;
} else if (65535 >= h) {
if (c + 2 >= d)
break;
b[c++] = 224 | h >> 12;
b[c++] = 128 | h >> 6 & 63;
b[c++] = 128 | h & 63;
} else {
if (c + 3 >= d)
break;
b[c++] = 240 | h >> 18;
b[c++] = 128 | h >> 12 & 63;
b[c++] = 128 | h >> 6 & 63;
b[c++] = 128 | h & 63;
g++;
}
}
b[c] = 0;
return c - e;
}, pb = [];
function qb(a, b) {
pb[a] = { input: [], output: [], kb: b };
rb(a, sb);
}
var sb = { open(a) {
var b = pb[a.node.nb];
if (!b)
throw new N(43);
a.Va = b;
a.seekable = false;
}, close(a) {
a.Va.kb.lb(a.Va);
}, lb(a) {
a.Va.kb.lb(a.Va);
}, read(a, b, c, d) {
if (!a.Va || !a.Va.kb.Qb)
throw new N(60);
for (var e = 0, g = 0; g < d; g++) {
try {
var h = a.Va.kb.Qb(a.Va);
} catch (q) {
throw new N(29);
}
if (void 0 === h && 0 === e)
throw new N(6);
if (null === h || void 0 === h)
break;
e++;
b[c + g] = h;
}
e && (a.node.$a = Date.now());
return e;
}, write(a, b, c, d) {
if (!a.Va || !a.Va.kb.Hb)
throw new N(60);
try {
for (var e = 0; e < d; e++)
a.Va.kb.Hb(a.Va, b[c + e]);
} catch (g) {
throw new N(29);
}
d && (a.node.Ua = a.node.Ta = Date.now());
return e;
} }, tb = { Qb() {
var _a2;
a: {
if (!fb.length) {
var a = null;
((_a2 = globalThis.window) == null ? void 0 : _a2.prompt) && (a = window.prompt("Input: "), null !== a && (a += "\n"));
if (!a) {
var b = null;
break a;
}
b = Array(gb(a) + 1);
a = M(a, b, 0, b.length);
b.length = a;
fb = b;
}
b = fb.shift();
}
return b;
}, Hb(a, b) {
null === b || 10 === b ? (Aa(db(a.output)), a.output = []) : 0 != b && a.output.push(b);
}, lb(a) {
var _a2;
0 < ((_a2 = a.output) == null ? void 0 : _a2.length) && (Aa(db(a.output)), a.output = []);
}, Dc() {
return { yc: 25856, Ac: 5, xc: 191, zc: 35387, wc: [
3,
28,
127,
21,
4,
0,
1,
0,
17,
19,
26,
0,
18,
15,
23,
22,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
] };
}, Ec() {
return 0;
}, Fc() {
return [24, 80];
} }, ub = { Hb(a, b) {
null === b || 10 === b ? (B(db(a.output)), a.output = []) : 0 != b && a.output.push(b);
}, lb(a) {
var _a2;
0 < ((_a2 = a.output) == null ? void 0 : _a2.length) && (B(db(a.output)), a.output = []);
} }, O = { Za: null, ab() {
return O.createNode(null, "/", 16895, 0);
}, createNode(a, b, c, d) {
if (24576 === (c & 61440) || 4096 === (c & 61440))
throw new N(63);
O.Za || (O.Za = { dir: { node: { Wa: O.La.Wa, Xa: O.La.Xa, mb: O.La.mb, rb: O.La.rb, Tb: O.La.Tb, xb: O.La.xb, vb: O.La.vb, Ib: O.La.Ib, wb: O.La.wb }, stream: { Ya: O.Ma.Ya } }, file: {
node: { Wa: O.La.Wa, Xa: O.La.Xa },
stream: { Ya: O.Ma.Ya, read: O.Ma.read, write: O.Ma.write, sb: O.Ma.sb, tb: O.Ma.tb }
}, link: { node: { Wa: O.La.Wa, Xa: O.La.Xa, eb: O.La.eb }, stream: {} }, Nb: { node: { Wa: O.La.Wa, Xa: O.La.Xa }, stream: vb } });
c = wb(a, b, c, d);
P(c.mode) ? (c.La = O.Za.dir.node, c.Ma = O.Za.dir.stream, c.Na = {}) : 32768 === (c.mode & 61440) ? (c.La = O.Za.file.node, c.Ma = O.Za.file.stream, c.Ra = 0, c.Na = null) : 40960 === (c.mode & 61440) ? (c.La = O.Za.link.node, c.Ma = O.Za.link.stream) : 8192 === (c.mode & 61440) && (c.La = O.Za.Nb.node, c.Ma = O.Za.Nb.stream);
c.$a = c.Ua = c.Ta = Date.now();
a && (a.Na[b] = c, a.$a = a.Ua = a.Ta = c.$a);
return c;
}, Cc(a) {
return a.Na ? a.Na.subarray ? a.Na.subarray(0, a.Ra) : new Uint8Array(a.Na) : new Uint8Array(0);
}, La: { Wa(a) {
var b = {};
b.cc = 8192 === (a.mode & 61440) ? a.id : 1;
b.oc = a.id;
b.mode = a.mode;
b.rc = 1;
b.uid = 0;
b.nc = 0;
b.nb = a.nb;
P(a.mode) ? b.size = 4096 : 32768 === (a.mode & 61440) ? b.size = a.Ra : 40960 === (a.mode & 61440) ? b.size = a.link.length : b.size = 0;
b.$a = new Date(a.$a);
b.Ua = new Date(a.Ua);
b.Ta = new Date(a.Ta);
b.Zb = 4096;
b.$b = Math.ceil(b.size / b.Zb);
return b;
}, Xa(a, b) {
for (var c of ["mode", "atime", "mtime", "ctime"])
null != b[c] && (a[c] = b[c]);
void 0 !== b.size && (b = b.size, a.Ra != b && (0 == b ? (a.Na = null, a.Ra = 0) : (c = a.Na, a.Na = new Uint8Array(b), c && a.Na.set(c.subarray(0, Math.min(b, a.Ra))), a.Ra = b)));
}, mb() {
O.zb || (O.zb = new N(44), O.zb.stack = "<generic error, no stack>");
throw O.zb;
}, rb(a, b, c, d) {
return O.createNode(a, b, c, d);
}, Tb(a, b, c) {
try {
var d = Q(b, c);
} catch (g) {
}
if (d) {
if (P(a.mode))
for (var e in d.Na)
throw new N(55);
xb(d);
}
delete a.parent.Na[a.name];
b.Na[c] = a;
a.name = c;
b.Ta = b.Ua = a.parent.Ta = a.parent.Ua = Date.now();
}, xb(a, b) {
delete a.Na[b];
a.Ta = a.Ua = Date.now();
}, vb(a, b) {
var c = Q(a, b), d;
for (d in c.Na)
throw new N(55);
delete a.Na[b];
a.Ta = a.Ua = Date.now();
}, Ib(a) {
return [".", "..", ...Object.keys(a.Na)];
}, wb(a, b, c) {
a = O.createNode(a, b, 41471, 0);
a.link = c;
return a;
}, eb(a) {
if (40960 !== (a.mode & 61440))
throw new N(28);
return a.link;
} }, Ma: { read(a, b, c, d, e) {
var g = a.node.Na;
if (e >= a.node.Ra)
return 0;
a = Math.min(a.node.Ra - e, d);
if (8 < a && g.subarray)
b.set(g.subarray(e, e + a), c);
else
for (d = 0; d < a; d++)
b[c + d] = g[e + d];
return a;
}, write(a, b, c, d, e, g) {
b.buffer === m.buffer && (g = false);
if (!d)
return 0;
a = a.node;
a.Ua = a.Ta = Date.now();
if (b.subarray && (!a.Na || a.Na.subarray)) {
if (g)
return a.Na = b.subarray(c, c + d), a.Ra = d;
if (0 === a.Ra && 0 === e)
return a.Na = b.slice(c, c + d), a.Ra = d;
if (e + d <= a.Ra)
return a.Na.set(b.subarray(c, c + d), e), d;
}
g = e + d;
var h = a.Na ? a.Na.length : 0;
h >= g || (g = Math.max(g, h * (1048576 > h ? 2 : 1.125) >>> 0), 0 != h && (g = Math.max(g, 256)), h = a.Na, a.Na = new Uint8Array(g), 0 < a.Ra && a.Na.set(h.subarray(0, a.Ra), 0));
if (a.Na.subarray && b.subarray)
a.Na.set(b.subarray(c, c + d), e);
else
for (g = 0; g < d; g++)
a.Na[e + g] = b[c + g];
a.Ra = Math.max(
a.Ra,
e + d
);
return d;
}, Ya(a, b, c) {
1 === c ? b += a.position : 2 === c && 32768 === (a.node.mode & 61440) && (b += a.node.Ra);
if (0 > b)
throw new N(28);
return b;
}, sb(a, b, c, d, e) {
if (32768 !== (a.node.mode & 61440))
throw new N(43);
a = a.node.Na;
if (e & 2 || !a || a.buffer !== m.buffer) {
e = true;
d = 65536 * Math.ceil(b / 65536);
var g = yb(65536, d);
g && C.fill(0, g, g + d);
d = g;
if (!d)
throw new N(48);
if (a) {
if (0 < c || c + b < a.length)
a.subarray ? a = a.subarray(c, c + b) : a = Array.prototype.slice.call(a, c, c + b);
m.set(a, d);
}
} else
e = false, d = a.byteOffset;
return { tc: d, Ub: e };
}, tb(a, b, c, d) {
O.Ma.write(
a,
b,
0,
d,
c,
false
);
return 0;
} } }, ia = (a, b) => {
var c = 0;
a && (c |= 365);
b && (c |= 146);
return c;
}, zb = null, Ab = {}, Bb = [], Cb = 1, R = null, Db = false, Eb = true, N = class {
constructor(a) {
__publicField(this, "name", "ErrnoError");
this.Pa = a;
}
}, Fb = class {
constructor() {
__publicField(this, "qb", {});
__publicField(this, "node", null);
}
get flags() {
return this.qb.flags;
}
set flags(a) {
this.qb.flags = a;
}
get position() {
return this.qb.position;
}
set position(a) {
this.qb.position = a;
}
}, Gb = class {
constructor(a, b, c, d) {
__publicField(this, "La", {});
__publicField(this, "Ma", {});
__publicField(this, "ib", null);
a || (a = this);
this.parent = a;
this.ab = a.ab;
this.id = Cb++;
this.name = b;
this.mode = c;
this.nb = d;
this.$a = this.Ua = this.Ta = Date.now();
}
get read() {
return 365 === (this.mode & 365);
}
set read(a) {
a ? this.mode |= 365 : this.mode &= -366;
}
get write() {
return 146 === (this.mode & 146);
}
set write(a) {
a ? this.mode |= 146 : this.mode &= -147;
}
};
function S(a, b = {}) {
var _a2;
if (!a)
throw new N(44);
(_a2 = b.Bb) != null ? _a2 : b.Bb = true;
"/" === a.charAt(0) || (a = "//" + a);
var c = 0;
a:
for (; 40 > c; c++) {
a = a.split("/").filter((q) => !!q);
for (var d = zb, e = "/", g = 0; g < a.length; g++) {
var h = g === a.length - 1;
if (h && b.parent)
break;
if ("." !== a[g])
if (".." === a[g])
if (e = Za(e), d === d.parent) {
a = e + "/" + a.slice(g + 1).join("/");
c--;
continue a;
} else
d = d.parent;
else {
e = ha(e + "/" + a[g]);
try {
d = Q(d, a[g]);
} catch (q) {
if (44 === (q == null ? void 0 : q.Pa) && h && b.sc)
return { path: e };
throw q;
}
!d.ib || h && !b.Bb || (d = d.ib.root);
if (40960 === (d.mode & 61440) && (!h || b.hb)) {
if (!d.La.eb)
throw new N(52);
d = d.La.eb(d);
"/" === d.charAt(0) || (d = Za(e) + "/" + d);
a = d + "/" + a.slice(g + 1).join("/");
continue a;
}
}
}
return { path: e, node: d };
}
throw new N(32);
}
function fa(a) {
for (var b; ; ) {
if (a === a.parent)
return a = a.ab.Sb, b ? "/" !== a[a.length - 1] ? `${a}/${b}` : a + b : a;
b = b ? `${a.name}/${b}` : a.name;
a = a.parent;
}
}
function Hb(a, b) {
for (var c = 0, d = 0; d < b.length; d++)
c = (c << 5) - c + b.charCodeAt(d) | 0;
return (a + c >>> 0) % R.length;
}
function xb(a) {
var b = Hb(a.parent.id, a.name);
if (R[b] === a)
R[b] = a.jb;
else
for (b = R[b]; b; ) {
if (b.jb === a) {
b.jb = a.jb;
break;
}
b = b.jb;
}
}
function Q(a, b) {
var c = P(a.mode) ? (c = Ib(a, "x")) ? c : a.La.mb ? 0 : 2 : 54;
if (c)
throw new N(c);
for (c = R[Hb(a.id, b)]; c; c = c.jb) {
var d = c.name;
if (c.parent.id === a.id && d === b)
return c;
}
return a.La.mb(a, b);
}
function wb(a, b, c, d) {
a = new Gb(a, b, c, d);
b = Hb(a.parent.id, a.name);
a.jb = R[b];
return R[b] = a;
}
function P(a) {
return 16384 === (a & 61440);
}
function Ib(a, b) {
return Eb ? 0 : b.includes("r") && !(a.mode & 292) || b.includes("w") && !(a.mode & 146) || b.includes("x") && !(a.mode & 73) ? 2 : 0;
}
function Jb(a, b) {
if (!P(a.mode))
return 54;
try {
return Q(a, b), 20;
} catch (c) {
}
return Ib(a, "wx");
}
function Kb(a, b, c) {
try {
var d = Q(a, b);
} catch (e) {
return e.Pa;
}
if (a = Ib(a, "wx"))
return a;
if (c) {
if (!P(d.mode))
return 54;
if (d === d.parent || "/" === fa(d))
return 10;
} else if (P(d.mode))
return 31;
return 0;
}
function Lb(a) {
if (!a)
throw new N(63);
return a;
}
function T(a) {
a = Bb[a];
if (!a)
throw new N(8);
return a;
}
function Mb(a, b = -1) {
a = Object.assign(new Fb(), a);
if (-1 == b)
a: {
for (b = 0; 4096 >= b; b++)
if (!Bb[b])
break a;
throw new N(33);
}
a.bb = b;
return Bb[b] = a;
}
function Nb(a, b = -1) {
var _a2, _b2;
a = Mb(a, b);
(_b2 = (_a2 = a.Ma) == null ? void 0 : _a2.Bc) == null ? void 0 : _b2.call(_a2, a);
return a;
}
function Ob(a, b, c) {
var d = a == null ? void 0 : a.Ma.Xa;
a = d ? a : b;
d != null ? d : d = b.La.Xa;
Lb(d);
d(a, c);
}
var vb = { open(a) {
var _a2, _b2;
a.Ma = Ab[a.node.nb].Ma;
(_b2 = (_a2 = a.Ma).open) == null ? void 0 : _b2.call(_a2, a);
}, Ya() {
throw new N(70);
} };
function rb(a, b) {
Ab[a] = { Ma: b };
}
function Pb(a, b) {
var c = "/" === b;
if (c && zb)
throw new N(10);
if (!c && b) {
var d = S(b, { Bb: false });
b = d.path;
d = d.node;
if (d.ib)
throw new N(10);
if (!P(d.mode))
throw new N(54);
}
b = { type: a, Gc: {}, Sb: b, qc: [] };
a = a.ab(b);
a.ab = b;
b.root = a;
c ? zb = a : d && (d.ib = b, d.ab && d.ab.qc.push(b));
}
function Qb(a, b, c) {
var d = S(a, { parent: true }).node;
a = $a(a);
if (!a)
throw new N(28);
if ("." === a || ".." === a)
throw new N(20);
var e = Jb(d, a);
if (e)
throw new N(e);
if (!d.La.rb)
throw new N(63);
return d.La.rb(d, a, b, c);
}
function ja(a, b = 438) {
return Qb(a, b & 4095 | 32768, 0);
}
function U(a, b = 511) {
return Qb(a, b & 1023 | 16384, 0);
}
function Rb(a, b, c) {
"undefined" == typeof c && (c = b, b = 438);
Qb(a, b | 8192, c);
}
function Sb(a, b) {
if (!cb(a))
throw new N(44);
var c = S(b, { parent: true }).node;
if (!c)
throw new N(44);
b = $a(b);
var d = Jb(c, b);
if (d)
throw new N(d);
if (!c.La.wb)
throw new N(63);
c.La.wb(c, b, a);
}
function Tb(a) {
var b = S(a, { parent: true }).node;
a = $a(a);
var c = Q(b, a), d = Kb(b, a, true);
if (d)
throw new N(d);
if (!b.La.vb)
throw new N(63);
if (c.ib)
throw new N(10);
b.La.vb(b, a);
xb(c);
}
function ta(a) {
var b = S(a, { parent: true }).node;
if (!b)
throw new N(44);
a = $a(a);
var c = Q(b, a), d = Kb(b, a, false);
if (d)
throw new N(d);
if (!b.La.xb)
throw new N(63);
if (c.ib)
throw new N(10);
b.La.xb(b, a);
xb(c);
}
function Ub(a, b) {
a = S(a, { hb: !b }).node;
return Lb(a.La.Wa)(a);
}
function Vb(a, b, c, d) {
Ob(a, b, { mode: c & 4095 | b.mode & -4096, Ta: Date.now(), dc: d });
}
function ka(a, b) {
a = "string" == typeof a ? S(a, { hb: true }).node : a;
Vb(null, a, b);
}
function Wb(a, b, c) {
if (P(b.mode))
throw new N(31);
if (32768 !== (b.mode & 61440))
throw new N(28);
var d = Ib(b, "w");
if (d)
throw new N(d);
Ob(a, b, { size: c, timestamp: Date.now() });
}
function la(a, b, c = 438) {
if ("" === a)
throw new N(44);
if ("string" == typeof b) {
var d = { r: 0, "r+": 2, w: 577, "w+": 578, a: 1089, "a+": 1090 }[b];
if ("undefined" == typeof d)
throw Error(`Unknown file open mode: ${b}`);
b = d;
}
c = b & 64 ? c & 4095 | 32768 : 0;
if ("object" == typeof a)
d = a;
else {
var e = a.endsWith("/");
var g = S(a, { hb: !(b & 131072), sc: true });
d = g.node;
a = g.path;
}
g = false;
if (b & 64)
if (d) {
if (b & 128)
throw new N(20);
} else {
if (e)
throw new N(31);
d = Qb(a, c | 511, 0);
g = true;
}
if (!d)
throw new N(44);
8192 === (d.mode & 61440) && (b &= -513);
if (b & 65536 && !P(d.mode))
throw new N(54);
if (!g && (d ? 40960 === (d.mode & 61440) ? e = 32 : (e = ["r", "w", "rw"][b & 3], b & 512 && (e += "w"), e = P(d.mode) && ("r" !== e || b & 576) ? 31 : Ib(d, e)) : e = 44, e))
throw new N(e);
b & 512 && !g && (e = d, e = "string" == typeof e ? S(e, { hb: true }).node : e, Wb(null, e, 0));
b = Mb({ node: d, path: fa(d), flags: b & -131713, seekable: true, position: 0, Ma: d.Ma, uc: [], error: false });
b.Ma.open && b.Ma.open(b);
g && ka(d, c & 511);
return b;
}
function na(a) {
if (null === a.bb)
throw new N(8);
a.Eb && (a.Eb = null);
try {
a.Ma.close && a.Ma.close(a);
} catch (b) {
throw b;
} finally {
Bb[a.bb] = null;
}
a.bb = null;
}
function Xb(a, b, c) {
if (null === a.bb)
throw new N(8);
if (!a.seekable || !a.Ma.Ya)
throw new N(70);
if (0 != c && 1 != c && 2 != c)
throw new N(28);
a.position = a.Ma.Ya(a, b, c);
a.uc = [];
}
function Yb(a, b, c, d, e) {
if (0 > d || 0 > e)
throw new N(28);
if (null === a.bb)
throw new N(8);
if (1 === (a.flags & 2097155))
throw new N(8);
if (P(a.node.mode))
throw new N(31);
if (!a.Ma.read)
throw new N(28);
var g = "undefined" != typeof e;
if (!g)
e = a.position;
else if (!a.seekable)
throw new N(70);
b = a.Ma.read(a, b, c, d, e);
g || (a.position += b);
return b;
}
function ma(a, b, c, d, e) {
if (0 > d || 0 > e)
throw new N(28);
if (null === a.bb)
throw new N(8);
if (0 === (a.flags & 2097155))
throw new N(8);
if (P(a.node.mode))
throw new N(31);
if (!a.Ma.write)
throw new N(28);
a.seekable && a.flags & 1024 && Xb(a, 0, 2);
var g = "undefined" != typeof e;
if (!g)
e = a.position;
else if (!a.seekable)
throw new N(70);
b = a.Ma.write(a, b, c, d, e, void 0);
g || (a.position += b);
return b;
}
function sa(a) {
var b = b || 0;
var c = "binary";
"utf8" !== c && "binary" !== c && Ja(`Invalid encoding type "${c}"`);
b = la(a, b);
a = Ub(a).size;
var d = new Uint8Array(a);
Yb(b, d, 0, a, 0);
"utf8" === c && (d = db(d));
na(b);
return d;
}
function W(a, b, c) {
var _a2;
a = ha("/dev/" + a);
var d = ia(!!b, !!c);
(_a2 = W.Rb) != null ? _a2 : W.Rb = 64;
var e = W.Rb++ << 8 | 0;
rb(e, { open(g) {
g.seekable = false;
}, close() {
var _a3;
((_a3 = c == null ? void 0 : c.buffer) == null ? void 0 : _a3.length) && c(10);
}, read(g, h, q, w) {
for (var t = 0, x = 0; x < w; x++) {
try {
var D = b();
} catch (ib) {
throw new N(29);
}
if (void 0 === D && 0 === t)
throw new N(6);
if (null === D || void 0 === D)
break;
t++;
h[q + x] = D;
}
t && (g.node.$a = Date.now());
return t;
}, write(g, h, q, w) {
for (var t = 0; t < w; t++)
try {
c(h[q + t]);
} catch (x) {
throw new N(29);
}
w && (g.node.Ua = g.node.Ta = Date.now());
return t;
} });
Rb(a, d, e);
}
var X = {};
function Y(a, b, c) {
if ("/" === b.charAt(0))
return b;
a = -100 === a ? "/" : T(a).path;
if (0 == b.length) {
if (!c)
throw new N(44);
return a;
}
return a + "/" + b;
}
function Zb(a, b) {
F[a >> 2] = b.cc;
F[a + 4 >> 2] = b.mode;
F[a + 8 >> 2] = b.rc;
F[a + 12 >> 2] = b.uid;
F[a + 16 >> 2] = b.nc;
F[a + 20 >> 2] = b.nb;
G[a + 24 >> 3] = BigInt(b.size);
E[a + 32 >> 2] = 4096;
E[a + 36 >> 2] = b.$b;
var c = b.$a.getTime(), d = b.Ua.getTime(), e = b.Ta.getTime();
G[a + 40 >> 3] = BigInt(Math.floor(c / 1e3));
F[a + 48 >> 2] = c % 1e3 * 1e6;
G[a + 56 >> 3] = BigInt(Math.floor(d / 1e3));
F[a + 64 >> 2] = d % 1e3 * 1e6;
G[a + 72 >> 3] = BigInt(Math.floor(e / 1e3));
F[a + 80 >> 2] = e % 1e3 * 1e6;
G[a + 88 >> 3] = BigInt(b.oc);
return 0;
}
var ic = void 0, Ac = () => {
var a = E[+ic >> 2];
ic += 4;
return a;
}, Cc = 0, Dc = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335], Ec = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334], Fc = {}, Gc = (a) => {
if (!(a instanceof Pa || "unwind" == a))
throw a;
}, Hc = (a) => {
var _a2;
Da = a;
Va || 0 < Cc || ((_a2 = k.onExit) == null ? void 0 : _a2.call(k, a), Ca = true);
throw new Pa(a);
}, Ic = (a) => {
if (!Ca)
try {
a();
} catch (b) {
Gc(b);
} finally {
if (!(Va || 0 < Cc))
try {
Da = a = Da, Hc(a);
} catch (b) {
Gc(b);
}
}
}, Jc = {}, Lc = () => {
var _a2, _b2;
if (!Kc) {
var a = { USER: "web_user", LOGNAME: "web_user", PATH: "/", PWD: "/", HOME: "/home/web_user", LANG: ((_b2 = (_a2 = globalThis.navigator) == null ? void 0 : _a2.language) != null ? _b2 : "C").replace("-", "_") + ".UTF-8", _: va || "./this.program" }, b;
for (b in Jc)
void 0 === Jc[b] ? delete a[b] : a[b] = Jc[b];
var c = [];
for (b in a)
c.push(`${b}=${a[b]}`);
Kc = c;
}
return Kc;
}, Kc, Mc = (a, b, c, d) => {
var e = { string: (t) => {
var x = 0;
if (null !== t && void 0 !== t && 0 !== t) {
x = gb(t) + 1;
var D = y(x);
M(t, C, D, x);
x = D;
}
return x;
}, array: (t) => {
var x = y(t.length);
m.set(t, x);
return x;
} };
a = k["_" + a];
var g = [], h = 0;
if (d)
for (var q = 0; q < d.length; q++) {
var w = e[c[q]];
w ? (0 === h && (h = oa()), g[q] = w(d[q])) : g[q] = d[q];
}
c = a(...g);
return c = function(t) {
0 !== h && qa(h);
return "string" === b ? z(t) : "boolean" === b ? !!t : t;
}(c);
}, ea = (a) => {
var b = gb(a) + 1, c = ca(b);
c && M(a, C, c, b);
return c;
}, Nc, Oc = [], A = (a) => {
Nc.delete(Z.get(a));
Z.set(a, null);
Oc.push(a);
}, Pc = (a) => {
const b = a.length;
return [b % 128 | 128, b >> 7, ...a];
}, Qc = { i: 127, p: 127, j: 126, f: 125, d: 124, e: 111 }, Rc = (a) => Pc(Array.from(a, (b) => Qc[b])), ua = (a, b) => {
if (!Nc) {
Nc = /* @__PURE__ */ new WeakMap();
var c = Z.length;
if (Nc)
for (var d = 0; d < 0 + c; d++) {
var e = Z.get(d);
e && Nc.set(e, d);
}
}
if (c = Nc.get(a) || 0)
return c;
c = Oc.length ? Oc.pop() : Z.grow(1);
try {
Z.set(c, a);
} catch (g) {
if (!(g instanceof TypeError))
throw g;
b = Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0, 1, ...Pc([1, 96, ...Rc(b.slice(1)), ...Rc("v" === b[0] ? "" : b[0])]), 2, 7, 1, 1, 101, 1, 102, 0, 0, 7, 5, 1, 1, 102, 0, 0);
b = new WebAssembly.Module(b);
b = new WebAssembly.Instance(b, { e: { f: a } }).exports.f;
Z.set(c, b);
}
Nc.set(a, c);
return c;
};
R = Array(4096);
Pb(O, "/");
U("/tmp");
U("/home");
U("/home/web_user");
(function() {
U("/dev");
rb(259, { read: () => 0, write: (d, e, g, h) => h, Ya: () => 0 });
Rb("/dev/null", 259);
qb(1280, tb);
qb(1536, ub);
Rb("/dev/tty", 1280);
Rb("/dev/tty1", 1536);
var a = new Uint8Array(1024), b = 0, c = () => {
0 === b && (bb(a), b = a.byteLength);
return a[--b];
};
W("random", c);
W("urandom", c);
U("/dev/shm");
U("/dev/shm/tmp");
})();
(function() {
U("/proc");
var a = U("/proc/self");
U("/proc/self/fd");
Pb({ ab() {
var b = wb(a, "fd", 16895, 73);
b.Ma = { Ya: O.Ma.Ya };
b.La = { mb(c, d) {
c = +d;
var e = T(c);
c = { parent: null, ab: { Sb: "fake" }, La: { eb: () => e.path }, id: c + 1 };
return c.parent = c;
}, Ib() {
return Array.from(Bb.entries()).filter(([, c]) => c).map(([c]) => c.toString());
} };
return b;
} }, "/proc/self/fd");
})();
k.noExitRuntime && (Va = k.noExitRuntime);
k.print && (Aa = k.print);
k.printErr && (B = k.printErr);
k.wasmBinary && (Ba = k.wasmBinary);
k.thisProgram && (va = k.thisProgram);
if (k.preInit)
for ("function" == typeof k.preInit && (k.preInit = [k.preInit]); 0 < k.preInit.length; )
k.preInit.shift()();
k.stackSave = () => oa();
k.stackRestore = (a) => qa(a);
k.stackAlloc = (a) => y(a);
k.cwrap = (a, b, c, d) => {
var e = !c || c.every((g) => "number" === g || "boolean" === g);
return "string" !== b && e && !d ? k["_" + a] : (...g) => Mc(a, b, c, g);
};
k.addFunction = ua;
k.removeFunction = A;
k.UTF8ToString = z;
k.stringToNewUTF8 = ea;
k.writeArrayToMemory = (a, b) => {
m.set(a, b);
};
var ca, da, yb, Sc, qa, y, oa, Ia, Z, Tc = {
a: (a, b, c, d) => Ja(`Assertion failed: ${z(a)}, at: ` + [b ? z(b) : "unknown filename", c, d ? z(d) : "unknown function"]),
i: function(a, b) {
try {
return a = z(a), ka(a, b), 0;
} catch (c) {
if ("undefined" == typeof X || "ErrnoError" !== c.name)
throw c;
return -c.Pa;
}
},
L: function(a, b, c) {
try {
b = z(b);
b = Y(a, b);
if (c & -8)
return -28;
var d = S(b, { hb: true }).node;
if (!d)
return -44;
a = "";
c & 4 && (a += "r");
c & 2 && (a += "w");
c & 1 && (a += "x");
return a && Ib(d, a) ? -2 : 0;
} catch (e) {
if ("undefined" == typeof X || "ErrnoError" !== e.name)
throw e;
return -e.Pa;
}
},
j: function(a, b) {
try {
var c = T(a);
Vb(c, c.node, b, false);
return 0;
} catch (d) {
if ("undefined" == typeof X || "ErrnoError" !== d.name)
throw d;
return -d.Pa;
}
},
h: function(a) {
try {
var b = T(a);
Ob(b, b.node, { timestamp: Date.now(), dc: false });
return 0;
} catch (c) {
if ("undefined" == typeof X || "ErrnoError" !== c.name)
throw c;
return -c.Pa;
}
},
b: function(a, b, c) {
ic = c;
try {
var d = T(a);
switch (b) {
case 0:
var e = Ac();
if (0 > e)
break;
for (; Bb[e]; )
e++;
return Nb(d, e).bb;
case 1:
case 2:
return 0;
case 3:
return d.flags;
case 4:
return e = Ac(), d.flags |= e, 0;
case 12:
return e = Ac(), Ea[e + 0 >> 1] = 2, 0;
case 13:
case 14:
return 0;
}
return -28;
} catch (g) {
if ("undefined" == typeof X || "ErrnoError" !== g.name)
throw g;
return -g.Pa;
}
},
g: function(a, b) {
try {
var c = T(a), d = c.node, e = c.Ma.Wa;
a = e ? c : d;
e != null ? e : e = d.La.Wa;
Lb(e);
var g = e(a);
return Zb(b, g);
} catch (h) {
if ("undefined" == typeof X || "ErrnoError" !== h.name)
throw h;
return -h.Pa;
}
},
H: function(a, b) {
b = -9007199254740992 > b || 9007199254740992 < b ? NaN : Number(b);
try {
if (isNaN(b))
return -61;
var c = T(a);
if (0 > b || 0 === (c.flags & 2097155))
throw new N(28);
Wb(c, c.node, b);
return 0;
} catch (d) {
if ("undefined" == typeof X || "ErrnoError" !== d.name)
throw d;
return -d.Pa;
}
},
G: function(a, b) {
try {
if (0 === b)
return -28;
var c = gb("/") + 1;
if (b < c)
return -68;
M("/", C, a, b);
return c;
} catch (d) {
if ("undefined" == typeof X || "ErrnoError" !== d.name)
throw d;
return -d.Pa;
}
},
K: function(a, b) {
try {
return a = z(a), Zb(b, Ub(a, true));
} catch (c) {
if ("undefined" == typeof X || "ErrnoError" !== c.name)
throw c;
return -c.Pa;
}
},
C: function(a, b, c) {
try {
return b = z(b), b = Y(a, b), U(b, c), 0;
} catch (d) {
if ("undefined" == typeof X || "ErrnoError" !== d.name)
throw d;
return -d.Pa;
}
},
J: function(a, b, c, d) {
try {
b = z(b);
var e = d & 256;
b = Y(a, b, d & 4096);
return Zb(c, e ? Ub(b, true) : Ub(b));
} catch (g) {
if ("undefined" == typeof X || "ErrnoError" !== g.name)
throw g;
return -g.Pa;
}
},
x: function(a, b, c, d) {
ic = d;
try {
b = z(b);
b = Y(a, b);
var e = d ? Ac() : 0;
return la(b, c, e).bb;
} catch (g) {
if ("undefined" == typeof X || "ErrnoError" !== g.name)
throw g;
return -g.Pa;
}
},
v: function(a, b, c, d) {
try {
b = z(b);
b = Y(a, b);
if (0 >= d)
return -28;
var e = S(b).node;
if (!e)
throw new N(44);
if (!e.La.eb)
throw new N(28);
var g = e.La.eb(e);
var h = Math.min(d, gb(g)), q = m[c + h];
M(g, C, c, d + 1);
m[c + h] = q;
return h;
} catch (w) {
if ("undefined" == typeof X || "ErrnoError" !== w.name)
throw w;
return -w.Pa;
}
},
u: function(a) {
try {
return a = z(a), Tb(a), 0;
} catch (b) {
if ("undefined" == typeof X || "ErrnoError" !== b.name)
throw b;
return -b.Pa;
}
},
f: function(a, b) {
try {
return a = z(a), Zb(b, Ub(a));
} catch (c) {
if ("undefined" == typeof X || "ErrnoError" !== c.name)
throw c;
return -c.Pa;
}
},
r: function(a, b, c) {
try {
b = z(b);
b = Y(a, b);
if (c)
if (512 === c)
Tb(b);
else
return -28;
else
ta(b);
return 0;
} catch (d) {
if ("undefined" == typeof X || "ErrnoError" !== d.name)
throw d;
return -d.Pa;
}
},
q: function(a, b, c) {
try {
b = z(b);
b = Y(a, b, true);
var d = Date.now(), e, g;
if (c) {
var h = F[c >> 2] + 4294967296 * E[c + 4 >> 2], q = E[c + 8 >> 2];
1073741823 == q ? e = d : 1073741822 == q ? e = null : e = 1e3 * h + q / 1e6;
c += 16;
h = F[c >> 2] + 4294967296 * E[c + 4 >> 2];
q = E[c + 8 >> 2];
1073741823 == q ? g = d : 1073741822 == q ? g = null : g = 1e3 * h + q / 1e6;
} else
g = e = d;
if (null !== (g != null ? g : e)) {
a = e;
var w = S(b, { hb: true }).node;
Lb(w.La.Xa)(w, { $a: a, Ua: g });
}
return 0;
} catch (t) {
if ("undefined" == typeof X || "ErrnoError" !== t.name)
throw t;
return -t.Pa;
}
},
m: () => Ja(""),
l: () => {
Va = false;
Cc = 0;
},
A: function(a, b) {
a = -9007199254740992 > a || 9007199254740992 < a ? NaN : Number(a);
a = new Date(1e3 * a);
E[b >> 2] = a.getSeconds();
E[b + 4 >> 2] = a.getMinutes();
E[b + 8 >> 2] = a.getHours();
E[b + 12 >> 2] = a.getDate();
E[b + 16 >> 2] = a.getMonth();
E[b + 20 >> 2] = a.getFullYear() - 1900;
E[b + 24 >> 2] = a.getDay();
var c = a.getFullYear();
E[b + 28 >> 2] = (0 !== c % 4 || 0 === c % 100 && 0 !== c % 400 ? Ec : Dc)[a.getMonth()] + a.getDate() - 1 | 0;
E[b + 36 >> 2] = -(60 * a.getTimezoneOffset());
c = new Date(a.getFullYear(), 6, 1).getTimezoneOffset();
var d = new Date(a.getFullYear(), 0, 1).getTimezoneOffset();
E[b + 32 >> 2] = (c != d && a.getTimezoneOffset() == Math.min(d, c)) | 0;
},
y: function(a, b, c, d, e, g, h) {
e = -9007199254740992 > e || 9007199254740992 < e ? NaN : Number(e);
try {
var q = T(d);
if (0 !== (b & 2) && 0 === (c & 2) && 2 !== (q.flags & 2097155))
throw new N(2);
if (1 === (q.flags & 2097155))
throw new N(2);
if (!q.Ma.sb)
throw new N(43);
if (!a)
throw new N(28);
var w = q.Ma.sb(q, a, e, b, c);
var t = w.tc;
E[g >> 2] = w.Ub;
F[h >> 2] = t;
return 0;
} catch (x) {
if ("undefined" == typeof X || "ErrnoError" !== x.name)
throw x;
return -x.Pa;
}
},
z: function(a, b, c, d, e, g) {
g = -9007199254740992 > g || 9007199254740992 < g ? NaN : Number(g);
try {
var h = T(e);
if (c & 2) {
if (32768 !== (h.node.mode & 61440))
throw new N(43);
d & 2 || h.Ma.tb && h.Ma.tb(h, C.slice(a, a + b), g, b, d);
}
} catch (q) {
if ("undefined" == typeof X || "ErrnoError" !== q.name)
throw q;
return -q.Pa;
}
},
n: (a, b) => {
Fc[a] && (clearTimeout(Fc[a].id), delete Fc[a]);
if (!b)
return 0;
var c = setTimeout(() => {
delete Fc[a];
Ic(() => Sc(a, performance.now()));
}, b);
Fc[a] = { id: c, Hc: b };
return 0;
},
B: (a, b, c, d) => {
var e = (/* @__PURE__ */ new Date()).getFullYear(), g = new Date(e, 0, 1).getTimezoneOffset();
e = new Date(e, 6, 1).getTimezoneOffset();
F[a >> 2] = 60 * Math.max(g, e);
E[b >> 2] = Number(g != e);
b = (h) => {
var q = Math.abs(h);
return `UTC${0 <= h ? "-" : "+"}${String(Math.floor(q / 60)).padStart(2, "0")}${String(q % 60).padStart(2, "0")}`;
};
a = b(g);
b = b(e);
e < g ? (M(a, C, c, 17), M(b, C, d, 17)) : (M(a, C, d, 17), M(b, C, c, 17));
},
d: () => Date.now(),
s: () => 2147483648,
c: () => performance.now(),
o: (a) => {
var b = C.length;
a >>>= 0;
if (2147483648 < a)
return false;
for (var c = 1; 4 >= c; c *= 2) {
var d = b * (1 + 0.2 / c);
d = Math.min(d, a + 100663296);
a: {
d = (Math.min(2147483648, 65536 * Math.ceil(Math.max(a, d) / 65536)) - Ia.buffer.byteLength + 65535) / 65536 | 0;
try {
Ia.grow(d);
Ha();
var e = 1;
break a;
} catch (g) {
}
e = void 0;
}
if (e)
return true;
}
return false;
},
E: (a, b) => {
var c = 0, d = 0, e;
for (e of Lc()) {
var g = b + c;
F[a + d >> 2] = g;
c += M(e, C, g, Infinity) + 1;
d += 4;
}
return 0;
},
F: (a, b) => {
var c = Lc();
F[a >> 2] = c.length;
a = 0;
for (var d of c)
a += gb(d) + 1;
F[b >> 2] = a;
return 0;
},
e: function(a) {
try {
var b = T(a);
na(b);
return 0;
} catch (c) {
if ("undefined" == typeof X || "ErrnoError" !== c.name)
throw c;
return c.Pa;
}
},
p: function(a, b) {
try {
var c = T(a);
m[b] = c.Va ? 2 : P(c.mode) ? 3 : 40960 === (c.mode & 61440) ? 7 : 4;
Ea[b + 2 >> 1] = 0;
G[b + 8 >> 3] = BigInt(0);
G[b + 16 >> 3] = BigInt(0);
return 0;
} catch (d) {
if ("undefined" == typeof X || "ErrnoError" !== d.name)
throw d;
return d.Pa;
}
},
w: function(a, b, c, d) {
try {
a: {
var e = T(a);
a = b;
for (var g, h = b = 0; h < c; h++) {
var q = F[a >> 2], w = F[a + 4 >> 2];
a += 8;
var t = Yb(e, m, q, w, g);
if (0 > t) {
var x = -1;
break a;
}
b += t;
if (t < w)
break;
"undefined" != typeof g && (g += t);
}
x = b;
}
F[d >> 2] = x;
return 0;
} catch (D) {
if ("undefined" == typeof X || "ErrnoError" !== D.name)
throw D;
return D.Pa;
}
},
D: function(a, b, c, d) {
b = -9007199254740992 > b || 9007199254740992 < b ? NaN : Number(b);
try {
if (isNaN(b))
return 61;
var e = T(a);
Xb(e, b, c);
G[d >> 3] = BigInt(e.position);
e.Eb && 0 === b && 0 === c && (e.Eb = null);
return 0;
} catch (g) {
if ("undefined" == typeof X || "ErrnoError" !== g.name)
throw g;
return g.Pa;
}
},
I: function(a) {
var _a2, _b2;
try {
var b = T(a);
return (_b2 = (_a2 = b.Ma) == null ? void 0 : _a2.lb) == null ? void 0 : _b2.call(_a2, b);
} catch (c) {
if ("undefined" == typeof X || "ErrnoError" !== c.name)
throw c;
return c.Pa;
}
},
t: function(a, b, c, d) {
try {
a: {
var e = T(a);
a = b;
for (var g, h = b = 0; h < c; h++) {
var q = F[a >> 2], w = F[a + 4 >> 2];
a += 8;
var t = ma(e, m, q, w, g);
if (0 > t) {
var x = -1;
break a;
}
b += t;
if (t < w)
break;
"undefined" != typeof g && (g += t);
}
x = b;
}
F[d >> 2] = x;
return 0;
} catch (D) {
if ("undefined" == typeof X || "ErrnoError" !== D.name)
throw D;
return D.Pa;
}
},
k: Hc
};
function Uc() {
function a() {
var _a2;
k.calledRun = true;
if (!Ca) {
if (!k.noFSInit && !Db) {
var b, c;
Db = true;
b != null ? b : b = k.stdin;
c != null ? c : c = k.stdout;
d != null ? d : d = k.stderr;
b ? W("stdin", b) : Sb("/dev/tty", "/dev/stdin");
c ? W("stdout", null, c) : Sb("/dev/tty", "/dev/stdout");
d ? W("stderr", null, d) : Sb("/dev/tty1", "/dev/stderr");
la("/dev/stdin", 0);
la("/dev/stdout", 1);
la("/dev/stderr", 1);
}
Vc.N();
Eb = false;
(_a2 = k.onRuntimeInitialized) == null ? void 0 : _a2.call(k);
if (k.postRun)
for ("function" == typeof k.postRun && (k.postRun = [k.postRun]); k.postRun.length; ) {
var d = k.postRun.shift();
Ra.push(d);
}
Qa(Ra);
}
}
if (0 < J)
Ua = Uc;
else {
if (k.preRun)
for ("function" == typeof k.preRun && (k.preRun = [k.preRun]); k.preRun.length; )
Ta();
Qa(Sa);
0 < J ? Ua = Uc : k.setStatus ? (k.setStatus("Running..."), setTimeout(() => {
setTimeout(() => k.setStatus(""), 1);
a();
}, 1)) : a();
}
}
var Vc;
(async function() {
var _a2;
function a(c) {
var _a3;
c = Vc = c.exports;
k._sqlite3_free = c.P;
k._sqlite3_value_text = c.Q;
k._sqlite3_prepare_v2 = c.R;
k._sqlite3_step = c.S;
k._sqlite3_reset = c.T;
k._sqlite3_exec = c.U;
k._sqlite3_finalize = c.V;
k._sqlite3_column_name = c.W;
k._sqlite3_column_text = c.X;
k._sqlite3_column_type = c.Y;
k._sqlite3_errmsg = c.Z;
k._sqlite3_clear_bindings = c._;
k._sqlite3_value_blob = c.$;
k._sqlite3_value_bytes = c.aa;
k._sqlite3_value_double = c.ba;
k._sqlite3_value_int = c.ca;
k._sqlite3_value_type = c.da;
k._sqlite3_result_blob = c.ea;
k._sqlite3_result_double = c.fa;
k._sqlite3_result_error = c.ga;
k._sqlite3_result_int = c.ha;
k._sqlite3_result_int64 = c.ia;
k._sqlite3_result_null = c.ja;
k._sqlite3_result_text = c.ka;
k._sqlite3_aggregate_context = c.la;
k._sqlite3_column_count = c.ma;
k._sqlite3_data_count = c.na;
k._sqlite3_column_blob = c.oa;
k._sqlite3_column_bytes = c.pa;
k._sqlite3_column_double = c.qa;
k._sqlite3_bind_blob = c.ra;
k._sqlite3_bind_double = c.sa;
k._sqlite3_bind_int = c.ta;
k._sqlite3_bind_text = c.ua;
k._sqlite3_bind_parameter_index = c.va;
k._sqlite3_sql = c.wa;
k._sqlite3_normalized_sql = c.xa;
k._sqlite3_changes = c.ya;
k._sqlite3_close_v2 = c.za;
k._sqlite3_create_function_v2 = c.Aa;
k._sqlite3_update_hook = c.Ba;
k._sqlite3_open = c.Ca;
ca = k._malloc = c.Da;
da = k._free = c.Ea;
k._RegisterExtensionFunctions = c.Fa;
yb = c.Ga;
Sc = c.Ha;
qa = c.Ia;
y = c.Ja;
oa = c.Ka;
Ia = c.M;
Z = c.O;
Ha();
J--;
(_a3 = k.monitorRunDependencies) == null ? void 0 : _a3.call(k, J);
0 == J && Ua && (c = Ua, Ua = null, c());
return Vc;
}
J++;
(_a2 = k.monitorRunDependencies) == null ? void 0 : _a2.call(k, J);
var b = { a: Tc };
if (k.instantiateWasm)
return new Promise((c) => {
k.instantiateWasm(b, (d, e) => {
c(a(d, e));
});
});
La != null ? La : La = k.locateFile ? k.locateFile("sql-wasm-browser.wasm", xa) : xa + "sql-wasm-browser.wasm";
return a((await Oa(b)).instance);
})();
Uc();
return Module;
});
return initSqlJsPromise;
};
if (typeof exports === "object" && typeof module2 === "object") {
module2.exports = initSqlJs2;
module2.exports.default = initSqlJs2;
} else if (typeof define === "function" && define["amd"]) {
define([], function() {
return initSqlJs2;
});
} else if (typeof exports === "object") {
exports["Module"] = initSqlJs2;
}
}
});
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => DictionaryPlugin
});
module.exports = __toCommonJS(main_exports);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian11 = require("obsidian");
// src/server.ts
var http = __toESM(require("http"));
var import_obsidian3 = require("obsidian");
// src/lexicon.ts
var import_obsidian2 = require("obsidian");
// src/base.ts
var import_obsidian = require("obsidian");
async function ensureDictionaryBase(app, settings) {
if (!settings.autoCreateBase)
return;
const folderPath = (0, import_obsidian.normalizePath)(settings.folder);
const baseName = (settings.baseName || "_Dictionary List").trim().replace(/[\\/:*?"<>|#^[\]]/g, "");
if (!baseName)
return;
const basePath = (0, import_obsidian.normalizePath)(`${folderPath}/${baseName}.base`);
if (app.vault.getAbstractFileByPath(basePath))
return;
const content = `filters:
and:
- file.inFolder("${folderPath}")
- file.ext != "base"
properties:
file.name:
displayName: Word
word-class:
displayName: Word class
familiarity:
displayName: Familiarity
date-added:
displayName: Date added
views:
- type: table
name: All words
order:
- file.name
- word-class
- definition
- familiarity
- source
- date-added
`;
await app.vault.create(basePath, content);
}
// src/familiarityData.ts
var COMMON_BLOB = "the\nof\nand\nto\na\nin\nfor\nis\non\nthat\nby\nthis\nwith\ni\nyou\nit\nnot\nor\nbe\nare\nfrom\nat\nas\nyour\nall\nhave\nnew\nmore\nan\nwas\nwe\nwill\nhome\ncan\nus\nabout\nif\npage\nmy\nhas\nsearch\nfree\nbut\nour\none\nother\ndo\nno\ninformation\ntime\nthey\nsite\nhe\nup\nmay\nwhat\nwhich\ntheir\nnews\nout\nuse\nany\nthere\nsee\nonly\nso\nhis\nwhen\ncontact\nhere\nbusiness\nwho\nweb\nalso\nnow\nhelp\nget\npm\nview\nonline\nc\ne\nfirst\nam\nbeen\nwould\nhow\nwere\nme\ns\nservices\nsome\nthese\nclick\nits\nlike\nservice\nx\nthan\nfind\nprice\ndate\nback\ntop\npeople\nhad\nlist\nname\njust\nover\nstate\nyear\nday\ninto\nemail\ntwo\nhealth\nn\nworld\nre\nnext\nused\ngo\nb\nwork\nlast\nmost\nproducts\nmusic\nbuy\ndata\nmake\nthem\nshould\nproduct\nsystem\npost\nher\ncity\nt\nadd\npolicy\nnumber\nsuch\nplease\navailable\ncopyright\nsupport\nmessage\nafter\nbest\nsoftware\nthen\njan\ngood\nvideo\nwell\nd\nwhere\ninfo\nrights\npublic\nbooks\nhigh\nschool\nthrough\nm\neach\nlinks\nshe\nreview\nyears\norder\nvery\nprivacy\nbook\nitems\ncompany\nr\nread\ngroup\nsex\nneed\nmany\nuser\nsaid\nde\ndoes\nset\nunder\ngeneral\nresearch\nuniversity\njanuary\nmail\nfull\nmap\nreviews\nprogram\nlife\nknow\ngames\nway\ndays\nmanagement\np\npart\ncould\ngreat\nunited\nhotel\nreal\nf\nitem\ninternational\ncenter\nebay\nmust\nstore\ntravel\ncomments\nmade\ndevelopment\nreport\noff\nmember\ndetails\nline\nterms\nbefore\nhotels\ndid\nsend\nright\ntype\nbecause\nlocal\nthose\nusing\nresults\noffice\neducation\nnational\ncar\ndesign\ntake\nposted\ninternet\naddress\ncommunity\nwithin\nstates\narea\nwant\nphone\ndvd\nshipping\nreserved\nsubject\nbetween\nforum\nfamily\nl\nlong\nbased\nw\ncode\nshow\no\neven\nblack\ncheck\nspecial\nprices\nwebsite\nindex\nbeing\nwomen\nmuch\nsign\nfile\nlink\nopen\ntoday\ntechnology\nsouth\ncase\nproject\nsame\npages\nuk\nversion\nsection\nown\nfound\nsports\nhouse\nrelated\nsecurity\nboth\ng\ncounty\namerican\nphoto\ngame\nmembers\npower\nwhile\ncare\nnetwork\ndown\ncomputer\nsystems\nthree\ntotal\nplace\nend\nfollowing\ndownload\nh\nhim\nwithout\nper\naccess\nthink\nnorth\nresources\ncurrent\nposts\nbig\nmedia\nlaw\ncontrol\nwater\nhistory\npictures\nsize\nart\npersonal\nsince\nincluding\nguide\nshop\ndirectory\nboard\nlocation\nchange\nwhite\ntext\nsmall\nrating\nrate\ngovernment\nchildren\nduring\nusa\nreturn\nstudents\nv\nshopping\naccount\ntimes\nsites\nlevel\ndigital\nprofile\nprevious\nform\nevents\nlove\nold\njohn\nmain\ncall\nhours\nimage\ndepartment\ntitle\ndescription\nnon\nk\ny\ninsurance\nanother\nwhy\nshall\nproperty\nclass\ncd\nstill\nmoney\nquality\nevery\nlisting\ncontent\ncountry\nprivate\nlittle\nvisit\nsave\ntools\nlow\nreply\ncustomer\ndecember\ncompare\nmovies\ninclude\ncollege\nvalue\narticle\nyork\nman\ncard\njobs\nprovide\nj\nfood\nsource\nauthor\ndifferent\npress\nu\nlearn\nsale\naround\nprint\ncourse\njob\ncanada\nprocess\nteen\nroom\nstock\ntraining\ntoo\ncredit\npoint\njoin\nscience\nmen\ncategories\nadvanced\nwest\nsales\nlook\nenglish\nleft\nteam\nestate\nbox\nconditions\nselect\nwindows\nphotos\ngay\nthread\nweek\ncategory\nnote\nlive\nlarge\ngallery\ntable\nregister\nhowever\njune\noctober\nnovember\nmarket\nlibrary\nreally\naction\nstart\nseries\nmodel\nfeatures\nair\nindustry\nplan\nhuman\nprovided\ntv\nyes\nrequired\nsecond\nhot\naccessories\ncost\nmovie\nforums\nmarch\nla\nseptember\nbetter\nsay\nquestions\njuly\nyahoo\ngoing\nmedical\ntest\nfriend\ncome\ndec\nserver\npc\nstudy\napplication\ncart\nstaff\narticles\nsan\nfeedback\nagain\nplay\nlooking\nissues\napril\nnever\nusers\ncomplete\nstreet\ntopic\ncomment\nfinancial\nthings\nworking\nagainst\nstandard\ntax\nperson\nbelow\nmobile\nless\ngot\nblog\nparty\npayment\nequipment\nlogin\nstudent\nlet\nprograms\noffers\nlegal\nabove\nrecent\npark\nstores\nside\nact\nproblem\nred\ngive\nmemory\nperformance\nsocial\nq\naugust\nquote\nlanguage\nstory\nsell\noptions\nexperience\nrates\ncreate\nkey\nbody\nyoung\namerica\nimportant\nfield\nfew\neast\npaper\nsingle\nii\nage\nactivities\nclub\nexample\ngirls\nadditional
var FAMILIAR_BLOB = "hills\njavascript\nfourth\nnm\nadvisor\nmn\nmarketplace\nnd\nevil\naware\nwilson\nshape\nevolution\nirish\ncertificates\nobjectives\nstations\nsuggested\ngps\nop\nremains\nacc\ngreatest\nfirms\nconcerned\neuro\noperator\nstructures\ngeneric\nencyclopedia\nusage\ncap\nink\ncharts\ncontinuing\nmixed\ncensus\ninterracial\npeak\ntn\ncompetitive\nexist\nwheel\ntransit\ndick\nsuppliers\nsalt\ncompact\npoetry\nlights\ntracking\nangel\nbell\nkeeping\npreparation\nattempt\nreceiving\nmatches\naccordance\nwidth\nnoise\nengines\nforget\narray\ndiscussed\naccurate\nstephen\nelizabeth\nclimate\nreservations\npin\nplaystation\nalcohol\ngreek\ninstruction\nmanaging\nannotation\nsister\nraw\ndifferences\nwalking\nexplain\nsmaller\nnewest\nestablish\ngnu\nhappened\nexpressed\njeff\nextent\nsharp\nlesbians\nben\nlane\nparagraph\nkill\nmathematics\naol\ncompensation\nce\nexport\nmanagers\naircraft\nmodules\nsweden\nconflict\nconducted\nversions\nemployer\noccur\npercentage\nknows\nmississippi\ndescribe\nconcern\nbackup\nrequested\ncitizens\nconnecticut\nheritage\npersonals\nimmediate\nholding\ntrouble\nspread\ncoach\nkevin\nagricultural\nexpand\nsupporting\naudience\nassigned\njordan\ncollections\nages\nparticipate\nplug\nspecialist\ncook\naffect\nvirgin\nexperienced\ninvestigation\nraised\nhat\ninstitution\ndirected\ndealers\nsearching\nsporting\nhelping\nperl\naffected\nlib\nbike\ntotally\nplate\nexpenses\nindicate\nblonde\nab\nproceedings\nfavourite\ntransmission\nanderson\nutc\ncharacteristics\nder\nlose\norganic\nseek\nexperiences\nalbums\ncheats\nextremely\nverzeichnis\ncontracts\nguests\nhosted\ndiseases\nconcerning\ndevelopers\nequivalent\nchemistry\ntony\nneighborhood\nnevada\nkits\nthailand\nvariables\nagenda\nanyway\ncontinues\ntracks\nadvisory\ncam\ncurriculum\nlogic\ntemplate\nprince\ncircle\nsoil\ngrants\nanywhere\npsychology\nresponses\natlantic\nwet\ncircumstances\nedward\ninvestor\nidentification\nram\nleaving\nwildlife\nappliances\nmatt\nelementary\ncooking\nspeaking\nsponsors\nfox\nunlimited\nrespond\nsizes\nplain\nexit\nentered\niran\narm\nkeys\nlaunch\nwave\nchecking\ncosta\nbelgium\nprintable\nholy\nacts\nguidance\nmesh\ntrail\nenforcement\nsymbol\ncrafts\nhighway\nbuddy\nhardcover\nobserved\ndean\nsetup\npoll\nbooking\nglossary\nfiscal\ncelebrity\nstyles\ndenver\nunix\nfilled\nbond\nchannels\nericsson\nappendix\nnotify\nblues\nchocolate\npub\nportion\nscope\nhampshire\nsupplier\ncables\ncotton\nbluetooth\ncontrolled\nrequirement\nauthorities\nbiology\ndental\nkilled\nborder\nancient\ndebate\nrepresentatives\nstarts\npregnancy\ncauses\narkansas\nbiography\nleisure\nattractions\nlearned\ntransactions\nnotebook\nexplorer\nhistoric\nattached\nopened\ntm\nhusband\ndisabled\nauthorized\ncrazy\nupcoming\nbritain\nconcert\nretirement\nscores\nfinancing\nefficiency\nsp\ncomedy\nadopted\nefficient\nweblog\nlinear\ncommitment\nspecialty\nbears\njean\nhop\ncarrier\nedited\nconstant\nvisa\nmouth\njewish\nmeter\nlinked\nportland\ninterviews\nconcepts\nnh\ngun\nreflect\npure\ndeliver\nwonder\nhell\nlessons\nfruit\nbegins\nqualified\nreform\nlens\nalerts\ntreated\ndiscovery\ndraw\nmysql\nclassified\nrelating\nassume\nconfidence\nalliance\nfm\nconfirm\nwarm\nneither\nlewis\nhoward\noffline\nleaves\nengineer\nlifestyle\nconsistent\nreplace\nclearance\nconnections\ninventory\nconverter\nsuck\norganisation\nbabe\nchecks\nreached\nbecoming\nblowjob\nsafari\nobjective\nindicated\nsugar\ncrew\nlegs\nsam\nstick\nsecurities\nallen\npdt\nrelation\nenabled\ngenre\nslide\nmontana\nvolunteer\ntested\nrear\ndemocratic\nenhance\nswitzerland\nexact\nbound\nparameter\nadapter\nprocessor\nnode\nformal\ndimensions\ncontribute\nlock\nhockey\nstorm\nmicro\ncolleges\nlaptops\nmile\nshowed\nchallenges\neditors\nmens\nthreads\nbowl\nsupreme\nbrothers\nrecognition\npresents\nref\ntank\nsubmission\ndolls\nestimate\nencourage\nnavy\nkid\nregulatory\ninspection\nconsumers\ncancel\nlimits\nterritory\ntransaction\nmanchester\nweapons\npaint\ndelay\npilot\noutlet\ncontributions\ncontinuous\ndb\nczech\nresulting\ncambridge\ninitiative\nnovel\
var COMMON_WORDS = new Set(COMMON_BLOB.split("\n"));
var FAMILIAR_WORDS = new Set(FAMILIAR_BLOB.split("\n"));
// src/familiarity.ts
function classifyFamiliarity(word) {
const normalized = word.trim().toLowerCase();
if (!normalized)
return "obscure";
for (const candidate of candidateForms(normalized)) {
if (COMMON_WORDS.has(candidate))
return "common";
if (FAMILIAR_WORDS.has(candidate))
return "familiar";
}
return "obscure";
}
function candidateForms(word) {
const forms = [word];
if (word.length > 3 && word.endsWith("s"))
forms.push(word.slice(0, -1));
if (word.length > 4 && word.endsWith("es"))
forms.push(word.slice(0, -2));
if (word.length > 4 && word.endsWith("ed")) {
forms.push(word.slice(0, -1));
forms.push(word.slice(0, -2));
}
if (word.length > 5 && word.endsWith("ing")) {
forms.push(word.slice(0, -3));
forms.push(word.slice(0, -3) + "e");
}
if (word.length > 4 && word.endsWith("ly"))
forms.push(word.slice(0, -2));
return forms;
}
// src/template.ts
var FRONTMATTER_PATTERN = /^(---\n)([\s\S]*?)(\n---\n?)([\s\S]*)$/;
var PLACEHOLDER_PATTERN = /\{\{(\w+)\}\}/g;
var EMPTY_LABEL_LINE = /^\s*\*\*[^*]+:\*\*\s*$/;
var TRIPLE_NEWLINE = /\n{3,}/g;
function renderTemplate(template, values, includeFrontmatter = true) {
const fmMatch = FRONTMATTER_PATTERN.exec(template);
let output;
if (fmMatch) {
const [, fmStart, fmContent, fmEnd, body] = fmMatch;
const renderedFm = substitute(fmContent, values, escapeYamlDouble);
const renderedBody = substitute(body, values, (s) => s);
output = includeFrontmatter ? fmStart + renderedFm + fmEnd + renderedBody : renderedBody;
} else {
output = substitute(template, values, (s) => s);
}
return stripEmptyLabelLines(output);
}
function substitute(text, values, transform) {
return text.replace(PLACEHOLDER_PATTERN, (match, key) => {
if (Object.prototype.hasOwnProperty.call(values, key)) {
return transform(values[key]);
}
return match;
});
}
function escapeYamlDouble(value) {
return (value != null ? value : "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
}
function stripEmptyLabelLines(content) {
return content.split("\n").filter((line) => !EMPTY_LABEL_LINE.test(line)).join("\n").replace(TRIPLE_NEWLINE, "\n\n");
}
// src/lexicon.ts
function createStubEntry(word, source = "") {
return {
word: word.trim(),
partOfSpeech: "",
definition: "",
example: "",
phonetic: "",
source
};
}
function wordNotePath(settings, word) {
let filename = word.trim();
if (settings.namingConvention === "lowercase") {
filename = filename.toLowerCase();
} else if (settings.namingConvention === "titlecase") {
filename = filename.charAt(0).toUpperCase() + filename.slice(1).toLowerCase();
}
filename = filename.replace(/[\\/:*?"<>|#^[\]]/g, "");
const folderPath = (0, import_obsidian2.normalizePath)(settings.folder);
return (0, import_obsidian2.normalizePath)(`${folderPath}/${filename}.md`);
}
function wordNoteExists(app, settings, word) {
return app.vault.getAbstractFileByPath(wordNotePath(settings, word)) instanceof import_obsidian2.TFile;
}
async function createWordNote(app, settings, entry) {
const { vault } = app;
const filePath = wordNotePath(settings, entry.word);
const folderPath = (0, import_obsidian2.normalizePath)(settings.folder);
await ensureFolder(app, folderPath);
const existing = vault.getAbstractFileByPath(filePath);
if (existing instanceof import_obsidian2.TFile) {
if (settings.duplicateHandling === "skip") {
await ensureDictionaryBase(app, settings);
return { created: false, path: filePath, action: "skipped" };
}
if (settings.duplicateHandling === "append") {
const current = await vault.read(existing);
const addition = "\n\n---\n\n" + renderEntry(entry, settings, false);
await vault.modify(existing, current + addition);
await ensureDictionaryBase(app, settings);
return { created: false, path: filePath, action: "appended" };
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await app.fileManager.trashFile(existing);
}
await vault.create(filePath, renderEntry(entry, settings, true));
await ensureDictionaryBase(app, settings);
const action = existing ? "overwritten" : "created";
return { created: true, path: filePath, action };
}
async function ensureFolder(app, folderPath) {
const parts = folderPath.split("/").filter(Boolean);
let current = "";
for (const part of parts) {
current = current ? `${current}/${part}` : part;
const existing = app.vault.getAbstractFileByPath(current);
if (!existing) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await app.vault.adapter.mkdir(current);
} else if (!(existing instanceof import_obsidian2.TFolder)) {
throw new Error(`"${current}" exists but is not a folder.`);
}
}
}
function renderEntry(entry, settings, includeFrontmatter) {
var _a, _b, _c, _d;
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const values = {
word: entry.word,
partOfSpeech: (_a = entry.partOfSpeech) != null ? _a : "",
definition: entry.definition,
example: (_b = entry.example) != null ? _b : "",
phonetic: (_c = entry.phonetic) != null ? _c : "",
date: today,
source: (_d = entry.source) != null ? _d : "",
familiarity: classifyFamiliarity(entry.word)
};
return renderTemplate(settings.wordTemplate, values, includeFrontmatter);
}
// src/server.ts
var DictionaryServer = class {
constructor(app, getSettings, log) {
this.server = null;
this.app = app;
this.getSettings = getSettings;
this.log = log;
}
start(port) {
return new Promise((resolve, reject) => {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res).catch((err) => {
this.log(`Unhandled request error: ${err}`);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal server error" }));
}
});
});
this.server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
reject(new Error(`Port ${port} is already in use. Change the port in plugin settings.`));
} else {
reject(err);
}
});
this.server.listen(port, "127.0.0.1", () => {
this.log(`Listening on port ${port}`);
resolve();
});
});
}
stop() {
return new Promise((resolve) => {
if (!this.server) {
resolve();
return;
}
this.server.close(() => {
this.server = null;
this.log("Server stopped");
resolve();
});
});
}
isRunning() {
return this.server !== null && this.server.listening;
}
setCorsHeaders(res) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
async handleRequest(req, res) {
var _a, _b;
this.setCorsHeaders(res);
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
if (req.method === "GET" && req.url === "/health") {
const settings2 = this.getSettings();
const authRequired = Boolean(settings2.apiToken);
let authValid = true;
if (authRequired) {
authValid = req.headers["authorization"] === `Bearer ${settings2.apiToken}`;
}
res.writeHead(authValid ? 200 : 401, { "Content-Type": "application/json" });
res.end(JSON.stringify({
status: authValid ? "ok" : "unauthorized",
plugin: "lexophile",
authRequired
}));
return;
}
if (req.method !== "POST" || req.url !== "/word") {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
return;
}
const settings = this.getSettings();
if (settings.apiToken) {
const auth = req.headers["authorization"];
if (auth !== `Bearer ${settings.apiToken}`) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
}
const body = await this.readBody(req);
let entry;
try {
entry = JSON.parse(body);
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON" }));
return;
}
if (!((_a = entry.word) == null ? void 0 : _a.trim()) || !((_b = entry.definition) == null ? void 0 : _b.trim())) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "word and definition are required" }));
return;
}
const result = await createWordNote(this.app, settings, entry);
this.log(`${result.action}: "${entry.word}" \u2192 ${result.path}`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result));
}
readBody(req) {
return new Promise((resolve, reject) => {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
};
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
// src/dictionaryStore.ts
var import_obsidian4 = require("obsidian");
// src/sqlite.ts
var import_sql = __toESM(require_sql_wasm_browser());
// node_modules/sql.js/dist/sql-wasm.wasm
var sql_wasm_default = __toBinary("AGFzbQEAAAABnwRFYAJ/fwF/YAF/AX9gA39/fwBgA39/fwF/YAF/AGACf38AYAR/f39/AX9gBX9/f39/AX9gBH9/f38AYAR/f39+AX9gBn9/f39/fwF/YAAAYAJ/fgF/YAV/f39/fwBgBn9/f39/fwBgB39/f39/f38Bf2AAAX9gAXwBfGABfgF/YAJ/fgBgBH9+f38Bf2ABfwF+YAN/f34Bf2ACf38BfmACf3wAYAl/f39/f39/f38Bf2AIf39/f39/f38Bf2ADf35/AX9gB39/f39/f38AYAl/f39/f39/f38AYAV/f39/fgBgBn9/f39/fgF/YAV/f35/fwF/YAN/f34AYAN/fn8AYAN/fn4Bf2AAAXxgAX8BfGAIf39/f39/f38AYAR/f35/AGALf39/f39/f39/f38AYAN8fH8BfGACfHwBfGABfgF+YAZ/f39+f38Bf2ACf34BfmACf3wBf2AHf39/f35/fwF/YAJ+fwBgA398fABgCH9+fn9/f39/AX9gAn58AX9gAnx/AXxgAXwBf2ALf39/f39/f39/f38Bf2ACfH8Bf2AHf39/f35/fwF+YAl/fn9+fn5+f38Bf2AEf39/fgBgBX9+f39/AX9gCn9/f39/f39/f38Bf2ACfH4Bf2ADf398AX9gB39/f39/fHwAYAV/fn5+fwF/YAh/f35+fn9/fwF/YAR/fn9/AGANf39/f39/f39/f39/fwBgAn9/AXwC5QEmAWEBYQAIAWEBYgADAWEBYwAkAWEBZAAkAWEBZQABAWEBZgAAAWEBZwAAAWEBaAADAWEBaQAAAWEBagAAAWEBawAEAWEBbAALAWEBbQALAWEBbgAuAWEBbwABAWEBcAAAAWEBcQAGAWEBcgADAWEBcwAQAWEBdAAGAWEBdQABAWEBdgAGAWEBdwAGAWEBeAAGAWEBeQAvAWEBegAfAWEBQQAwAWEBQgAIAWEBQwADAWEBRAAUAWEBRQAAAWEBRgAAAWEBRwAAAWEBSAAMAWEBSQABAWEBSgAGAWEBSwAAAWEBTAAGA9oO2A4GBAcCAQUDAQAFAQUKAQ8GBQMCFQMMCAEFAQUCEgEBAwEBAAAWJQQAAAwCAwQBBgAGAwAEGAAEAQUFBAAIBwATAwISBgYABQEBAgEMAAMDAAAFAQQIBhMFAwACAAIMAgMAAgMAAAMCAAQFAQEBAgQBFwADBAMAAgIWDAEBAgADBBkFBgMGCBITIAQNDQAADQUGFgUAAAYNBgEFEgwIAAAAABICCAUDAQUCAQMAAgcDBQECBTEHBQgBEAgFBQEIBgMDAgQABQQBAQQIBBIGFwgDAAYPAAEACAENAgIEFxcAAwEaDQADAwwFBAcbBAAABgUGBAAEBAMFAwQDAwQDAAENAyYDAQUGAQACAgEBBAAEDQMEAQMFAwYHJwEDAxQHAAQGAQICBQQBAAAEAAECCAIAAAgGAgQEAAUEBQAAAAICAQEEBAAFCAEUBAAGDAMFAAAAAQIEAwUGBwgBAAMCFAMSAwQGBQIAAwQDBAUEBQAAAAUBAAUDAgMGBgACAQAAAwEDBQoEGwgAGQQYBQQyAwEDAQcAHQAHAQ4IAgICBg4HCAAKKA0FDQcABgYFMwMIFgMABQQIBgYBBQUGAAEBCggHAAAFAQUDAQANAQAFAREpKgERNDURBAQBBAIFBgwBAAMDBgAEAQUFAAEEBAMHAgAFCAUCABwAAgIGAwAFBQ8ABgUIBQEFBAAGCAIABQMHBQUBAAMIBwEBAwUMCAQVBwQHAAQDBAMEAQQEAQwAAAA2GhUBBwYBBAIABAQCAQAFDgUBBQMFAQYDBgEFHgMEAAEAAxwGAxUEAQEECAMFAQQAAggABgAEAQUBEwUFAgAEAwUXAQ8ECgoCAA0IBAcBCh0GAwAIGgAFBQUADgADBCgFGgMCBgYBBQIFCAUAAQMOAgUFAwMIAwYCDgIOAwIAAAcFAA8NHAIGBQQBAAUFAAEDAQITDwMFBAABAwUBAAAKAQADAwEGJxIEAAEJAAAEAwUFCgMABAMAAwADAAQFBAQMBAMDAAQEBAQGAAQFAQEMGQcAAgIACAcEAyssAQUFAQQCAQY3BRERBREEBAEEAgcABAIDODkGBgAMBAAEAAUBBwkAAgQAAQQPBAQBAQEBAAAEBAUEBAMDBAEBBQAABgEFBAAAAw0HBQENAAYABgQNAAYCAAAXFgMcCAwOLAUGAgUBBg8NFQgIAAYADQgBAg0DCAUNBQQABQIGHQ4PAgUIAQgaBg0EAQUIBQUABwEABQABBQAABgQBBAIBBAQDAwICDQgFDQUIAwUFAAQCAwIGBggCBQIHAggCAwUcAgIDAgMFBAADAAIEBAwDAQQIAgMACQoAOgADBgYCAQMABQIEAQQAIAAGFQYGBAwAFQADBgAAAAYBAAEGAwYAAQgABBMBAQIBAQAFAQAXAQEDBAEABQQEBQcEBAEBBgMBAQEBAQABDgIAAAAgAAUEAwQBAAcACQIBAQECEw0JBgUHAQQBBwEFAQUABAA7BAAEAQIMFAQABQEDAQABCgc8BwADBAUABQUEAQMDAQ8DBAYGBQEHARsABQEEPQUDAwc+ACsAAAQIGAIBCwAAPwUYAREpEQMBBQADLQABESoAAgULBAIBBQEFBAABAwUBBgEABiEKCgYKCAQjFgAIAwdAAgcHIQMPAwcHAAIEAQAGBAEDAAQNAgMGAAABAQYODQAHLQ8BAAEBAQAGCAwAFgEDAEEAAwAGGQQAAQYAAQYCAAYCAQUABAEEAQIFAQAEAwoABQUIBQAAGAACBAICAgACBQgIBhMNBQEFDg0FACYdDgYGBwIHBQAOAAAABUIGBQABBgEFBAINCAUNDQoCBQMCAgIHHgEIBQUjDCMAAwIAAAMABgIGDQMHAAQAAQUAAwAFAgIFAgABAwYABAoACAIEAAMCAAUCBQgBCAEFBQUCAwAOQwAOAAUAAAAFAAAFAAAABQAAAAADAwAAAAMBBwMBAAAKBgoKBgoDBAYDAwQHAAIAAAIAAgAJCQADAAMFAwIABgYHBgYEGxQBAwAAAAwJCQEAAAAAAQABEAADGxQABgcBAQMAAAAADAkJAQAHBwAGAwIZAAEBAAEBAEQBAQQlBAQDAQABAwMBAB8AABAADAADAwABABABBCIEBAQiBAAAAgQEAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQAAAwEBBwEBAAoAAwEHAQABAAoDAwMDAwQDAgIDAwIDAgcBAAAAAAcBAQEGAAMBBwEAAQAKCgEKAwoDAAAABgYAAwEHAQABAAoKBgMBAQQCAQQFCAIDAQUDBAEBAQAEAQQEAgIEBAICAgICAgICAgICAgICAgICAgICAgQCBAICBAQCAgQCBAQEAgQCBAIAAgICAAACAAACAgIEBAICBAIEBAIEAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEBAICAgICAgICBAICAgAAAAACAAAFAAAAAAAAAAAABQAAAAAFAAAAAAAAAAUABQIFCwQFAXAA5wMFBwEB0gKAgAIGCQF/AUGgy8QCCwegAjUBTQIAAU4A/Q4BTwEAAVAAJwFRAC0BUgD9AgFTAEQBVAA9AVUA0wEBVgCVAQFXAPMOAVgAhAIBWQD1AgFaAMwCAV8AxwwBJAChAQJhYQBGAmJhAL4MAmNhAOEEAmRhAFMCZWEA3AECZmEAWgJnYQBQAmhhAHcCaWEAZQJqYQBZAmthADwCbGEASQJtYQC8DAJuYQC7DAJvYQD7AQJwYQDDAQJxYQC6DAJyYQC9AgJzYQCvCQJ0YQCYAQJ1YQCuCQJ2YQC5DAJ3YQC4DAJ4YQC3DAJ5YQC1DAJ6YQC0DAJBYQCyDAJCYQCwDAJDYQCuDAJEYQDsAQJFYQDrAQJGYQCKDQJHYQDNCQJIYQC+CQJJYQDXDAJKYQDWDAJLYQDVDAnNBwEAQQEL5gMnvQzsC60IggeEDIMM3gTnAp8J0wvSC9ALsQyNCa0MrAyvDLYMhQz3CIkE9Avz
// src/sqlite.ts
var sqlJsPromise = null;
function getSqlJs() {
if (!sqlJsPromise) {
sqlJsPromise = (0, import_sql.default)({ wasmBinary: sql_wasm_default });
}
return sqlJsPromise;
}
// src/dictionaryStore.ts
var DB_FILENAME = "dictionary.sqlite";
var DEFAULT_DICTIONARY_URL = "https://github.com/bryanmanio/obsidian-lexophile/releases/download/dictionary-v1/dictionary.sqlite";
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
var DictionaryNotReadyError = class extends Error {
constructor() {
super("Local dictionary not loaded. Download it from Settings \u2192 Lexophile.");
this.name = "DictionaryNotReadyError";
}
};
var DictionaryStore = class {
constructor(app, plugin) {
this.app = app;
this.plugin = plugin;
this.db = null;
this.entryCount = null;
}
// Resolves vault-relative path to the SQLite, e.g.
// ".obsidian/plugins/lexophile/dictionary.sqlite".
dbPath() {
const configDir = this.app.vault.configDir;
const id = this.plugin.manifest.id;
return `${configDir}/plugins/${id}/${DB_FILENAME}`;
}
// Attempts to load the file from disk. Safe to call repeatedly; subsequent
// calls are no-ops once loaded. Returns true if the dictionary is ready.
async init() {
if (this.db)
return true;
const path = this.dbPath();
const exists = await this.app.vault.adapter.exists(path);
if (!exists)
return false;
const buf = await this.app.vault.adapter.readBinary(path);
const SQL = await getSqlJs();
this.db = new SQL.Database(new Uint8Array(buf));
this.entryCount = this.countEntries();
return true;
}
isReady() {
return this.db !== null;
}
async status() {
var _a;
const path = this.dbPath();
const exists = await this.app.vault.adapter.exists(path);
let sizeBytes = null;
if (exists) {
const stat = await this.app.vault.adapter.stat(path);
sizeBytes = (_a = stat == null ? void 0 : stat.size) != null ? _a : null;
}
return { ready: this.isReady(), sizeBytes, entryCount: this.entryCount };
}
// Downloads the dictionary SQLite from the given URL into the plugin
// folder, then loads it. Caller is responsible for surfacing progress —
// requestUrl doesn't expose chunk events, so we report start and end only.
async download(url) {
const res = await (0, import_obsidian4.requestUrl)({ url, method: "GET", throw: false });
if (res.status !== 200) {
throw new Error(`Download failed: HTTP ${res.status}`);
}
const buf = res.arrayBuffer;
if (buf.byteLength < 16 || !this.looksLikeSqlite(new Uint8Array(buf))) {
throw new Error("Downloaded file does not look like a SQLite database.");
}
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const path = this.dbPath();
const parent = path.substring(0, path.lastIndexOf("/"));
if (parent && !await this.app.vault.adapter.exists(parent)) {
await this.app.vault.adapter.mkdir(parent);
}
await this.app.vault.adapter.writeBinary(path, buf);
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.close();
await this.init();
}
// Looks up a word case-insensitively. Returns null if not found. The
// caller (lookupWord in dictionary.ts) translates null into
// WordNotFoundError so existing call sites keep working.
lookup(word) {
var _a, _b, _c, _d, _e;
if (!this.db)
throw new DictionaryNotReadyError();
const stmt = this.db.prepare(
"SELECT word, part_of_speech, definition, example, phonetic FROM entries WHERE LOWER(word) = LOWER(?) LIMIT 1"
);
try {
stmt.bind([word.trim()]);
if (!stmt.step())
return null;
const row = stmt.getAsObject();
return {
word: String((_a = row.word) != null ? _a : word),
partOfSpeech: (_b = row.part_of_speech) != null ? _b : "",
definition: String((_c = row.definition) != null ? _c : ""),
example: (_d = row.example) != null ? _d : "",
phonetic: (_e = row.phonetic) != null ? _e : "",
source: ""
};
} finally {
stmt.free();
}
}
close() {
if (this.db) {
this.db.close();
this.db = null;
this.entryCount = null;
}
}
countEntries() {
var _a, _b, _c, _d;
if (!this.db)
return null;
const res = this.db.exec("SELECT COUNT(*) FROM entries");
return Number((_d = (_c = (_b = (_a = res == null ? void 0 : res[0]) == null ? void 0 : _a.values) == null ? void 0 : _b[0]) == null ? void 0 : _c[0]) != null ? _d : 0);
}
looksLikeSqlite(buf) {
const header = new TextDecoder().decode(buf.slice(0, 15));
return header === "SQLite format 3";
}
};
// src/wordModal.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian6 = require("obsidian");
// src/dictionary.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian5 = require("obsidian");
var WordNotFoundError = class extends Error {
constructor(word) {
super(`No definition found for "${word}".`);
this.word = word;
this.name = "WordNotFoundError";
}
};
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
async function lookupWord(store, word, source) {
if (source === "api")
return lookupWordViaApi(word);
return lookupWordViaLocal(store, word);
}
function lookupWordViaLocal(store, word) {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
const result = store.lookup(word);
if (!result)
throw new WordNotFoundError(word);
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
return result;
}
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var API_BASE = "https://api.dictionaryapi.dev/api/v2/entries/en/";
var MAX_RETRIES = 4;
var BASE_BACKOFF_MS = 1500;
async function lookupWordViaApi(word) {
var _a, _b, _c, _d, _e, _f, _g;
const url = API_BASE + encodeURIComponent(word.trim());
let res = await (0, import_obsidian5.requestUrl)({ url, method: "GET", throw: false });
let attempt = 0;
while ((res.status === 429 || res.status === 503) && attempt < MAX_RETRIES) {
const delay = BASE_BACKOFF_MS * Math.pow(2, attempt);
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delay));
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
attempt++;
res = await (0, import_obsidian5.requestUrl)({ url, method: "GET", throw: false });
}
if (res.status === 404) {
throw new WordNotFoundError(word);
}
if (res.status === 429) {
throw new Error(`Rate-limited by Dictionary API after ${attempt} retries. Try a smaller batch or wait a minute.`);
}
if (res.status !== 200) {
throw new Error(`Dictionary API returned status ${res.status}.`);
}
const data = res.json;
if (!Array.isArray(data) || data.length === 0) {
throw new WordNotFoundError(word);
}
const first = data[0];
const meaning = (_a = first.meanings) == null ? void 0 : _a[0];
const definition = (_b = meaning == null ? void 0 : meaning.definitions) == null ? void 0 : _b[0];
if (!meaning || !definition) {
throw new WordNotFoundError(word);
}
const phonetic = (_f = (_e = first.phonetic) != null ? _e : (_d = (_c = first.phonetics) == null ? void 0 : _c.find((p) => p.text)) == null ? void 0 : _d.text) != null ? _f : "";
return {
word: first.word,
partOfSpeech: meaning.partOfSpeech,
definition: definition.definition,
example: (_g = definition.example) != null ? _g : "",
phonetic,
source: ""
};
}
// src/wordModal.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var AddWordModal = class extends import_obsidian6.Modal {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
constructor(app, getSettings, store) {
super(app);
this.word = "";
this.submitting = false;
this.inputEl = null;
this.submitBtn = null;
this.getSettings = getSettings;
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.store = store;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h3", { text: "Add word to lexicon" });
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian6.Setting(contentEl).setName("Word").setDesc("Enter or paste a word to look up in the dictionary.").addText((text) => {
this.inputEl = text.inputEl;
text.setPlaceholder("serendipity");
text.onChange((v) => this.word = v);
text.inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
void this.submit();
}
});
});
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian6.Setting(contentEl).addButton((btn) => {
this.submitBtn = btn.buttonEl;
btn.setButtonText("Look up & save").setCta().onClick(() => void this.submit());
});
window.setTimeout(() => {
var _a;
return (_a = this.inputEl) == null ? void 0 : _a.focus();
}, 0);
}
async submit() {
if (this.submitting)
return;
const word = this.word.trim();
if (!word) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian6.Notice("Lexophile: please enter a word.");
return;
}
this.submitting = true;
if (this.submitBtn) {
this.submitBtn.disabled = true;
this.submitBtn.textContent = "Looking up\u2026";
}
const settings = this.getSettings();
try {
let entry;
let stubbed = false;
try {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
entry = await lookupWord(this.store, word, settings.dictionarySource);
} catch (err) {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
if (err instanceof DictionaryNotReadyError) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian6.Notice("Lexophile: download the local dictionary in Settings \u2192 Lexophile first.");
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.submitting = false;
if (this.submitBtn) {
this.submitBtn.disabled = false;
this.submitBtn.textContent = "Look up & save";
}
return;
}
if (err instanceof WordNotFoundError && settings.stubUnfoundWords) {
entry = createStubEntry(word);
stubbed = true;
} else {
throw err;
}
}
entry.source = "manual";
const result = await createWordNote(this.app, settings, entry);
const label = stubbed ? `stub saved for "${entry.word}"` : `${result.action} "${entry.word}"`;
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian6.Notice(`Lexophile: ${label}`);
this.close();
} catch (err) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian6.Notice(`Lexophile: ${err.message}`);
this.submitting = false;
if (this.submitBtn) {
this.submitBtn.disabled = false;
this.submitBtn.textContent = "Look up & save";
}
}
}
onClose() {
this.contentEl.empty();
}
};
// src/koboImportModal.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian8 = require("obsidian");
// src/kobo.ts
var import_fs = require("fs");
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
var TRIM_CHARS = /^[\s.,;:!?'"`‘’“”()[\]{}<>—–-]+|[\s.,;:!?'"`‘’“”()[\]{}<>—–-]+$/g;
function cleanKoboWord(text) {
if (!text)
return "";
return text.replace(TRIM_CHARS, "").trim();
}
async function fileExists(filePath) {
try {
const stat = await import_fs.promises.stat(filePath);
return stat.isFile();
} catch (e) {
return false;
}
}
async function readKoboWords(filePath) {
var _a;
let buf;
try {
buf = await import_fs.promises.readFile(filePath);
} catch (err) {
const code = err.code;
if (code === "ENOENT") {
throw new Error("File not found. Is your Kobo plugged in and mounted?");
}
if (code === "EACCES") {
throw new Error("Permission denied reading the database file.");
}
throw new Error(`Could not read file: ${err.message}`);
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
if (buf.length < 16 || buf.subarray(0, 15).toString("utf8") !== "SQLite format 3") {
throw new Error("That file is not a SQLite database.");
}
const SQL = await getSqlJs();
const db = new SQL.Database(new Uint8Array(buf));
try {
const stmt = db.prepare(`
SELECT
wl.Text AS word,
wl.DictSuffix AS lang,
wl.DateCreated AS dateCreated,
c.Title AS bookTitle
FROM WordList wl
LEFT JOIN content c ON wl.VolumeId = c.ContentID
WHERE wl.DictSuffix = '-en' OR wl.DictSuffix IS NULL
ORDER BY wl.DateCreated DESC
`);
const rows = [];
while (stmt.step()) {
const row = stmt.getAsObject();
const cleaned = cleanKoboWord(String((_a = row.word) != null ? _a : ""));
if (cleaned) {
rows.push({
word: cleaned,
bookTitle: row.bookTitle ? String(row.bookTitle) : null,
dateCreated: row.dateCreated ? String(row.dateCreated) : ""
});
}
}
stmt.free();
return rows;
} catch (err) {
const msg = err.message;
if (/no such table/i.test(msg)) {
throw new Error("This SQLite file does not look like a Kobo database (no WordList table).");
}
throw err;
} finally {
db.close();
}
}
// src/books.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian7 = require("obsidian");
var LOWERCASE_WORDS = /* @__PURE__ */ new Set([
"a",
"an",
"and",
"as",
"at",
"but",
"by",
"for",
"from",
"in",
"into",
"nor",
"of",
"on",
"onto",
"or",
"over",
"so",
"the",
"to",
"up",
"with",
"yet"
]);
function titleCase(input) {
const trimmed = (input || "").trim();
if (!trimmed)
return "";
const words = trimmed.split(/\s+/);
return words.map((word, i) => {
const lower = word.toLowerCase();
const isFirst = i === 0;
const isLast = i === words.length - 1;
if (!isFirst && !isLast && LOWERCASE_WORDS.has(lower))
return lower;
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}).join(" ");
}
function cleanBookTitle(title) {
return (title || "").replace(/[\\/:*?"<>|#^[\]]/g, "").trim();
}
function bookNoteExists(app, folder, title) {
const cleaned = cleanBookTitle(title);
if (!cleaned)
return false;
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const filePath = (0, import_obsidian7.normalizePath)(`${(0, import_obsidian7.normalizePath)(folder)}/${cleaned}.md`);
return app.vault.getAbstractFileByPath(filePath) instanceof import_obsidian7.TFile;
}
async function ensureBookStub(app, folder, title, template) {
const cleaned = cleanBookTitle(title);
if (!cleaned)
return;
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const folderPath = (0, import_obsidian7.normalizePath)(folder);
const filePath = (0, import_obsidian7.normalizePath)(`${folderPath}/${cleaned}.md`);
if (app.vault.getAbstractFileByPath(filePath))
return;
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const content = renderTemplate(template, { title: cleaned, date: today });
await app.vault.create(filePath, content);
}
// src/koboImportModal.ts
var DEFAULT_KOBO_PATH = "/Volumes/KOBOeReader/.kobo/KoboReader.sqlite";
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var API_DELAY_MS = 350;
var LOCAL_YIELD_MS = 0;
var KoboImportModal = class extends import_obsidian8.Modal {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
constructor(app, getSettings, store) {
super(app);
this.state = "path";
// Path state
this.filePath = DEFAULT_KOBO_PATH;
this.pathError = "";
this.reading = false;
// Word list state
this.items = [];
this.searchQuery = "";
this.listEl = null;
this.importBtn = null;
// Per-import override of settings.stubUnfoundWords. Initialized from
// settings in renderWordListState so each new import picks up the latest.
this.stubUnfound = false;
// Progress state
this.progressLine = "";
this.progressEl = null;
this.summary = { imported: 0, skipped: 0, stubbed: 0, notFound: [], errors: [] };
this.cancelRequested = false;
this.getSettings = getSettings;
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.store = store;
}
onOpen() {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.modalEl.addClass("lex-modal-wide");
this.render();
}
onClose() {
this.cancelRequested = true;
this.contentEl.empty();
}
// ── Render dispatch ─────────────────────────────────────────────
render() {
this.contentEl.empty();
if (this.state === "path")
return this.renderPathState();
if (this.state === "wordlist")
return this.renderWordListState();
if (this.state === "progress")
return this.renderProgressState();
this.renderDoneState();
}
// ── State 1: pick the SQLite file ──────────────────────────────
renderPathState() {
this.contentEl.createEl("h3", { text: "Import words from Kobo" });
this.contentEl.createEl("p", {
text: "Plug in your Kobo eReader, then point Lexophile at its database file. The default works on macOS when the device is mounted.",
cls: "setting-item-description"
});
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian8.Setting(this.contentEl).setName("Database path").addText((text) => {
text.setPlaceholder(DEFAULT_KOBO_PATH).setValue(this.filePath).onChange((v) => this.filePath = v);
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
text.inputEl.addClass("lex-path-input");
});
if (this.pathError) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.contentEl.createEl("p", { text: this.pathError, cls: "lex-error-line" });
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
new import_obsidian8.Setting(this.contentEl).addButton((btn) => btn.setButtonText("Cancel").onClick(() => this.close())).addButton((btn) => {
btn.setButtonText(this.reading ? "Reading\u2026" : "Read words").setCta().onClick(() => void this.readWords());
btn.buttonEl.disabled = this.reading;
});
}
async readWords() {
this.pathError = "";
this.reading = true;
this.render();
try {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const settings = this.getSettings();
if (settings.dictionarySource === "local" && !this.store.isReady()) {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
throw new Error("Local dictionary not loaded. Download it from Settings \u2192 Lexophile first.");
}
const path = this.filePath.trim();
if (!path)
throw new Error("Please enter a path.");
if (!await fileExists(path)) {
throw new Error("File not found at that path.");
}
const words = await readKoboWords(path);
if (words.length === 0) {
this.pathError = "No English words found in this database.";
this.reading = false;
this.render();
return;
}
this.items = words.map((kobo) => {
const dup = wordNoteExists(this.app, settings, kobo.word);
return {
kobo,
bookName: kobo.bookTitle ? titleCase(kobo.bookTitle) : "",
checked: !dup,
duplicate: dup
};
});
this.searchQuery = "";
this.reading = false;
this.stubUnfound = settings.stubUnfoundWords;
this.state = "wordlist";
this.render();
} catch (err) {
this.pathError = err.message;
this.reading = false;
this.render();
}
}
// ── State 2: word list with checkboxes ────────────────────────
renderWordListState() {
const totalWords = this.items.length;
const dupCount = this.items.filter((i) => i.duplicate).length;
this.contentEl.createEl("h3", {
text: `${totalWords} word${totalWords === 1 ? "" : "s"} found on your Kobo`
});
const desc = this.contentEl.createEl("p", { cls: "setting-item-description" });
desc.appendText("Pick which words to import. ");
if (dupCount > 0) {
desc.appendText(`${dupCount} ${dupCount === 1 ? "is" : "are"} already in your lexicon and pre-unchecked. `);
}
desc.appendText("Sources will link to the book each word came from.");
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const stubOpt = this.contentEl.createDiv({ cls: "lex-stub-option lex-stub-option--inline" });
const stubCheckbox = stubOpt.createEl("input");
stubCheckbox.type = "checkbox";
stubCheckbox.id = "lex-kobo-stub";
stubCheckbox.checked = this.stubUnfound;
stubCheckbox.addEventListener("change", () => {
this.stubUnfound = stubCheckbox.checked;
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const stubLabel = stubOpt.createEl("label", { cls: "lex-stub-option-label" });
stubLabel.htmlFor = "lex-kobo-stub";
stubLabel.appendText("Create stub notes for words not found in the dictionary");
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
stubOpt.createEl("span", { cls: "lex-stub-option-hint", text: "(names, slang, technical terms)" });
const toolbar = this.contentEl.createDiv({ cls: "lex-toolbar" });
const searchEl = toolbar.createEl("input", { cls: "lex-toolbar-search" });
searchEl.type = "text";
searchEl.placeholder = "Search words\u2026";
searchEl.value = this.searchQuery;
searchEl.addEventListener("input", () => {
this.searchQuery = searchEl.value;
this.refreshList();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const allBtn = toolbar.createEl("button", { text: "Select all" });
allBtn.addEventListener("click", () => {
const visible = this.visibleItems();
const allOn = visible.every((i) => i.checked);
for (const item of visible)
item.checked = !allOn;
this.refreshList();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const noneBtn = toolbar.createEl("button", { text: "Clear" });
noneBtn.addEventListener("click", () => {
for (const item of this.visibleItems())
item.checked = false;
this.refreshList();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.listEl = this.contentEl.createDiv({ cls: "lex-wordlist" });
this.refreshList();
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const footer = new import_obsidian8.Setting(this.contentEl);
footer.addButton(
(btn) => btn.setButtonText("Back").onClick(() => {
this.state = "path";
this.render();
})
);
footer.addButton((btn) => {
this.importBtn = btn.buttonEl;
btn.setButtonText("").setCta().onClick(() => void this.runImport());
});
this.updateImportButtonLabel();
}
visibleItems() {
const q = this.searchQuery.trim().toLowerCase();
if (!q)
return this.items;
return this.items.filter(
(i) => {
var _a;
return i.kobo.word.toLowerCase().includes(q) || i.bookName.toLowerCase().includes(q) || ((_a = i.kobo.bookTitle) != null ? _a : "").toLowerCase().includes(q);
}
);
}
refreshList() {
if (!this.listEl)
return;
this.listEl.empty();
const visible = this.visibleItems();
if (visible.length === 0) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.listEl.createDiv({ cls: "lex-wordlist-empty", text: "No words match." });
this.updateImportButtonLabel();
return;
}
for (const item of visible) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const row = this.listEl.createDiv({ cls: "lex-row" });
row.addEventListener("click", (e) => {
if (e.target.tagName === "INPUT")
return;
item.checked = !item.checked;
this.refreshList();
});
const checkbox = row.createEl("input");
checkbox.type = "checkbox";
checkbox.checked = item.checked;
checkbox.addEventListener("change", () => {
item.checked = checkbox.checked;
this.updateImportButtonLabel();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const wordCls = item.duplicate ? "lex-row-word lex-row-word--duplicate" : "lex-row-word";
row.createDiv({ cls: wordCls, text: item.kobo.word });
const book = row.createDiv({ cls: "lex-row-book" });
book.textContent = item.bookName || "(no book)";
book.title = item.bookName || "(no book)";
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const tagCell = row.createDiv({ cls: "lex-row-tag-cell" });
if (item.duplicate) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
tagCell.createSpan({ cls: "lex-row-tag-pill", text: "already saved" });
}
}
this.updateImportButtonLabel();
}
updateImportButtonLabel() {
if (!this.importBtn)
return;
const selected = this.items.filter((i) => i.checked).length;
this.importBtn.textContent = selected > 0 ? `Import ${selected} word${selected === 1 ? "" : "s"}` : "Import";
this.importBtn.disabled = selected === 0;
}
// ── State 3: progress ──────────────────────────────────────────
renderProgressState() {
this.contentEl.createEl("h3", { text: "Importing\u2026" });
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.progressEl = this.contentEl.createEl("p", {
cls: "lex-progress",
text: this.progressLine || "Starting\u2026"
});
}
updateProgressLine(line) {
this.progressLine = line;
if (this.progressEl)
this.progressEl.textContent = line;
}
async runImport() {
const queue = this.items.filter((i) => i.checked);
if (queue.length === 0)
return;
this.state = "progress";
this.summary = { imported: 0, skipped: 0, stubbed: 0, notFound: [], errors: [] };
this.cancelRequested = false;
this.render();
const settings = this.getSettings();
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const delayMs = settings.dictionarySource === "api" ? API_DELAY_MS : LOCAL_YIELD_MS;
const existingBooks = /* @__PURE__ */ new Set();
for (const item of queue) {
if (item.bookName && bookNoteExists(this.app, settings.booksFolder, item.bookName)) {
existingBooks.add(item.bookName.toLowerCase());
}
}
for (let i = 0; i < queue.length; i++) {
if (this.cancelRequested)
break;
const item = queue[i];
const word = item.kobo.word;
this.updateProgressLine(`Looking up "${word}" (${i + 1} of ${queue.length})\u2026`);
const source = await this.resolveSource(item, settings, existingBooks);
let entry;
let stubbed = false;
try {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
entry = await lookupWord(this.store, word, settings.dictionarySource);
} catch (err) {
if (err instanceof WordNotFoundError) {
if (this.stubUnfound) {
entry = createStubEntry(word);
stubbed = true;
} else {
this.summary.notFound.push({ word, source });
console.warn(`[Lexophile] "${word}": not found`);
if (i < queue.length - 1)
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delayMs));
continue;
}
} else {
this.summary.errors.push({ word, reason: err.message });
console.warn(`[Lexophile] "${word}":`, err.message);
if (i < queue.length - 1)
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delayMs));
continue;
}
}
entry.source = source;
try {
const result = await createWordNote(this.app, settings, entry);
if (result.action === "skipped")
this.summary.skipped++;
else if (stubbed)
this.summary.stubbed++;
else
this.summary.imported++;
} catch (err) {
this.summary.errors.push({ word, reason: err.message });
}
if (i < queue.length - 1) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delayMs));
}
}
this.state = "done";
this.render();
}
// Resolves the source frontmatter value for an item, creating a book stub
// if needed per settings.unmatchedBookHandling. Mutates `existingBooks` to
// remember any stubs it creates.
async resolveSource(item, settings, existingBooks) {
const bookName = cleanBookTitle(item.bookName);
if (!bookName)
return "kobo";
if (existingBooks.has(bookName.toLowerCase()))
return `[[${bookName}]]`;
switch (settings.unmatchedBookHandling) {
case "create":
await ensureBookStub(this.app, settings.booksFolder, bookName, settings.bookTemplate);
existingBooks.add(bookName.toLowerCase());
return `[[${bookName}]]`;
case "linkOnly":
return `[[${bookName}]]`;
case "plainText":
return `Kobo: ${bookName}`;
}
}
// ── State 4: done ──────────────────────────────────────────────
renderDoneState() {
this.contentEl.createEl("h3", { text: this.cancelRequested ? "Import cancelled" : "Done" });
const { imported, skipped, stubbed, notFound, errors } = this.summary;
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const totals = this.contentEl.createEl("ul", { cls: "lex-totals" });
totals.createEl("li", { text: `\u2713 Imported ${imported} word${imported === 1 ? "" : "s"}` });
if (stubbed) {
totals.createEl("li", {
text: `\u270E Created ${stubbed} stub${stubbed === 1 ? "" : "s"} for unknown words`
});
}
if (skipped) {
totals.createEl("li", { text: `${skipped} already in your lexicon (skipped)` });
}
if (notFound.length > 0) {
const words = notFound.map((n) => n.word);
this.renderWordListSection(
`${notFound.length} not found in the dictionary`,
words,
"These words weren't found in the dictionary. They might be names, slang, compounds, or don't exist in the online dictionary we pull from."
);
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const stubBtnWrap = this.contentEl.createDiv({ cls: "lex-done-stub-btn-wrap" });
const stubBtn = stubBtnWrap.createEl("button", {
text: `Create stub notes for these ${notFound.length} word${notFound.length === 1 ? "" : "s"}`
});
stubBtn.addEventListener("click", () => {
void this.stubNotFound(stubBtn);
});
}
if (errors.length > 0) {
const errorList = errors.map((e) => `${e.word} \u2014 ${e.reason}`);
this.renderWordListSection(
`${errors.length} error${errors.length === 1 ? "" : "s"}`,
errorList,
"Likely network issues. Try the import again later."
);
}
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian8.Setting(this.contentEl).addButton(
(btn) => btn.setButtonText("Close").setCta().onClick(() => this.close())
);
if (imported > 0 || stubbed > 0) {
const parts = [];
if (imported > 0)
parts.push(`imported ${imported}`);
if (stubbed > 0)
parts.push(`stubbed ${stubbed}`);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian8.Notice(`Lexophile: ${parts.join(", ")} word${imported + stubbed === 1 ? "" : "s"} from Kobo.`);
}
}
// Retroactively turn each not-found word into a stub note. Sources were
// captured during the original import, so we can reuse them verbatim.
async stubNotFound(triggerBtn) {
const pending = this.summary.notFound.slice();
if (pending.length === 0)
return;
triggerBtn.disabled = true;
triggerBtn.textContent = "Creating stubs\u2026";
const settings = this.getSettings();
let created = 0;
const stillFailed = [];
for (const item of pending) {
try {
const entry = createStubEntry(item.word, item.source);
const result = await createWordNote(this.app, settings, entry);
if (result.action !== "skipped")
created++;
} catch (err) {
stillFailed.push(item);
console.warn(`[Lexophile] stub "${item.word}":`, err.message);
}
}
this.summary.stubbed += created;
this.summary.notFound = stillFailed;
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian8.Notice(`Lexophile: created ${created} stub${created === 1 ? "" : "s"}.`);
this.render();
}
renderWordListSection(title, words, hint) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const wrap = this.contentEl.createDiv({ cls: "lex-done-section" });
wrap.createEl("h4", { cls: "lex-done-heading", text: title });
wrap.createEl("p", { cls: "setting-item-description lex-done-hint", text: hint });
const list = wrap.createDiv({ cls: "lex-done-list" });
for (const word of words) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
list.createDiv({ cls: "lex-done-list-row", text: word });
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const copyBtn = wrap.createEl("button", { cls: "lex-done-copy-btn", text: "Copy list" });
copyBtn.addEventListener("click", () => {
void (async () => {
try {
await navigator.clipboard.writeText(words.join("\n"));
const original = copyBtn.textContent;
copyBtn.textContent = "Copied!";
window.setTimeout(() => copyBtn.textContent = original, 1500);
} catch (e) {
new import_obsidian8.Notice("Could not copy to clipboard.");
}
})();
});
}
};
// src/massImportModal.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian9 = require("obsidian");
var API_DELAY_MS2 = 350;
var LOCAL_YIELD_MS2 = 0;
function parseWordList(raw) {
const tokens = raw.split(/[\s,;]+/).map((t) => t.replace(/^[^\p{L}'-]+|[^\p{L}'-]+$/gu, ""));
const seen = /* @__PURE__ */ new Set();
const out = [];
for (const t of tokens) {
if (!t)
continue;
const key = t.toLowerCase();
if (seen.has(key))
continue;
seen.add(key);
out.push(t);
}
return out;
}
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var MassImportModal = class extends import_obsidian9.Modal {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
constructor(app, getSettings, store) {
super(app);
this.state = "input";
// Input state
this.rawInput = "";
this.stubUnfound = false;
this.inputError = "";
// Word list state
this.items = [];
this.searchQuery = "";
this.listEl = null;
this.importBtn = null;
// Progress state
this.progressLine = "";
this.progressEl = null;
this.summary = { imported: 0, skipped: 0, stubbed: 0, notFound: [], errors: [] };
this.cancelRequested = false;
this.getSettings = getSettings;
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.store = store;
}
onOpen() {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.modalEl.addClass("lex-modal-wide");
this.stubUnfound = this.getSettings().stubUnfoundWords;
this.render();
}
onClose() {
this.cancelRequested = true;
this.contentEl.empty();
}
render() {
this.contentEl.empty();
if (this.state === "input")
return this.renderInputState();
if (this.state === "wordlist")
return this.renderWordListState();
if (this.state === "progress")
return this.renderProgressState();
this.renderDoneState();
}
// ── State 1: paste a list ──────────────────────────────────────
renderInputState() {
this.contentEl.createEl("h3", { text: "Mass-import words" });
this.contentEl.createEl("p", {
text: "Paste a list of words below. Separate them with commas, spaces, or new lines \u2014 Lexophile will figure it out.",
cls: "setting-item-description"
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const textarea = this.contentEl.createEl("textarea", { cls: "lex-mass-input" });
textarea.rows = 10;
textarea.placeholder = "serendipity, ephemeral, perspicacious\ngossamer\nalacrity";
textarea.value = this.rawInput;
textarea.addEventListener("input", () => this.rawInput = textarea.value);
if (this.inputError) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.contentEl.createEl("p", { text: this.inputError, cls: "lex-error-line" });
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const stubOpt = this.contentEl.createDiv({ cls: "lex-stub-option" });
const cb = stubOpt.createEl("input");
cb.type = "checkbox";
cb.id = "lex-mass-stub";
cb.checked = this.stubUnfound;
cb.addEventListener("change", () => this.stubUnfound = cb.checked);
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const label = stubOpt.createEl("label", { cls: "lex-stub-option-label" });
label.htmlFor = "lex-mass-stub";
label.appendText("Create stub notes for words not found in the dictionary");
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian9.Setting(this.contentEl).addButton((btn) => btn.setButtonText("Cancel").onClick(() => this.close())).addButton(
(btn) => btn.setButtonText("Parse list").setCta().onClick(() => this.parseInput())
);
window.setTimeout(() => textarea.focus(), 0);
}
parseInput() {
this.inputError = "";
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const settings = this.getSettings();
if (settings.dictionarySource === "local" && !this.store.isReady()) {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.inputError = "Local dictionary not loaded. Download it from Settings \u2192 Lexophile first.";
this.render();
return;
}
const words = parseWordList(this.rawInput);
if (words.length === 0) {
this.inputError = "No words detected. Paste a comma- or newline-separated list above.";
this.render();
return;
}
this.items = words.map((word) => {
const dup = wordNoteExists(this.app, settings, word);
return { word, checked: !dup, duplicate: dup };
});
this.searchQuery = "";
this.state = "wordlist";
this.render();
}
// ── State 2: review parsed words ──────────────────────────────
renderWordListState() {
const total = this.items.length;
const dupCount = this.items.filter((i) => i.duplicate).length;
this.contentEl.createEl("h3", {
text: `${total} word${total === 1 ? "" : "s"} ready to import`
});
const desc = this.contentEl.createEl("p", { cls: "setting-item-description" });
desc.appendText("Review the parsed list. ");
if (dupCount > 0) {
desc.appendText(`${dupCount} ${dupCount === 1 ? "is" : "are"} already in your lexicon and pre-unchecked.`);
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const stubOpt = this.contentEl.createDiv({ cls: "lex-stub-option lex-stub-option--inline" });
const cb = stubOpt.createEl("input");
cb.type = "checkbox";
cb.id = "lex-mass-stub-2";
cb.checked = this.stubUnfound;
cb.addEventListener("change", () => this.stubUnfound = cb.checked);
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const label = stubOpt.createEl("label", { cls: "lex-stub-option-label" });
label.htmlFor = "lex-mass-stub-2";
label.appendText("Create stub notes for words not found in the dictionary");
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const toolbar = this.contentEl.createDiv({ cls: "lex-toolbar" });
const searchEl = toolbar.createEl("input", { cls: "lex-toolbar-search" });
searchEl.type = "text";
searchEl.placeholder = "Search words\u2026";
searchEl.value = this.searchQuery;
searchEl.addEventListener("input", () => {
this.searchQuery = searchEl.value;
this.refreshList();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const allBtn = toolbar.createEl("button", { text: "Select all" });
allBtn.addEventListener("click", () => {
const visible = this.visibleItems();
const allOn = visible.every((i) => i.checked);
for (const item of visible)
item.checked = !allOn;
this.refreshList();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const noneBtn = toolbar.createEl("button", { text: "Clear" });
noneBtn.addEventListener("click", () => {
for (const item of this.visibleItems())
item.checked = false;
this.refreshList();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.listEl = this.contentEl.createDiv({ cls: "lex-wordlist lex-wordlist--narrow" });
this.refreshList();
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const footer = new import_obsidian9.Setting(this.contentEl);
footer.addButton(
(btn) => btn.setButtonText("Back").onClick(() => {
this.state = "input";
this.render();
})
);
footer.addButton((btn) => {
this.importBtn = btn.buttonEl;
btn.setButtonText("").setCta().onClick(() => void this.runImport());
});
this.updateImportButtonLabel();
}
visibleItems() {
const q = this.searchQuery.trim().toLowerCase();
if (!q)
return this.items;
return this.items.filter((i) => i.word.toLowerCase().includes(q));
}
refreshList() {
if (!this.listEl)
return;
this.listEl.empty();
const visible = this.visibleItems();
if (visible.length === 0) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.listEl.createDiv({ cls: "lex-wordlist-empty", text: "No words match." });
this.updateImportButtonLabel();
return;
}
for (const item of visible) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const row = this.listEl.createDiv({ cls: "lex-row lex-row--mass" });
row.addEventListener("click", (e) => {
if (e.target.tagName === "INPUT")
return;
item.checked = !item.checked;
this.refreshList();
});
const checkbox = row.createEl("input");
checkbox.type = "checkbox";
checkbox.checked = item.checked;
checkbox.addEventListener("change", () => {
item.checked = checkbox.checked;
this.updateImportButtonLabel();
});
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const wordCls = item.duplicate ? "lex-row-word lex-row-word--duplicate" : "lex-row-word";
row.createDiv({ cls: wordCls, text: item.word });
const tagCell = row.createDiv({ cls: "lex-row-tag-cell" });
if (item.duplicate) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
tagCell.createSpan({ cls: "lex-row-tag-pill", text: "already saved" });
}
}
this.updateImportButtonLabel();
}
updateImportButtonLabel() {
if (!this.importBtn)
return;
const selected = this.items.filter((i) => i.checked).length;
this.importBtn.textContent = selected > 0 ? `Import ${selected} word${selected === 1 ? "" : "s"}` : "Import";
this.importBtn.disabled = selected === 0;
}
// ── State 3: progress ─────────────────────────────────────────
renderProgressState() {
this.contentEl.createEl("h3", { text: "Importing\u2026" });
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
this.progressEl = this.contentEl.createEl("p", {
cls: "lex-progress",
text: this.progressLine || "Starting\u2026"
});
}
updateProgressLine(line) {
this.progressLine = line;
if (this.progressEl)
this.progressEl.textContent = line;
}
async runImport() {
const queue = this.items.filter((i) => i.checked);
if (queue.length === 0)
return;
this.state = "progress";
this.summary = { imported: 0, skipped: 0, stubbed: 0, notFound: [], errors: [] };
this.cancelRequested = false;
this.render();
const settings = this.getSettings();
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const delayMs = settings.dictionarySource === "api" ? API_DELAY_MS2 : LOCAL_YIELD_MS2;
for (let i = 0; i < queue.length; i++) {
if (this.cancelRequested)
break;
const { word } = queue[i];
this.updateProgressLine(`Looking up "${word}" (${i + 1} of ${queue.length})\u2026`);
let entry;
let stubbed = false;
try {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
entry = await lookupWord(this.store, word, settings.dictionarySource);
} catch (err) {
if (err instanceof WordNotFoundError) {
if (this.stubUnfound) {
entry = createStubEntry(word);
stubbed = true;
} else {
this.summary.notFound.push(word);
if (i < queue.length - 1)
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delayMs));
continue;
}
} else {
this.summary.errors.push({ word, reason: err.message });
if (i < queue.length - 1)
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delayMs));
continue;
}
}
entry.source = "manual";
try {
const result = await createWordNote(this.app, settings, entry);
if (result.action === "skipped")
this.summary.skipped++;
else if (stubbed)
this.summary.stubbed++;
else
this.summary.imported++;
} catch (err) {
this.summary.errors.push({ word, reason: err.message });
}
if (i < queue.length - 1)
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
await new Promise((r) => window.setTimeout(r, delayMs));
}
this.state = "done";
this.render();
}
// ── State 4: done ─────────────────────────────────────────────
renderDoneState() {
this.contentEl.createEl("h3", { text: this.cancelRequested ? "Import cancelled" : "Done" });
const { imported, skipped, stubbed, notFound, errors } = this.summary;
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const totals = this.contentEl.createEl("ul", { cls: "lex-totals" });
totals.createEl("li", { text: `\u2713 Imported ${imported} word${imported === 1 ? "" : "s"}` });
if (stubbed) {
totals.createEl("li", {
text: `\u270E Created ${stubbed} stub${stubbed === 1 ? "" : "s"} for unknown words`
});
}
if (skipped) {
totals.createEl("li", { text: `${skipped} already in your lexicon (skipped)` });
}
if (notFound.length > 0) {
this.renderWordListSection(
`${notFound.length} not found in the dictionary`,
notFound,
"These words weren't found in the dictionary. They might be names, slang, compounds, or don't exist in the online dictionary we pull from."
);
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const stubBtnWrap = this.contentEl.createDiv({ cls: "lex-done-stub-btn-wrap" });
const stubBtn = stubBtnWrap.createEl("button", {
text: `Create stub notes for these ${notFound.length} word${notFound.length === 1 ? "" : "s"}`
});
stubBtn.addEventListener("click", () => {
void this.stubNotFound(stubBtn);
});
}
if (errors.length > 0) {
const errorList = errors.map((e) => `${e.word} \u2014 ${e.reason}`);
this.renderWordListSection(
`${errors.length} error${errors.length === 1 ? "" : "s"}`,
errorList,
"Likely network issues. Try the import again later."
);
}
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian9.Setting(this.contentEl).addButton(
(btn) => btn.setButtonText("Close").setCta().onClick(() => this.close())
);
if (imported > 0 || stubbed > 0) {
const parts = [];
if (imported > 0)
parts.push(`imported ${imported}`);
if (stubbed > 0)
parts.push(`stubbed ${stubbed}`);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian9.Notice(`Lexophile: ${parts.join(", ")} word${imported + stubbed === 1 ? "" : "s"}.`);
}
}
async stubNotFound(triggerBtn) {
const pending = this.summary.notFound.slice();
if (pending.length === 0)
return;
triggerBtn.disabled = true;
triggerBtn.textContent = "Creating stubs\u2026";
const settings = this.getSettings();
let created = 0;
const stillFailed = [];
for (const word of pending) {
try {
const entry = createStubEntry(word, "manual");
const result = await createWordNote(this.app, settings, entry);
if (result.action !== "skipped")
created++;
} catch (err) {
stillFailed.push(word);
console.warn(`[Lexophile] stub "${word}":`, err.message);
}
}
this.summary.stubbed += created;
this.summary.notFound = stillFailed;
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian9.Notice(`Lexophile: created ${created} stub${created === 1 ? "" : "s"}.`);
this.render();
}
renderWordListSection(title, words, hint) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const wrap = this.contentEl.createDiv({ cls: "lex-done-section" });
wrap.createEl("h4", { cls: "lex-done-heading", text: title });
wrap.createEl("p", { cls: "setting-item-description lex-done-hint", text: hint });
const list = wrap.createDiv({ cls: "lex-done-list" });
for (const word of words) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
list.createDiv({ cls: "lex-done-list-row", text: word });
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const copyBtn = wrap.createEl("button", { cls: "lex-done-copy-btn", text: "Copy list" });
copyBtn.addEventListener("click", () => {
void (async () => {
try {
await navigator.clipboard.writeText(words.join("\n"));
const original = copyBtn.textContent;
copyBtn.textContent = "Copied!";
window.setTimeout(() => copyBtn.textContent = original, 1500);
} catch (e) {
new import_obsidian9.Notice("Could not copy to clipboard.");
}
})();
});
}
};
// src/folderSuggest.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var import_obsidian10 = require("obsidian");
var FolderSuggest = class extends import_obsidian10.AbstractInputSuggest {
constructor(app, inputEl, extraDefaults = []) {
super(app, inputEl);
this.folders = this.collectFolders(extraDefaults);
}
collectFolders(extraDefaults) {
const set = new Set(extraDefaults);
const walk = (folder) => {
if (folder.path)
set.add(folder.path);
for (const child of folder.children) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
if (child instanceof import_obsidian10.TFolder)
walk(child);
}
};
walk(this.app.vault.getRoot());
return Array.from(set).sort((a, b) => a.localeCompare(b));
}
getSuggestions(query) {
const q = query.trim().toLowerCase();
if (!q)
return this.folders.slice(0, 12);
return this.folders.filter((f) => f.toLowerCase().includes(q)).slice(0, 12);
}
renderSuggestion(folder, el) {
el.setText(folder);
}
selectSuggestion(folder) {
this.setValue(folder);
const inputEl = this.inputEl;
inputEl.dispatchEvent(new Event("input"));
this.close();
}
};
// src/settings.ts
var DEFAULT_WORD_TEMPLATE = `---
tags: dictionary
date-added: {{date}}
source: "{{source}}"
word-class: "{{partOfSpeech}}"
definition: "{{definition}}"
familiarity: {{familiarity}}
---
# {{word}}
**Word class:** {{partOfSpeech}}
**Definition:** {{definition}}
`;
var DEFAULT_BOOK_TEMPLATE = `---
tags: book
type: book
status: unread
author:
series:
rating:
isbn:
date-added: {{date}}
date-finished:
---
# {{title}}
`;
var DEFAULT_SETTINGS = {
folder: "Dictionary",
namingConvention: "titlecase",
wordTemplate: DEFAULT_WORD_TEMPLATE,
bookTemplate: DEFAULT_BOOK_TEMPLATE,
port: 27124,
apiToken: "",
duplicateHandling: "skip",
autoCreateBase: true,
baseName: "_Dictionary List",
stubUnfoundWords: false,
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
dictionarySource: "api",
enableKoboImport: false,
booksFolder: "Books",
unmatchedBookHandling: "create"
};
// src/main.ts
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var DictionaryPlugin = class extends import_obsidian11.Plugin {
async onload() {
await this.loadSettings();
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
this.store = new DictionaryStore(this.app, this);
try {
await this.store.init();
} catch (err) {
console.error("[Lexophile] dictionary init failed:", err);
}
this.server = new DictionaryServer(
this.app,
() => this.settings,
(msg) => console.log(`[Lexophile] ${msg}`)
);
await this.startServer();
this.addSettingTab(new DictionarySettingTab(this.app, this));
this.addCommand({
id: "add-word",
name: "Add word to lexicon",
callback: () => {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
new AddWordModal(this.app, () => this.settings, this.store).open();
}
});
this.addCommand({
id: "mass-import",
name: "Mass-import words from a list",
callback: () => {
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
new MassImportModal(this.app, () => this.settings, this.store).open();
}
});
this.addCommand({
id: "import-kobo",
name: "Import words from Kobo",
callback: () => {
if (!this.settings.enableKoboImport) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Notice("Lexophile: enable Kobo import in Settings \u2192 Lexophile first.");
return;
}
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
new KoboImportModal(this.app, () => this.settings, this.store).open();
}
});
this.addCommand({
id: "restart-server",
name: "Restart local server",
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
callback: () => {
void (async () => {
await this.stopServer();
await this.startServer();
})();
}
});
}
onunload() {
void (async () => {
var _a;
await this.stopServer();
(_a = this.store) == null ? void 0 : _a.close();
})();
}
async startServer() {
try {
await this.server.start(this.settings.port);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Notice(`Lexophile: server running on port ${this.settings.port}`);
} catch (err) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Notice(`Lexophile: failed to start server \u2014 ${err.message}`);
console.error("[Lexophile]", err);
}
}
async stopServer() {
var _a;
if ((_a = this.server) == null ? void 0 : _a.isRunning()) {
await this.server.stop();
}
}
async loadSettings() {
var _a;
const stored = (_a = await this.loadData()) != null ? _a : {};
if (stored.template && !stored.wordTemplate) {
stored.wordTemplate = stored.template;
}
delete stored.template;
this.settings = Object.assign({}, DEFAULT_SETTINGS, stored);
}
async saveSettings() {
await this.saveData(this.settings);
}
};
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
var DictionarySettingTab = class extends import_obsidian11.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const usage = containerEl.createDiv({ cls: "lex-usage-callout" });
usage.createEl("strong", { text: "How to add words" });
const list = usage.createEl("ul");
const fromObsidian = list.createEl("li");
fromObsidian.appendText("From Obsidian: open the command palette and run ");
fromObsidian.createEl("strong", { text: "Lexophile: Add word to lexicon" });
fromObsidian.appendText(". Type or paste a word, press Enter.");
const fromBrowser = list.createEl("li");
fromBrowser.appendText("From the web: install the Lexophile Chrome extension, highlight a word, right-click, and choose ");
fromBrowser.createEl("strong", { text: 'Add "<word>" to Lexophile' });
fromBrowser.appendText(".");
new import_obsidian11.Setting(containerEl).setName("Dictionary source").setDesc("Online uses the free Dictionary API. Local downloads ~23MB once and works offline.").addDropdown(
(drop) => drop.addOption("api", "Online (Free Dictionary API)").addOption("local", "Local (offline)").setValue(this.plugin.settings.dictionarySource).onChange(async (value) => {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
this.plugin.settings.dictionarySource = value;
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
await this.plugin.saveSettings();
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
this.display();
})
);
if (this.plugin.settings.dictionarySource === "local") {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const dictStatus = containerEl.createDiv({ cls: "lex-dict-status" });
void this.renderDictionaryStatus(dictStatus);
const credit = containerEl.createEl("p", { cls: "setting-item-description lex-dict-credit" });
credit.appendText("Data: ~167K English entries from Wiktionary via ");
credit.createEl("a", {
text: "MattDodsonEnglish/english-dictionary",
href: "https://github.com/MattDodsonEnglish/english-dictionary"
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
});
credit.appendText(".");
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
}
new import_obsidian11.Setting(containerEl).setName("Dictionary folder").setDesc("Folder where new notes will be created. Created automatically if it does not exist.").addText(
(text) => text.setPlaceholder("Dictionary").setValue(this.plugin.settings.folder).onChange(async (value) => {
this.plugin.settings.folder = value;
await this.plugin.saveSettings();
})
);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Note naming").setDesc("How to capitalize the note filename.").addDropdown(
(drop) => drop.addOption("asis", "As-is").addOption("lowercase", "lowercase").addOption("titlecase", "Title Case").setValue(this.plugin.settings.namingConvention).onChange(async (value) => {
this.plugin.settings.namingConvention = value;
await this.plugin.saveSettings();
})
);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Duplicate handling").setDesc("What to do when a note for the word already exists.").addDropdown(
(drop) => drop.addOption("skip", "Skip \u2014 keep existing note").addOption("append", "Append new definition").addOption("overwrite", "Overwrite").setValue(this.plugin.settings.duplicateHandling).onChange(async (value) => {
this.plugin.settings.duplicateHandling = value;
await this.plugin.saveSettings();
})
);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Create stubs for unknown words").setDesc(
"When the dictionary has no entry for a word (a name, slang, technical term), save a stub note with empty fields instead of failing. You can fill in the definition later."
).addToggle(
(toggle) => toggle.setValue(this.plugin.settings.stubUnfoundWords).onChange(async (value) => {
this.plugin.settings.stubUnfoundWords = value;
await this.plugin.saveSettings();
})
);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Automatically create dictionary base").setDesc("Create a Bases file in the dictionary folder that lists every word in a table view.").addToggle(
(toggle) => toggle.setValue(this.plugin.settings.autoCreateBase).onChange(async (value) => {
this.plugin.settings.autoCreateBase = value;
await this.plugin.saveSettings();
this.display();
})
);
if (this.plugin.settings.autoCreateBase) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Base name").setDesc("Filename (without extension) for the auto-created base.").addText(
(text) => text.setPlaceholder("_Dictionary List").setValue(this.plugin.settings.baseName).onChange(async (value) => {
this.plugin.settings.baseName = value;
await this.plugin.saveSettings();
})
);
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
new import_obsidian11.Setting(containerEl).setName("Kobo eReader import").setHeading();
const koboIntro = containerEl.createEl("p", { cls: "setting-item-description" });
koboIntro.appendText("Import words you saved on your Kobo. Plug your Kobo into your computer, then run ");
koboIntro.createEl("strong", { text: "Lexophile: Import words from Kobo" });
koboIntro.appendText(" from the command palette. Each word becomes a dictionary note; its source links back to the book it came from in your library.");
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Enable Kobo import").setDesc("Reveals the Kobo settings and unlocks the import command.").addToggle(
(toggle) => toggle.setValue(this.plugin.settings.enableKoboImport).onChange(async (value) => {
this.plugin.settings.enableKoboImport = value;
await this.plugin.saveSettings();
this.display();
})
);
if (this.plugin.settings.enableKoboImport) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
const booksFolderPath = (0, import_obsidian11.normalizePath)(this.plugin.settings.booksFolder || "Books");
const folderExists = this.app.vault.getAbstractFileByPath(booksFolderPath) instanceof import_obsidian11.TFolder;
new import_obsidian11.Setting(containerEl).setName("Books folder").setDesc(
folderExists ? `\u2713 Folder exists at "${booksFolderPath}". New book notes will be created here.` : `"${booksFolderPath}" doesn't exist yet. Create it below or pick another folder.`
).addText((text) => {
text.setPlaceholder("Books").setValue(this.plugin.settings.booksFolder).onChange(async (value) => {
this.plugin.settings.booksFolder = value;
await this.plugin.saveSettings();
this.display();
});
new FolderSuggest(this.app, text.inputEl, ["Books"]);
});
if (!folderExists) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Create books folder").setDesc(`Creates "${booksFolderPath}" so wikilinks resolve.`).addButton(
(btn) => btn.setButtonText("Create folder").setCta().onClick(async () => {
try {
await this.app.vault.createFolder(booksFolderPath);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Notice(`Lexophile: created "${booksFolderPath}".`);
this.display();
} catch (err) {
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Notice(`Lexophile: ${err.message}`);
}
})
);
}
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("When a book isn't in your library").setDesc("What to do during import if the chosen book name has no matching note in the books folder.").addDropdown(
(drop) => drop.addOption("create", "Auto-create a stub book note").addOption("linkOnly", "Link without creating (red wikilinks)").addOption("plainText", "Use plain text source instead").setValue(this.plugin.settings.unmatchedBookHandling).onChange(async (value) => {
this.plugin.settings.unmatchedBookHandling = value;
await this.plugin.saveSettings();
})
);
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
new import_obsidian11.Setting(containerEl).setName("Local server").setHeading();
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Port").setDesc("Port the plugin listens on. Requires a server restart to take effect.").addText(
(text) => text.setPlaceholder("27124").setValue(String(this.plugin.settings.port)).onChange(async (value) => {
const port = parseInt(value, 10);
if (!isNaN(port) && port > 1023 && port < 65536) {
this.plugin.settings.port = port;
await this.plugin.saveSettings();
}
})
);
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("API token").setDesc("Secret the Chrome extension must send. Leave blank to disable auth (not recommended).").addText((text) => {
text.setPlaceholder("leave blank to disable").setValue(this.plugin.settings.apiToken).onChange(async (value) => {
this.plugin.settings.apiToken = value;
await this.plugin.saveSettings();
});
text.inputEl.type = "password";
});
Make dictionary source a user setting; fix ENOENT on local download PR #12 removed the API path entirely and made every user commit to a 23MB local download. Two issues: 1. Clicking Download in cloud-synced vaults (Synology Drive, in this case) failed with ENOENT because vault.adapter.writeBinary does not create parent directories (confirmed in obsidian.d.ts). The plugin folder is usually present but isn't guaranteed at write time. 2. Forcing local mode on every user is too aggressive — many users are happy with the online API. Changes: - New settings.dictionarySource: 'api' | 'local', defaulting to 'api'. Existing users keep the online behaviour they had before #12. - Restore the API code path (requestUrl + retry-with-backoff on 429/503) alongside the local SQLite path. lookupWord(store, word, source) dispatches on the setting. - dictionaryStore.download() now mkdirs the parent before writeBinary. adapter.mkdir is idempotent and safe to call repeatedly. - Modals pick the right inter-request delay per mode: 350ms in API mode (rate-limit-friendly), 0ms in local mode (just a yield for repaints). The "dictionary not loaded" guard only fires in local mode. - Settings UI: new "Dictionary source" dropdown at the top. Local-mode controls (status pill, download URL, etc.) only render when local is selected; api mode shows a short note pointing to dictionaryapi.dev. - README updated to describe both modes; default behaviour is online. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:11:32 +00:00
new import_obsidian11.Setting(containerEl).setName("Restart server").setDesc("Apply port changes by restarting the local server.").addButton(
(btn) => btn.setButtonText("Restart").onClick(async () => {
await this.plugin.stopServer();
await this.plugin.startServer();
})
);
new import_obsidian11.Setting(containerEl).setName("Templates").setHeading();
containerEl.createEl("p", {
cls: "setting-item-description",
text: "Each entity type Lexophile creates has its own template. Use {{variable}} placeholders inside frontmatter or the body \u2014 empty values are stripped."
});
this.renderTemplateEditor(
containerEl,
"Word note template",
"Variables: {{word}}, {{partOfSpeech}}, {{definition}}, {{example}}, {{phonetic}}, {{familiarity}}, {{source}}, {{date}}",
"wordTemplate",
DEFAULT_WORD_TEMPLATE
);
this.renderTemplateEditor(
containerEl,
"Book note template",
"Used when Kobo import creates a stub for a book that isn't in your library. Variables: {{title}}, {{date}}",
"bookTemplate",
DEFAULT_BOOK_TEMPLATE
);
const support = containerEl.createEl("p", { cls: "setting-item-description" });
support.appendText("Need support? ");
support.createEl("a", {
text: "File an issue on GitHub",
href: "https://github.com/bryanmanio/obsidian-lexophile/issues/new"
});
support.appendText(".");
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const bmcWrap = containerEl.createEl("p", { cls: "lex-bmc-link" });
const bmcLink = bmcWrap.createEl("a", {
href: "https://buymeacoffee.com/bryanmanio"
});
bmcLink.setAttr("target", "_blank");
bmcLink.setAttr("rel", "noopener");
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const bmcImg = bmcLink.createEl("img", { cls: "lex-bmc-img" });
bmcImg.src = "https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png";
bmcImg.alt = "Buy Me A Coffee";
}
// Renders a labelled textarea bound to one of the template settings, with
// a Reset button. Shared by the Word and Book template editors.
renderTemplateEditor(container, label, helpText, settingKey, defaultValue) {
new import_obsidian11.Setting(container).setName(label);
container.createEl("p", { cls: "setting-item-description", text: helpText });
const wrap = container.createDiv({ cls: "lex-template-wrap" });
const textarea = wrap.createEl("textarea", { cls: "lex-template-textarea" });
textarea.rows = 14;
textarea.value = this.plugin.settings[settingKey];
textarea.addEventListener("input", () => {
this.plugin.settings[settingKey] = textarea.value;
void this.plugin.saveSettings();
});
new import_obsidian11.Setting(container).addButton(
(btn) => btn.setButtonText("Reset to default").onClick(async () => {
this.plugin.settings[settingKey] = defaultValue;
await this.plugin.saveSettings();
textarea.value = defaultValue;
})
);
}
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
// Renders the dictionary status pill + download/redownload button into the
// given container. Kept on the settings tab class so it can re-render
// itself after a download finishes without rebuilding the whole pane.
async renderDictionaryStatus(container) {
container.empty();
const status = await this.plugin.store.status();
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const row = container.createDiv({ cls: "lex-dict-status-row" });
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
const pill = row.createSpan();
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
pill.addClass("lex-dict-status-pill");
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
if (status.ready) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
pill.addClass("lex-dict-status-pill--ready");
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
pill.textContent = "Ready";
} else if (status.sizeBytes !== null) {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
pill.addClass("lex-dict-status-pill--pending");
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
pill.textContent = "Downloaded, not loaded";
} else {
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
pill.addClass("lex-dict-status-pill--missing");
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
pill.textContent = "Not downloaded";
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const detail = row.createDiv({ cls: "lex-dict-status-detail" });
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
if (status.ready && status.entryCount !== null) {
const mb = status.sizeBytes ? (status.sizeBytes / 1024 / 1024).toFixed(1) : "?";
detail.textContent = `${status.entryCount.toLocaleString()} entries \xB7 ${mb}MB on disk`;
} else {
detail.textContent = "Click Download to fetch the dictionary file.";
}
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
const btn = row.createEl("button", { text: status.ready ? "Re-download" : "Download" });
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
if (status.ready)
Address Obsidian community plugin submission warnings Source code: - Bump minAppVersion from 0.15.0 to 1.4.10 (covers AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder). - New styles.css with `lex-*` classes. Convert all inline `element.style` /`cssText` assignments in main.ts, koboImportModal.ts, and massImportModal.ts to class assignments. Use theme variables instead of hardcoded colors so the callout works in dark mode. - Replace `<h2>`/`<h3>` headings in the settings tab with `new Setting(containerEl).setName(...).setHeading()`. Modal titles remain on `<h3>` (allowed by Obsidian guidelines). - Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>` DOM building in both import done screens. - Replace `setTimeout` with `window.setTimeout` everywhere (popout window compatibility). - `vault.delete(existing)` → `fileManager.trashFile(existing)` so it respects the user's deletion preference. - `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working for both vault-content and config-dir paths. - Drop the broad `/* eslint-disable */` in familiarityData.ts and the `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts module declaration for `*.wasm`). - Tighten typing: stricter generics in lexicon.substitute, explicit `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in server.handleRequest, typed chunks in server.readBody. Use `void` operator for fire-and-forget promises. - Remove unused `DictionaryNotReadyError` imports from koboImportModal and massImportModal. - kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use `subarray(0, 15)` instead of the deprecated `Buffer.slice`. Build pipeline: - Replace the `builtin-modules` dependency with Node's built-in `module.builtinModules`; one fewer transitive dep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:51:39 +00:00
btn.addClass("lex-dict-status-button--ready");
btn.addEventListener("click", () => {
void (async () => {
btn.disabled = true;
btn.textContent = "Downloading\u2026";
try {
await this.plugin.store.download(DEFAULT_DICTIONARY_URL);
new import_obsidian11.Notice("Lexophile: dictionary downloaded and loaded.");
} catch (err) {
new import_obsidian11.Notice(`Lexophile: download failed \u2014 ${err.message}`);
} finally {
await this.renderDictionaryStatus(container);
}
})();
Replace Dictionary API with a local SQLite dictionary The plugin used to call dictionaryapi.dev for every lookup, which was slow, rate-limited, and broke offline. It now ships a local SQLite file (~23MB, ~167K English entries from Wiktionary via the MattDodsonEnglish/english-dictionary project) and queries it locally via sql.js. Plumbing: - DictionaryStore loads the SQLite from <vault>/.obsidian/plugins/lexophile/dictionary.sqlite, downloads on demand from a configurable URL (default: a release asset on this repo), and exposes a synchronous lookup() that returns a WordEntry or null. - lookupWord is now an async wrapper around store.lookup that throws WordNotFoundError on null. DictionaryNotReadyError surfaces the pre-download state so modals can fail fast with a helpful message. - sql.js initialization moved to its own module (src/sqlite.ts) and reused by both the Kobo import and the new dictionary store. - AddWordModal / MassImportModal / KoboImportModal each take the store via constructor and guard against the not-ready state before starting work. - Settings tab gains a "Local dictionary" section with a status pill (Ready / Downloaded-not-loaded / Not downloaded), an entry count and size summary, a Download/Re-download button, and a configurable URL. - Removed the inter-request rate limit (was 350ms for API throttling) — local lookups are sub-millisecond, so a 0ms yield is enough to let the progress line repaint. Bundled dictionary data is hosted as a release asset on bryanmanio/obsidian-lexophile (tag: dictionary-v1). The plugin does not call dictionaryapi.dev at any point during normal use. The Chrome extension still uses the API for its own lookups — out of scope here. Build artifacts: - scripts/build-dictionary.py converts the upstream parquet (167,585 entries) to the compact SQLite schema we ship; documented for reproducibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:44:09 +00:00
});
}
};