mirror of
https://github.com/hangeol-chang/obsidian-csv-allinone.git
synced 2026-07-22 05:37:25 +00:00
Merge branch 'master' into DOCS-README
This commit is contained in:
commit
a4b9173fd1
11 changed files with 626 additions and 174 deletions
40
main.ts
40
main.ts
|
|
@ -1,12 +1,12 @@
|
|||
import { readCSV_, saveCSV_} from 'src/csvfilemanager';
|
||||
import { createCsvInputModal_, createCsvTableView_ } from './src/csvdisplay';
|
||||
import { CSVTable } from './src/types'
|
||||
|
||||
import {
|
||||
App, Modal, Plugin, PluginSettingTab, Setting,
|
||||
TFile,
|
||||
App, Plugin, PluginSettingTab, Setting,
|
||||
} from 'obsidian';
|
||||
import { createInflate } from 'zlib';
|
||||
|
||||
import { createCsvInputModal_, createCsvTableView_ } from './src/csvPlugin';
|
||||
import { readCSV_, saveCSV_} from 'src/csvFilemanager';
|
||||
import { CSVTable, Header } from './src/types'
|
||||
import CsvExplorerModal, { getCsvFileStructure } from 'src/csvExplorer';
|
||||
import CsvCreateModal, { createCsvFile_ } from 'src/csvCreator';
|
||||
|
||||
interface CsvPluginSettings {
|
||||
mySetting: string;
|
||||
|
|
@ -28,13 +28,15 @@ export default class CsvPlugin extends Plugin {
|
|||
}
|
||||
|
||||
//// csvdisplay.ts
|
||||
openCsvInputModal = async (app: App, headers: string[], fileName: string) => {
|
||||
openCsvInputModal = async (app: App, headers: Header[], fileName: string) => {
|
||||
createCsvInputModal_(app, headers, fileName);
|
||||
}
|
||||
createCsvTableView = (csvTable: CSVTable): string => {
|
||||
createCsvTableView = (csvTable: CSVTable): HTMLElement => {
|
||||
return createCsvTableView_(csvTable);
|
||||
}
|
||||
|
||||
createCsvFile = (filename: string, filePath: string, columnData: { name:string, type: string }[]) => {
|
||||
createCsvFile_(this.app, filename, filePath, columnData);
|
||||
}
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -43,6 +45,24 @@ export default class CsvPlugin extends Plugin {
|
|||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new CsvSettingTab(this.app, this));
|
||||
|
||||
// commands
|
||||
this.addCommand({
|
||||
id: "create-csv-table",
|
||||
name: "Create CSV Table",
|
||||
callback: () => {
|
||||
new CsvCreateModal(this.app).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "open-csv-explorer",
|
||||
name: "Open CSV Explorer",
|
||||
callback: async () => {
|
||||
const csvStructure = await getCsvFileStructure(this.app);
|
||||
new CsvExplorerModal(this.app, csvStructure).open();
|
||||
}
|
||||
})
|
||||
}
|
||||
onunload() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "csv-allinone",
|
||||
"name": "CSV All-in-One",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.7.7",
|
||||
"description": "all about csv.",
|
||||
"author": "hihangeol",
|
||||
"authorUrl": "https://github.com/Hangeol-Chang/obsidian-csv-allinone",
|
||||
"fundingUrl": "https://github.com/Hangeol-Chang/obsidian-csv-allinone",
|
||||
"fundingUrl": "https://buymeacoffee.com/hihangeol",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
168
src/csvCreateor.ts
Normal file
168
src/csvCreateor.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { App, Modal, Notice, Plugin } from 'obsidian';
|
||||
import './styles/csvCreator.css';
|
||||
import { columnTypes } from './types';
|
||||
|
||||
// dataviewjs 등으로 자동적으로 파일을 생성하게 할 때 사용할 수 있는 api
|
||||
export const createCsvFile_ = (app: App, filename: string, filePath: string, columnData: { name:string, type: string }[]) : boolean => {
|
||||
// .csv 내용 생성 (헤더만 추가)
|
||||
const csvContent = columnData.map((col) => col.name).join(',') + '\n';
|
||||
|
||||
// .csv.meta 내용 생성
|
||||
const metaContent = JSON.stringify(
|
||||
columnData.reduce((acc, col) => {
|
||||
acc[col.name] = col.type;
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
// 파일 저장
|
||||
const vault = app.vault;
|
||||
const fullPathCsv = `${filePath}/${filename}.csv`;
|
||||
const fullPathMeta = `${filePath}/${filename}.csv.meta`;
|
||||
|
||||
// .csv 파일 저장
|
||||
vault.adapter.write(fullPathCsv, csvContent).then(() => {
|
||||
new Notice(`${fullPathCsv} file has been created.`);
|
||||
}).catch((err) => {
|
||||
new Notice(`Error occurred while creating .csv file:\n${err}`);
|
||||
return false;
|
||||
});
|
||||
|
||||
// .csv.meta 파일 저장
|
||||
vault.adapter.write(fullPathMeta, metaContent).then(() => {
|
||||
new Notice(`${fullPathMeta} file has been created.`);
|
||||
}).catch((err) => {
|
||||
new Notice(`Error occurred while creating .csv.meta file:\n${err}`);
|
||||
return false;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export default class CsvCreateModal extends Modal {
|
||||
|
||||
columnsWrapper: HTMLElement;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
addColumn() {
|
||||
let inputWarpper = this.columnsWrapper.createEl('tr');
|
||||
let columnTd = inputWarpper.createEl('td');
|
||||
let columnInput = columnTd.createEl('input');
|
||||
columnInput.classList.add('colname-input');
|
||||
columnInput.placeholder = 'name';
|
||||
|
||||
let typeTd = inputWarpper.createEl('td');
|
||||
let typeSelect = typeTd.createEl('select');
|
||||
typeSelect.classList.add('coltype-select');
|
||||
for (let type of columnTypes) {
|
||||
let option = typeSelect.createEl('option');
|
||||
option.value = type;
|
||||
option.text = type;
|
||||
};
|
||||
}
|
||||
|
||||
createCsvFile() {
|
||||
// .csv 파일 생성
|
||||
const { contentEl } = this;
|
||||
|
||||
// 파일 이름과 경로 가져오기
|
||||
const filenameInput = contentEl.querySelector('.filename-input') as HTMLInputElement;
|
||||
const filePathInput = contentEl.querySelector('.filepath-input') as HTMLInputElement;
|
||||
|
||||
let filename = filenameInput.value.trim();
|
||||
let filePath = filePathInput.value.trim() || '/';
|
||||
|
||||
// validation
|
||||
if (!filename) {
|
||||
new Notice('Please enter a file name.');
|
||||
return false;
|
||||
}
|
||||
if(filename.endsWith('.csv')) {
|
||||
filename = filename.slice(0, -4);
|
||||
}
|
||||
if(filePath.length > 1 && filePath.endsWith('/')) {
|
||||
filePath = filePath.slice(0, -1);
|
||||
}
|
||||
|
||||
// 컬럼 정보 수집
|
||||
const columnData: { name: string; type: string }[] = [];
|
||||
this.columnsWrapper.querySelectorAll('tr').forEach((columnEl) => {
|
||||
const inputEl = columnEl.querySelector('input') as HTMLInputElement;
|
||||
const selectEl = columnEl.querySelector('select') as HTMLSelectElement;
|
||||
|
||||
const columnName = inputEl?.value.trim();
|
||||
const columnType = selectEl?.value;
|
||||
|
||||
if (columnName && columnType) {
|
||||
columnData.push({ name: columnName, type: columnType });
|
||||
}
|
||||
});
|
||||
|
||||
if (columnData.length === 0) {
|
||||
new Notice('add at least one column');
|
||||
return false;
|
||||
}
|
||||
|
||||
return createCsvFile_(this.app, filename, filePath, columnData);
|
||||
}
|
||||
|
||||
// common api
|
||||
onOpen() {
|
||||
let {contentEl} = this;
|
||||
contentEl.createEl('h2', {text: 'Create CSV Table'});
|
||||
contentEl.createEl('hr');
|
||||
|
||||
// file setting
|
||||
let filenameContainer = contentEl.createEl('div');
|
||||
filenameContainer.classList.add('filename-container');
|
||||
|
||||
let filenameEl = filenameContainer.createEl('input');
|
||||
filenameEl.placeholder = 'Title';
|
||||
filenameEl.classList.add('filename-input');
|
||||
|
||||
let filePathEl = filenameContainer.createEl('input');
|
||||
filePathEl.placeholder = 'file path(default: root)';
|
||||
filePathEl.classList.add('filepath-input');
|
||||
|
||||
// add column button
|
||||
let addColumnButton = contentEl.createEl('button', {text: 'Add Column'});
|
||||
addColumnButton.addEventListener('click', () => { this.addColumn(); });
|
||||
|
||||
// column making table
|
||||
let columnsTable = contentEl.createEl('table');
|
||||
columnsTable.classList.add('columns-table');
|
||||
let tHead = columnsTable.createEl('thead');
|
||||
let tHeadRow = tHead.createEl('tr');
|
||||
tHeadRow.classList.add('columns-table-header');
|
||||
|
||||
tHeadRow.createEl('th', {text: 'Column Name'});
|
||||
tHeadRow.createEl('th', {text: 'Type'});
|
||||
|
||||
columnsTable.createEl('hr');
|
||||
this.columnsWrapper = columnsTable.createEl('tbody');
|
||||
this.columnsWrapper.classList.add('columns-wrapper');
|
||||
|
||||
// submit button
|
||||
contentEl.createEl('hr');
|
||||
let buttonEl = contentEl.createEl('button', {text: 'Create'});
|
||||
buttonEl.addEventListener('click', () => {
|
||||
event?.preventDefault();
|
||||
const result = this.createCsvFile();
|
||||
if(result) {
|
||||
this.close();
|
||||
}
|
||||
else {
|
||||
new Notice('Error occurred while creating .csv file');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
52
src/csvExplorer.ts
Normal file
52
src/csvExplorer.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { App, Modal, TFile } from 'obsidian';
|
||||
|
||||
/*
|
||||
obsidian 탐색기에서 csv 파일이 표시가 안되기 때문에,\
|
||||
파일 탐색하고 지우는 용도로 사용.
|
||||
*/
|
||||
export const getCsvFileStructure = async (app: App): Promise<Record<string, string[]>> => {
|
||||
const csvStructure: Record<string, string[]> = {};
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
// Vault 내의 모든 파일 순회
|
||||
files.forEach((file: TFile) => {
|
||||
if (file.extension === "csv") {
|
||||
if(file.parent) {
|
||||
const folderPath = file.parent.path;
|
||||
if (!csvStructure[folderPath])
|
||||
csvStructure[folderPath] = [];
|
||||
|
||||
csvStructure[folderPath].push(file.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
return csvStructure;
|
||||
}
|
||||
|
||||
export default class CsvExplorerModal extends Modal {
|
||||
csvStructure: Record<string, string[]> = {};
|
||||
|
||||
constructor(app: App, csvStructure: Record<string, string[]>) {
|
||||
super(app);
|
||||
this.csvStructure = csvStructure;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", { text: "CSV Explorer" });
|
||||
|
||||
Object.keys(this.csvStructure).forEach((folder) => {
|
||||
contentEl.createEl("h3", { text: folder });
|
||||
this.csvStructure[folder].forEach((file) => {
|
||||
contentEl.createEl("p", { text: file });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
94
src/csvFilemanager.ts
Normal file
94
src/csvFilemanager.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { CSVTable, Header } from './types'
|
||||
import { App, TFile } from 'obsidian';
|
||||
|
||||
// meta file을 수정하는 로직이 포함되어야 함.
|
||||
|
||||
// public api
|
||||
export const readCSV_ = async (app: App, fileName: string): Promise<CSVTable | null> => {
|
||||
if(fileName.endsWith(".csv")) { // 확장자 검사.
|
||||
// read .csv file,
|
||||
const csvContent = await loadFile(app, fileName); // 파일 로드.
|
||||
|
||||
const lines = csvContent.split("\n").map(line => line.trim());
|
||||
const headersString = lines.shift()?.split(",") ?? [];
|
||||
const rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
|
||||
// read .csv.meta file,
|
||||
// meta file 존재 여부 확인해서 없으면 생성.
|
||||
if(await app.vault.adapter.exists(`${fileName}.meta`) == false) {
|
||||
const headers: { name: string, type: string }[] =
|
||||
headersString.map((header) => ({ name: header, type: "string" }));
|
||||
|
||||
const metaContent = JSON.stringify(
|
||||
headers.reduce((acc, col) => {
|
||||
acc[col.name] = col.type;
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
null,
|
||||
2
|
||||
);
|
||||
// const metaContent = JSON.stringify(headers, null, 2);
|
||||
await saveFile(app, `${fileName}.meta`, metaContent);
|
||||
}
|
||||
|
||||
|
||||
// meta file 로드.
|
||||
const metaData = await loadFile(app, `${fileName}.meta`); // 메타 파일 로드.
|
||||
const headers: Header[] = Object.entries(JSON.parse(metaData));
|
||||
|
||||
return await new CSVTable(headers, rows) // CSV 파싱.
|
||||
} else {
|
||||
console.error(`file extension is not csv : ${fileName} (read)`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export const saveCSV_ = async (app: App, fileName: string, table: CSVTable): Promise<void> => {
|
||||
if(fileName.endsWith(".csv")) {
|
||||
// contents, meta로 분리 필요.
|
||||
const content = table.toCSV();
|
||||
await saveFile(app, fileName, content);
|
||||
// const meta = table.getMeta();
|
||||
// await saveMetaFile(app, `${fileName}.meta`, meta);
|
||||
} else {
|
||||
console.error(`file extension is not csv : ${fileName} (save)`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// save, load
|
||||
const saveFile = async (app: App, fileName: string, content: string): Promise<void> => {
|
||||
const vault = app.vault;
|
||||
|
||||
const existingFile = vault.getAbstractFileByPath(fileName);
|
||||
if (existingFile instanceof TFile) {
|
||||
// 파일이 존재하면 덮어쓰기
|
||||
// console.log(`file exists, modify : ${fileName}`);
|
||||
await vault.modify(existingFile, content);
|
||||
} else {
|
||||
// 파일이 존재하지 않으면 새로 생성
|
||||
// console.log(`file not exists, create : ${fileName}`);
|
||||
await vault.create(fileName, content);
|
||||
}
|
||||
}
|
||||
const saveMetaFile = async (app: App, fileName: string, content: Header[]): Promise<void> => {
|
||||
|
||||
}
|
||||
|
||||
const loadFile = async (app: App, fileName: string): Promise<string> => {
|
||||
const vault = app.vault;
|
||||
const file = vault.getAbstractFileByPath(fileName);
|
||||
if (file instanceof TFile) {
|
||||
return await vault.read(file);
|
||||
} else {
|
||||
throw new Error('file not found');
|
||||
}
|
||||
}
|
||||
|
||||
// api 재설계 필요. -> 애초에 사용할 일 있는지 모르겠음.
|
||||
// const parseCSV = (content: string): CSVTable => {
|
||||
// const lines = content.split("\n").map(line => line.trim());
|
||||
// const headers = lines.shift()?.split(",") ?? [];
|
||||
// const rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
|
||||
// return new CSVTable(headers, rows);
|
||||
// }
|
||||
169
src/csvPlugin.ts
Normal file
169
src/csvPlugin.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { App, Modal } from "obsidian";
|
||||
import { CSVRow, CSVTable, Header } from "./types";
|
||||
import { readCSV_, saveCSV_ } from "./csvFilemanager";
|
||||
import './styles/csvPlugin.css';
|
||||
|
||||
export const createCsvInputModal_ = async (app: App, headers: Header[], fileName: string) => {
|
||||
const form = generateForm(fileName, headers);
|
||||
|
||||
const modal = new Modal(app);
|
||||
modal.contentEl.empty();
|
||||
modal.contentEl.appendChild(form);
|
||||
modal.open();
|
||||
|
||||
// 파일을 새로 읽어서, 추가된 라인을 포함 저장.
|
||||
const formElement = modal.contentEl.querySelector('#csv-input-form') as HTMLFormElement;
|
||||
formElement.addEventListener('submit', async (event) => {
|
||||
event.preventDefault(); // 기본 제출 동작 방지
|
||||
// 입력 값 처리 가져오기.
|
||||
const formData = new FormData(formElement);
|
||||
|
||||
const newRow: CSVRow = [];
|
||||
for(const header of headers) {
|
||||
newRow.push(formData.get(header[0]) as string);
|
||||
}
|
||||
|
||||
// 파일 읽기
|
||||
const table = await readCSV_(app, fileName);
|
||||
if(table === null) {
|
||||
console.error("file not found");
|
||||
return;
|
||||
}
|
||||
// 파일 저장
|
||||
table?.append(newRow);
|
||||
// console.log(table);
|
||||
saveCSV_(app, fileName, table);
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
|
||||
const generateForm = (fileName: string, headers: Header[]): HTMLFormElement => {
|
||||
const form = document.createElement("form");
|
||||
form.id = "csv-input-form";
|
||||
|
||||
const fileNameArray = fileName.split('/');
|
||||
const fileNameShort = fileNameArray[fileNameArray.length - 1];
|
||||
|
||||
const titleTinkle = document.createElement("h6");
|
||||
titleTinkle.textContent = "Add new row to";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = fileNameShort;
|
||||
|
||||
form.appendChild(titleTinkle);
|
||||
form.appendChild(title);
|
||||
|
||||
// 수평선 삽입.
|
||||
form.appendChild(document.createElement("hr"));
|
||||
|
||||
// 폼 요소 생성
|
||||
// 입력 필드 생성 및 추가
|
||||
headers.forEach(header => {
|
||||
const field = createHeaderInputField(header);
|
||||
form.appendChild(field);
|
||||
});
|
||||
|
||||
// 버튼 그룹 생성 및 추가
|
||||
const buttonGroup = document.createElement("div");
|
||||
buttonGroup.className = "button-group";
|
||||
|
||||
const submitButton = document.createElement("button");
|
||||
submitButton.className = "submit-button";
|
||||
submitButton.type = "submit";
|
||||
submitButton.textContent = "Submit";
|
||||
|
||||
buttonGroup.appendChild(submitButton);
|
||||
form.appendChild(buttonGroup);
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
const createHeaderInputField = (key: [string, string]): HTMLDivElement => {
|
||||
// 입력 필드 래퍼 생성
|
||||
const formGroup = document.createElement("div");
|
||||
formGroup.className = "form-group";
|
||||
|
||||
// 라벨 생성
|
||||
const label = document.createElement("label");
|
||||
label.className = "input-key";
|
||||
label.htmlFor = key[0];
|
||||
label.textContent = key[0];
|
||||
|
||||
// 입력 필드 생성
|
||||
const input = document.createElement("input");
|
||||
input.className = "input-value";
|
||||
input.type = "text";
|
||||
input.id = key[0];
|
||||
input.name = key[0];
|
||||
input.placeholder = key[1];
|
||||
|
||||
// 요소 추가
|
||||
formGroup.appendChild(label);
|
||||
formGroup.appendChild(input);
|
||||
|
||||
return formGroup;
|
||||
}
|
||||
|
||||
const createInputField = (key: string): HTMLDivElement => {
|
||||
// 입력 필드 래퍼 생성
|
||||
const formGroup = document.createElement("div");
|
||||
formGroup.className = "form-group";
|
||||
|
||||
// 라벨 생성
|
||||
const label = document.createElement("label");
|
||||
label.className = "input-key";
|
||||
label.htmlFor = key;
|
||||
label.textContent = key;
|
||||
|
||||
// 입력 필드 생성
|
||||
const input = document.createElement("input");
|
||||
input.className = "input-value";
|
||||
input.type = "text";
|
||||
input.id = key;
|
||||
input.name = key;
|
||||
|
||||
// 요소 추가
|
||||
formGroup.appendChild(label);
|
||||
formGroup.appendChild(input);
|
||||
|
||||
return formGroup;
|
||||
}
|
||||
|
||||
export const createCsvTableView_ = (csvTable: CSVTable): HTMLElement => {
|
||||
const headers = csvTable.getHeaders();
|
||||
const rows = csvTable.getRows();
|
||||
|
||||
// 테이블 래퍼 생성
|
||||
const tableWrapper = document.createElement("div");
|
||||
tableWrapper.className = "table-wrapper";
|
||||
|
||||
// 테이블 생성
|
||||
const table = document.createElement("table");
|
||||
|
||||
// 테이블 헤더 생성
|
||||
const thead = document.createElement("thead");
|
||||
const headerRow = document.createElement("tr");
|
||||
headers.forEach(header => {
|
||||
const th = document.createElement("th");
|
||||
th.textContent = header[0];
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
thead.appendChild(headerRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
// 테이블 바디 생성
|
||||
const tbody = document.createElement("tbody");
|
||||
rows.forEach(row => {
|
||||
const tableRow = document.createElement("tr");
|
||||
row.forEach(cell => {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = cell?.toString() ?? "";
|
||||
tableRow.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tableRow);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
|
||||
tableWrapper.appendChild(table);
|
||||
|
||||
return tableWrapper;
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import { App, Modal } from "obsidian";
|
||||
import { CSVRow, CSVTable } from "./types";
|
||||
import { readCSV_, saveCSV_ } from "./csvfilemanager";
|
||||
import './styles/modal.css';
|
||||
|
||||
export const createCsvInputModal_ = async (app: App, headers: string[], fileName: string) => {
|
||||
const form = generateForm(headers);
|
||||
|
||||
const modal = new Modal(app);
|
||||
modal.contentEl.innerHTML = form;
|
||||
modal.open();
|
||||
|
||||
// 파일을 새로 읽어서, 추가된 라인을 포함 저장.
|
||||
const formElement = modal.contentEl.querySelector('#csv-input-form') as HTMLFormElement;
|
||||
formElement.addEventListener('submit', async (event) => {
|
||||
event.preventDefault(); // 기본 제출 동작 방지
|
||||
// 입력 값 처리 가져오기.
|
||||
const formData = new FormData(formElement);
|
||||
|
||||
const newRow: CSVRow = [];
|
||||
for(const header of headers) {
|
||||
newRow.push(formData.get(header) as string);
|
||||
}
|
||||
|
||||
// 파일 읽기
|
||||
const table = await readCSV_(app, fileName);
|
||||
if(table === null) {
|
||||
console.error("file not found");
|
||||
return;
|
||||
}
|
||||
// 파일 저장
|
||||
table?.append(newRow);
|
||||
// console.log(table);
|
||||
saveCSV_(app, fileName, table);
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
|
||||
const generateForm = (headers: string[]): string => {
|
||||
// 입력 필드를 HTML로 생성
|
||||
const fields = headers.map(header => createInputField(header));
|
||||
// 폼 래퍼에 입력 필드를 삽입
|
||||
return `
|
||||
<form id="csv-input-form">
|
||||
${fields.join("\n")}
|
||||
<div class="button-group">
|
||||
<div></div>
|
||||
<button class="submit-button" type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
const createInputField = (key: string): string => {
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="input-key" for="${key}">${key}</label>
|
||||
<input class="input-value" type="text" id="${key}" name="${key}"/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export const createCsvTableView_ = (csvTable: CSVTable): string => {
|
||||
const headers = csvTable.getHeaders();
|
||||
const rows = csvTable.getRows();
|
||||
|
||||
const table = rows.map(row => {
|
||||
return `
|
||||
<tr>
|
||||
${row.map(cell => `<td>${cell}</td>`).join("")}
|
||||
</tr>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
return `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
${headers.map(header => `<th>${header}</th>`).join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { CSVTable } from './types'
|
||||
import { App, TFile } from 'obsidian';
|
||||
|
||||
export const readCSV_ = async (app: App, fileName: string): Promise<CSVTable | null> => {
|
||||
if(fileName.endsWith(".csv")) { // 확장자 검사.
|
||||
const content = await loadFile(app, fileName); // 파일 로드.
|
||||
return await new CSVTable(content) // CSV 파싱.
|
||||
} else {
|
||||
console.error(`file extension is not csv : ${fileName} (read)`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export const saveCSV_ = async (app: App, fileName: string, table: CSVTable): Promise<void> => {
|
||||
if(fileName.endsWith(".csv")) {
|
||||
const content = table.toCSV();
|
||||
await saveFile(app, fileName, content);
|
||||
} else {
|
||||
console.error(`file extension is not csv : ${fileName} (save)`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// save, load
|
||||
const saveFile = async (app: App, fileName: string, content: string): Promise<void> => {
|
||||
const vault = app.vault;
|
||||
|
||||
const existingFile = vault.getAbstractFileByPath(fileName);
|
||||
if (existingFile instanceof TFile) {
|
||||
// 파일이 존재하면 덮어쓰기
|
||||
// console.log(`file exists, modify : ${fileName}`);
|
||||
await vault.modify(existingFile, content);
|
||||
} else {
|
||||
// 파일이 존재하지 않으면 새로 생성
|
||||
// console.log(`file not exists, create : ${fileName}`);
|
||||
await vault.create(fileName, content);
|
||||
}
|
||||
}
|
||||
const loadFile = async (app: App, fileName: string): Promise<string> => {
|
||||
const vault = app.vault;
|
||||
const file = vault.getAbstractFileByPath(fileName);
|
||||
if (file instanceof TFile) {
|
||||
return await vault.read(file);
|
||||
} else {
|
||||
throw new Error('file not found');
|
||||
}
|
||||
}
|
||||
|
||||
const parseCSV = (content: string): CSVTable => {
|
||||
const lines = content.split("\n").map(line => line.trim());
|
||||
const headers = lines.shift()?.split(",") ?? [];
|
||||
const rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
|
||||
return new CSVTable(headers, rows);
|
||||
}
|
||||
75
src/styles/csvCreator.css
Normal file
75
src/styles/csvCreator.css
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
.filename-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filename-input {
|
||||
width: 60%;
|
||||
flex-grow : 2;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
.filepath-input {
|
||||
width: 30%;
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.columns-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.columns-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.columns-wrapper > :nth-child(2n) {
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.columns-table-header {
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.colname-input {
|
||||
padding: 2px;
|
||||
width: 100%; height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.coltype-select {
|
||||
padding: 2px;
|
||||
width: 100%; height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +18,10 @@
|
|||
padding: 1rem;
|
||||
}
|
||||
|
||||
.input-type {
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.input-key {
|
||||
text-align: left;
|
||||
margin-right: 0.5rem;
|
||||
50
src/types.ts
50
src/types.ts
|
|
@ -1,24 +1,29 @@
|
|||
export type CSVCell = string | number | null;
|
||||
export type CSVRow = CSVCell[];
|
||||
export type Header = [string, string]; // [columnName, columnType]
|
||||
export class CSVTable {
|
||||
private headers: string[];
|
||||
private headers: Header[];
|
||||
private rows: CSVRow[];
|
||||
|
||||
// string으로 넣거나, 분리된 headers/rows로 생성자 호출 가능.
|
||||
constructor(contentOrHeaders: string | string[], rows: CSVRow[] = []) {
|
||||
if (typeof contentOrHeaders === 'string') {
|
||||
// content가 문자열일 때
|
||||
const lines = contentOrHeaders.split("\n").map(line => line.trim());
|
||||
this.headers = lines.shift()?.split(",") ?? [];
|
||||
this.rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
} else {
|
||||
// headers가 배열일 때
|
||||
this.headers = contentOrHeaders;
|
||||
this.rows = rows;
|
||||
}
|
||||
constructor(headers: Header[], rows: CSVRow[] = []) {
|
||||
this.headers = headers;
|
||||
this.rows = rows;
|
||||
}
|
||||
// constructor(contentOrHeaders: string | string[], rows: CSVRow[] = []) {
|
||||
// if (typeof contentOrHeaders === 'string') {
|
||||
// // content가 문자열일 때
|
||||
// const lines = contentOrHeaders.split("\n").map(line => line.trim());
|
||||
// this.headers = lines.shift()?.split(",") ?? [];
|
||||
// this.rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
// } else {
|
||||
// // headers가 배열일 때
|
||||
// this.headers = contentOrHeaders;
|
||||
// this.rows = rows;
|
||||
// }
|
||||
// }
|
||||
|
||||
getHeaders(): string[] {
|
||||
getHeaders(): Header[] {
|
||||
return this.headers;
|
||||
}
|
||||
getRows(): CSVRow[] {
|
||||
|
|
@ -35,8 +40,9 @@ export class CSVTable {
|
|||
this.rows.push(row);
|
||||
}
|
||||
else {
|
||||
// key-value로 들어올 때
|
||||
const newRow: CSVRow = [];
|
||||
for(const header of this.headers) { newRow.push(row[header]); }
|
||||
for(const header of this.headers) { newRow.push(row[header[0]]); }
|
||||
this.rows.push(newRow);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +54,7 @@ export class CSVTable {
|
|||
this.rows.splice(rowIndex, 1);
|
||||
}
|
||||
updateCell(rowIndex: number, columnName: string, value: CSVCell): void {
|
||||
const columnIndex = this.headers.indexOf(columnName);
|
||||
const columnIndex = this.headers.findIndex(([colname]) => colname === columnName);
|
||||
if (columnIndex === -1) {
|
||||
throw new Error(`Column '${columnName}' does not exist.`);
|
||||
}
|
||||
|
|
@ -67,14 +73,14 @@ export class CSVTable {
|
|||
}
|
||||
|
||||
// 실제로 사용하게될 api일지는 모르겠습니다.
|
||||
addColoumn(columnName: string, value: CSVCell = 0): void {
|
||||
this.headers.push(columnName);
|
||||
addColoumn(columnName: string, columnType: string, value: CSVCell = 0): void {
|
||||
this.headers.push([columnName, columnType]);
|
||||
for (let i = 0; i < this.rows.length; i++) {
|
||||
this.rows[i].push(value);
|
||||
}
|
||||
}
|
||||
deleteColumn(columnName: string): void {
|
||||
const columnIndex = this.headers.indexOf(columnName);
|
||||
const columnIndex = this.headers.findIndex(([colname]) => colname === columnName);
|
||||
if (columnIndex === -1) {
|
||||
throw new Error(`Column '${columnName}' does not exist.`);
|
||||
}
|
||||
|
|
@ -88,4 +94,10 @@ export interface CSVFile {
|
|||
name: string;
|
||||
path: string;
|
||||
content: CSVTable;
|
||||
}
|
||||
}
|
||||
|
||||
export const columnTypes: string[] = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
];
|
||||
Loading…
Reference in a new issue