chore: disable .csv-parser-settings for now #6 (waiting for further dev)

This commit is contained in:
JayBridge 2025-06-06 16:50:39 +08:00
parent 002759a4f9
commit 247872b3f8
7 changed files with 1009 additions and 920 deletions

View file

@ -1,48 +1,55 @@
import { Plugin, WorkspaceLeaf, moment } from 'obsidian';
import { Plugin, WorkspaceLeaf, moment } from "obsidian";
import { CSVView, VIEW_TYPE_CSV } from "./view";
import { i18n } from './i18n';
import { i18n } from "./i18n";
interface CSVPluginSettings {
csvSettings: string;
csvSettings: string;
}
const DEFAULT_SETTINGS: CSVPluginSettings = {
csvSettings: 'default'
}
csvSettings: "default",
};
export default class CSVPlugin extends Plugin {
settings: CSVPluginSettings;
settings: CSVPluginSettings;
async onload() {
await this.loadSettings();
async onload() {
await this.loadSettings();
// 使用 moment.locale() 来安全地获取 Obsidian 的当前语言设置
const obsidianLang = moment.locale();
i18n.setLocale(obsidianLang);
console.log(`CSV Plugin: Setting locale to '${obsidianLang}'`);
console.log(`CSV Plugin: Test translation - buttons.undo: '${i18n.t('buttons.undo')}'`);
// 使用 moment.locale() 来安全地获取 Obsidian 的当前语言设置
const obsidianLang = moment.locale();
i18n.setLocale(obsidianLang);
console.log(`CSV Plugin: Setting locale to '${obsidianLang}'`);
console.log(
`CSV Plugin: Test translation - buttons.undo: '${i18n.t(
"buttons.undo"
)}'`
);
// 注册CSV视图类型
this.registerView(
VIEW_TYPE_CSV,
(leaf: WorkspaceLeaf) => new CSVView(leaf)
);
// 注册CSV视图类型
this.registerView(
VIEW_TYPE_CSV,
(leaf: WorkspaceLeaf) => new CSVView(leaf)
);
// 将.csv文件扩展名与视图类型绑定
this.registerExtensions(["csv"], VIEW_TYPE_CSV);
// 将.csv文件扩展名与视图类型绑定
this.registerExtensions(["csv"], VIEW_TYPE_CSV);
}
}
onunload() {
// 移除视图
// this.app.workspace.detachLeavesOfType(VIEW_TYPE_CSV); // 删除此行
}
onunload() {
// 移除视图
// this.app.workspace.detachLeavesOfType(VIEW_TYPE_CSV); // 删除此行
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async saveSettings() {
await this.saveData(this.settings);
}
}

View file

@ -1,83 +1,86 @@
import * as Papa from 'papaparse';
import { Notice } from 'obsidian';
import { i18n } from '../i18n';
import * as Papa from "papaparse";
import { Notice } from "obsidian";
import { i18n } from "../i18n";
export interface CSVParseConfig {
header: boolean;
dynamicTyping: boolean;
skipEmptyLines: boolean;
delimiter?: string;
quoteChar: string;
escapeChar: string;
header: boolean;
dynamicTyping: boolean;
skipEmptyLines: boolean;
delimiter?: string;
quoteChar: string;
escapeChar: string;
}
export class CSVUtils {
// 现在这是默认设置的唯一权威来源
static defaultConfig: CSVParseConfig = {
header: false,
dynamicTyping: false,
skipEmptyLines: false,
delimiter: ',',
quoteChar: '"', // 关键这是修复报告bug的关键
escapeChar: '"',
};
// 现在这是默认设置的唯一权威来源
static defaultConfig: CSVParseConfig = {
header: false,
dynamicTyping: false,
skipEmptyLines: false,
delimiter: ",",
quoteChar: '"', // 关键这是修复报告bug的关键
escapeChar: '"',
};
/**
* CSV字符串为二维数组
*/
static parseCSV(csvString: string, config?: Partial<CSVParseConfig>): string[][] {
try {
const parseConfig = { ...this.defaultConfig, ...config };
const parseResult = Papa.parse(csvString, parseConfig);
if (parseResult.errors && parseResult.errors.length > 0) {
console.warn("CSV解析警告:", parseResult.errors);
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
}
return parseResult.data as string[][];
} catch (error) {
console.error("CSV解析错误:", error);
new Notice(`${i18n.t('csv.error')}: CSV解析失败请检查文件格式`);
return [[""]];
}
}
/**
* CSV字符串为二维数组
*/
static parseCSV(
csvString: string,
config?: Partial<CSVParseConfig>
): string[][] {
try {
const parseConfig = { ...this.defaultConfig, ...config };
const parseResult = Papa.parse(csvString, parseConfig);
/**
* CSV字符串
*/
static unparseCSV(data: string[][], config?: Papa.UnparseConfig): string {
const defaultUnparseConfig = {
header: false,
newline: "\n"
};
return Papa.unparse(data, { ...defaultUnparseConfig, ...config });
}
if (parseResult.errors && parseResult.errors.length > 0) {
console.warn("CSV解析警告:", parseResult.errors);
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
}
/**
*
*/
static normalizeTableData(tableData: string[][]): string[][] {
if (!tableData || tableData.length === 0) return [[""]];
// 找出最大列数
let maxCols = 0;
for (const row of tableData) {
if (row) {
maxCols = Math.max(maxCols, row.length);
}
}
// 确保每行都有相同的列数
const normalizedData = tableData.map(row => {
const newRow = row ? [...row] : [];
while (newRow.length < maxCols) {
newRow.push("");
}
return newRow;
});
return normalizedData;
}
return parseResult.data as string[][];
} catch (error) {
console.error("CSV解析错误:", error);
new Notice(`${i18n.t("csv.error")}: CSV解析失败请检查文件格式`);
return [[""]];
}
}
/**
* CSV字符串
*/
static unparseCSV(data: string[][], config?: Papa.UnparseConfig): string {
const defaultUnparseConfig = {
header: false,
newline: "\n",
};
return Papa.unparse(data, { ...defaultUnparseConfig, ...config });
}
/**
*
*/
static normalizeTableData(tableData: string[][]): string[][] {
if (!tableData || tableData.length === 0) return [[""]];
// 找出最大列数
let maxCols = 0;
for (const row of tableData) {
if (row) {
maxCols = Math.max(maxCols, row.length);
}
}
// 确保每行都有相同的列数
const normalizedData = tableData.map((row) => {
const newRow = row ? [...row] : [];
while (newRow.length < maxCols) {
newRow.push("");
}
return newRow;
});
return normalizedData;
}
}

View file

@ -1,50 +1,54 @@
import { Notice } from 'obsidian';
import { Notice } from "obsidian";
export class FileUtils {
/**
* Attempts to perform a file operation with retry logic
* @param operation The file operation function to execute
* @param maxRetries Maximum number of retry attempts
* @param delayMs Delay between retries in milliseconds
* @returns Promise resolving to the operation result or rejecting with an error
*/
static async withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 500
): Promise<T> {
let lastError: Error = new Error("Unknown error occurred");
/**
* Attempts to perform a file operation with retry logic
* @param operation The file operation function to execute
* @param maxRetries Maximum number of retry attempts
* @param delayMs Delay between retries in milliseconds
* @returns Promise resolving to the operation result or rejecting with an error
*/
static async withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 500
): Promise<T> {
let lastError: Error = new Error("Unknown error occurred");
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Attempt the operation
return await operation();
} catch (error) {
lastError = error as Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Attempt the operation
return await operation();
} catch (error) {
lastError = error as Error;
// Check if this is a "file busy" error
const isFileBusyError =
error instanceof Error &&
(error.message.includes('EBUSY') ||
error.message.includes('busy') ||
error.message.includes('locked'));
// Check if this is a "file busy" error
const isFileBusyError =
error instanceof Error &&
(error.message.includes("EBUSY") ||
error.message.includes("busy") ||
error.message.includes("locked"));
// If it's not a file busy error or we've used all retries, throw the error
if (!isFileBusyError || attempt === maxRetries) {
break;
}
// If it's not a file busy error or we've used all retries, throw the error
if (!isFileBusyError || attempt === maxRetries) {
break;
}
// Show a notice on first retry
if (attempt === 0) {
new Notice(`File is busy. Retrying... (${attempt + 1}/${maxRetries})`);
}
// Show a notice on first retry
if (attempt === 0) {
new Notice(
`File is busy. Retrying... (${
attempt + 1
}/${maxRetries})`
);
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
// If we get here, all retries failed
throw lastError;
}
// If we get here, all retries failed
throw lastError;
}
}

