diff --git a/src/formats/evernote-enex.ts b/src/formats/evernote-enex.ts index 796bb451..2909a54d 100644 --- a/src/formats/evernote-enex.ts +++ b/src/formats/evernote-enex.ts @@ -1,13 +1,55 @@ -import { FileSystemAdapter, Notice } from 'obsidian'; +import { FileSystemAdapter, Notice, Setting } from 'obsidian'; import { path } from '../filesystem'; import { FormatImporter } from '../format-importer'; import { ImportContext } from '../main'; import { defaultYarleOptions, dropTheRope } from './yarle/yarle'; export class EvernoteEnexImporter extends FormatImporter { + private includeTitleInFrontmatter: boolean; + private includeCreationTimeInFrontmatter: boolean; + private includeUpdateTimeInFrontmatter: boolean; + init() { this.addFileChooserSetting('Evernote', ['enex'], true); this.addOutputLocationSetting('Evernote'); + + this.includeTitleInFrontmatter = false; + let titleDescFragment = new DocumentFragment(); + titleDescFragment.createSpan({ text: 'This preserves titles with special characters like slashes, although you\'ll need a plugin like ' }); + titleDescFragment.createEl('a', { + text: 'Front Matter Title', + href: 'https://github.com/snezhig/obsidian-front-matter-title', + }); + titleDescFragment.createSpan({ text: ' to display them.' }); + new Setting(this.modal.contentEl) + .setName('Include original title in frontmatter') + .setDesc(titleDescFragment) + .addToggle(toggle => { + toggle.setValue(this.includeTitleInFrontmatter); + toggle.onChange(async (value) => { + this.includeTitleInFrontmatter = value; + }); + }); + + this.includeCreationTimeInFrontmatter = false; + new Setting(this.modal.contentEl) + .setName('Include created date in frontmatter') + .addToggle(toggle => { + toggle.setValue(this.includeCreationTimeInFrontmatter); + toggle.onChange(async (value) => { + this.includeCreationTimeInFrontmatter = value; + }); + }); + + this.includeUpdateTimeInFrontmatter = false; + new Setting(this.modal.contentEl) + .setName('Include updated date in frontmatter') + .addToggle(toggle => { + toggle.setValue(this.includeUpdateTimeInFrontmatter); + toggle.onChange(async (value) => { + this.includeUpdateTimeInFrontmatter = value; + }); + }); } async import(ctx: ImportContext) { @@ -32,6 +74,9 @@ export class EvernoteEnexImporter extends FormatImporter { ...{ enexSources: files, outputDir: path.join(adapter.getBasePath(), folder.path), + includeTitleInFrontmatter: this.includeTitleInFrontmatter, + includeCreationTimeInFrontmatter: this.includeCreationTimeInFrontmatter, + includeUpdateTimeInFrontmatter: this.includeUpdateTimeInFrontmatter, }, }; diff --git a/src/formats/yarle/options.ts b/src/formats/yarle/options.ts index dbbefb31..68ac1285 100644 --- a/src/formats/yarle/options.ts +++ b/src/formats/yarle/options.ts @@ -20,6 +20,9 @@ export interface YarleOptions { skipReminderOrder?: boolean; skipReminderDoneTime?: boolean; skipTags?: boolean; + includeTitleInFrontmatter?: boolean; + includeCreationTimeInFrontmatter?: boolean; + includeUpdateTimeInFrontmatter?: boolean; useHashTags?: boolean; replaceWhitespacesInTagsByUnderscore?: boolean; skipEnexFileNameFromOutputPath?: boolean; diff --git a/src/formats/yarle/utils/templates/apply-functions/apply-conditional-template.ts b/src/formats/yarle/utils/templates/apply-functions/apply-conditional-template.ts index 847586c3..ffaa52a2 100644 --- a/src/formats/yarle/utils/templates/apply-functions/apply-conditional-template.ts +++ b/src/formats/yarle/utils/templates/apply-functions/apply-conditional-template.ts @@ -1,6 +1,9 @@ +import { escapeYamlValue } from '../../yaml-utils'; + export const applyConditionalTemplate = (text: string, P: any, newValue?: string): string => { + const escapedValue = escapeYamlValue(newValue); return text - .replace(new RegExp(`${P.CONTENT_PLACEHOLDER}`, 'g'), newValue || '') + .replace(new RegExp(`${P.CONTENT_PLACEHOLDER}`, 'g'), escapedValue) .replace(new RegExp(`${P.START_BLOCK}`, 'g'), '') .replace(new RegExp(`${P.END_BLOCK}`, 'g'), ''); }; diff --git a/src/formats/yarle/utils/templates/apply-functions/apply-content-template.ts b/src/formats/yarle/utils/templates/apply-functions/apply-content-template.ts index e754a444..528b7379 100644 --- a/src/formats/yarle/utils/templates/apply-functions/apply-content-template.ts +++ b/src/formats/yarle/utils/templates/apply-functions/apply-content-template.ts @@ -5,7 +5,7 @@ import { applyTemplateOnBlock } from './apply-template-on-block'; import { getTemplateBlockSettings } from './get-templateblock-settings'; export const applyContentTemplate = (noteData: NoteData, inputText: string, check: Function): string => { - const contentTemplateSettings = getTemplateBlockSettings(inputText, check, P, noteData.content); + const contentTemplateSettings = getTemplateBlockSettings(inputText, check, P, noteData.content, true); return applyTemplateOnBlock(contentTemplateSettings); }; diff --git a/src/formats/yarle/utils/templates/apply-functions/apply-tags-yaml-list-template.ts b/src/formats/yarle/utils/templates/apply-functions/apply-tags-yaml-list-template.ts index efbb988c..388455ef 100644 --- a/src/formats/yarle/utils/templates/apply-functions/apply-tags-yaml-list-template.ts +++ b/src/formats/yarle/utils/templates/apply-functions/apply-tags-yaml-list-template.ts @@ -9,7 +9,7 @@ export const applyTagsYamlListTemplate = (noteData: NoteData, inputText: string, if (noteData.tags) { tags = '\n'+noteData.tags.split(' ').map(tag => ` - ${tag.replace(/^#/, '')}`).join('\n'); } - const tagsTemplateSettings = getTemplateBlockSettings(inputText, check, P, tags); + const tagsTemplateSettings = getTemplateBlockSettings(inputText, check, P, tags, true); return applyTemplateOnBlock(tagsTemplateSettings); }; diff --git a/src/formats/yarle/utils/templates/apply-functions/apply-template-on-block.ts b/src/formats/yarle/utils/templates/apply-functions/apply-template-on-block.ts index d4ea4cfa..721800c8 100644 --- a/src/formats/yarle/utils/templates/apply-functions/apply-template-on-block.ts +++ b/src/formats/yarle/utils/templates/apply-functions/apply-template-on-block.ts @@ -1,4 +1,5 @@ import { TemplateBlockSettings } from '../template-settings'; +import { escapeYamlValue } from '../../yaml-utils'; export const applyTemplateOnBlock = ({ template, @@ -7,12 +8,14 @@ export const applyTemplateOnBlock = ({ endBlockPlaceholder, valuePlaceholder, value, + skipYamlEscaping, }: TemplateBlockSettings): string => { if (value && check()) { + const finalValue = skipYamlEscaping ? value : escapeYamlValue(value); return template .replace(new RegExp(`${startBlockPlaceholder}`, 'g'), '') .replace(new RegExp(`${endBlockPlaceholder}`, 'g'), '') - .replace(new RegExp(`${valuePlaceholder}`, 'g'), value); + .replace(new RegExp(`${valuePlaceholder}`, 'g'), finalValue); } const reg = `${startBlockPlaceholder}([\\d\\D])(?:.|(\r\n|\r|\n))*?(?=${endBlockPlaceholder})${endBlockPlaceholder}`; diff --git a/src/formats/yarle/utils/templates/apply-functions/get-templateblock-settings.ts b/src/formats/yarle/utils/templates/apply-functions/get-templateblock-settings.ts index c8a419cd..71e59a1b 100644 --- a/src/formats/yarle/utils/templates/apply-functions/get-templateblock-settings.ts +++ b/src/formats/yarle/utils/templates/apply-functions/get-templateblock-settings.ts @@ -1,6 +1,6 @@ import { TemplateBlockSettings } from '../template-settings'; -export const getTemplateBlockSettings = (text: string, check: Function, T: any, value?: string): TemplateBlockSettings => { +export const getTemplateBlockSettings = (text: string, check: Function, T: any, value?: string, skipYamlEscaping?: boolean): TemplateBlockSettings => { return { template: text, check, @@ -8,5 +8,6 @@ export const getTemplateBlockSettings = (text: string, check: Function, T: any, endBlockPlaceholder: T.END_BLOCK, valuePlaceholder: T.CONTENT_PLACEHOLDER, value, + skipYamlEscaping, }; }; diff --git a/src/formats/yarle/utils/templates/default-template.ts b/src/formats/yarle/utils/templates/default-template.ts index 533589b3..3e26b41f 100644 --- a/src/formats/yarle/utils/templates/default-template.ts +++ b/src/formats/yarle/utils/templates/default-template.ts @@ -1,6 +1,9 @@ const frontmatterDelimiter = '---\n'; +const titleBlock = '{title-block}title: {title}{end-title-block}\n'; +const createdAtBlock = '{created-at-block}created: {created-at}{end-created-at-block}\n'; +const updatedAtBlock = '{updated-at-block}updated: {updated-at}{end-updated-at-block}\n'; const sourceBlock = '{source-url-block}source: {source-url}{end-source-url-block}\n'; const tagBlock = '{tags-yaml-list-block}\ntags: {tags-yaml-list}\n\n{end-tags-yaml-list-block}'; const contentBlock = '{content-block}{content}{end-content-block}\n'; -export const defaultTemplate = frontmatterDelimiter + tagBlock + sourceBlock +frontmatterDelimiter + contentBlock; +export const defaultTemplate = frontmatterDelimiter + titleBlock + tagBlock + createdAtBlock + updatedAtBlock + sourceBlock + frontmatterDelimiter + contentBlock; diff --git a/src/formats/yarle/utils/templates/remove-functions/index.ts b/src/formats/yarle/utils/templates/remove-functions/index.ts index 1ca00de2..3ca3e802 100644 --- a/src/formats/yarle/utils/templates/remove-functions/index.ts +++ b/src/formats/yarle/utils/templates/remove-functions/index.ts @@ -8,3 +8,4 @@ export * from './remove-link-to-original-placeholder'; export * from './remove-remindertime-placeholder'; export * from './remove-reminderdonetime-placeholder'; export * from './remove-reminderorder-placeholder'; +export * from './remove-title-placeholder'; diff --git a/src/formats/yarle/utils/templates/remove-functions/remove-title-placeholder.ts b/src/formats/yarle/utils/templates/remove-functions/remove-title-placeholder.ts new file mode 100644 index 00000000..0def2c66 --- /dev/null +++ b/src/formats/yarle/utils/templates/remove-functions/remove-title-placeholder.ts @@ -0,0 +1,7 @@ +import * as T from '../placeholders/title-placeholders'; + +import { removePlaceholder } from './remove-placeholder'; + +export const removeTitlePlaceholder = (text: string): string => { + return removePlaceholder(text, T); +}; diff --git a/src/formats/yarle/utils/templates/template-settings.ts b/src/formats/yarle/utils/templates/template-settings.ts index 2dc19d9b..b404d506 100644 --- a/src/formats/yarle/utils/templates/template-settings.ts +++ b/src/formats/yarle/utils/templates/template-settings.ts @@ -5,4 +5,5 @@ export interface TemplateBlockSettings { endBlockPlaceholder: string; valuePlaceholder: string; value?: string; + skipYamlEscaping?: boolean; } diff --git a/src/formats/yarle/utils/templates/templates.ts b/src/formats/yarle/utils/templates/templates.ts index ef5520d3..daa788e6 100644 --- a/src/formats/yarle/utils/templates/templates.ts +++ b/src/formats/yarle/utils/templates/templates.ts @@ -4,24 +4,27 @@ import { YarleOptions } from '../../options'; import { applyContentTemplate, applyCreatedAtTemplate, applyLocationTemplate, applyNotebookTemplate, applyReminderDoneTimeTemplate, applyReminderOrderTemplate, applyReminderTimeTemplate, applySourceUrlTemplate,applyTagsYamlListTemplate, applyTagsTemplate, applyTitleTemplate, applyUpdatedAtTemplate } from './apply-functions'; import * as T from './placeholders/metadata-placeholders'; -import { removeCreatedAtPlaceholder, removeLinkToOriginalTemplate, removeLocationPlaceholder, removeNotebookPlaceholder, removeReminderDoneTimePlaceholder, removeReminderOrderPlaceholder, removeReminderTimePlaceholder, removeSourceUrlPlaceholder, removeUpdatedAtPlaceholder } from './remove-functions'; +import { removeCreatedAtPlaceholder, removeLinkToOriginalTemplate, removeLocationPlaceholder, removeNotebookPlaceholder, removeReminderDoneTimePlaceholder, removeReminderOrderPlaceholder, removeReminderTimePlaceholder, removeSourceUrlPlaceholder, removeTitlePlaceholder, removeUpdatedAtPlaceholder } from './remove-functions'; export const applyTemplate = (noteData: NoteData, yarleOptions: YarleOptions) => { let result = yarleOptions.currentTemplate; - result = applyTitleTemplate(noteData, result, () => noteData.title); result = applyTagsTemplate(noteData, result, () => !yarleOptions.skipTags); result = applyTagsYamlListTemplate(noteData, result, () => !yarleOptions.skipTags); result = applyContentTemplate(noteData, result, () => noteData.content); result = removeLinkToOriginalTemplate(result); - result = (!yarleOptions.skipCreationTime && noteData.createdAt) + result = (yarleOptions.includeTitleInFrontmatter && noteData.title) + ? applyTitleTemplate(noteData, result, () => noteData.title) + : removeTitlePlaceholder(result); + + result = (yarleOptions.includeCreationTimeInFrontmatter && noteData.createdAt) ? applyCreatedAtTemplate(noteData, result) : removeCreatedAtPlaceholder(result); - result = (!yarleOptions.skipUpdateTime && noteData.updatedAt) + result = (yarleOptions.includeUpdateTimeInFrontmatter && noteData.updatedAt) ? applyUpdatedAtTemplate(noteData, result) : removeUpdatedAtPlaceholder(result); diff --git a/src/formats/yarle/utils/yaml-utils.ts b/src/formats/yarle/utils/yaml-utils.ts new file mode 100644 index 00000000..83ad5319 --- /dev/null +++ b/src/formats/yarle/utils/yaml-utils.ts @@ -0,0 +1,38 @@ +/** + * Escapes a string value for use in YAML frontmatter. + * Handles special characters like colons, quotes, newlines, etc. + * + * @param value - The string value to escape + * @returns YAML-safe string representation + */ +export function escapeYamlValue(value: string | undefined): string { + if (!value) { + return ''; + } + + const trimmed = value.trim(); + + // YAML doesn't allow newlines in simple quoted strings. Replace with a + // space, as there's no way to tell whether the user intended the linebreaks + // to be maintained (via a literal "|" block) or ignored (via a folded ">" + // block). + if (/[\r\n]/.test(trimmed)) { + const singleLine = trimmed.replace(/\s*[\r\n]+\s*/g, ' '); + return escapeYamlValue(singleLine); + } + + // Quote the string if it starts with a YAML special character, or contains + // a colon followed by a space. + const needsQuoting = + /^[-?:,\[\]{}#&*!|>'"%@`]/.test(trimmed) || + /:\s/.test(trimmed); + + if (needsQuoting) { + // Backslashes and double-quotes must be escaped inside of YAML + // double-quoted strings. So we replace \ -> \\ and " -> \", before + // wrapping in double quotes. + return '"' + trimmed.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; + } + + return trimmed; +}