PunctFlow is an Obsidian plugin that intelligently converts Chinese punctuation to the corresponding English Markdown punctuation while you type under a Chinese input method — most importantly · (interpunct) → ` (backtick) — reducing the cost of switching between Chinese and English input methods. Core design principles: smart, controllable, and never harmful.
It is an upgraded, generalized version of the dot-to-backtick plugin (which replaces the Chinese interpunct · with a backtick `), extended into a universal smart converter for Chinese punctuation.
| Feature | Description |
|---|---|
| 🔧 Configurable mapping table | Default · → `; add, edit, or remove any mapping in the settings (empty target = delete the character) |
| 🧠 Context awareness | No conversion inside code blocks, inline code, math formulas, URLs, link text, or frontmatter; Chinese personal-name interpuncts (卡尔·马克思) are never converted |
| ⚡ Backtick auto-pairing | Typing · inserts a pair of backticks and places the cursor between them; typing · again skips past the closing backtick so duplicates never appear |
| 🖥 Batch conversion commands | Convert selection / current line / whole document (selection falls back to the current line when nothing is selected) |
| 🎛 Three working modes | Realtime mode (default) / manual mode / paste conversion |
| 🚫 Exclusion rules | Exclude files by folder path or file extension; frontmatter is not converted by default |
| ↩️ Undo merging | Each conversion produces a single transaction, so one Ctrl+Z undoes it |
- Build the plugin (requires Node.js ≥ 18):
npm install npm run build
- Copy the project folder into
<your-vault>/.obsidian/plugins/punctflow/(make sure it containsmain.js,manifest.json, andstyles.css). - In Obsidian, open Settings → Community plugins, and enable PunctFlow.
Plugin ID:
punctflow.
Open Settings → PunctFlow settings:
- Punctuation mapping table: each entry is a "source character → target string". Leaving the target empty deletes the character. Rules are matched from top to bottom (the first hit wins). Use + Add mapping to add, the trash icon to delete, or Reset mappings to restore
·→`. - Auto-pair backticks: when enabled, typing
·inserts a pair of backticks with the cursor centered; typing·again skips past an existing closing backtick. - Working mode:
- Realtime (default): converts immediately as you type a mapped character;
- Manual: never auto-converts; conversion happens only through commands;
- Paste conversion: converts pasted content automatically after a paste.
- Excluded folders: one path per line (relative to the vault root, e.g.
journal); files inside these folders are not converted. - Excluded file extensions: one per line without the dot (e.g.
txt); files with these extensions are not converted.
- Realtime input: with a Chinese input method active, type
·(usually the backtick key) and the plugin turns it into`with auto-pairing; mapped characters such as。or,behave the same way (depending on your mapping table). - Command palette (Ctrl/Cmd + P):
PunctFlow: Convert selection(falls back to the current line when nothing is selected)PunctFlow: Convert current linePunctFlow: Convert whole document
- Paste conversion: switch the working mode to "Paste conversion", then pasted content is converted automatically.
- Type
·at the cursor →`` is inserted and the cursor is placed between the backticks; - Type your code →
`code`; - Type
·once more → the cursor skips past the closing backtick, no duplicate backticks.
```js
// The · inside code blocks is not converted
const a = '卡尔·马克思'; // Chinese personal-name interpuncts are not convertedThe · inside inline code is not converted, $The · inside math is not converted$, link text · not converted.
---
title: The · inside frontmatter is not converted
---Before converting, three levels of checks run (by priority):
- Line scanning: scans from the start of the file to determine whether the current line is inside frontmatter (
---…---) or a fenced code block (```/~~~); - CodeMirror 6 syntax tree: the plugin reads the editor's internal CM6 state through a minimal structured interface and resolves the node at the cursor position with
tree.resolveInner(offset), then walks up the parent chain. If the node name matchescode / math / url / link / image / html / comment / frontmatterand similar, conversion is skipped (special case: if the character right after the cursor is a backtick — the user is closing an inline code span — conversion is allowed); - Inline heuristics (fallback when the syntax tree is unavailable):
- Both sides of the cursor are Chinese characters with an even number of backticks on the line → Chinese personal-name interpunct (
卡尔·马克思), not converted; - Odd numbers of backticks both before and after the cursor → inside inline code, not converted;
- Odd number of
$before the cursor → inside inline math, not converted; https://...immediately before the cursor → inside a URL, not converted.
- Both sides of the cursor are Chinese characters with an even number of backticks on the line → Chinese personal-name interpunct (
When the syntax tree is unavailable, the plugin automatically falls back to the heuristics — no configuration needed.
When a character mapped to a backtick (·) is typed and conversion should happen:
- No backtick after the cursor: insert a pair
``, placing the cursor between them (opening inline code); - A backtick already follows the cursor (e.g. the closing backtick produced by auto-pairing): delete the just-typed character and move the cursor past the existing backtick — this both "closes" the span and avoids duplicates like
`code``; - Odd number of backticks on the line (the opening backtick was typed in English mode, e.g.
`code): insert a single closing backtick.
- Realtime conversion:
cm.dispatch({ changes, selection })performs the replacement and the cursor move in a single CodeMirror transaction, undoable with oneCtrl+Z; - Batch conversion: the whole range is replaced with a single
editor.replaceRange(), also producing a single undo record; - The
isApplyingguard flag prevents the plugin's own edits from triggeringeditor-changeand causing infinite loops.
npm install # install dependencies
npm run dev # watch mode build (main.js)
npm run build # production build (tsc type check + esbuild)
npm test # run the simulation test suite├── main.ts # plugin core logic (including the settings tab)
├── manifest.json # plugin manifest
├── styles.css # settings panel styles
├── esbuild.config.mjs # esbuild build configuration
├── tsconfig.json # TypeScript configuration
├── package.json # dependencies and scripts
├── versions.json # version compatibility manifest
└── .github/workflows/release.yml # release workflow with artifact attestations