View file

@ -1,127 +1,127 @@
import { Notice } from 'obsidian';
import { Notice } from "obsidian";
export class HistoryManager<T> {
private history: T[] = [];
private currentIndex: number = -1;
private maxSize: number;
constructor(initialState?: T, maxSize: number = 50) {
this.maxSize = maxSize;
if (initialState) {
this.push(initialState);
}
}
/**
*
*/
push(state: T): void {
// 删除当前索引之后的所有历史记录
if (this.currentIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.currentIndex + 1);
}
// 添加新状态
this.history.push(this.cloneState(state));
// 如果历史记录超过最大限制,删除最早的记录
if (this.history.length > this.maxSize) {
this.history.shift();
} else {
this.currentIndex++;
}
}
/**
*
*/
undo(): T | null {
if (this.canUndo()) {
this.currentIndex--;
new Notice("已撤销上一步操作");
return this.getCurrentState();
} else {
new Notice("没有更多可撤销的操作");
return null;
}
}
/**
*
*/
redo(): T | null {
if (this.canRedo()) {
this.currentIndex++;
new Notice("已重做操作");
return this.getCurrentState();
} else {
new Notice("没有更多可重做的操作");
return null;
}
}
/**
*
*/
getCurrentState(): T | null {
if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
return this.cloneState(this.history[this.currentIndex]);
}
return null;
}
/**
*
*/
canUndo(): boolean {
return this.currentIndex > 0;
}
/**
*
*/
canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
/**
*
*/
reset(initialState?: T): void {
this.history = [];
this.currentIndex = -1;
if (initialState) {
this.push(initialState);
}
}
/**
* 使
*/
protected cloneState(state: T): T {
// 默认实现,应在子类中重写
if (Array.isArray(state)) {
// 处理二维数组情况
if (state.length > 0 && Array.isArray(state[0])) {
return state.map(row => [...row]) as unknown as T;
}
return [...state] as unknown as T;
}
// 对象类型尝试深拷贝
if (typeof state === 'object' && state !== null) {
return JSON.parse(JSON.stringify(state));
}
return state;
}
private history: T[] = [];
private currentIndex: number = -1;
private maxSize: number;
constructor(initialState?: T, maxSize: number = 50) {
this.maxSize = maxSize;
if (initialState) {
this.push(initialState);
}
}
/**
*
*/
push(state: T): void {
// 删除当前索引之后的所有历史记录
if (this.currentIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.currentIndex + 1);
}
// 添加新状态
this.history.push(this.cloneState(state));
// 如果历史记录超过最大限制,删除最早的记录
if (this.history.length > this.maxSize) {
this.history.shift();
} else {
this.currentIndex++;
}
}
/**
*
*/
undo(): T | null {
if (this.canUndo()) {
this.currentIndex--;
new Notice("已撤销上一步操作");
return this.getCurrentState();
} else {
new Notice("没有更多可撤销的操作");
return null;
}
}
/**
*
*/
redo(): T | null {
if (this.canRedo()) {
this.currentIndex++;
new Notice("已重做操作");
return this.getCurrentState();
} else {
new Notice("没有更多可重做的操作");
return null;
}
}
/**
*
*/
getCurrentState(): T | null {
if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
return this.cloneState(this.history[this.currentIndex]);
}
return null;
}
/**
*
*/
canUndo(): boolean {
return this.currentIndex > 0;
}
/**
*
*/
canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
/**
*
*/
reset(initialState?: T): void {
this.history = [];
this.currentIndex = -1;
if (initialState) {
this.push(initialState);
}
}
/**
* 使
*/
protected cloneState(state: T): T {
// 默认实现,应在子类中重写
if (Array.isArray(state)) {
// 处理二维数组情况
if (state.length > 0 && Array.isArray(state[0])) {
return state.map((row) => [...row]) as unknown as T;
}
return [...state] as unknown as T;
}
// 对象类型尝试深拷贝
if (typeof state === "object" && state !== null) {
return JSON.parse(JSON.stringify(state));
}
return state;
}
}
/**
* CSV表格数据
*/
export class TableHistoryManager extends HistoryManager<string[][]> {
protected cloneState(state: string[][]): string[][] {
return state.map(row => [...row]);
}
protected cloneState(state: string[][]): string[][] {
return state.map((row) => [...row]);
}
}

