refactor(settings): remove diary and template features
This commit is contained in:
1 parent
a26ebea843
commit
849cd358d0
12 files changed
+19
-173
No files matched your search
@@ -20,8 +20,6 @@ if (!buildVersion) throw new Error('缺少应用构建版本信息')
|
||||
|
||||
const section = computed(() => (ws.settings === 'index' ? 'appearance' : ws.settings!))
|
||||
// 手机上一次只显示分组列表或分组内容
|
||||
const dailyTemplates = computed(() => ws.entries.filter(entry => !entry.dir && entry.path.endsWith('.md')))
|
||||
const missingDailyTemplate = computed(() => settings.dailyTemplate && !dailyTemplates.value.some(entry => entry.path === settings.dailyTemplate))
|
||||
const showList = ref(ws.settings === 'index')
|
||||
const current = computed(() => SETTINGS_SECTIONS.find((s) => s.id === section.value)!)
|
||||
const query = ref('')
|
||||
@@ -31,7 +29,7 @@ const dialog = ref<HTMLElement>()
|
||||
const content = ref<HTMLElement>()
|
||||
const heading = ref<HTMLElement>()
|
||||
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
||||
const preferenceSection = computed(() => ['appearance', 'editor', 'templates', 'shortcuts'].includes(section.value))
|
||||
const preferenceSection = computed(() => ['appearance', 'editor', 'shortcuts'].includes(section.value))
|
||||
const saveLabel = computed(() => settingsSave.pending ? '保存中…' : settingsSave.result === 'error' ? '保存失败' : settingsSave.result === 'saved' ? '已保存' : '修改自动保存')
|
||||
|
||||
async function open(id: SettingsSection) {
|
||||
@@ -114,15 +112,6 @@ function changeUIFont(event: Event) {
|
||||
if (value !== settings.uiFontSize) void change({ uiFontSize: value })
|
||||
}
|
||||
|
||||
function changeFolder(key: 'dailyFolder' | 'templatesFolder', event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const path = input.value.trim()
|
||||
if ((key === 'templatesFolder' && !path) || path && path.split('/').some(part => !part || part.startsWith('.') || /[\\\0]/.test(part))) {
|
||||
toast('请输入有效的仓库相对目录'); input.value = settings[key]; return
|
||||
}
|
||||
void change({ [key]: path })
|
||||
}
|
||||
|
||||
// ---- 修改登录密码 ----
|
||||
const pw = ref({ current: '', next: '', confirm: '' })
|
||||
const pwError = ref('')
|
||||
@@ -196,13 +185,6 @@ async function changePassword() {
|
||||
<SettingRow id="tab-size" control="rm-tab-size" resettable @reset="change({ tabSize: DEFAULTS.tabSize })"><select id="rm-tab-size" :value="settings.tabSize" @change="change({ tabSize: Number(($event.target as HTMLSelectElement).value) })"><option v-for="n in TAB_SIZES" :key="n" :value="n">{{ n }} 列</option></select></SettingRow>
|
||||
</template>
|
||||
|
||||
<template v-else-if="section === 'templates'">
|
||||
<SettingRow id="daily-folder" control="rm-daily-folder"><input id="rm-daily-folder" :value="settings.dailyFolder" placeholder="根目录" @change="changeFolder('dailyFolder', $event)" /></SettingRow>
|
||||
<SettingRow id="templates-folder" control="rm-templates-folder"><input id="rm-templates-folder" :value="settings.templatesFolder" @change="changeFolder('templatesFolder', $event)" /></SettingRow>
|
||||
<SettingRow id="daily-template" control="rm-daily-template"><select id="rm-daily-template" :value="settings.dailyTemplate" @change="change({ dailyTemplate: ($event.target as HTMLSelectElement).value })"><option value="">空白笔记</option><option v-for="entry in dailyTemplates" :key="entry.path" :value="entry.path">{{ entry.path }}</option><option v-if="missingDailyTemplate" :value="settings.dailyTemplate">{{ settings.dailyTemplate }}(不存在)</option></select></SettingRow>
|
||||
<p class="rm-share-note">模板支持 <code v-pre>{{date}}</code>、<code v-pre>{{time}}</code>、<code v-pre>{{title}}</code>。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="section === 'account'">
|
||||
<form id="rm-setting-password" class="rm-auth-card rm-settings-form" tabindex="-1" @submit.prevent="changePassword">
|
||||
<h3>修改登录密码</h3>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Icon from './Icon.vue'
|
||||
import { settings } from './settings'
|
||||
import { createFromTemplate } from './templates'
|
||||
import { createNote, toast, ws } from './workspace'
|
||||
const templates = computed(() => ws.entries.filter(entry => !entry.dir && entry.path.startsWith(settings.templatesFolder + '/') && entry.path.endsWith('.md')))
|
||||
const selected = ref('')
|
||||
const path = ref('未命名.md')
|
||||
const busy = ref(false)
|
||||
async function create() {
|
||||
if (!path.value.endsWith('.md') || path.value.split('/').some(part => !part || part.startsWith('.') || /[\\\0]/.test(part))) { toast('请输入有效的 Markdown 笔记路径'); return }
|
||||
busy.value = true
|
||||
try { if (await createFromTemplate(path.value, selected.value)) ws.templateOpen = false }
|
||||
catch (error) { toast('读取模板失败:' + (error instanceof Error ? error.message : '网络错误')) }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="rm-dialog-backdrop" @click.self="!busy && (ws.templateOpen = false)" @keydown.esc.stop="!busy && (ws.templateOpen = false)">
|
||||
<section class="rm-dialog rm-template-dialog" role="dialog" aria-label="从模板新建">
|
||||
<div class="rm-dialog-head"><h2>从模板开始</h2><button class="rm-icon-btn" title="关闭" :disabled="busy" @click="ws.templateOpen = false"><Icon name="close" /></button></div>
|
||||
<p class="rm-share-note">模板是 {{ settings.templatesFolder }}/ 下的 Markdown 文件。支持 <code v-pre>{{date}}</code>、<code v-pre>{{time}}</code> 和 <code v-pre>{{title}}</code>,图片沿用原模板资源。</p>
|
||||
<form @submit.prevent="create">
|
||||
<label>选择模板<select v-model="selected"><option value="">空白笔记</option><option v-for="template in templates" :key="template.path" :value="template.path">{{ template.path }}</option></select></label>
|
||||
<label>笔记路径<input v-model="path" aria-label="笔记路径" required /></label>
|
||||
<div class="rm-template-actions"><button class="rm-btn" type="button" :disabled="busy" @click="ws.templateOpen = false; createNote(settings.templatesFolder)">新建模板文件</button><button class="rm-btn rm-btn-primary" :disabled="busy">创建笔记</button></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.rm-template-dialog { width: min(520px, calc(100vw - 24px)); overflow-y: auto; } form { display: grid; gap: 20px; padding: 20px; } label { display: grid; gap: 9px; font-size: var(--rm-ui-font-size); } input, select { padding: 10px; border: 1px solid var(--rm-divider); border-radius: 8px; background: var(--rm-bg); color: var(--rm-text); font: inherit; } .rm-template-actions { display: flex; justify-content: space-between; gap: 12px; }
|
||||
@media(max-width:767px), (pointer:coarse) { input, select { font-size: max(16px, var(--rm-ui-font-size)); } }
|
||||
</style>
|
||||
@@ -4,7 +4,6 @@ import { api, ApiError } from './api'
|
||||
import { registerCommand } from './commands'
|
||||
import EditorPane from './EditorPane.vue'
|
||||
import KnowledgePanel from './KnowledgePanel.vue'
|
||||
import TemplateDialog from './TemplateDialog.vue'
|
||||
import EditorChrome from './EditorChrome.vue'
|
||||
import Icon from './Icon.vue'
|
||||
import SelectionToolbar from './SelectionToolbar.vue'
|
||||
@@ -133,7 +132,6 @@ onBeforeUnmount(() => {
|
||||
<OutlinePanel v-if="ws.outlineOpen && !ws.focusMode && ws.active && ws.view === 'notes' && ws.images === null" />
|
||||
</Transition>
|
||||
</div>
|
||||
<TemplateDialog v-if="ws.templateOpen" />
|
||||
<QuickSwitcher v-if="ws.switcher !== null" />
|
||||
<SettingsDialog v-if="ws.settings" />
|
||||
<ImportDialog v-if="ws.importing !== null" :dir="ws.importing" @close="ws.importing = null" />
|
||||
|
||||
@@ -41,13 +41,26 @@ async function search(selector: string, query: string) {
|
||||
}
|
||||
|
||||
describe('设置查找与重置', () => {
|
||||
it('移除日记和模板设置、搜索与命令入口', async () => {
|
||||
expect(searchSettings('日记')).toEqual([])
|
||||
expect(searchSettings('日历')).toEqual([])
|
||||
expect(DEFAULTS).not.toHaveProperty('dailyFolder')
|
||||
expect(searchSettings('模板')).toEqual([])
|
||||
expect(DEFAULTS).not.toHaveProperty('templatesFolder')
|
||||
ws.settings = 'appearance'
|
||||
mount(SettingsDialog)
|
||||
expect(host!.textContent).not.toContain('日记')
|
||||
expect(host!.querySelector('#rm-templates-folder')).toBeNull()
|
||||
expect(host!.textContent).not.toContain('模板')
|
||||
})
|
||||
|
||||
it('设置和命令不再提供分享入口', async () => {
|
||||
expect(searchSettings('分享')).toEqual([])
|
||||
expect(allCommands().map(command => command.id)).not.toContain('note.share')
|
||||
expect(allCommands().map(command => command.id)).not.toContain('app.shares')
|
||||
ws.settings = 'appearance'
|
||||
mount(SettingsDialog)
|
||||
expect(host!.querySelectorAll('.rm-settings-tab')).toHaveLength(9)
|
||||
expect(host!.querySelectorAll('.rm-settings-tab')).toHaveLength(8)
|
||||
expect(host!.textContent).not.toContain('分享')
|
||||
})
|
||||
|
||||
@@ -55,7 +68,6 @@ describe('设置查找与重置', () => {
|
||||
expect(searchSettings(' ')).toEqual([])
|
||||
expect(searchSettings('CSS 高级')).toEqual([expect.objectContaining({ id: 'css-snippets', section: 'appearance' })])
|
||||
expect(searchSettings('api')).toEqual(expect.arrayContaining([expect.objectContaining({ section: 'tokens', title: 'API 令牌' }), expect.objectContaining({ id: 'openapi' })]))
|
||||
settings.dailyFolder = 'private-note-folder'
|
||||
settings.cssSnippets = [{ id: 'private', name: 'private-snippet-name', enabled: true, css: ':root { --private: 1 }' }]
|
||||
expect(searchSettings('private')).toEqual([])
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ export const CONTENT_WIDTHS = [
|
||||
|
||||
export const SIDEBAR_WIDTH = { min: 200, max: 480 }
|
||||
|
||||
export const DEFAULTS = { dailyFolder: 'Daily', templatesFolder: 'Templates', dailyTemplate: '', selectionToolbar: true, ...DISPLAY_DEFAULTS, sidebarWidth: 288, spellcheck: false, tabSize: 4 }
|
||||
export const DEFAULTS = { selectionToolbar: true, ...DISPLAY_DEFAULTS, sidebarWidth: 288, spellcheck: false, tabSize: 4 }
|
||||
export type Settings = typeof DEFAULTS & Appearance & { shortcuts: Record<string, string[]> }
|
||||
|
||||
const CACHE_KEY = 'rawmark.settings'
|
||||
@@ -36,9 +36,6 @@ export class SettingsAppearanceError extends Error {
|
||||
function sanitize(raw: Record<string, unknown>): Settings {
|
||||
const pick = <T>(v: unknown, allowed: T[], def: T) => (allowed.includes(v as T) ? (v as T) : def)
|
||||
return {
|
||||
dailyFolder: typeof raw.dailyFolder === 'string' ? raw.dailyFolder : DEFAULTS.dailyFolder,
|
||||
templatesFolder: typeof raw.templatesFolder === 'string' && raw.templatesFolder !== '' ? raw.templatesFolder : DEFAULTS.templatesFolder,
|
||||
dailyTemplate: typeof raw.dailyTemplate === 'string' ? raw.dailyTemplate : DEFAULTS.dailyTemplate,
|
||||
...sanitizeDisplaySettings(raw),
|
||||
selectionToolbar: typeof raw.selectionToolbar === 'boolean' ? raw.selectionToolbar : true,
|
||||
shortcuts: sanitizeShortcuts(raw.shortcuts),
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { SettingsSection } from './workspace'
|
||||
export const SETTINGS_SECTIONS: { id: SettingsSection; title: string; icon: IconName; description: string }[] = [
|
||||
{ id: 'appearance', title: '外观', icon: 'palette', description: '界面字号、主题、明暗模式与 CSS 片段' },
|
||||
{ id: 'editor', title: '编辑器', icon: 'edit', description: '正文排版、代码字体与编辑行为' },
|
||||
{ id: 'templates', title: '日记与模板', icon: 'calendar', description: '日记目录、模板目录与日记模板' },
|
||||
{ id: 'shortcuts', title: '快捷键', icon: 'command', description: '查找命令,添加、移除或重置快捷键' },
|
||||
{ id: 'plugins', title: '插件', icon: 'grid', description: '安装、启停和管理插件及其设置页' },
|
||||
{ id: 'tokens', title: 'API 令牌', icon: 'key', description: '创建或撤销 REST API 访问令牌' },
|
||||
@@ -30,9 +29,6 @@ export const SETTING_ITEMS = {
|
||||
'spellcheck': { section: 'editor', title: '拼写检查', description: '使用浏览器的拼写检查,检查语言取决于浏览器设置。', keywords: '语言 拼写' },
|
||||
'tab-size': { section: 'editor', title: '制表符宽度', description: '仅调整 Tab 字符的显示宽度,不改写正文;Tab 键仍按两个空格缩进。', keywords: 'Tab 缩进 空格' },
|
||||
'typography-preview': { section: 'editor', title: '排版预览', description: '查看当前正文与代码字体的显示效果。', keywords: '字号 行高 段距 预览' },
|
||||
'daily-folder': { section: 'templates', title: '日记目录', description: '仓库相对目录;留空时保存在根目录,修改不会移动已有日记。', keywords: '路径 Daily' },
|
||||
'templates-folder': { section: 'templates', title: '模板目录', description: '仓库相对目录,修改不会移动已有模板。', keywords: '路径 Templates' },
|
||||
'daily-template': { section: 'templates', title: '日记模板', description: '日记按本地日期命名,已有日记直接打开。', keywords: '空白笔记 日期' },
|
||||
'password': { section: 'account', title: '修改登录密码', description: '修改后其他设备需要重新登录。', keywords: '账户 安全 鉴权' },
|
||||
'logout': { section: 'account', title: '退出登录', description: '退出当前设备的登录会话。', keywords: '账户 会话' },
|
||||
'install': { section: 'about', title: '安装应用', description: '浏览器支持安装时,可将 RawMark 添加到桌面或主屏幕。', keywords: 'PWA 安装' },
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { localDate, renderTemplate } from '../markdown/template'
|
||||
import { api } from './api'
|
||||
import { joinPath } from './paths'
|
||||
import { settings } from './settings'
|
||||
import { createNoteAt, openNote, toast, ws } from './workspace'
|
||||
|
||||
export async function createFromTemplate(path: string, template: string) {
|
||||
if (ws.entries.some(entry => entry.path === path)) { toast('目标笔记已存在'); return false }
|
||||
const content = template ? renderTemplate((await api.readFile(template)).content, template, path) : ''
|
||||
const created = await createNoteAt(path, content)
|
||||
if (created) ws.sidebarOpen = false
|
||||
return created
|
||||
}
|
||||
|
||||
let openingDaily = false
|
||||
export async function openDailyNote() {
|
||||
// 模板读取也属于打开日记的过程,此时尚未加入乐观目录。
|
||||
if (openingDaily) return
|
||||
openingDaily = true
|
||||
try {
|
||||
const path = joinPath(settings.dailyFolder, localDate() + '.md')
|
||||
if (ws.entries.some(entry => !entry.dir && entry.path === path)) { await openNote(path); return }
|
||||
await createFromTemplate(path, settings.dailyTemplate)
|
||||
}
|
||||
catch (error) { toast('创建日记失败:' + (error instanceof Error ? error.message : '网络错误')) }
|
||||
finally { openingDaily = false }
|
||||
}
|
||||
@@ -7,9 +7,6 @@ import { drafts, type Draft } from './drafts'
|
||||
import { connectEvents } from './events'
|
||||
import { pluginEvents } from '../plugins/registry'
|
||||
import { attachView, closeTab, createNoteAt, editorStateOf, flush, keepLocalVersion, importNotes, move, remove, readyToReload, openNote, refreshTree, replaceImage, restore, startWorkspace, stopWorkspace, storeViewState, ws } from './workspace'
|
||||
import { openDailyNote } from './templates'
|
||||
import { settings } from './settings'
|
||||
import { localDate } from '../markdown/template'
|
||||
|
||||
vi.mock('./events', () => ({ connectEvents: vi.fn(() => () => {}) }))
|
||||
|
||||
@@ -143,32 +140,6 @@ describe('笔记打开与创建', () => {
|
||||
expect(ws.toasts.some(toast => toast.text.includes('打开失败:文件不存在'))).toBe(false)
|
||||
})
|
||||
|
||||
it('日记模板读取期间重复打开只创建一个标签,之后仍可再次打开', async () => {
|
||||
const previousFolder = settings.dailyFolder, previousTemplate = settings.dailyTemplate
|
||||
settings.dailyFolder = 'Daily-review'
|
||||
settings.dailyTemplate = 'Templates/daily.md'
|
||||
const path = settings.dailyFolder + '/' + localDate() + '.md'
|
||||
let finish!: (value: { content: string; rev: string }) => void
|
||||
vi.spyOn(api, 'readFile').mockImplementation(() => new Promise(resolve => { finish = resolve }))
|
||||
vi.spyOn(api, 'writeFile').mockResolvedValue('r1')
|
||||
vi.spyOn(drafts, 'delete').mockResolvedValue(undefined)
|
||||
try {
|
||||
const first = openDailyNote(), second = openDailyNote()
|
||||
finish({ content: '# {{date}}\n\n日记', rev: 'template-r1' })
|
||||
await Promise.all([first, second])
|
||||
expect(ws.tabs.filter(tab => tab.path === path)).toHaveLength(1)
|
||||
expect(editorStateOf(path)?.doc.toString()).toContain('日记')
|
||||
await closeTab(path)
|
||||
vi.mocked(api.readFile).mockResolvedValue({ content: '重新打开的日记', rev: 'r1' })
|
||||
await openDailyNote()
|
||||
expect(ws.tabs.filter(tab => tab.path === path)).toHaveLength(1)
|
||||
expect(editorStateOf(path)?.doc.toString()).toBe('重新打开的日记')
|
||||
await closeTab(path)
|
||||
} finally {
|
||||
settings.dailyFolder = previousFolder
|
||||
settings.dailyTemplate = previousTemplate
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('保存与关闭', () => {
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface Tab {
|
||||
conflictRev?: string
|
||||
}
|
||||
|
||||
export type SettingsSection = 'appearance' | 'editor' | 'templates' | 'account' | 'shortcuts' | 'tokens' | 'trash' | 'plugins' | 'about'
|
||||
export type SettingsSection = 'appearance' | 'editor' | 'account' | 'shortcuts' | 'tokens' | 'trash' | 'plugins' | 'about'
|
||||
|
||||
export interface Toast {
|
||||
id: number
|
||||
@@ -78,7 +78,6 @@ export const ws = reactive({
|
||||
sourceMode: false,
|
||||
focusMode: false,
|
||||
view: 'notes' as 'notes' | 'graph' | 'tags',
|
||||
templateOpen: false,
|
||||
// 最近打开的笔记,最新的在前
|
||||
recent: JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[],
|
||||
toasts: [] as Toast[],
|
||||
@@ -1078,7 +1077,7 @@ registerBuiltinCommands()
|
||||
|
||||
function registerBuiltinCommands() {
|
||||
for (const command of editorCommands) registerCommand({ ...command,
|
||||
when: () => ws.view === 'notes' && ws.settings === null && ws.images === null && !ws.templateOpen && ws.active !== null && !!view,
|
||||
when: () => ws.view === 'notes' && ws.settings === null && ws.images === null && ws.active !== null && !!view,
|
||||
run: () => { if (view) { command.run(view); view.focus() } },
|
||||
})
|
||||
const hasNote = () => ws.active !== null
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { localDate, renderTemplate } from './template'
|
||||
|
||||
describe('Markdown 模板', () => {
|
||||
it('按本地日期替换变量,资源相对新笔记重定位并保留代码示例', () => {
|
||||
const now = new Date(2026, 9, 22, 8, 5)
|
||||
const source = '# {{title}}\n{{date}} {{time}}\n [文档][ref]\n\n[ref]: ../docs/guide.md#标题\n\n``\n```\n\n```'
|
||||
expect(localDate(now)).toBe('2026-10-22')
|
||||
const result = renderTemplate(source, 'Templates/日记.md', 'Daily/2026-10-22.md', now)
|
||||
expect(result).toContain('# 2026-10-22\n2026-10-22 08:05')
|
||||
expect(result).toContain('')
|
||||
expect(result).toContain('[ref]: ../docs/guide.md#%E6%A0%87%E9%A2%98')
|
||||
expect(result).toContain('``\n```\n\n```')
|
||||
})
|
||||
it('链接实体和 Markdown 转义按真实文件名重定位', () => {
|
||||
const result = renderTemplate('[文档](a&b.md) [括号](a\\(1\\).md)', 'Templates/日记.md', 'Daily/今天.md')
|
||||
expect(result).toContain('[文档](../Templates/a%26b.md)')
|
||||
expect(result).toContain('[括号](../Templates/a(1).md)')
|
||||
})
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
import { markdownParser } from './html'
|
||||
import { decodeLinkDestination } from './links'
|
||||
|
||||
export function localDate(now = new Date()): string {
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function renderTemplate(content: string, template: string, target: string, now = new Date()): string {
|
||||
const changes: { from: number; to: number; value: string }[] = []
|
||||
markdownParser.parse(content).iterate({ enter(node) {
|
||||
if (node.name !== 'URL' || !['Link', 'Image', 'LinkReference'].includes(node.node.parent?.name ?? '')) return
|
||||
const raw = decodeLinkDestination(content.slice(node.from, node.to).replace(/^<|>$/g, ''))
|
||||
if (/^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(raw)) return
|
||||
const url = new URL(raw, 'https://rawmark.local/' + template.split('/').map(encodeURIComponent).join('/'))
|
||||
const parts = decodeURIComponent(url.pathname.slice(1)).split('/')
|
||||
const base = target.slice(0, Math.max(0, target.lastIndexOf('/'))).split('/').filter(Boolean)
|
||||
while (base.length && base[0] === parts[0]) { base.shift(); parts.shift() }
|
||||
const relative = [...base.map(() => '..'), ...parts].map(part => encodeURIComponent(part)).join('/') + url.search + url.hash
|
||||
changes.push({ from: node.from, to: node.to, value: relative })
|
||||
} })
|
||||
for (const change of changes.reverse()) content = content.slice(0, change.from) + change.value + content.slice(change.to)
|
||||
const variables: Record<string, string> = { date: localDate(now), time: `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`, title: target.slice(target.lastIndexOf('/') + 1).replace(/\.md$/, '') }
|
||||
return content.replace(/\{\{(date|time|title)\}\}/g, (_, key: string) => variables[key])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user