重构插件架构,解耦主要功能模块

- 将主类拆分为多个专注的模块,如 DiscourseAPI、EmbedHandler
- 重命名主类为 PublishToDiscourse,提高语义清晰度
- 抽取通用工具函数到独立模块
- 优化代码结构,提高可维护性和可读性
- 调整 Front Matter 处理和嵌入内容处理逻辑
This commit is contained in:
wood chen 2025-03-10 19:33:50 +08:00
parent 310be30030
commit cc1e06ec5c
9 changed files with 999 additions and 811 deletions

390
src/api.ts Normal file
View File

@ -0,0 +1,390 @@
import { App, requestUrl, TFile } from 'obsidian';
import { DiscourseSyncSettings } from './config';
import { NotifyUser } from './notification';
import { t } from './i18n';
// 生成随机边界字符串
const genBoundary = (): string => {
return 'ObsidianBoundary' + Math.random().toString(16).substring(2);
};
export class DiscourseAPI {
constructor(
private app: App,
private settings: DiscourseSyncSettings
) {}
// 上传图片到Discourse
async uploadImage(file: TFile): Promise<string | null> {
try {
const imgfile = await this.app.vault.readBinary(file);
const boundary = genBoundary();
const sBoundary = '--' + boundary + '\r\n';
const imgForm = `${sBoundary}Content-Disposition: form-data; name="file"; filename="${file.name}"\r\nContent-Type: image/${file.extension}\r\n\r\n`;
let body = '';
body += `\r\n${sBoundary}Content-Disposition: form-data; name="type"\r\n\r\ncomposer\r\n`;
body += `${sBoundary}Content-Disposition: form-data; name="synchronous"\r\n\r\ntrue\r\n`;
const eBoundary = '\r\n--' + boundary + '--\r\n';
const imgFormArray = new TextEncoder().encode(imgForm);
const bodyArray = new TextEncoder().encode(body);
const endBoundaryArray = new TextEncoder().encode(eBoundary);
const formDataArray = new Uint8Array(imgFormArray.length + imgfile.byteLength + bodyArray.length + endBoundaryArray.length);
formDataArray.set(imgFormArray, 0);
formDataArray.set(new Uint8Array(imgfile), imgFormArray.length);
formDataArray.set(bodyArray, imgFormArray.length + imgfile.byteLength);
formDataArray.set(endBoundaryArray, imgFormArray.length + bodyArray.length + imgfile.byteLength);
const url = `${this.settings.baseUrl}/uploads.json`;
const headers = {
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
"Content-Type": `multipart/form-data; boundary=${boundary}`,
};
const response = await requestUrl({
url: url,
method: "POST",
body: formDataArray.buffer,
throw: false,
headers: headers,
});
if (response.status == 200) {
const jsonResponse = response.json;
return jsonResponse.short_url;
} else {
new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
return null;
}
} catch (error) {
new NotifyUser(this.app, `Exception while uploading image: ${error}`).open();
return null;
}
}
// 创建新帖子
async createPost(title: string, content: string, category: number, tags: string[]): Promise<{ success: boolean; postId?: number; topicId?: number; error?: string }> {
const url = `${this.settings.baseUrl}/posts.json`;
const headers = {
"Content-Type": "application/json",
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
};
try {
const response = await requestUrl({
url,
method: "POST",
contentType: "application/json",
body: JSON.stringify({
title: title,
raw: content,
category: category,
tags: tags || []
}),
headers,
throw: false
});
if (response.status === 200) {
const responseData = response.json;
if (responseData && responseData.id) {
return {
success: true,
postId: responseData.id,
topicId: responseData.topic_id
};
} else {
return {
success: false,
error: t('POST_ID_ERROR')
};
}
} else {
try {
const errorResponse = response.json;
if (errorResponse.errors && errorResponse.errors.length > 0) {
return {
success: false,
error: errorResponse.errors.join('\n')
};
}
if (errorResponse.error) {
return {
success: false,
error: errorResponse.error
};
}
} catch (parseError) {
return {
success: false,
error: `${t('PUBLISH_FAILED')} (${response.status})`
};
}
return {
success: false,
error: `${t('PUBLISH_FAILED')} (${response.status})`
};
}
} catch (error) {
return {
success: false,
error: `${t('PUBLISH_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
};
}
}
// 更新帖子
async updatePost(postId: number, topicId: number, title: string, content: string, category: number, tags: string[]): Promise<{ success: boolean; error?: string }> {
const postEndpoint = `${this.settings.baseUrl}/posts/${postId}`;
const topicEndpoint = `${this.settings.baseUrl}/t/${topicId}`;
const headers = {
"Content-Type": "application/json",
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
};
try {
// 首先更新帖子内容
const postResponse = await requestUrl({
url: postEndpoint,
method: "PUT",
contentType: "application/json",
body: JSON.stringify({
raw: content,
edit_reason: "Updated from Obsidian"
}),
headers,
throw: false
});
if (postResponse.status !== 200) {
return {
success: false,
error: `${t('UPDATE_FAILED')} (${postResponse.status})`
};
}
// 然后更新主题(标题、分类和标签)
const topicResponse = await requestUrl({
url: topicEndpoint,
method: "PUT",
contentType: "application/json",
body: JSON.stringify({
title: title,
category_id: category,
tags: tags || []
}),
headers,
throw: false
});
if (topicResponse.status === 200) {
return { success: true };
} else {
try {
const errorResponse = topicResponse.json;
if (errorResponse.errors && errorResponse.errors.length > 0) {
return {
success: false,
error: errorResponse.errors.join('\n')
};
}
if (errorResponse.error) {
return {
success: false,
error: errorResponse.error
};
}
} catch (parseError) {
return {
success: false,
error: `${t('UPDATE_FAILED')} (${topicResponse.status})`
};
}
return {
success: false,
error: `${t('UPDATE_FAILED')} (${topicResponse.status})`
};
}
} catch (error) {
return {
success: false,
error: `${t('UPDATE_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
};
}
}
// 获取分类列表
async fetchCategories(): Promise<{ id: number; name: string }[]> {
try {
const url = `${this.settings.baseUrl}/categories.json`;
const headers = {
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
};
const response = await requestUrl({
url,
method: "GET",
headers,
throw: false
});
if (response.status === 200) {
const data = response.json;
const categories: { id: number; name: string }[] = [];
if (data && data.category_list && data.category_list.categories) {
data.category_list.categories.forEach((category: any) => {
categories.push({
id: category.id,
name: category.name
});
// 添加子分类
if (category.subcategory_list) {
category.subcategory_list.forEach((subcategory: any) => {
categories.push({
id: subcategory.id,
name: `${category.name} > ${subcategory.name}`
});
});
}
});
}
return categories;
} else {
new NotifyUser(this.app, `Error fetching categories: ${response.status}`).open();
return [];
}
} catch (error) {
new NotifyUser(this.app, `Exception while fetching categories: ${error}`).open();
return [];
}
}
// 获取标签列表
async fetchTags(): Promise<{ name: string; canCreate: boolean }[]> {
try {
const url = `${this.settings.baseUrl}/tags.json`;
const headers = {
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
};
const response = await requestUrl({
url,
method: "GET",
headers,
throw: false
});
if (response.status === 200) {
const data = response.json;
const tags: { name: string; canCreate: boolean }[] = [];
if (data && data.tags) {
const canCreateTags = await this.checkCanCreateTags();
data.tags.forEach((tag: any) => {
tags.push({
name: tag.name,
canCreate: canCreateTags
});
});
}
return tags;
} else {
new NotifyUser(this.app, `Error fetching tags: ${response.status}`).open();
return [];
}
} catch (error) {
new NotifyUser(this.app, `Exception while fetching tags: ${error}`).open();
return [];
}
}
// 检查是否可以创建标签
async checkCanCreateTags(): Promise<boolean> {
try {
const url = `${this.settings.baseUrl}/site.json`;
const headers = {
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
};
const response = await requestUrl({
url,
method: "GET",
headers,
throw: false
});
if (response.status === 200) {
const data = response.json;
if (data && data.can_create_tag) {
return data.can_create_tag;
}
}
return false;
} catch (error) {
return false;
}
}
// 测试API密钥
async testApiKey(): Promise<{ success: boolean; message: string }> {
if (!this.settings.baseUrl || !this.settings.apiKey || !this.settings.disUser) {
return {
success: false,
message: t('MISSING_SETTINGS')
};
}
try {
const url = `${this.settings.baseUrl}/users/${this.settings.disUser}.json`;
const headers = {
"Api-Key": this.settings.apiKey,
"Api-Username": this.settings.disUser,
};
const response = await requestUrl({
url,
method: "GET",
headers,
throw: false
});
if (response.status === 200) {
const data = response.json;
if (data && data.user) {
return {
success: true,
message: t('API_TEST_SUCCESS')
};
} else {
return {
success: false,
message: t('API_KEY_INVALID')
};
}
} else {
return {
success: false,
message: `${t('API_KEY_INVALID')} (${response.status})`
};
}
} catch (error) {
return {
success: false,
message: `${t('API_KEY_INVALID')}: ${error.message || t('UNKNOWN_ERROR')}`
};
}
}
}

View File

@ -1,5 +1,5 @@
import { PluginSettingTab, Setting, App, Notice, ButtonComponent } from 'obsidian';
import DiscourseSyncPlugin from './main';
import PublishToDiscourse from './main';
import { t } from './i18n';
export interface DiscourseSyncSettings {
@ -19,8 +19,8 @@ export const DEFAULT_SETTINGS: DiscourseSyncSettings = {
};
export class DiscourseSyncSettingsTab extends PluginSettingTab {
plugin: DiscourseSyncPlugin;
constructor(app: App, plugin: DiscourseSyncPlugin) {
plugin: PublishToDiscourse;
constructor(app: App, plugin: PublishToDiscourse) {
super(app, plugin);
}
@ -78,7 +78,7 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
button.setButtonText(t('TESTING'));
button.setDisabled(true);
const result = await this.plugin.testApiKey();
const result = await this.plugin.api.testApiKey();
button.setButtonText(t('TEST_API_KEY'));
button.setDisabled(false);

77
src/embed-handler.ts Normal file
View File

@ -0,0 +1,77 @@
import { App, TFile } from 'obsidian';
import { expandEmbeds } from './expand-embeds';
import { NotifyUser } from './notification';
import { DiscourseAPI } from './api';
import { isImageFile } from './utils';
export class EmbedHandler {
constructor(
private app: App,
private api: DiscourseAPI
) {}
// 提取嵌入引用
extractEmbedReferences(content: string): string[] {
const regex = /!\[\[(.*?)\]\]/g;
const matches = [];
let match;
while ((match = regex.exec(content)) !== null) {
matches.push(match[1]);
}
return matches;
}
// 处理嵌入内容
async processEmbeds(embedReferences: string[], activeFileName: string): Promise<string[]> {
const uploadedUrls: string[] = [];
for (const ref of embedReferences) {
// 处理带有#的文件路径,分离文件名和标题部分
let filePart = ref;
const hashIndex = filePart.indexOf("#");
if (hashIndex >= 0) {
filePart = filePart.substring(0, hashIndex).trim();
}
const filePath = this.app.metadataCache.getFirstLinkpathDest(filePart, activeFileName)?.path;
if (filePath) {
const abstractFile = this.app.vault.getAbstractFileByPath(filePath);
if (abstractFile instanceof TFile) {
// 检查是否为图片或PDF文件
if (isImageFile(abstractFile)) {
const imageUrl = await this.api.uploadImage(abstractFile);
uploadedUrls.push(imageUrl || "");
} else {
// 非图片文件,返回空字符串
uploadedUrls.push("");
}
} else {
new NotifyUser(this.app, `File not found in vault: ${ref}`).open();
uploadedUrls.push("");
}
} else {
new NotifyUser(this.app, `Unable to resolve file path for: ${ref}`).open();
uploadedUrls.push("");
}
}
return uploadedUrls;
}
// 替换内容中的嵌入引用为Markdown格式
replaceEmbedReferences(content: string, embedReferences: string[], uploadedUrls: string[]): string {
let processedContent = content;
embedReferences.forEach((ref, index) => {
const obsRef = `![[${ref}]]`;
// 只有当上传URL不为空时即为图片才替换为Markdown格式的图片链接
if (uploadedUrls[index]) {
const discoRef = `![${ref}](${uploadedUrls[index]})`;
processedContent = processedContent.replace(obsRef, discoRef);
}
});
return processedContent;
}
// 处理文件内容,展开嵌入内容
async expandFileContent(file: TFile): Promise<string> {
return await expandEmbeds(this.app, file);
}
}

View File

@ -15,6 +15,27 @@
import { App, TFile, CachedMetadata } from "obsidian";
/**
* frontmatter转换为Markdown引用格式
*/
function convertFrontmatterToQuote(content: string): string {
// 检查是否有frontmatter
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
if (!fmMatch) return content;
// 提取frontmatter内容
const fmContent = fmMatch[1];
// 将frontmatter内容转换为引用格式
const quotedFm = fmContent
.split('\n')
.map(line => `> ${line}`)
.join('\n');
// 替换原始frontmatter
return content.replace(/^---\n[\s\S]*?\n---\n/, `${quotedFm}\n\n`);
}
/**
* Recursively expands embedded content (including subpath references),
* allowing the same (file+subpath) to appear multiple times if it's *not*
@ -64,7 +85,10 @@ export async function expandEmbeds(
}
// Recursively expand that subpath
return expandEmbeds(app, linkedTFile, stack, sub);
const expandedContent = await expandEmbeds(app, linkedTFile, stack, sub);
// 将嵌入内容中的frontmatter转换为引用格式
return convertFrontmatterToQuote(expandedContent);
});
// Pop it from stack
@ -101,9 +125,11 @@ function sliceSubpathContent(
const { start, end } = block.position;
if (!end) {
// Goes to EOF if no explicit end
return fileContent.substring(start.offset);
const slicedContent = fileContent.substring(start.offset);
return convertFrontmatterToQuote(slicedContent);
} else {
return fileContent.substring(start.offset, end.offset);
const slicedContent = fileContent.substring(start.offset, end.offset);
return convertFrontmatterToQuote(slicedContent);
}
}
@ -147,7 +173,8 @@ function sliceHeading(content: string, fileCache: CachedMetadata, headingName: s
}
console.log(`"Sliceheading for ${heading}, level ${thisLevel}, offsets ${startOffset} and ${endOffset}."`)
return content.substring(startOffset, endOffset).trim();
const slicedContent = content.substring(startOffset, endOffset).trim();
return convertFrontmatterToQuote(slicedContent);
}
/**

File diff suppressed because it is too large Load Diff

20
src/notification.ts Normal file
View File

@ -0,0 +1,20 @@
import { App, Modal } from 'obsidian';
// 通知用户的模态框
export class NotifyUser extends Modal {
message: string;
constructor(app: App, message: string) {
super(app);
this.message = message;
}
onOpen() {
const { contentEl } = this;
contentEl.setText(this.message);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

14
src/types.ts Normal file
View File

@ -0,0 +1,14 @@
import { DiscourseSyncSettings } from './config';
export interface ActiveFile {
name: string;
content: string;
postId?: number;
}
export interface PluginInterface {
settings: DiscourseSyncSettings;
activeFile: ActiveFile;
saveSettings(): Promise<void>;
publishTopic(): Promise<{ success: boolean; error?: string }>;
}

294
src/ui.ts Normal file
View File

@ -0,0 +1,294 @@
import { App, Modal } from 'obsidian';
import { t } from './i18n';
import { PluginInterface } from './types';
// 选择分类的模态框
export class SelectCategoryModal extends Modal {
plugin: PluginInterface;
categories: {id: number; name: string}[];
tags: { name: string; canCreate: boolean }[];
canCreateTags = false;
constructor(app: App, plugin: PluginInterface, categories: {id: number; name: string }[], tags: { name: string; canCreate: boolean }[]) {
super(app);
this.plugin = plugin;
this.categories = categories;
this.tags = tags;
this.canCreateTags = tags.length > 0 && tags[0].canCreate;
}
onOpen() {
// 添加模态框基础样式
this.modalEl.addClass('mod-discourse-sync');
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('discourse-sync-modal');
const isUpdate = this.plugin.activeFile.postId !== undefined;
contentEl.createEl('h1', { text: isUpdate ? t('UPDATE_POST') : t('PUBLISH_TO_DISCOURSE') });
// 创建表单区域容器
const formArea = contentEl.createEl('div', { cls: 'form-area' });
// 创建分类选择容器
const selectContainer = formArea.createEl('div', { cls: 'select-container' });
selectContainer.createEl('label', { text: t('CATEGORY') });
const selectEl = selectContainer.createEl('select');
// 添加分类选项
this.categories.forEach(category => {
const option = selectEl.createEl('option', { text: category.name });
option.value = category.id.toString();
});
// 设置默认选中的分类
selectEl.value = this.plugin.settings.category?.toString() || this.categories[0].id.toString();
// 监听分类选择变化
selectEl.onchange = () => {
this.plugin.settings.category = parseInt(selectEl.value);
this.plugin.saveSettings();
};
// 创建标签容器
const tagContainer = formArea.createEl('div', { cls: 'tag-container' });
tagContainer.createEl('label', { text: t('TAGS') });
// 创建标签选择区域
const tagSelectArea = tagContainer.createEl('div', { cls: 'tag-select-area' });
// 已选标签显示区域
const selectedTagsContainer = tagSelectArea.createEl('div', { cls: 'selected-tags' });
const selectedTags = new Set<string>();
// 初始化已选标签
if (this.plugin.settings.selectedTags && this.plugin.settings.selectedTags.length > 0) {
this.plugin.settings.selectedTags.forEach(tag => selectedTags.add(tag));
}
// 更新标签显示
const updateSelectedTags = () => {
selectedTagsContainer.empty();
selectedTags.forEach(tag => {
const tagEl = selectedTagsContainer.createEl('span', {
cls: 'tag',
text: tag
});
const removeBtn = tagEl.createEl('span', {
cls: 'remove-tag',
text: '×'
});
removeBtn.onclick = () => {
selectedTags.delete(tag);
updateSelectedTags();
};
});
};
// 初始化标签显示
updateSelectedTags();
// 创建标签输入容器
const tagInputContainer = tagSelectArea.createEl('div', { cls: 'tag-input-container' });
// 创建标签输入和建议
const tagInput = tagInputContainer.createEl('input', {
type: 'text',
placeholder: this.canCreateTags ? t('ENTER_TAG_WITH_CREATE') : t('ENTER_TAG')
});
// 创建标签建议容器
const tagSuggestions = tagInputContainer.createEl('div', { cls: 'tag-suggestions' });
// 处理输入事件,显示匹配的标签
tagInput.oninput = () => {
const value = tagInput.value.toLowerCase();
tagSuggestions.empty();
if (value) {
const matches = this.tags
.filter(tag =>
tag.name.toLowerCase().includes(value) &&
!selectedTags.has(tag.name)
)
.slice(0, 10);
if (matches.length > 0) {
// 获取输入框位置和宽度
const inputRect = tagInput.getBoundingClientRect();
const modalRect = this.modalEl.getBoundingClientRect();
// 确保建议列表不超过模态框宽度
const maxWidth = modalRect.right - inputRect.left - 24; // 24px是右边距
// 设置建议列表位置和宽度
tagSuggestions.style.top = `${inputRect.bottom + 4}px`;
tagSuggestions.style.left = `${inputRect.left}px`;
tagSuggestions.style.width = `${Math.min(inputRect.width, maxWidth)}px`;
matches.forEach(tag => {
const suggestion = tagSuggestions.createEl('div', {
cls: 'tag-suggestion',
text: tag.name
});
suggestion.onclick = () => {
selectedTags.add(tag.name);
tagInput.value = '';
tagSuggestions.empty();
updateSelectedTags();
};
});
}
}
};
// 处理回车事件
tagInput.onkeydown = (e) => {
if (e.key === 'Enter' && tagInput.value) {
e.preventDefault();
const value = tagInput.value.trim();
if (value && !selectedTags.has(value)) {
const existingTag = this.tags.find(t => t.name.toLowerCase() === value.toLowerCase());
if (existingTag) {
selectedTags.add(existingTag.name);
updateSelectedTags();
} else if (this.canCreateTags) {
selectedTags.add(value);
updateSelectedTags();
} else {
// 显示权限提示
const notice = contentEl.createEl('div', {
cls: 'tag-notice',
text: t('PERMISSION_ERROR')
});
setTimeout(() => {
notice.remove();
}, 2000);
}
}
tagInput.value = '';
tagSuggestions.empty();
}
};
// 处理失焦事件,隐藏建议
tagInput.onblur = () => {
// 延迟隐藏,以便可以点击建议
setTimeout(() => {
tagSuggestions.empty();
}, 200);
};
// 处理窗口滚动,更新建议列表位置
const updateSuggestionsPosition = () => {
if (tagSuggestions.childNodes.length > 0) {
const inputRect = tagInput.getBoundingClientRect();
tagSuggestions.style.top = `${inputRect.bottom + 4}px`;
tagSuggestions.style.left = `${inputRect.left}px`;
tagSuggestions.style.width = `${inputRect.width}px`;
}
};
// 监听滚动事件
this.modalEl.addEventListener('scroll', updateSuggestionsPosition);
// 模态框关闭时移除事件监听器
this.modalEl.onclose = () => {
this.modalEl.removeEventListener('scroll', updateSuggestionsPosition);
};
// 创建按钮区域
const buttonArea = contentEl.createEl('div', { cls: 'button-area' });
const submitButton = buttonArea.createEl('button', {
text: isUpdate ? t('UPDATE') : t('PUBLISH'),
cls: 'submit-button'
});
// 创建通知容器
const noticeContainer = buttonArea.createEl('div', { cls: 'notice-container' });
submitButton.onclick = async () => {
// 保存选中的标签
this.plugin.settings.selectedTags = Array.from(selectedTags);
await this.plugin.saveSettings();
// 禁用提交按钮,显示加载状态
submitButton.disabled = true;
submitButton.textContent = isUpdate ? t('UPDATING') : t('PUBLISHING');
try {
// 发布主题
const result = await this.plugin.publishTopic();
// 显示结果
noticeContainer.empty();
if (result.success) {
// 成功
noticeContainer.createEl('div', {
cls: 'notice success',
text: isUpdate ? t('UPDATE_SUCCESS') : t('PUBLISH_SUCCESS')
});
// 2秒后自动关闭
setTimeout(() => {
this.close();
}, 2000);
} else {
// 失败
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
errorContainer.createEl('div', {
cls: 'error-title',
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
});
errorContainer.createEl('div', {
cls: 'error-message',
text: result.error || t('UNKNOWN_ERROR')
});
// 添加重试按钮
const retryButton = errorContainer.createEl('button', {
cls: 'retry-button',
text: t('RETRY')
});
retryButton.onclick = () => {
noticeContainer.empty();
submitButton.disabled = false;
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
};
}
} catch (error) {
// 显示错误
noticeContainer.empty();
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
errorContainer.createEl('div', {
cls: 'error-title',
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
});
errorContainer.createEl('div', {
cls: 'error-message',
text: error.message || t('UNKNOWN_ERROR')
});
// 添加重试按钮
const retryButton = errorContainer.createEl('button', {
cls: 'retry-button',
text: t('RETRY')
});
retryButton.onclick = () => {
noticeContainer.empty();
submitButton.disabled = false;
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
};
}
};
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

27
src/utils.ts Normal file
View File

@ -0,0 +1,27 @@
import { TFile } from 'obsidian';
import * as yaml from 'yaml';
// 从内容中提取Front Matter
export function getFrontMatter(content: string): any {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
try {
return yaml.parse(fmMatch[1]);
} catch (e) {
return null;
}
}
return null;
}
// 移除内容中的Front Matter
export function removeFrontMatter(content: string): string {
return content.replace(/^---[\s\S]*?---\n/, '');
}
// 检查文件是否为图片或PDF
export function isImageFile(file: TFile): boolean {
const imageExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "pdf"];
return imageExtensions.includes(file.extension.toLowerCase());
}