View file

@ -1,88 +1,94 @@
import { Notice } from 'obsidian';
import { Notice } from "obsidian";
export class TableUtils {
/**
*
*/
static calculateColumnWidths(tableData: string[][]): number[] {
if (!tableData || tableData.length === 0 || !tableData[0]) return [];
// 初始化所有列为默认宽度
const columnWidths = tableData[0].map(() => 100);
// 根据内容长度进行简单调整
tableData.forEach(row => {
row.forEach((cell, index) => {
// 根据内容长度估算合适的宽度
const estimatedWidth = Math.max(50, Math.min(300, cell.length * 10));
columnWidths[index] = Math.max(columnWidths[index], estimatedWidth);
});
});
return columnWidths;
}
/**
*
*/
static addRow(tableData: string[][]): string[][] {
const colCount = tableData.length > 0 ? tableData[0].length : 1;
const newRow = Array(colCount).fill("");
return [...tableData, newRow];
}
/**
*
*/
static deleteRow(tableData: string[][]): string[][] {
if (tableData.length <= 1) {
new Notice("至少需要保留一行");
return tableData;
}
return tableData.slice(0, -1);
}
/**
*
*/
static addColumn(tableData: string[][]): string[][] {
return tableData.map(row => [...row, ""]);
}
/**
*
*/
static deleteColumn(tableData: string[][]): string[][] {
if (!tableData[0] || tableData[0].length <= 1) {
new Notice("至少需要保留一列");
return tableData;
}
return tableData.map(row => row.slice(0, -1));
}
/**
* (A, B, C, ..., Z, AA, AB, ...)
*/
static getColumnLabel(index: number): string {
let label = '';
let n = index;
while (n >= 0) {
label = String.fromCharCode(65 + (n % 26)) + label;
n = Math.floor(n / 26) - 1;
}
return label;
}
/**
* A1, B2
*/
static getCellAddress(rowIndex: number, colIndex: number): string {
const colAddress = this.getColumnLabel(colIndex);
const rowAddress = rowIndex + 1;
return `${colAddress}${rowAddress}`;
}
/**
*
*/
static calculateColumnWidths(tableData: string[][]): number[] {
if (!tableData || tableData.length === 0 || !tableData[0]) return [];
// 初始化所有列为默认宽度
const columnWidths = tableData[0].map(() => 100);
// 根据内容长度进行简单调整
tableData.forEach((row) => {
row.forEach((cell, index) => {
// 根据内容长度估算合适的宽度
const estimatedWidth = Math.max(
50,
Math.min(300, cell.length * 10)
);
columnWidths[index] = Math.max(
columnWidths[index],
estimatedWidth
);
});
});
return columnWidths;
}
/**
*
*/
static addRow(tableData: string[][]): string[][] {
const colCount = tableData.length > 0 ? tableData[0].length : 1;
const newRow = Array(colCount).fill("");
return [...tableData, newRow];
}
/**
*
*/
static deleteRow(tableData: string[][]): string[][] {
if (tableData.length <= 1) {
new Notice("至少需要保留一行");
return tableData;
}
return tableData.slice(0, -1);
}
/**
*
*/
static addColumn(tableData: string[][]): string[][] {
return tableData.map((row) => [...row, ""]);
}
/**
*
*/
static deleteColumn(tableData: string[][]): string[][] {
if (!tableData[0] || tableData[0].length <= 1) {
new Notice("至少需要保留一列");
return tableData;
}
return tableData.map((row) => row.slice(0, -1));
}
/**
* (A, B, C, ..., Z, AA, AB, ...)
*/
static getColumnLabel(index: number): string {
let label = "";
let n = index;
while (n >= 0) {
label = String.fromCharCode(65 + (n % 26)) + label;
n = Math.floor(n / 26) - 1;
}
return label;
}
/**
* A1, B2
*/
static getCellAddress(rowIndex: number, colIndex: number): string {
const colAddress = this.getColumnLabel(colIndex);
const rowAddress = rowIndex + 1;
return `${colAddress}${rowAddress}`;
}
}

