pahkey_obsidian-wikidocs-pl.../lib/md.ts

575 lines
18 KiB
TypeScript

import { App, TFile, TFolder } from "obsidian";
import {
ensureFolderExists,
extractTitleFromFilePath,
getFileModifiedTime,
readTopLevelMetadata,
sanitizeFileName,
toObsidianSafeTitle,
} from "./utils";
export class PageMetadata {
id: number;
subject: string;
book_id?: number;
parent_id?: number;
open_yn?: string;
last_synced?: string;
constructor(data: {
id: number;
subject: string;
last_synced?: string;
book_id?: number;
parent_id?: number;
open_yn?: string;
}) {
this.id = data.id;
this.subject = data.subject;
this.last_synced = data.last_synced;
this.book_id = data.book_id;
this.parent_id = data.parent_id;
this.open_yn = data.open_yn;
if (this.subject.includes("#")) {
this.subject = `"${this.subject}"`; // 따옴표로 감싸기
}
}
// MetadataCache에서 제공된 frontmatter 객체를 처리
static fromFrontMatter(frontMatter: Record<string, any>): PageMetadata {
if (!frontMatter.id || !frontMatter.subject) {
throw new Error("Front Matter must contain 'id' and 'subject'.");
}
return new PageMetadata({
id: frontMatter.id,
subject: frontMatter.subject,
last_synced: frontMatter.last_synced,
book_id: frontMatter.book_id,
parent_id: frontMatter.parent_id,
open_yn: frontMatter.open_yn,
});
}
// 필요한 추가 메서드
isLocked(): boolean {
return this.open_yn === "N";
}
updateLastSynced(): void {
this.last_synced = new Date().toISOString();
}
getFrontMatter(): string {
const frontMatter = `---\n` +
`id: ${this.id}\n` +
`subject: ${this.subject}\n` +
`book_id: ${this.book_id ?? -1}\n` +
`parent_id: ${this.parent_id ?? -1}\n` +
`open_yn: ${this.open_yn}\n` +
`last_synced: ${this.last_synced}\n` +
`---\n`;
return frontMatter;
}
}
export interface PageSyncStatus {
needsSync: boolean;
reasons: string[];
lastSynced: Date | null;
fileModifiedAt: Date;
fileSubject: string;
subjectMatchesFilename: boolean;
}
export async function saveBookMetadata(folderPath: string, bookId: number, bookTitle: string) {
const metadataPath = `${folderPath}/metadata.md`;
const metadataContent = `---\n` +
`id: ${bookId}\n` +
`title: ${bookTitle}\n` +
`---\n`;
const existingFile = this.app.vault.getAbstractFileByPath(metadataPath);
if (existingFile instanceof TFile) {
// 기존 파일 업데이트
await this.app.vault.modify(existingFile, metadataContent);
} else if (!existingFile) {
// 새 파일 생성
await this.app.vault.create(metadataPath, metadataContent);
} else {
// 예상치 못한 타입의 파일 처리
console.error(`The path "${metadataPath}" exists but is not a valid file.`);
}
}
async function getParentId(file: TFile) {
// 1. 부모 폴더 가져오기
const parentFolder = file.parent;
if (!(parentFolder instanceof TFolder)) {
// console.warn(`The file "${file.path}" does not have a parent folder.`);
return -1;
}
const grandParentFolder = parentFolder.parent;
if (!(grandParentFolder instanceof TFolder)) {
// console.warn(`The parent folder "${parentFolder.path}" does not have a parent folder.`);
return -1;
}
// 2. 부모 폴더와 동일한 이름의 .md 파일 경로 찾기
const parentFolderName = parentFolder.name;
const potentialFilePath = `${grandParentFolder.path}/${parentFolderName}.md`;
const abstractFile = file.vault.getAbstractFileByPath(potentialFilePath);
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "md") {
// console.warn(`No matching .md file found for parent folder "${parentFolderName}".`);
return -1;
}
// 3. MetadataCache를 사용하여 FrontMatter에서 id 가져오기
const fileCache = this.app.metadataCache.getFileCache(abstractFile);
if (!fileCache || !fileCache.frontmatter) {
// console.warn(`No FrontMatter found in the file: ${potentialFilePath}`);
return -1;
}
const frontMatter = fileCache.frontmatter;
if (!frontMatter.id || typeof frontMatter.id !== "number") {
// console.warn(`No 'id' field found in FrontMatter of the file: ${potentialFilePath}`);
return -1;
}
return frontMatter.id;
}
export function getPureContent(content: string): string {
// Front Matter 감지 (---로 시작하고 끝나는 블록)
const frontMatterMatch = content.match(/^---[\s\S]*?---\n/);
if (frontMatterMatch) {
// Front Matter 제외하고 순수 콘텐츠 반환
return content.slice(frontMatterMatch[0].length).trim();
}
// Front Matter가 없으면 원본 콘텐츠 반환
return content.trim();
}
export async function addFrontMatterToFile(file: TFile) {
let bookId = -1; // 기본값
const metadata = await readTopLevelMetadata(file);
if (metadata && metadata.id) {
bookId = typeof metadata.id === "string" ? parseInt(metadata.id, 10) : metadata.id as number;
} else {
// console.warn("No ID found in metadata.md.");
}
const parentId = await getParentId(file);
// processFrontMatter를 사용하여 파일의 Front Matter 수정 또는 추가
await this.app.fileManager.processFrontMatter(file, (frontMatter: Record<string, unknown>) => {
if (!frontMatter) {
// Front Matter가 없는 경우 새로 추가
frontMatter = {};
}
// 필요한 값 추가 또는 업데이트
frontMatter["id"] = frontMatter["id"] || -1;
frontMatter["subject"] = frontMatter["subject"] || toObsidianSafeTitle(file.basename);
frontMatter["last_synced"] = ''; // 동기화를 위해 비워둔다.
frontMatter["book_id"] = bookId;
frontMatter["parent_id"] = parentId;
});
}
export async function getBookIdFromMetadata(folderPath: string): Promise<number | null> {
const metadataPath = `${folderPath}/metadata.md`;
const file = this.app.vault.getAbstractFileByPath(metadataPath);
if (file instanceof TFile) {
const fileCache = this.app.metadataCache.getFileCache(file);
// Check if the file has frontmatter
if (fileCache?.frontmatter && fileCache.frontmatter.id) {
return parseInt(fileCache.frontmatter.id, 10);
}
}
return null;
}
export async function extractMetadataFromFrontMatter(file: TFile):
Promise<PageMetadata> {
// Access the metadata cache
const fileCache = this.app.metadataCache.getFileCache(file);
// Check if frontMatter exists in the cache
if (fileCache?.frontmatter) {
return PageMetadata.fromFrontMatter(fileCache.frontmatter);
} else {
throw new Error(`No Front Matter found in file: ${file.path}`);
}
}
export async function tryExtractMetadataFromFrontMatter(file: TFile):
Promise<PageMetadata | null> {
const fileCache = this.app.metadataCache.getFileCache(file);
const frontMatter = fileCache?.frontmatter;
if (!frontMatter) {
return null;
}
if (frontMatter.id == null || frontMatter.subject == null) {
return null;
}
return PageMetadata.fromFrontMatter(frontMatter);
}
export function evaluatePageSyncStatus(file: TFile, metadata: PageMetadata): PageSyncStatus {
const reasons: string[] = [];
const lastSynced = metadata.last_synced ? new Date(metadata.last_synced) : null;
const fileModifiedAt = getFileModifiedTime(file);
const fileSubject = extractTitleFromFilePath(file.path);
const subjectMatchesFilename =
sanitizeFileName(metadata.subject) === sanitizeFileName(fileSubject);
if (!lastSynced) {
reasons.push("missing_last_synced");
}
if (lastSynced && fileModifiedAt.getTime() - lastSynced.getTime() > 1000) {
reasons.push("mtime_newer_than_last_synced");
}
if (!subjectMatchesFilename) {
reasons.push("subject_filename_mismatch");
}
return {
needsSync: reasons.length > 0,
reasons,
lastSynced,
fileModifiedAt,
fileSubject,
subjectMatchesFilename,
};
}
export async function updatePageFrontMatterAfterSync(
file: TFile,
metadata: PageMetadata,
syncedAt: string
) {
metadata.subject = toObsidianSafeTitle(extractTitleFromFilePath(file.path));
metadata.last_synced = syncedAt;
await this.app.fileManager.processFrontMatter(file, (frontMatter: Record<string, unknown>) => {
if (!frontMatter) {
frontMatter = {};
}
frontMatter["id"] = metadata.id;
frontMatter["subject"] = metadata.subject;
frontMatter["book_id"] = metadata.book_id ?? -1;
frontMatter["parent_id"] = metadata.parent_id ?? -1;
frontMatter["open_yn"] = metadata.open_yn;
frontMatter["last_synced"] = metadata.last_synced;
});
}
export async function savePagesToMarkdown(app:App, pages: any[], folderPath: string) {
for (const page of pages) {
const sanitizedFileName = toObsidianSafeTitle(page.subject);
const filePath = `${folderPath}/${sanitizedFileName}.md`;
try {
const now = new Date().toISOString();
// Front Matter 생성
const metadata = new PageMetadata(page);
metadata.subject = sanitizedFileName;
metadata.last_synced = now;
const frontMatter = metadata.getFrontMatter();
// 페이지 내용 추가
const content = frontMatter + (page.content ?? "No content available.");
// 파일 생성
await this.app.vault.create(filePath, content);
if (page.open_yn === "N") {
// addLockIcon(filePath);
// await addLockIcon(app, filePath);
}
// 하위 페이지 처리
if (page.children && page.children.length > 0) {
const childFolderPath = `${folderPath}/${sanitizedFileName}`;
await ensureFolderExists(childFolderPath);
await savePagesToMarkdown(app, page.children, childFolderPath);
}
} catch (error) {
console.error(`Failed to save page: ${page.subject}`, error);
}
}
}
export async function isNeedSync(app:App, folder:TFolder) {
const files = this.app.vault.getFiles().filter((file: { path: string; }) => file.path.startsWith(folder.path));
let changedCount = 0;
for (const file of files) {
if (file.name === "metadata.md") {
continue;
}
if (file.extension !== "md") {
continue;
}
const metadata = await tryExtractMetadataFromFrontMatter(file);
if (!metadata) {
continue;
}
if (!metadata.id) {
console.error(`No ID found in Front Matter for file: ${file.path}`);
continue;
}
const syncStatus = evaluatePageSyncStatus(file, metadata);
if (syncStatus.needsSync) {
changedCount++;
}
}
if(changedCount > 0) {
return true;
}else {
return false;
}
}
export async function addLockIconToFile(file: TFile) {
// 메타데이터 읽기
const metadata = this.app.metadataCache.getFileCache(file);
if (metadata?.frontmatter?.open_yn === "N") {
// 파일 탐색기에서 해당 파일에 자물쇠 아이콘 추가
const explorerLeaf = document.querySelector(
`.nav-file-title[data-path="${file.path}"]`
);
if (explorerLeaf) {
// 이미 아이콘이 추가된 경우 중복 추가 방지
const existingIcon = explorerLeaf.querySelector(".lock-icon");
if (!existingIcon) {
const lockIcon = document.createElement("span");
lockIcon.className = "lock-icon";
explorerLeaf.appendChild(lockIcon);
}
}
} else {
// open_yn이 "Y" 또는 없는 경우 아이콘 제거
const explorerLeaf = document.querySelector(
`.nav-file-title[data-path="${file.path}"]`
);
if (explorerLeaf) {
const existingIcon = explorerLeaf.querySelector(".lock-icon");
if (existingIcon) {
existingIcon.remove();
}
}
}
}
export async function addBlogIconToFolder(folderPath:any) {
/*
setTimeout(() => {
const explorerLeaf = document.querySelector(
`.nav-folder-title[data-path="${folderPath}"]`
);
if (explorerLeaf) {
// 이미 아이콘이 추가된 경우 중복 추가 방지
const existingIcon = explorerLeaf.querySelector(".blog-icon");
if (!existingIcon) {
const blogIcon = document.createElement("span");
blogIcon.className = "blog-icon";
blogIcon.textContent = "📝"; // 블로그 아이콘 (이모지 또는 아이콘 라이브러리 활용 가능)
blogIcon.style.marginRight = "5px"; // 아이콘과 글씨 사이 여백 추가
explorerLeaf.prepend(blogIcon); // 폴더명 앞에 아이콘 추가
}
}
}, 500); // 500ms 딜레이 추가
*/
}
export async function addBlogIconToFile(folderPath:any, blog:any) {
/*
setTimeout(() => {
console.log(`folderPath:${folderPath}, is_public:${blog.is_public}`);
const explorerLeaf = document.querySelector(
`.nav-file-title[data-path="${folderPath}"]`
);
if (explorerLeaf) {
// 이미 아이콘이 추가된 경우 중복 추가 방지
const existingIcon = explorerLeaf.querySelector(".blog-icon");
if (!existingIcon) {
const blogIcon = document.createElement("span");
blogIcon.className = "blog-icon";
if (blog.is_public) {
blogIcon.textContent = "🌐"; // 블로그 아이콘 (이모지 또는 아이콘 라이브러리 활용 가능)
}else {
blogIcon.textContent = "🔒"; // 블로그 아이콘 (이모지 또는 아이콘 라이브러리 활용 가능)
}
blogIcon.style.marginRight = "5px"; // 아이콘과 글씨 사이 여백 추가
explorerLeaf.prepend(blogIcon); // 폴더명 앞에 아이콘 추가
}else {
// 아이콘이 이미 있는 경우 상태에 맞게 업데이트
if ((existingIcon.textContent === "🌐" && !blog.is_public) ||
(existingIcon.textContent === "🔒" && blog.is_public)) {
// 현재 상태와 다른 경우에만 아이콘 업데이트
existingIcon.textContent = blog.is_public ? "🌐" : "🔒";
}
}
}
}, 500); // 500ms 딜레이 추가
*/
}
// blog
export class BlogMetadata {
id: number;
tags: any;
is_public: boolean;
last_synced?: string;
constructor(data: {
id: number;
tags: string;
is_public: boolean;
last_synced?: string;
}) {
this.id = data.id;
// console.log(data.tags)
if(data.tags) {
this.tags = data.tags;
}else {
this.tags = [];
}
// this.tags = data.tags;
this.is_public = data.is_public;
this.last_synced = data.last_synced;
}
// MetadataCache에서 제공된 frontmatter 객체를 처리
static fromFrontMatter(frontMatter: Record<string, any>): BlogMetadata {
if (!frontMatter.id) {
throw new Error("Front Matter must contain 'id'.");
}
return new BlogMetadata({
id: frontMatter.id,
tags: frontMatter.tags,
is_public: frontMatter.is_public,
last_synced: frontMatter.last_synced,
});
}
getFrontMatter(): string {
const frontMatter = `---\n` +
`id: ${this.id}\n` +
`tags: ${this.tags}\n` +
`is_public: ${this.is_public}\n` +
`last_synced: ${this.last_synced}\n` +
`---\n`;
return frontMatter;
}
}
export async function saveBlogToMarkdown(app:App, blog: any, folderPath: string) {
const sanitizedFileName = sanitizeFileName(blog.title);
const filePath = `${folderPath}/${sanitizedFileName}.md`;
try {
const now = new Date().toISOString();
// Front Matter 생성
const metadata = new BlogMetadata(blog);
metadata.last_synced = now;
const frontMatter = metadata.getFrontMatter();
// 페이지 내용 추가
const content = frontMatter + (blog.content ?? "No content available.");
// 파일 생성
let file = this.app.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
await this.app.vault.modify(file, content);
}else {
file = await this.app.vault.create(filePath, content);
}
await addBlogIconToFile(filePath, blog);
// 파일열기
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
} catch (error) {
console.error(`Failed to save blog: ${blog.title}`, error);
}
}
export async function addBlogFrontMatterToFile(file: TFile) {
const now = new Date().toISOString();
// processFrontMatter를 사용하여 파일의 Front Matter 수정 또는 추가
await this.app.fileManager.processFrontMatter(file, (frontMatter: Record<string, unknown>) => {
if (!frontMatter) {
// Front Matter가 없는 경우 새로 추가
frontMatter = {};
}
// 필요한 값 추가 또는 업데이트
frontMatter["id"] = frontMatter["id"] || -1;
frontMatter["tags"] = '';
frontMatter["is_public"] = true;
frontMatter["last_synced"] = ''; // 동기화를 위해 비워둔다.
});
}
export async function extractMetadataFromBlogFrontMatter(file: TFile):
Promise<BlogMetadata> {
// Access the metadata cache
const fileCache = this.app.metadataCache.getFileCache(file);
// Check if frontMatter exists in the cache
if (fileCache?.frontmatter) {
return BlogMetadata.fromFrontMatter(fileCache.frontmatter);
} else {
throw new Error(`No Front Matter found in file: ${file.path}`);
}
}