import { App, Modal, Setting } from "obsidian"; import { Pair, composeFrontmatter, docFrontmatterToPairs, parseNote } from "./frontmatter"; import { Strategy } from "./images"; import { JekyllPublishSettings } from "./settings"; import { deriveDate, deriveSlug } from "./slug"; export interface ModalResult { frontmatterPairs: Pair[]; slug: string; date?: string; strategy: Strategy; commitMessage: string; } export class PublishModal extends Modal { private result: ModalResult; private mergeDoc = true; /** Editable rows = preset fields (seeded from settings) + custom additions. */ private editableRows: Pair[]; /** The note's own frontmatter, shown read-only when merge is on. */ private readonly docPairs: Pair[]; private fmEl!: HTMLElement; constructor( app: App, settings: JekyllPublishSettings, private noteText: string, filename: string, private onSubmit: (r: ModalResult) => void ) { super(app); const { frontmatter } = parseNote(noteText); const title = typeof frontmatter.title === "string" ? frontmatter.title : ""; this.docPairs = docFrontmatterToPairs(frontmatter); // Deep-copy the presets so editing/deleting rows here never mutates the // saved settings. this.editableRows = settings.presetFrontmatter.map((p) => ({ key: p.key, value: p.value })); this.result = { frontmatterPairs: [], slug: deriveSlug({ title, filename }), date: deriveDate({ frontmatterDate: frontmatter.date }), strategy: settings.defaultImageStrategy, commitMessage: settings.commitMessageTemplate.replace("{{title}}", title || filename), }; } private recompute() { this.result.frontmatterPairs = composeFrontmatter(this.docPairs, this.editableRows, this.mergeDoc); } onOpen() { this.recompute(); const { contentEl } = this; contentEl.createEl("h2", { text: "Publish to Jekyll" }); new Setting(contentEl).setName("Slug").addText((t) => t.setValue(this.result.slug).onChange((v) => (this.result.slug = v)) ); new Setting(contentEl).setName("Date").addText((t) => t.setValue(this.result.date ?? "").onChange((v) => (this.result.date = v)) ).setDesc("The date to prepend to the filename. Useful for blog posts."); new Setting(contentEl).setName("Image strategy").addDropdown((d) => d.addOption("flat-slug", "Flat, renamed to slug") .addOption("per-post-folder", "Per-post subfolder") .setValue(this.result.strategy) .onChange((v) => (this.result.strategy = v as Strategy)) ); new Setting(contentEl) .setName("Merge document frontmatter") .setDesc("Include the note's own frontmatter (shown read-only below).") .addToggle((t) => t.setValue(this.mergeDoc).onChange((v) => { this.mergeDoc = v; this.renderFrontmatter(); }) ); contentEl.createEl("h3", { text: "Frontmatter" }); this.fmEl = contentEl.createDiv(); this.renderFrontmatter(); new Setting(contentEl).addButton((b) => b.setButtonText("Add property").onClick(() => { this.editableRows = [...this.editableRows, { key: "", value: "" }]; this.renderFrontmatter(); }) ); new Setting(contentEl).setName("Commit message").addText((t) => t.setValue(this.result.commitMessage).onChange((v) => (this.result.commitMessage = v)) ); new Setting(contentEl).addButton((b) => b.setButtonText("Publish").setCta().onClick(() => { this.recompute(); this.close(); this.onSubmit(this.result); }) ); } private renderFrontmatter() { this.recompute(); this.fmEl.empty(); const editableKeys = new Set( this.editableRows.filter((r) => r.key.trim() !== "").map((r) => r.key) ); // Document fields (read-only) first — only those not overridden by an // editable row, so the modal never shows a key twice. if (this.mergeDoc) { const docRows = this.docPairs.filter((p) => !editableKeys.has(p.key)); for (const p of docRows) { const row = this.fmEl.createDiv({ cls: "jekyll-publish-row" }); const k = row.createEl("input", { cls: "jekyll-publish-doc", value: p.key }); const v = row.createEl("input", { cls: "jekyll-publish-doc", value: p.value }); k.disabled = true; v.disabled = true; k.title = "From the note's frontmatter (edit the note to change)"; v.title = k.title; } } // Editable rows (presets + custom) — same style, all editable and deletable. this.editableRows.forEach((row, i) => { const div = this.fmEl.createDiv({ cls: "jekyll-publish-row" }); const k = div.createEl("input", { value: row.key, placeholder: "key" }); const v = div.createEl("input", { value: row.value, placeholder: "value" }); k.oninput = () => { this.editableRows = this.editableRows.map((r, j) => (j === i ? { ...r, key: k.value } : r)); this.recompute(); }; v.oninput = () => { this.editableRows = this.editableRows.map((r, j) => (j === i ? { ...r, value: v.value } : r)); this.recompute(); }; const del = div.createEl("button", { cls: "jekyll-publish-del", text: "×" }); del.setAttr("aria-label", "Delete property"); del.onclick = () => { this.editableRows = this.editableRows.filter((_, j) => j !== i); this.renderFrontmatter(); }; }); } onClose() { this.contentEl.empty(); } }