diff --git a/package-lock.json b/package-lock.json
index f76aabd..cf445aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,13 +8,19 @@
"name": "obsidian-cubox",
"version": "1.0.0",
"license": "MIT",
+ "dependencies": {
+ "luxon": "^3.1.1",
+ "mustache": "^4.2.0"
+ },
"devDependencies": {
- "@types/mustache": "^4.2.0",
+ "@types/luxon": "^3.4.2",
+ "@types/mustache": "^4.2.5",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
+ "luxon": "^3.1.1",
"mustache": "^4.2.0",
"obsidian": "latest",
"tslib": "2.4.0",
@@ -559,6 +565,12 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
+ "node_modules/@types/luxon": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
+ "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
+ "dev": true
+ },
"node_modules/@types/mustache": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz",
@@ -1686,6 +1698,15 @@
"dev": true,
"peer": true
},
+ "node_modules/luxon": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
+ "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
diff --git a/package.json b/package.json
index 53244c2..de021ad 100644
--- a/package.json
+++ b/package.json
@@ -12,15 +12,21 @@
"author": "",
"license": "MIT",
"devDependencies": {
+ "@types/mustache": "^4.2.5",
"@types/node": "^16.11.6",
+ "@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
+ "mustache": "^4.2.0",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
- "@types/mustache": "^4.2.0",
- "mustache": "^4.2.0"
+ "luxon": "^3.1.1"
+ },
+ "dependencies": {
+ "mustache": "^4.2.0",
+ "luxon": "^3.1.1"
}
}
diff --git a/src/cuboxApi.ts b/src/cuboxApi.ts
index 1e12b5f..dc1ce78 100644
--- a/src/cuboxApi.ts
+++ b/src/cuboxApi.ts
@@ -31,11 +31,6 @@ export interface CuboxHighlight {
create_time: string;
}
-export interface CuboxApiOptions {
- domain: string;
- apiKey: string;
-}
-
export interface CuboxFolder {
id: string;
name: string;
@@ -78,17 +73,17 @@ export class CuboxApi {
private endpoint: string;
private apiKey: string;
- constructor(options: CuboxApiOptions) {
- this.endpoint = 'https://test.cubox.pro'//`https://${options.domain}`;
- this.apiKey = options.apiKey;
+ constructor(domain: string, apiKey: string) {
+ this.endpoint = 'https://test.cubox.pro'//`https://${domain}`;
+ this.apiKey = apiKey;
}
/**
* 同时更新域名和 API Key
*/
- updateConfig(options: CuboxApiOptions): void {
- this.endpoint = 'https://test.cubox.pro'// `https://${options.domain}`;
- this.apiKey = options.apiKey;
+ updateConfig(domain: string, apiKey: string): void {
+ this.endpoint = 'https://test.cubox.pro'// `https://${domain}`;
+ this.apiKey = apiKey;
}
/**
diff --git a/src/main.ts b/src/main.ts
index 1b0fa0a..c4f4c06 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,9 +1,9 @@
import { addIcon, App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFolder } from 'obsidian';
-import { CuboxApi, CuboxApiOptions } from './cuboxApi';
-import { TemplateProcessor } from './templateProcessor';
-import { formatISODateTime, getCurrentFormattedTime } from './utils';
+import { CuboxApi } from './cuboxApi';
+import { TemplateProcessor, FRONT_MATTER_VARIABLES } from './templateProcessor';
+import { formatDateTime } from './utils';
import { ALL_FOLDERS_ID, FolderSelectModal } from './modal/folderSelectModal';
-import { filenameTemplateInstructions, metadataTemplateInstructions, contentTemplateInstructions } from './templateInstructions';
+import { filenameTemplateInstructions, metadataVariablesInstructions, contentTemplateInstructions } from './templateInstructions';
import { ALL_CONTENT_TYPES, TypeSelectModal } from './modal/typeSelectModal';
import { StatusSelectModal } from './modal/statusSelectModal';
import { ALL_TAGS_ID, TagSelectModal } from './modal/tagSelectModal';
@@ -22,7 +22,7 @@ interface CuboxSyncSettings {
syncFrequency: number;
targetFolder: string;
filenameTemplate: string;
- frontMatterTemplate: string;
+ frontMatterVariables: string[];
contentTemplate: string;
highlightInContent: boolean;
dateFormat: string;
@@ -44,7 +44,7 @@ const DEFAULT_SETTINGS: CuboxSyncSettings = {
syncFrequency: 30, // 分钟
targetFolder: 'Cubox',
filenameTemplate: '{{title}}-{{create_time}}',
- frontMatterTemplate: 'id: {{{id}}}',
+ frontMatterVariables: ['id', 'cubox_url', 'url', 'tags'],
contentTemplate: '# {{{title}}}\n\n{{{description}}}\n\n[Read in Cubox]({{{cubox_url}}})\n[Read Original]({{{url}}})\n\n{{#highlights.length}}\n## Annotations\n\n{{#highlights}}\n> {{{highlight_text}}}\n{{{highlight_note}}}\n[Link️]({{{highlight_url}}})\n\n{{/highlights}}\n{{/highlights.length}}',
highlightInContent: true,
dateFormat: 'YYYY-MM-dd',
@@ -63,10 +63,7 @@ export default class CuboxSyncPlugin extends Plugin {
await this.loadSettings();
// 初始化 API 和模板处理器
- this.cuboxApi = new CuboxApi({
- domain: this.settings.domain,
- apiKey: this.settings.apiKey
- });
+ this.cuboxApi = new CuboxApi(this.settings.domain, this.settings.apiKey);
this.templateProcessor = new TemplateProcessor();
// 设置模板处理器的日期格式
this.templateProcessor.setDateFormat(this.settings.dateFormat);
@@ -126,16 +123,10 @@ export default class CuboxSyncPlugin extends Plugin {
// 更新 API 配置
if (this.cuboxApi) {
- this.cuboxApi.updateConfig({
- domain: this.settings.domain,
- apiKey: this.settings.apiKey
- });
+ this.cuboxApi.updateConfig(this.settings.domain, this.settings.apiKey);
} else {
// 如果实例不存在,创建新实例
- this.cuboxApi = new CuboxApi({
- domain: this.settings.domain,
- apiKey: this.settings.apiKey
- });
+ this.cuboxApi = new CuboxApi(this.settings.domain, this.settings.apiKey);
}
// 更新模板处理器的日期格式
@@ -224,8 +215,8 @@ export default class CuboxSyncPlugin extends Plugin {
fullArticle
);
- const frontMatter = this.templateProcessor.processFrontMatterTemplate(
- this.settings.frontMatterTemplate,
+ const frontMatter = this.templateProcessor.processFrontMatter(
+ this.settings.frontMatterVariables,
fullArticle
);
@@ -236,7 +227,7 @@ export default class CuboxSyncPlugin extends Plugin {
// 组合最终内容
let finalContent = '';
- if (frontMatter) {
+ if (frontMatter.length > 0) {
finalContent = `---\n${frontMatter}\n---\n\n`;
}
finalContent += fullArticle.content;
@@ -308,7 +299,7 @@ export default class CuboxSyncPlugin extends Plugin {
}
// 使用新的格式化方法
- return formatISODateTime(new Date(this.settings.lastSyncTime).toISOString(), 'yyyy-MM-dd HH:mm');
+ return formatDateTime(new Date(this.settings.lastSyncTime).toISOString(), 'yyyy-MM-dd HH:mm');
}
}
@@ -587,16 +578,23 @@ class CuboxSyncSettingTab extends PluginSettingTab {
});
// 更新前置元数据模板设置
- const metadataInstructionsFragment = document.createRange().createContextualFragment(metadataTemplateInstructions);
+ const metadataInstructionsFragment = document.createRange().createContextualFragment(metadataVariablesInstructions);
new Setting(containerEl)
- .setName('Metadata Template')
+ .setName('Metadata Variables')
.setDesc(metadataInstructionsFragment)
.addTextArea(text => text
- .setPlaceholder('Enter front matter template')
- .setValue(this.plugin.settings.frontMatterTemplate)
+ .setPlaceholder('Enter front matter variables')
+ .setValue(this.plugin.settings.frontMatterVariables.join(','))
.onChange(async (value) => {
- this.plugin.settings.frontMatterTemplate = value;
+ this.plugin.settings.frontMatterVariables = value
+ .split(',')
+ .map((v) => v.trim())
+ .filter(
+ (v, i, a) =>
+ FRONT_MATTER_VARIABLES.includes(v.split('::')[0]) &&
+ a.indexOf(v) === i,
+ )
await this.plugin.saveSettings();
})
// 设置文本区域的大小
@@ -608,7 +606,7 @@ class CuboxSyncSettingTab extends PluginSettingTab {
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
- this.plugin.settings.frontMatterTemplate = DEFAULT_SETTINGS.frontMatterTemplate;
+ this.plugin.settings.frontMatterVariables = DEFAULT_SETTINGS.frontMatterVariables;
await this.plugin.saveSettings();
this.display(); // 刷新显示
}));
diff --git a/src/templateInstructions.ts b/src/templateInstructions.ts
index c200f44..380caf9 100644
--- a/src/templateInstructions.ts
+++ b/src/templateInstructions.ts
@@ -15,7 +15,7 @@ Enter template for creating synced article file name.
`;
-export const metadataTemplateInstructions = `
+export const metadataVariablesInstructions = `
Enter the metadata separated by comma. you can also use custom aliases in the format of metadata::alias.
diff --git a/src/templateProcessor.ts b/src/templateProcessor.ts
index 432e407..b4fbffb 100644
--- a/src/templateProcessor.ts
+++ b/src/templateProcessor.ts
@@ -1,8 +1,26 @@
import { CuboxArticle, CuboxHighlight } from './cuboxApi';
-import { formatISODateTime, generateSafeFileArticleName } from './utils';
-import * as Mustache from 'mustache';
+import { formatDateTime, generateSafeFileArticleName } from './utils';
+//import * as Mustache from 'mustache';
+import Mustache from 'mustache';
import { parseYaml, stringifyYaml } from 'obsidian';
+export const FRONT_MATTER_VARIABLES = [
+ 'title',
+ 'article_title',
+ 'tags',
+ 'create_time',
+ 'update_time',
+ 'domain',
+ 'url',
+ 'cubox_url',
+ 'description',
+ 'description',
+ 'words_count',
+ 'type',
+ 'words_count',
+ 'id'
+]
+
export class TemplateProcessor {
// 存储日期格式
private dateFormat: string = 'yyyy-MM-dd';
@@ -25,106 +43,75 @@ export class TemplateProcessor {
return article.title || 'Untitled';
}
- // 使用简单的字符串替换而不是 Mustache
- let filename = template;
+ // 准备用于 Mustache 的视图对象
+ const view = {
+ title: article.title || '',
+ article_title: article.article_title || '',
+ create_time: article.create_time ? formatDateTime(article.create_time, this.dateFormat) : '',
+ update_time: article.update_time ? formatDateTime(article.update_time, this.dateFormat) : '',
+ domain: article.domain || '',
+ type: article.type || '',
+ id: article.id || '',
+ // 可以根据需要添加更多字段
+ };
- // 替换所有支持的模板字段
- if (article.title) {
- filename = filename.replace(/{{{title}}}/g, article.title);
- }
- if (article.article_title) {
- filename = filename.replace(/{{{article_title}}}/g, article.article_title);
- }
-
- const createDate = article.create_time ? formatISODateTime(article.create_time, this.dateFormat) : '';
- if (createDate) {
- filename = filename.replace(/{{{create_time}}}/g, createDate);
+ let filename = '';
+ try {
+ filename = Mustache.render(template, view);
+ } catch (error) {
+ console.error('模板渲染失败:', error);
+ // 使用备用方案
+ filename = article.title || 'Untitled';
}
- const updateDate = article.update_time ? formatISODateTime(article.update_time, this.dateFormat) : '';
- if (updateDate) {
- filename = filename.replace(/{{{update_time}}}/g, updateDate);
+ // 限制文件名长度不超过100个字符
+ const MAX_LENGTH = 100;
+ if (filename.length > MAX_LENGTH) {
+ // 如果超过长度限制,截取前100个字符
+ filename = filename.substring(0, MAX_LENGTH);
}
- if (article.domain) {
- filename = filename.replace(/{{{domain}}}/g, article.domain);
- }
-
- if (article.type) {
- filename = filename.replace(/{{{type}}}/g, article.type);
- }
-
- // 移除任何未被替换的模板标记 (如果某些字段不存在)
- filename = filename.replace(/{{{[^}]+}}}/g, '');
-
// 处理文件名安全性
return generateSafeFileArticleName(filename);
}
/**
* 处理前置元数据模板
- * @param template 模板字符串
+ * @param templateVariables 模板字符串
* @param article 文章数据
*/
- processFrontMatterTemplate(template: string, article: CuboxArticle): string {
- if (!template) {
+ processFrontMatter(templateVariables: string[], article: CuboxArticle): string {
+ if (templateVariables.length === 0) {
return '';
}
- try {
- // 首先尝试将模板解析为 YAML 对象
- // 如果模板已经是 YAML 格式,则直接使用
- const yamlObj = parseYaml(template);
-
- // 准备用于替换的数据
- const data = this.prepareTemplateData(article);
-
- // 递归处理 YAML 对象中的所有字符串值
- const processedYaml = this.processYamlObject(yamlObj, data);
-
- // 将处理后的对象转换回 YAML 字符串
- return stringifyYaml(processedYaml);
- } catch (e) {
- // 如果解析失败,则使用 Mustache 渲染模板
- // 这允许用户直接输入 YAML 格式的字符串
- const data = this.prepareTemplateData(article);
- const renderedTemplate = Mustache.render(template, data);
-
- // 尝试将渲染后的模板解析为 YAML 并重新格式化
- try {
- const yamlObj = parseYaml(renderedTemplate);
- return stringifyYaml(yamlObj);
- } catch (e) {
- // 如果仍然无法解析,则返回原始渲染结果
- return renderedTemplate;
- }
+ let frontMatter: { [id: string]: unknown } = {
+ id: article.id, // id is required for deduplication
}
- }
- /**
- * 递归处理 YAML 对象中的所有字符串值
- * @param obj YAML 对象
- * @param data 替换数据
- */
- private processYamlObject(obj: any, data: any): any {
- if (typeof obj === 'string') {
- // 如果是字符串,使用 Mustache 进行模板替换
- return Mustache.render(obj, data);
- } else if (Array.isArray(obj)) {
- // 如果是数组,递归处理每个元素
- return obj.map(item => this.processYamlObject(item, data));
- } else if (obj !== null && typeof obj === 'object') {
- // 如果是对象,递归处理每个属性
- const result: {[key: string]: any} = {};
- for (const key in obj) {
- // 处理键名中的模板变量
- const processedKey = Mustache.render(key, data);
- result[processedKey] = this.processYamlObject(obj[key], data);
+ for (const item of templateVariables) {
+ // split the item into variable and alias
+ const aliasedVariables = item.split('::')
+ const variable = aliasedVariables[0]
+ if (
+ variable === 'tags' &&
+ article.tags &&
+ article.tags.length > 0
+ ) {
+ // tags are handled separately
+ // use label names as tags
+ frontMatter[variable] = article.tags
+ continue
+ }
+
+ const value = (article as any)[variable]
+ if (value) {
+ // if variable is in article, use it
+ frontMatter[variable] = value
}
- return result;
}
- // 其他类型(数字、布尔值等)直接返回
- return obj;
+
+ return stringifyYaml(frontMatter)
}
/**
@@ -164,7 +151,7 @@ export class TemplateProcessor {
private prepareTemplateData(article: CuboxArticle): any {
// 格式化日期
const createDate = article.create_time || '';
- const formattedDate = createDate ? formatISODateTime(createDate, this.dateFormat) : '';
+ const formattedDate = createDate ? formatDateTime(createDate, this.dateFormat) : '';
// 格式化高亮内容
let highlightsText = '';
@@ -190,39 +177,4 @@ export class TemplateProcessor {
tags: article.tags || []
};
}
-
- /**
- * 将对象转换为 YAML 格式的 frontmatter
- * @param obj 要转换的对象
- */
- objectToYamlFrontmatter(obj: any): string {
- try {
- return stringifyYaml(obj);
- } catch (e) {
- console.error('将对象转换为 YAML 失败:', e);
- return '';
- }
- }
-
- /**
- * 解析 YAML 格式的 frontmatter 为对象
- * @param yaml YAML 字符串
- */
- yamlFrontmatterToObject(yaml: string): any {
- try {
- return parseYaml(yaml);
- } catch (e) {
- console.error('解析 YAML 失败:', e);
- return {};
- }
- }
-
- /**
- * 格式化日期 - 使用工具方法
- * @param dateStr 日期字符串
- * @param format 格式模板
- */
- formatDate(dateStr: string, format: string): string {
- return formatISODateTime(dateStr, format);
- }
}
\ No newline at end of file
diff --git a/src/utils.ts b/src/utils.ts
index 53c5c19..fb053ee 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,3 +1,5 @@
+import { DateTime } from 'luxon';
+
// 文件名非法字符正则表达式
export const ILLEGAL_CHAR_REGEX_FILE = /[<>:"/\\|?*\u0000-\u001F]/g;
export const ILLEGAL_CHAR_REGEX_FOLDER = /[<>:"\\|?*\u0000-\u001F]/g;
@@ -32,46 +34,60 @@ export const generateSafeFileArticleName = (title: string): string => {
};
/**
- * 格式化 ISO 时间字符串为可读格式
- * @param isoString ISO 格式的时间字符串
- * @param format 可选的格式化模式
+ * 格式化时间字符串为指定格式
+ * @param dateString 输入的时间字符串
+ * @param format 输出格式,支持:YYYY(年), MM(月), DD(日), HH(时), mm(分), ss(秒)
* @returns 格式化后的时间字符串
*/
-export const formatISODateTime = (isoString: string, format: string = 'yyyy-MM-dd HH:mm'): string => {
- if (!isoString) return '';
+export const formatDateTime = (dateString: string, format: string = 'YYYY-MM-DD HH:mm:ss'): string => {
+ if (!dateString) return '';
try {
- // 直接使用 Date 对象处理时间
- const date = new Date(isoString);
+ // 预处理输入的时间字符串,处理非标准格式
+ let normalizedString = dateString;
- // 检查日期是否有效
- if (isNaN(date.getTime())) {
- return isoString;
- }
+ // 处理类似 2024-04-23T14:30:42:780+08:00 格式(秒与毫秒之间用冒号)
+ normalizedString = normalizedString.replace(/(\d{2}):(\d{2}):(\d{2}):(\d{3})/, '$1:$2:$3.$4');
- // 根据不同格式返回不同的日期字符串
- if (format === 'yyyy-MM-dd HH:mm' || format === 'YYYY-MM-dd HH:mm') {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
-
- return `${year}-${month}-${day} ${hours}:${minutes}`;
- }
- else if (format === 'yyyy-MM-dd' || format === 'YYYY-MM-dd') {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
-
- return `${year}-${month}-${day}`;
- }
+ return DateTime.fromISO(normalizedString).toFormat('yyyy-MM-dd HH:mm');
- // 默认返回原始字符串
- return isoString;
+ // // 检查日期是否有效
+ // if (isNaN(date.getTime())) {
+ // console.warn(`Invalid date string: ${dateString}`);
+ // return dateString;
+ // }
+
+ // // 提取日期各部分
+ // const year = date.getFullYear();
+ // const month = date.getMonth() + 1;
+ // const day = date.getDate();
+ // const hours = date.getHours();
+ // const minutes = date.getMinutes();
+ // const seconds = date.getSeconds();
+
+ // // 格式化各部分,补零
+ // const YYYY = String(year);
+ // const YY = String(year).slice(-2);
+ // const MM = String(month).padStart(2, '0');
+ // const DD = String(day).padStart(2, '0');
+ // const HH = String(hours).padStart(2, '0');
+ // const mm = String(minutes).padStart(2, '0');
+ // const ss = String(seconds).padStart(2, '0');
+
+ // // 使用正则表达式替换格式字符串中的占位符
+ // let result = format
+ // .replace(/YYYY/g, YYYY)
+ // .replace(/YY/g, YY)
+ // .replace(/MM/g, MM)
+ // .replace(/DD/g, DD)
+ // .replace(/HH/g, HH)
+ // .replace(/mm/g, mm)
+ // .replace(/ss/g, ss);
+
+ // return result;
} catch (error) {
- console.error('Error formatting ISO date:', error);
- return isoString;
+ console.error('Error formatting date:', error);
+ return dateString;
}
};
@@ -81,5 +97,5 @@ export const formatISODateTime = (isoString: string, format: string = 'yyyy-MM-d
* @returns 格式化后的当前时间字符串
*/
export const getCurrentFormattedTime = (format: string = 'yyyy-MM-dd HH:mm'): string => {
- return formatISODateTime(new Date().toISOString(), format);
+ return formatDateTime(new Date().toISOString(), format);
};
\ No newline at end of file