From c0c3ebe3594d2ecd860d089295d891ed7a6d3d4a Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:03:15 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20correct=20evergreenSince=20typo=20(everg?= =?UTF-8?q?reeSince=20=E2=86=92=20evergreenSince)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specs/2026-03-30-syndication-dialog-design.md | 12 ++++++++---- main.js | 2 +- src/Publisher.ts | 6 +++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md b/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md index 8377364..e98b720 100644 --- a/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md +++ b/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md @@ -67,7 +67,7 @@ mp-syndicate-to: [] - Render modal with checkbox list of targets - Pre-check targets from `defaultSyndicateTo` setting - Handle OK/Cancel actions -- Return selected target UIDs +- Return selected target UIDs via promise **Interface:** ```typescript @@ -75,10 +75,14 @@ export class SyndicationDialog extends Modal { constructor( app: App, targets: SyndicationTarget[], - defaultSelected: string[], - onConfirm: (selected: string[]) => void, - onCancel: () => void + defaultSelected: string[] ); + + /** + * Opens the dialog and waits for user selection. + * @returns Selected target UIDs, or null if cancelled. + */ + async awaitSelection(): Promise; } ``` diff --git a/main.js b/main.js index 25c998f..40af209 100644 --- a/main.js +++ b/main.js @@ -9,7 +9,7 @@ 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.evergreeSince=[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],`![${l}](${a})`))}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],`![${l}](${c})`))}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(` +`,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],`![${l}](${a})`))}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],`![${l}](${c})`))}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(` `);await this.app.vault.modify(t,`--- ${c} --- diff --git a/src/Publisher.ts b/src/Publisher.ts index 6a82f25..8a6b6fe 100644 --- a/src/Publisher.ts +++ b/src/Publisher.ts @@ -167,9 +167,9 @@ export class Publisher { props["gardenStage"] = [gardenStage]; // Pass through the evergreen date so Indiekit writes it to the blog post. if (gardenStage === "evergreen") { - const evergreeSince = fm["evergreen-since"] as string | undefined; - if (evergreeSince) { - props["evergreeSince"] = [String(evergreeSince)]; + const evergreenSince = fm["evergreen-since"] as string | undefined; + if (evergreenSince) { + props["evergreenSince"] = [String(evergreenSince)]; } } }