mirror of
https://github.com/kennkyou/obsidian-kobo-note-sync.git
synced 2026-07-22 06:05:46 +00:00
feat: initial release of Kobo Note Sync
Import Kobo highlights and annotations into Obsidian with customizable Eta.js templates, cross-platform support (macOS/Windows), append-only sync, and i18n (en/zh-TW).
This commit is contained in:
commit
788961859f
9 changed files with 1570 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.idea
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 kenkyou
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
129
README.md
Normal file
129
README.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Kobo Note Sync
|
||||
|
||||
[繁體中文](README.zh-TW.md)
|
||||
|
||||
An Obsidian plugin that imports highlights and annotations from your Kobo e-reader into Obsidian notes.
|
||||
|
||||
## Features
|
||||
|
||||
- Import highlights and annotations from Kobo Desktop App's SQLite database
|
||||
- Customizable note and highlight templates using [Eta.js](https://eta.js.org/) syntax
|
||||
- Append-only sync — existing highlights in Obsidian are preserved even if deleted on Kobo
|
||||
- Rich metadata support (23 book variables + 5 highlight variables)
|
||||
- Cross-platform: macOS and Windows
|
||||
- i18n: English and Traditional Chinese (follows Obsidian language setting)
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Kobo Device / App → Kobo Cloud → Kobo Desktop App → Kobo.sqlite → This Plugin → Obsidian Notes
|
||||
```
|
||||
|
||||
The plugin reads the local `Kobo.sqlite` database created by the Kobo Desktop App and generates Markdown notes in your vault.
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Download `main.js`, `manifest.json`, and `sql-wasm.wasm` from the [latest release](https://github.com/KennKyou/obsidian-kobo-note-sync/releases)
|
||||
2. Create a folder `kobo-note-sync` in your vault's `.obsidian/plugins/` directory
|
||||
3. Copy the downloaded files into that folder
|
||||
4. Restart Obsidian and enable the plugin in Settings → Community Plugins
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Kobo Desktop App](https://www.kobo.com/desktop) installed and signed in with your Kobo account
|
||||
- Sync your library in Kobo Desktop App so that `Kobo.sqlite` is up to date
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open Command Palette (`Cmd/Ctrl + P`) and run **Kobo Note Sync**
|
||||
2. Or click the book icon in the left sidebar
|
||||
|
||||
Highlights will be created as Markdown files in the configured output folder (default: `Kobo Note Sync/`).
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| **Kobo database path** | Path to `Kobo.sqlite`. Auto-detected based on your OS. |
|
||||
| **Output folder** | Folder in your vault for highlight notes |
|
||||
| **Note template** | Eta.js template for each book note |
|
||||
| **Highlight template** | Eta.js template for each highlight |
|
||||
|
||||
### Default Database Paths
|
||||
|
||||
| OS | Path |
|
||||
|----|------|
|
||||
| macOS | `~/Library/Application Support/Kobo/Kobo Desktop Edition/Kobo.sqlite` |
|
||||
| Windows | `%LOCALAPPDATA%\Kobo\Kobo Desktop Edition\Kobo.sqlite` |
|
||||
|
||||
## Templates
|
||||
|
||||
Templates use [Eta.js](https://eta.js.org/) syntax: `<%= it.variableName %>` for output, `<% if (it.condition) { %>` for logic.
|
||||
|
||||
### Note Template Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `it.bookTitle` | Book title |
|
||||
| `it.bookAuthor` | Author |
|
||||
| `it.publisher` | Publisher |
|
||||
| `it.isbn` | ISBN |
|
||||
| `it.series` | Series name |
|
||||
| `it.seriesNumber` | Series number |
|
||||
| `it.subtitle` | Subtitle |
|
||||
| `it.language` | Language |
|
||||
| `it.description` | Book description (HTML stripped) |
|
||||
| `it.progress` | Reading progress % |
|
||||
| `it.status` | Reading status (`unread` / `reading` / `finished`) |
|
||||
| `it.numPages` | Number of pages |
|
||||
| `it.wordCount` | Word count |
|
||||
| `it.averageRating` | Average rating on Kobo |
|
||||
| `it.ratingCount` | Rating count |
|
||||
| `it.timeSpentReading` | Time spent reading (seconds) |
|
||||
| `it.timesStartedReading` | Times started reading |
|
||||
| `it.dateAdded` | Date added to library |
|
||||
| `it.dateCreated` | Book creation date |
|
||||
| `it.lastRead` | Last read date |
|
||||
| `it.lastFinished` | Finished reading date |
|
||||
| `it.highlightCount` | Number of highlights |
|
||||
| `it.highlights` | Rendered highlights content |
|
||||
|
||||
### Highlight Template Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `it.highlightText` | Highlighted text |
|
||||
| `it.annotation` | Your note/annotation |
|
||||
| `it.dateCreated` | Highlight date |
|
||||
| `it.chapter` | Chapter title |
|
||||
| `it.chapterProgress` | Progress within chapter (0-1) |
|
||||
|
||||
### Example Templates
|
||||
|
||||
**Note template:**
|
||||
|
||||
```
|
||||
---
|
||||
title: "<%= it.bookTitle %>"
|
||||
author: "<%= it.bookAuthor %>"
|
||||
publisher: "<%= it.publisher %>"
|
||||
last_read: <%= it.lastRead %>
|
||||
highlight_count: <%= it.highlightCount %>
|
||||
source: Kobo
|
||||
---
|
||||
<%= it.highlights %>
|
||||
```
|
||||
|
||||
**Highlight template:**
|
||||
|
||||
```
|
||||
*<%= it.dateCreated %>*<% if (it.chapter) { %> | <%= it.chapter %><% } %>
|
||||
|
||||
> <%= it.highlightText %>
|
||||
<% if (it.annotation) { %>
|
||||
**Note:** <%= it.annotation %>
|
||||
<% } %>
|
||||
---
|
||||
```
|
||||
129
README.zh-TW.md
Normal file
129
README.zh-TW.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Kobo Note Sync
|
||||
|
||||
[English](README.md)
|
||||
|
||||
一款 Obsidian 插件,將 Kobo 電子書的畫線與筆記匯入 Obsidian。
|
||||
|
||||
## 功能
|
||||
|
||||
- 從 Kobo Desktop App 的 SQLite 資料庫匯入畫線與筆記
|
||||
- 使用 [Eta.js](https://eta.js.org/) 語法自訂筆記與畫線模板
|
||||
- 僅新增同步 — 即使在 Kobo 上刪除畫線,Obsidian 中的紀錄仍會保留
|
||||
- 豐富的 metadata 支援(23 個書籍變數 + 5 個畫線變數)
|
||||
- 跨平台:macOS 與 Windows
|
||||
- 多語系:英文與繁體中文(自動跟隨 Obsidian 語言設定)
|
||||
|
||||
## 運作原理
|
||||
|
||||
```
|
||||
Kobo 閱讀器 / App → Kobo 雲端 → Kobo Desktop App → Kobo.sqlite → 本插件 → Obsidian 筆記
|
||||
```
|
||||
|
||||
插件會讀取 Kobo Desktop App 建立的本機 `Kobo.sqlite` 資料庫,將畫線生成為 Markdown 筆記。
|
||||
|
||||
## 安裝
|
||||
|
||||
### 手動安裝
|
||||
|
||||
1. 從 [最新版本](https://github.com/KennKyou/obsidian-kobo-note-sync/releases) 下載 `main.js`、`manifest.json` 和 `sql-wasm.wasm`
|
||||
2. 在 Vault 的 `.obsidian/plugins/` 目錄下建立 `kobo-note-sync` 資料夾
|
||||
3. 將下載的檔案複製到該資料夾
|
||||
4. 重新啟動 Obsidian,在 設定 → 社群插件 中啟用
|
||||
|
||||
### 前置需求
|
||||
|
||||
- 安裝 [Kobo Desktop App](https://www.kobo.com/desktop) 並登入你的 Kobo 帳號
|
||||
- 在 Kobo Desktop App 中同步書庫,確保 `Kobo.sqlite` 為最新狀態
|
||||
|
||||
## 使用方式
|
||||
|
||||
1. 開啟命令面板(`Cmd/Ctrl + P`)執行 **Kobo Note Sync**
|
||||
2. 或點擊左側工具列的書本圖示
|
||||
|
||||
畫線會以 Markdown 檔案儲存在設定的輸出資料夾中(預設:`Kobo Note Sync/`)。
|
||||
|
||||
## 設定
|
||||
|
||||
| 設定項目 | 說明 |
|
||||
|---------|------|
|
||||
| **Kobo 資料庫路徑** | `Kobo.sqlite` 的路徑,會根據作業系統自動偵測 |
|
||||
| **輸出資料夾** | Vault 中儲存畫線筆記的資料夾 |
|
||||
| **筆記模板** | 每本書的 Eta.js 模板 |
|
||||
| **畫線模板** | 每條畫線的 Eta.js 模板 |
|
||||
|
||||
### 預設資料庫路徑
|
||||
|
||||
| 作業系統 | 路徑 |
|
||||
|---------|------|
|
||||
| macOS | `~/Library/Application Support/Kobo/Kobo Desktop Edition/Kobo.sqlite` |
|
||||
| Windows | `%LOCALAPPDATA%\Kobo\Kobo Desktop Edition\Kobo.sqlite` |
|
||||
|
||||
## 模板
|
||||
|
||||
模板使用 [Eta.js](https://eta.js.org/) 語法:`<%= it.variableName %>` 輸出變數,`<% if (it.condition) { %>` 做條件判斷。
|
||||
|
||||
### 筆記模板變數
|
||||
|
||||
| 變數 | 說明 |
|
||||
|------|------|
|
||||
| `it.bookTitle` | 書名 |
|
||||
| `it.bookAuthor` | 作者 |
|
||||
| `it.publisher` | 出版社 |
|
||||
| `it.isbn` | ISBN |
|
||||
| `it.series` | 系列名稱 |
|
||||
| `it.seriesNumber` | 系列編號 |
|
||||
| `it.subtitle` | 副標題 |
|
||||
| `it.language` | 語言 |
|
||||
| `it.description` | 書籍簡介(已去除 HTML 標籤) |
|
||||
| `it.progress` | 閱讀進度 % |
|
||||
| `it.status` | 閱讀狀態(`unread` / `reading` / `finished`) |
|
||||
| `it.numPages` | 頁數 |
|
||||
| `it.wordCount` | 字數 |
|
||||
| `it.averageRating` | Kobo 平均評分 |
|
||||
| `it.ratingCount` | 評分數量 |
|
||||
| `it.timeSpentReading` | 閱讀花費時間(秒) |
|
||||
| `it.timesStartedReading` | 開始閱讀次數 |
|
||||
| `it.dateAdded` | 加入書庫日期 |
|
||||
| `it.dateCreated` | 書籍建立日期 |
|
||||
| `it.lastRead` | 最後閱讀日期 |
|
||||
| `it.lastFinished` | 完成閱讀日期 |
|
||||
| `it.highlightCount` | 畫線數量 |
|
||||
| `it.highlights` | 渲染後的畫線內容 |
|
||||
|
||||
### 畫線模板變數
|
||||
|
||||
| 變數 | 說明 |
|
||||
|------|------|
|
||||
| `it.highlightText` | 畫線原文 |
|
||||
| `it.annotation` | 你的筆記備註 |
|
||||
| `it.dateCreated` | 畫線日期 |
|
||||
| `it.chapter` | 章節名稱 |
|
||||
| `it.chapterProgress` | 章節內進度(0-1) |
|
||||
|
||||
### 模板範例
|
||||
|
||||
**筆記模板:**
|
||||
|
||||
```
|
||||
---
|
||||
title: "<%= it.bookTitle %>"
|
||||
author: "<%= it.bookAuthor %>"
|
||||
publisher: "<%= it.publisher %>"
|
||||
last_read: <%= it.lastRead %>
|
||||
highlight_count: <%= it.highlightCount %>
|
||||
source: Kobo
|
||||
---
|
||||
<%= it.highlights %>
|
||||
```
|
||||
|
||||
**畫線模板:**
|
||||
|
||||
```
|
||||
*<%= it.dateCreated %>*<% if (it.chapter) { %> | <%= it.chapter %><% } %>
|
||||
|
||||
> <%= it.highlightText %>
|
||||
<% if (it.annotation) { %>
|
||||
**Note:** <%= it.annotation %>
|
||||
<% } %>
|
||||
---
|
||||
```
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "kobo-note-sync",
|
||||
"name": "Kobo Note Sync",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Import highlights and annotations from Kobo e-reader into Obsidian notes.",
|
||||
"author": "kenkyou",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
610
package-lock.json
generated
Normal file
610
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
{
|
||||
"name": "kobo-note-sync",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kobo-note-sync",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"eta": "^4.5.1",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"obsidian": "^1.5.0",
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
},
|
||||
"node_modules/eta": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/eta/-/eta-4.5.1.tgz",
|
||||
"integrity": "sha512-EaNCGm+8XEIU7YNcc+THptWAO5NfKBHHARxt+wxZljj9bTr/+arRoOm9/MpGt4n6xn9fLnPFRSoLD0WFYGFUxQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/bgub/eta?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
|
||||
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
22
package.json
Normal file
22
package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "kobo-note-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Import Kobo highlights and annotations into Obsidian notes",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"build": "esbuild src/main.ts --bundle --outfile=dist/main.js --external:obsidian --external:electron --format=cjs --platform=node --target=es2020 && npm run copy-assets",
|
||||
"dev": "esbuild src/main.ts --bundle --outfile=dist/main.js --external:obsidian --external:electron --format=cjs --platform=node --target=es2020 --watch",
|
||||
"copy-assets": "cp manifest.json node_modules/sql.js/dist/sql-wasm.wasm dist/",
|
||||
"deploy": "npm run build && cp dist/main.js dist/manifest.json dist/sql-wasm.wasm \"$OBSIDIAN_PLUGINS/kobo-note-sync/\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"obsidian": "^1.5.0",
|
||||
"typescript": "^5.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eta": "^4.5.1",
|
||||
"sql.js": "^1.14.1"
|
||||
}
|
||||
}
|
||||
632
src/main.ts
Normal file
632
src/main.ts
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
import { App, Notice, Plugin, PluginSettingTab, Setting, normalizePath } from "obsidian";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { Eta } from "eta";
|
||||
import initSqlJs from "sql.js";
|
||||
|
||||
const eta = new Eta({ autoEscape: false, rmWhitespace: false, autoTrim: [false, false] });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface KoboHighlight {
|
||||
highlightText: string;
|
||||
annotation: string;
|
||||
dateCreated: string;
|
||||
chapter: string;
|
||||
chapterProgress: number;
|
||||
}
|
||||
|
||||
interface BookMeta {
|
||||
bookTitle: string;
|
||||
bookAuthor: string;
|
||||
publisher: string;
|
||||
isbn: string;
|
||||
series: string;
|
||||
seriesNumber: string;
|
||||
subtitle: string;
|
||||
language: string;
|
||||
description: string;
|
||||
progress: number;
|
||||
status: string;
|
||||
numPages: number;
|
||||
wordCount: number;
|
||||
averageRating: number;
|
||||
ratingCount: number;
|
||||
timeSpentReading: number;
|
||||
timesStartedReading: number;
|
||||
dateAdded: string;
|
||||
dateCreated: string;
|
||||
lastRead: string;
|
||||
lastFinished: string;
|
||||
}
|
||||
|
||||
interface BookData {
|
||||
meta: BookMeta;
|
||||
highlights: KoboHighlight[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface KoboHighlighterSettings {
|
||||
dbPath: string;
|
||||
outputFolder: string;
|
||||
noteTemplate: string;
|
||||
highlightTemplate: string;
|
||||
}
|
||||
|
||||
const DEFAULT_NOTE_TEMPLATE = `---
|
||||
title: "<%= it.bookTitle %>"
|
||||
author: "<%= it.bookAuthor %>"
|
||||
publisher: "<%= it.publisher %>"
|
||||
last_read: <%= it.lastRead %>
|
||||
highlight_count: <%= it.highlightCount %>
|
||||
source: Kobo
|
||||
---
|
||||
<%= it.highlights %>`;
|
||||
|
||||
const DEFAULT_HIGHLIGHT_TEMPLATE = `*<%= it.dateCreated %>*<% if (it.chapter) { %> | <%= it.chapter %><% } %>
|
||||
|
||||
> <%= it.highlightText %>
|
||||
<% if (it.annotation) { %>
|
||||
**Note:** <%= it.annotation %>
|
||||
<% } %>
|
||||
---
|
||||
`;
|
||||
|
||||
function getDefaultDbPath(): string {
|
||||
const platform = os.platform();
|
||||
if (platform === "win32") {
|
||||
return path.join(os.homedir(), "AppData", "Local", "Kobo", "Kobo Desktop Edition", "Kobo.sqlite");
|
||||
}
|
||||
// macOS and Linux
|
||||
return "~/Library/Application Support/Kobo/Kobo Desktop Edition/Kobo.sqlite";
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: KoboHighlighterSettings = {
|
||||
dbPath: getDefaultDbPath(),
|
||||
outputFolder: "Kobo Note Sync",
|
||||
noteTemplate: DEFAULT_NOTE_TEMPLATE,
|
||||
highlightTemplate: DEFAULT_HIGHLIGHT_TEMPLATE,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// i18n
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type LocaleStrings = {
|
||||
settingsTitle: string;
|
||||
dbPath: string;
|
||||
dbPathDesc: string;
|
||||
outputFolder: string;
|
||||
outputFolderDesc: string;
|
||||
noteTemplate: string;
|
||||
noteTemplateDesc: string;
|
||||
highlightTemplate: string;
|
||||
highlightTemplateDesc: string;
|
||||
noteVarsTitle: string;
|
||||
highlightVarsTitle: string;
|
||||
syncing: string;
|
||||
noHighlights: string;
|
||||
syncDone: (highlights: number, books: number) => string;
|
||||
error: (msg: string) => string;
|
||||
};
|
||||
|
||||
const locales: Record<string, LocaleStrings> = {
|
||||
en: {
|
||||
settingsTitle: "Kobo Note Sync Settings",
|
||||
dbPath: "Kobo database path",
|
||||
dbPathDesc: "Path to Kobo.sqlite (supports ~ for home directory)",
|
||||
outputFolder: "Output folder",
|
||||
outputFolderDesc: "Folder in your vault to save highlight notes",
|
||||
noteTemplate: "Note template",
|
||||
noteTemplateDesc: "Eta.js template for each book note. Use <%= it.varName %> for variables.",
|
||||
highlightTemplate: "Highlight template",
|
||||
highlightTemplateDesc: "Eta.js template for each highlight. Use <%= it.varName %> for variables.",
|
||||
noteVarsTitle: "Note template variables",
|
||||
highlightVarsTitle: "Highlight template variables",
|
||||
syncing: "Kobo Note Sync: Syncing...",
|
||||
noHighlights: "Kobo Note Sync: No highlights found.",
|
||||
syncDone: (h, b) => `Kobo Note Sync: Synced ${h} highlights from ${b} book(s).`,
|
||||
error: (msg) => `Kobo Note Sync: Error — ${msg}`,
|
||||
},
|
||||
"zh-TW": {
|
||||
settingsTitle: "Kobo Note Sync 設定",
|
||||
dbPath: "Kobo 資料庫路徑",
|
||||
dbPathDesc: "Kobo.sqlite 的路徑(支援 ~ 代表家目錄)",
|
||||
outputFolder: "輸出資料夾",
|
||||
outputFolderDesc: "在 Vault 中儲存畫線筆記的資料夾",
|
||||
noteTemplate: "筆記模板",
|
||||
noteTemplateDesc: "每本書的 Eta.js 模板。使用 <%= it.varName %> 插入變數。",
|
||||
highlightTemplate: "畫線模板",
|
||||
highlightTemplateDesc: "每條畫線的 Eta.js 模板。使用 <%= it.varName %> 插入變數。",
|
||||
noteVarsTitle: "筆記模板可用變數",
|
||||
highlightVarsTitle: "畫線模板可用變數",
|
||||
syncing: "Kobo Note Sync:同步中…",
|
||||
noHighlights: "Kobo Note Sync:未找到任何畫線。",
|
||||
syncDone: (h, b) => `Kobo Note Sync:已同步 ${b} 本書的 ${h} 條畫線。`,
|
||||
error: (msg) => `Kobo Note Sync:錯誤 — ${msg}`,
|
||||
},
|
||||
};
|
||||
|
||||
function getLocale(): LocaleStrings {
|
||||
const lang = window.localStorage.getItem("language") || "en";
|
||||
return locales[lang] || locales["en"];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variable reference (for settings UI)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface VarInfo {
|
||||
name: string;
|
||||
en: string;
|
||||
zhTW: string;
|
||||
}
|
||||
|
||||
const NOTE_VARS: VarInfo[] = [
|
||||
{ name: "bookTitle", en: "Book title", zhTW: "書名" },
|
||||
{ name: "bookAuthor", en: "Author", zhTW: "作者" },
|
||||
{ name: "publisher", en: "Publisher", zhTW: "出版社" },
|
||||
{ name: "isbn", en: "ISBN", zhTW: "ISBN" },
|
||||
{ name: "series", en: "Series name", zhTW: "系列名稱" },
|
||||
{ name: "seriesNumber", en: "Series number", zhTW: "系列編號" },
|
||||
{ name: "subtitle", en: "Subtitle", zhTW: "副標題" },
|
||||
{ name: "language", en: "Language", zhTW: "語言" },
|
||||
{ name: "description", en: "Book description (HTML stripped)", zhTW: "書籍簡介(已去 HTML)" },
|
||||
{ name: "progress", en: "Reading progress %", zhTW: "閱讀進度 %" },
|
||||
{ name: "status", en: "Reading status (unread/reading/finished)", zhTW: "閱讀狀態(unread/reading/finished)" },
|
||||
{ name: "numPages", en: "Number of pages", zhTW: "頁數" },
|
||||
{ name: "wordCount", en: "Word count", zhTW: "字數" },
|
||||
{ name: "averageRating", en: "Average rating on Kobo", zhTW: "Kobo 平均評分" },
|
||||
{ name: "ratingCount", en: "Rating count", zhTW: "評分數量" },
|
||||
{ name: "timeSpentReading", en: "Time spent reading (seconds)", zhTW: "閱讀花費時間(秒)" },
|
||||
{ name: "timesStartedReading", en: "Times started reading", zhTW: "開始閱讀次數" },
|
||||
{ name: "dateAdded", en: "Date added to library", zhTW: "加入書庫日期" },
|
||||
{ name: "dateCreated", en: "Book creation date", zhTW: "書籍建立日期" },
|
||||
{ name: "lastRead", en: "Last read date", zhTW: "最後閱讀日期" },
|
||||
{ name: "lastFinished", en: "Finished reading date", zhTW: "完成閱讀日期" },
|
||||
{ name: "highlightCount", en: "Number of highlights", zhTW: "畫線數量" },
|
||||
{ name: "highlights", en: "Rendered highlights content", zhTW: "渲染後的畫線內容" },
|
||||
];
|
||||
|
||||
const HIGHLIGHT_VARS: VarInfo[] = [
|
||||
{ name: "highlightText", en: "Highlighted text", zhTW: "畫線原文" },
|
||||
{ name: "annotation", en: "Your note/annotation", zhTW: "你的筆記備註" },
|
||||
{ name: "dateCreated", en: "Highlight date", zhTW: "畫線日期" },
|
||||
{ name: "chapter", en: "Chapter title", zhTW: "章節名稱" },
|
||||
{ name: "chapterProgress", en: "Progress within chapter (0-1)", zhTW: "章節內進度(0-1)" },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Database (cross-platform via sql.js WASM)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function expandHome(p: string): string {
|
||||
if (p.startsWith("~/") || p === "~") {
|
||||
return p.replace("~", os.homedir());
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function readStatusToString(status: number): string {
|
||||
switch (status) {
|
||||
case 2: return "finished";
|
||||
case 1: return "reading";
|
||||
default: return "unread";
|
||||
}
|
||||
}
|
||||
|
||||
function parseDate(d: unknown): string {
|
||||
if (!d || typeof d !== "string" || d.startsWith("0000")) return "";
|
||||
return d.split("T")[0];
|
||||
}
|
||||
|
||||
function strVal(v: unknown): string {
|
||||
if (v == null) return "";
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function numVal(v: unknown, fallback = 0): number {
|
||||
if (v == null) return fallback;
|
||||
const n = Number(v);
|
||||
return isNaN(n) ? fallback : n;
|
||||
}
|
||||
|
||||
async function readKoboDb(dbPath: string, pluginDir: string): Promise<BookData[]> {
|
||||
const resolvedPath = expandHome(dbPath);
|
||||
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
throw new Error(`Database not found: ${resolvedPath}`);
|
||||
}
|
||||
|
||||
// Copy to temp to avoid locking issues
|
||||
const tmpDir = process.env.TMPDIR || os.tmpdir();
|
||||
const tmpPath = path.join(tmpDir, `kobo-highlighter-${Date.now()}.sqlite`);
|
||||
fs.copyFileSync(resolvedPath, tmpPath);
|
||||
|
||||
const wasmPath = path.join(pluginDir, "sql-wasm.wasm");
|
||||
const wasmBinary = fs.readFileSync(wasmPath);
|
||||
const SQL = await initSqlJs({ wasmBinary });
|
||||
const buffer = fs.readFileSync(tmpPath);
|
||||
const db = new SQL.Database(buffer);
|
||||
|
||||
try {
|
||||
// Query book metadata
|
||||
const metaStmt = db.prepare(`
|
||||
SELECT
|
||||
c.ContentID,
|
||||
c.Title,
|
||||
c.Attribution,
|
||||
c.Publisher,
|
||||
c.ISBN,
|
||||
c.Series,
|
||||
c.SeriesNumber,
|
||||
c.Subtitle,
|
||||
c.Language,
|
||||
c.Description,
|
||||
c.ReadStatus,
|
||||
c.___PercentRead,
|
||||
c.___NumPages,
|
||||
c.WordCount,
|
||||
c.AverageRating,
|
||||
c.RatingCount,
|
||||
c.TimeSpentReading,
|
||||
c.TimesStartedReading,
|
||||
c.DateCreated,
|
||||
c.DateLastRead,
|
||||
c.DateAdded,
|
||||
c.LastTimeFinishedReading
|
||||
FROM content c
|
||||
WHERE c.ContentType = 6
|
||||
AND c.ContentID IN (SELECT DISTINCT VolumeID FROM Bookmark WHERE Text IS NOT NULL AND Text <> '')
|
||||
ORDER BY c.Title
|
||||
`);
|
||||
|
||||
const metaRows: Record<string, any>[] = [];
|
||||
while (metaStmt.step()) {
|
||||
const row = metaStmt.getAsObject();
|
||||
metaRows.push(row);
|
||||
}
|
||||
metaStmt.free();
|
||||
|
||||
if (metaRows.length === 0) return [];
|
||||
|
||||
// Query highlights with chapter info
|
||||
const hlStmt = db.prepare(`
|
||||
SELECT
|
||||
b.VolumeID,
|
||||
b.Text,
|
||||
b.Annotation,
|
||||
b.DateCreated,
|
||||
b.ChapterProgress,
|
||||
b.ContentID,
|
||||
(SELECT c2.Title FROM content c2 WHERE c2.ContentID = b.ContentID || '-1') AS ChapterTitle
|
||||
FROM Bookmark b
|
||||
WHERE b.Text IS NOT NULL AND b.Text <> ''
|
||||
ORDER BY b.DateCreated
|
||||
`);
|
||||
|
||||
const highlightMap = new Map<string, KoboHighlight[]>();
|
||||
while (hlStmt.step()) {
|
||||
const row = hlStmt.getAsObject();
|
||||
const volumeId = strVal(row.VolumeID);
|
||||
if (!highlightMap.has(volumeId)) {
|
||||
highlightMap.set(volumeId, []);
|
||||
}
|
||||
highlightMap.get(volumeId)!.push({
|
||||
highlightText: strVal(row.Text),
|
||||
annotation: strVal(row.Annotation),
|
||||
dateCreated: parseDate(row.DateCreated),
|
||||
chapter: strVal(row.ChapterTitle),
|
||||
chapterProgress: numVal(row.ChapterProgress),
|
||||
});
|
||||
}
|
||||
hlStmt.free();
|
||||
|
||||
// Build BookData
|
||||
const books: BookData[] = [];
|
||||
for (const row of metaRows) {
|
||||
const contentId = strVal(row.ContentID);
|
||||
const highlights = highlightMap.get(contentId) || [];
|
||||
if (highlights.length === 0) continue;
|
||||
|
||||
const desc = strVal(row.Description).replace(/<[^>]*>/g, "").trim();
|
||||
|
||||
books.push({
|
||||
meta: {
|
||||
bookTitle: strVal(row.Title) || "Unknown",
|
||||
bookAuthor: strVal(row.Attribution) || "Unknown",
|
||||
publisher: strVal(row.Publisher),
|
||||
isbn: strVal(row.ISBN),
|
||||
series: strVal(row.Series),
|
||||
seriesNumber: strVal(row.SeriesNumber),
|
||||
subtitle: strVal(row.Subtitle),
|
||||
language: strVal(row.Language),
|
||||
description: desc,
|
||||
progress: numVal(row.___PercentRead),
|
||||
status: readStatusToString(numVal(row.ReadStatus)),
|
||||
numPages: numVal(row.___NumPages, -1),
|
||||
wordCount: numVal(row.WordCount, -1),
|
||||
averageRating: numVal(row.AverageRating),
|
||||
ratingCount: numVal(row.RatingCount),
|
||||
timeSpentReading: numVal(row.TimeSpentReading),
|
||||
timesStartedReading: numVal(row.TimesStartedReading),
|
||||
dateAdded: parseDate(row.DateAdded),
|
||||
dateCreated: parseDate(row.DateCreated),
|
||||
lastRead: parseDate(row.DateLastRead),
|
||||
lastFinished: parseDate(row.LastTimeFinishedReading),
|
||||
},
|
||||
highlights,
|
||||
});
|
||||
}
|
||||
|
||||
return books;
|
||||
} finally {
|
||||
db.close();
|
||||
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filename sanitizer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sanitizeFilename(name: string): string {
|
||||
return name.replace(/[\\/:*?"<>|]/g, "_").trim();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default class KoboHighlighterPlugin extends Plugin {
|
||||
settings: KoboHighlighterSettings = DEFAULT_SETTINGS;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
this.addCommand({
|
||||
id: "sync-kobo-highlights",
|
||||
name: "Kobo Note Sync",
|
||||
callback: () => this.syncHighlights(),
|
||||
});
|
||||
|
||||
this.addRibbonIcon("book-open", "Kobo Note Sync", () => {
|
||||
this.syncHighlights();
|
||||
});
|
||||
|
||||
this.addSettingTab(new KoboHighlighterSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
async syncHighlights() {
|
||||
const t = getLocale();
|
||||
try {
|
||||
new Notice(t.syncing);
|
||||
|
||||
const pluginDir = (this.app.vault.adapter as any).getBasePath()
|
||||
+ path.sep + this.manifest.dir;
|
||||
const books = await readKoboDb(this.settings.dbPath, pluginDir);
|
||||
if (books.length === 0) {
|
||||
new Notice(t.noHighlights);
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = normalizePath(this.settings.outputFolder);
|
||||
if (!this.app.vault.getAbstractFileByPath(folder)) {
|
||||
await this.app.vault.createFolder(folder);
|
||||
}
|
||||
|
||||
let totalHighlights = 0;
|
||||
|
||||
for (const book of books) {
|
||||
const filename = sanitizeFilename(book.meta.bookTitle) + ".md";
|
||||
const filePath = normalizePath(`${folder}/${filename}`);
|
||||
const existing = this.app.vault.getAbstractFileByPath(filePath);
|
||||
|
||||
// Read existing highlights to avoid duplicates
|
||||
let existingHighlightTexts = new Set<string>();
|
||||
let existingBody = "";
|
||||
if (existing) {
|
||||
const content = await this.app.vault.read(existing as any);
|
||||
for (const line of content.split("\n")) {
|
||||
if (line.startsWith("> ")) {
|
||||
existingHighlightTexts.add(line.slice(2).trim());
|
||||
}
|
||||
}
|
||||
const fmEnd = content.indexOf("---", content.indexOf("---") + 1);
|
||||
if (fmEnd !== -1) {
|
||||
existingBody = content.slice(fmEnd + 3).trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Render only new highlights
|
||||
const newHighlights = book.highlights.filter(
|
||||
(h) => !existingHighlightTexts.has(h.highlightText.trim())
|
||||
);
|
||||
|
||||
let newRendered = "";
|
||||
if (newHighlights.length > 0) {
|
||||
newRendered = newHighlights
|
||||
.map((h) => eta.renderString(this.settings.highlightTemplate, h))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// Combine: keep existing + append new
|
||||
const combinedHighlights = existingBody
|
||||
? existingBody + (newRendered ? "\n\n" + newRendered : "")
|
||||
: book.highlights
|
||||
.map((h) => eta.renderString(this.settings.highlightTemplate, h))
|
||||
.join("\n");
|
||||
|
||||
const noteVars = {
|
||||
...book.meta,
|
||||
highlightCount: book.highlights.length,
|
||||
highlights: combinedHighlights,
|
||||
};
|
||||
|
||||
const noteContent = eta.renderString(this.settings.noteTemplate, noteVars);
|
||||
|
||||
if (existing) {
|
||||
await this.app.vault.modify(existing as any, noteContent);
|
||||
} else {
|
||||
await this.app.vault.create(filePath, noteContent);
|
||||
}
|
||||
|
||||
totalHighlights += book.highlights.length;
|
||||
}
|
||||
|
||||
new Notice(t.syncDone(totalHighlights, books.length));
|
||||
} catch (e: any) {
|
||||
console.error("Kobo Note Sync error:", e);
|
||||
new Notice(t.error(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings Tab
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class KoboHighlighterSettingTab extends PluginSettingTab {
|
||||
plugin: KoboHighlighterPlugin;
|
||||
|
||||
constructor(app: App, plugin: KoboHighlighterPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
const t = getLocale();
|
||||
const isZh = (window.localStorage.getItem("language") || "").startsWith("zh");
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: t.settingsTitle });
|
||||
|
||||
// Database path
|
||||
new Setting(containerEl)
|
||||
.setName(t.dbPath)
|
||||
.setDesc(t.dbPathDesc)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.dbPath)
|
||||
.setValue(this.plugin.settings.dbPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dbPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// Output folder
|
||||
new Setting(containerEl)
|
||||
.setName(t.outputFolder)
|
||||
.setDesc(t.outputFolderDesc)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.outputFolder)
|
||||
.setValue(this.plugin.settings.outputFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.outputFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// Note template
|
||||
new Setting(containerEl)
|
||||
.setName(t.noteTemplate)
|
||||
.setDesc(t.noteTemplateDesc)
|
||||
.addTextArea((text) => {
|
||||
text.inputEl.rows = 12;
|
||||
text.inputEl.cols = 50;
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.fontFamily = "monospace";
|
||||
text.inputEl.style.fontSize = "13px";
|
||||
text
|
||||
.setPlaceholder(DEFAULT_NOTE_TEMPLATE)
|
||||
.setValue(this.plugin.settings.noteTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.noteTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Note variables reference
|
||||
this.renderVarTable(containerEl, t.noteVarsTitle, NOTE_VARS, isZh);
|
||||
|
||||
// Highlight template
|
||||
new Setting(containerEl)
|
||||
.setName(t.highlightTemplate)
|
||||
.setDesc(t.highlightTemplateDesc)
|
||||
.addTextArea((text) => {
|
||||
text.inputEl.rows = 8;
|
||||
text.inputEl.cols = 50;
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.fontFamily = "monospace";
|
||||
text.inputEl.style.fontSize = "13px";
|
||||
text
|
||||
.setPlaceholder(DEFAULT_HIGHLIGHT_TEMPLATE)
|
||||
.setValue(this.plugin.settings.highlightTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.highlightTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight variables reference
|
||||
this.renderVarTable(containerEl, t.highlightVarsTitle, HIGHLIGHT_VARS, isZh);
|
||||
}
|
||||
|
||||
private renderVarTable(container: HTMLElement, title: string, vars: VarInfo[], isZh: boolean): void {
|
||||
const details = container.createEl("details");
|
||||
details.style.marginBottom = "1em";
|
||||
details.style.fontSize = "13px";
|
||||
|
||||
const summary = details.createEl("summary");
|
||||
summary.style.cursor = "pointer";
|
||||
summary.style.fontWeight = "bold";
|
||||
summary.style.color = "var(--text-muted)";
|
||||
summary.setText(title);
|
||||
|
||||
const table = details.createEl("table");
|
||||
table.style.width = "100%";
|
||||
table.style.borderCollapse = "collapse";
|
||||
table.style.marginTop = "0.5em";
|
||||
|
||||
const thead = table.createEl("thead");
|
||||
const headerRow = thead.createEl("tr");
|
||||
for (const h of [isZh ? "變數" : "Variable", isZh ? "說明" : "Description"]) {
|
||||
const th = headerRow.createEl("th", { text: h });
|
||||
th.style.textAlign = "left";
|
||||
th.style.padding = "4px 8px";
|
||||
th.style.borderBottom = "1px solid var(--background-modifier-border)";
|
||||
}
|
||||
|
||||
const tbody = table.createEl("tbody");
|
||||
for (const v of vars) {
|
||||
const row = tbody.createEl("tr");
|
||||
const codeCell = row.createEl("td");
|
||||
codeCell.style.padding = "3px 8px";
|
||||
codeCell.style.borderBottom = "1px solid var(--background-modifier-border)";
|
||||
const code = codeCell.createEl("code");
|
||||
code.setText(`it.${v.name}`);
|
||||
code.style.fontSize = "12px";
|
||||
|
||||
const descCell = row.createEl("td", { text: isZh ? v.zhTW : v.en });
|
||||
descCell.style.padding = "3px 8px";
|
||||
descCell.style.borderBottom = "1px solid var(--background-modifier-border)";
|
||||
}
|
||||
}
|
||||
}
|
||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": ".",
|
||||
"sourceMap": false,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue