diff --git a/main.js b/main.js
index 40af209..ff1e599 100644
--- a/main.js
+++ b/main.js
@@ -3,16 +3,19 @@ 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 Tt=Object.create;var U=Object.defineProperty;var Et=Object.getOwnPropertyDescriptor;var xt=Object.getOwnPropertyNames;var Pt=Object.getPrototypeOf,Rt=Object.prototype.hasOwnProperty;var At=(g,t)=>{for(var i in t)U(g,i,{get:t[i],enumerable:!0})},ft=(g,t,i,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let e of xt(t))!Rt.call(g,e)&&e!==i&&U(g,e,{get:()=>t[e],enumerable:!(n=Et(t,e))||n.enumerable});return g};var $t=(g,t,i)=>(i=g!=null?Tt(Pt(g)):{},ft(t||!g||!g.__esModule?U(i,"default",{value:g,enumerable:!0}):i,g)),Ct=g=>ft(U({},"__esModule",{value:!0}),g);var Ut={};At(Ut,{default:()=>O});module.exports=Ct(Ut);var f=require("obsidian");var bt={micropubEndpoint:"",mediaEndpoint:"",accessToken:"",defaultSyndicateTo:[],autoDiscover:!1,siteUrl:"",authorizationEndpoint:"",tokenEndpoint:"",me:"",writeUrlToFrontmatter:!0,mapGardenTags:!0,defaultVisibility:"public"};var h=require("obsidian");var k=require("obsidian"),S=class{constructor(t,i,n){this.getEndpoint=t;this.getMediaEndpoint=i;this.getToken=n}async fetchConfig(){let t=`${this.getEndpoint()}?q=config`;return(await(0,k.requestUrl)({url:t,method:"GET",headers:this.authHeaders()})).json}async discoverEndpoints(t){let n=(await(0,k.requestUrl)({url:t,method:"GET"})).text,e=this.extractLinkRel(n,"micropub"),r=this.extractLinkRel(n,"token_endpoint"),s;if(e)try{s=(await this.fetchConfigFrom(e))["media-endpoint"]}catch(o){}return{micropubEndpoint:e,tokenEndpoint:r,mediaEndpoint:s}}async createPost(t){var n,e,r;let i={type:["h-entry"],properties:t};try{let s=await(0,k.requestUrl)({url:this.getEndpoint(),method:"POST",headers:{...this.authHeaders(),"Content-Type":"application/json"},body:JSON.stringify(i),throw:!1});if(s.status===201||s.status===202)return{success:!0,url:((n=s.headers)==null?void 0:n.location)||((e=s.headers)==null?void 0:e.Location)||((r=s.json)==null?void 0:r.url)};let o=this.extractError(s.text);return{success:!1,error:`HTTP ${s.status}: ${o}`}}catch(s){return{success:!1,error:String(s)}}}async updatePost(t,i){let n={action:"update",url:t,replace:i};try{let e=await(0,k.requestUrl)({url:this.getEndpoint(),method:"POST",headers:{...this.authHeaders(),"Content-Type":"application/json"},body:JSON.stringify(n),throw:!1});return e.status>=200&&e.status<300?{success:!0,url:t}:{success:!1,error:`HTTP ${e.status}: ${this.extractError(e.text)}`}}catch(e){return{success:!1,error:String(e)}}}async uploadMedia(t,i,n){var u,T,b;let e=this.getMediaEndpoint()||`${this.getEndpoint()}/media`,r=`----MicropubBoundary${Date.now()}`,s=`--${r}\r
-Content-Disposition: form-data; name="file"; filename="${i}"\r
+"use strict";var xt=Object.create;var U=Object.defineProperty;var Pt=Object.getOwnPropertyDescriptor;var Rt=Object.getOwnPropertyNames;var At=Object.getPrototypeOf,Ct=Object.prototype.hasOwnProperty;var $t=(g,t)=>{for(var e in t)U(g,e,{get:t[e],enumerable:!0})},bt=(g,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of Rt(t))!Ct.call(g,i)&&i!==e&&U(g,i,{get:()=>t[i],enumerable:!(n=Pt(t,i))||n.enumerable});return g};var Dt=(g,t,e)=>(e=g!=null?xt(At(g)):{},bt(t||!g||!g.__esModule?U(e,"default",{value:g,enumerable:!0}):e,g)),Mt=g=>bt(U({},"__esModule",{value:!0}),g);var Ft={};$t(Ft,{default:()=>O});module.exports=Mt(Ft);var y=require("obsidian");var wt={micropubEndpoint:"",mediaEndpoint:"",accessToken:"",defaultSyndicateTo:[],autoDiscover:!1,siteUrl:"",authorizationEndpoint:"",tokenEndpoint:"",me:"",writeUrlToFrontmatter:!0,mapGardenTags:!0,defaultVisibility:"public",showSyndicationDialog:"when-needed"};var h=require("obsidian");var k=require("obsidian"),b=class{constructor(t,e,n){this.getEndpoint=t;this.getMediaEndpoint=e;this.getToken=n}async fetchConfig(){let t=`${this.getEndpoint()}?q=config`;return(await(0,k.requestUrl)({url:t,method:"GET",headers:this.authHeaders()})).json}async discoverEndpoints(t){let n=(await(0,k.requestUrl)({url:t,method:"GET"})).text,i=this.extractLinkRel(n,"micropub"),s=this.extractLinkRel(n,"token_endpoint"),r;if(i)try{r=(await this.fetchConfigFrom(i))["media-endpoint"]}catch(o){}return{micropubEndpoint:i,tokenEndpoint:s,mediaEndpoint:r}}async createPost(t){var n,i,s;let e={type:["h-entry"],properties:t};try{let r=await(0,k.requestUrl)({url:this.getEndpoint(),method:"POST",headers:{...this.authHeaders(),"Content-Type":"application/json"},body:JSON.stringify(e),throw:!1});if(r.status===201||r.status===202)return{success:!0,url:((n=r.headers)==null?void 0:n.location)||((i=r.headers)==null?void 0:i.Location)||((s=r.json)==null?void 0:s.url)};let o=this.extractError(r.text);return{success:!1,error:`HTTP ${r.status}: ${o}`}}catch(r){return{success:!1,error:String(r)}}}async updatePost(t,e){let n={action:"update",url:t,replace:e};try{let i=await(0,k.requestUrl)({url:this.getEndpoint(),method:"POST",headers:{...this.authHeaders(),"Content-Type":"application/json"},body:JSON.stringify(n),throw:!1});return i.status>=200&&i.status<300?{success:!0,url:t}:{success:!1,error:`HTTP ${i.status}: ${this.extractError(i.text)}`}}catch(i){return{success:!1,error:String(i)}}}async uploadMedia(t,e,n){var u,f,T;let i=this.getMediaEndpoint()||`${this.getEndpoint()}/media`,s=`----MicropubBoundary${Date.now()}`,r=`--${s}\r
+Content-Disposition: form-data; name="file"; filename="${e}"\r
Content-Type: ${n}\r
\r
`,o=`\r
---${r}--\r
-`,p=new TextEncoder().encode(s),l=new TextEncoder().encode(o),a=new Uint8Array(t),c=new Uint8Array(p.length+a.length+l.length);c.set(p,0),c.set(a,p.length),c.set(l,p.length+a.length);let d=await(0,k.requestUrl)({url:e,method:"POST",headers:{...this.authHeaders(),"Content-Type":`multipart/form-data; boundary=${r}`},body:c.buffer,throw:!1});if(d.status===201||d.status===202){let y=((u=d.headers)==null?void 0:u.location)||((T=d.headers)==null?void 0:T.Location)||((b=d.json)==null?void 0:b.url);if(y)return y}throw new Error(`Media upload failed (HTTP ${d.status}): ${this.extractError(d.text)}`)}authHeaders(){return{Authorization:`Bearer ${this.getToken()}`}}extractLinkRel(t,i){var r;let n=new RegExp(`]+rel=["']${i}["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["']${i}["']`,"i"),e=t.match(n);return(r=e==null?void 0:e[1])!=null?r:e==null?void 0:e[2]}async fetchConfigFrom(t){return(await(0,k.requestUrl)({url:`${t}?q=config`,method:"GET",headers:this.authHeaders()})).json}extractError(t){var i,n;try{let e=JSON.parse(t);return(n=(i=e.error_description)!=null?i:e.error)!=null?n:t.slice(0,200)}catch(e){return t.slice(0,200)}}};var R=$t(require("crypto")),_=require("obsidian"),yt="https://svemagie.github.io/obsidian-micropub/",wt="https://svemagie.github.io/obsidian-micropub/callback",vt="create update media",Mt=300*1e3,P=null;function kt(g){if(!P)return;let{resolve:t,state:i}=P;P=null,t(g)}var D=class g{static async discoverEndpoints(t){let n=(await(0,_.requestUrl)({url:t,method:"GET"})).text,e=g.extractLinkRel(n,"authorization_endpoint"),r=g.extractLinkRel(n,"token_endpoint"),s=g.extractLinkRel(n,"micropub");if(!e)throw new Error(`No found at ${t}. Make sure Indiekit is running and SITE_URL is set correctly.`);if(!r)throw new Error(`No found at ${t}.`);return{authorizationEndpoint:e,tokenEndpoint:r,micropubEndpoint:s}}static async signIn(t){var T,b,y,A,E,$;let{authorizationEndpoint:i,tokenEndpoint:n,micropubEndpoint:e}=await g.discoverEndpoints(t),r=g.base64url(R.randomBytes(16)),s=g.base64url(R.randomBytes(64)),o=g.base64url(R.createHash("sha256").update(s).digest()),p=new Promise((C,x)=>{let w=setTimeout(()=>{P=null,x(new Error("Sign-in timed out (5 min). Please try again."))},Mt);P={state:r,resolve:M=>{clearTimeout(w),C(M)}}}),l=new URL(i);l.searchParams.set("response_type","code"),l.searchParams.set("client_id",yt),l.searchParams.set("redirect_uri",wt),l.searchParams.set("state",r),l.searchParams.set("code_challenge",o),l.searchParams.set("code_challenge_method","S256"),l.searchParams.set("scope",vt),l.searchParams.set("me",t),window.open(l.toString());let a=await p;if(a.state!==r)throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");let c=a.code;if(!c)throw new Error((b=(T=a.error_description)!=null?T:a.error)!=null?b:"No authorization code received.");let d=await(0,_.requestUrl)({url:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"authorization_code",code:c,client_id:yt,redirect_uri:wt,code_verifier:s}).toString(),throw:!1}),u=d.json;if(!u.access_token)throw new Error((A=(y=u.error_description)!=null?y:u.error)!=null?A:`Token exchange failed (HTTP ${d.status})`);return{accessToken:u.access_token,scope:(E=u.scope)!=null?E:vt,me:($=u.me)!=null?$:t,authorizationEndpoint:i,tokenEndpoint:n,micropubEndpoint:e}}static base64url(t){return t.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}static extractLinkRel(t,i){var r;let n=new RegExp(`]+rel=["'][^"']*\\b${i}\\b[^"']*["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["'][^"']*\\b${i}\\b[^"']*["']`,"i"),e=t.match(n);return(r=e==null?void 0:e[1])!=null?r:e==null?void 0:e[2]}};var F=class extends h.PluginSettingTab{constructor(i,n){super(i,n);this.plugin=n}display(){let{containerEl:i}=this;i.empty(),i.createEl("h2",{text:"Micropub Publisher"}),i.createEl("h3",{text:"Account"}),this.plugin.settings.me&&this.plugin.settings.accessToken?this.renderSignedIn(i):this.renderSignedOut(i),i.createEl("h3",{text:"Endpoints"}),i.createEl("p",{text:"These are filled automatically when you sign in. Only edit them manually if your server uses non-standard paths.",cls:"setting-item-description"}),new h.Setting(i).setName("Micropub endpoint").setDesc("e.g. https://blog.giersig.eu/micropub").addText(n=>n.setPlaceholder("https://example.com/micropub").setValue(this.plugin.settings.micropubEndpoint).onChange(async e=>{this.plugin.settings.micropubEndpoint=e.trim(),await this.plugin.saveSettings()})),new h.Setting(i).setName("Media endpoint").setDesc("For image uploads. Auto-discovered if blank.").addText(n=>n.setPlaceholder("https://example.com/micropub/media").setValue(this.plugin.settings.mediaEndpoint).onChange(async e=>{this.plugin.settings.mediaEndpoint=e.trim(),await this.plugin.saveSettings()})),i.createEl("h3",{text:"Publish Behaviour"}),new h.Setting(i).setName("Default visibility").setDesc("Applies when the note has no explicit visibility property.").addDropdown(n=>n.addOption("public","Public").addOption("unlisted","Unlisted").addOption("private","Private").setValue(this.plugin.settings.defaultVisibility).onChange(async e=>{this.plugin.settings.defaultVisibility=e,await this.plugin.saveSettings()})),new h.Setting(i).setName("Write URL back to note").setDesc("After publishing, store the post URL as `mp-url` in frontmatter. Subsequent publishes will update the existing post instead of creating a new one.").addToggle(n=>n.setValue(this.plugin.settings.writeUrlToFrontmatter).onChange(async e=>{this.plugin.settings.writeUrlToFrontmatter=e,await this.plugin.saveSettings()})),i.createEl("h3",{text:"Digital Garden"}),new h.Setting(i).setName("Map #garden/* tags to gardenStage").setDesc("Obsidian tags like #garden/plant become a `garden-stage: plant` Micropub property. The blog renders these as growth stage badges at /garden/.").addToggle(n=>n.setValue(this.plugin.settings.mapGardenTags).onChange(async e=>{this.plugin.settings.mapGardenTags=e,await this.plugin.saveSettings()})),i.createEl("p",{text:"Stages: plant \u{1F331} \xB7 cultivate \u{1F33F} \xB7 question \u2753 \xB7 repot \u{1FAB4} \xB7 revitalize \u2728 \xB7 revisit \u{1F504}",cls:"setting-item-description"})}renderSignedOut(i){new h.Setting(i).setName("Site URL").setDesc("Your site's home page. Clicking Sign in opens your blog's login page in the browser \u2014 the same flow iA Writer uses.").addText(e=>e.setPlaceholder("https://blog.giersig.eu").setValue(this.plugin.settings.siteUrl).onChange(async r=>{this.plugin.settings.siteUrl=r.trim(),await this.plugin.saveSettings()})).addButton(e=>{e.setButtonText("Sign in").setCta().onClick(async()=>{let r=this.plugin.settings.siteUrl.trim();if(!r){new h.Notice("Enter your site URL first.");return}e.setDisabled(!0),e.setButtonText("Opening browser\u2026");try{let s=await D.signIn(r);if(this.plugin.settings.accessToken=s.accessToken,this.plugin.settings.me=s.me,this.plugin.settings.authorizationEndpoint=s.authorizationEndpoint,this.plugin.settings.tokenEndpoint=s.tokenEndpoint,s.micropubEndpoint&&(this.plugin.settings.micropubEndpoint=s.micropubEndpoint),s.mediaEndpoint&&(this.plugin.settings.mediaEndpoint=s.mediaEndpoint),await this.plugin.saveSettings(),!this.plugin.settings.mediaEndpoint)try{let p=await new S(()=>this.plugin.settings.micropubEndpoint,()=>this.plugin.settings.mediaEndpoint,()=>this.plugin.settings.accessToken).fetchConfig();p["media-endpoint"]&&(this.plugin.settings.mediaEndpoint=p["media-endpoint"],await this.plugin.saveSettings())}catch(o){}new h.Notice(`\u2705 Signed in as ${s.me}`),this.display()}catch(s){new h.Notice(`Sign-in failed: ${String(s)}`,8e3),e.setDisabled(!1),e.setButtonText("Sign in")}})});let n=i.createEl("details");n.createEl("summary",{text:"Or paste a token manually",cls:"setting-item-description"}),n.style.marginTop="8px",n.style.marginBottom="8px",new h.Setting(n).setName("Access token").setDesc("Bearer token from your Indiekit admin panel.").addText(e=>{e.setPlaceholder("your-bearer-token").setValue(this.plugin.settings.accessToken).onChange(async r=>{this.plugin.settings.accessToken=r.trim(),await this.plugin.saveSettings()}),e.inputEl.type="password"}).addButton(e=>e.setButtonText("Verify").onClick(async()=>{if(!this.plugin.settings.micropubEndpoint||!this.plugin.settings.accessToken){new h.Notice("Set the Micropub endpoint and token first.");return}e.setDisabled(!0);try{await new S(()=>this.plugin.settings.micropubEndpoint,()=>this.plugin.settings.mediaEndpoint,()=>this.plugin.settings.accessToken).fetchConfig(),new h.Notice("\u2705 Token is valid!")}catch(r){new h.Notice(`Token check failed: ${String(r)}`)}finally{e.setDisabled(!1)}}))}renderSignedIn(i){let n=this.plugin.settings.me,e=i.createDiv({cls:"micropub-auth-banner"});e.style.cssText="display:flex;align-items:center;gap:12px;padding:12px 16px;border:1px solid var(--background-modifier-border);border-radius:8px;margin-bottom:16px;background:var(--background-secondary);";let r=e.createDiv();r.style.cssText="width:40px;height:40px;border-radius:50%;background:var(--interactive-accent);display:flex;align-items:center;justify-content:center;font-size:1.2rem;flex-shrink:0;",r.textContent="\u{1F310}";let s=e.createDiv();s.createEl("div",{text:"Signed in",attr:{style:"font-size:.75rem;color:var(--text-muted);margin-bottom:2px"}}),s.createEl("div",{text:n,attr:{style:"font-weight:500;word-break:break-all"}}),new h.Setting(i).setName("Site URL").addText(o=>o.setValue(this.plugin.settings.siteUrl).setDisabled(!0)).addButton(o=>o.setButtonText("Sign out").setWarning().onClick(async()=>{this.plugin.settings.accessToken="",this.plugin.settings.me="",this.plugin.settings.authorizationEndpoint="",this.plugin.settings.tokenEndpoint="",await this.plugin.saveSettings(),this.display()}))}};var St=require("obsidian");var N="garden/",L=class{constructor(t,i){this.app=t;this.settings=i;this.client=new S(()=>i.micropubEndpoint,()=>i.mediaEndpoint,()=>i.accessToken)}async publish(t){let i=await this.app.vault.read(t),{frontmatter:n,body:e}=this.parseFrontmatter(i),r=n["mp-url"]!=null?String(n["mp-url"]):n.url!=null?String(n.url):void 0,{content:s,uploadedUrls:o}=await this.processImages(e),p=this.resolveWikilinks(s,t.path),l=this.buildProperties(n,p,o,t.basename,t.path),a;if(r){let c={};for(let[d,u]of Object.entries(l))c[d]=Array.isArray(u)?u:[u];a=await this.client.updatePost(r,c)}else a=await this.client.createPost(l);return a.success&&a.url&&this.settings.writeUrlToFrontmatter&&await this.writeUrlToNote(t,i,a.url),a}buildProperties(t,i,n,e,r){var V,H,W,q,Y,J,Q,X,K,Z,tt,et,it,nt,st,rt,ot,at,ct,lt,pt,gt,dt,ut,ht,mt;let s={},o=i.trim(),p=(V=t.bookmarkOf)!=null?V:t["bookmark-of"],l=(H=t.likeOf)!=null?H:t["like-of"],a=(W=t.inReplyTo)!=null?W:t["in-reply-to"],c=(q=t.repostOf)!=null?q:t["repost-of"];p&&(s["bookmark-of"]=[String(p)]),l&&(s["like-of"]=[String(l)]),a&&(s["in-reply-to"]=[String(a)]),c&&(s["repost-of"]=[String(c)]),(l||c)&&!o||(s.content=o?[{html:o}]:[{html:""}]);let u=(Q=(J=(Y=t.postType)!=null?Y:t.posttype)!=null?J:t["post-type"])!=null?Q:t.type;if(u==="article"||!u&&!!((X=t.title)!=null?X:t.name)){let m=(Z=(K=t.title)!=null?K:t.name)!=null?Z:e;s.name=[String(m)]}((tt=t.summary)!=null?tt:t.excerpt)&&(s.summary=[String((et=t.summary)!=null?et:t.excerpt)]);let b=(it=t.created)!=null?it:t.date;b&&(s.published=[new Date(String(b)).toISOString()]);let y=[...this.resolveArray(t.tags),...this.resolveArray(t.category)],A=this.extractGardenStage(y),E=y.filter(m=>!m.startsWith(N)&&m!=="garden");if(E.length>0&&(s.category=[...new Set(E)]),this.settings.mapGardenTags){let m=(nt=t.gardenStage)!=null?nt:A;if(m&&(s.gardenStage=[m],m==="evergreen")){let v=t["evergreen-since"];v&&(s.evergreenSince=[String(v)])}}let $=this.resolveArray((st=t["mp-syndicate-to"])!=null?st:t.mpSyndicateTo),C=[...new Set([...this.settings.defaultSyndicateTo,...$])];C.length>0&&(s["mp-syndicate-to"]=C);let x=(rt=t.visibility)!=null?rt:this.settings.defaultVisibility;x&&x!=="public"&&(s.visibility=[x]);let w=t.ai&&typeof t.ai=="object"?t.ai:{},M=(at=(ot=t["ai-text-level"])!=null?ot:t.aiTextLevel)!=null?at:w.textLevel,j=(lt=(ct=t["ai-code-level"])!=null?ct:t.aiCodeLevel)!=null?lt:w.codeLevel,B=(dt=(gt=(pt=t["ai-tools"])!=null?pt:t.aiTools)!=null?gt:w.aiTools)!=null?dt:w.tools,G=(mt=(ht=(ut=t["ai-description"])!=null?ut:t.aiDescription)!=null?ht:w.aiDescription)!=null?mt:w.description;M!=null&&(s["ai-text-level"]=[String(M)]),j!=null&&(s["ai-code-level"]=[String(j)]),B!=null&&(s["ai-tools"]=[String(B)]),G!=null&&(s["ai-description"]=[String(G)]);let I=this.resolvePhotoArray(t.photo);I.length>0&&(s.photo=I);let z=this.resolveArray(t.related);if(z.length>0){let m=z.map(v=>this.resolveWikilinkToUrl(v,r)).filter(v=>v!==null);m.length>0&&(s.related=m)}for(let[m,v]of Object.entries(t))m.startsWith("mp-")&&m!=="mp-url"&&m!=="mp-syndicate-to"&&(s[m]=this.resolveArray(v));return s}resolvePhotoArray(t){return t?(Array.isArray(t)?t:[t]).map(n=>{var e,r;if(typeof n=="string")return{value:n};if(typeof n=="object"&&n!==null){let s=n,o=String((r=(e=s.url)!=null?e:s.value)!=null?r:"");return o?s.alt?{value:o,alt:String(s.alt)}:{value:o}:null}return null}).filter(n=>n!==null):[]}extractGardenStage(t){for(let i of t){let n=i.replace(/^#/,"");if(n.startsWith(N)){let e=n.slice(N.length);if(["plant","cultivate","evergreen","question","repot","revitalize","revisit"].includes(e))return e}}}async processImages(t){let i=[],n=/!\[\[([^\]]+\.(png|jpg|jpeg|gif|webp|svg))\]\]/gi,e=/!\[([^\]]*)\]\(([^)]+\.(png|jpg|jpeg|gif|webp|svg))\)/gi,r=t,s=[...t.matchAll(n)];for(let p of s){let l=p[1];try{let a=await this.uploadLocalFile(l);a&&(i.push(a),r=r.replace(p[0],``))}catch(a){console.warn(`[micropub] Failed to upload ${l}:`,a)}}let o=[...r.matchAll(e)];for(let p of o){let l=p[1],a=p[2];if(!a.startsWith("http"))try{let c=await this.uploadLocalFile(a);c&&(i.push(c),r=r.replace(p[0],``))}catch(c){console.warn(`[micropub] Failed to upload ${a}:`,c)}}return{content:r,uploadedUrls:i}}async uploadLocalFile(t){let i=this.app.vault.getFiles().find(r=>r.name===t||r.path===t);if(!i)return;let n=await this.app.vault.readBinary(i),e=this.guessMimeType(i.extension);return this.client.uploadMedia(n,i.name,e)}parseFrontmatter(t){var e;let i=t.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);if(!i)return{frontmatter:{},body:t};let n={};try{n=(e=(0,St.parseYaml)(i[1]))!=null?e:{}}catch(r){}return{frontmatter:n,body:i[2]}}async writeUrlToNote(t,i,n){var a;let e=new Date,r=[e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0")].join("-"),s=[["mp-url",`"${n}"`],["post-status","published"],["published",r]];if(this.settings.siteUrl)try{let c=new URL(this.settings.siteUrl).hostname.replace(/^www\./,"");s.push(["medium",`"[[${c}]]"`])}catch(c){}{let{frontmatter:c}=this.parseFrontmatter(i);if(!c["evergreen-since"]){let d=[...this.resolveArray(c.tags),...this.resolveArray(c.category)];((a=c.gardenStage)!=null?a:this.extractGardenStage(d))==="evergreen"&&s.push(["evergreen-since",r])}}let o=i.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/);if(!o){let c=s.map(([d,u])=>`${d}: ${u}`).join(`
+--${s}--\r
+`,a=new TextEncoder().encode(r),c=new TextEncoder().encode(o),l=new Uint8Array(t),d=new Uint8Array(a.length+l.length+c.length);d.set(a,0),d.set(l,a.length),d.set(c,a.length+l.length);let p=await(0,k.requestUrl)({url:i,method:"POST",headers:{...this.authHeaders(),"Content-Type":`multipart/form-data; boundary=${s}`},body:d.buffer,throw:!1});if(p.status===201||p.status===202){let w=((u=p.headers)==null?void 0:u.location)||((f=p.headers)==null?void 0:f.Location)||((T=p.json)==null?void 0:T.url);if(w)return w}throw new Error(`Media upload failed (HTTP ${p.status}): ${this.extractError(p.text)}`)}authHeaders(){return{Authorization:`Bearer ${this.getToken()}`}}extractLinkRel(t,e){var s;let n=new RegExp(`]+rel=["']${e}["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["']${e}["']`,"i"),i=t.match(n);return(s=i==null?void 0:i[1])!=null?s:i==null?void 0:i[2]}async fetchConfigFrom(t){return(await(0,k.requestUrl)({url:`${t}?q=config`,method:"GET",headers:this.authHeaders()})).json}extractError(t){var e,n;try{let i=JSON.parse(t);return(n=(e=i.error_description)!=null?e:i.error)!=null?n:t.slice(0,200)}catch(i){return t.slice(0,200)}}};var A=Dt(require("crypto")),j=require("obsidian"),vt="https://svemagie.github.io/obsidian-micropub/",St="https://svemagie.github.io/obsidian-micropub/callback",kt="create update media",Ut=300*1e3,R=null;function Tt(g){if(!R)return;let{resolve:t,state:e}=R;R=null,t(g)}var F=class g{static async discoverEndpoints(t){let n=(await(0,j.requestUrl)({url:t,method:"GET"})).text,i=g.extractLinkRel(n,"authorization_endpoint"),s=g.extractLinkRel(n,"token_endpoint"),r=g.extractLinkRel(n,"micropub");if(!i)throw new Error(`No found at ${t}. Make sure Indiekit is running and SITE_URL is set correctly.`);if(!s)throw new Error(`No found at ${t}.`);return{authorizationEndpoint:i,tokenEndpoint:s,micropubEndpoint:r}}static async signIn(t){var f,T,w,E,$,x;let{authorizationEndpoint:e,tokenEndpoint:n,micropubEndpoint:i}=await g.discoverEndpoints(t),s=g.base64url(A.randomBytes(16)),r=g.base64url(A.randomBytes(64)),o=g.base64url(A.createHash("sha256").update(r).digest()),a=new Promise((D,P)=>{let v=setTimeout(()=>{R=null,P(new Error("Sign-in timed out (5 min). Please try again."))},Ut);R={state:s,resolve:M=>{clearTimeout(v),D(M)}}}),c=new URL(e);c.searchParams.set("response_type","code"),c.searchParams.set("client_id",vt),c.searchParams.set("redirect_uri",St),c.searchParams.set("state",s),c.searchParams.set("code_challenge",o),c.searchParams.set("code_challenge_method","S256"),c.searchParams.set("scope",kt),c.searchParams.set("me",t),window.open(c.toString());let l=await a;if(l.state!==s)throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");let d=l.code;if(!d)throw new Error((T=(f=l.error_description)!=null?f:l.error)!=null?T:"No authorization code received.");let p=await(0,j.requestUrl)({url:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"authorization_code",code:d,client_id:vt,redirect_uri:St,code_verifier:r}).toString(),throw:!1}),u=p.json;if(!u.access_token)throw new Error((E=(w=u.error_description)!=null?w:u.error)!=null?E:`Token exchange failed (HTTP ${p.status})`);return{accessToken:u.access_token,scope:($=u.scope)!=null?$:kt,me:(x=u.me)!=null?x:t,authorizationEndpoint:e,tokenEndpoint:n,micropubEndpoint:i}}static base64url(t){return t.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}static extractLinkRel(t,e){var s;let n=new RegExp(`]+rel=["'][^"']*\\b${e}\\b[^"']*["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["'][^"']*\\b${e}\\b[^"']*["']`,"i"),i=t.match(n);return(s=i==null?void 0:i[1])!=null?s:i==null?void 0:i[2]}};var L=class extends h.PluginSettingTab{constructor(e,n){super(e,n);this.plugin=n}display(){let{containerEl:e}=this;e.empty(),e.createEl("h2",{text:"Micropub Publisher"}),e.createEl("h3",{text:"Account"}),this.plugin.settings.me&&this.plugin.settings.accessToken?this.renderSignedIn(e):this.renderSignedOut(e),e.createEl("h3",{text:"Endpoints"}),e.createEl("p",{text:"These are filled automatically when you sign in. Only edit them manually if your server uses non-standard paths.",cls:"setting-item-description"}),new h.Setting(e).setName("Micropub endpoint").setDesc("e.g. https://blog.giersig.eu/micropub").addText(s=>s.setPlaceholder("https://example.com/micropub").setValue(this.plugin.settings.micropubEndpoint).onChange(async r=>{this.plugin.settings.micropubEndpoint=r.trim(),await this.plugin.saveSettings()})),new h.Setting(e).setName("Media endpoint").setDesc("For image uploads. Auto-discovered if blank.").addText(s=>s.setPlaceholder("https://example.com/micropub/media").setValue(this.plugin.settings.mediaEndpoint).onChange(async r=>{this.plugin.settings.mediaEndpoint=r.trim(),await this.plugin.saveSettings()})),e.createEl("h3",{text:"Publish Behaviour"}),new h.Setting(e).setName("Default visibility").setDesc("Applies when the note has no explicit visibility property.").addDropdown(s=>s.addOption("public","Public").addOption("unlisted","Unlisted").addOption("private","Private").setValue(this.plugin.settings.defaultVisibility).onChange(async r=>{this.plugin.settings.defaultVisibility=r,await this.plugin.saveSettings()})),new h.Setting(e).setName("Write URL back to note").setDesc("After publishing, store the post URL as `mp-url` in frontmatter. Subsequent publishes will update the existing post instead of creating a new one.").addToggle(s=>s.setValue(this.plugin.settings.writeUrlToFrontmatter).onChange(async r=>{this.plugin.settings.writeUrlToFrontmatter=r,await this.plugin.saveSettings()})),new h.Setting(e).setName("Syndication dialog").setDesc("When to show the cross-posting dialog before publishing. 'When needed' shows it only if the note has no mp-syndicate-to frontmatter.").addDropdown(s=>s.addOption("when-needed","When needed").addOption("always","Always").addOption("never","Never").setValue(this.plugin.settings.showSyndicationDialog).onChange(async r=>{this.plugin.settings.showSyndicationDialog=r,await this.plugin.saveSettings()}));let n=this.plugin.settings.defaultSyndicateTo,i=new h.Setting(e).setName("Default syndication targets").setDesc(n.length>0?n.join(", "):"None configured. Targets checked by default in the publish dialog.");n.length>0&&i.addButton(s=>s.setButtonText("Clear defaults").setWarning().onClick(async()=>{this.plugin.settings.defaultSyndicateTo=[],await this.plugin.saveSettings(),this.display()})),e.createEl("h3",{text:"Digital Garden"}),new h.Setting(e).setName("Map #garden/* tags to gardenStage").setDesc("Obsidian tags like #garden/plant become a `garden-stage: plant` Micropub property. The blog renders these as growth stage badges at /garden/.").addToggle(s=>s.setValue(this.plugin.settings.mapGardenTags).onChange(async r=>{this.plugin.settings.mapGardenTags=r,await this.plugin.saveSettings()})),e.createEl("p",{text:"Stages: plant \u{1F331} \xB7 cultivate \u{1F33F} \xB7 question \u2753 \xB7 repot \u{1FAB4} \xB7 revitalize \u2728 \xB7 revisit \u{1F504}",cls:"setting-item-description"})}renderSignedOut(e){new h.Setting(e).setName("Site URL").setDesc("Your site's home page. Clicking Sign in opens your blog's login page in the browser \u2014 the same flow iA Writer uses.").addText(i=>i.setPlaceholder("https://blog.giersig.eu").setValue(this.plugin.settings.siteUrl).onChange(async s=>{this.plugin.settings.siteUrl=s.trim(),await this.plugin.saveSettings()})).addButton(i=>{i.setButtonText("Sign in").setCta().onClick(async()=>{let s=this.plugin.settings.siteUrl.trim();if(!s){new h.Notice("Enter your site URL first.");return}i.setDisabled(!0),i.setButtonText("Opening browser\u2026");try{let r=await F.signIn(s);if(this.plugin.settings.accessToken=r.accessToken,this.plugin.settings.me=r.me,this.plugin.settings.authorizationEndpoint=r.authorizationEndpoint,this.plugin.settings.tokenEndpoint=r.tokenEndpoint,r.micropubEndpoint&&(this.plugin.settings.micropubEndpoint=r.micropubEndpoint),r.mediaEndpoint&&(this.plugin.settings.mediaEndpoint=r.mediaEndpoint),await this.plugin.saveSettings(),!this.plugin.settings.mediaEndpoint)try{let a=await new b(()=>this.plugin.settings.micropubEndpoint,()=>this.plugin.settings.mediaEndpoint,()=>this.plugin.settings.accessToken).fetchConfig();a["media-endpoint"]&&(this.plugin.settings.mediaEndpoint=a["media-endpoint"],await this.plugin.saveSettings())}catch(o){}new h.Notice(`\u2705 Signed in as ${r.me}`),this.display()}catch(r){new h.Notice(`Sign-in failed: ${String(r)}`,8e3),i.setDisabled(!1),i.setButtonText("Sign in")}})});let n=e.createEl("details");n.createEl("summary",{text:"Or paste a token manually",cls:"setting-item-description"}),n.style.marginTop="8px",n.style.marginBottom="8px",new h.Setting(n).setName("Access token").setDesc("Bearer token from your Indiekit admin panel.").addText(i=>{i.setPlaceholder("your-bearer-token").setValue(this.plugin.settings.accessToken).onChange(async s=>{this.plugin.settings.accessToken=s.trim(),await this.plugin.saveSettings()}),i.inputEl.type="password"}).addButton(i=>i.setButtonText("Verify").onClick(async()=>{if(!this.plugin.settings.micropubEndpoint||!this.plugin.settings.accessToken){new h.Notice("Set the Micropub endpoint and token first.");return}i.setDisabled(!0);try{await new b(()=>this.plugin.settings.micropubEndpoint,()=>this.plugin.settings.mediaEndpoint,()=>this.plugin.settings.accessToken).fetchConfig(),new h.Notice("\u2705 Token is valid!")}catch(s){new h.Notice(`Token check failed: ${String(s)}`)}finally{i.setDisabled(!1)}}))}renderSignedIn(e){let n=this.plugin.settings.me,i=e.createDiv({cls:"micropub-auth-banner"});i.style.cssText="display:flex;align-items:center;gap:12px;padding:12px 16px;border:1px solid var(--background-modifier-border);border-radius:8px;margin-bottom:16px;background:var(--background-secondary);";let s=i.createDiv();s.style.cssText="width:40px;height:40px;border-radius:50%;background:var(--interactive-accent);display:flex;align-items:center;justify-content:center;font-size:1.2rem;flex-shrink:0;",s.textContent="\u{1F310}";let r=i.createDiv();r.createEl("div",{text:"Signed in",attr:{style:"font-size:.75rem;color:var(--text-muted);margin-bottom:2px"}}),r.createEl("div",{text:n,attr:{style:"font-weight:500;word-break:break-all"}}),new h.Setting(e).setName("Site URL").addText(o=>o.setValue(this.plugin.settings.siteUrl).setDisabled(!0)).addButton(o=>o.setButtonText("Sign out").setWarning().onClick(async()=>{this.plugin.settings.accessToken="",this.plugin.settings.me="",this.plugin.settings.authorizationEndpoint="",this.plugin.settings.tokenEndpoint="",await this.plugin.saveSettings(),this.display()}))}};var Et=require("obsidian");var _="garden/",N=class{constructor(t,e){this.app=t;this.settings=e;this.client=new b(()=>e.micropubEndpoint,()=>e.mediaEndpoint,()=>e.accessToken)}async publish(t,e){let n=await this.app.vault.read(t),{frontmatter:i,body:s}=this.parseFrontmatter(n),r=i["mp-url"]!=null?String(i["mp-url"]):i.url!=null?String(i.url):void 0,{content:o,uploadedUrls:a}=await this.processImages(s),c=this.resolveWikilinks(o,t.path),l=this.buildProperties(i,c,a,t.basename,t.path,e),d;if(r){let p={};for(let[u,f]of Object.entries(l))p[u]=Array.isArray(f)?f:[f];d=await this.client.updatePost(r,p)}else d=await this.client.createPost(l);return d.success&&this.settings.writeUrlToFrontmatter&&(d.url?await this.writeUrlToNote(t,n,d.url,e):e!==void 0&&await this.writeSyndicateToNote(t,n,e)),d}buildProperties(t,e,n,i,s,r){var H,q,Y,J,Q,X,K,Z,tt,et,it,nt,st,rt,ot,at,ct,lt,dt,pt,gt,ut,ht,mt,ft,yt;let o={},a=e.trim(),c=(H=t.bookmarkOf)!=null?H:t["bookmark-of"],l=(q=t.likeOf)!=null?q:t["like-of"],d=(Y=t.inReplyTo)!=null?Y:t["in-reply-to"],p=(J=t.repostOf)!=null?J:t["repost-of"];c&&(o["bookmark-of"]=[String(c)]),l&&(o["like-of"]=[String(l)]),d&&(o["in-reply-to"]=[String(d)]),p&&(o["repost-of"]=[String(p)]),(l||p)&&!a||(o.content=a?[{html:a}]:[{html:""}]);let f=(K=(X=(Q=t.postType)!=null?Q:t.posttype)!=null?X:t["post-type"])!=null?K:t.type;if(f==="article"||!f&&!!((Z=t.title)!=null?Z:t.name)){let m=(et=(tt=t.title)!=null?tt:t.name)!=null?et:i;o.name=[String(m)]}((it=t.summary)!=null?it:t.excerpt)&&(o.summary=[String((nt=t.summary)!=null?nt:t.excerpt)]);let w=(st=t.created)!=null?st:t.date;w&&(o.published=[new Date(String(w)).toISOString()]);let E=[...this.resolveArray(t.tags),...this.resolveArray(t.category)],$=this.extractGardenStage(E),x=E.filter(m=>!m.startsWith(_)&&m!=="garden");if(x.length>0&&(o.category=[...new Set(x)]),this.settings.mapGardenTags){let m=(rt=t.gardenStage)!=null?rt:$;if(m&&(o.gardenStage=[m],m==="evergreen")){let S=t["evergreen-since"];S&&(o.evergreenSince=[String(S)])}}let D=r!==void 0?r:[...new Set([...this.settings.defaultSyndicateTo,...this.resolveArray((ot=t["mp-syndicate-to"])!=null?ot:t.mpSyndicateTo)])];D.length>0&&(o["mp-syndicate-to"]=D);let P=(at=t.visibility)!=null?at:this.settings.defaultVisibility;P&&P!=="public"&&(o.visibility=[P]);let v=t.ai&&typeof t.ai=="object"?t.ai:{},M=(lt=(ct=t["ai-text-level"])!=null?ct:t.aiTextLevel)!=null?lt:v.textLevel,G=(pt=(dt=t["ai-code-level"])!=null?dt:t.aiCodeLevel)!=null?pt:v.codeLevel,I=(ht=(ut=(gt=t["ai-tools"])!=null?gt:t.aiTools)!=null?ut:v.aiTools)!=null?ht:v.tools,z=(yt=(ft=(mt=t["ai-description"])!=null?mt:t.aiDescription)!=null?ft:v.aiDescription)!=null?yt:v.description;M!=null&&(o["ai-text-level"]=[String(M)]),G!=null&&(o["ai-code-level"]=[String(G)]),I!=null&&(o["ai-tools"]=[String(I)]),z!=null&&(o["ai-description"]=[String(z)]);let W=this.resolvePhotoArray(t.photo);W.length>0&&(o.photo=W);let V=this.resolveArray(t.related);if(V.length>0){let m=V.map(S=>this.resolveWikilinkToUrl(S,s)).filter(S=>S!==null);m.length>0&&(o.related=m)}for(let[m,S]of Object.entries(t))m.startsWith("mp-")&&m!=="mp-url"&&m!=="mp-syndicate-to"&&(o[m]=this.resolveArray(S));return o}resolvePhotoArray(t){return t?(Array.isArray(t)?t:[t]).map(n=>{var i,s;if(typeof n=="string")return{value:n};if(typeof n=="object"&&n!==null){let r=n,o=String((s=(i=r.url)!=null?i:r.value)!=null?s:"");return o?r.alt?{value:o,alt:String(r.alt)}:{value:o}:null}return null}).filter(n=>n!==null):[]}extractGardenStage(t){for(let e of t){let n=e.replace(/^#/,"");if(n.startsWith(_)){let i=n.slice(_.length);if(["plant","cultivate","evergreen","question","repot","revitalize","revisit"].includes(i))return i}}}async processImages(t){let e=[],n=/!\[\[([^\]]+\.(png|jpg|jpeg|gif|webp|svg))\]\]/gi,i=/!\[([^\]]*)\]\(([^)]+\.(png|jpg|jpeg|gif|webp|svg))\)/gi,s=t,r=[...t.matchAll(n)];for(let a of r){let c=a[1];try{let l=await this.uploadLocalFile(c);l&&(e.push(l),s=s.replace(a[0],``))}catch(l){console.warn(`[micropub] Failed to upload ${c}:`,l)}}let o=[...s.matchAll(i)];for(let a of o){let c=a[1],l=a[2];if(!l.startsWith("http"))try{let d=await this.uploadLocalFile(l);d&&(e.push(d),s=s.replace(a[0],``))}catch(d){console.warn(`[micropub] Failed to upload ${l}:`,d)}}return{content:s,uploadedUrls:e}}async uploadLocalFile(t){let e=this.app.vault.getFiles().find(s=>s.name===t||s.path===t);if(!e)return;let n=await this.app.vault.readBinary(e),i=this.guessMimeType(e.extension);return this.client.uploadMedia(n,e.name,i)}parseFrontmatter(t){var i;let e=t.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);if(!e)return{frontmatter:{},body:t};let n={};try{n=(i=(0,Et.parseYaml)(e[1]))!=null?i:{}}catch(s){}return{frontmatter:n,body:e[2]}}async writeUrlToNote(t,e,n,i){var d;let s=new Date,r=[s.getFullYear(),String(s.getMonth()+1).padStart(2,"0"),String(s.getDate()).padStart(2,"0")].join("-"),o=[["mp-url",`"${n}"`],["post-status","published"],["published",r]];if(i!==void 0&&o.push(["mp-syndicate-to",`[${i.join(", ")}]`]),this.settings.siteUrl)try{let p=new URL(this.settings.siteUrl).hostname.replace(/^www\./,"");o.push(["medium",`"[[${p}]]"`])}catch(p){}{let{frontmatter:p}=this.parseFrontmatter(e);if(!p["evergreen-since"]){let u=[...this.resolveArray(p.tags),...this.resolveArray(p.category)];((d=p.gardenStage)!=null?d:this.extractGardenStage(u))==="evergreen"&&o.push(["evergreen-since",r])}}let a=e.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/);if(!a){let p=o.map(([u,f])=>`${u}: ${f}`).join(`
`);await this.app.vault.modify(t,`---
-${c}
+${p}
---
-`+i);return}let p=o[1],l=o[2];for(let[c,d]of s)p=this.setFrontmatterField(p,c,d);await this.app.vault.modify(t,p+l)}setFrontmatterField(t,i,n){let e=new RegExp(`^${i}:.*$`,"m");return e.test(t)?t.replace(e,`${i}: ${n}`):t.replace(/(\r?\n---\r?\n)$/,`
-${i}: ${n}$1`)}resolveWikilinks(t,i){return t.replace(/(?{let o=e.trim(),p=(s==null?void 0:s.trim())||o.split("/").pop()||o,l=this.resolveWikilinkToUrl(`[[${o}]]`,i);if(!l)return p;let a=r?r.toLowerCase().replace(/\s+/g,"-"):"";return`[${p}](${l}${a})`})}resolveWikilinkToUrl(t,i){var r,s,o;if(t.startsWith("http"))return t;let n=t.match(/^\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]$/);if(!n)return null;let e=this.app.metadataCache.getFirstLinkpathDest(n[1].trim(),i);return e&&(o=(s=(r=this.app.metadataCache.getFileCache(e))==null?void 0:r.frontmatter)==null?void 0:s["mp-url"])!=null?o:null}resolveArray(t){return t?Array.isArray(t)?t.map(String):[String(t)]:[]}guessMimeType(t){var n;return(n={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml"}[t.toLowerCase()])!=null?n:"application/octet-stream"}};var O=class extends f.Plugin{async onload(){await this.loadSettings(),this.addCommand({id:"publish-to-micropub",name:"Publish to Micropub",checkCallback:t=>{let i=this.app.workspace.getActiveFile();return!i||i.extension!=="md"?!1:(t||this.publishActiveNote(i),!0)}}),this.addCommand({id:"publish-to-micropub-update",name:"Update existing Micropub post",checkCallback:t=>{let i=this.app.workspace.getActiveFile();return!i||i.extension!=="md"?!1:(t||this.publishActiveNote(i),!0)}}),this.registerObsidianProtocolHandler("micropub-auth",t=>{kt(t)}),this.addSettingTab(new F(this.app,this)),this.addRibbonIcon("send","Publish to Micropub",()=>{let t=this.app.workspace.getActiveFile();if(!t||t.extension!=="md"){new f.Notice("Open a Markdown note to publish.");return}this.publishActiveNote(t)})}onunload(){}async publishActiveNote(t){if(!this.settings.micropubEndpoint){new f.Notice("\u26A0\uFE0F Micropub endpoint not configured. Open plugin settings to add it.");return}if(!this.settings.accessToken){new f.Notice("\u26A0\uFE0F Access token not configured. Open plugin settings to add it.");return}let i=new f.Notice("Publishing\u2026",0);try{let e=await new L(this.app,this.settings).publish(t);if(i.hide(),e.success){let r=e.url?`
-${e.url}`:"";new f.Notice(`\u2705 Published!${r}`,8e3)}else new f.Notice(`\u274C Publish failed: ${e.error}`,1e4),console.error("[micropub] Publish failed:",e.error)}catch(n){i.hide();let e=n instanceof Error?n.message:String(n);new f.Notice(`\u274C Error: ${e}`,1e4),console.error("[micropub] Unexpected error:",n)}}async loadSettings(){this.settings=Object.assign({},bt,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
+`+e);return}let c=a[1],l=a[2];for(let[p,u]of o)c=this.setFrontmatterField(c,p,u);await this.app.vault.modify(t,c+l)}async writeSyndicateToNote(t,e,n){let i=e.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/),s=`[${n.join(", ")}]`;if(!i){await this.app.vault.modify(t,`---
+mp-syndicate-to: ${s}
+---
+`+e);return}let r=this.setFrontmatterField(i[1],"mp-syndicate-to",s);await this.app.vault.modify(t,r+i[2])}setFrontmatterField(t,e,n){let i=new RegExp(`^${e}:.*$`,"m");return i.test(t)?t.replace(i,`${e}: ${n}`):t.replace(/(\r?\n---\r?\n)$/,`
+${e}: ${n}$1`)}resolveWikilinks(t,e){return t.replace(/(?{let o=i.trim(),a=(r==null?void 0:r.trim())||o.split("/").pop()||o,c=this.resolveWikilinkToUrl(`[[${o}]]`,e);if(!c)return a;let l=s?s.toLowerCase().replace(/\s+/g,"-"):"";return`[${a}](${c}${l})`})}resolveWikilinkToUrl(t,e){var s,r,o;if(t.startsWith("http"))return t;let n=t.match(/^\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]$/);if(!n)return null;let i=this.app.metadataCache.getFirstLinkpathDest(n[1].trim(),e);return i&&(o=(r=(s=this.app.metadataCache.getFileCache(i))==null?void 0:s.frontmatter)==null?void 0:r["mp-url"])!=null?o:null}resolveArray(t){return t?Array.isArray(t)?t.map(String):[String(t)]:[]}guessMimeType(t){var n;return(n={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml"}[t.toLowerCase()])!=null?n:"application/octet-stream"}};var C=require("obsidian"),B=class extends C.Modal{constructor(e,n,i){super(e);this.targets=n;this.resolvePromise=null;this.resolved=!1;this.selected=new Set(i.filter(s=>n.some(r=>r.uid===s)))}async awaitSelection(){return new Promise(e=>{this.resolvePromise=e,this.open()})}onOpen(){let{contentEl:e}=this;e.createEl("h2",{text:"Syndication targets"}),e.createEl("p",{text:"Choose where to cross-post this note.",cls:"setting-item-description"});for(let n of this.targets)new C.Setting(e).setName(n.name).addToggle(i=>i.setValue(this.selected.has(n.uid)).onChange(s=>{s?this.selected.add(n.uid):this.selected.delete(n.uid)}));new C.Setting(e).addButton(n=>n.setButtonText("Cancel").onClick(()=>{this.finish(null)})).addButton(n=>n.setButtonText("Publish").setCta().onClick(()=>{this.finish([...this.selected])}))}onClose(){this.finish(null),this.contentEl.empty()}finish(e){var n;this.resolved||(this.resolved=!0,(n=this.resolvePromise)==null||n.call(this,e),this.resolvePromise=null)}};var O=class extends y.Plugin{async onload(){await this.loadSettings(),this.addCommand({id:"publish-to-micropub",name:"Publish to Micropub",checkCallback:t=>{let e=this.app.workspace.getActiveFile();return!e||e.extension!=="md"?!1:(t||this.publishActiveNote(e),!0)}}),this.addCommand({id:"publish-to-micropub-update",name:"Update existing Micropub post",checkCallback:t=>{let e=this.app.workspace.getActiveFile();return!e||e.extension!=="md"?!1:(t||this.publishActiveNote(e),!0)}}),this.registerObsidianProtocolHandler("micropub-auth",t=>{Tt(t)}),this.addSettingTab(new L(this.app,this)),this.addRibbonIcon("send","Publish to Micropub",()=>{let t=this.app.workspace.getActiveFile();if(!t||t.extension!=="md"){new y.Notice("Open a Markdown note to publish.");return}this.publishActiveNote(t)})}onunload(){}async publishActiveNote(t){if(!this.settings.micropubEndpoint){new y.Notice("\u26A0\uFE0F Micropub endpoint not configured. Open plugin settings to add it.");return}if(!this.settings.accessToken){new y.Notice("\u26A0\uFE0F Access token not configured. Open plugin settings to add it.");return}let e=await this.resolveSyndicationTargets(t);if(e===null)return;let n=new y.Notice("Publishing\u2026",0);try{let s=await new N(this.app,this.settings).publish(t,e);if(n.hide(),s.success){let r=s.url?`
+${s.url}`:"";new y.Notice(`\u2705 Published!${r}`,8e3)}else new y.Notice(`\u274C Publish failed: ${s.error}`,1e4),console.error("[micropub] Publish failed:",s.error)}catch(i){n.hide();let s=i instanceof Error?i.message:String(i);new y.Notice(`\u274C Error: ${s}`,1e4),console.error("[micropub] Unexpected error:",i)}}async resolveSyndicationTargets(t){var a,c;let e=this.settings.showSyndicationDialog;if(e==="never")return;let n=[];try{n=(a=(await new b(()=>this.settings.micropubEndpoint,()=>this.settings.mediaEndpoint,()=>this.settings.accessToken).fetchConfig())["syndicate-to"])!=null?a:[]}catch(l){new y.Notice("\u26A0\uFE0F Could not fetch syndication targets. Publishing without dialog.",4e3);return}if(n.length===0)return;let i;try{let d=(await this.app.vault.read(t)).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(d){let u=((c=(0,y.parseYaml)(d[1]))!=null?c:{})["mp-syndicate-to"];u!==void 0&&(i=Array.isArray(u)?u.map(String):[String(u)])}}catch(l){}if(!(e==="always"||e==="when-needed"&&i===void 0||i!==void 0&&i.length===0))return;let r=i&&i.length>0?i:this.settings.defaultSyndicateTo;return new B(this.app,n,r).awaitSelection()}async loadSettings(){this.settings=Object.assign({},wt,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
diff --git a/src/Publisher.ts b/src/Publisher.ts
index 8a6b6fe..d70a7db 100644
--- a/src/Publisher.ts
+++ b/src/Publisher.ts
@@ -35,7 +35,7 @@ export class Publisher {
}
/** Publish the given file. Returns PublishResult. */
- async publish(file: TFile): Promise {
+ async publish(file: TFile, syndicateToOverride?: string[]): Promise {
const raw = await this.app.vault.read(file);
const { frontmatter, body } = this.parseFrontmatter(raw);
@@ -53,7 +53,7 @@ export class Publisher {
const linkedBody = this.resolveWikilinks(processedBody, file.path);
// Build Micropub properties
- const properties = this.buildProperties(frontmatter, linkedBody, uploadedUrls, file.basename, file.path);
+ const properties = this.buildProperties(frontmatter, linkedBody, uploadedUrls, file.basename, file.path, syndicateToOverride);
let result: PublishResult;
@@ -69,9 +69,14 @@ export class Publisher {
result = await this.client.createPost(properties);
}
- // Write URL back to frontmatter
- if (result.success && result.url && this.settings.writeUrlToFrontmatter) {
- await this.writeUrlToNote(file, raw, result.url);
+ // Write URL (and syndication targets) back to frontmatter
+ if (result.success && this.settings.writeUrlToFrontmatter) {
+ if (result.url) {
+ await this.writeUrlToNote(file, raw, result.url, syndicateToOverride);
+ } else if (syndicateToOverride !== undefined) {
+ // No URL returned but we still want to record the syndication targets
+ await this.writeSyndicateToNote(file, raw, syndicateToOverride);
+ }
}
return result;
@@ -85,6 +90,7 @@ export class Publisher {
uploadedUrls: string[],
basename: string,
filePath: string,
+ syndicateToOverride?: string[],
): Record {
const props: Record = {};
@@ -176,13 +182,17 @@ export class Publisher {
}
// Syndication targets
- // Support both camelCase (mpSyndicateTo) used in existing blog posts and mp-syndicate-to
- const syndicateTo = this.resolveArray(
- fm["mp-syndicate-to"] ?? fm["mpSyndicateTo"],
- );
- const allSyndicateTo = [
- ...new Set([...this.settings.defaultSyndicateTo, ...syndicateTo]),
- ];
+ // When the dialog was shown, syndicateToOverride contains the user's selection
+ // and takes precedence over frontmatter + settings defaults.
+ // Support both camelCase (mpSyndicateTo) used in existing blog posts and mp-syndicate-to.
+ const allSyndicateTo = syndicateToOverride !== undefined
+ ? syndicateToOverride
+ : [
+ ...new Set([
+ ...this.settings.defaultSyndicateTo,
+ ...this.resolveArray(fm["mp-syndicate-to"] ?? fm["mpSyndicateTo"]),
+ ]),
+ ];
if (allSyndicateTo.length > 0) {
props["mp-syndicate-to"] = allSyndicateTo;
}
@@ -375,6 +385,7 @@ export class Publisher {
file: TFile,
originalContent: string,
url: string,
+ syndicateToOverride?: string[],
): Promise {
// Build all fields to write back after a successful publish
const now = new Date();
@@ -390,6 +401,11 @@ export class Publisher {
["published", publishedDate],
];
+ // Record the syndication targets used so future publishes know what was sent
+ if (syndicateToOverride !== undefined) {
+ fields.push(["mp-syndicate-to", `[${syndicateToOverride.join(", ")}]`]);
+ }
+
if (this.settings.siteUrl) {
try {
const hostname = new URL(this.settings.siteUrl).hostname.replace(/^www\./, "");
@@ -437,6 +453,32 @@ export class Publisher {
await this.app.vault.modify(file, fmBlock + body);
}
+ /**
+ * Write mp-syndicate-to to frontmatter without touching other fields.
+ * Used when publish succeeds but returns no URL (e.g. update responses).
+ */
+ private async writeSyndicateToNote(
+ file: TFile,
+ originalContent: string,
+ syndicateTo: string[],
+ ): Promise {
+ const fmMatch = originalContent.match(
+ /^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/,
+ );
+ const value = `[${syndicateTo.join(", ")}]`;
+
+ if (!fmMatch) {
+ await this.app.vault.modify(
+ file,
+ `---\nmp-syndicate-to: ${value}\n---\n` + originalContent,
+ );
+ return;
+ }
+
+ const fmBlock = this.setFrontmatterField(fmMatch[1], "mp-syndicate-to", value);
+ await this.app.vault.modify(file, fmBlock + fmMatch[2]);
+ }
+
/**
* Replace the value of an existing frontmatter field, or insert it before
* the closing `---` if the field is not yet present.
diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts
index 94cc9d3..3f1ee7c 100644
--- a/src/SettingsTab.ts
+++ b/src/SettingsTab.ts
@@ -110,6 +110,49 @@ export class MicropubSettingsTab extends PluginSettingTab {
}),
);
+ new Setting(containerEl)
+ .setName("Syndication dialog")
+ .setDesc(
+ "When to show the cross-posting dialog before publishing. " +
+ "'When needed' shows it only if the note has no mp-syndicate-to frontmatter.",
+ )
+ .addDropdown((drop) =>
+ drop
+ .addOption("when-needed", "When needed")
+ .addOption("always", "Always")
+ .addOption("never", "Never")
+ .setValue(this.plugin.settings.showSyndicationDialog)
+ .onChange(async (value) => {
+ this.plugin.settings.showSyndicationDialog = value as
+ | "when-needed"
+ | "always"
+ | "never";
+ await this.plugin.saveSettings();
+ }),
+ );
+
+ // Show configured defaults with a clear button
+ const defaults = this.plugin.settings.defaultSyndicateTo;
+ const defaultsSetting = new Setting(containerEl)
+ .setName("Default syndication targets")
+ .setDesc(
+ defaults.length > 0
+ ? defaults.join(", ")
+ : "None configured. Targets checked by default in the publish dialog.",
+ );
+ if (defaults.length > 0) {
+ defaultsSetting.addButton((btn) =>
+ btn
+ .setButtonText("Clear defaults")
+ .setWarning()
+ .onClick(async () => {
+ this.plugin.settings.defaultSyndicateTo = [];
+ await this.plugin.saveSettings();
+ this.display();
+ }),
+ );
+ }
+
// ── Digital Garden ───────────────────────────────────────────────────
containerEl.createEl("h3", { text: "Digital Garden" });
diff --git a/src/SyndicationDialog.ts b/src/SyndicationDialog.ts
new file mode 100644
index 0000000..5111fbb
--- /dev/null
+++ b/src/SyndicationDialog.ts
@@ -0,0 +1,89 @@
+/**
+ * SyndicationDialog.ts
+ *
+ * Modal that lets the user choose which syndication targets to cross-post to.
+ * Opens as a promise — resolves with the selected UIDs, or null if cancelled.
+ */
+
+import { App, Modal, Setting } from "obsidian";
+import type { SyndicationTarget } from "./types";
+
+export class SyndicationDialog extends Modal {
+ private selected: Set;
+ private resolvePromise: ((value: string[] | null) => void) | null = null;
+ private resolved = false;
+
+ constructor(
+ app: App,
+ private readonly targets: SyndicationTarget[],
+ defaultSelected: string[],
+ ) {
+ super(app);
+ this.selected = new Set(defaultSelected.filter((uid) =>
+ targets.some((t) => t.uid === uid),
+ ));
+ }
+
+ /**
+ * Opens the dialog and waits for user selection.
+ * @returns Selected target UIDs, or null if cancelled.
+ */
+ async awaitSelection(): Promise {
+ return new Promise((resolve) => {
+ this.resolvePromise = resolve;
+ this.open();
+ });
+ }
+
+ onOpen(): void {
+ const { contentEl } = this;
+ contentEl.createEl("h2", { text: "Syndication targets" });
+ contentEl.createEl("p", {
+ text: "Choose where to cross-post this note.",
+ cls: "setting-item-description",
+ });
+
+ for (const target of this.targets) {
+ new Setting(contentEl)
+ .setName(target.name)
+ .addToggle((toggle) =>
+ toggle
+ .setValue(this.selected.has(target.uid))
+ .onChange((value) => {
+ if (value) this.selected.add(target.uid);
+ else this.selected.delete(target.uid);
+ }),
+ );
+ }
+
+ new Setting(contentEl)
+ .addButton((btn) =>
+ btn
+ .setButtonText("Cancel")
+ .onClick(() => {
+ this.finish(null);
+ }),
+ )
+ .addButton((btn) =>
+ btn
+ .setButtonText("Publish")
+ .setCta()
+ .onClick(() => {
+ this.finish([...this.selected]);
+ }),
+ );
+ }
+
+ onClose(): void {
+ // Resolve as cancelled if user pressed Escape or clicked outside
+ this.finish(null);
+ this.contentEl.empty();
+ }
+
+ private finish(value: string[] | null): void {
+ if (this.resolved) return;
+ this.resolved = true;
+ this.resolvePromise?.(value);
+ this.resolvePromise = null;
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 2420bb8..a338424 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -15,10 +15,12 @@
* Based on: https://github.com/svemagie/obsidian-microblog (MIT)
*/
-import { Notice, Plugin, TFile } from "obsidian";
+import { Notice, Plugin, TFile, parseYaml } from "obsidian";
import { DEFAULT_SETTINGS, type MicropubSettings } from "./types";
import { MicropubSettingsTab } from "./SettingsTab";
import { Publisher } from "./Publisher";
+import { MicropubClient } from "./MicropubClient";
+import { SyndicationDialog } from "./SyndicationDialog";
import { handleProtocolCallback } from "./IndieAuth";
export default class MicropubPlugin extends Plugin {
@@ -101,11 +103,19 @@ export default class MicropubPlugin extends Plugin {
return;
}
+ // ── Syndication dialog ────────────────────────────────────────────────
+ // Determine which syndication targets to use, optionally showing a dialog.
+ const syndicateToOverride = await this.resolveSyndicationTargets(file);
+ if (syndicateToOverride === null) {
+ // User cancelled the dialog — abort publish
+ return;
+ }
+
const notice = new Notice("Publishing…", 0 /* persist until dismissed */);
try {
const publisher = new Publisher(this.app, this.settings);
- const result = await publisher.publish(file);
+ const result = await publisher.publish(file, syndicateToOverride);
notice.hide();
@@ -126,6 +136,81 @@ export default class MicropubPlugin extends Plugin {
}
}
+ /**
+ * Decide whether to show the syndication dialog and return the selected targets.
+ *
+ * Returns:
+ * string[] — targets to use as override (may be empty)
+ * undefined — no override; Publisher will use frontmatter + settings defaults
+ * null — user cancelled; abort publish
+ */
+ private async resolveSyndicationTargets(
+ file: TFile,
+ ): Promise {
+ const dialogSetting = this.settings.showSyndicationDialog;
+
+ // "never" — skip dialog entirely, let Publisher handle targets from frontmatter + settings
+ if (dialogSetting === "never") return undefined;
+
+ // Fetch available targets from the server
+ let availableTargets: import("./types").SyndicationTarget[] = [];
+ try {
+ const client = new MicropubClient(
+ () => this.settings.micropubEndpoint,
+ () => this.settings.mediaEndpoint,
+ () => this.settings.accessToken,
+ );
+ const config = await client.fetchConfig();
+ availableTargets = config["syndicate-to"] ?? [];
+ } catch {
+ // Config fetch failed — fall back to normal publish without dialog
+ new Notice(
+ "⚠️ Could not fetch syndication targets. Publishing without dialog.",
+ 4000,
+ );
+ return undefined;
+ }
+
+ // No targets on this server — skip dialog (backward compatible)
+ if (availableTargets.length === 0) return undefined;
+
+ // Read mp-syndicate-to from frontmatter
+ let fmSyndicateTo: string[] | undefined;
+ try {
+ const raw = await this.app.vault.read(file);
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
+ if (fmMatch) {
+ const fm = (parseYaml(fmMatch[1]) ?? {}) as Record;
+ const val = fm["mp-syndicate-to"];
+ if (val !== undefined) {
+ fmSyndicateTo = Array.isArray(val) ? val.map(String) : [String(val)];
+ }
+ }
+ } catch {
+ // Malformed frontmatter — treat as absent
+ }
+
+ // Decide whether to show dialog
+ const showDialog =
+ dialogSetting === "always" ||
+ (dialogSetting === "when-needed" && fmSyndicateTo === undefined) ||
+ (fmSyndicateTo !== undefined && fmSyndicateTo.length === 0);
+
+ if (!showDialog) {
+ // Frontmatter has values and setting is "when-needed" — skip dialog
+ return undefined;
+ }
+
+ // Pre-check: use frontmatter values if non-empty, otherwise plugin defaults
+ const defaultSelected =
+ fmSyndicateTo && fmSyndicateTo.length > 0
+ ? fmSyndicateTo
+ : this.settings.defaultSyndicateTo;
+
+ const dialog = new SyndicationDialog(this.app, availableTargets, defaultSelected);
+ return dialog.awaitSelection();
+ }
+
// ── Settings persistence ──────────────────────────────────────────────────
async loadSettings(): Promise {
diff --git a/src/types.ts b/src/types.ts
index 905e02c..f768da8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -68,6 +68,14 @@ export interface MicropubSettings {
/** Visibility default for new posts: "public" | "unlisted" | "private" */
defaultVisibility: "public" | "unlisted" | "private";
+
+ /**
+ * Controls when the syndication target dialog is shown before publishing.
+ * "when-needed" — Show only if mp-syndicate-to is absent from frontmatter
+ * "always" — Show every time, even if frontmatter has targets
+ * "never" — Never show dialog; use defaultSyndicateTo + frontmatter
+ */
+ showSyndicationDialog: "when-needed" | "always" | "never";
}
export const DEFAULT_SETTINGS: MicropubSettings = {
@@ -83,6 +91,7 @@ export const DEFAULT_SETTINGS: MicropubSettings = {
writeUrlToFrontmatter: true,
mapGardenTags: true,
defaultVisibility: "public",
+ showSyndicationDialog: "when-needed",
};
/** A syndication target as returned by Micropub config query */