274 lines
8.3 KiB
JavaScript
274 lines
8.3 KiB
JavaScript
const { Plugin, Notice, Setting, PluginSettingTab, TFile, moment } = require("obsidian");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
exportDir: "/Users/logicluo/Project/astro-chiri/src/content/posts",
|
|
dateFormat: "YYYY-MM-DD",
|
|
modifiedDateKey: "modifiedDate",
|
|
preserveSubfolders: false
|
|
};
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function escapeYamlString(value) {
|
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
}
|
|
|
|
function getFrontmatterBlock(content) {
|
|
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
const match = content.match(frontmatterRegex);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
function hasFrontmatterKey(content, key) {
|
|
const block = getFrontmatterBlock(content);
|
|
if (!block) return false;
|
|
const keyRegex = new RegExp(`^${escapeRegExp(key)}\\s*:`, "m");
|
|
return keyRegex.test(block);
|
|
}
|
|
|
|
function upsertFrontmatter(content, frontmatterLines, keys) {
|
|
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
const match = content.match(frontmatterRegex);
|
|
|
|
if (match) {
|
|
const existing = match[1];
|
|
const rest = content.slice(match[0].length);
|
|
const existingLines = existing.split(/\r?\n/);
|
|
const filteredLines = existingLines.filter((line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) return false;
|
|
return !keys.some((key) => new RegExp(`^${escapeRegExp(key)}\\s*:`).test(trimmed));
|
|
});
|
|
const mergedLines = [...filteredLines, ...frontmatterLines];
|
|
return ["---", ...mergedLines, "---", "", rest].join("\n");
|
|
}
|
|
|
|
return ["---", ...frontmatterLines, "---", "", content].join("\n");
|
|
}
|
|
|
|
class AstroPostExporter extends Plugin {
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
this.addCommand({
|
|
id: "export-current-note-to-astro",
|
|
name: "Export current note to Astro",
|
|
checkCallback: (checking) => {
|
|
const file = this.getActiveMarkdownFile();
|
|
if (!file) return false;
|
|
if (!checking) {
|
|
this.exportFiles([file]);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "export-selected-notes-to-astro",
|
|
name: "Export selected notes to Astro",
|
|
callback: () => {
|
|
this.exportSelectedOrActive();
|
|
}
|
|
});
|
|
|
|
this.addSettingTab(new AstroPostExporterSettingTab(this.app, this));
|
|
}
|
|
|
|
getActiveMarkdownFile() {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (file && file instanceof TFile && file.extension === "md") {
|
|
return file;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getSelectedMarkdownFiles() {
|
|
const files = [];
|
|
const leaves = this.app.workspace.getLeavesOfType("file-explorer");
|
|
|
|
for (const leaf of leaves) {
|
|
const explorer = leaf.view && leaf.view.fileExplorer;
|
|
if (!explorer) continue;
|
|
|
|
const selected = explorer.selectedItems || explorer.selectedFileItems || explorer.selectedFiles;
|
|
if (!selected) continue;
|
|
|
|
for (const item of this.iterateSelection(selected)) {
|
|
const file = item instanceof TFile ? item : item && item.file;
|
|
if (file instanceof TFile && file.extension === "md") {
|
|
files.push(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
const seen = new Set();
|
|
return files.filter((file) => {
|
|
if (!file || seen.has(file.path)) return false;
|
|
seen.add(file.path);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
iterateSelection(selection) {
|
|
if (Array.isArray(selection)) return selection;
|
|
if (selection instanceof Set) return Array.from(selection);
|
|
if (selection instanceof Map) return Array.from(selection.values());
|
|
if (selection && typeof selection === "object") return Object.values(selection);
|
|
return [];
|
|
}
|
|
|
|
async exportSelectedOrActive() {
|
|
const selected = this.getSelectedMarkdownFiles();
|
|
const files = selected.length > 0 ? selected : [this.getActiveMarkdownFile()].filter(Boolean);
|
|
|
|
if (files.length === 0) {
|
|
new Notice("No Markdown file selected or active.");
|
|
return;
|
|
}
|
|
|
|
await this.exportFiles(files);
|
|
}
|
|
|
|
async exportFiles(files) {
|
|
const exportDir = (this.settings.exportDir || "").trim();
|
|
if (!exportDir) {
|
|
new Notice("Astro export path is not set. Configure it in plugin settings.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await fs.promises.mkdir(exportDir, { recursive: true });
|
|
} catch (error) {
|
|
console.error(error);
|
|
new Notice("Failed to create export directory. Check the path and permissions.");
|
|
return;
|
|
}
|
|
|
|
let exported = 0;
|
|
for (const file of files) {
|
|
try {
|
|
const content = await this.app.vault.read(file);
|
|
const output = this.applyFrontmatter(file, content);
|
|
const destinationPath = this.getDestinationPath(file, exportDir);
|
|
await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
await fs.promises.writeFile(destinationPath, output, "utf8");
|
|
exported += 1;
|
|
} catch (error) {
|
|
console.error(error);
|
|
new Notice(`Failed to export: ${file.path}`);
|
|
}
|
|
}
|
|
|
|
if (exported > 0) {
|
|
new Notice(`Exported ${exported} file${exported === 1 ? "" : "s"} to Astro.`);
|
|
}
|
|
}
|
|
|
|
applyFrontmatter(file, content) {
|
|
const created = file.stat && file.stat.ctime ? file.stat.ctime : Date.now();
|
|
const modified = file.stat && file.stat.mtime ? file.stat.mtime : Date.now();
|
|
const title = file.basename;
|
|
|
|
const includeDescription = !hasFrontmatterKey(content, "description");
|
|
const pubDate = moment(created).format(this.settings.dateFormat);
|
|
const modifiedDate = moment(modified).format(this.settings.dateFormat);
|
|
|
|
const frontmatterLines = [
|
|
`title: "${escapeYamlString(title)}"`,
|
|
...(includeDescription ? [`description: "${escapeYamlString(title)}"`] : []),
|
|
`pubDate: ${pubDate}`,
|
|
`${this.settings.modifiedDateKey}: ${modifiedDate}`
|
|
];
|
|
|
|
const keysToReplace = [
|
|
"title",
|
|
...(includeDescription ? ["description"] : []),
|
|
"pubDate",
|
|
this.settings.modifiedDateKey
|
|
];
|
|
return upsertFrontmatter(content, frontmatterLines, keysToReplace);
|
|
}
|
|
|
|
getDestinationPath(file, exportDir) {
|
|
if (this.settings.preserveSubfolders) {
|
|
return path.join(exportDir, file.path);
|
|
}
|
|
return path.join(exportDir, file.name);
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
}
|
|
|
|
class AstroPostExporterSettingTab extends PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
new Setting(containerEl)
|
|
.setName("Astro posts directory")
|
|
.setDesc("Absolute path to the Astro posts folder.")
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("/path/to/src/content/posts")
|
|
.setValue(this.plugin.settings.exportDir)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.exportDir = value.trim();
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("Date format")
|
|
.setDesc("Format for pubDate and ModifiedDate.")
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("YYYY-MM-DD")
|
|
.setValue(this.plugin.settings.dateFormat)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.dateFormat = value.trim() || DEFAULT_SETTINGS.dateFormat;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("Modified date key")
|
|
.setDesc("Frontmatter key name for the modified date.")
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("modifiedDate")
|
|
.setValue(this.plugin.settings.modifiedDateKey)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.modifiedDateKey = value.trim() || DEFAULT_SETTINGS.modifiedDateKey;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("Preserve subfolders")
|
|
.setDesc("Keep the vault folder structure under the export directory.")
|
|
.addToggle((toggle) =>
|
|
toggle.setValue(this.plugin.settings.preserveSubfolders).onChange(async (value) => {
|
|
this.plugin.settings.preserveSubfolders = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = AstroPostExporter;
|