feat(exporter): add Astro Post Exporter plugin

This commit is contained in:
gameloader
2026-01-18 19:17:36 +08:00
commit 6638f40fc9
2 changed files with 280 additions and 0 deletions

271
main.js Normal file
View File

@@ -0,0 +1,271 @@
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 includePubDate = !hasFrontmatterKey(content, "pubDate");
const pubDate = moment(created).format(this.settings.dateFormat);
const modifiedDate = moment(modified).format(this.settings.dateFormat);
const frontmatterLines = [
`title: "${escapeYamlString(title)}"`,
...(includePubDate ? [`pubDate: "${pubDate}"`] : []),
`${this.settings.modifiedDateKey}: "${modifiedDate}"`
];
const keysToReplace = [
"title",
...(includePubDate ? ["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;