File diff suppressed because it is too large Load diff

View file

@ -37,48 +37,48 @@ If your plugin does not need CSS, delete this file.
.csv-lite-cell-long {
white-space: pre-wrap;
}
/* 输入框共享样式 */
.csv-cell-input,
.csv-edit-input {
height: auto;
padding: 4px 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
box-sizing: border-box;
background: var(--background-primary);
color: var(--text-normal);
font-size: inherit;
line-height: 1.4;
}
/* 输入框共享样式 */
.csv-cell-input,
.csv-edit-input {
height: auto;
padding: 4px 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
box-sizing: border-box;
background: var(--background-primary);
color: var(--text-normal);
font-size: inherit;
line-height: 1.4;
}
/* csv-cell-input 特有样式(宽度由 JS 控制) */
.csv-cell-input {
width: 100%;
}
/* csv-cell-input 特有样式(宽度由 JS 控制) */
.csv-cell-input {
width: 100%;
}
/* csv-edit-input 特有样式 */
.csv-edit-input {
width: 75%;
margin: 0 auto;
display: block;
text-align: center;
}
/* csv-edit-input 特有样式 */
.csv-edit-input {
width: 75%;
margin: 0 auto;
display: block;
text-align: center;
}
/* 焦点状态样式 */
.csv-cell-input:focus,
.csv-edit-input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
}
.csv-operation-buttons {
/* 焦点状态样式 */
.csv-cell-input:focus,
.csv-edit-input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
}
.csv-operation-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* 列宽调整手柄 */
.resize-handle {
}
/* 列宽调整手柄 */
.resize-handle {
position: absolute;
right: 0;
top: 0;
@ -86,104 +86,102 @@ If your plugin does not need CSS, delete this file.
width: 5px;
background-color: transparent;
cursor: col-resize;
}
.resize-handle:hover,
.resize-handle:active {
}
.resize-handle:hover,
.resize-handle:active {
background-color: var(--interactive-accent);
}
/* 长文本单元格样式 */
td input:focus {
}
/* 长文本单元格样式 */
td input:focus {
white-space: pre-wrap;
}
/* 表格容器,允许水平滚动 */
.table-container {
}
/* 表格容器,允许水平滚动 */
.table-container {
max-width: 100%;
overflow-x: auto;
}
/* 标签页样式 */
.csv-tabs {
}
/* 标签页样式 */
.csv-tabs {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 10px;
}
.csv-tab-headers {
}
.csv-tab-headers {
display: flex;
border-bottom: 1px solid var(--background-modifier-border);
}
.csv-tab-header {
}
.csv-tab-header {
padding: 8px 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-right: 8px;
font-weight: 500;
}
.csv-tab-header:hover {
}
.csv-tab-header:hover {
color: var(--interactive-accent);
}
.csv-tab-header.csv-tab-active {
}
.csv-tab-header.csv-tab-active {
color: var(--interactive-accent);
border-bottom-color: var(--interactive-accent);
}
.csv-tab-panel {
}
.csv-tab-panel {
display: none;
padding: 12px 0;
}
.csv-tab-panel-active {
}
.csv-tab-panel-active {
display: block;
}
/* CSV导入导出选项样式 */
.csv-export-import {
}
/* CSV导入导出选项样式 */
.csv-export-import {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--background-modifier-border);
}
.csv-delimiter-container {
}
.csv-delimiter-container {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
/* 表头行样式 */
.csv-header-row th {
}
/* 表头行样式 */
.csv-header-row th {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}
/* 解析器设置样式 */
}
/* 解析器设置样式 暂时注释掉 */
.csv-parser-settings {
margin-bottom: 16px;
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background-color: var(--background-secondary);
display: none;
margin-bottom: 16px;
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background-color: var(--background-secondary);
}
.csv-parser-settings .setting-item {
border: none;
padding: 8px 0;
border: none;
padding: 8px 0;
}
.csv-parser-settings .setting-item-info {
margin-right: 16px;
margin-right: 16px;
}
.csv-parser-settings .setting-item-control input {
max-width: 200px;
max-width: 200px;
}