HandTranscriptMd

pending

by gabriele-cusato

Inline handwriting canvas with OCR conversion to structured markdown. Requires a free Gemini API key.

Updated 8d agoMITDiscovered via Obsidian Unofficial Plugins
View on GitHub

Handwriting to Markdown β€” Obsidian Plugin

Convert handwritten notes (drawn with a stylus on a canvas) into structured Markdown, directly inside Obsidian. Works on both Windows (desktop) and Android (mobile with stylus).


Table of Contents

  1. User Guide
  2. Project Structure & Architecture
  3. Maintainability Cheat Sheet

User Guide

What the Plugin Does

This plugin embeds a handwriting canvas inside any .md file. You draw or write with a stylus (or mouse), and the plugin can:

  • Save the drawing as an SVG file in your vault β€” visible as an image even without the plugin installed
  • Convert the handwriting to Markdown using Google Gemini OCR, replacing the drawing block with structured text (headings, lists, tables, etc.)

The SVG embed is standard Obsidian wiki syntax (![[_handwriting/hw_xxx.svg]]), so the image appears in any Obsidian view and is readable by tools like Claude Code.


Inserting a Handwriting Block

  1. Open a Markdown file.
  2. Click the pencil ribbon icon in the left sidebar (or run the command Insert handwriting block via Ctrl+P).
  3. A new block ![[_handwriting/hw_xxx.svg]] is inserted at the cursor position.
  4. A portal panel (toolbar overlay) appears on the image. Click the pencil button (✏️) to open the drawing editor.

The Drawing Editor

The editor opens differently depending on your platform:

PlatformHow it opens
Windows (desktop)Full-screen overlay modal on top of your document
Android (mobile)A new Obsidian tab

Toolbar Buttons

ButtonAction
PenSwitch to drawing mode (stylus or mouse draws strokes)
EraserSwitch to eraser mode (drag to erase strokes under the pointer)
Color dots (4)Select the current drawing color
UndoUndo last stroke or erase action
RedoRedo last undone action
ClearRemove all strokes and reset canvas to default size
ConvertRun OCR and replace the drawing block with Markdown text
SaveSave the current drawing as SVG and update the preview
Delete (πŸ—‘οΈ)Delete the handwriting block and its SVG file
Close / ←Close the editor (Windows: close modal; Android: go back)

Drawing Tips

  • Stylus draws, finger scrolls β€” on Android, a finger touch scrolls the canvas; the stylus draws. No conflict.
  • Canvas auto-expands β€” as you draw near the bottom edge, the canvas grows automatically.
  • Horizontal lines β€” the canvas shows ruled lines (like a notebook) as a visual guide; they appear in the saved SVG too.
  • Colors adapt to theme β€” strokes drawn in black on a light theme are automatically remapped to white when you switch to dark theme (and vice versa).

Portal Panel (Inline Controls)

When you hover over a handwriting image in your document, a small floating panel appears with four buttons:

ButtonAction
✏️Open drawing editor
πŸ“„Convert drawing to Markdown (OCR) directly from the preview
↕️Collapse / expand the image preview
βœ•Delete the block and its SVG file

OCR Conversion to Markdown

The plugin uses Google Gemini to recognize handwritten text and converts it to Markdown based on special keywords you write in the drawing.

Supported Keywords

Write these keywords in your drawing to produce structured Markdown output. All keywords start with // and are case-insensitive (//list = //LIST). The colon after the keyword name is optional (//H1 Title and //H1: Title both work).

