filename/fontmatter process

This commit is contained in:
delphi 2025-03-06 17:08:41 +08:00
parent cf5adc0511
commit cb81eac092
7 changed files with 182 additions and 194 deletions

23
package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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;
}
/**

View file

@ -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(); // 刷新显示
}));

View file

@ -15,7 +15,7 @@ Enter template for creating synced article file name.
</div>
`;
export const metadataTemplateInstructions = `
export const metadataVariablesInstructions = `
Enter the metadata separated by comma. you can also use custom aliases in the format of metadata::alias.
<div class="cubox-variables-container">

View file

@ -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);
}
}

View file

@ -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);
};