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>
- Bump minAppVersion to 1.6.6 (FileManager.trashFile, introduced in 1.6.6,
is now in use by lexicon.createWordNote).
- onunload(): drop the `async` modifier — Plugin.onunload is typed `void`
in obsidian.d.ts and the validator rejects a Promise-returning
override. Cleanup still runs via a fire-and-forget `void (async () => …)`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Source code:
- Bump minAppVersion from 0.15.0 to 1.4.10 (covers
AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder).
- New styles.css with `lex-*` classes. Convert all inline `element.style`
/`cssText` assignments in main.ts, koboImportModal.ts, and
massImportModal.ts to class assignments. Use theme variables instead
of hardcoded colors so the callout works in dark mode.
- Replace `<h2>`/`<h3>` headings in the settings tab with
`new Setting(containerEl).setName(...).setHeading()`. Modal titles
remain on `<h3>` (allowed by Obsidian guidelines).
- Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>`
DOM building in both import done screens.
- Replace `setTimeout` with `window.setTimeout` everywhere (popout
window compatibility).
- `vault.delete(existing)` → `fileManager.trashFile(existing)` so it
respects the user's deletion preference.
- `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working
for both vault-content and config-dir paths.
- Drop the broad `/* eslint-disable */` in familiarityData.ts and the
`@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts
module declaration for `*.wasm`).
- Tighten typing: stricter generics in lexicon.substitute, explicit
`WordEntry` typing in wordModal.submit, `parsed as WordEntry` in
server.handleRequest, typed chunks in server.readBody. Use `void`
operator for fire-and-forget promises.
- Remove unused `DictionaryNotReadyError` imports from koboImportModal
and massImportModal.
- kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use
`subarray(0, 15)` instead of the deprecated `Buffer.slice`.
Build pipeline:
- Replace the `builtin-modules` dependency with Node's built-in
`module.builtinModules`; one fewer transitive dep.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename setting "Where to look up definitions" → "Dictionary source".
- Consolidate the two paragraphs in the section into one short
.setDesc. Drop "thereafter" and "SQL" from the wording.
- API mode shows no extra copy now; the dropdown desc is enough.
- Local mode keeps the status pill + Download button, plus a small
credit line linking to the upstream data project.
- Drop the dictionary download URL setting (we only have one canonical
URL — DEFAULT_DICTIONARY_URL in dictionaryStore.ts — and exposing it
invites broken configurations).
- Rewrite the not-found copy in both mass-import and Kobo done screens:
"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."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #12 removed the API path entirely and made every user commit to a
23MB local download. Two issues:
1. Clicking Download in cloud-synced vaults (Synology Drive, in this
case) failed with ENOENT because vault.adapter.writeBinary does not
create parent directories (confirmed in obsidian.d.ts). The plugin
folder is usually present but isn't guaranteed at write time.
2. Forcing local mode on every user is too aggressive — many users are
happy with the online API.
Changes:
- New settings.dictionarySource: 'api' | 'local', defaulting to 'api'.
Existing users keep the online behaviour they had before #12.
- Restore the API code path (requestUrl + retry-with-backoff on 429/503)
alongside the local SQLite path. lookupWord(store, word, source)
dispatches on the setting.
- dictionaryStore.download() now mkdirs the parent before writeBinary.
adapter.mkdir is idempotent and safe to call repeatedly.
- Modals pick the right inter-request delay per mode: 350ms in API mode
(rate-limit-friendly), 0ms in local mode (just a yield for repaints).
The "dictionary not loaded" guard only fires in local mode.
- Settings UI: new "Dictionary source" dropdown at the top. Local-mode
controls (status pill, download URL, etc.) only render when local is
selected; api mode shows a short note pointing to dictionaryapi.dev.
- README updated to describe both modes; default behaviour is online.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
The free dictionaryapi.dev returns 429 (Too Many Requests) when hit
faster than a few req/s. Importing 100+ words at the previous 150ms
spacing reliably tripped it, leaving most of the batch as "errors" on
the done screen.
Two changes:
1. lookupWord() now retries 429 and 503 with exponential backoff
(1.5s, 3s, 6s, 12s) up to 4 times before giving up. A genuine 429
after retries surfaces a clearer message suggesting a smaller batch.
2. Bump the inter-word spacing in both import modals from 150ms to
350ms (~3 req/s steady-state). Combined with retries, this should
handle realistic batch sizes without tripping the limiter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lets the user paste an arbitrary list of words and import them all in
one go. Accessible via the command palette: "Lexophile: Mass-import
words from a list".
The input parser is lenient — splits on commas, semicolons, and any
whitespace (newlines, tabs, spaces), strips surrounding punctuation,
and dedupes case-insensitively. The user can paste a real comma list,
one word per line, or a messy mix of both.
Flow mirrors the Kobo import: parse -> review wordlist with checkboxes
and dedup pills -> rate-limited progress -> done summary with retroactive
"Create stub notes" button. Per-import stub override defaults to the
global setting. Sources are tagged "manual" so mass-imports are
indistinguishable from single-word adds downstream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Create stubs for unknown words" setting (default off) so words
the dictionary API can't find no longer dead-end the user. When enabled,
those words get a note with empty fields, ready to fill in by hand.
Surfaces:
- Add word modal: looks up the setting and falls back to a stub on
WordNotFoundError, otherwise re-throws.
- Kobo import: per-import checkbox in the word list step (seeded from
the global setting), plus a retroactive "Create stub notes for these
N words" button on the done screen for words skipped during the run.
Internals:
- New WordNotFoundError type so callers can distinguish "no entry" from
network / 5xx / malformed-response errors.
- createStubEntry(word, source) helper in lexicon.ts.
- Kobo source resolution extracted into resolveSource() so the
retroactive stub path reuses the same book-linking logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each word note gets a `familiarity: common | familiar | obscure`
frontmatter property and a column in the auto-generated dictionary base,
giving a rough sense of how recognizable the word is.
Classification uses Peter Norvig's English unigram corpus, trimmed to
30,000 purely-alphabetic words:
- common = top 3,000 (everyday vocabulary)
- familiar = ranks 3K-30K (educated adult vocabulary)
- obscure = everything else
The lookup tries a few simple morphological reductions (drop -s/-es/-ed
/-ing/-ly) so inflected forms still resolve, e.g. "running" → "run".
Bundle impact: main.js grows by ~260KB (1.0M → 1.2M). The frequency
table is stored as newline-delimited blobs and split into Sets at
module load to keep the source file ~2x smaller than JSON literals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move plugin source files from obsidian-plugin/ to repo root (Obsidian
requires manifest.json and main.js at the top level)
- Remove chrome-extension/ (moved to separate repo: bryanmanio/lexophile-chrome-extension)
- Add versions.json mapping 0.1.0 → minAppVersion 0.15.0
- Build and commit main.js (production bundle)
- Update .gitignore for the new layout
- Update README: plugin-only focus, links to Chrome extension repo,
simplified install instructions pointing at GitHub releases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>