KeywordSyntaxOutput
//H1//H1 My Title# My Title
//H2//H2 Section## Section
//H3//H3 Sub### Sub
//H4//H4 Sub#### Sub
//LIST//LIST item1, item2, item3bullet list
//NUMLIST//NUMLIST item1, item2numbered list (starts at 1)
//NUMLIST (offset)//NUMLIST 3 item1, item2numbered list starting at 3
//CHECK//CHECK task1, task2checklist (all unchecked)
//CHECK (mixed)//CHECK x done, pending, x also donechecklist with checked/unchecked items
//QUOTE//QUOTE Text> Text
//NOTE//NOTE TitleObsidian callout [!NOTE]
//WARN//WARN TitleObsidian callout [!WARNING]
//TIP//TIP TitleObsidian callout [!TIP]
//INFO//INFO TitleObsidian callout [!INFO]
//ERROR//ERROR TitleObsidian callout [!ERROR]
//IMPORTANT//IMPORTANT TitleObsidian callout [!IMPORTANT]
//CODE//CODE snippet`snippet` (inline code)
//CODEBLOCK//CODEBLOCK js + lines + blank linefenced code block
//B / //BOLD//BOLD text**text**
//I//I text*text*
//BI//BI text***text***
//S / //STRIKE//S text~~text~~
//HL//HL text==text== (highlight)
//LINK//LINK label, url[label](url)
//IMG//IMG alt, url![alt](url)
//TABLE//TABLE Col1, Col2 + rows + //TABLEMarkdown table
//HR / //SEP//HR---
//FN//FN footnote text[^1]: footnote text (auto-numbered)
//MATH//MATH x^2$x^2$ (inline math)
//MATHBLOCK//MATHBLOCK + lines + blank line$$...$$ math block
//TAG//TAG my tag#my_tag
//DATE//DATEtoday's date (YYYY-MM-DD)
//TIME//TIMEcurrent time (HH:MM)
//DATETIME//DATETIMEdate + time
//INDENT//INDENT texttext indented by 2 spaces

