|
/** |
|
* QuickAdd user script that recreates the core commands from the |
|
* luhman-obsidian-plugin without requiring the original plugin. |
|
* |
|
* Exports individual helpers (`::createChildZettel`, `::insertZettelLink`, ...) |
|
* and a default `entry` function that lets you pick an action via a suggester. |
|
*/ |
|
|
|
const CHECK_SETTINGS_MESSAGE = "Try adjusting the script settings if this seems wrong."; |
|
const idOnlyRegex = /([0-9]+|[a-z]+)/g; |
|
const lettersIDComponentSuccessors = { |
|
a: "b", |
|
b: "c", |
|
c: "d", |
|
d: "e", |
|
e: "f", |
|
f: "g", |
|
g: "h", |
|
h: "i", |
|
i: "j", |
|
j: "k", |
|
k: "l", |
|
l: "m", |
|
m: "n", |
|
n: "o", |
|
o: "p", |
|
p: "q", |
|
q: "r", |
|
r: "s", |
|
s: "t", |
|
t: "u", |
|
u: "v", |
|
v: "w", |
|
w: "x", |
|
x: "y", |
|
y: "z", |
|
z: "aa", |
|
}; |
|
|
|
const DEFAULT_SETTINGS = { |
|
matchRule: "strict", |
|
separator: "⁝ ", |
|
addTitle: false, |
|
addAlias: false, |
|
useLinkAlias: false, |
|
templateFile: "", |
|
templateRequireTitle: true, |
|
templateRequireLink: true, |
|
}; |
|
|
|
const OPTION_KEYS = { |
|
matchRule: "ID Matching Rule", |
|
separator: "ID Separator", |
|
addTitle: "Append Title To Filename", |
|
addAlias: "Add Title Alias To Frontmatter", |
|
useLinkAlias: "Use Title Alias In Inserted Links", |
|
templateFile: "Template File Path", |
|
templateRequireTitle: "Template Requires {{title}}", |
|
templateRequireLink: "Template Requires {{link}}", |
|
}; |
|
|
|
const ACTIONS = [ |
|
{ |
|
key: "createSibling", |
|
label: "Create sibling zettel", |
|
runner: (engine) => engine.createSibling(true), |
|
}, |
|
{ |
|
key: "createSiblingSilent", |
|
label: "Create sibling zettel (stay on current note)", |
|
runner: (engine) => engine.createSibling(false), |
|
}, |
|
{ |
|
key: "createChild", |
|
label: "Create child zettel", |
|
runner: (engine) => engine.createChild(true), |
|
}, |
|
{ |
|
key: "createChildSilent", |
|
label: "Create child zettel (stay on current note)", |
|
runner: (engine) => engine.createChild(false), |
|
}, |
|
{ |
|
key: "insertZettelLink", |
|
label: "Insert link to existing zettel", |
|
runner: (engine) => engine.insertZettelLink(), |
|
}, |
|
{ |
|
key: "openZettelByTitle", |
|
label: "Open zettel by markdown title", |
|
runner: (engine) => engine.openZettelByTitle(), |
|
}, |
|
{ |
|
key: "openParentZettel", |
|
label: "Open parent zettel", |
|
runner: (engine) => engine.openParentZettel(), |
|
}, |
|
{ |
|
key: "outdentZettel", |
|
label: "Outdent current zettel", |
|
runner: (engine) => engine.outdentActiveZettel(), |
|
}, |
|
]; |
|
|
|
const ACTION_LOOKUP = Object.fromEntries( |
|
ACTIONS.map((action) => [action.key, action.runner]) |
|
); |
|
|
|
const script = { |
|
entry: async (params, settings = {}) => { |
|
const engine = new LuhmannQuickAddEngine(params, settings); |
|
const labels = ACTIONS.map((action) => action.label); |
|
const keys = ACTIONS.map((action) => action.key); |
|
const selection = await params.quickAddApi.suggester( |
|
labels, |
|
keys, |
|
"Select a Luhmann action" |
|
); |
|
if (!selection) return; |
|
await ACTION_LOOKUP[selection](engine); |
|
}, |
|
settings: { |
|
name: "Luhmann QuickAdd Toolkit", |
|
author: "QuickAdd", |
|
options: { |
|
[OPTION_KEYS.matchRule]: { |
|
type: "dropdown", |
|
options: ["strict", "separator", "fuzzy"], |
|
defaultValue: DEFAULT_SETTINGS.matchRule, |
|
description: |
|
"Strict = filename is only the ID. Separator = enforce the separator after the ID. Fuzzy = stop at the first non-alphanumeric character.", |
|
}, |
|
[OPTION_KEYS.separator]: { |
|
type: "text", |
|
defaultValue: DEFAULT_SETTINGS.separator, |
|
description: "Used between the ID and the title when titles are appended.", |
|
}, |
|
[OPTION_KEYS.addTitle]: { |
|
type: "toggle", |
|
defaultValue: DEFAULT_SETTINGS.addTitle, |
|
description: "Append the title after the ID when creating files.", |
|
}, |
|
[OPTION_KEYS.addAlias]: { |
|
type: "toggle", |
|
defaultValue: DEFAULT_SETTINGS.addAlias, |
|
description: "Add the created title to the note's frontmatter aliases.", |
|
}, |
|
[OPTION_KEYS.useLinkAlias]: { |
|
type: "toggle", |
|
defaultValue: DEFAULT_SETTINGS.useLinkAlias, |
|
description: "Use the title as an alias in links that get inserted into the current note.", |
|
}, |
|
[OPTION_KEYS.templateFile]: { |
|
type: "text", |
|
defaultValue: DEFAULT_SETTINGS.templateFile, |
|
description: "Optional vault path to a template containing {{title}} and/or {{link}} placeholders.", |
|
}, |
|
[OPTION_KEYS.templateRequireTitle]: { |
|
type: "toggle", |
|
defaultValue: DEFAULT_SETTINGS.templateRequireTitle, |
|
description: "Require {{title}} to exist inside the template file before running.", |
|
}, |
|
[OPTION_KEYS.templateRequireLink]: { |
|
type: "toggle", |
|
defaultValue: DEFAULT_SETTINGS.templateRequireLink, |
|
description: "Require {{link}} to exist inside the template file before running.", |
|
}, |
|
}, |
|
}, |
|
}; |
|
|
|
const ACTION_EXPORTS = [ |
|
{ exportName: "createSiblingZettel", key: "createSibling" }, |
|
{ exportName: "createSiblingZettelNoOpen", key: "createSiblingSilent" }, |
|
{ exportName: "createChildZettel", key: "createChild" }, |
|
{ exportName: "createChildZettelNoOpen", key: "createChildSilent" }, |
|
{ exportName: "insertZettelLink", key: "insertZettelLink" }, |
|
{ exportName: "openZettel", key: "openZettelByTitle" }, |
|
{ exportName: "openParentZettel", key: "openParentZettel" }, |
|
{ exportName: "outdentZettel", key: "outdentZettel" }, |
|
]; |
|
|
|
for (const { exportName, key } of ACTION_EXPORTS) { |
|
script[exportName] = createRunnerForAction(key); |
|
} |
|
|
|
module.exports = script; |
|
|
|
function createRunnerForAction(actionKey) { |
|
const runner = ACTION_LOOKUP[actionKey]; |
|
return async (params, settings = {}) => { |
|
const engine = new LuhmannQuickAddEngine(params, settings); |
|
return await runner(engine); |
|
}; |
|
} |
|
|
|
class LuhmannQuickAddEngine { |
|
constructor(params, rawSettings = {}) { |
|
this.app = params.app; |
|
this.params = params; |
|
this.quickAddApi = params.quickAddApi; |
|
this.obsidian = params.obsidian || globalThis.obsidian || {}; |
|
this.MarkdownView = this.obsidian.MarkdownView; |
|
this.Notice = this.obsidian.Notice || globalThis.Notice || FakeNotice; |
|
this.settings = normalizeSettings(rawSettings); |
|
} |
|
|
|
notice(message, duration = 5000) { |
|
try { |
|
new this.Notice(`[Luhmann QuickAdd] ${message}`, duration); |
|
} catch (error) { |
|
console.log(`[Luhmann QuickAdd] ${message}`); |
|
} |
|
} |
|
|
|
currentFile() { |
|
return this.app.workspace.getActiveFile(); |
|
} |
|
|
|
getEditor() { |
|
if (this.MarkdownView) { |
|
return this.app.workspace.getActiveViewOfType(this.MarkdownView)?.editor; |
|
} |
|
const leaf = this.app.workspace.getActiveViewOfType?.("markdown"); |
|
return leaf?.editor; |
|
} |
|
|
|
isZettelFile(name) { |
|
const match = /(.*)\.md$/.exec(name); |
|
return Boolean(match && this.fileToId(match[1])); |
|
} |
|
|
|
fileToId(basename) { |
|
const separator = escapeRegExp(this.settings.separator || ""); |
|
const ruleRegexes = { |
|
strict: /^((?:[0-9]+|[a-z]+)+)$/, |
|
separator: new RegExp(`^((?:[0-9]+|[a-z]+)+)${separator}.*`), |
|
fuzzy: /^((?:[0-9]+|[a-z]+)+).*/, |
|
}; |
|
const rule = ruleRegexes[this.settings.matchRule] || ruleRegexes.strict; |
|
const match = basename.match(rule); |
|
return match ? match[1] : ""; |
|
} |
|
|
|
idExists(id) { |
|
return this.app.vault |
|
.getMarkdownFiles() |
|
.some((file) => this.fileToId(file.basename) === id); |
|
} |
|
|
|
firstAvailableID(startingID) { |
|
let next = startingID; |
|
while (this.idExists(next)) { |
|
next = this.incrementID(next); |
|
} |
|
return next; |
|
} |
|
|
|
incrementID(id) { |
|
const parts = id.match(idOnlyRegex); |
|
if (!parts || parts.length === 0) return id; |
|
const last = parts.pop(); |
|
return parts.concat([this.incrementIDComponent(last)]).join(""); |
|
} |
|
|
|
incrementIDComponent(idComponent) { |
|
if (/^\d+$/.test(idComponent)) { |
|
return (parseInt(idComponent, 10) + 1).toString(); |
|
} |
|
return this.incrementStringIDComponent(idComponent); |
|
} |
|
|
|
incrementStringIDComponent(component) { |
|
const bits = component.split(""); |
|
const last = bits.pop(); |
|
const next = lettersIDComponentSuccessors[last]; |
|
if (!next) { |
|
return component + "a"; |
|
} |
|
return bits.concat([next]).join(""); |
|
} |
|
|
|
parentID(id) { |
|
const parts = id.match(idOnlyRegex); |
|
if (!parts || parts.length === 0) return ""; |
|
parts.pop(); |
|
return parts.join(""); |
|
} |
|
|
|
nextComponentOf(id) { |
|
const parts = id.match(idOnlyRegex); |
|
if (!parts || parts.length === 0) return "1"; |
|
const last = parts.pop(); |
|
return /^\d+$/.test(last) ? "a" : "1"; |
|
} |
|
|
|
firstChildOf(parentID) { |
|
return parentID + this.nextComponentOf(parentID); |
|
} |
|
|
|
makeNoteForNextSiblingOf(file) { |
|
return this.firstAvailableID( |
|
this.incrementID(this.fileToId(file.basename)) |
|
); |
|
} |
|
|
|
makeNoteForNextChildOf(file) { |
|
return this.firstAvailableID( |
|
this.firstChildOf(this.fileToId(file.basename)) |
|
); |
|
} |
|
|
|
async createSibling(openNewFile) { |
|
return this.createLinkedZettel({ |
|
openNewFile, |
|
idFactory: (file) => this.makeNoteForNextSiblingOf(file), |
|
}); |
|
} |
|
|
|
async createChild(openNewFile) { |
|
return this.createLinkedZettel({ |
|
openNewFile, |
|
idFactory: (file) => this.makeNoteForNextChildOf(file), |
|
}); |
|
} |
|
|
|
async createLinkedZettel({ openNewFile, idFactory }) { |
|
const file = this.currentFile(); |
|
if (!file) { |
|
this.notice("No active file."); |
|
return; |
|
} |
|
if (!this.isZettelFile(file.name)) { |
|
this.notice(`Couldn't find an ID in "${file.basename}". ${CHECK_SETTINGS_MESSAGE}`); |
|
return; |
|
} |
|
const editor = this.getEditor(); |
|
if (!editor) { |
|
this.notice("Open a Markdown editor pane before running this script."); |
|
return; |
|
} |
|
|
|
const nextID = idFactory(file); |
|
const parentLink = `[[${file.basename}]]`; |
|
const selectionText = editor.getSelection(); |
|
let replacementRange = null; |
|
let spacesBefore = 0; |
|
let spacesAfter = 0; |
|
let title = ""; |
|
if (selectionText) { |
|
const info = this.extractSelectionInfo(selectionText); |
|
title = info.title; |
|
spacesBefore = info.spacesBefore; |
|
spacesAfter = info.spacesAfter; |
|
replacementRange = this.getOrderedSelectionRange(editor); |
|
} else { |
|
title = await this.promptForTitle(); |
|
} |
|
|
|
const filename = this.buildFilename(nextID, title); |
|
const folder = this.app.fileManager.getNewFileParent(file.path).path; |
|
const path = joinPath(folder, `${filename}.md`); |
|
const linkText = this.buildLinkText(nextID, title); |
|
const successCallback = replacementRange |
|
? () => { |
|
const prefix = " ".repeat(spacesBefore); |
|
const suffix = " ".repeat(spacesAfter); |
|
editor.replaceRange( |
|
`${prefix}${linkText}${suffix}`, |
|
replacementRange.from, |
|
replacementRange.to |
|
); |
|
} |
|
: () => this.insertLinkAtCursor(editor, linkText); |
|
|
|
await this.makeNote({ |
|
path, |
|
title, |
|
fileLink: parentLink, |
|
placeCursorAtStart: true, |
|
openZettel: openNewFile, |
|
successCallback, |
|
}); |
|
} |
|
|
|
async insertZettelLink() { |
|
const entries = await this.getZettelTitleEntries(); |
|
if (entries.length === 0) { |
|
this.notice("No zettels found."); |
|
return; |
|
} |
|
const display = entries.map( |
|
(entry) => `${entry.title} (${entry.file.basename})` |
|
); |
|
const selected = await this.quickAddApi.suggester( |
|
display, |
|
entries, |
|
"Insert which zettel?" |
|
); |
|
if (!selected) return; |
|
const editor = this.getEditor(); |
|
if (!editor) { |
|
this.notice("Open a Markdown editor pane before inserting links."); |
|
return; |
|
} |
|
const text = `[[${selected.file.basename}]]`; |
|
this.insertLinkAtCursor(editor, text); |
|
} |
|
|
|
async openZettelByTitle() { |
|
const entries = await this.getZettelTitleEntries(); |
|
if (entries.length === 0) { |
|
this.notice("No zettels available to open."); |
|
return; |
|
} |
|
const display = entries.map( |
|
(entry) => `${entry.title} (${entry.file.basename})` |
|
); |
|
const selected = await this.quickAddApi.suggester( |
|
display, |
|
entries, |
|
"Open which zettel?" |
|
); |
|
if (!selected) return; |
|
await this.app.workspace.getLeaf().openFile(selected.file); |
|
} |
|
|
|
async openParentZettel() { |
|
const file = this.currentFile(); |
|
if (!file) { |
|
this.notice("No active file."); |
|
return; |
|
} |
|
const id = this.fileToId(file.basename); |
|
const parentId = this.parentID(id); |
|
if (!parentId) { |
|
this.notice(`No parent found for "${file.basename}". ${CHECK_SETTINGS_MESSAGE}`); |
|
return; |
|
} |
|
const parentFile = this.findZettelById(parentId); |
|
if (!parentFile) { |
|
this.notice(`Couldn't find file for ID ${parentId}. ${CHECK_SETTINGS_MESSAGE}`); |
|
return; |
|
} |
|
await this.app.workspace.getLeaf().openFile(parentFile); |
|
} |
|
|
|
async outdentActiveZettel() { |
|
const file = this.currentFile(); |
|
if (!file) { |
|
this.notice("No active file."); |
|
return; |
|
} |
|
const id = this.fileToId(file.basename); |
|
if (!id) { |
|
this.notice(`Couldn't read the ID from "${file.basename}". ${CHECK_SETTINGS_MESSAGE}`); |
|
return; |
|
} |
|
await this.outdentZettel(id); |
|
} |
|
|
|
findZettelById(id) { |
|
return this.app.vault |
|
.getMarkdownFiles() |
|
.find((file) => this.fileToId(file.basename) === id); |
|
} |
|
|
|
async renameZettel(id, toId) { |
|
const target = this.findZettelById(id); |
|
if (!target) { |
|
this.notice(`Couldn't find file for ID ${id}. ${CHECK_SETTINGS_MESSAGE}`); |
|
return; |
|
} |
|
const rest = target.basename.replace(this.fileToId(target.basename), ""); |
|
const dir = target.parent?.path || ""; |
|
const nextPath = joinPath(dir, `${toId}${rest}.${target.extension}`); |
|
await this.app.fileManager.renameFile(target, nextPath); |
|
} |
|
|
|
async moveChildrenDown(id) { |
|
const children = this.getDirectChildZettels(id); |
|
for (const child of children) { |
|
await this.moveZettelDown(this.fileToId(child.basename)); |
|
} |
|
} |
|
|
|
async moveZettelDown(id) { |
|
await this.moveChildrenDown(id); |
|
await this.renameZettel(id, this.firstAvailableID(id)); |
|
} |
|
|
|
async outdentZettel(id) { |
|
const newID = this.incrementID(this.parentID(id)); |
|
if (this.idExists(newID)) { |
|
await this.moveZettelDown(newID); |
|
} |
|
const childZettels = this.getDirectChildZettels(id); |
|
for (const child of childZettels) { |
|
const childID = this.firstAvailableID(this.firstChildOf(newID)); |
|
await this.renameZettel(this.fileToId(child.basename), childID); |
|
} |
|
await this.renameZettel(id, newID); |
|
} |
|
|
|
getZettels() { |
|
return this.app.vault.getMarkdownFiles().filter((file) => { |
|
const shouldIgnore = /^(_layouts|templates|scripts)/.test(file.path); |
|
return !shouldIgnore && this.fileToId(file.basename) !== ""; |
|
}); |
|
} |
|
|
|
getDirectChildZettels(parentId) { |
|
return this.getZettels().filter((file) => { |
|
return this.parentID(this.fileToId(file.basename)) === parentId; |
|
}); |
|
} |
|
|
|
async getZettelTitleEntries() { |
|
const regex = /^#\s+(.+)$/m; |
|
const entries = []; |
|
for (const file of this.getZettels()) { |
|
const text = await this.app.vault.cachedRead(file); |
|
const match = text.match(regex); |
|
entries.push({ |
|
title: match ? match[1].trim() : file.basename, |
|
file, |
|
}); |
|
} |
|
return entries.sort((a, b) => a.title.localeCompare(b.title)); |
|
} |
|
|
|
buildFilename(id, title) { |
|
if (this.settings.addTitle && title) { |
|
return `${id}${this.settings.separator}${title}`; |
|
} |
|
return id; |
|
} |
|
|
|
buildLinkText(id, title) { |
|
const alias = this.settings.useLinkAlias && title ? `|${title}` : ""; |
|
const suffix = this.settings.addTitle && title ? `${this.settings.separator}${title}` : ""; |
|
return `[[${id}${suffix}${alias}]]`; |
|
} |
|
|
|
insertLinkAtCursor(editor, text) { |
|
const cursor = editor.getCursor(); |
|
editor.replaceRange(text, cursor, cursor); |
|
} |
|
|
|
getOrderedSelectionRange(editor) { |
|
const selection = editor.listSelections?.()[0]; |
|
if (!selection) return null; |
|
const { anchor, head } = selection; |
|
const anchorBeforeHead = |
|
anchor.line < head.line || |
|
(anchor.line === head.line && anchor.ch <= head.ch); |
|
return anchorBeforeHead |
|
? { from: anchor, to: head } |
|
: { from: head, to: anchor }; |
|
} |
|
|
|
extractSelectionInfo(selectionText) { |
|
const trimStart = selectionText.trimStart(); |
|
const trimBoth = trimStart.trimEnd(); |
|
const spacesBefore = selectionText.length - trimStart.length; |
|
const spacesAfter = trimStart.length - trimBoth.length; |
|
return { |
|
spacesBefore, |
|
spacesAfter, |
|
title: toTitleCase(trimBoth), |
|
}; |
|
} |
|
|
|
async promptForTitle() { |
|
try { |
|
const value = await this.quickAddApi.inputPrompt( |
|
"Zettel title", |
|
"Title (optional)", |
|
"" |
|
); |
|
return value || ""; |
|
} catch (error) { |
|
throw error; |
|
} |
|
} |
|
|
|
async makeNote({ |
|
path, |
|
title, |
|
fileLink, |
|
placeCursorAtStart, |
|
openZettel, |
|
successCallback, |
|
}) { |
|
const useTemplate = Boolean(this.settings.templateFile.trim()); |
|
const headingText = title |
|
? (useTemplate ? title.trimStart() : `# ${title.trimStart()}`) |
|
: ""; |
|
const backlinkRegex = /{{link}}/g; |
|
const titleRegex = /{{title}}/g; |
|
let file; |
|
if (useTemplate) { |
|
const templatePath = this.settings.templateFile.trim(); |
|
let templateContent = ""; |
|
try { |
|
templateContent = await this.app.vault.adapter.read(templatePath); |
|
} catch (error) { |
|
this.notice( |
|
`Couldn't read template file at "${templatePath}". Check the script settings.`, |
|
15000 |
|
); |
|
return; |
|
} |
|
const requiresTitle = this.settings.templateRequireTitle; |
|
const requiresLink = this.settings.templateRequireLink; |
|
if ( |
|
(requiresTitle && !titleRegex.test(templateContent)) || |
|
(requiresLink && !backlinkRegex.test(templateContent)) |
|
) { |
|
this.notice( |
|
"Template is missing required {{title}} and/or {{link}} placeholders.", |
|
15000 |
|
); |
|
return; |
|
} |
|
const content = templateContent |
|
.replace(titleRegex, headingText) |
|
.replace(backlinkRegex, fileLink || ""); |
|
file = await this.app.vault.create(path, content); |
|
} else { |
|
const fullContent = `${headingText}\n\n${fileLink || ""}`; |
|
file = await this.app.vault.create(path, fullContent); |
|
} |
|
|
|
if (typeof successCallback === "function") { |
|
await successCallback(); |
|
} |
|
|
|
if (this.settings.addAlias && file && title) { |
|
await this.app.fileManager.processFrontMatter(file, (frontmatter = {}) => { |
|
const aliases = Array.isArray(frontmatter.aliases) |
|
? frontmatter.aliases |
|
: frontmatter.aliases |
|
? [frontmatter.aliases] |
|
: []; |
|
if (!aliases.includes(title)) { |
|
aliases.push(title); |
|
} |
|
frontmatter.aliases = aliases; |
|
return frontmatter; |
|
}); |
|
} |
|
|
|
if (!openZettel) return; |
|
const leaf = this.app.workspace.getLeaf(); |
|
if (!leaf) return; |
|
await leaf.openFile(file); |
|
if (placeCursorAtStart && !useTemplate) { |
|
const editor = this.getEditor(); |
|
if (!editor) return; |
|
let line = 2; |
|
if (this.settings.addAlias) { |
|
line += 4; |
|
} |
|
editor.setCursor({ line, ch: 0 }); |
|
} else { |
|
const editor = this.getEditor(); |
|
if (editor?.exec) { |
|
editor.exec("goEnd"); |
|
} else if (editor) { |
|
const lastLine = editor.lastLine ? editor.lastLine() : editor.lineCount?.() - 1 || 0; |
|
const lastCh = editor.getLine ? editor.getLine(lastLine).length : 0; |
|
editor.setCursor({ line: lastLine, ch: lastCh }); |
|
} |
|
} |
|
} |
|
} |
|
|
|
function normalizeSettings(settings = {}) { |
|
return { |
|
matchRule: settings[OPTION_KEYS.matchRule] || DEFAULT_SETTINGS.matchRule, |
|
separator: settings[OPTION_KEYS.separator] ?? DEFAULT_SETTINGS.separator, |
|
addTitle: Boolean( |
|
settings[OPTION_KEYS.addTitle] ?? DEFAULT_SETTINGS.addTitle |
|
), |
|
addAlias: Boolean( |
|
settings[OPTION_KEYS.addAlias] ?? DEFAULT_SETTINGS.addAlias |
|
), |
|
useLinkAlias: Boolean( |
|
settings[OPTION_KEYS.useLinkAlias] ?? DEFAULT_SETTINGS.useLinkAlias |
|
), |
|
templateFile: |
|
(settings[OPTION_KEYS.templateFile] ?? DEFAULT_SETTINGS.templateFile).trim(), |
|
templateRequireTitle: Boolean( |
|
settings[OPTION_KEYS.templateRequireTitle] ?? |
|
DEFAULT_SETTINGS.templateRequireTitle |
|
), |
|
templateRequireLink: Boolean( |
|
settings[OPTION_KEYS.templateRequireLink] ?? |
|
DEFAULT_SETTINGS.templateRequireLink |
|
), |
|
}; |
|
} |
|
|
|
function escapeRegExp(text) { |
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
|
} |
|
|
|
function toTitleCase(text) { |
|
if (!text) return ""; |
|
return text |
|
.split(/\s+/) |
|
.filter(Boolean) |
|
.map((word) => word[0].toUpperCase() + word.slice(1)) |
|
.join(" "); |
|
} |
|
|
|
function joinPath(folder, filename) { |
|
if (!folder) return filename; |
|
return folder.endsWith("/") ? `${folder}${filename}` : `${folder}/${filename}`; |
|
} |
|
|
|
class FakeNotice { |
|
constructor(message) { |
|
console.log(message); |
|
} |
|
} |
|
|