Files
pantosite/src/content/.obsidian/plugins/astro-composer/main.js
T

4468 lines
638 KiB
JavaScript
Raw Normal View History

2026-04-11 00:41:28 +02:00
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => AstroComposerPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian14 = require("obsidian");
// src/types.ts
var KNOWN_ARRAY_KEYS = ["tags", "aliases", "cssclasses"];
var CONSTANTS = {
DEBOUNCE_MS: 500,
STAT_MTIME_THRESHOLD: 5e3,
EDITOR_STABILIZE_DELAY: 100,
FILE_EXPLORER_REVEAL_DELAY: 200
};
// src/settings.ts
var DEFAULT_SETTINGS = {
defaultTemplate: '---\ntitle: "{{title}}"\ndate: {{date}}\ntags: []\n---\n',
autoInsertProperties: true,
dateFormat: "YYYY-MM-DD",
enableCopyHeadingLink: true,
copyHeadingLinkFormat: "obsidian",
addTrailingSlashToLinks: false,
enableOpenTerminalCommand: false,
terminalProjectRootPath: "",
terminalApplicationName: "",
enableTerminalDebugLogging: false,
enableTerminalRibbonIcon: false,
enableOpenConfigFileCommand: false,
configFilePath: "",
enableConfigRibbonIcon: false,
contentTypes: [],
migrationCompleted: false,
helpButtonReplacement: {
enabled: false,
commandId: "edit-astro-config",
iconId: "rocket"
},
showMdxFilesInExplorer: false,
processBackgroundFileChanges: true,
syncDraftDate: false,
draftDetectionMode: "property",
draftProperty: "",
draftLogic: "true-is-draft",
publishDateField: "",
renameOnTitleClick: false
};
// src/commands/index.ts
var import_obsidian5 = require("obsidian");
// src/utils/file-operations.ts
var import_obsidian = require("obsidian");
// src/utils/path-matching.ts
function matchesFolderPattern(filePath, folderPattern) {
const normalizedFilePath = filePath.toLowerCase();
const normalizedPattern = folderPattern.toLowerCase().replace(/^\/|\/$/g, "");
if (!normalizedPattern || normalizedPattern.trim() === "") {
return !normalizedFilePath.includes("/") || normalizedFilePath.split("/").length === 1;
}
if (!normalizedPattern.includes("*")) {
return normalizedFilePath === normalizedPattern || normalizedFilePath.startsWith(normalizedPattern + "/");
}
const escapedPattern = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]+");
const regexPattern = `^${escapedPattern}(?:/|$)`;
const regex = new RegExp(regexPattern);
return regex.test(normalizedFilePath);
}
function getPatternDepth(folderPattern) {
if (!folderPattern || folderPattern.trim() === "") return 0;
return folderPattern.split("/").length;
}
function sortByPatternSpecificity(types) {
return [...types].sort((a, b) => {
const depthA = getPatternDepth(a.folder);
const depthB = getPatternDepth(b.folder);
return depthB - depthA;
});
}
// src/utils/string-utils.ts
function toKebabCase(str) {
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2$3").toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
}
// src/utils/file-operations.ts
var FileOperations = class {
constructor(app, settings, plugin) {
this.app = app;
this.settings = settings;
this.plugin = plugin;
}
// Get fresh settings from plugin if available, otherwise use stored settings
getSettings() {
var _a;
if ((_a = this.plugin) == null ? void 0 : _a.settings) {
return this.plugin.settings;
}
return this.settings;
}
generateFilename(title, enableUnderscorePrefix = false) {
const kebabTitle = toKebabCase(title);
const safeKebabTitle = kebabTitle || "untitled";
const prefix = enableUnderscorePrefix ? "_" : "";
return `${prefix}${safeKebabTitle}`;
}
determineType(file) {
const filePath = file.path;
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
const sortedTypes = sortByPatternSpecificity(contentTypes);
for (const contentType of sortedTypes) {
if (!contentType.enabled) continue;
if (!contentType.folder || contentType.folder.trim() === "") {
if (!filePath.includes("/") || filePath.split("/").length === 1) {
return contentType.id;
}
} else if (matchesFolderPattern(filePath, contentType.folder)) {
if (contentType.ignoreSubfolders) {
const pathSegments = filePath.split("/");
const pathDepth = pathSegments.length;
const patternSegments = contentType.folder.split("/");
const expectedDepth = patternSegments.length;
if (contentType.creationMode === "folder") {
const folderDepth = pathDepth - 1;
if (folderDepth === expectedDepth || folderDepth === expectedDepth + 1) {
return contentType.id;
}
} else {
if (pathDepth === expectedDepth) {
return contentType.id;
}
}
} else {
return contentType.id;
}
}
}
return "note";
}
getContentType(typeId) {
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
return contentTypes.find((ct) => ct.id === typeId) || null;
}
/**
* Helper to get content type for a given file path
*/
getContentTypeByPath(filePath) {
const dummyFile = { path: filePath };
const typeId = this.determineType(dummyFile);
if (typeId === "note") return null;
return this.getContentType(typeId);
}
getTitleKey(type) {
if (type === "note") return "title";
const contentType = this.getContentType(type);
if (!contentType) return "title";
const template = contentType.template;
const lines = template.split("\n");
let inProperties = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "---") {
inProperties = !inProperties;
continue;
}
if (inProperties) {
const match = trimmed.match(/^(\w+):\s*(.+)$/);
if (match) {
const key = match[1];
const value = match[2];
if (value.includes("{{title}}")) {
return key;
}
}
}
}
return "title";
}
async createFile(options) {
var _a;
const { file, title, type } = options;
if (!title) {
new import_obsidian.Notice(`Title is required to create a ${type}.`);
return null;
}
const contentType = this.getContentType(type);
if (!contentType && type !== "note") {
new import_obsidian.Notice(`Content type ${type} not found.`);
return null;
}
const kebabTitle = toKebabCase(title);
const enableUnderscorePrefix = (contentType == null ? void 0 : contentType.enableUnderscorePrefix) || false;
const prefix = enableUnderscorePrefix ? "_" : "";
let targetFolder = "";
if (type === "note") {
targetFolder = "";
} else if (contentType) {
const originalDir = ((_a = file.parent) == null ? void 0 : _a.path) || "";
if (originalDir === "" || originalDir === "/") {
targetFolder = contentType.folder || "";
} else {
targetFolder = originalDir;
}
}
if (targetFolder) {
const folder = this.app.vault.getAbstractFileByPath(targetFolder);
if (!(folder instanceof import_obsidian.TFolder)) {
await this.app.vault.createFolder(targetFolder);
}
}
const creationMode = (contentType == null ? void 0 : contentType.creationMode) || "file";
if (creationMode === "folder") {
return this.createFolderStructure(file, kebabTitle, prefix, targetFolder, type, contentType);
} else {
return this.createFileStructure(file, kebabTitle, prefix, targetFolder, contentType);
}
}
async createFolderStructure(file, kebabTitle, prefix, targetFolder, type, contentType) {
const folderName = `${prefix}${kebabTitle}`;
let folderPath;
if (targetFolder) {
folderPath = `${targetFolder}/${folderName}`;
} else {
const currentDir = file.parent ? file.parent.path : "";
if (currentDir && currentDir !== "/") {
folderPath = `${currentDir}/${folderName}`;
} else {
folderPath = folderName;
}
}
try {
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof import_obsidian.TFolder)) {
await this.app.vault.createFolder(folderPath);
}
} catch (e) {
}
const indexFileName = (contentType == null ? void 0 : contentType.indexFileName) || "index";
const extension = (contentType == null ? void 0 : contentType.useMdxExtension) ? ".mdx" : ".md";
const fileName = `${indexFileName}${extension}`;
const newPath = `${folderPath}/${fileName}`;
const existingFile = this.app.vault.getAbstractFileByPath(newPath);
if (existingFile instanceof import_obsidian.TFile) {
new import_obsidian.Notice(`File already exists at ${newPath}.`);
return null;
}
if (this.plugin) {
this.plugin.pluginCreatedFiles.set(newPath, Date.now());
}
try {
await this.app.fileManager.renameFile(file, newPath);
const newFile = this.app.vault.getAbstractFileByPath(newPath);
if (!(newFile instanceof import_obsidian.TFile)) {
return null;
}
setTimeout(() => {
const fileExplorer = this.app.workspace.getLeavesOfType("file-explorer")[0];
if (fileExplorer && fileExplorer.view) {
const view = fileExplorer.view;
if (view && typeof view === "object" && "tree" in view) {
const fileTree = view.tree;
if (fileTree && newFile instanceof import_obsidian.TFile && typeof fileTree.revealFile === "function") {
fileTree.revealFile(newFile);
}
}
}
}, 200);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(newFile);
const positionCursor = () => {
var _a;
const view = leaf.view;
if (view && "editor" in view) {
const editor = view.editor;
if (editor) {
const content = editor.getValue();
if (content) {
const lines = content.split("\n");
const lastLine = lines.length - 1;
const lastLineLength = ((_a = lines[lastLine]) == null ? void 0 : _a.length) || 0;
editor.setCursor({ line: lastLine, ch: lastLineLength });
editor.focus();
return true;
}
}
}
return false;
};
setTimeout(() => {
if (!positionCursor()) {
setTimeout(() => {
positionCursor();
}, 200);
}
}, 100);
return newFile;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Failed to create folder structure: ${errorMessage}.`);
return null;
}
}
async createFileStructure(file, kebabTitle, prefix, targetFolder, contentType) {
const extension = (contentType == null ? void 0 : contentType.useMdxExtension) ? ".mdx" : ".md";
const newName = `${prefix}${kebabTitle}${extension}`;
let newPath;
if (targetFolder) {
newPath = `${targetFolder}/${newName}`;
} else {
const currentDir = file.parent ? file.parent.path : "";
if (currentDir && currentDir !== "/") {
newPath = `${currentDir}/${newName}`;
} else {
newPath = newName;
}
}
const existingFile = this.app.vault.getAbstractFileByPath(newPath);
if (existingFile instanceof import_obsidian.TFile && existingFile !== file) {
new import_obsidian.Notice(`File with name "${newName}" already exists.`);
return null;
}
if (this.plugin) {
this.plugin.pluginCreatedFiles.set(newPath, Date.now());
}
try {
await this.app.fileManager.renameFile(file, newPath);
const newFile = this.app.vault.getAbstractFileByPath(newPath);
if (!(newFile instanceof import_obsidian.TFile)) {
new import_obsidian.Notice("Failed to locate renamed file.");
return null;
}
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(newFile);
const positionCursor = () => {
var _a;
const view = leaf.view;
if (view && "editor" in view) {
const editor = view.editor;
if (editor) {
const content = editor.getValue();
if (content) {
const lines = content.split("\n");
const lastLine = lines.length - 1;
const lastLineLength = ((_a = lines[lastLine]) == null ? void 0 : _a.length) || 0;
editor.setCursor({ line: lastLine, ch: lastLineLength });
editor.focus();
return true;
}
}
}
return false;
};
setTimeout(() => {
if (!positionCursor()) {
setTimeout(() => {
positionCursor();
}, 200);
}
}, 100);
return newFile;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Failed to rename file: ${errorMessage}.`);
return null;
}
}
async renameFile(options) {
const { file, title, type } = options;
if (!title) {
new import_obsidian.Notice(`Title is required to rename the content.`);
return null;
}
const contentType = this.getContentType(type);
if (!contentType && type !== "note") {
new import_obsidian.Notice(`Content type ${type} not found.`);
return null;
}
const kebabTitle = toKebabCase(title);
const prefix = "";
const creationMode = (contentType == null ? void 0 : contentType.creationMode) || "file";
if (creationMode === "folder") {
return this.renameFolderStructure(file, kebabTitle, prefix, type, contentType);
} else {
return this.renameFileStructure(file, kebabTitle, prefix, contentType);
}
}
async renameFolderStructure(file, kebabTitle, prefix, type, contentType) {
const indexFileName = (contentType == null ? void 0 : contentType.indexFileName) || "index";
const isIndex = file.basename === indexFileName;
if (isIndex) {
if (!file.parent) {
new import_obsidian.Notice("Cannot rename: file has no parent folder.");
return null;
}
prefix = file.parent.name.startsWith("_") ? "_" : "";
const newFolderName = `${prefix}${kebabTitle}`;
const parentFolder = file.parent.parent;
if (!parentFolder) {
new import_obsidian.Notice("Cannot rename: parent folder has no parent.");
return null;
}
let newFolderPath;
if (parentFolder.path === "" || parentFolder.path === "/") {
newFolderPath = newFolderName;
} else {
newFolderPath = `${parentFolder.path}/${newFolderName}`;
}
const existingFolder = this.app.vault.getAbstractFileByPath(newFolderPath);
if (existingFolder instanceof import_obsidian.TFolder) {
new import_obsidian.Notice(`Folder already exists at ${newFolderPath}.`);
return null;
}
try {
await this.app.fileManager.renameFile(file.parent, newFolderPath);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Failed to rename folder: ${errorMessage}.`);
return null;
}
const newFilePath = `${newFolderPath}/${file.name}`;
const newFile = this.app.vault.getAbstractFileByPath(newFilePath);
if (!(newFile instanceof import_obsidian.TFile)) {
new import_obsidian.Notice("Failed to locate renamed file.");
return null;
}
return newFile;
} else {
if (!file.parent) {
new import_obsidian.Notice("Cannot rename: file has no parent folder.");
return null;
}
prefix = file.basename.startsWith("_") ? "_" : "";
const extension = file.extension;
const newName = `${prefix}${kebabTitle}.${extension}`;
const newPath = `${file.parent.path}/${newName}`;
const existingFile = this.app.vault.getAbstractFileByPath(newPath);
if (existingFile instanceof import_obsidian.TFile && existingFile !== file) {
new import_obsidian.Notice(`File already exists at ${newPath}.`);
return null;
}
await this.app.fileManager.renameFile(file, newPath);
const newFile = this.app.vault.getAbstractFileByPath(newPath);
if (!(newFile instanceof import_obsidian.TFile)) {
new import_obsidian.Notice("Failed to locate renamed file.");
return null;
}
return newFile;
}
}
async renameFileStructure(file, kebabTitle, prefix, contentType) {
if (!file.parent) {
new import_obsidian.Notice("Cannot rename: file has no parent folder.");
return null;
}
const indexFileName = (contentType == null ? void 0 : contentType.indexFileName) || "";
const isIndex = indexFileName && indexFileName.trim() !== "" && file.basename === indexFileName;
if (isIndex) {
prefix = file.parent.name.startsWith("_") ? "_" : "";
const newFolderName = `${prefix}${kebabTitle}`;
const parentFolder = file.parent.parent;
if (!parentFolder) {
new import_obsidian.Notice("Cannot rename: parent folder has no parent.");
return null;
}
let newFolderPath;
if (parentFolder.path === "" || parentFolder.path === "/") {
newFolderPath = newFolderName;
} else {
newFolderPath = `${parentFolder.path}/${newFolderName}`;
}
const existingFolder = this.app.vault.getAbstractFileByPath(newFolderPath);
if (existingFolder instanceof import_obsidian.TFolder) {
new import_obsidian.Notice(`Folder already exists at ${newFolderPath}.`);
return null;
}
const newFilePath = `${newFolderPath}/${file.name}`;
if (this.plugin) {
this.plugin.pluginCreatedFiles.set(newFilePath, Date.now());
}
try {
await this.app.fileManager.renameFile(file.parent, newFolderPath);
} catch (error) {
console.error("FileOperations: Folder rename failed:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Failed to rename folder: ${errorMessage}.`);
return null;
}
const newFile2 = this.app.vault.getAbstractFileByPath(newFilePath);
if (!(newFile2 instanceof import_obsidian.TFile)) {
new import_obsidian.Notice("Failed to locate renamed file.");
return null;
}
return newFile2;
}
prefix = file.basename.startsWith("_") ? "_" : "";
const extension = file.extension;
const newName = `${prefix}${kebabTitle}.${extension}`;
let newPath;
if (file.parent.path === "" || file.parent.path === "/") {
newPath = newName;
} else {
newPath = `${file.parent.path}/${newName}`;
}
const existingFile = this.app.vault.getAbstractFileByPath(newPath);
if (existingFile instanceof import_obsidian.TFile && existingFile !== file) {
new import_obsidian.Notice(`File already exists at ${newPath}.`);
return null;
}
if (this.plugin) {
this.plugin.pluginCreatedFiles.set(newPath, Date.now());
}
try {
await this.app.fileManager.renameFile(file, newPath);
} catch (error) {
console.error("FileOperations: File rename failed:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Failed to rename file: ${errorMessage}.`);
return null;
}
const newFile = this.app.vault.getAbstractFileByPath(newPath);
if (!(newFile instanceof import_obsidian.TFile)) {
new import_obsidian.Notice("Failed to locate renamed file.");
return null;
}
return newFile;
}
};
// src/utils/template-parsing.ts
var import_obsidian2 = require("obsidian");
var TemplateParser = class {
constructor(app, settings, plugin) {
this.app = app;
this.settings = settings;
this.plugin = plugin;
}
// Get fresh settings from plugin if available, otherwise use stored settings
getSettings() {
var _a;
if ((_a = this.plugin) == null ? void 0 : _a.settings) {
return this.plugin.settings;
}
return this.settings;
}
/**
* Convert a string to kebab-case for slug generation
*/
toKebabCase(str) {
return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
}
parseFrontmatter(content) {
let propertiesEnd = 0;
let propertiesText = "";
const existingProperties = {};
if (content.startsWith("---")) {
propertiesEnd = content.indexOf("\n---", 3);
if (propertiesEnd === -1) {
propertiesEnd = content.length;
} else {
propertiesEnd += 4;
}
propertiesText = content.slice(4, propertiesEnd - 4).trim();
try {
let currentKey = null;
const arrayKeys = /* @__PURE__ */ new Set();
propertiesText.split("\n").forEach((line) => {
const trimmedLine = line.trim();
const match = trimmedLine.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
if (match) {
const [, key, value] = match;
currentKey = key;
const trimmedValue = value ? value.trim() : "";
const bracketArrayMatch = trimmedValue.match(/^\[(.*)\]$/);
if (bracketArrayMatch) {
const arrayContent = bracketArrayMatch[1].trim();
existingProperties[key] = [];
arrayKeys.add(key);
if (arrayContent) {
const items = [];
let currentItem = "";
let inQuotes = false;
let quoteChar = "";
for (let i = 0; i < arrayContent.length; i++) {
const char = arrayContent[i];
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
} else if (inQuotes && char === quoteChar) {
if (i > 0 && arrayContent[i - 1] === "\\") {
currentItem += char;
} else {
inQuotes = false;
quoteChar = "";
}
} else if (!inQuotes && char === ",") {
const trimmedItem = currentItem.trim();
if (trimmedItem) {
const unquoted = trimmedItem.replace(/^["']|["']$/g, "");
items.push(unquoted);
}
currentItem = "";
} else {
currentItem += char;
}
}
if (currentItem.trim()) {
const trimmedItem = currentItem.trim();
const unquoted = trimmedItem.replace(/^["']|["']$/g, "");
items.push(unquoted);
}
existingProperties[key] = items;
}
} else {
const isKnownArrayKey = KNOWN_ARRAY_KEYS.includes(key);
const isEmptyArray = !trimmedValue || trimmedValue === "";
const isArrayProperty = isKnownArrayKey || isEmptyArray;
if (isArrayProperty) {
existingProperties[key] = [];
arrayKeys.add(key);
} else {
const unquotedValue = trimmedValue.replace(/^["']|["']$/g, "");
existingProperties[key] = [unquotedValue];
}
}
} else if (currentKey && trimmedLine.startsWith("- ")) {
const isArrayProperty = arrayKeys.has(currentKey);
if (isArrayProperty) {
const item = trimmedLine.replace(/^-\s*/, "");
if (item) existingProperties[currentKey].push(item);
}
} else if (trimmedLine && !trimmedLine.startsWith("- ") && !trimmedLine.startsWith("#")) {
const keyMatch = trimmedLine.match(/^([^:]+):\s*(.*)$/);
if (keyMatch) {
const [, key, value] = keyMatch;
if (!existingProperties[key]) {
existingProperties[key] = [value ? value.trim() : ""];
}
}
}
});
KNOWN_ARRAY_KEYS.forEach((key) => {
if (propertiesText.includes(key + ":") && !existingProperties[key]) {
existingProperties[key] = [];
}
});
} catch (e) {
new import_obsidian2.Notice("Falling back to template due to parsing error.");
}
}
const bodyContent = content.slice(propertiesEnd);
return {
properties: existingProperties,
propertiesText,
propertiesEnd,
bodyContent
};
}
parseTemplate(templateString, title) {
const templateLines = templateString.split("\n");
const templateProps = [];
const templateValues = {};
let inProperties = false;
for (let i = 0; i < templateLines.length; i++) {
const line = templateLines[i].trim();
if (line === "---") {
inProperties = !inProperties;
if (!inProperties) {
break;
}
continue;
}
if (inProperties) {
const match = line.match(/^(\w+):\s*(.*)$/);
if (match) {
const [, key, value] = match;
templateProps.push(key);
const isKnownArrayKey = KNOWN_ARRAY_KEYS.includes(key);
const isEmptyArray = !value || value.trim() === "" || value.trim() === "[]";
const isArrayProperty = isKnownArrayKey || isEmptyArray;
if (isArrayProperty) {
if (value && value.startsWith("[")) {
const items = value.replace(/[[\]]/g, "").split(",").map((t) => t.trim()).filter((t) => t);
templateValues[key] = items;
} else {
templateValues[key] = [];
for (let j = i + 1; j < templateLines.length; j++) {
const nextLine = templateLines[j].trim();
if (nextLine.startsWith("- ")) {
const item = nextLine.replace(/^-\s*/, "").trim();
if (item) {
const arrayValue = templateValues[key];
if (Array.isArray(arrayValue)) {
arrayValue.push(item);
}
}
} else if (nextLine === "---" || nextLine && !nextLine.startsWith("- ") && nextLine.includes(":")) {
break;
}
}
}
} else {
const slug = this.toKebabCase(title);
const settings = this.getSettings();
const stringValue = (value || "").replace(/\{\{title\}\}/g, title).replace(/\{\{date\}\}/g, window.moment(/* @__PURE__ */ new Date()).format(settings.dateFormat)).replace(/\{\{slug\}\}/g, slug);
templateValues[key] = stringValue;
}
}
}
}
return { templateProps, templateValues };
}
buildFrontmatterContent(finalProps, arrayKeys) {
let newContent = "---\n";
for (const key in finalProps) {
const isArrayProperty = KNOWN_ARRAY_KEYS.includes(key) || arrayKeys && arrayKeys.has(key);
if (isArrayProperty) {
newContent += `${key}:
`;
if (finalProps[key].length > 0) {
finalProps[key].forEach((item) => {
newContent += ` - ${item}
`;
});
}
} else {
newContent += `${key}: ${finalProps[key][0] || ""}
`;
}
}
newContent += "---";
return newContent;
}
async updateTitleInFrontmatter(file, newTitle, type) {
const titleKey = this.getTitleKey(type);
const hasTitleInTemplate = this.templateHasTitle(type);
if (!hasTitleInTemplate) {
return;
}
const content = await this.app.vault.read(file);
let propertiesEnd = 0;
let propertiesText = "";
let hasFrontmatter = false;
if (content.startsWith("---")) {
hasFrontmatter = true;
propertiesEnd = content.indexOf("\n---", 3);
if (propertiesEnd === -1) {
propertiesEnd = content.length;
} else {
propertiesEnd += 4;
}
propertiesText = content.slice(4, propertiesEnd - 4).trim();
}
const propOrder = [];
const existing = {};
let currentKey = null;
let titleKeyPosition = -1;
const arrayKeys = /* @__PURE__ */ new Set();
propertiesText.split("\n").forEach((line, index) => {
const trimmedLine = line.trim();
const match = trimmedLine.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
if (match) {
const [, key, value] = match;
propOrder.push(key);
currentKey = key;
if (key === titleKey) {
titleKeyPosition = index;
}
const isKnownArrayKey = KNOWN_ARRAY_KEYS.includes(key);
const isEmptyArray = !value || value.trim() === "" || value.trim() === "[]";
const isArrayProperty = isKnownArrayKey || isEmptyArray;
if (isArrayProperty) {
existing[key] = [];
arrayKeys.add(key);
} else {
existing[key] = value ? value.trim() : "";
}
} else if (currentKey && arrayKeys.has(currentKey) && trimmedLine.startsWith("- ")) {
const item = trimmedLine.replace(/^-\s*/, "");
if (item) existing[currentKey].push(item);
} else if (trimmedLine && !trimmedLine.startsWith("- ") && !trimmedLine.startsWith("#")) {
const keyMatch = trimmedLine.match(/^([^:]+):\s*(.*)$/);
if (keyMatch) {
const [, key, value] = keyMatch;
if (!propOrder.includes(key)) {
propOrder.push(key);
existing[key] = value ? value.trim() : "";
}
}
}
});
let titleVal;
if (newTitle.includes('"') || newTitle.includes("'") || newTitle.includes("\n") || newTitle.includes("\\")) {
titleVal = `'${newTitle.replace(/'/g, "''")}'`;
} else if (newTitle.includes(" ") || newTitle.includes(":") || newTitle.includes("#") || newTitle.includes("@")) {
titleVal = `"${newTitle.replace(/"/g, '\\"')}"`;
} else {
titleVal = newTitle;
}
existing[titleKey] = titleVal;
if ("slug" in existing) {
const newSlug = this.toKebabCase(newTitle);
existing["slug"] = newSlug;
}
if (titleKeyPosition === -1) {
propOrder.push(titleKey);
}
if (!hasFrontmatter) {
return;
}
let newContent = "---\n";
for (const key of propOrder) {
const val = existing[key];
if (Array.isArray(val)) {
newContent += `${key}:
`;
if (val.length > 0) {
val.forEach((item) => {
newContent += ` - ${item}
`;
});
}
} else {
newContent += `${key}: ${val || ""}
`;
}
}
newContent += "---\n";
const bodyContent = content.slice(propertiesEnd);
newContent += bodyContent;
await this.app.vault.modify(file, newContent);
}
getTitleKey(type) {
if (type === "note") return "title";
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
const contentType = contentTypes.find((ct) => ct.id === type);
if (!contentType) return "title";
const template = contentType.template;
const lines = template.split("\n");
let inProperties = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "---") {
inProperties = !inProperties;
continue;
}
if (inProperties) {
const match = trimmed.match(/^(\w+):\s*(.+)$/);
if (match) {
const key = match[1];
const value = match[2];
if (value.includes("{{title}}")) {
return key;
}
}
}
}
return "title";
}
// Check if the template for this content type has {{title}}
templateHasTitle(type) {
if (type === "note") return true;
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
const contentType = contentTypes.find((ct) => ct.id === type);
if (!contentType) return true;
const template = contentType.template;
return template.includes("{{title}}");
}
};
// src/utils/link-conversion.ts
var import_obsidian3 = require("obsidian");
var LinkConverter = class {
constructor(settings, plugin) {
this.settings = settings;
this.plugin = plugin;
}
// Get fresh settings from plugin if available, otherwise use stored settings
getSettings() {
var _a;
if ((_a = this.plugin) == null ? void 0 : _a.settings) {
return this.plugin.settings;
}
return this.settings;
}
// Local toKebabCase removed, using imported one instead
getAstroUrlFromInternalLink(link) {
const hashIndex = link.indexOf("#");
let path = hashIndex >= 0 ? link.slice(0, hashIndex) : link;
const anchor = hashIndex >= 0 ? link.slice(hashIndex) : "";
path = decodeURIComponent(path);
path = path.replace(/\.(md|mdx)$/, "");
const fileExtension = link.endsWith(".mdx") ? ".mdx" : ".md";
const contentTypeInfo = this.getContentTypeForPath(path + fileExtension);
let basePath = contentTypeInfo.basePath || "";
let contentFolder = contentTypeInfo.contentFolder || "";
let indexFileName = contentTypeInfo.indexFileName || "";
if (contentFolder) {
path = path.slice(contentFolder.length + 1);
}
let addTrailingSlash = false;
const parts = path.split("/");
const lastPart = parts[parts.length - 1];
if (indexFileName && indexFileName.trim() !== "" && lastPart === indexFileName) {
parts.pop();
path = parts.join("/");
addTrailingSlash = true;
} else if ((!indexFileName || indexFileName.trim() === "") && lastPart === "index") {
parts.pop();
path = parts.join("/");
addTrailingSlash = true;
}
const slugParts = path.split("/").map((part) => toKebabCase(part));
const slug = slugParts.join("/");
if (basePath) {
if (!basePath.startsWith("/")) {
basePath = "/" + basePath;
}
if (!basePath.endsWith("/")) {
basePath += "/";
}
} else {
basePath = "/";
}
const settings = this.getSettings();
const shouldAddTrailingSlash = (settings.addTrailingSlashToLinks || addTrailingSlash) && !anchor;
return `${basePath}${slug}${shouldAddTrailingSlash ? "/" : ""}${anchor}`;
}
getAstroUrlFromInternalLinkWithContext(link, currentFilePath, currentFileContentType) {
const hashIndex = link.indexOf("#");
let path = hashIndex >= 0 ? link.slice(0, hashIndex) : link;
const anchor = hashIndex >= 0 ? link.slice(hashIndex) : "";
path = decodeURIComponent(path);
path = path.replace(/\.(md|mdx)$/, "");
let basePath = "";
let contentFolder = "";
let indexFileName = "";
const fileExtension = link.endsWith(".mdx") ? ".mdx" : ".md";
const targetContentType = this.getContentTypeForPath(path + fileExtension);
if (!targetContentType.basePath && currentFileContentType.basePath) {
basePath = currentFileContentType.basePath;
indexFileName = currentFileContentType.indexFileName;
contentFolder = currentFileContentType.contentFolder;
} else {
basePath = targetContentType.basePath;
indexFileName = targetContentType.indexFileName;
contentFolder = targetContentType.contentFolder;
}
if (contentFolder) {
path = path.slice(contentFolder.length + 1);
}
let addTrailingSlash = false;
const parts = path.split("/");
const lastPart = parts[parts.length - 1];
if (indexFileName && indexFileName.trim() !== "" && lastPart === indexFileName) {
parts.pop();
path = parts.join("/");
addTrailingSlash = true;
} else if ((!indexFileName || indexFileName.trim() === "") && lastPart === "index") {
parts.pop();
path = parts.join("/");
addTrailingSlash = true;
}
const slugParts = path.split("/").map((part) => toKebabCase(part));
const slug = slugParts.join("/");
if (basePath) {
if (!basePath.startsWith("/")) {
basePath = "/" + basePath;
}
if (!basePath.endsWith("/")) {
basePath += "/";
}
} else {
basePath = "/";
}
const settings = this.getSettings();
const shouldAddTrailingSlash = (settings.addTrailingSlashToLinks || addTrailingSlash) && !anchor;
return `${basePath}${slug}${shouldAddTrailingSlash ? "/" : ""}${anchor}`;
}
isInConfiguredContentDirectory(filePath) {
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
const sortedTypes = sortByPatternSpecificity(contentTypes);
for (const contentType of sortedTypes) {
if (!contentType.enabled) continue;
if (!contentType.folder || contentType.folder.trim() === "") {
if (!filePath.includes("/") || filePath.split("/").length === 1) {
return true;
}
} else if (matchesFolderPattern(filePath, contentType.folder)) {
if (contentType.ignoreSubfolders) {
const pathSegments = filePath.split("/");
const pathDepth = pathSegments.length;
const patternSegments = contentType.folder.split("/");
const expectedDepth = patternSegments.length;
if (contentType.creationMode === "folder") {
const folderDepth = pathDepth - 1;
if (folderDepth === expectedDepth || folderDepth === expectedDepth + 1) {
return true;
}
} else {
if (pathDepth === expectedDepth) {
return true;
}
}
} else {
return true;
}
}
}
return false;
}
getContentTypeForPath(filePath) {
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
const sortedTypes = sortByPatternSpecificity(contentTypes);
for (const contentType of sortedTypes) {
if (!contentType.enabled) continue;
if (!contentType.folder || contentType.folder.trim() === "") {
if (!filePath.includes("/") || filePath.split("/").length === 1) {
return {
basePath: contentType.linkBasePath || "",
creationMode: contentType.creationMode,
indexFileName: contentType.indexFileName || "",
contentFolder: ""
};
}
} else if (matchesFolderPattern(filePath, contentType.folder)) {
if (contentType.ignoreSubfolders) {
const pathSegments = filePath.split("/");
const pathDepth = pathSegments.length;
const patternSegments = contentType.folder.split("/");
const expectedDepth = patternSegments.length;
if (contentType.creationMode === "folder") {
const folderDepth = pathDepth - 1;
if (folderDepth === expectedDepth || folderDepth === expectedDepth + 1) {
return {
basePath: contentType.linkBasePath || "",
creationMode: contentType.creationMode,
indexFileName: contentType.indexFileName || "",
contentFolder: contentType.folder
};
}
} else {
if (pathDepth === expectedDepth) {
return {
basePath: contentType.linkBasePath || "",
creationMode: contentType.creationMode,
indexFileName: contentType.indexFileName || "",
contentFolder: contentType.folder
};
}
}
} else {
return {
basePath: contentType.linkBasePath || "",
creationMode: contentType.creationMode,
indexFileName: contentType.indexFileName || "",
contentFolder: contentType.folder
};
}
}
}
return {
basePath: "",
creationMode: "file",
indexFileName: "",
contentFolder: ""
};
}
convertWikilinksForAstro(editor, file) {
var _a, _b;
if (!(file instanceof import_obsidian3.TFile)) {
new import_obsidian3.Notice("No active file.");
return;
}
const cursor = editor.getCursor();
const originalLine = cursor.line;
const originalCh = cursor.ch;
const originalContent = editor.getValue();
const originalLineCount = originalContent.split("\n").length;
const originalLineLength = ((_a = originalContent.split("\n")[originalLine]) == null ? void 0 : _a.length) || 0;
const content = editor.getValue();
let newContent = content;
let convertedCount = 0;
let skippedCount = 0;
const skippedLinks = [];
const currentFileContentType = this.getContentTypeForPath(file.path);
const imageExtensions = /\.(png|jpg|jpeg|gif|svg)$/i;
const canConvertLink = (linkText) => {
if (imageExtensions.test(linkText)) {
return false;
}
if (linkText.match(/^https?:\/\//)) {
return false;
}
if (!linkText.includes(".md") && !linkText.includes(".mdx") && !linkText.match(/^[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)*$/)) {
return false;
}
let targetPath;
if (linkText.endsWith(".md") || linkText.endsWith(".mdx")) {
targetPath = linkText;
} else {
targetPath = linkText + ".md";
}
const isInConfiguredDirectory = this.isInConfiguredContentDirectory(targetPath);
const isSimpleFilename = !targetPath.includes("/");
const hasCurrentContentType = currentFileContentType.basePath !== "" || currentFileContentType.creationMode !== "file" || currentFileContentType.indexFileName !== "";
return isInConfiguredDirectory || isSimpleFilename && hasCurrentContentType;
};
newContent = newContent.replace(
/\[\[([^\]|]+)(\|([^\]]+))?\]\]/g,
(match, linkText, _pipe, displayText) => {
if (imageExtensions.test(linkText)) {
skippedCount++;
skippedLinks.push(linkText);
return match;
}
if (!canConvertLink(linkText)) {
skippedCount++;
skippedLinks.push(linkText);
return match;
}
const display = displayText || linkText.replace(/\.(md|mdx)$/, "");
const url = this.getAstroUrlFromInternalLinkWithContext(linkText, file.path, currentFileContentType);
convertedCount++;
return `[${display}](${url})`;
}
);
newContent = newContent.replace(
/\[([^\]]+)\]\(([^)]+\.(md|mdx)[^)]*)\)/g,
(match, displayText, link) => {
if (link.match(/^https?:\/\//) || imageExtensions.test(link)) {
skippedCount++;
skippedLinks.push(link);
return match;
}
if (!canConvertLink(link)) {
skippedCount++;
skippedLinks.push(link);
return match;
}
const url = this.getAstroUrlFromInternalLinkWithContext(link, file.path, currentFileContentType);
convertedCount++;
return `[${displayText}](${url})`;
}
);
newContent = newContent.replace(
/!\[(.*?)\]\(([^)]+)\)/g,
(match) => {
skippedCount++;
return match;
}
);
newContent = newContent.replace(/\{\{([^}]+)\}\}/g, (match, fileName) => {
if (imageExtensions.test(fileName)) {
skippedCount++;
skippedLinks.push(fileName);
return match;
}
if (!canConvertLink(fileName)) {
skippedCount++;
skippedLinks.push(fileName);
return match;
}
const url = this.getAstroUrlFromInternalLinkWithContext(fileName, file.path, currentFileContentType);
convertedCount++;
return `[Embedded: ${fileName}](${url})`;
});
editor.setValue(newContent);
const newLineCount = newContent.split("\n").length;
const newLineLength = ((_b = newContent.split("\n")[originalLine]) == null ? void 0 : _b.length) || 0;
let newLine = originalLine;
let newCh = originalCh;
if (newLineCount !== originalLineCount) {
if (newLine >= newLineCount) {
newLine = Math.max(0, newLineCount - 1);
}
}
if (newLineLength !== originalLineLength) {
if (newCh > newLineLength) {
newCh = Math.max(0, newLineLength);
}
}
editor.setCursor({ line: newLine, ch: newCh });
if (convertedCount > 0 && skippedCount === 0) {
new import_obsidian3.Notice(`Converted ${convertedCount} internal link${convertedCount > 1 ? "s" : ""} for Astro.`);
} else if (convertedCount > 0 && skippedCount > 0) {
new import_obsidian3.Notice(`Converted ${convertedCount} link${convertedCount > 1 ? "s" : ""} for Astro. Skipped ${skippedCount} link${skippedCount > 1 ? "s" : ""} outside configured content directories.`);
} else if (skippedCount > 0) {
new import_obsidian3.Notice(`No links converted. All ${skippedCount} link${skippedCount > 1 ? "s" : ""} are outside configured content directories or are images/external links.`);
} else {
new import_obsidian3.Notice("No internal links found to convert.");
}
}
};
// src/ui/title-modal.ts
var import_obsidian4 = require("obsidian");
var TitleModal = class extends import_obsidian4.Modal {
constructor(app, file, plugin, type, isRename = false, isNewNote = false) {
super(app);
this.file = file;
this.plugin = plugin;
this.type = type;
this.isRename = isRename;
this.isNewNote = isNewNote;
const settings = plugin.settings;
this.fileOps = new FileOperations(app, settings, plugin);
this.templateParser = new TemplateParser(app, settings);
}
async getCurrentTitleAsync() {
if (!this.file) {
return "";
}
try {
const content = await this.app.vault.read(this.file);
const titleKey = this.fileOps.getTitleKey(this.type);
const { properties } = this.templateParser.parseFrontmatter(content);
if (titleKey in properties) {
const titleValue = properties[titleKey];
if (Array.isArray(titleValue) && titleValue.length > 0) {
return String(titleValue[0]);
}
if (titleValue !== null && titleValue !== void 0) {
return String(titleValue);
}
}
} catch (error) {
console.error("Error reading file for title:", error);
}
return this.getFallbackTitle();
}
getCurrentTitle() {
if (!this.file) {
return "";
}
const titleKey = this.fileOps.getTitleKey(this.type);
const cache = this.app.metadataCache.getFileCache(this.file);
if ((cache == null ? void 0 : cache.frontmatter) && titleKey in cache.frontmatter) {
const titleValue = cache.frontmatter[titleKey];
if (typeof titleValue === "string") {
return titleValue;
}
if (Array.isArray(titleValue) && titleValue.length > 0) {
const firstValue = titleValue[0];
if (typeof firstValue === "string") {
return firstValue;
}
if (firstValue != null) {
if (typeof firstValue === "number" || typeof firstValue === "boolean") {
return String(firstValue);
}
if (typeof firstValue === "string") {
return firstValue;
}
}
}
if (titleValue == null) {
return "";
}
if (typeof titleValue === "number" || typeof titleValue === "boolean") {
return String(titleValue);
}
if (typeof titleValue === "string") {
return titleValue;
}
return "";
}
return this.getFallbackTitle();
}
getFallbackTitle() {
if (!this.file) {
return "";
}
let basename = this.file.basename;
if (this.file.parent && this.type !== "note") {
const contentType = this.fileOps.getContentType(this.type);
const indexFileName = (contentType == null ? void 0 : contentType.indexFileName) || "";
if (indexFileName.trim() !== "" && basename === indexFileName) {
basename = this.file.parent.name;
}
}
if (basename.startsWith("_")) {
basename = basename.slice(1);
}
return basename.replace(/-/g, " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
}
/**
* Extracts a suggested title from the file basename for newly created files.
* This is used when a file is created from a link (e.g., [[sEfsleif]]).
* Preserves the original text as much as possible.
*/
getSuggestedTitleFromBasename() {
if (!this.file) {
return "";
}
let basename = this.file.basename;
if (this.file.parent && this.type !== "note") {
const contentType = this.fileOps.getContentType(this.type);
const indexFileName = (contentType == null ? void 0 : contentType.indexFileName) || "";
if (indexFileName.trim() !== "" && basename === indexFileName) {
basename = this.file.parent.name;
}
}
if (basename.startsWith("_")) {
basename = basename.slice(1);
}
return basename;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
const isMobile = window.innerWidth <= 768 || import_obsidian4.Platform.isMobile;
if (isMobile) {
this.modalEl.addClass("astro-composer-mobile-modal");
}
if (this.isRename) {
const typeName = this.getTypeDisplayName();
if (this.type === "note") {
contentEl.createEl("h2", { text: "Rename content" });
contentEl.createEl("p", { text: "Enter a title for this content:" });
} else {
contentEl.createEl("h2", { text: `Rename ${typeName} content` });
contentEl.createEl("p", { text: `Enter new title for your ${typeName} content:` });
}
this.titleInput = contentEl.createEl("input", {
type: "text",
placeholder: "New Title",
cls: "astro-composer-title-input"
});
void this.getCurrentTitleAsync().then((title) => {
this.titleInput.value = title;
});
} else if (this.isNewNote) {
const typeName = this.getTypeDisplayName();
if (this.type === "note") {
contentEl.createEl("h2", { text: "New content" });
contentEl.createEl("p", { text: "Enter a title for this content:" });
} else {
contentEl.createEl("h2", { text: `Create new ${typeName} content` });
contentEl.createEl("p", { text: `Enter a title for your new ${typeName} content:` });
}
this.titleInput = contentEl.createEl("input", {
type: "text",
placeholder: "New Title",
cls: "astro-composer-title-input"
});
} else {
const typeName = this.getTypeDisplayName();
if (this.type === "note") {
contentEl.createEl("h2", { text: "New content" });
contentEl.createEl("p", { text: "Enter a title for this content:" });
} else {
contentEl.createEl("h2", { text: `Create new ${typeName} content` });
contentEl.createEl("p", { text: `Enter a title for your new ${typeName} content:` });
}
this.titleInput = contentEl.createEl("input", {
type: "text",
placeholder: "New Title",
cls: "astro-composer-title-input"
});
if (this.file) {
const suggestedTitle = this.getSuggestedTitleFromBasename();
if (suggestedTitle) {
this.titleInput.value = suggestedTitle;
}
}
}
this.titleInput.focus();
if (this.isNewNote) {
setTimeout(() => {
this.titleInput.setSelectionRange(0, 0);
}, 0);
}
const buttonContainer = contentEl.createDiv({ cls: "astro-composer-button-container" });
const cancelButton = buttonContainer.createEl("button", { text: "Cancel", cls: "astro-composer-cancel-button" });
cancelButton.onclick = () => this.close();
const submitButton = buttonContainer.createEl("button", { text: this.isRename ? "Rename" : "Create", cls: ["astro-composer-create-button", "mod-cta"] });
submitButton.onclick = () => this.submit();
this.titleInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") void this.submit();
});
}
async submit() {
const title = this.titleInput.value.trim();
if (!title) {
new import_obsidian4.Notice("Please enter a title.");
return;
}
try {
let newFile = null;
if (this.isRename) {
newFile = await this.fileOps.renameFile({ file: this.file, title, type: this.type });
if (newFile) {
await this.templateParser.updateTitleInFrontmatter(newFile, title, this.type);
} else {
this.close();
return;
}
} else if (this.isNewNote) {
if (this.file) {
newFile = await this.fileOps.createFile({ file: this.file, title, type: this.type });
const shouldInsertProperties = this.plugin.settings.autoInsertProperties;
if (newFile && shouldInsertProperties) {
await this.addPropertiesToFile(newFile, title, this.type);
this.positionCursorAtEnd(newFile);
}
}
} else if (this.file) {
newFile = await this.fileOps.createFile({ file: this.file, title, type: this.type });
const shouldInsertProperties = this.plugin.settings.autoInsertProperties;
if (newFile && shouldInsertProperties) {
await this.addPropertiesToFile(newFile, title, this.type);
this.positionCursorAtEnd(newFile);
}
} else {
newFile = await this.createNewFile(title);
}
if (!newFile) {
new import_obsidian4.Notice(`Failed to ${this.isRename ? "rename" : "create"} ${this.type}.`);
this.close();
return;
}
} catch (error) {
console.error("TitleModal: Error during process:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian4.Notice(`Error ${this.isRename ? "renaming" : "creating"} ${this.type}: ${errorMessage}.`);
this.close();
return;
}
this.close();
}
getTypeDisplayName() {
if (this.type === "note") {
return "Content";
}
const contentType = this.fileOps.getContentType(this.type);
return contentType ? contentType.name : "Content";
}
async createNewFile(title) {
var _a, _b;
let targetFolder;
const originalDir = ((_b = (_a = this.file) == null ? void 0 : _a.parent) == null ? void 0 : _b.path) || "";
if (this.type !== "note") {
const contentType2 = this.fileOps.getContentType(this.type);
if (originalDir === "" || originalDir === "/") {
targetFolder = (contentType2 == null ? void 0 : contentType2.folder) || "";
} else {
targetFolder = originalDir;
}
} else {
targetFolder = originalDir;
}
const filename = this.fileOps.generateFilename(title);
const contentType = this.fileOps.getContentType(this.type);
const extension = (contentType == null ? void 0 : contentType.useMdxExtension) ? ".mdx" : ".md";
const filePath = targetFolder ? `${targetFolder}/${filename}${extension}` : `${filename}${extension}`;
if (this.plugin) {
this.plugin.pluginCreatedFiles.set(filePath, Date.now());
}
let initialContent = "";
if (this.plugin.settings.autoInsertProperties) {
initialContent = this.generateInitialContent(title);
}
try {
const newFile = await this.app.vault.create(filePath, initialContent);
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(newFile);
const positionCursor = () => {
var _a2;
const view = leaf.view;
if (view instanceof import_obsidian4.MarkdownView && view.editor) {
const editor = view.editor;
const content = editor.getValue();
if (content) {
const lines = content.split("\n");
const lastLine = lines.length - 1;
const lastLineLength = ((_a2 = lines[lastLine]) == null ? void 0 : _a2.length) || 0;
editor.setCursor({ line: lastLine, ch: lastLineLength });
editor.focus();
return true;
}
}
return false;
};
setTimeout(() => {
if (!positionCursor()) {
setTimeout(() => {
positionCursor();
}, 200);
}
}, 100);
return newFile;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create file: ${errorMessage}`);
}
}
generateInitialContent(title) {
const now = /* @__PURE__ */ new Date();
const dateString = window.moment(now).format(this.plugin.settings.dateFormat);
const slug = toKebabCase(title);
let template;
if (this.type === "note") {
const escapedTitle = this.escapeYamlString(title);
template = `---
title: ${escapedTitle}
date: ${dateString}
---
`;
} else {
const contentType = this.fileOps.getContentType(this.type);
if (!contentType) {
const escapedTitle = this.escapeYamlString(title);
template = `---
title: ${escapedTitle}
date: ${dateString}
---
`;
} else {
template = contentType.template;
}
}
template = template.replace(/\{\{title\}\}/g, title);
template = template.replace(/\{\{date\}\}/g, dateString);
template = template.replace(/\{\{slug\}\}/g, slug);
return template;
}
async addPropertiesToFile(file, title, type) {
const now = /* @__PURE__ */ new Date();
const dateString = window.moment(now).format(this.plugin.settings.dateFormat);
const slug = toKebabCase(title);
let template;
if (type === "note") {
const escapedTitle = this.escapeYamlString(title);
template = `---
title: ${escapedTitle}
date: ${dateString}
---
`;
} else {
const contentType = this.fileOps.getContentType(type);
if (!contentType) {
const escapedTitle = this.escapeYamlString(title);
template = `---
title: ${escapedTitle}
date: ${dateString}
---
`;
} else {
template = contentType.template;
}
}
template = template.replace(/\{\{title\}\}/g, title);
template = template.replace(/\{\{date\}\}/g, dateString);
template = template.replace(/\{\{slug\}\}/g, slug);
await this.app.vault.modify(file, template);
}
positionCursorAtEnd(file) {
const positionCursor = () => {
var _a;
const view = this.app.workspace.getActiveViewOfType(import_obsidian4.MarkdownView);
if (view && view.file === file && view.editor) {
const editor = view.editor;
const content = editor.getValue();
if (content) {
const lines = content.split("\n");
const lastLine = lines.length - 1;
const lastLineLength = ((_a = lines[lastLine]) == null ? void 0 : _a.length) || 0;
editor.setCursor({ line: lastLine, ch: lastLineLength });
editor.focus();
return true;
}
}
return false;
};
setTimeout(() => {
if (!positionCursor()) {
setTimeout(() => {
positionCursor();
}, 200);
}
}, 100);
}
escapeYamlString(str) {
if (str.includes('"') || str.includes("'") || str.includes("\n") || str.includes("\\")) {
return `'${str.replace(/'/g, "''")}'`;
} else if (str.includes(" ") || str.includes(":") || str.includes("#") || str.includes("@")) {
return `"${str.replace(/"/g, '\\"')}"`;
} else {
return str;
}
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
};
// src/commands/index.ts
function registerCommands(plugin, settings) {
const isMobile = import_obsidian5.Platform.isMobile;
if (isMobile) {
let hasMatchingContentType2 = function(file, settings2) {
const type = fileOps.determineType(file);
if (type === "note") {
return false;
}
const contentType = fileOps.getContentType(type);
return contentType !== null && contentType.enabled;
};
var hasMatchingContentType = hasMatchingContentType2;
const pluginInterface2 = plugin;
const fileOps = new FileOperations(plugin.app, settings, pluginInterface2);
const linkConverter = new LinkConverter(settings, pluginInterface2);
plugin.addCommand({
id: "standardize-properties",
name: "Standardize properties",
icon: "file-check",
editorCallback: (editor, ctx) => {
const file = ctx instanceof import_obsidian5.MarkdownView ? ctx.file : ctx.file;
if (file instanceof import_obsidian5.TFile) {
const currentSettings = pluginInterface2.settings || settings;
void standardizeProperties(plugin.app, currentSettings, file, pluginInterface2, editor);
}
}
});
plugin.addCommand({
id: "convert-wikilinks-astro",
name: "Convert internal links for astro",
icon: "link-2",
editorCallback: (editor, ctx) => {
const file = ctx instanceof import_obsidian5.MarkdownView ? ctx.file : ctx.file;
if (file instanceof import_obsidian5.TFile) {
linkConverter.convertWikilinksForAstro(editor, file);
}
}
});
plugin.addCommand({
id: "rename-content",
name: "Rename current content",
icon: "pencil",
editorCallback: (editor, ctx) => {
const file = ctx instanceof import_obsidian5.MarkdownView ? ctx.file : ctx.file;
if (file instanceof import_obsidian5.TFile) {
if (!hasMatchingContentType2(file, settings)) {
new import_obsidian5.Notice("Cannot rename: this file is not part of a configured content type folder.");
return;
}
const type = fileOps.determineType(file);
const cache = plugin.app.metadataCache.getFileCache(file);
const titleKey = fileOps.getTitleKey(type);
if (!(cache == null ? void 0 : cache.frontmatter) || !(titleKey in cache.frontmatter)) {
new import_obsidian5.Notice(`Cannot rename: No ${titleKey} found in properties`);
return;
}
new TitleModal(plugin.app, file, plugin, type, true).open();
}
}
});
return;
}
const pluginInterface = plugin;
function hasMatchingContentType(file, settings2) {
const currentSettings = (plugin == null ? void 0 : plugin.settings) || settings2;
const tempFileOps = new FileOperations(plugin.app, currentSettings, plugin);
const type = tempFileOps.determineType(file);
if (type === "note") {
return false;
}
const contentType = tempFileOps.getContentType(type);
return contentType !== null && contentType.enabled;
}
plugin.addCommand({
id: "standardize-properties",
name: "Standardize properties",
icon: "file-check",
editorCallback: (editor, ctx) => {
const file = ctx instanceof import_obsidian5.MarkdownView ? ctx.file : ctx.file;
if (file instanceof import_obsidian5.TFile) {
void standardizeProperties(plugin.app, settings, file, plugin, editor);
}
}
});
plugin.addCommand({
id: "convert-wikilinks-astro",
name: "Convert internal links for astro",
icon: "link-2",
editorCallback: (editor, ctx) => {
const file = ctx instanceof import_obsidian5.MarkdownView ? ctx.file : ctx.file;
if (file instanceof import_obsidian5.TFile) {
const currentSettings = pluginInterface.settings || settings;
const currentLinkConverter = new LinkConverter(currentSettings, pluginInterface);
currentLinkConverter.convertWikilinksForAstro(editor, file);
}
}
});
plugin.addCommand({
id: "rename-content",
name: "Rename current content",
icon: "pencil",
editorCallback: (editor, ctx) => {
const file = ctx instanceof import_obsidian5.MarkdownView ? ctx.file : ctx.file;
if (file instanceof import_obsidian5.TFile) {
const currentSettings = pluginInterface.settings || settings;
const currentFileOps = new FileOperations(plugin.app, currentSettings, pluginInterface);
if (!hasMatchingContentType(file, currentSettings)) {
new import_obsidian5.Notice("Cannot rename: this file is not part of a configured content type folder.");
return;
}
const type = currentFileOps.determineType(file);
new TitleModal(plugin.app, file, pluginInterface, type, true).open();
}
}
});
if (!isMobile) {
plugin.addCommand({
id: "open-project-terminal",
name: "Open project terminal",
icon: "terminal-square",
callback: () => {
const currentSettings = plugin.settings;
if (!currentSettings.enableOpenTerminalCommand) {
new import_obsidian5.Notice("Open terminal command is disabled. Enable it in settings to use this command.");
return;
}
openTerminalInProjectRoot(plugin.app, currentSettings);
}
});
}
if (!isMobile) {
plugin.addCommand({
id: "edit-astro-config",
name: "Edit astro config",
icon: "rocket",
callback: async () => {
const currentSettings = plugin.settings;
if (!currentSettings.enableOpenConfigFileCommand) {
new import_obsidian5.Notice("Edit config file command is disabled. Enable it in settings to use this command.");
return;
}
await openConfigFile(plugin.app, currentSettings);
}
});
}
}
async function standardizeProperties(app, settings, file, plugin, editor) {
var _a;
const currentSettings = (plugin == null ? void 0 : plugin.settings) || settings;
const templateParser = new TemplateParser(app, currentSettings);
const fileOps = new FileOperations(app, currentSettings, plugin);
let cursorPosition = null;
let originalContent = "";
if (editor) {
const cursor = editor.getCursor();
cursorPosition = { line: cursor.line, ch: cursor.ch };
originalContent = editor.getValue();
}
const type = fileOps.determineType(file);
if (type === "note") {
new import_obsidian5.Notice("No properties template specified for this content. This file doesn't match any configured content type folders.");
return;
}
let templateString;
if (type === "note") {
new import_obsidian5.Notice("No properties template specified for this content. This file doesn't match any configured content type folders.");
return;
}
const contentType = fileOps.getContentType(type);
if (!contentType) {
new import_obsidian5.Notice("Content type not found.");
return;
}
templateString = contentType.template;
await new Promise((resolve) => setTimeout(resolve, 100));
const content = await app.vault.read(file);
const title = file.basename.replace(/^_/, "");
const parsed = templateParser.parseFrontmatter(content);
const { templateProps, templateValues } = templateParser.parseTemplate(templateString, title);
const finalProps = { ...parsed.properties };
const arrayKeys = /* @__PURE__ */ new Set();
const slug = toKebabCase(title);
for (const key of templateProps) {
if (!(key in parsed.properties)) {
const templateValue = templateValues[key];
if (Array.isArray(templateValue)) {
finalProps[key] = templateValue;
arrayKeys.add(key);
} else {
finalProps[key] = [templateValue || ""];
}
} else {
const templateValue = templateValues[key];
const isArrayValue = Array.isArray(templateValue);
if (isArrayValue) {
const existingItems = parsed.properties[key] || [];
const newItems = templateValue.filter((item) => !existingItems.includes(item));
finalProps[key] = [...existingItems, ...newItems];
arrayKeys.add(key);
} else {
if (key === "slug") {
const existingSlug = parsed.properties[key][0] || "";
if (!existingSlug || existingSlug.trim() === "") {
finalProps[key] = [slug];
}
}
}
}
}
if ("slug" in parsed.properties && templateString.includes("{{slug}}")) {
const existingSlug = parsed.properties["slug"][0] || "";
if (!existingSlug || existingSlug.trim() === "") {
finalProps["slug"] = [slug];
}
}
for (const key in parsed.properties) {
if (parsed.properties[key].length > 1) {
arrayKeys.add(key);
}
}
const newContent = templateParser.buildFrontmatterContent(finalProps, arrayKeys) + parsed.bodyContent;
await app.vault.modify(file, newContent);
if (editor && cursorPosition) {
await new Promise((resolve) => setTimeout(resolve, 50));
const activeView = app.workspace.getActiveViewOfType(import_obsidian5.MarkdownView);
if (activeView && activeView.file === file && activeView.editor) {
const activeEditor = activeView.editor;
const newLineCount = newContent.split("\n").length;
const originalLineCount = originalContent.split("\n").length;
let newLine = cursorPosition.line;
let newCh = cursorPosition.ch;
if (newLineCount !== originalLineCount) {
if (newLine >= newLineCount) {
newLine = Math.max(0, newLineCount - 1);
}
}
const newLineLength = ((_a = newContent.split("\n")[newLine]) == null ? void 0 : _a.length) || 0;
if (newCh > newLineLength) {
newCh = Math.max(0, newLineLength);
}
activeEditor.setCursor({ line: newLine, ch: newCh });
}
}
new import_obsidian5.Notice("Properties standardized using template.");
}
function renameContentByPath(app, filePath, settings, plugin) {
const file = app.vault.getAbstractFileByPath(filePath);
if (!(file instanceof import_obsidian5.TFile)) {
new import_obsidian5.Notice(`File not found: ${filePath}`);
return;
}
const fileOps = new FileOperations(app, settings, plugin);
function hasMatchingContentType(file2, settings2) {
const type2 = fileOps.determineType(file2);
if (type2 === "note") {
return false;
}
const contentType = fileOps.getContentType(type2);
return contentType !== null && contentType.enabled;
}
if (!hasMatchingContentType(file, settings)) {
new import_obsidian5.Notice("Cannot rename: this file is not part of a configured content type folder.");
return;
}
const type = fileOps.determineType(file);
new TitleModal(app, file, plugin, type, true).open();
}
function registerContentTypeCommands(plugin, settings) {
const pluginInterface = plugin;
const contentTypes = settings.contentTypes || [];
for (const contentType of contentTypes) {
if (!contentType.enabled) {
continue;
}
const commandId = `create-content-type-${contentType.id}`;
const commandName = `Create new content type: ${contentType.name}`;
plugin.addCommand({
id: commandId,
name: commandName,
callback: async () => {
let targetFolder = contentType.folder || "";
if (targetFolder && targetFolder.trim() !== "") {
const folder = plugin.app.vault.getAbstractFileByPath(targetFolder);
if (!(folder instanceof import_obsidian5.TFolder)) {
try {
await plugin.app.vault.createFolder(targetFolder);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian5.Notice(`Failed to create folder: ${errorMessage}`);
return;
}
}
}
const tempFileName = "Untitled.md";
const filePath = targetFolder ? `${targetFolder}/${tempFileName}` : tempFileName;
const existingFile = plugin.app.vault.getAbstractFileByPath(filePath);
if (existingFile instanceof import_obsidian5.TFile) {
new TitleModal(plugin.app, existingFile, pluginInterface, contentType.id, false, true).open();
return;
}
if (pluginInterface && "pluginCreatedFiles" in pluginInterface) {
pluginInterface.pluginCreatedFiles.set(filePath, Date.now());
}
try {
const tempFile = await plugin.app.vault.create(filePath, "");
new TitleModal(plugin.app, tempFile, pluginInterface, contentType.id, false, true).open();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
new import_obsidian5.Notice(`Failed to create file: ${errorMessage}`);
if (pluginInterface && "pluginCreatedFiles" in pluginInterface) {
pluginInterface.pluginCreatedFiles.delete(filePath);
}
}
}
});
}
}
var terminalLogger = {
enabled: false,
setEnabled(value) {
this.enabled = value;
},
log(...args) {
if (this.enabled) {
console.debug("[astro-composer:terminal]", ...args);
}
}
};
function getDefaultTerminalApp() {
if (!import_obsidian5.Platform.isDesktopApp) {
return "";
}
if (import_obsidian5.Platform.isMacOS) {
return "Terminal";
}
if (import_obsidian5.Platform.isWin) {
try {
const os = require("os");
const release = os.release();
const majorVersion = parseInt(release.split(".")[0]);
const buildNumber = parseInt(release.split(".")[2]);
if (majorVersion > 10 || majorVersion === 10 && buildNumber >= 22e3) {
return "wt.exe";
}
} catch (e) {
}
return "cmd.exe";
}
if (import_obsidian5.Platform.isLinux) {
return "gnome-terminal";
}
return "";
}
function sanitizeTerminalApp(value) {
return value.trim();
}
function escapeDoubleQuotes(value) {
return value.replace(/"/g, '\\"');
}
function openTerminalInProjectRoot(app, settings) {
terminalLogger.setEnabled(settings.enableTerminalDebugLogging);
try {
const { exec } = require("child_process");
const path = require("path");
const fs = require("fs");
const adapter = app.vault.adapter;
const vaultPath = adapter.basePath || adapter.path;
const vaultPathString = typeof vaultPath === "string" ? vaultPath : String(vaultPath);
let projectPath;
if (settings.terminalProjectRootPath && settings.terminalProjectRootPath.trim()) {
projectPath = path.resolve(vaultPathString, settings.terminalProjectRootPath);
} else {
projectPath = vaultPathString;
}
if (!fs.existsSync(projectPath)) {
new import_obsidian5.Notice(`Project root directory not found at: ${projectPath}`);
return;
}
const configuredApp = sanitizeTerminalApp(settings.terminalApplicationName || "");
const terminalApp = configuredApp || getDefaultTerminalApp();
if (!configuredApp && !terminalApp) {
new import_obsidian5.Notice("Terminal application name is empty. Please configure it in settings.");
return;
}
const platform = process.platform;
terminalLogger.log("Opening terminal", { platform, terminalApp, projectPath });
if (platform === "win32") {
const escapedPath = projectPath.replace(/"/g, '"');
const lowerApp = terminalApp.toLowerCase();
if (lowerApp === "wt.exe" || lowerApp === "wt" || lowerApp === "windows terminal") {
exec("where wt", (error) => {
if (!error) {
const command = `start "" wt.exe -d "${escapedPath}"`;
terminalLogger.log("Windows launch (wt)", { command, projectPath });
exec(command, (execError) => {
if (execError) {
terminalLogger.log("Windows Terminal failed, falling back to cmd", { error: execError.message });
const fallbackCommand = `start "" cmd.exe /K "cd /d "${escapedPath}""`;
exec(fallbackCommand, (cmdError) => {
if (cmdError) {
new import_obsidian5.Notice(`Error opening terminal: ${cmdError.message || "Unknown error"}`);
}
});
}
});
} else {
terminalLogger.log("Windows Terminal not found, using cmd", {});
const fallbackCommand = `start "" cmd.exe /K "cd /d "${escapedPath}""`;
exec(fallbackCommand, (cmdError) => {
if (cmdError) {
new import_obsidian5.Notice(`Error opening terminal: ${cmdError.message || "Unknown error"}`);
}
});
}
});
} else if (lowerApp === "powershell" || lowerApp === "powershell.exe") {
const escapedPathForPS = projectPath.replace(/'/g, "''");
const command = `start "" powershell -NoExit -Command "Set-Location '${escapedPathForPS}';"`;
terminalLogger.log("Windows launch (powershell)", { command, projectPath });
exec(command, (error) => {
if (error) {
new import_obsidian5.Notice(`Error opening terminal: ${error.message || "Unknown error"}`);
}
});
} else if (lowerApp === "cmd.exe" || lowerApp === "cmd") {
const command = `start "" cmd.exe /K "cd /d "${escapedPath}""`;
terminalLogger.log("Windows launch (cmd)", { command, projectPath });
exec(command, (error) => {
if (error) {
new import_obsidian5.Notice(`Error opening terminal: ${error.message || "Unknown error"}`);
}
});
} else {
const command = `start "" "${terminalApp}"`;
terminalLogger.log("Windows launch (generic)", { command, terminalApp, projectPath });
exec(command, (error) => {
if (error) {
terminalLogger.log("Generic terminal failed, falling back to cmd", { error: error.message });
const fallbackCommand = `start "" cmd.exe /K "cd /d "${escapedPath}""`;
exec(fallbackCommand, (cmdError) => {
if (cmdError) {
new import_obsidian5.Notice(`Error opening terminal: ${cmdError.message || "Unknown error"}`);
}
});
}
});
}
} else if (platform === "darwin") {
const escapedApp = escapeDoubleQuotes(terminalApp);
const escapedPath = escapeDoubleQuotes(projectPath);
const command = `open -na "${escapedApp}" "${escapedPath}"`;
terminalLogger.log("macOS launch", { command, terminalApp, projectPath });
exec(command, (error) => {
if (error) {
new import_obsidian5.Notice(`Error opening terminal: ${error.message || "Unknown error"}`);
}
});
} else {
const terminals = terminalApp ? [terminalApp] : ["gnome-terminal", "konsole", "xterm"];
const projectPathEscaped = projectPath.replace(/"/g, '\\"');
const tryTerminal = (index) => {
if (index >= terminals.length) {
new import_obsidian5.Notice("No supported terminal found. Please install a terminal application or configure one in settings.");
return;
}
const currentTerminal = terminals[index];
const terminalName = currentTerminal.split(" ")[0];
exec(`which ${terminalName}`, (error) => {
if (!error) {
let command;
if (currentTerminal.includes("gnome-terminal")) {
command = `gnome-terminal --working-directory="${projectPathEscaped}"`;
} else if (currentTerminal.includes("konsole")) {
command = `konsole --workdir "${projectPathEscaped}"`;
} else {
command = `${currentTerminal} -e "cd \\"${projectPathEscaped}\\" && bash"`;
}
terminalLogger.log("Linux launch", { command, terminal: currentTerminal, projectPath });
exec(command, (execError) => {
if (execError && index < terminals.length - 1) {
terminalLogger.log("Terminal launch failed, trying next", { terminal: currentTerminal, error: execError.message });
tryTerminal(index + 1);
} else if (execError) {
new import_obsidian5.Notice(`Error opening terminal: ${execError.message || "Unknown error"}`);
}
});
} else {
terminalLogger.log("Terminal not found, trying next", { terminal: currentTerminal });
tryTerminal(index + 1);
}
});
};
tryTerminal(0);
}
} catch (error) {
terminalLogger.log("Unexpected error", { error });
new import_obsidian5.Notice(`Error opening terminal: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function openConfigFile(app, settings) {
try {
const fs = require("fs");
const path = require("path");
const { shell } = require("electron");
const adapter = app.vault.adapter;
const vaultPath = adapter.basePath || adapter.path;
const vaultPathString = typeof vaultPath === "string" ? vaultPath : String(vaultPath);
if (!settings.configFilePath || !settings.configFilePath.trim()) {
new import_obsidian5.Notice("Please specify a config file path in settings.");
return;
}
const configPath = path.resolve(vaultPathString, settings.configFilePath);
if (!fs.existsSync(configPath)) {
new import_obsidian5.Notice(`Config file not found at: ${configPath}`);
return;
}
await shell.openPath(configPath);
} catch (error) {
new import_obsidian5.Notice(`Error opening config file: ${error instanceof Error ? error.message : String(error)}`);
}
}
// src/ui/settings-tab.ts
var import_obsidian9 = require("obsidian");
// src/ui/components/CommandPickerModal.ts
var import_obsidian6 = require("obsidian");
var CommandPickerModal = class extends import_obsidian6.FuzzySuggestModal {
constructor(app, onSelect) {
super(app);
this.onSelect = onSelect;
}
getItems() {
const commandRegistry = this.app.commands;
const commandMap = /* @__PURE__ */ new Map();
if (commandRegistry && typeof commandRegistry.listCommands === "function") {
try {
const commands = commandRegistry.listCommands();
for (const command of commands) {
if (command && command.id && command.name && !commandMap.has(command.id)) {
commandMap.set(command.id, {
id: command.id,
name: command.name
});
}
}
} catch (e) {
console.warn("[Astro Composer] Error getting commands via listCommands():", e);
}
}
try {
const registry = commandRegistry == null ? void 0 : commandRegistry.commands;
if (registry && typeof registry === "object") {
const allCommands = Object.values(registry);
for (const command of allCommands) {
if (command && command.id && command.name && !commandMap.has(command.id)) {
commandMap.set(command.id, {
id: command.id,
name: command.name
});
}
}
}
} catch (e) {
console.warn("[Astro Composer] Error getting commands via registry:", e);
}
try {
const internalRegistry = commandRegistry == null ? void 0 : commandRegistry.commandRegistry;
if (internalRegistry && typeof internalRegistry === "object") {
const allCommands = Object.values(internalRegistry);
for (const command of allCommands) {
if (command && command.id && command.name && !commandMap.has(command.id)) {
commandMap.set(command.id, {
id: command.id,
name: command.name
});
}
}
}
} catch (e) {
console.warn("[Astro Composer] Error getting commands via internal registry:", e);
}
const commandOptions = Array.from(commandMap.values());
commandOptions.sort((a, b) => a.name.localeCompare(b.name));
return commandOptions;
}
getItemText(item) {
return item.name;
}
onChooseItem(item, evt) {
this.onSelect(item.id);
}
// Override to show command name only
renderSuggestion(match, el) {
const item = match.item;
el.createDiv({ cls: "suggestion-title", text: item.name });
}
};
// src/ui/components/IconPickerModal.ts
var import_obsidian7 = require("obsidian");
var getIconList = () => {
if (import_obsidian7.requireApiVersion && (0, import_obsidian7.requireApiVersion)("1.7.3") && import_obsidian7.getIconIds) {
try {
return (0, import_obsidian7.getIconIds)();
} catch (e) {
console.warn("[Astro Composer] Error getting icon IDs from Obsidian:", e);
}
}
return [
"settings-2",
"settings",
"help-circle",
"info",
"star",
"heart",
"bookmark",
"home",
"search",
"bell",
"mail",
"user",
"users",
"folder",
"file",
"file-text",
"image",
"video",
"music",
"calendar",
"clock",
"edit",
"pencil",
"trash",
"copy",
"cut",
"paste",
"download",
"upload",
"save",
"share",
"link",
"external-link",
"lock",
"unlock",
"eye",
"eye-off",
"key",
"shield",
"check",
"x",
"plus",
"minus",
"arrow-left",
"arrow-right",
"arrow-up",
"arrow-down",
"chevron-left",
"chevron-right",
"chevron-up",
"chevron-down",
"menu",
"more-horizontal",
"more-vertical",
"grid",
"list",
"layout",
"columns",
"rows",
"maximize",
"minimize",
"zoom-in",
"zoom-out",
"refresh-cw",
"play",
"pause",
"stop",
"sun",
"moon",
"cloud",
"zap",
"wand-2",
"wand",
"wand-sparkles",
"palette",
"brush",
"sliders",
"power",
"wifi",
"bluetooth",
"monitor",
"laptop",
"smartphone",
"camera",
"mic",
"headphones",
"code",
"terminal",
"terminal-square",
"github",
"gitlab",
"git-branch",
"git-commit",
"database",
"server",
"cloud-download",
"cloud-upload",
"tag",
"tags",
"flag",
"pin",
"map-pin",
"compass",
"globe",
"rocket",
"car",
"bike",
"robot",
"apple",
"windows",
"linux",
"chrome",
"firefox",
"safari",
"credit-card",
"wallet",
"coins",
"book",
"book-open",
"award",
"trophy",
"badge",
"wrench",
"tool",
"package",
"box",
"archive",
"send",
"reply",
"forward",
"mail-open",
"tag-plus",
"tag-minus",
"flag-off",
"pin-off",
"map-pin-off",
"navigation",
"map",
"earth",
"plane",
"ship",
"anchor",
"helicopter",
"drone",
"android",
"keyhole",
"keys",
"fingerprint",
"scan",
"qr-code",
"barcode",
"receipt",
"piggy-bank",
"banknote"
];
};
var LUCIDE_ICONS = getIconList().map((id) => ({
id,
name: id.replace(/^lucide-/, "").replace(/-/g, " ").replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase())
})).sort((a, b) => a.name.localeCompare(b.name));
var IconPickerModal = class extends import_obsidian7.FuzzySuggestModal {
constructor(app, onSelect) {
super(app);
this.onSelect = onSelect;
}
getItems() {
return LUCIDE_ICONS;
}
getItemText(item) {
return item.name;
}
onChooseItem(item, evt) {
const normalizedId = item.id.replace(/^lucide-/, "");
this.onSelect(normalizedId);
}
// Override to show icon preview
renderSuggestion(match, el) {
const item = match.item;
el.addClass("mod-complex");
const content = el.createDiv({ cls: "suggestion-content" });
content.createDiv({ cls: "suggestion-title", text: item.name });
const aux = el.createDiv({ cls: "suggestion-aux" });
(0, import_obsidian7.setIcon)(aux.createSpan({ cls: "suggestion-flair" }), item.id);
}
};
// src/ui/components/ConfirmModal.ts
var import_obsidian8 = require("obsidian");
var ConfirmModal = class extends import_obsidian8.Modal {
constructor(app, message, confirmText = "Confirm", cancelText = "Cancel") {
super(app);
this.message = message;
this.confirmText = confirmText;
this.cancelText = cancelText;
this.result = false;
this.resolvePromise = null;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass("astro-composer-confirm-modal");
contentEl.createEl("p", { text: this.message });
const buttonContainer = contentEl.createDiv({ cls: "modal-button-container" });
const cancelButton = buttonContainer.createEl("button", {
text: this.cancelText
});
cancelButton.onclick = () => {
this.result = false;
this.close();
};
const confirmButton = buttonContainer.createEl("button", {
text: this.confirmText,
cls: "mod-cta mod-warning"
});
confirmButton.onclick = () => {
this.result = true;
this.close();
};
}
onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.resolvePromise) {
this.resolvePromise(this.result);
}
}
async waitForResult() {
return new Promise((resolve) => {
this.resolvePromise = resolve;
this.open();
});
}
};
// src/ui/settings-tab.ts
var AstroComposerSettingTab = class extends import_obsidian9.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.icon = "lucide-pencil-line";
this.autoRenameContainer = null;
this.postsFolderContainer = null;
this.onlyAutomateContainer = null;
this.creationModeContainer = null;
this.indexFileContainer = null;
this.excludedDirsContainer = null;
this.underscorePrefixContainer = null;
this.autoInsertContainer = null;
this.pagesFieldsContainer = null;
this.pagesIndexFileContainer = null;
this.pagesOnlyAutomateContainer = null;
this.terminalCommandContainer = null;
this.configCommandContainer = null;
this.customContentTypesContainer = null;
this.terminalRibbonToggle = null;
this.configRibbonToggle = null;
this.terminalRibbonToggleComponent = null;
this.configRibbonToggleComponent = null;
this.plugin = plugin;
}
/**
* Refresh just the content types section
* More efficient than refreshing the entire settings tab
*/
refreshContentTypes() {
if (this.customContentTypesContainer) {
this.renderCustomContentTypes();
}
}
display() {
const { containerEl } = this;
containerEl.empty();
const settings = this.plugin.settings;
this.renderSettingsTab(containerEl, settings);
}
renderSettingsTab(containerEl, settings) {
var _a;
const generalGroup = new import_obsidian9.SettingGroup(containerEl);
generalGroup.addSetting((setting) => {
setting.setName("Date format").setDesc("Format for the date in properties (yyyy-mm-dd, MMMM D, yyyy, yyyy-mm-dd HH:mm)").addText(
(text) => text.setPlaceholder("YYYY-MM-DD").setValue(settings.dateFormat).onChange(async (value) => {
settings.dateFormat = value || "YYYY-MM-DD";
await this.plugin.saveSettings();
})
);
});
generalGroup.addSetting((setting) => {
setting.setName("Enable copy heading links").setDesc("Add right-click context menu option to copy heading links in various formats.").addToggle(
(toggle) => toggle.setValue(settings.enableCopyHeadingLink).onChange(async (value) => {
settings.enableCopyHeadingLink = value;
await this.plugin.saveSettings();
this.updateCopyHeadingFields();
})
);
});
generalGroup.addSetting((setting) => {
setting.setName("Default heading link format").setDesc("Choose the default format for copied heading links. Obsidian format respects your Obsidian settings for wikilink vs markdown preference. Astro link uses your link base path from above and converts the heading into kebab-case format as an anchor link").addDropdown(
(dropdown) => dropdown.addOption("obsidian", "Obsidian link").addOption("astro", "Astro link").setValue(settings.copyHeadingLinkFormat).onChange(async (value) => {
settings.copyHeadingLinkFormat = value;
await this.plugin.saveSettings();
})
);
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableCopyHeadingLink);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableCopyHeadingLink);
});
generalGroup.addSetting((setting) => {
setting.setName("Add trailing slash to links").setDesc("Add trailing slashes to all converted internal links (/about/ instead of /about).").addToggle(
(toggle) => toggle.setValue(settings.addTrailingSlashToLinks).onChange(async (value) => {
settings.addTrailingSlashToLinks = value;
await this.plugin.saveSettings();
})
);
});
generalGroup.addSetting((setting) => {
setting.setName("Process background file changes").setDesc("Automatically process new files when they're changed in the background (by Git or other plugins). Disable to prevent modal spam when files are already processed on other devices during a sync.").addToggle(
(toggle) => toggle.setValue(settings.processBackgroundFileChanges).onChange(async (value) => {
settings.processBackgroundFileChanges = value;
await this.plugin.saveSettings();
})
);
});
generalGroup.addSetting((setting) => {
setting.setName("Show MDX files in file explorer").setDesc("Make .mdx files visible in Obsidian's file explorer. Requires reload to take effect.").addToggle(
(toggle) => toggle.setValue(settings.showMdxFilesInExplorer).onChange(async (value) => {
settings.showMdxFilesInExplorer = value;
await this.plugin.saveSettings();
})
);
});
const automationGroup = new import_obsidian9.SettingGroup(containerEl).setHeading("Property automation");
automationGroup.addSetting((setting) => {
setting.setName("Auto-insert properties").setDesc("Automatically insert the properties template when creating new files.").addToggle(
(toggle) => toggle.setValue(settings.autoInsertProperties).onChange(async (value) => {
settings.autoInsertProperties = value;
await this.plugin.saveSettings();
})
);
});
automationGroup.addSetting((setting) => {
setting.setName("Rename file on title property click").setDesc("When enabled, clicking into the title property will trigger the rename file command, keeping the file slug in sync.").addToggle(
(toggle) => toggle.setValue(settings.renameOnTitleClick).onChange(async (value) => {
settings.renameOnTitleClick = value;
await this.plugin.saveSettings();
})
);
});
automationGroup.addSetting((setting) => {
setting.setName("Update date on publish").setDesc("Update 'date' property when switching from draft to published status.").addToggle(
(toggle) => toggle.setValue(settings.syncDraftDate).onChange(async (value) => {
settings.syncDraftDate = value;
await this.plugin.saveSettings();
const scrollTop = this.containerEl.scrollTop;
this.display();
this.containerEl.scrollTop = scrollTop;
})
);
});
if (settings.syncDraftDate) {
automationGroup.addSetting((setting) => {
setting.setName("Draft detection mode").setDesc("How draft status is determined. Property-based uses a boolean property (draft: true). Underscore prefix uses the file name (_my-post.md = draft).").addDropdown(
(dropdown) => dropdown.addOption("property", "Property-based").addOption("underscore-prefix", "Underscore prefix").setValue(settings.draftDetectionMode || "property").onChange(async (value) => {
var _a2;
settings.draftDetectionMode = value;
await this.plugin.saveSettings();
(_a2 = this.plugin.frontmatterService) == null ? void 0 : _a2.initializeDraftStatusMap();
const scrollTop = this.containerEl.scrollTop;
this.display();
this.containerEl.scrollTop = scrollTop;
})
);
});
if (settings.draftDetectionMode !== "underscore-prefix") {
automationGroup.addSetting((setting) => {
setting.setName("Draft property name").setDesc("The property field to use for draft status.").addText(
(text) => text.setPlaceholder("draft").setValue(settings.draftProperty || "").onChange(async (value) => {
var _a2;
settings.draftProperty = value;
await this.plugin.saveSettings();
(_a2 = this.plugin.frontmatterService) == null ? void 0 : _a2.initializeDraftStatusMap();
})
);
});
automationGroup.addSetting((setting) => {
setting.setName("Draft logic").setDesc("Whether the property value 'true' means it is a draft or published.").addDropdown(
(dropdown) => dropdown.addOption("true-is-draft", "True = Draft").addOption("false-is-draft", "True = Published").setValue(settings.draftLogic || "true-is-draft").onChange(async (value) => {
var _a2;
settings.draftLogic = value;
await this.plugin.saveSettings();
(_a2 = this.plugin.frontmatterService) == null ? void 0 : _a2.initializeDraftStatusMap();
})
);
});
}
automationGroup.addSetting((setting) => {
setting.setName("Published date property name").setDesc("The property field to update when published ('date' or 'pubDate').").addText(
(text) => text.setPlaceholder("date").setValue(settings.publishDateField || "").onChange(async (value) => {
settings.publishDateField = value;
await this.plugin.saveSettings();
})
);
});
}
const contentTypesGroup = new import_obsidian9.SettingGroup(containerEl).setHeading("Content types");
contentTypesGroup.addSetting((setting) => {
setting.settingEl.addClass("astro-composer-setting-hidden-elements");
setting.settingEl.addClass("astro-composer-setting-container-full-width");
this.customContentTypesContainer = setting.settingEl.createDiv({
cls: "custom-content-types-container astro-composer-custom-types-container-visible"
});
});
if (this.customContentTypesContainer) {
this.renderCustomContentTypes();
}
if (!import_obsidian9.Platform.isMobile) {
const developerGroup = new import_obsidian9.SettingGroup(containerEl).setHeading("Developer commands");
developerGroup.addSetting((setting) => {
setting.setName("Enable open terminal command").setDesc("Enable command to open terminal in project root directory.").addToggle(
(toggle) => toggle.setValue(settings.enableOpenTerminalCommand).onChange(async (value) => {
settings.enableOpenTerminalCommand = value;
await this.plugin.saveSettings();
this.updateTerminalCommandFields();
if (this.plugin.registerRibbonIcons) {
this.plugin.registerRibbonIcons();
}
})
);
});
this.terminalCommandContainer = containerEl.createDiv({ cls: "terminal-command-fields" });
this.terminalCommandContainer.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenTerminalCommand);
this.terminalCommandContainer.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenTerminalCommand);
developerGroup.addSetting((setting) => {
const descFragment = document.createDocumentFragment();
descFragment.createEl("div", { text: "Path relative to the Obsidian vault root folder. Use ../.. for two levels up. Leave blank to use the vault folder" });
descFragment.createEl("div", { text: "This is where the terminal will open. Absolute paths work also." });
setting.setName("Project root directory path").setDesc(descFragment).addText(
(text) => text.setPlaceholder("../..").setValue(settings.terminalProjectRootPath).onChange(async (value) => {
settings.terminalProjectRootPath = value;
await this.plugin.saveSettings();
})
);
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenTerminalCommand);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenTerminalCommand);
});
developerGroup.addSetting((setting) => {
const descFragment = document.createDocumentFragment();
descFragment.createEl("div", { text: "Leave blank to use platform defaults. On macOS, the default is Terminal. On Windows, it's Windows Terminal (Win 11) or cmd.exe (Win 10). On Linux, it's gnome-terminal, konsole, or xterm" });
descFragment.createEl("div", { text: "Examples include Terminal, iTerm, PowerShell, and Alacritty" });
setting.setName("Terminal application name").setDesc(descFragment).addText(
(text) => text.setPlaceholder("Terminal").setValue(settings.terminalApplicationName).onChange(async (value) => {
settings.terminalApplicationName = value;
await this.plugin.saveSettings();
})
);
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenTerminalCommand);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenTerminalCommand);
});
developerGroup.addSetting((setting) => {
setting.setName("Enable debug logging").setDesc("Log terminal launch commands and platform decisions to the developer console for troubleshooting.").addToggle(
(toggle) => toggle.setValue(settings.enableTerminalDebugLogging).onChange(async (value) => {
settings.enableTerminalDebugLogging = value;
await this.plugin.saveSettings();
})
);
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenTerminalCommand);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenTerminalCommand);
});
developerGroup.addSetting((setting) => {
setting.setName("Show open terminal ribbon icon").setDesc("Add a ribbon icon to launch the terminal command.").addToggle((toggle) => {
this.terminalRibbonToggleComponent = toggle;
toggle.setValue(settings.enableTerminalRibbonIcon).setDisabled(!settings.enableOpenTerminalCommand).onChange(async (value) => {
this.plugin.settings.enableTerminalRibbonIcon = value;
settings.enableTerminalRibbonIcon = value;
await this.plugin.saveSettings();
setTimeout(() => {
if (this.plugin.registerRibbonIcons) {
this.plugin.registerRibbonIcons();
}
}, 50);
});
});
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenTerminalCommand);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenTerminalCommand);
this.terminalRibbonToggle = setting;
});
developerGroup.addSetting((setting) => {
setting.setName("Enable edit config file command").setDesc("Enable command to open astro config file in default editor.").addToggle(
(toggle) => toggle.setValue(settings.enableOpenConfigFileCommand).onChange(async (value) => {
settings.enableOpenConfigFileCommand = value;
await this.plugin.saveSettings();
this.updateConfigCommandFields();
if (this.plugin.registerRibbonIcons) {
this.plugin.registerRibbonIcons();
}
})
);
});
this.configCommandContainer = containerEl.createDiv({ cls: "config-command-fields" });
this.configCommandContainer.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenConfigFileCommand);
this.configCommandContainer.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenConfigFileCommand);
developerGroup.addSetting((setting) => {
const descFragment = document.createDocumentFragment();
descFragment.createEl("div", { text: "Path to the config file relative to the vault root. Use ../config.ts or ../../astro.config.mjs." });
descFragment.createEl("div", { text: "Absolute paths work also." });
setting.setName("Config file path").setDesc(descFragment).addText(
(text) => text.setPlaceholder("../config.ts").setValue(settings.configFilePath).onChange(async (value) => {
settings.configFilePath = value;
await this.plugin.saveSettings();
})
);
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenConfigFileCommand);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenConfigFileCommand);
});
developerGroup.addSetting((setting) => {
setting.setName("Show open config ribbon icon").setDesc("Add a ribbon icon to launch the config file command.").addToggle((toggle) => {
this.configRibbonToggleComponent = toggle;
toggle.setValue(settings.enableConfigRibbonIcon).setDisabled(!settings.enableOpenConfigFileCommand).onChange(async (value) => {
this.plugin.settings.enableConfigRibbonIcon = value;
settings.enableConfigRibbonIcon = value;
await this.plugin.saveSettings();
setTimeout(() => {
if (this.plugin.registerRibbonIcons) {
this.plugin.registerRibbonIcons();
}
}, 50);
});
});
setting.settingEl.classList.toggle("astro-composer-setting-container-visible", settings.enableOpenConfigFileCommand);
setting.settingEl.classList.toggle("astro-composer-setting-container-hidden", !settings.enableOpenConfigFileCommand);
this.configRibbonToggle = setting;
});
developerGroup.addSetting((setting) => {
setting.setName("Swap out help button for custom action").setDesc("Replace the help button in the vault profile area with a custom action.").addToggle((toggle) => {
var _a2, _b;
return toggle.setValue((_b = (_a2 = settings.helpButtonReplacement) == null ? void 0 : _a2.enabled) != null ? _b : false).onChange(async (value) => {
if (!settings.helpButtonReplacement) {
settings.helpButtonReplacement = {
enabled: false,
commandId: "edit-astro-config",
iconId: "rocket"
};
}
settings.helpButtonReplacement.enabled = value;
await this.plugin.saveSettings();
if (this.plugin.updateHelpButton) {
await this.plugin.updateHelpButton();
}
this.display();
});
});
});
if ((_a = settings.helpButtonReplacement) == null ? void 0 : _a.enabled) {
const commandName = this.getCommandName(settings.helpButtonReplacement.commandId);
developerGroup.addSetting((setting) => {
setting.setName("Command").setDesc("Select the command to execute when the button is clicked.").addButton((button) => button.setButtonText(commandName || "Select command").onClick(() => {
const modal = new CommandPickerModal(this.app, (commandId) => {
void (async () => {
if (!settings.helpButtonReplacement) {
settings.helpButtonReplacement = {
enabled: true,
commandId: "edit-astro-config",
iconId: "rocket"
};
}
settings.helpButtonReplacement.commandId = commandId;
await this.plugin.saveSettings();
if (this.plugin.updateHelpButton) {
await this.plugin.updateHelpButton();
}
this.display();
})();
});
modal.open();
}));
});
const iconName = this.getIconName(settings.helpButtonReplacement.iconId);
developerGroup.addSetting((setting) => {
setting.setName("Icon").setDesc("Select the icon to display on the button.").addButton((button) => button.setButtonText(iconName || "Select icon...").onClick(() => {
const modal = new IconPickerModal(this.app, (iconId) => {
void (async () => {
if (!settings.helpButtonReplacement) {
settings.helpButtonReplacement = {
enabled: true,
commandId: "edit-astro-config",
iconId: "rocket"
};
}
settings.helpButtonReplacement.iconId = iconId;
await this.plugin.saveSettings();
if (this.plugin.updateHelpButton) {
await this.plugin.updateHelpButton();
}
this.display();
})();
});
modal.open();
}));
});
}
}
this.updateCopyHeadingFields();
if (!import_obsidian9.Platform.isMobile) {
this.updateTerminalCommandFields();
this.updateConfigCommandFields();
}
}
updateCopyHeadingFields() {
const settings = this.plugin.settings;
const isVisible = settings.enableCopyHeadingLink;
const containerEl = this.containerEl;
const allSettings = containerEl.querySelectorAll(".setting-item");
allSettings.forEach((settingEl) => {
var _a;
const nameEl = settingEl.querySelector(".setting-item-name");
if (nameEl && ((_a = nameEl.textContent) == null ? void 0 : _a.trim()) === "Default heading link format") {
settingEl.classList.toggle("astro-composer-setting-container-visible", isVisible);
settingEl.classList.toggle("astro-composer-setting-container-hidden", !isVisible);
}
});
}
updateTerminalCommandFields() {
const settings = this.plugin.settings;
const isVisible = settings.enableOpenTerminalCommand;
if (this.terminalCommandContainer) {
this.terminalCommandContainer.classList.toggle("astro-composer-setting-container-visible", isVisible);
this.terminalCommandContainer.classList.toggle("astro-composer-setting-container-hidden", !isVisible);
}
const containerEl = this.containerEl;
const allSettings = containerEl.querySelectorAll(".setting-item");
allSettings.forEach((settingEl) => {
var _a;
const nameEl = settingEl.querySelector(".setting-item-name");
if (nameEl) {
const name = (_a = nameEl.textContent) == null ? void 0 : _a.trim();
if (name === "Project root directory path" || name === "Show open terminal ribbon icon") {
settingEl.classList.toggle("astro-composer-setting-container-visible", isVisible);
settingEl.classList.toggle("astro-composer-setting-container-hidden", !isVisible);
}
}
});
if (this.terminalRibbonToggleComponent) {
this.terminalRibbonToggleComponent.setDisabled(!this.plugin.settings.enableOpenTerminalCommand);
}
}
updateConfigCommandFields() {
const settings = this.plugin.settings;
const isVisible = settings.enableOpenConfigFileCommand;
if (this.configCommandContainer) {
this.configCommandContainer.classList.toggle("astro-composer-setting-container-visible", isVisible);
this.configCommandContainer.classList.toggle("astro-composer-setting-container-hidden", !isVisible);
}
const containerEl = this.containerEl;
const allSettings = containerEl.querySelectorAll(".setting-item");
allSettings.forEach((settingEl) => {
var _a;
const nameEl = settingEl.querySelector(".setting-item-name");
if (nameEl) {
const name = (_a = nameEl.textContent) == null ? void 0 : _a.trim();
if (name === "Config file path" || name === "Show open config ribbon icon") {
settingEl.classList.toggle("astro-composer-setting-container-visible", isVisible);
settingEl.classList.toggle("astro-composer-setting-container-hidden", !isVisible);
}
}
});
if (this.configRibbonToggleComponent) {
this.configRibbonToggleComponent.setDisabled(!this.plugin.settings.enableOpenConfigFileCommand);
}
}
checkForFolderConflicts() {
const settings = this.plugin.settings;
const blankFolders = [];
const folderConflicts = {};
const contentTypes = settings.contentTypes || [];
for (const contentType of contentTypes) {
if (contentType.enabled) {
if (!contentType.folder || contentType.folder.trim() === "") {
blankFolders.push(contentType.name || "Content");
} else {
if (!folderConflicts[contentType.folder]) {
folderConflicts[contentType.folder] = [];
}
folderConflicts[contentType.folder].push(contentType.name || "Content");
}
}
}
}
addCustomContentType() {
const settings = this.plugin.settings;
const contentTypes = settings.contentTypes || [];
const newType = {
id: `content-${Date.now()}`,
name: `Content ${contentTypes.length + 1}`,
folder: "",
linkBasePath: "",
template: '---\ntitle: "{{title}}"\ndate: {{date}}\n---\n',
enabled: true,
creationMode: "file",
indexFileName: "",
ignoreSubfolders: false,
enableUnderscorePrefix: false,
useMdxExtension: false,
modifiedDateField: ""
};
contentTypes.push(newType);
settings.contentTypes = contentTypes;
void this.plugin.saveSettings();
this.renderCustomContentTypes();
this.plugin.registerCreateEvent();
registerContentTypeCommands(this.plugin, settings);
}
renderCustomContentTypes() {
if (!this.customContentTypesContainer) return;
this.customContentTypesContainer.empty();
const settings = this.plugin.settings;
const contentTypes = settings.contentTypes || [];
contentTypes.forEach((customType, index) => {
var _a, _b;
if (!this.customContentTypesContainer) return;
const typeContainer = this.customContentTypesContainer.createDiv({
cls: "custom-content-type-item",
attr: { "data-type-id": customType.id }
});
const header = typeContainer.createDiv({ cls: "custom-content-type-header" });
header.classList.add("astro-composer-custom-type-header");
const collapseButton = header.createEl("button", {
cls: "astro-composer-collapse-button",
attr: { "aria-label": "Collapse/expand" }
});
const isCollapsed = (_a = customType.collapsed) != null ? _a : false;
(0, import_obsidian9.setIcon)(collapseButton, "chevron-down");
if (isCollapsed) {
collapseButton.classList.add("is-collapsed");
}
collapseButton.addEventListener("click", () => {
void this.toggleContentTypeCollapse(customType.id);
const updatedType = this.plugin.settings.contentTypes.find((ct) => ct.id === customType.id);
if (updatedType) {
if (updatedType.collapsed) {
collapseButton.classList.add("is-collapsed");
} else {
collapseButton.classList.remove("is-collapsed");
}
}
});
const headerName = header.createDiv({ cls: "astro-composer-header-name" });
headerName.createEl("div", { text: customType.name || `Content ${index + 1}`, cls: "setting-item-name" });
const reorderContainer = header.createDiv({ cls: "astro-composer-reorder-buttons" });
const upButton = reorderContainer.createEl("button", {
cls: "astro-composer-reorder-button",
attr: { "aria-label": "Move up" }
});
(0, import_obsidian9.setIcon)(upButton, "chevron-up");
upButton.disabled = index === 0;
upButton.addEventListener("click", () => {
void this.moveContentTypeUp(customType.id);
});
const downButton = reorderContainer.createEl("button", {
cls: "astro-composer-reorder-button",
attr: { "aria-label": "Move down" }
});
(0, import_obsidian9.setIcon)(downButton, "chevron-down");
downButton.disabled = index === contentTypes.length - 1;
downButton.addEventListener("click", () => {
void this.moveContentTypeDown(customType.id);
});
const toggleContainer = header.createDiv({ cls: "checkbox-container" });
if (customType.enabled) {
toggleContainer.classList.add("is-enabled");
}
const toggle = toggleContainer.createEl("input", { type: "checkbox", cls: "checkbox-input" });
toggle.checked = customType.enabled;
toggleContainer.addEventListener("click", (e) => {
void (async () => {
e.preventDefault();
const newValue = !customType.enabled;
customType.enabled = newValue;
toggle.checked = newValue;
await this.plugin.saveSettings();
this.plugin.registerCreateEvent();
if (newValue) {
toggleContainer.classList.add("is-enabled");
} else {
toggleContainer.classList.remove("is-enabled");
}
this.updateCustomContentTypeVisibility(customType.id, newValue);
registerContentTypeCommands(this.plugin, this.plugin.settings);
})();
});
toggle.addEventListener("change", (e) => {
void (async () => {
const value = e.target.checked;
customType.enabled = value;
await this.plugin.saveSettings();
this.plugin.registerCreateEvent();
if (value) {
toggleContainer.classList.add("is-enabled");
} else {
toggleContainer.classList.remove("is-enabled");
}
this.updateCustomContentTypeVisibility(customType.id, value);
registerContentTypeCommands(this.plugin, this.plugin.settings);
})();
});
const settingsContainer = typeContainer.createDiv({
cls: "custom-content-type-settings",
attr: { "data-type-id": customType.id }
});
const initiallyCollapsed = (_b = customType.collapsed) != null ? _b : false;
const initiallyVisible = customType.enabled && !initiallyCollapsed;
if (initiallyVisible) {
settingsContainer.classList.add("astro-composer-setting-container-visible");
} else {
settingsContainer.classList.add("astro-composer-setting-container-hidden");
}
const nameContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(nameContainer).setName("Content type name").setDesc("Display name for this content type ('projects', 'notes', 'tutorials')").addText((text) => {
text.setPlaceholder("Enter content type name").setValue(customType.name).onChange(async (value) => {
customType.name = value;
await this.plugin.saveSettings();
registerContentTypeCommands(this.plugin, this.plugin.settings);
});
});
const folderContainer = settingsContainer.createDiv();
const folderSetting = new import_obsidian9.Setting(folderContainer).setName("Folder location").setDesc("Folder path where this content type will be created. Leave blank to use the vault folder. Supports wildcards like directory/* or directory/*/* to match specific folder depths.").addText((text) => {
text.setPlaceholder("Enter folder path ('docs', 'docs/*', 'docs/*/*') or leave blank for vault root").setValue(customType.folder).onChange(async (value) => {
customType.folder = value;
await this.plugin.saveSettings();
this.plugin.registerCreateEvent();
this.updateCustomContentTypeIgnoreSubfoldersField(customType.id);
const allContentTypes = this.plugin.settings.contentTypes || [];
for (const ct of allContentTypes) {
this.updateFolderConflictWarning(ct.id, null);
}
});
});
folderContainer.createDiv({ cls: "astro-composer-conflict-warning hidden", attr: { "data-type-id": customType.id } });
this.updateFolderConflictWarning(customType.id, folderSetting);
const ignoreSubfoldersContainer = settingsContainer.createDiv({ cls: "custom-ignore-subfolders-field" });
ignoreSubfoldersContainer.setAttribute("data-type-id", customType.id);
ignoreSubfoldersContainer.classList.toggle("astro-composer-setting-container-visible", !!customType.folder);
ignoreSubfoldersContainer.classList.toggle("astro-composer-setting-container-hidden", !customType.folder);
new import_obsidian9.Setting(ignoreSubfoldersContainer).setName("Ignore subfolders").setDesc("When enabled, automation will only trigger for new .md files within this content type's folder and one level down (for folder-based content). Files in deeper subfolders will be ignored.").addToggle(
(toggle2) => toggle2.setValue(customType.ignoreSubfolders || false).onChange(async (value) => {
customType.ignoreSubfolders = value;
await this.plugin.saveSettings();
})
);
const underscorePrefixContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(underscorePrefixContainer).setName("Use underscore prefix for drafts").setDesc("Add an underscore prefix (_content-title) to new notes by default when enabled. This hides them from astro, which can be helpful for drafts").addToggle(
(toggle2) => toggle2.setValue(customType.enableUnderscorePrefix || false).onChange(async (value) => {
customType.enableUnderscorePrefix = value;
await this.plugin.saveSettings();
})
);
const linkContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(linkContainer).setName("Link base path").setDesc("Base path for converted links ('/projects/', '/notes/tutorials/', leave blank for root /).").addText((text) => {
text.setPlaceholder("Enter link base path").setValue(customType.linkBasePath || "").onChange(async (value) => {
customType.linkBasePath = value;
await this.plugin.saveSettings();
});
});
const creationModeContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(creationModeContainer).setName("Creation mode").setDesc("How to create new entries: file-based or folder-based with an index file.").addDropdown(
(dropdown) => dropdown.addOption("file", "File-based (content-title.md)").addOption("folder", "Folder-based (content-title/index.md)").setValue(customType.creationMode).onChange(async (value) => {
customType.creationMode = value;
await this.plugin.saveSettings();
this.updateCustomContentTypeIndexFileField(customType.id);
})
);
const indexFileContainer = settingsContainer.createDiv({ cls: "custom-index-file-field" });
indexFileContainer.classList.toggle("astro-composer-setting-container-visible", customType.creationMode === "folder");
indexFileContainer.classList.toggle("astro-composer-setting-container-hidden", customType.creationMode !== "folder");
new import_obsidian9.Setting(indexFileContainer).setName("Index file name").setDesc("Name for index files in folder-based content (without .md extension). Defaults to 'index' if left blank.").addText(
(text) => text.setPlaceholder("index").setValue(customType.indexFileName).onChange(async (value) => {
customType.indexFileName = value;
await this.plugin.saveSettings();
})
);
const useMdxContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(useMdxContainer).setName("Use MDX instead of MD").setDesc("Create files with .mdx extension instead of .md extension.").addToggle(
(toggle2) => toggle2.setValue(customType.useMdxExtension || false).onChange(async (value) => {
customType.useMdxExtension = value;
await this.plugin.saveSettings();
})
);
const modifiedDateContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(modifiedDateContainer).setName("Modified date property").setDesc("The property field to update with the modified date for this content type. Leave blank to disable.").addText(
(text) => text.setPlaceholder("modified").setValue(customType.modifiedDateField || "").onChange(async (value) => {
customType.modifiedDateField = value;
await this.plugin.saveSettings();
})
);
const templateContainer = settingsContainer.createDiv();
new import_obsidian9.Setting(templateContainer).setName("Properties template").addTextArea((text) => {
text.setPlaceholder('---\ntitle: "{{title}}"\ndate: {{date}}\n---\n').setValue(customType.template).onChange(async (value) => {
customType.template = value;
await this.plugin.saveSettings();
});
text.inputEl.classList.add("astro-composer-template-textarea");
return text;
}).then((setting) => {
setting.descEl.empty();
const descDiv = setting.descEl.createEl("div");
descDiv.createEl("div", { text: "Template for new files of this content type." });
descDiv.createEl("div", { text: "Variables include {{title}}, {{date}}, and {{slug}}." });
descDiv.createEl("div", { text: "Do not wrap {{date}} in quotes as it represents a datetime value, not a string." });
});
const removeContainer = settingsContainer.createDiv();
const removeSetting = new import_obsidian9.Setting(removeContainer).setName("").addButton((button) => {
button.setButtonText("Remove").setWarning().onClick(async () => {
const contentType = this.plugin.settings.contentTypes.find((ct) => ct.id === customType.id);
const typeName = (contentType == null ? void 0 : contentType.name) || "content type";
const modal = new ConfirmModal(
this.app,
`Are you sure you want to remove "${typeName}"? This action cannot be undone.`,
"Remove",
"Cancel"
);
const confirmed = await modal.waitForResult();
if (confirmed) {
await this.removeCustomContentType(customType.id);
}
});
});
removeSetting.settingEl.classList.add("astro-composer-remove-setting");
this.updateCustomContentTypeVisibility(customType.id, customType.enabled);
});
contentTypes.forEach((customType) => {
this.updateFolderConflictWarning(customType.id, null);
});
const addButtonContainer = this.customContentTypesContainer.createDiv({ cls: "astro-composer-add-button-container" });
const addButton = addButtonContainer.createEl("button", {
cls: "mod-cta",
text: "Add content type"
});
addButton.addEventListener("click", () => {
this.addCustomContentType();
});
}
updateCustomContentTypeVisibility(typeId, enabled) {
var _a, _b;
const settingsContainer = (_a = this.customContentTypesContainer) == null ? void 0 : _a.querySelector(`[data-type-id="${typeId}"].custom-content-type-settings`);
if (settingsContainer) {
const contentTypes = this.plugin.settings.contentTypes || [];
const contentType = contentTypes.find((ct) => ct.id === typeId);
const isCollapsed = (_b = contentType == null ? void 0 : contentType.collapsed) != null ? _b : false;
const shouldBeVisible = enabled && !isCollapsed;
settingsContainer.classList.toggle("astro-composer-setting-container-visible", shouldBeVisible);
settingsContainer.classList.toggle("astro-composer-setting-container-hidden", !shouldBeVisible);
}
}
updateCustomContentTypeIndexFileField(typeId) {
var _a;
const contentTypes = this.plugin.settings.contentTypes || [];
const customType = contentTypes.find((type) => type.id === typeId);
if (!customType) return;
const indexFileContainer = (_a = this.customContentTypesContainer) == null ? void 0 : _a.querySelector(`[data-type-id="${typeId}"] .custom-index-file-field`);
if (indexFileContainer) {
indexFileContainer.classList.toggle("astro-composer-setting-container-visible", customType.creationMode === "folder");
indexFileContainer.classList.toggle("astro-composer-setting-container-hidden", customType.creationMode !== "folder");
}
}
updateCustomContentTypeIgnoreSubfoldersField(typeId) {
var _a;
const contentTypes = this.plugin.settings.contentTypes || [];
const customType = contentTypes.find((type) => type.id === typeId);
if (!customType) return;
const ignoreSubfoldersContainer = (_a = this.customContentTypesContainer) == null ? void 0 : _a.querySelector(`[data-type-id="${typeId}"].custom-ignore-subfolders-field`);
if (ignoreSubfoldersContainer) {
ignoreSubfoldersContainer.classList.toggle("astro-composer-setting-container-visible", !!customType.folder && customType.folder.trim() !== "");
ignoreSubfoldersContainer.classList.toggle("astro-composer-setting-container-hidden", !customType.folder || customType.folder.trim() === "");
}
}
updateFolderConflictWarning(typeId, setting) {
var _a;
const contentTypes = this.plugin.settings.contentTypes || [];
const currentType = contentTypes.find((type) => type.id === typeId);
if (!currentType) return;
const conflictWarningEl = (_a = this.customContentTypesContainer) == null ? void 0 : _a.querySelector(`[data-type-id="${typeId}"].astro-composer-conflict-warning`);
if (!conflictWarningEl) return;
const currentFolder = (currentType.folder || "").trim();
const conflictingTypes = [];
for (const otherType of contentTypes) {
if (otherType.id === typeId || !otherType.enabled) continue;
const otherFolder = (otherType.folder || "").trim();
if (currentFolder === "" && otherFolder === "") {
conflictingTypes.push(otherType.name || "Unnamed");
} else if (currentFolder === otherFolder && currentFolder !== "") {
conflictingTypes.push(otherType.name || "Unnamed");
}
}
if (conflictingTypes.length > 0) {
conflictWarningEl.removeClass("hidden");
conflictWarningEl.textContent = `Conflict: ${conflictingTypes.join(", ")} also use${conflictingTypes.length === 1 ? "s" : ""} this folder. More specific patterns will take priority.`;
} else {
conflictWarningEl.addClass("hidden");
}
}
async moveContentTypeUp(typeId) {
const settings = this.plugin.settings;
const contentTypes = settings.contentTypes || [];
const currentIndex = contentTypes.findIndex((ct) => ct.id === typeId);
if (currentIndex <= 0) return;
[contentTypes[currentIndex], contentTypes[currentIndex - 1]] = [contentTypes[currentIndex - 1], contentTypes[currentIndex]];
settings.contentTypes = contentTypes;
await this.plugin.saveSettings();
this.renderCustomContentTypes();
}
async moveContentTypeDown(typeId) {
const settings = this.plugin.settings;
const contentTypes = settings.contentTypes || [];
const currentIndex = contentTypes.findIndex((ct) => ct.id === typeId);
if (currentIndex < 0 || currentIndex >= contentTypes.length - 1) return;
[contentTypes[currentIndex], contentTypes[currentIndex + 1]] = [contentTypes[currentIndex + 1], contentTypes[currentIndex]];
settings.contentTypes = contentTypes;
await this.plugin.saveSettings();
this.renderCustomContentTypes();
}
async toggleContentTypeCollapse(typeId) {
const settings = this.plugin.settings;
const contentTypes = settings.contentTypes || [];
const contentType = contentTypes.find((ct) => ct.id === typeId);
if (!contentType) return;
contentType.collapsed = !contentType.collapsed;
await this.plugin.saveSettings();
this.updateCustomContentTypeVisibility(typeId, contentType.enabled);
}
async removeCustomContentType(typeId) {
const settings = this.plugin.settings;
const contentTypes = settings.contentTypes || [];
settings.contentTypes = contentTypes.filter((ct) => ct.id !== typeId);
await this.plugin.saveSettings();
this.renderCustomContentTypes();
this.plugin.registerCreateEvent();
registerContentTypeCommands(this.plugin, settings);
}
getCommandName(commandId) {
if (!commandId) return "";
try {
const commandRegistry = this.app.commands;
if (commandRegistry && typeof commandRegistry.listCommands === "function") {
try {
const allCommands = commandRegistry.listCommands();
const command = allCommands.find((cmd) => cmd.id === commandId);
if (command == null ? void 0 : command.name) {
return command.name;
}
} catch (e) {
console.warn("[Astro Composer] Error getting command name via listCommands():", e);
}
}
try {
const registry = commandRegistry == null ? void 0 : commandRegistry.commands;
if (registry && typeof registry === "object") {
const command = registry[commandId];
if (command == null ? void 0 : command.name) {
return command.name;
}
}
} catch (e) {
console.warn("[Astro Composer] Error getting command name via registry:", e);
}
} catch (e) {
console.warn("[Astro Composer] Error getting command name:", e);
}
return "";
}
getIconName(iconId) {
if (!iconId) return "";
return iconId.replace(/^lucide-/, "").split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
}
};
// src/utils/heading-link-generator.ts
var HeadingLinkGenerator = class {
constructor(settings, plugin) {
this.settings = settings;
this.plugin = plugin;
}
// Get fresh settings from plugin if available, otherwise use stored settings
getSettings() {
var _a;
if ((_a = this.plugin) == null ? void 0 : _a.settings) {
return this.plugin.settings;
}
return this.settings;
}
/**
* Converts text to kebab-case slug for URLs
*/
// Local toKebabCase removed, using imported one instead
/**
* Gets the Astro-compatible URL from an internal link (copied from LinkConverter)
*/
getAstroUrlFromInternalLink(link) {
const hashIndex = link.indexOf("#");
let path = hashIndex >= 0 ? link.slice(0, hashIndex) : link;
const anchor = hashIndex >= 0 ? link.slice(hashIndex) : "";
path = path.replace(/\.md$/, "");
let basePath = "";
let contentFolder = "";
let creationMode = "file";
let indexFileName = "";
const settings = this.getSettings();
const contentTypes = settings.contentTypes || [];
const sortedTypes = sortByPatternSpecificity(contentTypes);
for (const contentType of sortedTypes) {
if (!contentType.enabled) continue;
let matches = false;
if (!contentType.folder || contentType.folder.trim() === "") {
if (!path.includes("/") || path.split("/").length === 1) {
matches = true;
}
} else if (matchesFolderPattern(path, contentType.folder)) {
matches = true;
}
if (matches) {
contentFolder = contentType.folder || "";
basePath = contentType.linkBasePath || "";
creationMode = contentType.creationMode;
indexFileName = contentType.indexFileName || "";
break;
}
}
if (contentFolder) {
path = path.slice(contentFolder.length + 1);
}
let addTrailingSlash = false;
if (indexFileName && indexFileName.trim() !== "") {
const parts = path.split("/");
if (parts[parts.length - 1] === indexFileName) {
parts.pop();
path = parts.join("/");
addTrailingSlash = true;
}
} else if (creationMode === "folder") {
const defaultIndexName = "index";
const parts = path.split("/");
if (parts[parts.length - 1] === defaultIndexName) {
parts.pop();
path = parts.join("/");
addTrailingSlash = true;
}
}
const slugParts = path.split("/").map((part) => toKebabCase(part));
const slug = slugParts.join("/");
if (basePath) {
if (!basePath.startsWith("/")) basePath = "/" + basePath;
if (!basePath.endsWith("/")) basePath += "/";
}
const shouldAddTrailingSlash = (settings.addTrailingSlashToLinks || addTrailingSlash) && !anchor;
return `${basePath}${slug}${shouldAddTrailingSlash ? "/" : ""}${anchor}`;
}
/**
* Generates a standard Obsidian link to a heading, respecting user's link format preference
*/
generateObsidianLink(app, file, heading) {
const headingText = heading.heading;
const testLink = app.fileManager.generateMarkdownLink(file, "", "");
if (testLink.startsWith("[[")) {
const fileName = file.basename;
return `[[${fileName}#${headingText}|${headingText}]]`;
} else {
const baseLink = app.fileManager.generateMarkdownLink(file, "", "");
if (baseLink.startsWith("[[")) {
const fileName = file.basename;
return `[[${fileName}#${headingText}|${headingText}]]`;
} else {
const match = baseLink.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (match) {
const [, , path] = match;
return `[${headingText}](${path}#${encodeURIComponent(headingText)})`;
} else {
const encodedFilename = encodeURIComponent(file.name);
return `[${headingText}](${encodedFilename}#${encodeURIComponent(headingText)})`;
}
}
}
}
/**
* Generates a standard Obsidian wikilink to a heading
*/
generateObsidianWikilink(file, heading) {
const headingText = heading.heading;
const fileName = file.basename;
return `[[${fileName}#${headingText}|${headingText}]]`;
}
/**
* Generates an Astro-compatible markdown link to a heading
*/
generateAstroLink(file, heading) {
const headingText = heading.heading;
const anchor = toKebabCase(headingText);
const internalLink = `${file.path}#${anchor}`;
const astroUrl = this.getAstroUrlFromInternalLink(internalLink);
return `[${headingText}](${astroUrl})`;
}
/**
* Generates an Astro-compatible wikilink to a heading
*/
generateAstroWikilink(file, heading) {
const headingText = heading.heading;
const anchor = toKebabCase(headingText);
const internalLink = `${file.path}#${anchor}`;
const astroUrl = this.getAstroUrlFromInternalLink(internalLink);
return `[[${headingText}|${astroUrl}]]`;
}
/**
* Extracts the URL from a markdown link or wikilink
*/
extractUrl(link) {
const markdownMatch = link.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (markdownMatch) {
return markdownMatch[2];
}
const wikilinkMatch = link.match(/\[\[([^\]]+)\]\]/);
if (wikilinkMatch) {
const content = wikilinkMatch[1];
const pathPart = content.split("|")[0];
return pathPart;
}
return link;
}
/**
* Generates the appropriate link format based on settings
*/
generateLink(app, file, heading) {
const settings = this.getSettings();
if (settings.copyHeadingLinkFormat === "astro") {
return this.generateAstroLink(file, heading);
} else {
return this.generateObsidianLink(app, file, heading);
}
}
/**
* Finds the heading at a specific line in a file
*/
findHeadingAtLine(app, file, line) {
const cache = app.metadataCache.getFileCache(file);
if (!cache || !cache.headings) {
return null;
}
for (let i = cache.headings.length - 1; i >= 0; i--) {
const heading = cache.headings[i];
if (heading.position.start.line <= line) {
return heading;
}
}
return null;
}
};
// src/services/MigrationService.ts
var import_obsidian11 = require("obsidian");
// src/ui/components/MigrationModal.ts
var import_obsidian10 = require("obsidian");
var MigrationModal = class extends import_obsidian10.Modal {
constructor(app, conflicts) {
super(app);
this.result = null;
this.resolvePromise = null;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass("astro-composer-migration-modal");
contentEl.createEl("h2", { text: "Migration conflict detected" });
contentEl.createEl("p", {
text: "You have existing content types with names that conflict with posts or pages. How would you like to proceed?"
});
const conflictList = contentEl.createEl("ul");
conflictList.createEl("li", { text: "Skip migration: keep your existing posts/pages settings (they will be ignored)" });
conflictList.createEl("li", { text: "Migrate with renamed types: create 'posts (migrated)' and 'pages (migrated)' content types" });
const buttonContainer = contentEl.createDiv({ cls: "modal-button-container" });
const skipButton = buttonContainer.createEl("button", {
text: "Skip migration",
cls: "mod-cta"
});
skipButton.onclick = () => {
this.result = { action: "skip" };
this.close();
};
const migrateButton = buttonContainer.createEl("button", {
text: "Migrate with renamed types",
cls: "mod-cta"
});
migrateButton.onclick = () => {
this.result = { action: "migrate" };
this.close();
};
}
onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.resolvePromise && this.result) {
this.resolvePromise(this.result);
}
}
async waitForResult() {
return new Promise((resolve) => {
this.resolvePromise = resolve;
this.open();
});
}
};
// src/services/MigrationService.ts
var MigrationService = class {
constructor(app, plugin) {
this.app = app;
this.plugin = plugin;
}
/**
* Migrate old posts/pages settings to unified content types
*/
async migrateSettingsIfNeeded() {
const settings = this.plugin.settings;
if (settings.migrationCompleted) {
return;
}
const hasPostsSettings = settings.automatePostCreation !== void 0 && settings.automatePostCreation;
const hasPagesSettings = settings.enablePages !== void 0 && settings.enablePages;
if (!hasPostsSettings && !hasPagesSettings) {
settings.migrationCompleted = true;
await this.plugin.saveSettings();
return;
}
const legacyContentTypes = settings.customContentTypes;
const existingContentTypes = settings.contentTypes || legacyContentTypes || [];
const conflicts = [];
if (existingContentTypes.some((ct) => ct.name === "Posts")) {
conflicts.push("Posts");
}
if (existingContentTypes.some((ct) => ct.name === "Pages")) {
conflicts.push("Pages");
}
let shouldMigrate = true;
if (conflicts.length > 0) {
await new Promise((resolve) => {
setTimeout(() => {
void (async () => {
try {
const modal = new MigrationModal(this.app, conflicts);
const timeoutPromise = new Promise((timeoutResolve) => {
setTimeout(() => {
timeoutResolve({ action: "skip" });
}, 3e4);
});
const result = await Promise.race([
modal.waitForResult(),
timeoutPromise
]);
if (result.action === "skip") {
shouldMigrate = false;
new import_obsidian11.Notice("Migration skipped. Old posts/pages settings will be ignored.");
}
} catch (error) {
console.warn("Migration modal error:", error);
shouldMigrate = false;
new import_obsidian11.Notice("Migration skipped due to error. You can migrate manually in settings.");
}
resolve();
})();
}, 500);
});
}
if (!shouldMigrate) {
settings.migrationCompleted = true;
await this.plugin.saveSettings();
return;
}
const migratedTypes = [];
if (hasPostsSettings && !conflicts.includes("Posts")) {
const postsType = {
id: `posts-${Date.now()}`,
name: "Posts",
folder: settings.postsFolder || "",
linkBasePath: settings.postsLinkBasePath || "",
template: settings.defaultTemplate || '---\ntitle: "{{title}}"\ndate: {{date}}\ntags: []\n---\n',
enabled: true,
creationMode: settings.creationMode || "file",
indexFileName: settings.indexFileName || "",
ignoreSubfolders: settings.onlyAutomateInPostsFolder || false,
enableUnderscorePrefix: settings.enableUnderscorePrefix || false,
useMdxExtension: false,
modifiedDateField: ""
};
migratedTypes.push(postsType);
}
if (hasPagesSettings && !conflicts.includes("Pages")) {
const pagesType = {
id: `pages-${Date.now()}`,
name: "Pages",
folder: settings.pagesFolder || "",
linkBasePath: settings.pagesLinkBasePath || "",
template: settings.pageTemplate || '---\ntitle: "{{title}}"\ndescription: ""\n---\n',
enabled: true,
creationMode: settings.pagesCreationMode || "file",
indexFileName: settings.pagesIndexFileName || "",
ignoreSubfolders: settings.onlyAutomateInPagesFolder || false,
enableUnderscorePrefix: false,
useMdxExtension: false,
modifiedDateField: ""
};
migratedTypes.push(pagesType);
}
const existingFromNew = settings.contentTypes || [];
const existingFromLegacy = legacyContentTypes || [];
let existingTypes = existingFromNew.length > 0 ? existingFromNew : existingFromLegacy;
let finalTypes = [...existingTypes];
if (migratedTypes.length > 0) {
const existingNames = new Set(existingTypes.map((ct) => ct.name));
const newMigratedTypes = migratedTypes.filter((mt) => !existingNames.has(mt.name));
if (newMigratedTypes.length > 0) {
finalTypes = [...existingTypes, ...newMigratedTypes];
}
}
settings.contentTypes = finalTypes;
const legacyFields = [
"customContentTypes",
"enableUnderscorePrefix",
"postsFolder",
"postsLinkBasePath",
"automatePostCreation",
"creationMode",
"indexFileName",
"excludedDirectories",
"onlyAutomateInPostsFolder",
"enablePages",
"pagesFolder",
"pagesLinkBasePath",
"pagesCreationMode",
"pagesIndexFileName",
"pageTemplate",
"onlyAutomateInPagesFolder"
];
const settingsRecord = settings;
for (const field of legacyFields) {
delete settingsRecord[field];
}
settings.migrationCompleted = true;
await this.plugin.saveSettings();
await this.plugin.loadSettings();
if (migratedTypes.length > 0) {
new import_obsidian11.Notice(`Migration completed: ${migratedTypes.length} content type(s) migrated.`);
setTimeout(() => {
if (this.plugin.settingsTab instanceof AstroComposerSettingTab) {
const settingsTab = this.plugin.settingsTab;
try {
if (settingsTab.customContentTypesContainer || settingsTab.containerEl) {
settingsTab.display();
}
} catch (e) {
console.warn("Could not refresh settings tab after migration:", e);
}
}
}, 300);
}
}
};
// src/services/CreateEventService.ts
var import_obsidian12 = require("obsidian");
var CreateEventService = class {
constructor(app, plugin) {
this.app = app;
this.plugin = plugin;
this.lastProcessedFiles = /* @__PURE__ */ new Map();
}
handleCreate(file) {
void (async () => {
const now = Date.now();
if (!(file instanceof import_obsidian12.TFile) || file.extension !== "md" && file.extension !== "mdx") {
return;
}
const filePath = file.path;
const createdTime = this.plugin.pluginCreatedFiles.get(filePath);
if (createdTime && now - createdTime < 5 * 60 * 1e3) {
return;
}
const lastProcessed = this.lastProcessedFiles.get(filePath) || 0;
if (lastProcessed > 0 && now - lastProcessed < CONSTANTS.DEBOUNCE_MS) {
return;
}
if (lastProcessed > 0 && now - lastProcessed > 2e3) {
this.lastProcessedFiles.delete(filePath);
}
const periodicCutoff = now - CONSTANTS.DEBOUNCE_MS * 2;
for (const [path, time] of this.lastProcessedFiles.entries()) {
if (time < periodicCutoff) {
this.lastProcessedFiles.delete(path);
}
}
const contentTypes = this.plugin.settings.contentTypes || [];
const hasEnabledContentTypes = contentTypes.some((ct) => ct.enabled);
if (!hasEnabledContentTypes) {
return;
}
const sortedContentTypes = sortByPatternSpecificity(contentTypes);
let matchedContentTypeId = null;
const matchingTypes = [];
for (const contentType of sortedContentTypes) {
if (!contentType.enabled) continue;
let matches = false;
if (!contentType.folder || contentType.folder.trim() === "") {
if (!filePath.includes("/") || filePath.split("/").length === 1) {
matches = true;
}
} else if (matchesFolderPattern(filePath, contentType.folder)) {
if (contentType.ignoreSubfolders) {
const pathSegments = filePath.split("/");
const pathDepth = pathSegments.length;
const patternSegments = contentType.folder.split("/");
const expectedDepth = patternSegments.length;
if (contentType.creationMode === "folder") {
const folderDepth = pathDepth - 1;
if (folderDepth === expectedDepth || folderDepth === expectedDepth + 1) {
matches = true;
}
} else {
if (pathDepth === expectedDepth) {
matches = true;
}
}
} else {
matches = true;
}
}
if (matches) {
matchingTypes.push(contentType);
if (!matchedContentTypeId) {
matchedContentTypeId = contentType.id;
}
}
}
if (matchingTypes.length > 1) {
const typeNames = matchingTypes.map((ct) => ct.name || "Unnamed").join(", ");
new import_obsidian12.Notice(`Multiple content types (${typeNames}) match this file. Using most specific: ${matchingTypes[0].name || "Unnamed"}`);
}
if (!matchedContentTypeId) {
return;
}
const fileName = file.basename;
const isUntitled = /^Untitled(\s\d+)?$/.test(fileName);
if (!isUntitled) {
if (!this.plugin.settings.processBackgroundFileChanges) {
return;
}
const stat = await this.app.vault.adapter.stat(file.path);
const isRecent = (stat == null ? void 0 : stat.mtime) && now - stat.mtime < CONSTANTS.STAT_MTIME_THRESHOLD;
if (!isRecent) {
return;
}
let content;
try {
content = await this.app.vault.read(file);
} catch (e) {
return;
}
if (content.trim().length > 0) {
const contentWithoutFrontmatter = content.startsWith("---") ? content.slice(content.indexOf("\n---", 3) + 4).trim() : content.trim();
if (contentWithoutFrontmatter.length > 0) {
return;
}
}
}
await new Promise((resolve) => setTimeout(resolve, 100));
this.lastProcessedFiles.set(file.path, now);
setTimeout(() => {
this.lastProcessedFiles.delete(file.path);
}, CONSTANTS.DEBOUNCE_MS + 100);
new TitleModal(this.app, file, this.plugin, matchedContentTypeId, false, true).open();
})();
}
};
// src/services/FrontmatterService.ts
var import_obsidian13 = require("obsidian");
var FrontmatterService = class {
constructor(app, plugin) {
this.app = app;
this.plugin = plugin;
this.lastProcessedFile = "";
this.lastProcessedTime = 0;
this.debounceTimeout = null;
this.draftStatusMap = /* @__PURE__ */ new Map();
this.contentHashCache = /* @__PURE__ */ new Map();
this.registerEvents();
this.app.workspace.onLayoutReady(() => {
this.initializeDraftStatusMap();
});
}
destroy() {
}
initializeDraftStatusMap() {
var _a;
this.draftStatusMap.clear();
const settings = this.plugin.settings;
const isUnderscoreMode = settings.draftDetectionMode === "underscore-prefix";
const draftProp = settings.draftProperty || "draft";
const files = this.app.vault.getFiles().filter((f) => f instanceof import_obsidian13.TFile && (f.extension === "md" || f.extension === "mdx"));
for (const file of files) {
if (isUnderscoreMode) {
this.draftStatusMap.set(file.path, file.name.startsWith("_"));
} else {
const cache = this.app.metadataCache.getFileCache(file);
const rawValue = (_a = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _a[draftProp];
this.draftStatusMap.set(file.path, this.calculateIsDraft(rawValue, settings));
}
}
}
calculateIsDraft(rawValue, settings) {
if (rawValue === void 0 || rawValue === null) return false;
const val = String(rawValue).toLowerCase();
if (settings.draftLogic === "false-is-draft") {
return val === "false" || val === "0" || rawValue === false;
} else {
return val === "true" || val === "1" || rawValue === true;
}
}
registerEvents() {
this.plugin.registerEvent(
this.app.metadataCache.on("changed", (file) => {
if (file instanceof import_obsidian13.TFile) {
this.onMetadataChange(file);
}
})
);
this.plugin.registerEvent(
this.app.vault.on("rename", (file, oldPath) => {
if (file instanceof import_obsidian13.TFile) {
this.onRename(file, oldPath);
}
})
);
this.plugin.registerEvent(
this.app.workspace.on("file-open", (file) => {
if (file instanceof import_obsidian13.TFile) {
void (async () => {
try {
const content = await this.app.vault.read(file);
this.contentHashCache.set(file.path, this.getContentHash(content));
} catch (e) {
console.error(`Failed to lazily initialize content hash for ${file.path}:`, e);
}
})();
}
})
);
}
onRename(file, oldPath) {
var _a;
const settings = this.plugin.settings;
if (!settings.syncDraftDate) return;
const oldName = oldPath.split("/").pop() || "";
const newName = file.name;
if (oldName.startsWith("_") && !newName.startsWith("_")) {
if (settings.draftDetectionMode === "underscore-prefix") {
void this.updateDate(file);
} else {
const contentType = (_a = this.plugin.fileOps) == null ? void 0 : _a.getContentTypeByPath(file.path);
if (contentType == null ? void 0 : contentType.enableUnderscorePrefix) {
void this.updateDate(file);
}
}
}
}
onMetadataChange(file) {
var _a, _b, _c;
const settings = this.plugin.settings;
if (settings.draftDetectionMode === "underscore-prefix") {
const contentType2 = (_a = this.plugin.fileOps) == null ? void 0 : _a.getContentTypeByPath(file.path);
const hasModifiedField2 = !!(contentType2 == null ? void 0 : contentType2.modifiedDateField);
if (!hasModifiedField2) return;
const activeFile2 = this.app.workspace.getActiveFile();
const isActiveFile2 = activeFile2 && activeFile2.path === file.path;
if (!settings.processBackgroundFileChanges && !isActiveFile2) return;
void this.processFile(file, false, contentType2);
return;
}
const activeFile = this.app.workspace.getActiveFile();
const isActiveFile = activeFile && activeFile.path === file.path;
if (!settings.processBackgroundFileChanges && !isActiveFile) {
return;
}
const contentType = (_b = this.plugin.fileOps) == null ? void 0 : _b.getContentTypeByPath(file.path);
const hasModifiedField = !!(contentType == null ? void 0 : contentType.modifiedDateField);
if (!settings.syncDraftDate && !hasModifiedField) {
return;
}
const cache = this.app.metadataCache.getFileCache(file);
const draftProp = settings.draftProperty || "draft";
const rawValue = (_c = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _c[draftProp];
const isCurrentlyDraft = this.calculateIsDraft(rawValue, settings);
if (!this.draftStatusMap.has(file.path)) {
this.draftStatusMap.set(file.path, isCurrentlyDraft);
return;
}
const previousDraftStatus = this.draftStatusMap.get(file.path);
let draftStatusChangedToPublished = false;
if (previousDraftStatus === true && isCurrentlyDraft === false) {
draftStatusChangedToPublished = true;
}
this.draftStatusMap.set(file.path, isCurrentlyDraft);
if (!draftStatusChangedToPublished && !hasModifiedField) {
return;
}
const now = Date.now();
if (this.lastProcessedFile === file.path && now - this.lastProcessedTime < 2e3) {
return;
}
if (this.debounceTimeout) {
window.clearTimeout(this.debounceTimeout);
}
this.debounceTimeout = window.setTimeout(async () => {
try {
const content = await this.app.vault.read(file);
const currentHash = this.getContentHash(content);
const previousHash = this.contentHashCache.get(file.path);
this.contentHashCache.set(file.path, currentHash);
if (previousHash === void 0) {
if (!draftStatusChangedToPublished) {
return;
}
} else if (previousHash === currentHash) {
if (!draftStatusChangedToPublished) {
return;
}
}
} catch (e) {
console.error(`Failed to check content hash for ${file.path}:`, e);
return;
}
void this.processFile(file, draftStatusChangedToPublished, contentType);
}, 500);
}
getContentHash(content) {
let body = content;
if (content.startsWith("---")) {
const end = content.indexOf("\n---", 3);
if (end !== -1) {
body = content.slice(end + 4);
}
}
const normalized = body.replace(/\s+/g, " ").trim();
return this.simpleHash(normalized);
}
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0;
}
return hash.toString() + "_" + str.length;
}
async updateDate(file) {
const settings = this.plugin.settings;
const dateField = settings.publishDateField || "date";
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
const today = (0, import_obsidian13.moment)().format(settings.dateFormat);
if (frontmatter[dateField] !== today) {
frontmatter[dateField] = today;
this.lastProcessedFile = file.path;
this.lastProcessedTime = Date.now();
}
});
}
async processFile(file, draftStatusChangedToPublished, contentType) {
const settings = this.plugin.settings;
const publishDateField = settings.publishDateField || "date";
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
let changed = false;
if (settings.syncDraftDate && draftStatusChangedToPublished) {
const today = (0, import_obsidian13.moment)().format(settings.dateFormat);
if (frontmatter[publishDateField] !== today) {
frontmatter[publishDateField] = today;
changed = true;
}
}
const modifiedField = contentType == null ? void 0 : contentType.modifiedDateField;
if (modifiedField && frontmatter[modifiedField] !== void 0) {
const now = (0, import_obsidian13.moment)().format(settings.dateFormat);
if (frontmatter[modifiedField] !== now) {
frontmatter[modifiedField] = now;
changed = true;
}
}
if (changed) {
this.lastProcessedFile = file.path;
this.lastProcessedTime = Date.now();
}
});
}
};
// src/main.ts
var AstroComposerPlugin = class extends import_obsidian14.Plugin {
constructor() {
super(...arguments);
this.pluginCreatedFiles = /* @__PURE__ */ new Map();
this.processedFiles = /* @__PURE__ */ new Map();
this.terminalRibbonIcon = null;
this.configRibbonIcon = null;
}
/**
* Migrate old posts/pages settings to unified content types
*/
async migrateSettingsIfNeeded() {
if (!this.migrationService) {
this.migrationService = new MigrationService(this.app, this);
}
await this.migrationService.migrateSettingsIfNeeded();
}
async onload() {
try {
await this.loadSettings();
this.fileOps = new FileOperations(this.app, this.settings, this);
this.migrationService = new MigrationService(this.app, this);
this.createEventService = new CreateEventService(this.app, this);
this.frontmatterService = new FrontmatterService(this.app, this);
this.templateParser = new TemplateParser(this.app, this.settings, this);
this.headingLinkGenerator = new HeadingLinkGenerator(this.settings, this);
if (this.settings.showMdxFilesInExplorer) {
try {
this.registerExtensions(["mdx"], "markdown");
} catch (error) {
console.warn("[Astro Composer] MDX extension already registered:", error);
}
}
this.app.workspace.onLayoutReady(() => {
this.registerCreateEvent();
if (!import_obsidian14.Platform.isMobile) {
this.startHelpButtonMonitor();
}
this.registerTitlePropertyClickListener();
void this.migrateSettingsIfNeeded();
});
registerCommands(this, this.settings);
registerContentTypeCommands(this, this.settings);
this.settingsTab = new AstroComposerSettingTab(this.app, this);
this.addSettingTab(this.settingsTab);
this.registerContextMenu();
this.registerRibbonIcons();
this.setupRibbonContextMenuHandling();
} catch (error) {
console.error("[Astro Composer] Critical error during onload:", error);
new import_obsidian14.Notice("Astro Composer failed to load. Check console (Ctrl+Shift+I) for details.");
throw error;
}
}
registerCreateEvent() {
if (this.createEventRef) {
this.app.vault.offref(this.createEventRef);
this.createEventRef = void 0;
}
const createEventRef = this.app.vault.on("create", (file) => {
if (file instanceof import_obsidian14.TFile) {
this.createEventService.handleCreate(file);
this.cleanupPluginCreatedFiles();
}
});
this.registerEvent(createEventRef);
this.createEventRef = createEventRef;
}
registerTitlePropertyClickListener() {
this.registerDomEvent(document, "click", (evt) => {
if (!this.settings.renameOnTitleClick) return;
const target = evt.target;
const propertyEl = target.closest(".metadata-property");
if (!propertyEl) return;
const propertyKey = propertyEl.getAttribute("data-property-key");
if (!propertyKey) return;
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) return;
const typeId = this.fileOps.determineType(activeFile);
const titleKey = this.fileOps.getTitleKey(typeId);
if (propertyKey === titleKey) {
evt.preventDefault();
evt.stopPropagation();
this.renameContentByPath(activeFile.path);
}
}, true);
}
cleanupPluginCreatedFiles() {
const now = Date.now();
const ttl = 5 * 60 * 1e3;
for (const [path, timestamp] of this.pluginCreatedFiles.entries()) {
if (now - timestamp > ttl) {
this.pluginCreatedFiles.delete(path);
}
}
}
async loadSettings() {
const loadedData = await this.loadData();
if (!this.settings) {
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData);
} else {
Object.assign(this.settings, loadedData);
}
if (!this.settings.contentTypes || !Array.isArray(this.settings.contentTypes)) {
this.settings.contentTypes = [];
}
if (!this.settings.migrationCompleted) {
const legacySettings = this.settings;
const hasLegacyTypes = legacySettings.customContentTypes && Array.isArray(legacySettings.customContentTypes) && legacySettings.customContentTypes.length > 0;
const hasNewTypes = this.settings.contentTypes && Array.isArray(this.settings.contentTypes) && this.settings.contentTypes.length > 0;
if (hasLegacyTypes && !hasNewTypes) {
this.settings.contentTypes = legacySettings.customContentTypes || [];
}
} else {
const legacyFields = [
"customContentTypes",
"enableUnderscorePrefix",
"postsFolder",
"postsLinkBasePath",
"automatePostCreation",
"creationMode",
"indexFileName",
"excludedDirectories",
"onlyAutomateInPostsFolder",
"enablePages",
"pagesFolder",
"pagesLinkBasePath",
"pagesCreationMode",
"pagesIndexFileName",
"pageTemplate",
"onlyAutomateInPagesFolder",
"linkBasePath",
"enableAutoRename",
"enableAutoInsertFrontmatter",
"draftStyle"
];
const settingsRecord = this.settings;
let fieldsRemoved = false;
for (const field of legacyFields) {
if (settingsRecord[field] !== void 0) {
delete settingsRecord[field];
fieldsRemoved = true;
}
}
if (fieldsRemoved) {
await this.saveSettings();
}
}
}
async saveSettings() {
await this.saveData(this.settings);
}
registerContextMenu() {
this.registerEvent(
this.app.workspace.on("editor-menu", (menu, editor, view) => {
if (!this.settings.enableCopyHeadingLink) {
return;
}
const cursor = editor.getCursor();
const file = view.file;
if (!(file instanceof import_obsidian14.TFile)) {
return;
}
const heading = this.headingLinkGenerator.findHeadingAtLine(this.app, file, cursor.line);
if (heading) {
const fullLink = this.headingLinkGenerator.generateLink(this.app, file, heading);
const urlOnly = this.headingLinkGenerator.extractUrl(fullLink);
menu.addItem((item) => {
item.setTitle("Copy heading link").setIcon("link-2").onClick(async () => {
await navigator.clipboard.writeText(urlOnly);
new import_obsidian14.Notice("Heading link copied to clipboard");
});
});
menu.addItem((item) => {
item.setTitle("Copy heading link with text").setIcon("heading").onClick(async () => {
await navigator.clipboard.writeText(fullLink);
new import_obsidian14.Notice("Heading link with text copied to clipboard");
});
});
}
})
);
}
renameContentByPath(filePath) {
renameContentByPath(this.app, filePath, this.settings, this);
}
registerRibbonIcons() {
if (import_obsidian14.Platform.isMobile) {
if (this.terminalRibbonIcon) {
try {
if (this.terminalRibbonIcon.parentNode) this.terminalRibbonIcon.remove();
} catch (e) {
}
this.terminalRibbonIcon = null;
}
if (this.configRibbonIcon) {
try {
if (this.configRibbonIcon.parentNode) this.configRibbonIcon.remove();
} catch (e) {
}
this.configRibbonIcon = null;
}
try {
const terminalIcons = document.querySelectorAll('.side-dock-ribbon-action[aria-label="Open project terminal"]');
terminalIcons.forEach((icon) => icon.remove());
const configIcons = document.querySelectorAll('.side-dock-ribbon-action[aria-label="Edit astro config"]');
configIcons.forEach((icon) => icon.remove());
} catch (e) {
}
return;
}
const terminalShouldExist = this.settings.enableTerminalRibbonIcon && this.settings.enableOpenTerminalCommand;
const configShouldExist = this.settings.enableConfigRibbonIcon && this.settings.enableOpenConfigFileCommand;
if (this.terminalRibbonIcon) {
try {
if (this.terminalRibbonIcon.parentNode) this.terminalRibbonIcon.remove();
} catch (e) {
}
this.terminalRibbonIcon = null;
}
if (this.configRibbonIcon) {
try {
if (this.configRibbonIcon.parentNode) this.configRibbonIcon.remove();
} catch (e) {
}
this.configRibbonIcon = null;
}
try {
document.querySelectorAll('.side-dock-ribbon-action[aria-label="Open project terminal"]').forEach((el) => el.remove());
document.querySelectorAll('.side-dock-ribbon-action[aria-label="Edit astro config"]').forEach((el) => el.remove());
} catch (e) {
}
if (terminalShouldExist) {
this.terminalRibbonIcon = this.addRibbonIcon("terminal-square", "Open project terminal", () => {
if (!this.settings.enableOpenTerminalCommand) {
new import_obsidian14.Notice("Open terminal command is disabled.");
return;
}
openTerminalInProjectRoot(this.app, this.settings);
});
if (this.terminalRibbonIcon) this.terminalRibbonIcon.setAttribute("data-astro-composer-terminal-ribbon", "true");
}
if (configShouldExist) {
this.configRibbonIcon = this.addRibbonIcon("rocket", "Edit astro config", async () => {
if (!this.settings.enableOpenConfigFileCommand) {
new import_obsidian14.Notice("Edit config file command is disabled.");
return;
}
await openConfigFile(this.app, this.settings);
});
if (this.configRibbonIcon) this.configRibbonIcon.setAttribute("data-astro-composer-config-ribbon", "true");
}
this.updateRibbonContextMenuCSS();
this.setupRibbonContextMenuObserver();
}
onunload() {
var _a;
(_a = this.frontmatterService) == null ? void 0 : _a.destroy();
if (this.terminalRibbonIcon) {
this.terminalRibbonIcon.remove();
this.terminalRibbonIcon = null;
}
if (this.configRibbonIcon) {
this.configRibbonIcon.remove();
this.configRibbonIcon = null;
}
if (this.ribbonContextMenuObserver) {
this.ribbonContextMenuObserver.disconnect();
this.ribbonContextMenuObserver = void 0;
}
document.body.removeClass("astro-composer-hide-terminal-icon");
document.body.removeClass("astro-composer-hide-config-icon");
if (this.helpButtonObserver) {
this.helpButtonObserver.disconnect();
this.helpButtonObserver = void 0;
}
if (this.customHelpButton) {
this.customHelpButton.remove();
this.customHelpButton = void 0;
}
this.helpButtonElement = void 0;
}
setupRibbonContextMenuHandling() {
this.updateRibbonContextMenuCSS();
this.setupRibbonContextMenuObserver();
}
updateRibbonContextMenuCSS() {
const terminalShouldBeHidden = !this.settings.enableTerminalRibbonIcon || !this.settings.enableOpenTerminalCommand;
const configShouldBeHidden = !this.settings.enableConfigRibbonIcon || !this.settings.enableOpenConfigFileCommand;
if (terminalShouldBeHidden) document.body.addClass("astro-composer-hide-terminal-icon");
else document.body.removeClass("astro-composer-hide-terminal-icon");
if (configShouldBeHidden) document.body.addClass("astro-composer-hide-config-icon");
else document.body.removeClass("astro-composer-hide-config-icon");
}
setupRibbonContextMenuObserver() {
if (this.ribbonContextMenuObserver) this.ribbonContextMenuObserver.disconnect();
const terminalShouldBeHidden = !this.settings.enableTerminalRibbonIcon || !this.settings.enableOpenTerminalCommand;
const configShouldBeHidden = !this.settings.enableConfigRibbonIcon || !this.settings.enableOpenConfigFileCommand;
if (!terminalShouldBeHidden && !configShouldBeHidden) return;
this.ribbonContextMenuObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
for (const node of Array.from(mutation.addedNodes)) {
if (node instanceof HTMLElement) {
if (node.classList.contains("menu") || node.querySelector(".menu")) {
this.removeRibbonIconsFromContextMenu(node);
}
}
}
}
}
});
this.ribbonContextMenuObserver.observe(document.body, { childList: true, subtree: true });
}
/**
* Starts a robust monitor that keeps the help button in sync with settings.
*/
startHelpButtonMonitor() {
if (this.helpButtonObserver) this.helpButtonObserver.disconnect();
this.syncHelpButton();
let timer = null;
let mutationCount = 0;
this.helpButtonObserver = new MutationObserver(() => {
mutationCount++;
if (timer) window.clearTimeout(timer);
const delay = mutationCount < 20 ? 0 : 100;
if (delay === 0) {
this.syncHelpButton();
} else {
timer = window.setTimeout(() => this.syncHelpButton(), delay);
}
});
this.helpButtonObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class", "src", "aria-label"]
});
}
/**
* Synchronizes the help button state based on settings.
*/
syncHelpButton() {
var _a, _b, _c, _d;
const enabled = (_a = this.settings.helpButtonReplacement) == null ? void 0 : _a.enabled;
if (enabled) document.body.addClass("astro-composer-hide-help-button");
else document.body.removeClass("astro-composer-hide-help-button");
if (!enabled) {
if (this.customHelpButton) {
this.customHelpButton.remove();
this.customHelpButton = void 0;
}
return;
}
const selectors = [
".workspace-drawer-vault-actions .clickable-icon svg.help",
".workspace-sidedock-vault-profile .clickable-icon svg.help",
".workspace-drawer .clickable-icon svg.help",
".clickable-icon svg.help"
];
let helpButtonSvg = null;
for (const selector of selectors) {
helpButtonSvg = document.querySelector(selector);
if (helpButtonSvg) break;
}
if (!helpButtonSvg) return;
const originalHelpButton = helpButtonSvg.parentElement;
if (!originalHelpButton) return;
const existingReplacement = (_b = originalHelpButton.parentElement) == null ? void 0 : _b.querySelector('[data-astro-composer-help-replacement="true"]');
if (existingReplacement) {
this.customHelpButton = existingReplacement;
return;
}
const customButton = originalHelpButton.cloneNode(true);
customButton.addClass("astro-composer-help-replacement");
customButton.removeAttribute("aria-label");
customButton.setAttribute("data-astro-composer-help-replacement", "true");
customButton.onclick = null;
const iconContainer = ((_c = customButton.querySelector("svg")) == null ? void 0 : _c.parentElement) || customButton;
try {
if (iconContainer instanceof HTMLElement) {
(0, import_obsidian14.setIcon)(iconContainer, this.settings.helpButtonReplacement.iconId);
}
} catch (error) {
console.warn("[Astro Composer] Error setting replacement icon:", error);
}
customButton.addEventListener("click", (evt) => {
var _a2, _b2;
evt.preventDefault();
evt.stopPropagation();
const commandId = (_a2 = this.settings.helpButtonReplacement) == null ? void 0 : _a2.commandId;
if (commandId) {
const appWithCommands = this.app;
if ((_b2 = appWithCommands.commands) == null ? void 0 : _b2.executeCommandById) {
void appWithCommands.commands.executeCommandById(commandId);
}
}
}, true);
(_d = originalHelpButton.parentElement) == null ? void 0 : _d.insertBefore(customButton, originalHelpButton);
this.customHelpButton = customButton;
}
restoreHelpButton() {
document.body.removeClass("astro-composer-hide-help-button");
if (this.customHelpButton) {
this.customHelpButton.remove();
this.customHelpButton = void 0;
}
this.helpButtonElement = void 0;
}
removeRibbonIconsFromContextMenu(menuElement) {
var _a, _b;
const terminalShouldBeHidden = !this.settings.enableTerminalRibbonIcon || !this.settings.enableOpenTerminalCommand;
const configShouldBeHidden = !this.settings.enableConfigRibbonIcon || !this.settings.enableOpenConfigFileCommand;
const menuItems = menuElement.querySelectorAll(".menu-item");
for (const item of Array.from(menuItems)) {
const svg = item.querySelector("svg");
if (svg) {
let iconName = svg.getAttribute("data-lucide") || svg.getAttribute("xmlns:lucide") || svg.getAttribute("data-icon") || (svg.classList.contains("lucide-terminal-square") ? "terminal-square" : null) || (svg.classList.contains("lucide-rocket") ? "rocket" : null) || (svg.classList.contains("lucide-wrench") ? "wrench" : null);
if (iconName) iconName = iconName.replace(/^lucide-/, "");
if (terminalShouldBeHidden && iconName === "terminal-square") {
if ((_a = item.textContent) == null ? void 0 : _a.toLowerCase().includes("terminal")) item.remove();
}
if (configShouldBeHidden && (iconName === "rocket" || iconName === "wrench")) {
if ((_b = item.textContent) == null ? void 0 : _b.toLowerCase().includes("config")) item.remove();
}
}
}
}
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21haW4udHMiLCAic3JjL3R5cGVzLnRzIiwgInNyYy9zZXR0aW5ncy50cyIsICJzcmMvY29tbWFuZHMvaW5kZXgudHMiLCAic3JjL3V0aWxzL2ZpbGUtb3BlcmF0aW9ucy50cyIsICJzcmMvdXRpbHMvcGF0aC1tYXRjaGluZy50cyIsICJzcmMvdXRpbHMvc3RyaW5nLXV0aWxzLnRzIiwgInNyYy91dGlscy90ZW1wbGF0ZS1wYXJzaW5nLnRzIiwgInNyYy91dGlscy9saW5rLWNvbnZlcnNpb24udHMiLCAic3JjL3VpL3RpdGxlLW1vZGFsLnRzIiwgInNyYy91aS9zZXR0aW5ncy10YWIudHMiLCAic3JjL3VpL2NvbXBvbmVudHMvQ29tbWFuZFBpY2tlck1vZGFsLnRzIiwgInNyYy91aS9jb21wb25lbnRzL0ljb25QaWNrZXJNb2RhbC50cyIsICJzcmMvdWkvY29tcG9uZW50cy9Db25maXJtTW9kYWwudHMiLCAic3JjL3V0aWxzL2hlYWRpbmctbGluay1nZW5lcmF0b3IudHMiLCAic3JjL3NlcnZpY2VzL01pZ3JhdGlvblNlcnZpY2UudHMiLCAic3JjL3VpL2NvbXBvbmVudHMvTWlncmF0aW9uTW9kYWwudHMiLCAic3JjL3NlcnZpY2VzL0NyZWF0ZUV2ZW50U2VydmljZS50cyIsICJzcmMvc2VydmljZXMvRnJvbnRtYXR0ZXJTZXJ2aWNlLnRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQge1xyXG5cdFBsdWdpbixcclxuXHRURmlsZSxcclxuXHROb3RpY2UsXHJcblx0c2V0SWNvbixcclxuXHRQbGF0Zm9ybSxcclxuXHRFdmVudFJlZixcclxufSBmcm9tIFwib2JzaWRpYW5cIjtcclxuXHJcbmltcG9ydCB7IEFzdHJvQ29tcG9zZXJTZXR0aW5ncywgREVGQVVMVF9TRVRUSU5HUyB9IGZyb20gXCIuL3NldHRpbmdzXCI7XHJcbmltcG9ydCB7IEFzdHJvQ29tcG9zZXJQbHVnaW5JbnRlcmZhY2UsIENvbnRlbnRUeXBlIH0gZnJvbSBcIi4vdHlwZXNcIjtcclxuaW1wb3J0IHsgcmVnaXN0ZXJDb21tYW5kcywgcmVnaXN0ZXJDb250ZW50VHlwZUNvbW1hbmRzLCByZW5hbWVDb250ZW50QnlQYXRoIGFzIHJlbmFtZUNvbnRlbnRCeVBhdGhGdW5jdGlvbiwgb3BlblRlcm1pbmFsSW5Qcm9qZWN0Um9vdCwgb3BlbkNvbmZpZ0ZpbGUgfSBmcm9tIFwiLi9jb21tYW5kc1wiO1xyXG5pbXBvcnQgeyBBc3Ryb0NvbXBvc2VyU2V0dGluZ1RhYiB9IGZyb20gXCIuL3VpL3NldHRpbmdzLXRhYlwiO1xyXG5pbXBvcnQgeyBGaWxlT3BlcmF0aW9ucyB9IGZyb20gXCIuL3V0aWxzL2ZpbGUtb3BlcmF0aW9uc1wiO1xyXG5pbXBvcnQgeyBUZW1wbGF0ZVBhcnNlciB9IGZyb20gXCIuL3V0aWxzL3RlbXBsYXRlLXBhcnNpbmdcIjtcclxuaW1wb3J0IHsgSGVhZGluZ0xpbmtHZW5lcmF0b3IgfSBmcm9tIFwiLi91dGlscy9oZWFkaW5nLWxpbmstZ2VuZXJhdG9yXCI7XHJcbmltcG9ydCB7IE1pZ3JhdGlvblNlcnZpY2UgfSBmcm9tIFwiLi9zZXJ2aWNlcy9NaWdyYXRpb25TZXJ2aWNlXCI7XHJcbmltcG9ydCB7IENyZWF0ZUV2ZW50U2VydmljZSB9IGZyb20gXCIuL3NlcnZpY2VzL0NyZWF0ZUV2ZW50U2VydmljZVwiO1xyXG5pbXBvcnQgeyBGcm9udG1hdHRlclNlcnZpY2UgfSBmcm9tIFwiLi9zZXJ2aWNlcy9Gcm9udG1hdHRlclNlcnZpY2VcIjtcclxuaW1wb3J0IHsgd2FpdEZvckVsZW1lbnQgfSBmcm9tIFwiLi91dGlscy9kb21cIjtcclxuXHJcbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEFzdHJvQ29tcG9zZXJQbHVnaW4gZXh0ZW5kcyBQbHVnaW4gaW1wbGVtZW50cyBBc3Ryb0NvbXBvc2VyUGx1Z2luSW50ZXJmYWNlIHtcclxuXHRzZXR0aW5ncyE6IEFzdHJvQ29tcG9zZXJTZXR0aW5ncztcclxuXHRwcml2YXRlIGNyZWF0ZUV2ZW50UmVmPzogRXZlbnRSZWY7XHJcblx0cHVibGljIGZpbGVPcHMhOiBGaWxlT3BlcmF0aW9ucztcclxuXHRwdWJsaWMgdGVtcGxhdGVQYXJzZXIhOiBUZW1wbGF0ZVBhcnNlcjtcclxuXHRwdWJsaWMgaGVhZGluZ0xpbmtHZW5lcmF0b3IhOiBIZWFkaW5nTGlua0dlbmVyYXRvcjtcclxuXHRwdWJsaWMgcGx1Z2luQ3JlYXRlZEZpbGVzOiBNYXA8c3RyaW5nLCBudW1iZXI+ID0gbmV3IE1hcCgpO1xyXG5cdHByaXZhdGUgcHJvY2Vzc2VkRmlsZXM6IE1hcDxzdHJpbmcsIG51bWJlcj4gPSBuZXcgTWFwKCk7XHJcblx0cHJpdmF0ZSB0ZXJtaW5hbFJpYmJvbkljb246IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XHJcblx0cHJpdmF0ZSBjb25maWdSaWJib25JY29uOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xyXG5cdHByaXZhdGUgcmliYm9uQ29udGV4dE1lbnVPYnNlcnZlcj86IE11dGF0aW9uT2JzZXJ2ZXI7XHJcblx0cHJpdmF0ZSBoZWxwQnV0dG9uT2JzZXJ2ZXI/OiBNdXRhdGlvbk9ic2VydmVyO1xyXG5cdHByaXZhdGUgaGVscEJ1dHRvbkVsZW1lbnQ/OiBIVE1MRWxlbWVudDtcclxuXHRwcml2YXRlIGN1c3RvbUhlbHBCdXR0b24/OiBIVE1MRWxlbWVudDtcclxuXHRwdWJsaWMgc2V0dGluZ3NUYWI/OiBBc3Ryb0NvbXBvc2VyU2V0dGluZ1RhYjtcclxuXHJcblx0cHJpdmF0ZSBtaWdyYXRpb25TZXJ2aWNlITogTWlncmF0aW9uU2VydmljZTtcclxuXHRwcml2YXRlIGNyZWF0ZUV2ZW50U2VydmljZSE6IENyZWF0ZUV2ZW50U2VydmljZTtcclxuXHRwdWJsaWMgZnJvbnRtYXR0ZXJTZXJ2aWNlITogRnJvbnRtYXR0ZXJTZXJ2aWNlO1xyXG5cclxuXHQvKipcclxuXHQgKiBNaWdyYXRlIG9sZCBwb3N0cy9wYWdlcyBzZXR0aW5ncyB0byB1bmlmaWVkIGNvbnRlbnQgdHlwZXNcclxuXHQgKi9cclxuXHRwcml2YXRlIGFzeW5jIG1pZ3JhdGVTZXR0aW5nc0lmTmVlZGVkKCk6IFByb21pc2U8dm9pZD4ge1xyXG5cdFx0aWYgKCF0aGlzLm1pZ3JhdGlvblNlcnZpY2UpIHtcclxuXHRcdFx0dGhpcy5taWdyYXRpb25TZXJ2aWNlID0gbmV3IE1pZ3JhdGlvblNlcnZpY2UodGhpcy5hcHAsIHRoaXMpO1xyXG5cdFx0fVxyXG5cdFx0YXdhaXQgdGhpcy5taWdyYXRpb25TZXJ2aWNlLm1pZ3JhdGVTZXR0aW5nc0lmTmVlZGVkKCk7XHJcblx0fVxyXG5cclxuXHRhc3luYyBvbmxvYWQoKSB7XHJcblx0XHR0cnkge1xyXG5cdFx0XHRhd2FpdCB0aGlzLmxvYWRTZXR0aW5ncygpO1xyXG5cclxuXHRcdFx0L