2026-05-10 02:34:42 +00:00
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 ) ;
2026-05-11 18:11:32 +00:00
var import _obsidian11 = require ( "obsidian" ) ;
2026-05-10 02:34:42 +00:00
// 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
2026-05-11 07:31:40 +00:00
familiarity :
displayName : Familiarity
2026-05-10 02:34:42 +00:00
date - added :
displayName : Date added
views :
- type : table
name : All words
order :
- file . name
- word - class
- definition
2026-05-11 07:31:40 +00:00
- familiarity
2026-05-10 02:34:42 +00:00
- source
- date - added
` ;
await app . vault . create ( basePath , content ) ;
}
2026-05-11 07:31:40 +00:00
// 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 ;
}
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
// 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" ) ;
}
2026-05-10 02:34:42 +00:00
// src/lexicon.ts
2026-05-11 07:50:43 +00:00
function createStubEntry ( word , source = "" ) {
return {
word : word . trim ( ) ,
partOfSpeech : "" ,
definition : "" ,
example : "" ,
phonetic : "" ,
source
} ;
}
2026-05-10 02:34:42 +00:00
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" } ;
}
2026-05-12 19:51:39 +00:00
await app . fileManager . trashFile ( existing ) ;
2026-05-10 02:34:42 +00:00
}
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 ) {
2026-05-12 19:51:39 +00:00
await app . vault . adapter . mkdir ( current ) ;
2026-05-10 02:34:42 +00:00
} 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 ,
2026-05-11 07:31:40 +00:00
source : ( _d = entry . source ) != null ? _d : "" ,
familiarity : classifyFamiliarity ( entry . word )
2026-05-10 02:34:42 +00:00
} ;
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
return renderTemplate ( settings . wordTemplate , values , includeFrontmatter ) ;
2026-05-10 02:34:42 +00:00
}
// 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 } " \u 2192 ${ result . path } ` ) ;
res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
res . end ( JSON . stringify ( result ) ) ;
}
readBody ( req ) {
return new Promise ( ( resolve , reject ) => {
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" ) ) ) ;
2026-05-10 02:34:42 +00:00
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" ;
2026-05-11 19:31:57 +00:00
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." ) ;
}
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" ;
}
} ;
2026-05-10 02:34:42 +00:00
// src/wordModal.ts
2026-05-11 18:11:32 +00:00
var import _obsidian6 = require ( "obsidian" ) ;
2026-05-10 02:34:42 +00:00
// src/dictionary.ts
2026-05-11 18:11:32 +00:00
var import _obsidian5 = require ( "obsidian" ) ;
2026-05-11 07:50:43 +00:00
var WordNotFoundError = class extends Error {
constructor ( word ) {
super ( ` No definition found for " ${ word } ". ` ) ;
this . word = word ;
this . name = "WordNotFoundError" ;
}
} ;
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 )
2026-05-11 07:50:43 +00:00
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 ;
2026-05-10 02:34:42 +00:00
}
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 ) ;
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delay ) ) ;
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 : ""
} ;
}
2026-05-10 02:34:42 +00:00
// src/wordModal.ts
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 ) {
2026-05-10 02:34:42 +00:00
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 ;
2026-05-10 02:34:42 +00:00
}
onOpen ( ) {
const { contentEl } = this ;
contentEl . empty ( ) ;
contentEl . createEl ( "h3" , { text : "Add word to lexicon" } ) ;
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 ) => {
2026-05-10 02:34:42 +00:00
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 ( ) ;
}
} ) ;
} ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian6 . Setting ( contentEl ) . addButton ( ( btn ) => {
2026-05-10 02:34:42 +00:00
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 ) {
2026-05-11 18:11:32 +00:00
new import _obsidian6 . Notice ( "Lexophile: please enter a word." ) ;
2026-05-10 02:34:42 +00:00
return ;
}
this . submitting = true ;
if ( this . submitBtn ) {
this . submitBtn . disabled = true ;
this . submitBtn . textContent = "Looking up\u2026" ;
}
2026-05-11 07:50:43 +00:00
const settings = this . getSettings ( ) ;
2026-05-10 02:34:42 +00:00
try {
2026-05-11 07:50:43 +00:00
let entry ;
let stubbed = false ;
try {
2026-05-11 18:11:32 +00:00
entry = await lookupWord ( this . store , word , settings . dictionarySource ) ;
2026-05-11 07:50:43 +00:00
} 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 ) {
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 ;
}
2026-05-11 07:50:43 +00:00
if ( err instanceof WordNotFoundError && settings . stubUnfoundWords ) {
entry = createStubEntry ( word ) ;
stubbed = true ;
} else {
throw err ;
}
}
2026-05-10 02:34:42 +00:00
entry . source = "manual" ;
2026-05-11 07:50:43 +00:00
const result = await createWordNote ( this . app , settings , entry ) ;
const label = stubbed ? ` stub saved for " ${ entry . word } " ` : ` ${ result . action } " ${ entry . word } " ` ;
2026-05-11 18:11:32 +00:00
new import _obsidian6 . Notice ( ` Lexophile: ${ label } ` ) ;
2026-05-10 02:34:42 +00:00
this . close ( ) ;
} catch ( err ) {
2026-05-11 18:11:32 +00:00
new import _obsidian6 . Notice ( ` Lexophile: ${ err . message } ` ) ;
2026-05-10 02:34:42 +00:00
this . submitting = false ;
if ( this . submitBtn ) {
this . submitBtn . disabled = false ;
this . submitBtn . textContent = "Look up & save" ;
}
}
}
onClose ( ) {
this . contentEl . empty ( ) ;
}
} ;
// src/koboImportModal.ts
2026-05-11 18:11:32 +00:00
var import _obsidian8 = require ( "obsidian" ) ;
2026-05-10 02:34:42 +00:00
// src/kobo.ts
var import _fs = require ( "fs" ) ;
2026-05-12 19:51:39 +00:00
var TRIM _CHARS = /^[\s.,;:!?'"`‘’“”()[\]{}<>—–-]+|[\s.,;:!?'"`‘’“”()[\]{}<>—–-]+$/g ;
2026-05-10 02:34:42 +00:00
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 } ` ) ;
}
2026-05-12 19:51:39 +00:00
if ( buf . length < 16 || buf . subarray ( 0 , 15 ) . toString ( "utf8" ) !== "SQLite format 3" ) {
2026-05-10 02:34:42 +00:00
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
2026-05-11 18:11:32 +00:00
var import _obsidian7 = require ( "obsidian" ) ;
2026-05-10 02:34:42 +00:00
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 ;
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 ;
2026-05-10 02:34:42 +00:00
}
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
async function ensureBookStub ( app , folder , title , template ) {
2026-05-10 02:34:42 +00:00
const cleaned = cleanBookTitle ( title ) ;
if ( ! cleaned )
return ;
2026-05-11 18:11:32 +00:00
const folderPath = ( 0 , import _obsidian7 . normalizePath ) ( folder ) ;
const filePath = ( 0 , import _obsidian7 . normalizePath ) ( ` ${ folderPath } / ${ cleaned } .md ` ) ;
2026-05-10 02:34:42 +00:00
if ( app . vault . getAbstractFileByPath ( filePath ) )
return ;
const today = ( /* @__PURE__ */ new Date ( ) ) . toISOString ( ) . split ( "T" ) [ 0 ] ;
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
const content = renderTemplate ( template , { title : cleaned , date : today } ) ;
2026-05-10 02:34:42 +00:00
await app . vault . create ( filePath , content ) ;
}
// src/koboImportModal.ts
var DEFAULT _KOBO _PATH = "/Volumes/KOBOeReader/.kobo/KoboReader.sqlite" ;
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 ) {
2026-05-10 02:34:42 +00:00
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 ;
2026-05-11 07:50:43 +00:00
// Per-import override of settings.stubUnfoundWords. Initialized from
// settings in renderWordListState so each new import picks up the latest.
this . stubUnfound = false ;
2026-05-10 02:34:42 +00:00
// Progress state
this . progressLine = "" ;
this . progressEl = null ;
2026-05-11 07:50:43 +00:00
this . summary = { imported : 0 , skipped : 0 , stubbed : 0 , notFound : [ ] , errors : [ ] } ;
2026-05-10 02:34:42 +00:00
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 ;
2026-05-10 02:34:42 +00:00
}
onOpen ( ) {
2026-05-12 19:51:39 +00:00
this . modalEl . addClass ( "lex-modal-wide" ) ;
2026-05-10 02:34:42 +00:00
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"
} ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian8 . Setting ( this . contentEl ) . setName ( "Database path" ) . addText ( ( text ) => {
2026-05-10 02:34:42 +00:00
text . setPlaceholder ( DEFAULT _KOBO _PATH ) . setValue ( this . filePath ) . onChange ( ( v ) => this . filePath = v ) ;
2026-05-12 19:51:39 +00:00
text . inputEl . addClass ( "lex-path-input" ) ;
2026-05-10 02:34:42 +00:00
} ) ;
if ( this . pathError ) {
2026-05-12 19:51:39 +00:00
this . contentEl . createEl ( "p" , { text : this . pathError , cls : "lex-error-line" } ) ;
2026-05-10 02:34:42 +00:00
}
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 ;
} ) ;
2026-05-10 02:34:42 +00:00
}
async readWords ( ) {
this . pathError = "" ;
this . reading = true ;
this . render ( ) ;
try {
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." ) ;
}
2026-05-10 02:34:42 +00:00
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 ;
2026-05-11 07:50:43 +00:00
this . stubUnfound = settings . stubUnfoundWords ;
2026-05-10 02:34:42 +00:00
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." ) ;
2026-05-12 19:51:39 +00:00
const stubOpt = this . contentEl . createDiv ( { cls : "lex-stub-option lex-stub-option--inline" } ) ;
2026-05-11 07:50:43 +00:00
const stubCheckbox = stubOpt . createEl ( "input" ) ;
stubCheckbox . type = "checkbox" ;
stubCheckbox . id = "lex-kobo-stub" ;
stubCheckbox . checked = this . stubUnfound ;
stubCheckbox . addEventListener ( "change" , ( ) => {
this . stubUnfound = stubCheckbox . checked ;
} ) ;
2026-05-12 19:51:39 +00:00
const stubLabel = stubOpt . createEl ( "label" , { cls : "lex-stub-option-label" } ) ;
2026-05-11 07:50:43 +00:00
stubLabel . htmlFor = "lex-kobo-stub" ;
stubLabel . appendText ( "Create stub notes for words not found in the dictionary" ) ;
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" } ) ;
2026-05-10 02:34:42 +00:00
searchEl . type = "text" ;
searchEl . placeholder = "Search words\u2026" ;
searchEl . value = this . searchQuery ;
searchEl . addEventListener ( "input" , ( ) => {
this . searchQuery = searchEl . value ;
this . refreshList ( ) ;
} ) ;
2026-05-12 19:51:39 +00:00
const allBtn = toolbar . createEl ( "button" , { text : "Select all" } ) ;
2026-05-10 02:34:42 +00:00
allBtn . addEventListener ( "click" , ( ) => {
const visible = this . visibleItems ( ) ;
const allOn = visible . every ( ( i ) => i . checked ) ;
for ( const item of visible )
item . checked = ! allOn ;
this . refreshList ( ) ;
} ) ;
2026-05-12 19:51:39 +00:00
const noneBtn = toolbar . createEl ( "button" , { text : "Clear" } ) ;
2026-05-10 02:34:42 +00:00
noneBtn . addEventListener ( "click" , ( ) => {
for ( const item of this . visibleItems ( ) )
item . checked = false ;
this . refreshList ( ) ;
} ) ;
2026-05-12 19:51:39 +00:00
this . listEl = this . contentEl . createDiv ( { cls : "lex-wordlist" } ) ;
2026-05-10 02:34:42 +00:00
this . refreshList ( ) ;
2026-05-11 18:11:32 +00:00
const footer = new import _obsidian8 . Setting ( this . contentEl ) ;
2026-05-10 02:34:42 +00:00
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 ) {
2026-05-12 19:51:39 +00:00
this . listEl . createDiv ( { cls : "lex-wordlist-empty" , text : "No words match." } ) ;
2026-05-10 02:34:42 +00:00
this . updateImportButtonLabel ( ) ;
return ;
}
for ( const item of visible ) {
2026-05-12 19:51:39 +00:00
const row = this . listEl . createDiv ( { cls : "lex-row" } ) ;
2026-05-10 02:34:42 +00:00
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 ( ) ;
} ) ;
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" } ) ;
2026-05-10 02:34:42 +00:00
book . textContent = item . bookName || "(no book)" ;
book . title = item . bookName || "(no book)" ;
2026-05-12 19:51:39 +00:00
const tagCell = row . createDiv ( { cls : "lex-row-tag-cell" } ) ;
2026-05-10 02:34:42 +00:00
if ( item . duplicate ) {
2026-05-12 19:51:39 +00:00
tagCell . createSpan ( { cls : "lex-row-tag-pill" , text : "already saved" } ) ;
2026-05-10 02:34:42 +00:00
}
}
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" } ) ;
2026-05-12 19:51:39 +00:00
this . progressEl = this . contentEl . createEl ( "p" , {
cls : "lex-progress" ,
text : this . progressLine || "Starting\u2026"
} ) ;
2026-05-10 02:34:42 +00:00
}
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" ;
2026-05-11 07:50:43 +00:00
this . summary = { imported : 0 , skipped : 0 , stubbed : 0 , notFound : [ ] , errors : [ ] } ;
2026-05-10 02:34:42 +00:00
this . cancelRequested = false ;
this . render ( ) ;
const settings = this . getSettings ( ) ;
2026-05-11 18:11:32 +00:00
const delayMs = settings . dictionarySource === "api" ? API _DELAY _MS : LOCAL _YIELD _MS ;
2026-05-10 02:34:42 +00:00
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 } ) \u 2026 ` ) ;
2026-05-11 07:50:43 +00:00
const source = await this . resolveSource ( item , settings , existingBooks ) ;
let entry ;
let stubbed = false ;
2026-05-10 02:34:42 +00:00
try {
2026-05-11 18:11:32 +00:00
entry = await lookupWord ( this . store , word , settings . dictionarySource ) ;
2026-05-11 07:50:43 +00:00
} 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 )
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delayMs ) ) ;
2026-05-11 07:50:43 +00:00
continue ;
2026-05-10 02:34:42 +00:00
}
2026-05-11 07:50:43 +00:00
} else {
this . summary . errors . push ( { word , reason : err . message } ) ;
console . warn ( ` [Lexophile] " ${ word } ": ` , err . message ) ;
if ( i < queue . length - 1 )
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delayMs ) ) ;
2026-05-11 07:50:43 +00:00
continue ;
2026-05-10 02:34:42 +00:00
}
2026-05-11 07:50:43 +00:00
}
entry . source = source ;
try {
2026-05-10 02:34:42 +00:00
const result = await createWordNote ( this . app , settings , entry ) ;
if ( result . action === "skipped" )
this . summary . skipped ++ ;
2026-05-11 07:50:43 +00:00
else if ( stubbed )
this . summary . stubbed ++ ;
2026-05-10 02:34:42 +00:00
else
this . summary . imported ++ ;
} catch ( err ) {
2026-05-11 07:50:43 +00:00
this . summary . errors . push ( { word , reason : err . message } ) ;
2026-05-10 02:34:42 +00:00
}
if ( i < queue . length - 1 ) {
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delayMs ) ) ;
2026-05-10 02:34:42 +00:00
}
}
this . state = "done" ;
this . render ( ) ;
}
2026-05-11 07:50:43 +00:00
// 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" :
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
await ensureBookStub ( this . app , settings . booksFolder , bookName , settings . bookTemplate ) ;
2026-05-11 07:50:43 +00:00
existingBooks . add ( bookName . toLowerCase ( ) ) ;
return ` [[ ${ bookName } ]] ` ;
case "linkOnly" :
return ` [[ ${ bookName } ]] ` ;
case "plainText" :
return ` Kobo: ${ bookName } ` ;
}
}
2026-05-10 02:34:42 +00:00
// ── State 4: done ──────────────────────────────────────────────
renderDoneState ( ) {
this . contentEl . createEl ( "h3" , { text : this . cancelRequested ? "Import cancelled" : "Done" } ) ;
2026-05-11 07:50:43 +00:00
const { imported , skipped , stubbed , notFound , errors } = this . summary ;
2026-05-12 19:51:39 +00:00
const totals = this . contentEl . createEl ( "ul" , { cls : "lex-totals" } ) ;
totals . createEl ( "li" , { text : ` \u 2713 Imported ${ imported } word ${ imported === 1 ? "" : "s" } ` } ) ;
if ( stubbed ) {
totals . createEl ( "li" , {
text : ` \u 270E Created ${ stubbed } stub ${ stubbed === 1 ? "" : "s" } for unknown words `
} ) ;
}
if ( skipped ) {
totals . createEl ( "li" , { text : ` ${ skipped } already in your lexicon (skipped) ` } ) ;
}
2026-05-10 02:34:42 +00:00
if ( notFound . length > 0 ) {
2026-05-11 07:50:43 +00:00
const words = notFound . map ( ( n ) => n . word ) ;
2026-05-10 02:34:42 +00:00
this . renderWordListSection (
` ${ notFound . length } not found in the dictionary ` ,
2026-05-11 07:50:43 +00:00
words ,
2026-05-11 19:31:57 +00:00
"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."
2026-05-10 02:34:42 +00:00
) ;
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 ) ;
} ) ;
2026-05-10 02:34:42 +00:00
}
if ( errors . length > 0 ) {
const errorList = errors . map ( ( e ) => ` ${ e . word } \u 2014 ${ e . reason } ` ) ;
this . renderWordListSection (
` ${ errors . length } error ${ errors . length === 1 ? "" : "s" } ` ,
errorList ,
"Likely network issues. Try the import again later."
) ;
}
2026-05-11 18:11:32 +00:00
new import _obsidian8 . Setting ( this . contentEl ) . addButton (
2026-05-10 02:34:42 +00:00
( btn ) => btn . setButtonText ( "Close" ) . setCta ( ) . onClick ( ( ) => this . close ( ) )
) ;
2026-05-11 07:50:43 +00:00
if ( imported > 0 || stubbed > 0 ) {
const parts = [ ] ;
if ( imported > 0 )
parts . push ( ` imported ${ imported } ` ) ;
if ( stubbed > 0 )
parts . push ( ` stubbed ${ stubbed } ` ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian8 . Notice ( ` Lexophile: ${ parts . join ( ", " ) } word ${ imported + stubbed === 1 ? "" : "s" } from Kobo. ` ) ;
2026-05-11 07:50:43 +00:00
}
}
// 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 ) ;
}
2026-05-10 02:34:42 +00:00
}
2026-05-11 07:50:43 +00:00
this . summary . stubbed += created ;
this . summary . notFound = stillFailed ;
2026-05-11 18:11:32 +00:00
new import _obsidian8 . Notice ( ` Lexophile: created ${ created } stub ${ created === 1 ? "" : "s" } . ` ) ;
2026-05-11 07:50:43 +00:00
this . render ( ) ;
2026-05-10 02:34:42 +00:00
}
renderWordListSection ( title , words , hint ) {
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" } ) ;
2026-05-10 02:34:42 +00:00
for ( const word of words ) {
2026-05-12 19:51:39 +00:00
list . createDiv ( { cls : "lex-done-list-row" , text : word } ) ;
2026-05-10 02:34:42 +00:00
}
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." ) ;
}
} ) ( ) ;
2026-05-10 02:34:42 +00:00
} ) ;
}
} ;
2026-05-11 08:06:46 +00:00
// src/massImportModal.ts
2026-05-11 18:11:32 +00:00
var import _obsidian9 = require ( "obsidian" ) ;
var API _DELAY _MS2 = 350 ;
var LOCAL _YIELD _MS2 = 0 ;
2026-05-11 08:06:46 +00:00
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 ;
}
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 ) {
2026-05-11 08:06:46 +00:00
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 ;
2026-05-11 08:06:46 +00:00
}
onOpen ( ) {
2026-05-12 19:51:39 +00:00
this . modalEl . addClass ( "lex-modal-wide" ) ;
2026-05-11 08:06:46 +00:00
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"
} ) ;
2026-05-12 19:51:39 +00:00
const textarea = this . contentEl . createEl ( "textarea" , { cls : "lex-mass-input" } ) ;
2026-05-11 08:06:46 +00:00
textarea . rows = 10 ;
textarea . placeholder = "serendipity, ephemeral, perspicacious\ngossamer\nalacrity" ;
textarea . value = this . rawInput ;
textarea . addEventListener ( "input" , ( ) => this . rawInput = textarea . value ) ;
if ( this . inputError ) {
2026-05-12 19:51:39 +00:00
this . contentEl . createEl ( "p" , { text : this . inputError , cls : "lex-error-line" } ) ;
2026-05-11 08:06:46 +00:00
}
2026-05-12 19:51:39 +00:00
const stubOpt = this . contentEl . createDiv ( { cls : "lex-stub-option" } ) ;
2026-05-11 08:06:46 +00:00
const cb = stubOpt . createEl ( "input" ) ;
cb . type = "checkbox" ;
cb . id = "lex-mass-stub" ;
cb . checked = this . stubUnfound ;
cb . addEventListener ( "change" , ( ) => this . stubUnfound = cb . checked ) ;
2026-05-12 19:51:39 +00:00
const label = stubOpt . createEl ( "label" , { cls : "lex-stub-option-label" } ) ;
2026-05-11 08:06:46 +00:00
label . htmlFor = "lex-mass-stub" ;
label . appendText ( "Create stub notes for words not found in the dictionary" ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian9 . Setting ( this . contentEl ) . addButton ( ( btn ) => btn . setButtonText ( "Cancel" ) . onClick ( ( ) => this . close ( ) ) ) . addButton (
2026-05-11 08:06:46 +00:00
( btn ) => btn . setButtonText ( "Parse list" ) . setCta ( ) . onClick ( ( ) => this . parseInput ( ) )
) ;
window . setTimeout ( ( ) => textarea . focus ( ) , 0 ) ;
}
parseInput ( ) {
this . inputError = "" ;
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 ;
}
2026-05-11 08:06:46 +00:00
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. ` ) ;
}
2026-05-12 19:51:39 +00:00
const stubOpt = this . contentEl . createDiv ( { cls : "lex-stub-option lex-stub-option--inline" } ) ;
2026-05-11 08:06:46 +00:00
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 ) ;
2026-05-12 19:51:39 +00:00
const label = stubOpt . createEl ( "label" , { cls : "lex-stub-option-label" } ) ;
2026-05-11 08:06:46 +00:00
label . htmlFor = "lex-mass-stub-2" ;
label . appendText ( "Create stub notes for words not found in the dictionary" ) ;
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" } ) ;
2026-05-11 08:06:46 +00:00
searchEl . type = "text" ;
searchEl . placeholder = "Search words\u2026" ;
searchEl . value = this . searchQuery ;
searchEl . addEventListener ( "input" , ( ) => {
this . searchQuery = searchEl . value ;
this . refreshList ( ) ;
} ) ;
2026-05-12 19:51:39 +00:00
const allBtn = toolbar . createEl ( "button" , { text : "Select all" } ) ;
2026-05-11 08:06:46 +00:00
allBtn . addEventListener ( "click" , ( ) => {
const visible = this . visibleItems ( ) ;
const allOn = visible . every ( ( i ) => i . checked ) ;
for ( const item of visible )
item . checked = ! allOn ;
this . refreshList ( ) ;
} ) ;
2026-05-12 19:51:39 +00:00
const noneBtn = toolbar . createEl ( "button" , { text : "Clear" } ) ;
2026-05-11 08:06:46 +00:00
noneBtn . addEventListener ( "click" , ( ) => {
for ( const item of this . visibleItems ( ) )
item . checked = false ;
this . refreshList ( ) ;
} ) ;
2026-05-12 19:51:39 +00:00
this . listEl = this . contentEl . createDiv ( { cls : "lex-wordlist lex-wordlist--narrow" } ) ;
2026-05-11 08:06:46 +00:00
this . refreshList ( ) ;
2026-05-11 18:11:32 +00:00
const footer = new import _obsidian9 . Setting ( this . contentEl ) ;
2026-05-11 08:06:46 +00:00
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 ) {
2026-05-12 19:51:39 +00:00
this . listEl . createDiv ( { cls : "lex-wordlist-empty" , text : "No words match." } ) ;
2026-05-11 08:06:46 +00:00
this . updateImportButtonLabel ( ) ;
return ;
}
for ( const item of visible ) {
2026-05-12 19:51:39 +00:00
const row = this . listEl . createDiv ( { cls : "lex-row lex-row--mass" } ) ;
2026-05-11 08:06:46 +00:00
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 ( ) ;
} ) ;
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" } ) ;
2026-05-11 08:06:46 +00:00
if ( item . duplicate ) {
2026-05-12 19:51:39 +00:00
tagCell . createSpan ( { cls : "lex-row-tag-pill" , text : "already saved" } ) ;
2026-05-11 08:06:46 +00:00
}
}
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" } ) ;
2026-05-12 19:51:39 +00:00
this . progressEl = this . contentEl . createEl ( "p" , {
cls : "lex-progress" ,
text : this . progressLine || "Starting\u2026"
} ) ;
2026-05-11 08:06:46 +00:00
}
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 ( ) ;
2026-05-11 18:11:32 +00:00
const delayMs = settings . dictionarySource === "api" ? API _DELAY _MS2 : LOCAL _YIELD _MS2 ;
2026-05-11 08:06:46 +00:00
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 } ) \u 2026 ` ) ;
let entry ;
let stubbed = false ;
try {
2026-05-11 18:11:32 +00:00
entry = await lookupWord ( this . store , word , settings . dictionarySource ) ;
2026-05-11 08:06:46 +00:00
} 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 )
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delayMs ) ) ;
2026-05-11 08:06:46 +00:00
continue ;
}
} else {
this . summary . errors . push ( { word , reason : err . message } ) ;
if ( i < queue . length - 1 )
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delayMs ) ) ;
2026-05-11 08:06:46 +00:00
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 )
2026-05-12 19:51:39 +00:00
await new Promise ( ( r ) => window . setTimeout ( r , delayMs ) ) ;
2026-05-11 08:06:46 +00:00
}
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 ;
2026-05-12 19:51:39 +00:00
const totals = this . contentEl . createEl ( "ul" , { cls : "lex-totals" } ) ;
totals . createEl ( "li" , { text : ` \u 2713 Imported ${ imported } word ${ imported === 1 ? "" : "s" } ` } ) ;
if ( stubbed ) {
totals . createEl ( "li" , {
text : ` \u 270E Created ${ stubbed } stub ${ stubbed === 1 ? "" : "s" } for unknown words `
} ) ;
}
if ( skipped ) {
totals . createEl ( "li" , { text : ` ${ skipped } already in your lexicon (skipped) ` } ) ;
}
2026-05-11 08:06:46 +00:00
if ( notFound . length > 0 ) {
this . renderWordListSection (
` ${ notFound . length } not found in the dictionary ` ,
notFound ,
2026-05-11 19:31:57 +00:00
"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."
2026-05-11 08:06:46 +00:00
) ;
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 ) ;
} ) ;
2026-05-11 08:06:46 +00:00
}
if ( errors . length > 0 ) {
const errorList = errors . map ( ( e ) => ` ${ e . word } \u 2014 ${ e . reason } ` ) ;
this . renderWordListSection (
` ${ errors . length } error ${ errors . length === 1 ? "" : "s" } ` ,
errorList ,
"Likely network issues. Try the import again later."
) ;
}
2026-05-11 18:11:32 +00:00
new import _obsidian9 . Setting ( this . contentEl ) . addButton (
2026-05-11 08:06:46 +00:00
( 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 } ` ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian9 . Notice ( ` Lexophile: ${ parts . join ( ", " ) } word ${ imported + stubbed === 1 ? "" : "s" } . ` ) ;
2026-05-11 08:06:46 +00:00
}
}
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 ;
2026-05-11 18:11:32 +00:00
new import _obsidian9 . Notice ( ` Lexophile: created ${ created } stub ${ created === 1 ? "" : "s" } . ` ) ;
2026-05-11 08:06:46 +00:00
this . render ( ) ;
}
renderWordListSection ( title , words , hint ) {
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" } ) ;
2026-05-11 08:06:46 +00:00
for ( const word of words ) {
2026-05-12 19:51:39 +00:00
list . createDiv ( { cls : "lex-done-list-row" , text : word } ) ;
2026-05-11 08:06:46 +00:00
}
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." ) ;
}
} ) ( ) ;
2026-05-11 08:06:46 +00:00
} ) ;
}
} ;
// src/folderSuggest.ts
2026-05-11 18:11:32 +00:00
var import _obsidian10 = require ( "obsidian" ) ;
var FolderSuggest = class extends import _obsidian10 . AbstractInputSuggest {
2026-05-10 02:34:42 +00:00
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 ) {
2026-05-11 18:11:32 +00:00
if ( child instanceof import _obsidian10 . TFolder )
2026-05-10 02:34:42 +00:00
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
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
var DEFAULT _WORD _TEMPLATE = ` ---
2026-05-10 02:34:42 +00:00
tags : dictionary
date - added : { { date } }
source : "{{source}}"
word - class : "{{partOfSpeech}}"
definition : "{{definition}}"
2026-05-11 07:31:40 +00:00
familiarity : { { familiarity } }
2026-05-10 02:34:42 +00:00
-- -
# { { word } }
* * Word class : * * { { partOfSpeech } }
* * Definition : * * { { definition } }
` ;
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
var DEFAULT _BOOK _TEMPLATE = ` ---
tags : book
type : book
status : unread
author :
series :
rating :
isbn :
date - added : { { date } }
date - finished :
-- -
# { { title } }
` ;
2026-05-10 02:34:42 +00:00
var DEFAULT _SETTINGS = {
folder : "Dictionary" ,
namingConvention : "titlecase" ,
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
wordTemplate : DEFAULT _WORD _TEMPLATE ,
bookTemplate : DEFAULT _BOOK _TEMPLATE ,
2026-05-10 02:34:42 +00:00
port : 27124 ,
apiToken : "" ,
duplicateHandling : "skip" ,
autoCreateBase : true ,
baseName : "_Dictionary List" ,
2026-05-11 07:50:43 +00:00
stubUnfoundWords : false ,
2026-05-11 18:11:32 +00:00
dictionarySource : "api" ,
2026-05-10 02:34:42 +00:00
enableKoboImport : false ,
booksFolder : "Books" ,
unmatchedBookHandling : "create"
} ;
// src/main.ts
2026-05-11 18:11:32 +00:00
var DictionaryPlugin = class extends import _obsidian11 . Plugin {
2026-05-10 02:34:42 +00:00
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 ) ;
}
2026-05-10 02:34:42 +00:00
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 ( ) ;
2026-05-10 02:34:42 +00:00
}
} ) ;
2026-05-11 08:06:46 +00:00
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 ( ) ;
2026-05-11 08:06:46 +00:00
}
} ) ;
2026-05-10 02:34:42 +00:00
this . addCommand ( {
id : "import-kobo" ,
name : "Import words from Kobo" ,
callback : ( ) => {
if ( ! this . settings . enableKoboImport ) {
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Notice ( "Lexophile: enable Kobo import in Settings \u2192 Lexophile first." ) ;
2026-05-10 02:34:42 +00:00
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 ( ) ;
2026-05-10 02:34:42 +00:00
}
} ) ;
this . addCommand ( {
id : "restart-server" ,
name : "Restart local server" ,
2026-05-12 19:51:39 +00:00
callback : ( ) => {
void ( async ( ) => {
await this . stopServer ( ) ;
await this . startServer ( ) ;
} ) ( ) ;
2026-05-10 02:34:42 +00:00
}
} ) ;
}
2026-05-12 20:53:22 +00:00
onunload ( ) {
void ( async ( ) => {
var _a ;
await this . stopServer ( ) ;
( _a = this . store ) == null ? void 0 : _a . close ( ) ;
} ) ( ) ;
2026-05-10 02:34:42 +00:00
}
async startServer ( ) {
try {
await this . server . start ( this . settings . port ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Notice ( ` Lexophile: server running on port ${ this . settings . port } ` ) ;
2026-05-10 02:34:42 +00:00
} catch ( err ) {
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Notice ( ` Lexophile: failed to start server \u 2014 ${ err . message } ` ) ;
2026-05-10 02:34:42 +00:00
console . error ( "[Lexophile]" , err ) ;
}
}
async stopServer ( ) {
var _a ;
if ( ( _a = this . server ) == null ? void 0 : _a . isRunning ( ) ) {
await this . server . stop ( ) ;
}
}
async loadSettings ( ) {
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
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 ) ;
2026-05-10 02:34:42 +00:00
}
async saveSettings ( ) {
await this . saveData ( this . settings ) ;
}
} ;
2026-05-11 18:11:32 +00:00
var DictionarySettingTab = class extends import _obsidian11 . PluginSettingTab {
2026-05-10 02:34:42 +00:00
constructor ( app , plugin ) {
super ( app , plugin ) ;
this . plugin = plugin ;
}
display ( ) {
const { containerEl } = this ;
containerEl . empty ( ) ;
2026-05-12 19:51:39 +00:00
const usage = containerEl . createDiv ( { cls : "lex-usage-callout" } ) ;
2026-05-10 02:34:42 +00:00
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 ( "." ) ;
2026-05-11 19:31:57 +00:00
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 ) => {
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 ( ) ;
2026-05-11 18:11:32 +00:00
this . display ( ) ;
} )
) ;
2026-05-11 19:31:57 +00:00
if ( this . plugin . settings . dictionarySource === "local" ) {
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" } ) ;
2026-05-11 19:31:57 +00:00
credit . appendText ( "Data: ~167K English entries from Wiktionary via " ) ;
credit . createEl ( "a" , {
text : "MattDodsonEnglish/english-dictionary" ,
href : "https://github.com/MattDodsonEnglish/english-dictionary"
2026-05-11 18:11:32 +00:00
} ) ;
2026-05-11 19:31:57 +00:00
credit . appendText ( "." ) ;
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 (
2026-05-10 02:34:42 +00:00
( text ) => text . setPlaceholder ( "Dictionary" ) . setValue ( this . plugin . settings . folder ) . onChange ( async ( value ) => {
this . plugin . settings . folder = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Setting ( containerEl ) . setName ( "Note naming" ) . setDesc ( "How to capitalize the note filename." ) . addDropdown (
2026-05-10 02:34:42 +00:00
( 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 ( ) ;
} )
) ;
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 (
2026-05-10 02:34:42 +00:00
( 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 ( ) ;
} )
) ;
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Setting ( containerEl ) . setName ( "Create stubs for unknown words" ) . setDesc (
2026-05-11 07:50:43 +00:00
"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 ( ) ;
} )
) ;
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 (
2026-05-10 02:34:42 +00:00
( 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 ) {
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 (
2026-05-10 02:34:42 +00:00
( text ) => text . setPlaceholder ( "_Dictionary List" ) . setValue ( this . plugin . settings . baseName ) . onChange ( async ( value ) => {
this . plugin . settings . baseName = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
}
2026-05-12 19:51:39 +00:00
new import _obsidian11 . Setting ( containerEl ) . setName ( "Kobo eReader import" ) . setHeading ( ) ;
2026-05-10 02:34:42 +00:00
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." ) ;
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 (
2026-05-10 02:34:42 +00:00
( 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 ) {
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 (
2026-05-10 02:34:42 +00:00
folderExists ? ` \u 2713 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 ) {
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Setting ( containerEl ) . setName ( "Create books folder" ) . setDesc ( ` Creates " ${ booksFolderPath } " so wikilinks resolve. ` ) . addButton (
2026-05-10 02:34:42 +00:00
( btn ) => btn . setButtonText ( "Create folder" ) . setCta ( ) . onClick ( async ( ) => {
try {
await this . app . vault . createFolder ( booksFolderPath ) ;
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Notice ( ` Lexophile: created " ${ booksFolderPath } ". ` ) ;
2026-05-10 02:34:42 +00:00
this . display ( ) ;
} catch ( err ) {
2026-05-11 18:11:32 +00:00
new import _obsidian11 . Notice ( ` Lexophile: ${ err . message } ` ) ;
2026-05-10 02:34:42 +00:00
}
} )
) ;
}
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 (
2026-05-10 02:34:42 +00:00
( 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 ( ) ;
} )
) ;
}
2026-05-12 19:51:39 +00:00
new import _obsidian11 . Setting ( containerEl ) . setName ( "Local server" ) . setHeading ( ) ;
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 (
2026-05-10 02:34:42 +00:00
( 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 ( ) ;
}
} )
) ;
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 ) => {
2026-05-10 02:34:42 +00:00
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" ;
} ) ;
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 (
2026-05-10 02:34:42 +00:00
( btn ) => btn . setButtonText ( "Restart" ) . onClick ( async ( ) => {
await this . plugin . stopServer ( ) ;
await this . plugin . startServer ( ) ;
} )
) ;
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
new import _obsidian11 . Setting ( containerEl ) . setName ( "Templates" ) . setHeading ( ) ;
2026-05-10 02:34:42 +00:00
containerEl . createEl ( "p" , {
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
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."
2026-05-10 02:34:42 +00:00
} ) ;
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
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
2026-05-10 02:34:42 +00:00
) ;
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 ( "." ) ;
2026-05-12 19:51:39 +00:00
const bmcWrap = containerEl . createEl ( "p" , { cls : "lex-bmc-link" } ) ;
2026-05-10 02:34:42 +00:00
const bmcLink = bmcWrap . createEl ( "a" , {
href : "https://buymeacoffee.com/bryanmanio"
} ) ;
bmcLink . setAttr ( "target" , "_blank" ) ;
bmcLink . setAttr ( "rel" , "noopener" ) ;
2026-05-12 19:51:39 +00:00
const bmcImg = bmcLink . createEl ( "img" , { cls : "lex-bmc-img" } ) ;
2026-05-10 02:34:42 +00:00
bmcImg . src = "https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" ;
bmcImg . alt = "Buy Me A Coffee" ;
}
Per-entity templates for words and books
Users following Kepano-style vault conventions want every note type to
follow the same frontmatter schema. Previously the plugin had a single
"word note template" and Kobo book stubs were hardcoded to two lines of
frontmatter. Now there are two configurable templates, one per entity
type:
- Word note template (default unchanged from previous behavior).
- Book note template, used by Kobo import when it creates a stub for a
book that isn't already in the vault. Default ships with Kepano-style
fields: tags/type, status, author, series, rating, isbn, date-added,
date-finished — empty values for the user to fill in.
Internals:
- Extract the substitution + frontmatter escape + empty-label-line pass
out of lexicon.ts into src/template.ts. Both lexicon (word notes) and
books (book stubs) now share the same renderer.
- ensureBookStub() takes the template via argument; koboImportModal
passes settings.bookTemplate.
- DictionarySettings.template → wordTemplate. loadSettings() migrates
the legacy `template` key on load so existing vaults keep their word
template across upgrade.
- New renderTemplateEditor() helper builds each labelled textarea +
reset button in the settings tab; the Templates section now has both
editors side by side with the variables documented under each label.
Variables available to the book template: {{title}}, {{date}}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 06:28:22 +00:00
// 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 ( ) ;
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 ( ) ;
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 ) {
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 ) {
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 {
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" ;
}
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 \x B7 ${ mb } MB on disk ` ;
} else {
detail . textContent = "Click Download to fetch the dictionary file." ;
}
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 )
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 \u 2014 ${ 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
} ) ;
}
2026-05-10 02:34:42 +00:00
} ;