feat(exporter): add Astro Post Exporter plugin
This commit is contained in:
271
main.js
Normal file
271
main.js
Normal 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;
|
||||
Reference in New Issue
Block a user