Plain text lines (without a // keyword) are inserted as-is.


Multi-line Continuation

Any keyword that accepts a comma-separated list (//LIST, //NUMLIST, //CHECK, //TABLE rows) supports wrapping across lines: if a line ends with a comma, the next line is automatically treated as a continuation.

//LIST groceries, milk, bread,
butter, eggs

Output:

- groceries
- milk
- bread
- butter
- eggs

CHECK with Mixed States

Prefix any item with x or X (with or without brackets) to mark it as already checked:

//CHECK x bought milk, prepare slides, x sent email, review PR

Output:

- [x] bought milk
- [ ] prepare slides
- [x] sent email
- [ ] review PR

Multi-line Callouts

The text on the keyword line becomes the callout title. Any lines that follow (up to the first blank line or next // keyword) become the callout body:

//NOTE Database connection
The connection may fail on an unstable network.
Always verify the timeout in the settings.

Normal paragraph β€” outside the callout.

Output:

> [!NOTE] Database connection
> The connection may fail on an unstable network.
> Always verify the timeout in the settings.

Normal paragraph β€” outside the callout.

After conversion, the SVG is archived to _handwriting/_converted/YYYY-MM-DD_HH-MM-SS.svg and the drawing block is replaced with the generated Markdown.


Settings

Open Settings β†’ Handwriting to Markdown to configure:

SettingDescription
Interface languageLanguage for the settings UI. "Auto" follows Obsidian's language.
SVG folderVault subfolder where SVG drawing files are saved (default: _handwriting)
Canvas width / heightDefault canvas resolution in pixels
Canvas backgroundLight / Dark / Auto (follows Obsidian theme)
Gemini API keyRequired for OCR. Get it free at aistudio.google.com.
OCR languagesComma-separated BCP-47 codes (e.g. it, en, fr). Tells Gemini which languages to expect.

Note β€” Free API key limitations: With the free tier of Google AI Studio, your data may be used by Google to improve their models. Additionally, under high traffic you may see a "Too many requests β€” please try again later" error. To avoid rate limits, enable billing on Google AI Studio; costs are minimal for occasional OCR use.


Platform Support

FeatureWindowsAndroid
Drawing (stylus/mouse)βœ…βœ…
Finger scroll while drawingβ€”βœ…
OCR conversionβœ…βœ…
Editor opens as modalβœ…β€”
Editor opens as new tabβ€”βœ…
Collapse/expand previewβœ…βœ…

Project Structure & Architecture

This section explains how the codebase is organized, how Obsidian's plugin system works, and which file to open for any given task.


Folder & File Layout

HandTranscriptMd/
β”‚
β”œβ”€β”€ src/                        ← all TypeScript source files
β”‚   β”œβ”€β”€ main.ts                 ← plugin entry point (class HandwritingPlugin)
β”‚   β”œβ”€β”€ settings.ts             ← settings definition, defaults, settings tab UI
β”‚   β”œβ”€β”€ i18n.ts                 ← translation loader and t() helper
β”‚   β”œβ”€β”€ locales/                ← one JSON file per language
β”‚   β”‚   β”œβ”€β”€ en.json             ← English (the fallback β€” always the reference)
β”‚   β”‚   β”œβ”€β”€ it.json
β”‚   β”‚   β”œβ”€β”€ de.json  fr.json  es.json  ru.json  ja.json
β”‚   β”‚   β”œβ”€β”€ zh-cn.json  pt-br.json  pl.json
β”‚   β”œβ”€β”€ drawing-canvas.ts       ← HTML Canvas drawing engine (strokes, eraser, undo)
β”‚   β”œβ”€β”€ svg-utils.ts            ← SVG ↔ strokes serialization, PNG conversion, archive
β”‚   β”œβ”€β”€ embed.ts                ← inline preview decoration + portal panel
β”‚   β”œβ”€β”€ editor-view.ts          ← drawing editor (modal on Windows, tab on Android)
β”‚   β”œβ”€β”€ recognizer.ts           ← Gemini OCR interface + HTTP call
β”‚   β”œβ”€β”€ md-parser.ts            ← keyword-based OCR text β†’ Markdown converter
β”‚   └── parser.test.ts          ← unit tests for the markdown parser
β”‚
β”œβ”€β”€ main.js                     ← ⚠ compiled output (generated by esbuild, do not edit)
β”œβ”€β”€ styles.css                  ← all plugin CSS (classes prefixed with hwm_)
β”œβ”€β”€ manifest.json               ← plugin metadata (id, name, version, minAppVersion)
β”œβ”€β”€ package.json                ← npm dependencies, build scripts
β”œβ”€β”€ esbuild.config.mjs          ← build configuration (entry: src/main.ts β†’ main.js)
β”œβ”€β”€ deploy.sh                   ← copies main.js + manifest.json + styles.css to local vault
β”œβ”€β”€ cloudDeploy.sh              ← same but to Google Drive vault (for Android testing)
β”œβ”€β”€ README.md                   ← this file
β”œβ”€β”€ CLAUDE.md                   ← context notes for Claude Code AI assistant
└── NOTES.md                    ← developer session log, resolved bugs, completed tasks

The three files that Obsidian loads are: main.js, manifest.json, styles.css. Everything under src/ is TypeScript source that gets compiled down to the single main.js by esbuild.


How an Obsidian Plugin Works

Obsidian plugins are JavaScript modules that run inside the Obsidian Electron app (desktop) or WebView (mobile). The key concepts:

1. The Plugin Class (src/main.ts)

Every plugin exports a default class that extends Obsidian's Plugin. Obsidian calls onload() when the plugin is enabled and onunload() when it is disabled.

export default class HandwritingPlugin extends Plugin {
    async onload() { /* register everything here */ }
}

Inside onload() this plugin registers:

  • A view type (registerView) β€” the drawing editor tab on Android
  • A code block processor (registerMarkdownCodeBlockProcessor) β€” renders handwriting code blocks
  • Commands (addCommand) β€” appear in Ctrl+P palette
  • A ribbon icon (addRibbonIcon) β€” the pencil button in the left sidebar
  • A settings tab (addSettingTab)
  • Event listeners (registerEvent) β€” e.g. the right-click file menu

The plugin class also carries three shared state maps used to coordinate between the preview (embed.ts) and the editor (editor-view.ts):

  • previewCallbacks β€” after a save, the editor calls refreshPreview() to update the inline image
  • embedPaths β€” maps embed IDs to SVG file paths, used for color remapping on theme change
  • bgModeListeners β€” Set of callbacks notified when the background mode setting changes
  • embedActions β€” maps embed IDs to their expand/collapse/convert functions, used by the right-click menu

2. The Vault API

The Vault is Obsidian's file system abstraction. Use this.app.vault (or plugin.app.vault) to read/write files:

// Read a file as text
const content = await plugin.app.vault.read(tFile);

// Write / overwrite a file
await plugin.app.vault.modify(tFile, newContent);

// Create a file
await plugin.app.vault.create(path, content);

// Move / rename
await plugin.app.vault.rename(tFile, newPath);

A TFile is Obsidian's object for a file. Get one with:

const file = plugin.app.vault.getAbstractFileByPath('folder/name.md');

3. The Workspace API

The Workspace manages the layout of open tabs and panels. Used to open the editor tab on Android:

const leaf = plugin.app.workspace.getLeaf('tab'); // open in a new tab
await leaf.setViewState({ type: VIEW_TYPE_HANDWRITING, state: { ... } });

4. ItemView β€” The Drawing Editor Tab (src/editor-view.ts)

DrawingEditorView extends ItemView is an Obsidian custom view β€” a full tab with its own DOM. Key lifecycle methods:

  • getViewType() β€” returns a unique string ID ('handwriting-editor')
  • getDisplayText() β€” the tab title
  • onOpen() β€” called when the tab opens; here buildEditor() is called to build the canvas UI
  • onClose() β€” called when the tab closes; cleanup (remove listeners, disconnect observers)

The view receives data (which SVG to load, which MD file to update) via leaf.setViewState({ state: { svgPath, sourcePath, embedId } }), read back in getState().

5. Modal β€” The Desktop Drawing Overlay (src/editor-view.ts)

DrawingModal extends Modal is an Obsidian modal dialog β€” a fullscreen overlay on desktop. Key methods:

  • onOpen() β€” builds the canvas UI by calling buildEditor()
  • onClose() β€” cleanup
  • this.close() β€” closes the modal programmatically (used in the ← and βœ• buttons)

Modal and ItemView are completely different Obsidian base classes, which is why buildEditorUI() was extracted as a shared standalone function β€” both classes call it and pass their specific callbacks for save/close/delete.

6. Code Block Processor (Legacy Format)

registerMarkdownCodeBlockProcessor('handwriting', callback) tells Obsidian: "when you render a ```handwriting ``` block, run my callback instead." The callback receives the block source text and the DOM element to fill. This is the legacy embed format.

7. MutationObserver (Wiki Format)

For the new ![[svg]] format, Obsidian renders the embed itself as a <span class="internal-embed image-embed">. The plugin cannot intercept this with a code block processor. Instead, a MutationObserver watches document.body for new nodes and decorates any span whose src attribute points to the _handwriting/ folder. This happens in registerEmbed() in embed.ts.

8. Settings (src/settings.ts)

Settings are stored as a JSON object in Obsidian's data.json (inside the plugin folder). plugin.loadData() reads it; plugin.saveData(obj) writes it. The HandwritingSettings interface defines the shape; DEFAULT_SETTINGS provides initial values. HandwritingSettingTab extends PluginSettingTab builds the settings UI using new Setting(containerEl).

9. The Build System

esbuild bundles all TypeScript files starting from src/main.ts into a single main.js. The obsidian package is marked external β€” it is provided at runtime by Obsidian itself and must never be bundled. esbuild does not run TypeScript type-checking β€” type errors are invisible at build time. To catch them: npx tsc --noEmit.

Two build modes:

  • npm run dev β†’ watch mode, inline sourcemap, not minified
  • node esbuild.config.mjs production β†’ single build, minified, no sourcemap

What File to Open for a Given Task

I want to…Open this file
Change what happens when the plugin loads/unloadssrc/main.ts β†’ onload() / onunload()
Add or remove a command (Ctrl+P)src/main.ts β†’ this.addCommand(...)
Add or remove the ribbon iconsrc/main.ts β†’ this.addRibbonIcon(...)
Add an item to the right-click file menusrc/main.ts β†’ this.app.workspace.on('file-menu', ...)
Change a setting (add field, change default, add UI control)src/settings.ts β†’ HandwritingSettings, DEFAULT_SETTINGS, HandwritingSettingTab.display()
Change the color palette for light/dark themesrc/settings.ts β†’ LIGHT_COLORS, DARK_COLORS
Change how "is dark mode" is resolvedsrc/settings.ts β†’ resolveIsDark()
Add or fix a translation stringsrc/locales/en.json first, then all other locale files
Add a new interface languagesrc/locales/XX.json + src/i18n.ts β†’ locales map + localeNames
Change how the t() lookup or fallback workssrc/i18n.ts
Change drawing behavior (stroke, eraser, pressure, auto-expand)src/drawing-canvas.ts β†’ DrawingCanvas class
Change the ruler line spacingsrc/drawing-canvas.ts β†’ export const LINE_SPACING
Change how strokes are saved into / read from SVGsrc/svg-utils.ts β†’ strokesToSvg(), svgToStrokes()
Change how the SVG is converted to a PNG for OCRsrc/svg-utils.ts β†’ svgToBase64Png()
Change where archived SVGs go after conversionsrc/svg-utils.ts β†’ archiveSvgFile()
Change how the inline image preview is decoratedsrc/embed.ts β†’ tryDecorate(), decorateWikiEmbed()
Add or change buttons in the portal panel overlaysrc/embed.ts β†’ createPortalPanel()
Change the OCR pipeline (what happens when "Convert" is clicked from the preview)src/embed.ts β†’ runOcrPipeline()
Change the drawing editor toolbar or canvas layoutsrc/editor-view.ts β†’ buildEditorUI()
Change behavior specific to the desktop modal onlysrc/editor-view.ts β†’ DrawingModal class
Change behavior specific to the Android tab onlysrc/editor-view.ts β†’ DrawingEditorView class
Change the save / delete / convert logic inside the editorsrc/editor-view.ts β†’ DrawingModal.doSave/doConvert/doDelete or DrawingEditorView.doSave/doConvert/doDelete
Change which OCR model is called or the prompt sent to Geminisrc/recognizer.ts β†’ GeminiRecognizer.recognize()
Change how OCR text is parsed into Markdown keywordssrc/md-parser.ts β†’ parseHandwritingToMarkdown(), expandKeywords()
Change how //TABLE blocks are parsedsrc/md-parser.ts β†’ table handling logic inside parseHandwritingToMarkdown()
Change plugin CSS (colors, sizes, layout)styles.css
Change the plugin versionmanifest.json + package.json (both must match)
Change the build configurationesbuild.config.mjs
Change the deploy target path (local vault)deploy.sh β†’ VAULT_PLUGIN variable
Change the deploy target path (Google Drive / Android)cloudDeploy.sh β†’ VAULT_PLUGIN variable

Data Flow: From Drawing to Saved SVG

User draws strokes on <canvas>
        β”‚
        β–Ό
DrawingCanvas (drawing-canvas.ts)
  stores strokes as Stroke[] array in memory
        β”‚
        β–Ό  (on Save button or auto-save debounce)
saveSvgToDisk()  ─── editor-view.ts (module-level helper)
        β”‚
        β–Ό
strokesToSvg()  ─── svg-utils.ts
  builds an SVG string:
  - <path> elements for each BΓ©zier stroke
  - <line> elements for ruler lines
  - <desc class="hwm-strokes"> with JSON of all strokes (for re-editing)
        β”‚
        β–Ό
plugin.app.vault.modify(tFile, svgString)
  saves the .svg file to the vault
        β”‚
        β–Ό
plugin.refreshPreview(embedId, svgString)
  calls the previewCallback registered by embed.ts
        β”‚
        β–Ό
embed.ts updates img.src with a cache-busting ?t=timestamp
  so the inline preview refreshes without reloading the page

Data Flow: From Drawing to Markdown (OCR)

User clicks Convert (in editor toolbar or portal panel)
        β”‚
        β–Ό
runOcrPipeline() / doConvert()
        β”‚
        β”œβ”€ reads SVG content from vault
        β”œβ”€ parses SVG to DOM via DOMParser
        β”‚
        β–Ό
svgToBase64Png()  ─── svg-utils.ts
  draws SVG onto a temporary <canvas>
  exports as base64 PNG via canvas.toDataURL()
        β”‚
        β–Ό
GeminiRecognizer.recognize(base64)  ─── recognizer.ts
  POST to Gemini REST API with inline_data (image) + text prompt
  returns recognized text as a plain string
        β”‚
        β–Ό
parseHandwritingToMarkdown(text)  ─── md-parser.ts
  splits text into lines
  maps //keywords β†’ Markdown syntax
  returns final Markdown string
        β”‚
        β–Ό
replaceInMdFile()  ─── editor-view.ts (module-level helper)
  reads the .md source file
  finds the ![[svg]] embed line via regex
  replaces it with the Markdown text
  writes the .md file back to vault
        β”‚
        β–Ό
archiveSvgFile()  ─── svg-utils.ts
  moves the .svg from _handwriting/ to _handwriting/_converted/YYYY-MM-DD_HH-MM-SS.svg

CSS Class Naming Convention

All plugin CSS classes use the hwm_ prefix (short for HandWriting Markdown) to avoid collisions with Obsidian's own classes or other plugins.

Examples: hwm_portal-panel, hwm_portal-btn, hwm_modal, hwm_toolbar, hwm-badge-mode.

All styles live in styles.css at the project root. There is no CSS-in-JS.


Maintainability Cheat Sheet

This section is a quick reference for developers who need to extend or modify the plugin. Assumes familiarity with TypeScript and the Obsidian Plugin API.


How to Add a Toolbar Button

The entire toolbar for both the desktop modal and the Android tab is built by the shared function buildEditorUI() in src/editor-view.ts. You only need to edit one place.

  1. Add the i18n key (see How to Add a Language Key).
  2. Inside buildEditorUI(), find the toolbar section and call mkBtn(toolbar, 'icon-name', 'your_i18n_key').
    • mkBtn returns the button element if you need to attach a click handler.
  3. Add the click handler immediately after: btn.addEventListener('click', () => { ... }).

mkBtn(parent, icon, key) is a module-level helper that creates a <button> with the Obsidian icon and the localized title attribute.

Why one place? Before the refactor, DrawingEditorView.buildEditor() and DrawingModal.buildEditor() were two separate copies. The buildEditorUI() function eliminates that duplication.


How to Add a Portal Panel Button

The portal panel (the floating overlay on the preview image) is built in src/embed.ts inside createPortalPanel().

  1. Add the i18n key.
  2. Create a button element: const btn = panel.createEl('button', { cls: 'hwm_portal-btn' }).
  3. Set its icon: setIcon(btn, 'icon-name') and tooltip: btn.title = t('your_key', plugin).
  4. Add the click handler.

How to Add an Obsidian Command (Shortcut)

Commands are registered in src/main.ts inside onload(), using this.addCommand({...}).

this.addCommand({
  id: 'your-command-id',
  name: 'Human readable name',   // shown in Ctrl+P palette
  callback: () => { /* your logic */ },
  // optional: hotkeys: [{ modifiers: ['Ctrl'], key: 'K' }]
});

Obsidian users can reassign hotkeys in Settings β†’ Hotkeys.


How to Add a Ribbon Icon

Ribbon icons are registered in src/main.ts inside onload().

this.addRibbonIcon('icon-name', 'Tooltip text', (evt) => {
  /* your logic */
});

Find icon names in the Obsidian Lucide icon set.


How to Add a Language Key (i18n)

The plugin has a simple i18n system. Locale files live in src/locales/.

  1. Add the new key to every locale file (en.json, it.json, de.json, fr.json, es.json, ru.json, ja.json, zh-cn.json, pt-br.json, pl.json). Always start with en.json (the fallback language).
  2. Use the t('your_key', plugin) helper wherever you need the translated string.

The t() function falls back to en.json if the key is missing in the active locale.


How to Add a New Language

  1. Create src/locales/XX.json (where XX is the BCP-47 code, e.g. ko for Korean).
  2. Copy all keys from en.json and translate the values.
  3. In src/settings.ts, add the language to the UI_LANGUAGES array:
    { code: 'ko', label: 'ν•œκ΅­μ–΄' }
    
  4. In src/settings.ts, update the dynamic import() switch inside the loadLocale() function (or equivalent loader) to handle the new code.

How to Add a Setting

Settings are defined in src/settings.ts.

  1. Add the new field to the HandwritingSettings interface and to DEFAULT_SETTINGS.
  2. In HandwritingSettingTab.display(), add a new Setting(containerEl) block with .setName(t(...)), .setDesc(t(...)), and the appropriate control (.addText(), .addToggle(), .addDropdown(), etc.).
  3. Save the value in the control's onChange callback: this.plugin.settings.yourField = value; await this.plugin.saveSettings();.

How to Add an OCR Keyword

Keywords are parsed in src/md-parser.ts and documented in src/settings.ts.

Rule: both files must be updated together. They must stay in sync.

  1. src/md-parser.ts β€” in expandKeywords(), add a new case (or if/else) for the new //KEYWORD. Return the corresponding Markdown string.
  2. src/settings.ts β€” in the KEYWORDS constant (displayed in the settings table), add a new row:
    { keyword: '//KEYWORD', syntax: '//KEYWORD text', output: 'markdown output' }
    

How to Update the Plugin Version

The version is declared in two files that must be kept in sync:

  • package.json β†’ "version" field
  • manifest.json β†’ "version" field

The settings page reads the version from plugin.manifest.version at runtime, so no code changes are needed in TypeScript.


How to Add a Third Embed Format

Currently the plugin supports two embed formats:

  • Wiki (new default): ![[_handwriting/hw_xxx.svg]]
  • Legacy code block: ```handwriting {"id":"...", "svg":"..."}```

To add a third format:

  1. Registration β€” in src/main.ts β†’ onload(), register a new processor (e.g. this.registerMarkdownCodeBlockProcessor('new-format', ...) or a new MutationObserver pattern).
  2. Detection β€” in src/embed.ts, the tryDecorate() function checks for the wiki format. Add detection logic for your new format alongside it.
  3. Read/Write β€” in src/editor-view.ts, the module-level helpers wikiEmbedRegex() / codeBlockRegex() and replaceInMdFile() handle finding and replacing the embed text in the .md file. Add a new regex + replacement branch for the new format. The doSave, doConvert, and doDelete callbacks passed to buildEditorUI() call these helpers β€” update them to try the new format as well.
  4. Backward compat β€” always try the new format first, then fall back to wiki, then legacy code block (follow the existing fallback pattern in replaceInMdFile).

Key File Map

FileResponsibility
src/main.tsPlugin entry point: commands, ribbon, embed registration, settings, MutationObserver
src/settings.tsSettings interface, defaults, tab UI, i18n loader, LIGHT_COLORS, DARK_COLORS, resolveIsDark()
src/drawing-canvas.tsCanvas drawing engine: BΓ©zier strokes, eraser, undo/redo, auto-expand, LINE_SPACING
src/svg-utils.tsSVG ↔ strokes serialization, svgToBase64Png(), archiveSvgFile()
src/embed.tsPreview decoration (wiki + legacy), portal panel, OCR pipeline runner
src/editor-view.tsbuildEditorUI() shared builder, DrawingEditorView (Android tab), DrawingModal (desktop)
src/recognizer.tsIRecognizer interface + GeminiRecognizer (REST call to Gemini)
src/md-parser.tsparseHandwritingToMarkdown(): keyword expansion, OCR text β†’ Markdown
src/locales/*.jsonLocale strings for each supported language

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.