Compare commits

...

29 commits

Author SHA1 Message Date
Jay Bridge
4a28663ebd
Merge pull request #50 from LIUBINfighter/dev
1.7.0
2026-02-07 14:52:59 +08:00
JayBridge
11a83d77b5 chore: bump version to 1.7.0 in manifest, package, and lock files 2026-02-07 14:16:36 +08:00
Jay Bridge
05b0014275
Merge pull request #49 from LIUBINfighter/feat/newcsv
feat: add functionality to create new CSV files and enhance UI for fi…
2026-02-07 14:13:29 +08:00
JayBridge
3c85dde0d2 feat: add functionality to create new CSV files and enhance UI for file creation 2026-02-07 14:12:34 +08:00
JayBridge
c7133cd538 chore: update version to 1.6.1 in package-lock.json 2026-02-07 14:03:17 +08:00
Jay Bridge
378f336e27
Merge pull request #47 from LIUBINfighter/dev
chore: bump version to 1.6.1 in manifest and package files and add zenodo doi badge
2026-02-01 14:13:40 +08:00
JayBridge
09bb48c16a docs: update README files to include DOI badge and enhance visibility of download statistics 2026-02-01 14:12:35 +08:00
JayBridge
8a18041c99 chore: bump version to 1.6.1 in manifest and package files 2026-02-01 14:09:26 +08:00
Jay Bridge
1252c65e22
Merge pull request #46 from LIUBINfighter/feat/url
feat/url: add clickable URLs in cells. Fixes #13 Fixes #14
2026-02-01 14:02:46 +08:00
JayBridge
e7d2fe56ca feat: enhance clickable URLs feature to support Markdown links
- Updated the clickable URLs functionality to automatically detect and render both plain-text URLs and Markdown-style links in CSV cells.
- Enhanced documentation to reflect the new capabilities and provide examples for users.
- Added tests to ensure proper detection and parsing of Markdown links alongside plain URLs.
2026-02-01 14:01:08 +08:00
JayBridge
d43e2063df style: adjust padding for compact dropdown alignment
- Added left and right padding adjustments to the setting item control in the compact dropdown to ensure proper alignment with buttons.
2026-02-01 13:54:37 +08:00
JayBridge
672d162195 feat: add clickable URLs feature in CSV cells
- Implemented automatic detection and rendering of plain-text URLs as clickable links in CSV cells.
- Added functionality to toggle between display and edit modes for URL cells.
- Updated styles for URL display and interaction.
- Created utility functions for URL detection and parsing.
- Added tests for URL detection and parsing functionality.
2026-02-01 13:53:34 +08:00
JayBridge
a23c224b9f chore: update esbuild to version 0.27.2 and bump package version to 1.1.5 2026-02-01 13:40:11 +08:00
Jay Bridge
c73e596e08
Merge pull request #44 from LIUBINfighter/dev
Some fixes (Fixes #41, Fixes #35)
2025-12-07 05:14:08 +08:00
Jay Bridge
b81bd0dd82
Merge branch 'master' into dev 2025-12-07 05:07:54 +08:00
JayBridge
30c0b8081a chore: bump version to 1.1.5 2025-12-07 05:05:15 +08:00
Jay Bridge
5c4d53deef
Fixes #41 i18n and prevent global undo/redo from inactive views. (#43)
* fix: localize notices + restrict undo/redo to active view; add history manager i18n and tests

* fix: i18n for parse warnings and table utils; add tests for history manager
2025-12-07 04:58:04 +08:00
Jay Bridge
26a679a4c9
Fix/viewname (#42)
* fix: avoid view name collision with other plugins by renaming to csv-lite-view & csv-lite-source-view

* chore: update comments to reflect csv-lite view name
2025-12-07 04:57:44 +08:00
JayBridge
7e4205e051 chore: add ci.yml 2025-09-16 17:17:03 +08:00
Jay Bridge
afe16b2e26
Mobile support and style adjustment. (#38)
* feat: Support for other delimiters (especially ";")
Fixes #36

* fix: delimiter-compact style adjustment 调整紧凑型下拉菜单样式以确保与按钮垂直对齐

* refactor: 移除操作栏组件及其相关接口

* chore: update to 1.1.4

* chore: 更新 package-lock.json 文件
2025-09-16 17:12:08 +08:00
JayBridge
ae94a89e83 chore: 更新 package-lock.json 文件 2025-09-16 16:44:55 +08:00
JayBridge
d8d0785cf1 chore: update to 1.1.4 2025-09-16 16:43:22 +08:00
JayBridge
24a7a75753 refactor: 移除操作栏组件及其相关接口 2025-09-16 16:43:07 +08:00
JayBridge
e2b11ffa32 fix: delimiter-compact style adjustment 调整紧凑型下拉菜单样式以确保与按钮垂直对齐 2025-09-16 16:38:41 +08:00
JayBridge
b6d2e44006 Merge branch 'dev' of github.com:LIUBINfighter/csv-lite into dev 2025-09-16 16:31:04 +08:00
JayBridge
0aa3384793 chore: bump version to 1.1.3 2025-09-16 16:28:50 +08:00
JayBridge
8b5afe3a32 fix: 确保紧凑型下拉菜单与按钮对齐(相同高度和垂直居中) 2025-09-16 16:28:31 +08:00
JayBridge
508751a80c chore: update to 1.1.2 and beta support mobile #8 2025-09-16 16:17:59 +08:00
JayBridge
10274cc4eb feat: Support for other delimiters (especially ";")
Fixes #36
2025-09-16 07:38:27 +08:00
25 changed files with 1162 additions and 183 deletions

42
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: CI
on:
push:
branches: [ "dev", "master", "main" ]
pull_request:
branches: [ "dev", "master", "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Prettier check (if present)
run: |
if npx --no-install prettier --version >/dev/null 2>&1; then
npx prettier --check "**/*.{ts,js,json,md,css}" || (echo "Prettier check failed" && exit 1)
else
echo "Prettier not installed - skipping check"
fi
- name: TypeScript typecheck and build
run: npm run build
- name: Run tests
run: npm test

View file

@ -4,7 +4,7 @@ Simple enough for csv, no more fancy function you need to learn and think!
[中文版本 Readme](./README_zh.md)
![Obsidian Download](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=Downloads&query=$%5B%22csv-lite%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![Total Downloads](https://img.shields.io/github/downloads/LIUBINfighter/csv-lite/total?style=flat&label=Total%20Downloads) ![GitHub Issues](https://img.shields.io/github/issues/LIUBINfighter/csv-lite?style=flat&label=Issues) ![GitHub Last Commit](https://img.shields.io/github/last-commit/LIUBINfighter/csv-lite?style=flat&label=Last%20Commit)
[![DOI](https://zenodo.org/badge/953422745.svg)](https://doi.org/10.5281/zenodo.18447042) ![Obsidian Download](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=Downloads&query=$%5B%22csv-lite%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![Total Downloads](https://img.shields.io/github/downloads/LIUBINfighter/csv-lite/total?style=flat&label=Total%20Downloads) ![GitHub Issues](https://img.shields.io/github/issues/LIUBINfighter/csv-lite?style=flat&label=Issues) ![GitHub Last Commit](https://img.shields.io/github/last-commit/LIUBINfighter/csv-lite?style=flat&label=Last%20Commit)
<!-- ![image](https://github.com/user-attachments/assets/6d956e79-4be7-4172-92e2-6f14ddba0dda) -->
@ -27,7 +27,9 @@ A plugin designed to view and edit `CSV files` directly within Obsidian.
- **Toggle** between the table view and raw source-mode.
- **Edit** cells directly by clicking and typing.
- **Manage** rows and columns (add, delete, move) with a simple right-click on the header.
- **Create** new CSV files: use the command palette or right-click in the File Explorer to quickly create a new `.csv` file. A unique name like `new.csv` or `new-1.csv` will be generated automatically.
- **Switch Delimiter NonDestructively**: Autodetects the file delimiter (comma, semicolon, tab, etc.). Changing the delimiter in the toolbar only re-parses the view; it does NOT rewrite your file. Your original delimiter is preserved when saving edits.
- **Clickable URLs**: Plain-text URLs and Markdown-style links (`[text](url)`) in cells are automatically detected and rendered as clickable links. Click a link to open it in your browser, or click the edit button (✎) to edit the cell content.
I have a plan to design my own database using json and csv only. If you have fancy idea about tables or csv, please feel free to issue (I will consider it in csv-lite or my new plugin) or search it in community. <!-- For in-markdown edit, I recommend `anyblock` with a much more complex syntax. -->

View file

@ -4,6 +4,8 @@
[English Version Readme](./README.md)
[![DOI](https://zenodo.org/badge/953422745.svg)](https://doi.org/10.5281/zenodo.18447042) ![Obsidian Download](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=Downloads&query=$%5B%22csv-lite%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![Total Downloads](https://img.shields.io/github/downloads/LIUBINfighter/csv-lite/total?style=flat&label=Total%20Downloads) ![GitHub Issues](https://img.shields.io/github/issues/LIUBINfighter/csv-lite?style=flat&label=Issues) ![GitHub Last Commit](https://img.shields.io/github/last-commit/LIUBINfighter/csv-lite?style=flat&label=Last%20Commit)
<!-- ![test-sample](./asssets/test-sample.png) -->
<!-- ![v1.0.2 support searching](./asssets/searching.png) -->
@ -23,6 +25,7 @@
- **编辑** 直接点击并输入即可编辑单元格
- **管理** 行和列(添加、删除、移动),只需右键点击表头
- **非破坏性分隔符切换**:自动检测文件原始分隔符(逗号、分号、制表符等)。在工具栏切换分隔符只会重新解析视图,不会改写文件;保存编辑时仍使用文件原始分隔符,避免产生大规模 diff。
- **可点击的 URL 链接**:单元格中的纯文本 URL 和 Markdown 风格链接(`[文本](url)`)会自动识别并渲染为可点击的链接。点击链接在浏览器中打开,或点击编辑按钮(✎)编辑单元格内容。
我计划仅用 json 和 csv 设计自己的数据库。如果你有关于表格或 csv 的新想法,欢迎提 issue我会考虑加入 csv-lite 或新插件),或在社区中搜索。<!-- 对于 Markdown 内编辑,推荐 `anyblock`,但语法更复杂。 -->

134
docs/CLICKABLE_URLS.md Normal file
View file

@ -0,0 +1,134 @@
# Clickable URLs Feature
## Overview
The Clickable URLs feature automatically detects and renders plain-text URLs and Markdown-style links in CSV cells as clickable links. This makes it easy to navigate to external resources directly from your CSV data while keeping your tables clean and readable.
## How It Works
### URL Detection
The plugin detects two types of links:
**1. Plain URLs:**
- `http://example.com`
- `https://example.com`
- URLs with paths: `https://example.com/path/to/page`
- URLs with query parameters: `https://example.com?param=value`
- URLs with fragments: `https://example.com#section`
**2. Markdown-style Links:**
- `[GitHub](https://github.com)` - Displays as "GitHub" but links to the URL
- `[Documentation](https://docs.example.com)` - Clean, readable link text
- Mixed content: `Visit [our site](https://example.com) for more` - Text with embedded Markdown links
### Display Modes
Each cell with a URL has two modes:
1. **Display Mode** (default)
- URLs are rendered as clickable links
- Links open in a new browser tab
- Click the cell (not the link) to enter edit mode
2. **Edit Mode**
- Shows the plain text input field
- Allows editing the URL text
- Automatically switches back to display mode on blur
### Implementation Details
#### Files Created/Modified
1. **`src/utils/url-utils.ts`** (new)
- `containsUrl()`: Detects if text contains URLs
- `parseTextWithUrls()`: Parses text into segments (URL and non-URL)
- `createUrlDisplay()`: Creates DOM elements with clickable links
2. **`src/view/table-render.ts`** (modified)
- Integrated URL display layer
- Toggle between display and edit modes
- Dynamic URL detection on input changes
3. **`styles.css`** (modified)
- Added styles for `.csv-cell-display`
- Added styles for `.csv-cell-link` with hover effects
#### Architecture
```
Cell Structure:
├── <td>
├── <div class="csv-cell-display"> (shown when not editing)
│ ├── <span>Plain text</span>
│ ├── <a href="..." class="csv-cell-link">URL</a>
│ └── <span>More text</span>
└── <input class="csv-cell-input"> (shown when editing)
```
### User Experience
1. **Viewing**: URLs appear as underlined, colored links
2. **Clicking a link**: Opens in new browser tab (Ctrl/Cmd + Click for background tab)
3. **Editing**: Click anywhere in the cell (including the display area, but not on the link itself) to enter edit mode
4. **Saving**: Changes are saved automatically when you blur the input
### Click Behavior
- **Click on link**: Opens URL in new tab
- **Click on cell (anywhere except link)**: Enters edit mode and focuses the input
- **Hover over cell**: Shows background highlight to indicate it's editable
- **Hover over link**: Shows link-specific hover effect
### Styling
The links use CSS variables for theming:
- `--link-color`: Link color
- `--link-color-hover`: Hover state color
- Transitions for smooth hover effects
### Testing
Unit tests are provided in `test/url-utils.test.ts`:
- URL detection accuracy
- Text parsing with single/multiple URLs
- Edge cases (URLs at start/end, with special characters)
### Demo
Sample CSV files are provided for testing:
- `test/url-test-sample.csv` - Plain URLs
- `test/markdown-links-sample.csv` - Markdown-style links
### Examples
**Plain URLs:**
```csv
name,website
GitHub,https://github.com
```
Displays: GitHub | https://github.com (as clickable link)
**Markdown Links:**
```csv
name,website
GitHub,[Visit GitHub](https://github.com)
```
Displays: GitHub | Visit GitHub (as clickable link, cleaner!)
**Mixed Content:**
```csv
description
Check [our docs](https://docs.example.com) and https://example.com
```
Both links are clickable, with Markdown link showing as "our docs"
## Future Enhancements
Possible improvements:
- Support for other URL formats (ftp://, file://, etc.)
- URL validation and error indicators
- Custom link styling per column
- Option to disable auto-linking for specific columns
- Support for Obsidian internal links (`[[note]]`)
- Support for Wiki-style links

View file

@ -1,12 +1,12 @@
{
"id": "csv-lite",
"name": "CSV Lite",
"version": "1.1.1",
"version": "1.7.0",
"minAppVersion": "1.8.0",
"description": "Just open and edit CSV files directly, no more. Keep it simple.",
"author": "Jay Bridge",
"authorUrl": "https://github.com/LIUBINfighter",
"fundingUrl": "",
"isDesktopOnly": true
"isDesktopOnly": false
}

324
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-csv",
"version": "1.1.1",
"version": "1.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-csv",
"version": "1.1.1",
"version": "1.7.0",
"license": "MIT",
"dependencies": {
"@codemirror/commands": "^6.8.1",
@ -18,7 +18,7 @@
"@types/node": "^16.11.6",
"@types/papaparse": "^5.3.7",
"builtin-modules": "^3.3.0",
"esbuild": "0.17.3",
"esbuild": "^0.27.2",
"jest": "^29.7.0",
"obsidian": "latest",
"ts-jest": "^29.1.1",
@ -572,10 +572,27 @@
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.3.tgz",
"integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
@ -586,13 +603,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz",
"integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
@ -603,13 +620,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.3.tgz",
"integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
@ -620,13 +637,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz",
"integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
@ -637,13 +654,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz",
"integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
@ -654,13 +671,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz",
"integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
@ -671,13 +688,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz",
"integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
@ -688,13 +705,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz",
"integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
@ -705,13 +722,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz",
"integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
@ -722,13 +739,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz",
"integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
@ -739,13 +756,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz",
"integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
@ -756,13 +773,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz",
"integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
@ -773,13 +790,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz",
"integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
@ -790,13 +807,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz",
"integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
@ -807,13 +824,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz",
"integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
@ -824,13 +841,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz",
"integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
@ -841,13 +858,30 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz",
"integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
@ -858,13 +892,30 @@
"netbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz",
"integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
@ -875,13 +926,30 @@
"openbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz",
"integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
@ -892,13 +960,13 @@
"sunos"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz",
"integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
@ -909,13 +977,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz",
"integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
@ -926,13 +994,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz",
"integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
@ -943,7 +1011,7 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
@ -1733,9 +1801,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2144,9 +2212,9 @@
}
},
"node_modules/esbuild": {
"version": "0.17.3",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.3.tgz",
"integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -2154,31 +2222,35 @@
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.17.3",
"@esbuild/android-arm64": "0.17.3",
"@esbuild/android-x64": "0.17.3",
"@esbuild/darwin-arm64": "0.17.3",
"@esbuild/darwin-x64": "0.17.3",
"@esbuild/freebsd-arm64": "0.17.3",
"@esbuild/freebsd-x64": "0.17.3",
"@esbuild/linux-arm": "0.17.3",
"@esbuild/linux-arm64": "0.17.3",
"@esbuild/linux-ia32": "0.17.3",
"@esbuild/linux-loong64": "0.17.3",
"@esbuild/linux-mips64el": "0.17.3",
"@esbuild/linux-ppc64": "0.17.3",
"@esbuild/linux-riscv64": "0.17.3",
"@esbuild/linux-s390x": "0.17.3",
"@esbuild/linux-x64": "0.17.3",
"@esbuild/netbsd-x64": "0.17.3",
"@esbuild/openbsd-x64": "0.17.3",
"@esbuild/sunos-x64": "0.17.3",
"@esbuild/win32-arm64": "0.17.3",
"@esbuild/win32-ia32": "0.17.3",
"@esbuild/win32-x64": "0.17.3"
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/escalade": {
@ -2293,9 +2365,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3325,9 +3397,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-csv",
"version": "1.1.1",
"version": "1.7.0",
"description": "CSV viewer and editor for Obsidian",
"main": "main.js",
"scripts": {
@ -23,7 +23,7 @@
"@types/node": "^16.11.6",
"@types/papaparse": "^5.3.7",
"builtin-modules": "^3.3.0",
"esbuild": "0.17.3",
"esbuild": "^0.27.2",
"jest": "^29.7.0",
"obsidian": "latest",
"ts-jest": "^29.1.1",

View file

@ -15,7 +15,8 @@ export const enUS = {
},
csv: {
error: 'Error',
parsingFailed: 'Failed to parse CSV. Please check file format.'
parsingFailed: 'Failed to parse CSV. Please check file format.',
parseWarning: 'CSV parse warning:'
},
settings: {
fieldSeparator: 'Field Separator',
@ -44,5 +45,22 @@ export const enUS = {
deleteCol: 'Delete this column',
moveColLeft: 'Move column left',
moveColRight: 'Move column right',
createNewCsv: 'Create new CSV file'
}
,
commands: {
createNewCsv: 'Create new CSV file',
fileExists: 'File already exists'
},
tableMessages: {
atLeastOneRow: 'At least one row must remain',
atLeastOneColumn: 'At least one column must remain'
}
,
notifications: {
undo: 'Undid last action',
noMoreUndo: 'There is nothing more to undo',
redo: 'Redid action',
noMoreRedo: 'There is nothing more to redo'
}
};

View file

@ -10,14 +10,15 @@ export const zhCN = {
sourceMode: '源码模式',
tableMode: '表格模式',
insertRowBefore: '上方插入行',
insertRowAfter: '下方插入行',
insertRowAfter: '下方插入行'
},
editBar: {
placeholder: '编辑选中单元格...'
},
csv: {
error: '错误',
parsingFailed: 'CSV解析失败请检查文件格式'
parsingFailed: 'CSV解析失败请检查文件格式',
parseWarning: 'CSV解析提示:'
},
settings: {
fieldSeparator: '字段分隔符',
@ -46,5 +47,22 @@ export const zhCN = {
deleteCol: '删除本列',
moveColLeft: '向左移动一列',
moveColRight: '向右移动一列',
createNewCsv: '创建新 CSV 文件'
}
,
commands: {
createNewCsv: '创建新 CSV 文件',
fileExists: '文件已存在'
},
tableMessages: {
atLeastOneRow: '至少需要保留一行',
atLeastOneColumn: '至少需要保留一列'
}
,
notifications: {
undo: '已撤销上一步操作',
noMoreUndo: '没有更多可撤销的操作',
redo: '已重做操作',
noMoreRedo: '没有更多可重做的操作'
}
};

View file

@ -1,7 +1,8 @@
import { Plugin, WorkspaceLeaf, moment } from "obsidian";
import { Plugin, WorkspaceLeaf, moment, TFile, Notice } from "obsidian";
import { CSVView, VIEW_TYPE_CSV } from "./view";
import { SourceView, VIEW_TYPE_CSV_SOURCE } from "./source-view";
import { i18n } from "./i18n";
import { FileUtils } from "./utils/file-utils";
interface CSVPluginSettings {
csvSettings: string;
@ -43,6 +44,66 @@ export default class CSVPlugin extends Plugin {
// 将.csv文件扩展名与视图类型绑定
this.registerExtensions(["csv"], VIEW_TYPE_CSV);
// Command: create new csv file (from command palette) — direct creation, no modal
this.addCommand({
id: 'csv-lite-create-new-csv-file',
name: i18n.t('commands.createNewCsv'),
callback: () => {
this.createCsvInFolder('');
}
});
// File explorer context menu: create csv file inside folder or next to file — direct creation
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file) => {
menu.addItem((item) => {
item.setTitle(i18n.t('contextMenu.createNewCsv') || i18n.t('commands.createNewCsv'))
.setIcon('file-plus')
.onClick(() => {
let defaultFolder = '';
if ((file as any).path) {
const fp = (file as any).path as string;
// if path has a slash, take parent folder
const idx = fp.lastIndexOf('/');
if (idx > 0) defaultFolder = fp.substring(0, idx);
}
this.createCsvInFolder(defaultFolder);
});
});
})
);
}
// Create a new CSV in the given folder with an auto-incremented name if necessary
private async createCsvInFolder(folder: string) {
const baseName = 'new.csv';
let name = baseName;
let idx = 0;
while (this.app.vault.getAbstractFileByPath(folder ? `${folder}/${name}` : name)) {
idx++;
name = `new-${idx}.csv`;
if (idx > 1000) {
new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file');
return null;
}
}
const path = folder ? `${folder}/${name}` : name;
try {
await FileUtils.withRetry(() => this.app.vault.create(path, ''));
const created = this.app.vault.getAbstractFileByPath(path) as TFile | null;
if (created) {
await this.app.workspace.getLeaf(true).openFile(created);
}
return created;
} catch (err) {
console.error('CreateCsv: failed to create file', err);
new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file');
return null;
}
}
onunload() {

View file

@ -3,7 +3,7 @@ import { EditorState, Extension, RangeSetBuilder } from "@codemirror/state";
import { EditorView, keymap, placeholder, lineNumbers, drawSelection, Decoration, ViewPlugin, ViewUpdate, DecorationSet } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
export const VIEW_TYPE_CSV_SOURCE = "csv-source-view";
export const VIEW_TYPE_CSV_SOURCE = "csv-lite-source-view";
// 分隔符高亮插件(逗号、分号、制表符)
const separatorHighlightPlugin = ViewPlugin.fromClass(class {
@ -62,7 +62,7 @@ export class SourceView extends TextFileView {
// 1. 在 view header 的 view-actions 区域插入切换按钮lucide/table 图标)
// 交互说明:
// - 切换按钮始终位于 header 区域,风格与 Obsidian 原生一致。
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-view
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-lite-view
// - 若有,则激活该 leafworkspace.setActiveLeaf
// - 若无,则新建 leaf 并打开目标视图。
// - 不主动关闭原有视图,用户可自行关闭。
@ -75,7 +75,7 @@ export class SourceView extends TextFileView {
btn.onclick = async () => {
const file = this.file;
if (!file) return;
const leaves = this.app.workspace.getLeavesOfType('csv-view');
const leaves = this.app.workspace.getLeavesOfType('csv-lite-view');
let found = false;
for (const leaf of leaves) {
if (leaf.view && (leaf.view as any).file && (leaf.view as any).file.path === file.path) {
@ -88,7 +88,7 @@ export class SourceView extends TextFileView {
const newLeaf = this.app.workspace.getLeaf(true);
await newLeaf.openFile(file, { active: true });
await newLeaf.setViewState({
type: 'csv-view',
type: 'csv-lite-view',
active: true,
state: { file: file.path }
});

View file

@ -0,0 +1,90 @@
import { App, Modal, Setting, Notice } from 'obsidian';
import { i18n } from '../i18n';
export interface CreateCsvModalOptions {
defaultFolder?: string;
onSubmit: (path: string) => Promise<void>;
}
export class CreateCsvModal extends Modal {
private defaultFolder: string;
private onSubmit: (path: string) => Promise<void>;
private fileNameInput!: HTMLInputElement;
private folderInput!: HTMLInputElement;
constructor(app: App, options: CreateCsvModalOptions) {
super(app);
this.defaultFolder = options.defaultFolder ?? '';
this.onSubmit = options.onSubmit;
this.titleEl.setText(i18n.t('commands.createNewCsv'));
}
onOpen() {
const { contentEl } = this;
// filename setting
new Setting(contentEl)
.setName(i18n.t('modal.fileName') || 'File name')
.setDesc(i18n.t('modal.fileNameDesc') || 'File name (will have .csv appended if missing)')
.addText((text) => {
this.fileNameInput = text.inputEl;
text.setPlaceholder('new-file.csv');
});
// folder setting
new Setting(contentEl)
.setName(i18n.t('modal.folder') || 'Folder')
.setDesc(i18n.t('modal.folderDesc') || 'Relative folder inside vault (leave empty for root)')
.addText((text) => {
this.folderInput = text.inputEl;
this.folderInput.value = this.defaultFolder ?? '';
text.setPlaceholder('Folder/Subfolder');
});
// buttons
const btnContainer = contentEl.createDiv({ cls: 'mod-cta-container' });
const submitBtn = btnContainer.createEl('button', { text: i18n.t('buttons.create') || 'Create' });
submitBtn.addEventListener('click', async (e) => {
e.preventDefault();
await this.handleSubmit();
});
const cancelBtn = btnContainer.createEl('button', { text: i18n.t('buttons.cancel') || 'Cancel' });
cancelBtn.addEventListener('click', () => this.close());
}
async handleSubmit() {
const rawName = this.fileNameInput?.value.trim() ?? '';
let folder = this.folderInput?.value.trim() ?? '';
if (!rawName) {
new Notice(i18n.t('modal.errors.missingFileName') || 'Please provide a file name');
return;
}
let name = rawName;
if (!name.toLowerCase().endsWith('.csv')) {
name = `${name}.csv`;
}
// normalize folder path: remove leading/trailing slashes
folder = folder.replace(/^\/+|\/+$/g, '');
const path = folder ? `${folder}/${name}` : name;
// prevent overwriting existing files
const existing = this.app.vault.getAbstractFileByPath(path);
if (existing) {
new Notice(i18n.t('commands.fileExists') || 'File already exists');
return;
}
try {
await this.onSubmit(path);
this.close();
} catch (err) {
console.error('CreateCsvModal: failed to create file', err);
new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file');
}
}
}

View file

@ -114,14 +114,14 @@ export class CSVUtils {
const parseResult: any = Papa.parse(csvString, parseConfig as any);
if (parseResult.errors && parseResult.errors.length > 0) {
console.warn("CSV解析警告:", parseResult.errors);
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
console.warn("CSV parse warnings:", parseResult.errors);
new Notice(`${i18n.t("csv.parseWarning")} ${parseResult.errors[0].message}`);
}
return parseResult.data as string[][];
} catch (error) {
console.error("CSV解析错误:", error);
new Notice(`${i18n.t("csv.error")}: CSV解析失败请检查文件格式`);
console.error("CSV parse error:", error);
new Notice(i18n.t("csv.parsingFailed"));
return [[""]];
}
}

View file

@ -1,4 +1,5 @@
import { Notice } from "obsidian";
import { i18n } from "../i18n";
export class HistoryManager<T> {
private history: T[] = [];
@ -38,10 +39,10 @@ export class HistoryManager<T> {
undo(): T | null {
if (this.canUndo()) {
this.currentIndex--;
new Notice("已撤销上一步操作");
new Notice(i18n.t("notifications.undo"));
return this.getCurrentState();
} else {
new Notice("没有更多可撤销的操作");
new Notice(i18n.t("notifications.noMoreUndo"));
return null;
}
}
@ -52,10 +53,10 @@ export class HistoryManager<T> {
redo(): T | null {
if (this.canRedo()) {
this.currentIndex++;
new Notice("已重做操作");
new Notice(i18n.t("notifications.redo"));
return this.getCurrentState();
} else {
new Notice("没有更多可重做的操作");
new Notice(i18n.t("notifications.noMoreRedo"));
return null;
}
}

View file

@ -1,4 +1,5 @@
import { Notice } from "obsidian";
import { i18n } from "../i18n";
export class TableUtils {
/**
@ -42,7 +43,7 @@ export class TableUtils {
*/
static deleteRow(tableData: string[][]): string[][] {
if (tableData.length <= 1) {
new Notice("至少需要保留一行");
new Notice(i18n.t("tableMessages.atLeastOneRow"));
return tableData;
}
@ -61,7 +62,7 @@ export class TableUtils {
*/
static deleteColumn(tableData: string[][]): string[][] {
if (!tableData[0] || tableData[0].length <= 1) {
new Notice("至少需要保留一列");
new Notice(i18n.t("tableMessages.atLeastOneColumn"));
return tableData;
}

172
src/utils/url-utils.ts Normal file
View file

@ -0,0 +1,172 @@
/**
* Utility functions for URL detection and rendering
*/
// URL regex pattern that matches common URL formats
const URL_PATTERN = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
// Markdown link pattern: [text](url)
const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g;
/**
* Detect if text contains URLs or Markdown links
*/
export function containsUrl(text: string): boolean {
const urlRegex = new RegExp(URL_PATTERN);
const markdownRegex = new RegExp(MARKDOWN_LINK_PATTERN);
return urlRegex.test(text) || markdownRegex.test(text);
}
/**
* Parse text and return segments with URL information
*/
export interface TextSegment {
text: string;
isUrl: boolean;
url?: string;
displayText?: string; // For Markdown links
}
export function parseTextWithUrls(text: string): TextSegment[] {
const segments: TextSegment[] = [];
// Find all matches (both URLs and Markdown links) with their positions
interface Match {
index: number;
length: number;
displayText: string;
url: string;
}
const matches: Match[] = [];
// Find Markdown links first (they take precedence)
const markdownRegex = new RegExp(MARKDOWN_LINK_PATTERN);
let mdMatch: RegExpExecArray | null;
while ((mdMatch = markdownRegex.exec(text)) !== null) {
matches.push({
index: mdMatch.index,
length: mdMatch[0].length,
displayText: mdMatch[1], // The text inside [...]
url: mdMatch[2] // The URL inside (...)
});
}
// Find plain URLs (but skip those inside Markdown links)
const urlRegex = new RegExp(URL_PATTERN);
let urlMatch: RegExpExecArray | null;
while ((urlMatch = urlRegex.exec(text)) !== null) {
// Check if this URL is already part of a Markdown link
const isPartOfMarkdown = matches.some(m =>
urlMatch!.index >= m.index && urlMatch!.index < m.index + m.length
);
if (!isPartOfMarkdown) {
matches.push({
index: urlMatch.index,
length: urlMatch[0].length,
displayText: urlMatch[0],
url: urlMatch[0]
});
}
}
// Sort matches by position
matches.sort((a, b) => a.index - b.index);
// Build segments
let lastIndex = 0;
for (const match of matches) {
// Add text before this match
if (match.index > lastIndex) {
segments.push({
text: text.substring(lastIndex, match.index),
isUrl: false
});
}
// Add URL/link segment
segments.push({
text: match.displayText,
isUrl: true,
url: match.url,
displayText: match.displayText
});
lastIndex = match.index + match.length;
}
// Add remaining text
if (lastIndex < text.length) {
segments.push({
text: text.substring(lastIndex),
isUrl: false
});
}
// If no URLs found, return the whole text as one segment
if (segments.length === 0) {
segments.push({
text: text,
isUrl: false
});
}
return segments;
}
/**
* Create a display element with clickable URLs
*/
export function createUrlDisplay(text: string, onClick?: () => void): HTMLElement {
const display = document.createElement('div');
display.className = 'csv-cell-display';
const segments = parseTextWithUrls(text);
for (const segment of segments) {
if (segment.isUrl && segment.url) {
const link = document.createElement('a');
link.href = segment.url;
// Use displayText if available (for Markdown links), otherwise use text
link.textContent = segment.displayText || segment.text;
link.className = 'csv-cell-link';
link.target = '_blank';
link.rel = 'noopener noreferrer';
// Prevent link click from triggering cell edit
link.onclick = (e) => {
e.stopPropagation();
};
display.appendChild(link);
} else {
const span = document.createElement('span');
span.textContent = segment.text;
display.appendChild(span);
}
}
// Add an edit button for cells that are entirely URLs (no other clickable area)
if (onClick) {
const editBtn = document.createElement('span');
editBtn.className = 'csv-cell-edit-btn';
editBtn.textContent = '✎';
editBtn.title = 'Click to edit';
editBtn.onclick = (e) => {
e.stopPropagation();
onClick();
};
display.appendChild(editBtn);
// Also make display clickable (for areas that aren't links)
display.onclick = (e) => {
// Only trigger if not clicking on a link
if ((e.target as HTMLElement).tagName !== 'A') {
onClick();
}
};
}
return display;
}

View file

@ -19,7 +19,7 @@ import { renderTable } from "./view/table-render";
import { HighlightManager } from "./utils/highlight-manager";
import { setupHeaderContextMenu } from "./view/header-context-menu";
export const VIEW_TYPE_CSV = "csv-view";
export const VIEW_TYPE_CSV = "csv-lite-view";
export class CSVView extends TextFileView {
public file: TFile | null;
@ -559,7 +559,7 @@ export class CSVView extends TextFileView {
// 1. 在 view header 的 view-actions 区域插入切换按钮lucide/file-code 图标)
// 交互说明:
// - 切换按钮始终位于 header 区域,风格与 Obsidian 原生一致。
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-source-view
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-lite-source-view
// - 若有,则激活该 leafworkspace.setActiveLeaf
// - 若无,则新建 leaf 并打开目标视图。
// - 不主动关闭原有视图,用户可自行关闭。
@ -572,7 +572,7 @@ export class CSVView extends TextFileView {
btn.onclick = async () => {
const file = this.file;
if (!file) return;
const leaves = this.app.workspace.getLeavesOfType('csv-source-view');
const leaves = this.app.workspace.getLeavesOfType('csv-lite-source-view');
let found = false;
for (const leaf of leaves) {
if (leaf.view && (leaf.view as any).file && (leaf.view as any).file.path === file.path) {
@ -585,7 +585,7 @@ export class CSVView extends TextFileView {
const newLeaf = this.app.workspace.getLeaf(true);
await newLeaf.openFile(file, { active: true, state: { mode: "source" } });
await newLeaf.setViewState({
type: "csv-source-view",
type: "csv-lite-source-view",
active: true,
state: { file: file.path }
});
@ -793,6 +793,9 @@ export class CSVView extends TextFileView {
document,
"keydown",
(event: KeyboardEvent) => {
// Only handle undo/redo when this view is the active leaf
if (this.app.workspace.activeLeaf !== this.leaf) return;
// 检测Ctrl+Z (或Mac上的Cmd+Z)
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
if (event.shiftKey) {
@ -983,7 +986,7 @@ export class CSVView extends TextFileView {
const leaf = this.app.workspace.getLeaf(true);
await leaf.openFile(file, { active: true, state: { mode: "source" } });
await leaf.setViewState({
type: "csv-source-view",
type: "csv-lite-source-view",
active: true,
state: { file: file.path }
});

View file

@ -1,23 +0,0 @@
import { ButtonComponent, Setting } from "obsidian";
import { i18n } from "../i18n";
export interface OperationBarOptions {
operationEl: HTMLElement;
delimiter: string;
quoteChar: string;
onDelimiterChange: (delimiter: string) => void;
onQuoteCharChange: (quoteChar: string) => void;
onUndo: () => void;
onRedo: () => void;
onAddRow: () => void;
onDeleteRow: () => void;
onAddColumn: () => void;
onDeleteColumn: () => void;
onResetColumnWidth: () => void;
isSourceMode: boolean;
onToggleSourceMode: () => void;
}
export function renderOperationBar(options: OperationBarOptions) {
// ...操作栏渲染逻辑骨架...
}

View file

@ -2,6 +2,7 @@ import { TableUtils } from "../utils/table-utils";
import { CSVUtils } from "../utils/csv-utils";
import { i18n } from "../i18n";
import { setIcon } from "obsidian";
import { containsUrl, createUrlDisplay } from "../utils/url-utils";
export interface TableRenderOptions {
tableData: string[][];
@ -270,11 +271,39 @@ export function renderTable(options: TableRenderOptions) {
const td = tableRow.createEl("td", {
attr: { style: `width: ${columnWidths[j] || 100}px` },
});
const input = td.createEl("input", {
cls: "csv-cell-input",
attr: { value: cell },
});
// Create display layer for URL rendering
const hasUrl = containsUrl(cell);
let displayEl: HTMLElement | null = null;
// Function to enter edit mode
const enterEditMode = () => {
const display = td.querySelector('.csv-cell-display') as HTMLElement;
if (display) {
display.style.display = 'none';
}
input.style.display = 'block';
input.focus();
};
if (hasUrl) {
displayEl = createUrlDisplay(cell, enterEditMode);
td.insertBefore(displayEl, input);
}
setupAutoResize(input);
// Hide input initially if URL display is shown
if (hasUrl && displayEl) {
input.style.display = 'none';
displayEl.style.display = 'block';
}
input.oninput = (ev) => {
if (ev.currentTarget instanceof HTMLInputElement) {
saveSnapshot();
@ -290,10 +319,63 @@ export function renderTable(options: TableRenderOptions) {
if (autoResize) {
adjustInputHeight(ev.currentTarget);
}
// Update display on input change
const newHasUrl = containsUrl(ev.currentTarget.value);
const tdEl = ev.currentTarget.parentElement;
if (tdEl) {
const existingDisplay = tdEl.querySelector('.csv-cell-display');
if (newHasUrl) {
// Create or update display
if (existingDisplay) {
existingDisplay.remove();
}
const inputEl = ev.currentTarget;
const newDisplay = createUrlDisplay(ev.currentTarget.value, () => {
// Enter edit mode: show input first, then focus
const disp = tdEl.querySelector('.csv-cell-display') as HTMLElement;
if (disp) {
disp.style.display = 'none';
}
inputEl.style.display = 'block';
inputEl.focus();
});
tdEl.insertBefore(newDisplay, ev.currentTarget);
} else if (existingDisplay) {
// Remove display if no URLs
existingDisplay.remove();
}
}
}
};
input.onfocus = (ev) => {
setActiveCell(i, j, ev.currentTarget as HTMLInputElement);
if (ev.currentTarget instanceof HTMLInputElement) {
setActiveCell(i, j, ev.currentTarget);
// Hide display and show input when focused
const tdEl = ev.currentTarget.parentElement;
if (tdEl) {
const display = tdEl.querySelector('.csv-cell-display') as HTMLElement;
if (display) {
display.style.display = 'none';
ev.currentTarget.style.display = 'block';
}
}
}
};
input.onblur = (ev) => {
if (ev.currentTarget instanceof HTMLInputElement) {
// Show display and hide input when blurred (if contains URL)
const tdEl = ev.currentTarget.parentElement;
if (tdEl) {
const display = tdEl.querySelector('.csv-cell-display') as HTMLElement;
if (display && containsUrl(ev.currentTarget.value)) {
display.style.display = 'block';
ev.currentTarget.style.display = 'none';
}
}
}
};
});
}

View file

@ -53,6 +53,74 @@ If your plugin does not need CSS, delete this file.
line-height: 1.4;
}
/* URL display layer */
.csv-cell-display {
padding: 2px 4px;
min-height: 24px;
cursor: text;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.4;
user-select: text;
transition: background-color 0.15s ease;
position: relative;
padding-right: 24px; /* Space for edit button */
}
/* Hover effect on cell to show it's editable */
.csv-cell-display:hover {
background-color: var(--background-modifier-hover);
border-radius: 3px;
}
/* Edit button for URL cells */
.csv-cell-edit-btn {
cursor: pointer;
color: var(--text-muted);
font-size: 12px;
padding: 2px 4px;
border-radius: 3px;
opacity: 0;
transition: opacity 0.15s ease, background-color 0.15s ease;
position: absolute;
right: 2px;
top: 50%;
transform: translateY(-50%);
}
.csv-cell-display:hover .csv-cell-edit-btn {
opacity: 1;
}
.csv-cell-edit-btn:hover {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}
/* Clickable URL links */
.csv-cell-link {
color: var(--link-color);
text-decoration: none;
cursor: pointer;
border-bottom: 1px solid var(--link-color);
transition: all 0.2s ease;
position: relative;
z-index: 1;
}
.csv-cell-link:hover {
color: var(--link-color-hover);
border-bottom-color: var(--link-color-hover);
text-decoration: none;
background-color: rgba(var(--interactive-accent-rgb), 0.1);
padding: 0 2px;
border-radius: 2px;
}
.csv-cell-link:active {
opacity: 0.7;
}
/* csv-cell-input 特有样式(宽度由 JS 控制) */
.csv-cell-input {
width: 100%;
@ -108,15 +176,28 @@ If your plugin does not need CSS, delete this file.
align-items: center;
}
.csv-delimiter-compact .setting-item {
padding-top: 0 !important;
padding-bottom: 0 !important; /* 去掉 11.25px 的底部内边距 */
margin: 0 !important;
display: flex !important;
align-items: center !important;
padding-top: 0 !important;
padding-bottom: 0 !important; /* 去掉 11.25px 的底部内边距 */
margin: 0 !important;
}
.csv-delimiter-compact .setting-item .setting-item-info {
display: none; /* 隐藏多余描述,保持紧凑 */
}
.csv-delimiter-compact .dropdown {
min-width: 72px; /* 视觉上和按钮宽度接近 */
min-width: 72px; /* 视觉上和按钮宽度接近 */
}
/* Align dropdown vertically with buttons */
.csv-delimiter-compact .setting-item-control {
margin: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
.csv-delimiter-compact .setting-item-control .dropdown {
height: 32px !important;
padding: 0 8px !important;
line-height: 32px !important;
}
/* Pin按钮样式 */

View file

@ -0,0 +1,50 @@
import { TableHistoryManager } from "../src/utils/history-manager";
import { Notice } from "obsidian";
import { i18n } from "../src/i18n";
beforeEach(() => {
// reset Notice mock
(Notice as any).mockClear && (Notice as any).mockClear();
});
test('undo on empty history shows noMoreUndo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
// Initially only one state, cannot undo
const res = hm.undo();
expect(res).toBeNull();
expect(Notice).toHaveBeenCalledTimes(1);
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.noMoreUndo'));
});
test('undo when available shows undo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
hm.push([["b"]]);
// Now we can undo
const res = hm.undo();
expect(res).toEqual([["a"]]);
expect(Notice).toHaveBeenCalledTimes(1);
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.undo'));
});
test('redo on no-op shows noMoreRedo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
// Nothing to redo
const res = hm.redo();
expect(res).toBeNull();
expect(Notice).toHaveBeenCalledTimes(1);
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.noMoreRedo'));
});
test('redo when available shows redo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
hm.push([["b"]]);
// undo back to first
const u = hm.undo();
expect(u).toEqual([["a"]]);
// redo
const r = hm.redo();
expect(r).toEqual([["b"]]);
// Two notices were called (undo, redo)
expect(Notice).toHaveBeenCalledTimes(2);
expect((Notice as any).mock.calls[1][0]).toBe(i18n.t('notifications.redo'));
});

View file

@ -0,0 +1,7 @@
name,website,documentation,description
GitHub,[Visit GitHub](https://github.com),[Docs](https://docs.github.com),Code hosting with Markdown links
Stack Overflow,[SO Community](https://stackoverflow.com),[Help Center](https://stackoverflow.com/help),Q&A for developers - compact display
MDN,[MDN Docs](https://developer.mozilla.org),https://developer.mozilla.org/en-US/docs/Web,Mixed: Markdown and plain URL
Obsidian,https://obsidian.md,[Help](https://help.obsidian.md),Mixed: Plain URL and Markdown
TypeScript,[TS Lang](https://www.typescriptlang.org),[Documentation](https://www.typescriptlang.org/docs/),"Cleaner tables with [TS](https://www.typescriptlang.org)"
Multiple Links,[GitHub](https://github.com) and [GitLab](https://gitlab.com),[Docs1](https://docs.github.com) | [Docs2](https://docs.gitlab.com),Multiple Markdown links in one cell
1 name website documentation description
2 GitHub [Visit GitHub](https://github.com) [Docs](https://docs.github.com) Code hosting with Markdown links
3 Stack Overflow [SO Community](https://stackoverflow.com) [Help Center](https://stackoverflow.com/help) Q&A for developers - compact display
4 MDN [MDN Docs](https://developer.mozilla.org) https://developer.mozilla.org/en-US/docs/Web Mixed: Markdown and plain URL
5 Obsidian https://obsidian.md [Help](https://help.obsidian.md) Mixed: Plain URL and Markdown
6 TypeScript [TS Lang](https://www.typescriptlang.org) [Documentation](https://www.typescriptlang.org/docs/) Cleaner tables with [TS](https://www.typescriptlang.org)
7 Multiple Links [GitHub](https://github.com) and [GitLab](https://gitlab.com) [Docs1](https://docs.github.com) | [Docs2](https://docs.gitlab.com) Multiple Markdown links in one cell

12
test/url-test-sample.csv Normal file
View file

@ -0,0 +1,12 @@
name,website,documentation,description
GitHub,https://github.com,https://docs.github.com,Popular code hosting platform
Stack Overflow,https://stackoverflow.com,https://stackoverflow.com/help,Q&A site for developers
MDN,https://developer.mozilla.org,https://developer.mozilla.org/en-US/docs/Web,Web technology documentation
Obsidian,https://obsidian.md,https://help.obsidian.md,Note-taking and knowledge base app
TypeScript,https://www.typescriptlang.org,https://www.typescriptlang.org/docs/,JavaScript with types
React,https://react.dev,https://react.dev/learn,UI library for web
Vue,https://vuejs.org,https://vuejs.org/guide/,Progressive framework
Node.js,https://nodejs.org,https://nodejs.org/docs/latest/api/,JavaScript runtime
NPM,https://www.npmjs.com,https://docs.npmjs.com/,Package manager
Python,https://www.python.org,https://docs.python.org/3/,General-purpose programming
Mixed Content,https://example.com,Check https://docs.example.com for details,Text with inline URLs
1 name website documentation description
2 GitHub https://github.com https://docs.github.com Popular code hosting platform
3 Stack Overflow https://stackoverflow.com https://stackoverflow.com/help Q&A site for developers
4 MDN https://developer.mozilla.org https://developer.mozilla.org/en-US/docs/Web Web technology documentation
5 Obsidian https://obsidian.md https://help.obsidian.md Note-taking and knowledge base app
6 TypeScript https://www.typescriptlang.org https://www.typescriptlang.org/docs/ JavaScript with types
7 React https://react.dev https://react.dev/learn UI library for web
8 Vue https://vuejs.org https://vuejs.org/guide/ Progressive framework
9 Node.js https://nodejs.org https://nodejs.org/docs/latest/api/ JavaScript runtime
10 NPM https://www.npmjs.com https://docs.npmjs.com/ Package manager
11 Python https://www.python.org https://docs.python.org/3/ General-purpose programming
12 Mixed Content https://example.com Check https://docs.example.com for details Text with inline URLs

151
test/url-utils.test.ts Normal file
View file

@ -0,0 +1,151 @@
import { containsUrl, parseTextWithUrls, createUrlDisplay } from '../src/utils/url-utils';
describe('URL Utils', () => {
describe('containsUrl', () => {
it('should detect HTTP URLs', () => {
expect(containsUrl('http://example.com')).toBe(true);
expect(containsUrl('Visit http://example.com for more')).toBe(true);
});
it('should detect HTTPS URLs', () => {
expect(containsUrl('https://example.com')).toBe(true);
expect(containsUrl('Check out https://github.com')).toBe(true);
});
it('should detect URLs with paths', () => {
expect(containsUrl('https://example.com/path/to/page')).toBe(true);
expect(containsUrl('http://site.com/docs?q=test')).toBe(true);
});
it('should detect URLs with query parameters', () => {
expect(containsUrl('https://example.com?foo=bar&baz=qux')).toBe(true);
});
it('should detect URLs with fragments', () => {
expect(containsUrl('https://example.com#section')).toBe(true);
expect(containsUrl('https://example.com/page#top')).toBe(true);
});
it('should return false for non-URLs', () => {
expect(containsUrl('Just plain text')).toBe(false);
expect(containsUrl('example.com')).toBe(false); // No protocol
expect(containsUrl('www.example.com')).toBe(false); // No protocol
expect(containsUrl('')).toBe(false);
});
it('should handle multiple URLs in text', () => {
expect(containsUrl('Visit https://site1.com and https://site2.com')).toBe(true);
});
it('should detect Markdown-style links', () => {
expect(containsUrl('[GitHub](https://github.com)')).toBe(true);
expect(containsUrl('Check [this link](https://example.com) out')).toBe(true);
});
it('should detect mixed URLs and Markdown links', () => {
expect(containsUrl('[GitHub](https://github.com) and https://example.com')).toBe(true);
});
});
describe('parseTextWithUrls', () => {
it('should parse text with no URLs', () => {
const result = parseTextWithUrls('Just plain text');
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
text: 'Just plain text',
isUrl: false
});
});
it('should parse text with single URL', () => {
const result = parseTextWithUrls('Visit https://example.com today');
expect(result).toHaveLength(3);
expect(result[0]).toEqual({
text: 'Visit ',
isUrl: false
});
expect(result[1]).toEqual({
text: 'https://example.com',
isUrl: true,
url: 'https://example.com',
displayText: 'https://example.com'
});
expect(result[2]).toEqual({
text: ' today',
isUrl: false
});
});
it('should parse text with multiple URLs', () => {
const result = parseTextWithUrls('Check https://site1.com and https://site2.com out');
expect(result).toHaveLength(5);
expect(result[1].isUrl).toBe(true);
expect(result[3].isUrl).toBe(true);
});
it('should parse URL at start of text', () => {
const result = parseTextWithUrls('https://example.com is great');
expect(result[0]).toEqual({
text: 'https://example.com',
isUrl: true,
url: 'https://example.com',
displayText: 'https://example.com'
});
});
it('should parse URL at end of text', () => {
const result = parseTextWithUrls('Visit https://example.com');
expect(result[result.length - 1]).toEqual({
text: 'https://example.com',
isUrl: true,
url: 'https://example.com',
displayText: 'https://example.com'
});
});
it('should parse URL with complex path', () => {
const url = 'https://example.com/path/to/page?param=value&other=123#section';
const result = parseTextWithUrls(url);
expect(result).toHaveLength(1);
expect(result[0].isUrl).toBe(true);
expect(result[0].url).toBe(url);
});
it('should parse Markdown-style link', () => {
const result = parseTextWithUrls('[GitHub](https://github.com)');
expect(result).toHaveLength(1);
expect(result[0].isUrl).toBe(true);
expect(result[0].url).toBe('https://github.com');
expect(result[0].displayText).toBe('GitHub');
});
it('should parse text with Markdown link and plain text', () => {
const result = parseTextWithUrls('Visit [GitHub](https://github.com) now');
expect(result).toHaveLength(3);
expect(result[0]).toEqual({
text: 'Visit ',
isUrl: false
});
expect(result[1].isUrl).toBe(true);
expect(result[1].url).toBe('https://github.com');
expect(result[1].displayText).toBe('GitHub');
expect(result[2]).toEqual({
text: ' now',
isUrl: false
});
});
it('should parse mixed Markdown links and plain URLs', () => {
const result = parseTextWithUrls('[GitHub](https://github.com) and https://example.com');
expect(result).toHaveLength(3);
expect(result[0].isUrl).toBe(true);
expect(result[0].displayText).toBe('GitHub');
expect(result[1].text).toBe(' and ');
expect(result[2].isUrl).toBe(true);
expect(result[2].url).toBe('https://example.com');
});
});
// Note: createUrlDisplay tests are skipped as they require DOM environment
// The function is tested through integration tests in the actual plugin
});

View file

@ -1,3 +1,5 @@
{
"1.0.0": "0.15.0"
"1.0.0": "0.15.0",
"1.1.5": "1.8.0",
"1.7.0": "1.8.0"
}