Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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.
- **Switch Delimiter Non‑Destructively**: Auto‑detects 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.

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. -->

Expand All @@ -38,27 +39,38 @@ Because it is designed to be simple and straightforward. It also keeps up with t

## Philosophy

- No fancy UI, SAY NO TO
- modals
- sidebar
- settingTab <!-- - Readme. Actually it's important to update readme, I hope you won't notice this line QAQ [#33](https://github.com/LIUBINfighter/csv-lite/issues/33) -->
- other online docs & tutorials
- All functions of the ui components above will be covered in a single File view.
- All in TextFileView/workspace.
- No more pollution to your vault, all metadata store in `./.obsidian/plugins/csv` in json format. (Currently no `data.json`)
- Every function must be completed within 3 steps:
- No fancy UI, SAY NO TO
- modals
- sidebar
- settingTab <!-- - Readme. Actually it's important to update readme, I hope you won't notice this line QAQ [#33](https://github.com/LIUBINfighter/csv-lite/issues/33) -->
- other online docs & tutorials
- All functions of the ui components above will be covered in a single File view.
- All in TextFileView/workspace.
- No more pollution to your vault, all metadata store in `./.obsidian/plugins/csv` in json format. (Currently no `data.json`)
- Every function must be completed within 3 steps:
0. Locate it visually
1. Click/Hotkey
2. Input (if needed)
3. Confirm/Leave
- The interface should remain minimal yet functional.
- Users shouldn't need to leave their workflow environment.
- CSV manipulation should be as natural as text editing.
1. Click/Hotkey
2. Input (if needed)
3. Confirm/Leave
- The interface should remain minimal yet functional.
- Users shouldn't need to leave their workflow environment.
- CSV manipulation should be as natural as text editing.

## Purpose

This plugin enhances Obsidian's functionality by allowing users to work with CSV (Comma-Separated Values) files seamlessly within their vault, eliminating the need to switch between different applications for CSV handling.

### Delimiter Handling Philosophy

Team repositories or shared datasets often mix delimiter styles (`,` `;` `\t`). For safety:

1. The plugin auto-detects the delimiter when opening a file.
2. The dropdown ("Auto, , ;") lets you temporarily re-interpret the file without changing it on disk.
3. Saving edits (cell changes, row/column operations) writes the file back using the ORIGINAL detected delimiter, not the one you temporarily selected—unless the file already used that delimiter.
4. This prevents accidental mass diffs in version control or breaking pipelines that assume a specific separator.

If you ever need an explicit “convert delimiter” feature, open an issue—we’ll gate it behind a confirmation instead of doing it silently.

## Getting Started

Install the plugin through Obsidian's community plugins section and start viewing your CSV files directly in your notes.
Expand Down
36 changes: 24 additions & 12 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- **切换** 表格视图和原始源码模式
- **编辑** 直接点击并输入即可编辑单元格
- **管理** 行和列(添加、删除、移动),只需右键点击表头
- **非破坏性分隔符切换**:自动检测文件原始分隔符(逗号、分号、制表符等)。在工具栏切换分隔符只会重新解析视图,不会改写文件;保存编辑时仍使用文件原始分隔符,避免产生大规模 diff。

我计划仅用 json 和 csv 设计自己的数据库。如果你有关于表格或 csv 的新想法,欢迎提 issue(我会考虑加入 csv-lite 或新插件),或在社区中搜索。<!-- 对于 Markdown 内编辑,推荐 `anyblock`,但语法更复杂。 -->

Expand All @@ -33,27 +34,38 @@

## 理念

- 没有花哨的 UI,拒绝以下内容:
- 模态框
- 侧边栏
- 设置选项卡 <!-- - Readme. 实际上更新 readme 很重要,希望你不会注意到这一行 QAQ [#33](https://github.com/LIUBINfighter/csv-lite/issues/33) -->
- 多余的在线文档和教程
- 上述所有 UI 组件的功能都将在单一文件视图中实现
- 一切都在 TextFileView/workspace 中完成
- 不再污染你的库,所有元数据都以 json 格式存储在 `./.obsidian/plugins/csv`(目前没有 `data.json`)
- 每个功能必须在 3 步内完成:
- 没有花哨的 UI,拒绝以下内容:
- 模态框
- 侧边栏
- 设置选项卡 <!-- - Readme. 实际上更新 readme 很重要,希望你不会注意到这一行 QAQ [#33](https://github.com/LIUBINfighter/csv-lite/issues/33) -->
- 多余的在线文档和教程
- 上述所有 UI 组件的功能都将在单一文件视图中实现
- 一切都在 TextFileView/workspace 中完成
- 不再污染你的库,所有元数据都以 json 格式存储在 `./.obsidian/plugins/csv`(目前没有 `data.json`)
- 每个功能必须在 3 步内完成:
0. 视觉定位
1. 点击/快捷键
2. 输入(如需)
3. 确认/离开
- 界面应保持极简但实用
- 用户无需离开工作流环境
- CSV 操作应像文本编辑一样自然
- 界面应保持极简但实用
- 用户无需离开工作流环境
- CSV 操作应像文本编辑一样自然

## 目的

本插件增强了 Obsidian 的功能,让用户可以在库内无缝处理 CSV(逗号分隔值)文件,无需在不同应用间切换。

### 分隔符处理策略

多人协作或跨区域数据常混用不同分隔符(`,` `;` `\t`)。为降低风险:

1. 打开文件时自动检测分隔符。
2. 工具栏下拉(Auto, , ;)只是临时“重新解释”数据,不修改磁盘文件。
3. 你在表格中做的内容编辑(单元格/行列操作)保存时仍按最初检测出的原始分隔符写回。
4. 避免意外把所有分号文件改成逗号,减少版本库噪音。

如果需要真正“转换分隔符”的功能,欢迎提 issue,我们会加确认步骤而不是静默改写。

## 开始使用

通过 Obsidian 的社区插件区安装本插件,即可直接在笔记中查看和编辑 CSV 文件。
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { i18n } from "./i18n";

interface CSVPluginSettings {
csvSettings: string;
preferredDelimiter?: string; // user global preference, e.g. ',' ';' '\t' or 'auto'
}

const DEFAULT_SETTINGS: CSVPluginSettings = {
csvSettings: "default",
preferredDelimiter: 'auto',
};

export default class CSVPlugin extends Plugin {
Expand Down
89 changes: 85 additions & 4 deletions src/utils/csv-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface CSVParseConfig {
header: boolean;
dynamicTyping: boolean;
skipEmptyLines: boolean;
delimiter?: string;
delimiter?: string; // use 'auto' to enable auto-detection
quoteChar: string;
escapeChar: string;
}
Expand All @@ -17,10 +17,85 @@ export class CSVUtils {
header: false,
dynamicTyping: false,
skipEmptyLines: false,
delimiter: ",",
delimiter: 'auto',
quoteChar: '"', // 关键:这是修复报告bug的关键
escapeChar: '"',
};
};

/**
* 简单的分隔符检测器:在前几行统计候选分隔符(逗号/分号/制表符/竖线)
* 对每个候选符号,统计每行在引号外出现的分隔符数量,选择字段数量最一致且>1的分隔符。
*/
static detectDelimiter(csvString: string, quoteChar = '"') : string {
if (!csvString || csvString.length === 0) return ',';
const candidates = [',', ';', '\t', '|'];

// Build logical records by honoring quoted multiline fields.
const records: string[] = [];
let cur = '';
let inQuote = false;
for (let i = 0; i < csvString.length; i++) {
const ch = csvString[i];
if (ch === quoteChar) {
// handle escaped quote ""
if (i + 1 < csvString.length && csvString[i + 1] === quoteChar) {
cur += quoteChar;
i++; // skip escaped
continue;
}
inQuote = !inQuote;
cur += ch;
continue;
}
if (!inQuote && ch === '\n') {
records.push(cur);
cur = '';
continue;
}
// keep CR if present inside quotes or ignore standalone CR
if (!inQuote && ch === '\r') continue;
cur += ch;
}
if (cur.length > 0) records.push(cur);

// limit to first 20 non-empty records
const sample = records.map(r => r).filter(r => r.trim().length > 0).slice(0, 20);
if (sample.length === 0) return ',';

function countFields(record: string, delim: string) {
let inQ = false;
let count = 0;
for (let i = 0; i < record.length; i++) {
const ch = record[i];
if (ch === quoteChar) {
if (i + 1 < record.length && record[i + 1] === quoteChar) {
i++; // skip escaped
continue;
}
inQ = !inQ;
continue;
}
if (!inQ && ch === delim) count++;
}
return count + 1;
}

let best: { delim: string; score: number; avgFields: number; consistency: number } | null = null;
for (const d of candidates) {
const counts = sample.map(r => countFields(r, d));
const avg = counts.reduce((a,b) => a+b,0)/counts.length;
const variance = counts.reduce((a,b) => a + Math.pow(b - avg, 2), 0) / counts.length;
const score = (avg > 1 ? avg : 0) - variance * 0.1;
if (!best || score > best.score) {
best = { delim: d, score, avgFields: avg, consistency: variance };
}
}
if (best && best.avgFields >= 1.5) {
return best.delim;
}
return ',';
}


/**
* 解析CSV字符串为二维数组
Expand All @@ -31,7 +106,12 @@ export class CSVUtils {
): string[][] {
try {
const parseConfig = { ...this.defaultConfig, ...config };
const parseResult = Papa.parse(csvString, parseConfig);
// 如果启用了自动检测,则尝试检测分隔符并覆盖parseConfig.delimiter
if (!parseConfig.delimiter || parseConfig.delimiter === 'auto') {
const detected = this.detectDelimiter(csvString, parseConfig.quoteChar);
parseConfig.delimiter = detected;
}
const parseResult: any = Papa.parse(csvString, parseConfig as any);

if (parseResult.errors && parseResult.errors.length > 0) {
console.warn("CSV解析警告:", parseResult.errors);
Expand All @@ -58,6 +138,7 @@ export class CSVUtils {
return Papa.unparse(data, { ...defaultUnparseConfig, ...config });
}


/**
* 确保表格数据规整(所有行的列数相同)
*/
Expand Down
Loading