Commit graph

11 commits

Author SHA1 Message Date
Bryan Maniotakis
d8f0076311 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 00:28:22 -06:00
Bryan Maniotakis
31e5ef5296 Fix last two submission warnings; release 1.2.1
- 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>
2026-05-12 14:53:22 -06:00
Bryan Maniotakis
5ca2254589 Address Obsidian community plugin submission warnings
Source code:
- Bump minAppVersion from 0.15.0 to 1.4.10 (covers
  AbstractInputSuggest, ButtonComponent.setDisabled, Vault.createFolder).
- New styles.css with `lex-*` classes. Convert all inline `element.style`
  /`cssText` assignments in main.ts, koboImportModal.ts, and
  massImportModal.ts to class assignments. Use theme variables instead
  of hardcoded colors so the callout works in dark mode.
- Replace `<h2>`/`<h3>` headings in the settings tab with
  `new Setting(containerEl).setName(...).setHeading()`. Modal titles
  remain on `<h3>` (allowed by Obsidian guidelines).
- Replace `innerHTML = totalLines.map(...).join('<br>')` with `<ul><li>`
  DOM building in both import done screens.
- Replace `setTimeout` with `window.setTimeout` everywhere (popout
  window compatibility).
- `vault.delete(existing)` → `fileManager.trashFile(existing)` so it
  respects the user's deletion preference.
- `vault.createFolder` → `vault.adapter.mkdir` in ensureFolder, working
  for both vault-content and config-dir paths.
- Drop the broad `/* eslint-disable */` in familiarityData.ts and the
  `@ts-ignore` in sqlite.ts (replaced with the existing src/types.d.ts
  module declaration for `*.wasm`).
- Tighten typing: stricter generics in lexicon.substitute, explicit
  `WordEntry` typing in wordModal.submit, `parsed as WordEntry` in
  server.handleRequest, typed chunks in server.readBody. Use `void`
  operator for fire-and-forget promises.
- Remove unused `DictionaryNotReadyError` imports from koboImportModal
  and massImportModal.
- kobo.ts: drop unnecessary `\-` escape in TRIM_CHARS and use
  `subarray(0, 15)` instead of the deprecated `Buffer.slice`.

Build pipeline:
- Replace the `builtin-modules` dependency with Node's built-in
  `module.builtinModules`; one fewer transitive dep.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:51:39 -06:00
Bryan Maniotakis
879058d07d Tighten dictionary source UI copy
- 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>
2026-05-11 13:31:57 -06:00
Bryan Maniotakis
1469c1144d Make dictionary source a user setting; fix ENOENT on local download
PR #12 removed the API path entirely and made every user commit to a
23MB local download. Two issues:

1. Clicking Download in cloud-synced vaults (Synology Drive, in this
   case) failed with ENOENT because vault.adapter.writeBinary does not
   create parent directories (confirmed in obsidian.d.ts). The plugin
   folder is usually present but isn't guaranteed at write time.
2. Forcing local mode on every user is too aggressive — many users are
   happy with the online API.

Changes:

- New settings.dictionarySource: 'api' | 'local', defaulting to 'api'.
  Existing users keep the online behaviour they had before #12.
- Restore the API code path (requestUrl + retry-with-backoff on 429/503)
  alongside the local SQLite path. lookupWord(store, word, source)
  dispatches on the setting.
- dictionaryStore.download() now mkdirs the parent before writeBinary.
  adapter.mkdir is idempotent and safe to call repeatedly.
- Modals pick the right inter-request delay per mode: 350ms in API mode
  (rate-limit-friendly), 0ms in local mode (just a yield for repaints).
  The "dictionary not loaded" guard only fires in local mode.
- Settings UI: new "Dictionary source" dropdown at the top. Local-mode
  controls (status pill, download URL, etc.) only render when local is
  selected; api mode shows a short note pointing to dictionaryapi.dev.
- README updated to describe both modes; default behaviour is online.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:11:32 -06:00
Bryan Maniotakis
cc355c5fbd 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 02:44:09 -06:00
Bryan Maniotakis
9a8036ee2c Handle 429 rate limits from the Dictionary API
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>
2026-05-11 02:12:20 -06:00
Bryan Maniotakis
33167a3361 Add mass-import command
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>
2026-05-11 02:06:46 -06:00
Bryan Maniotakis
93adc7a453 Create stub notes for words not in the dictionary
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>
2026-05-11 01:50:43 -06:00
Bryan Maniotakis
5ae7108cb0 Add familiarity index for words
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>
2026-05-11 01:31:40 -06:00
Bryan Maniotakis
156c7ec05f Prepare for Obsidian community plugin submission
- 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>
2026-05-09 20:34:42 -06:00