From a1afa653be1779bd329894cbfa4fa54080933845 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:10:17 +0100 Subject: [PATCH] feat: use kebab-case AI frontmatter keys (ai-text-level, ai-tools, etc.) Micropub properties now sent as kebab-case; camelCase still accepted as input for backward compatibility. README updated accordingly. Co-Authored-By: Claude Opus 4.6 --- README.md | 32 ++++++++++++++++++-------------- main.js | 8 ++++---- src/Publisher.ts | 35 +++++++++++++++++++---------------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 3c7c7d8..0e072a4 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ Tag any note in Obsidian with a `#garden/*` tag, or set `gardenStage` directly i | Obsidian tag | Published property | Blog display | |---|---|---| -| `#garden/plant` | `gardenStage: plant` | ðŸŒą Seedling | -| `#garden/cultivate` | `gardenStage: cultivate` | ðŸŒŋ Growing | | `#garden/evergreen` | `gardenStage: evergreen` | ðŸŒģ Evergreen | +| `#garden/cultivate` | `gardenStage: cultivate` | ðŸŒŋ Growing | +| `#garden/plant` | `gardenStage: plant` | ðŸŒą Seedling | | `#garden/question` | `gardenStage: question` | ❓ Open Question | | `#garden/repot` | `gardenStage: repot` | ðŸŠī Repotting | | `#garden/revitalize` | `gardenStage: revitalize` | âœĻ Revitalizing | @@ -84,14 +84,15 @@ The Eleventy blog renders a coloured badge on each post and groups all garden po --- title: "On building in public" tags: - - garden/cultivate + - garden/plant +category: - indieweb --- Some early thoughts on the merits of building in public... ``` -After publishing, the frontmatter gains: +After publishing, the frontmatter/property in Obsidian gains: ```yaml mp-url: "https://example.com/articles/2026/on-building-in-public" @@ -108,7 +109,7 @@ mp-url: "https://example.com/articles/2026/on-building-in-public" | `title` | Sets the post `name` (article mode) | | `created` / `date` | Sets `published` date (`created` takes priority — matches Obsidian's default date field) | | `postType` | Force post type: `article` sends a title (uses filename if none set), `note` skips title | -| `tags` / `category` | Becomes Micropub `category` (excluding `garden/*` and bare `garden` tags) | +| `tags` + `category` | Both merged into Micropub `category` (excluding `garden/*` and bare `garden` tags, deduplicated) | | `summary` / `excerpt` | Sets `summary` property | | `visibility` | `public` / `unlisted` / `private` | | `gardenStage` | Explicit garden stage — see table below | @@ -118,14 +119,14 @@ mp-url: "https://example.com/articles/2026/on-building-in-public" ### AI disclosure properties -Use flat top-level properties for best Obsidian compatibility (Obsidian's Properties UI handles them more reliably than nested objects): +Use flat kebab-case properties (camelCase fallback supported for backward compatibility): | Property | Values | Meaning | |---|---|---| -| `aiTextLevel` | `"0"` `"1"` `"2"` `"3"` | None / Editorial / Co-drafted / AI-generated | -| `aiCodeLevel` | `"0"` `"1"` `"2"` | None / AI-assisted / AI-generated | -| `aiTools` | string | Tools used, e.g. `"Claude"` | -| `aiDescription` | string | Free-text disclosure note | +| `ai-text-level` | `"0"` `"1"` `"2"` `"3"` | None / Editorial / Co-drafted / AI-generated | +| `ai-code-level` | `"0"` `"1"` `"2"` | None / AI-assisted / AI-generated | +| `ai-tools` | string | Tools used, e.g. `"Claude"` | +| `ai-description` | string | Free-text disclosure note | Nested `ai:` objects (e.g. `ai: {textLevel: "1"}`) also work but flat keys are recommended. @@ -137,11 +138,14 @@ title: "My Post" created: 2026-03-15T10:00:00 postType: article tags: - - garden/cultivate + - garden/evergreen +category: - indieweb -aiTextLevel: "1" -aiCodeLevel: "0" -aiTools: "Claude" + - lang/en +ai-text-level: "1" +ai-code-level: "0" +ai-tools: "Claude" +ai-description: "AI helped refine the structure" --- ``` diff --git a/main.js b/main.js index ae53a66..0d21e55 100644 --- a/main.js +++ b/main.js @@ -3,15 +3,15 @@ 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 bt=Object.create;var $=Object.defineProperty;var yt=Object.getOwnPropertyDescriptor;var wt=Object.getOwnPropertyNames;var vt=Object.getPrototypeOf,kt=Object.prototype.hasOwnProperty;var Tt=(a,t)=>{for(var i in t)$(a,i,{get:t[i],enumerable:!0})},lt=(a,t,i,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let e of wt(t))!kt.call(a,e)&&e!==i&&$(a,e,{get:()=>t[e],enumerable:!(n=yt(t,e))||n.enumerable});return a};var Et=(a,t,i)=>(i=a!=null?bt(vt(a)):{},lt(t||!a||!a.__esModule?$(i,"default",{value:a,enumerable:!0}):i,a)),St=a=>lt($({},"__esModule",{value:!0}),a);var Pt={};Tt(Pt,{default:()=>O});module.exports=St(Pt);var f=require("obsidian");var pt={micropubEndpoint:"",mediaEndpoint:"",accessToken:"",defaultSyndicateTo:[],autoDiscover:!1,siteUrl:"",authorizationEndpoint:"",tokenEndpoint:"",me:"",writeUrlToFrontmatter:!0,mapGardenTags:!0,defaultVisibility:"public"};var d=require("obsidian");var k=require("obsidian"),T=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"),s=this.extractLinkRel(n,"token_endpoint"),r;if(e)try{r=(await this.fetchConfigFrom(e))["media-endpoint"]}catch(o){}return{micropubEndpoint:e,tokenEndpoint:s,mediaEndpoint:r}}async createPost(t){var n,e,s;let i={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(i),throw:!1});if(r.status===201||r.status===202)return{success:!0,url:((n=r.headers)==null?void 0:n.location)||((e=r.headers)==null?void 0:e.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,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 m,b,y;let e=this.getMediaEndpoint()||`${this.getEndpoint()}/media`,s=`----MicropubBoundary${Date.now()}`,r=`--${s}\r +"use strict";var vt=Object.create;var $=Object.defineProperty;var kt=Object.getOwnPropertyDescriptor;var Tt=Object.getOwnPropertyNames;var Et=Object.getPrototypeOf,St=Object.prototype.hasOwnProperty;var xt=(a,t)=>{for(var i in t)$(a,i,{get:t[i],enumerable:!0})},ut=(a,t,i,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let e of Tt(t))!St.call(a,e)&&e!==i&&$(a,e,{get:()=>t[e],enumerable:!(n=kt(t,e))||n.enumerable});return a};var Pt=(a,t,i)=>(i=a!=null?vt(Et(a)):{},ut(t||!a||!a.__esModule?$(i,"default",{value:a,enumerable:!0}):i,a)),Rt=a=>ut($({},"__esModule",{value:!0}),a);var Ct={};xt(Ct,{default:()=>O});module.exports=Rt(Ct);var f=require("obsidian");var gt={micropubEndpoint:"",mediaEndpoint:"",accessToken:"",defaultSyndicateTo:[],autoDiscover:!1,siteUrl:"",authorizationEndpoint:"",tokenEndpoint:"",me:"",writeUrlToFrontmatter:!0,mapGardenTags:!0,defaultVisibility:"public"};var d=require("obsidian");var k=require("obsidian"),T=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"),s=this.extractLinkRel(n,"token_endpoint"),r;if(e)try{r=(await this.fetchConfigFrom(e))["media-endpoint"]}catch(o){}return{micropubEndpoint:e,tokenEndpoint:s,mediaEndpoint:r}}async createPost(t){var n,e,s;let i={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(i),throw:!1});if(r.status===201||r.status===202)return{success:!0,url:((n=r.headers)==null?void 0:n.location)||((e=r.headers)==null?void 0:e.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,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 m,b,y;let e=this.getMediaEndpoint()||`${this.getEndpoint()}/media`,s=`----MicropubBoundary${Date.now()}`,r=`--${s}\r Content-Disposition: form-data; name="file"; filename="${i}"\r Content-Type: ${n}\r \r `,o=`\r --${s}--\r -`,p=new TextEncoder().encode(r),c=new TextEncoder().encode(o),l=new Uint8Array(t),g=new Uint8Array(p.length+l.length+c.length);g.set(p,0),g.set(l,p.length),g.set(c,p.length+l.length);let u=await(0,k.requestUrl)({url:e,method:"POST",headers:{...this.authHeaders(),"Content-Type":`multipart/form-data; boundary=${s}`},body:g.buffer,throw:!1});if(u.status===201||u.status===202){let v=((m=u.headers)==null?void 0:m.location)||((b=u.headers)==null?void 0:b.Location)||((y=u.json)==null?void 0:y.url);if(v)return v}throw new Error(`Media upload failed (HTTP ${u.status}): ${this.extractError(u.text)}`)}authHeaders(){return{Authorization:`Bearer ${this.getToken()}`}}extractLinkRel(t,i){var s;let n=new RegExp(`]+rel=["']${i}["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["']${i}["']`,"i"),e=t.match(n);return(s=e==null?void 0:e[1])!=null?s: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=Et(require("crypto")),F=require("obsidian"),dt="https://svemagie.github.io/obsidian-micropub/",ut="https://svemagie.github.io/obsidian-micropub/callback",gt="create update media",xt=300*1e3,P=null;function ht(a){if(!P)return;let{resolve:t,state:i}=P;P=null,t(a)}var D=class a{static async discoverEndpoints(t){let n=(await(0,F.requestUrl)({url:t,method:"GET"})).text,e=a.extractLinkRel(n,"authorization_endpoint"),s=a.extractLinkRel(n,"token_endpoint"),r=a.extractLinkRel(n,"micropub");if(!e)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:e,tokenEndpoint:s,micropubEndpoint:r}}static async signIn(t){var b,y,v,E,A,S;let{authorizationEndpoint:i,tokenEndpoint:n,micropubEndpoint:e}=await a.discoverEndpoints(t),s=a.base64url(R.randomBytes(16)),r=a.base64url(R.randomBytes(64)),o=a.base64url(R.createHash("sha256").update(r).digest()),p=new Promise((x,w)=>{let C=setTimeout(()=>{P=null,w(new Error("Sign-in timed out (5 min). Please try again."))},xt);P={state:s,resolve:M=>{clearTimeout(C),x(M)}}}),c=new URL(i);c.searchParams.set("response_type","code"),c.searchParams.set("client_id",dt),c.searchParams.set("redirect_uri",ut),c.searchParams.set("state",s),c.searchParams.set("code_challenge",o),c.searchParams.set("code_challenge_method","S256"),c.searchParams.set("scope",gt),c.searchParams.set("me",t),window.open(c.toString());let l=await p;if(l.state!==s)throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");let g=l.code;if(!g)throw new Error((y=(b=l.error_description)!=null?b:l.error)!=null?y:"No authorization code received.");let u=await(0,F.requestUrl)({url:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"authorization_code",code:g,client_id:dt,redirect_uri:ut,code_verifier:r}).toString(),throw:!1}),m=u.json;if(!m.access_token)throw new Error((E=(v=m.error_description)!=null?v:m.error)!=null?E:`Token exchange failed (HTTP ${u.status})`);return{accessToken:m.access_token,scope:(A=m.scope)!=null?A:gt,me:(S=m.me)!=null?S: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 s;let n=new RegExp(`]+rel=["'][^"']*\\b${i}\\b[^"']*["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["'][^"']*\\b${i}\\b[^"']*["']`,"i"),e=t.match(n);return(s=e==null?void 0:e[1])!=null?s:e==null?void 0:e[2]}};var L=class extends d.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 d.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 d.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 d.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 d.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 d.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 d.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 s=>{this.plugin.settings.siteUrl=s.trim(),await this.plugin.saveSettings()})).addButton(e=>{e.setButtonText("Sign in").setCta().onClick(async()=>{let s=this.plugin.settings.siteUrl.trim();if(!s){new d.Notice("Enter your site URL first.");return}e.setDisabled(!0),e.setButtonText("Opening browser\u2026");try{let r=await D.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 p=await new T(()=>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 d.Notice(`\u2705 Signed in as ${r.me}`),this.display()}catch(r){new d.Notice(`Sign-in failed: ${String(r)}`,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 d.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 s=>{this.plugin.settings.accessToken=s.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 d.Notice("Set the Micropub endpoint and token first.");return}e.setDisabled(!0);try{await new T(()=>this.plugin.settings.micropubEndpoint,()=>this.plugin.settings.mediaEndpoint,()=>this.plugin.settings.accessToken).fetchConfig(),new d.Notice("\u2705 Token is valid!")}catch(s){new d.Notice(`Token check failed: ${String(s)}`)}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 s=e.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=e.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 d.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 mt=require("obsidian");var _="garden/",U=class{constructor(t,i){this.app=t;this.settings=i;this.client=new T(()=>i.micropubEndpoint,()=>i.mediaEndpoint,()=>i.accessToken)}async publish(t){let i=await this.app.vault.read(t),{frontmatter:n,body:e}=this.parseFrontmatter(i),s=n["mp-url"]!=null?String(n["mp-url"]):n.url!=null?String(n.url):void 0,{content:r,uploadedUrls:o}=await this.processImages(e),p=this.buildProperties(n,r,o,t.basename),c;if(s){let l={};for(let[g,u]of Object.entries(p))l[g]=Array.isArray(u)?u:[u];c=await this.client.updatePost(s,l)}else c=await this.client.createPost(p);return c.success&&c.url&&this.settings.writeUrlToFrontmatter&&await this.writeUrlToNote(t,i,c.url),c}buildProperties(t,i,n,e){var G,I,z,V,H,q,W,J,Y,Q,X,K,Z,tt,et,it,nt,st,rt,ot,at,ct;let s={},r=i.trim(),o=(G=t.bookmarkOf)!=null?G:t["bookmark-of"],p=(I=t.likeOf)!=null?I:t["like-of"],c=(z=t.inReplyTo)!=null?z:t["in-reply-to"],l=(V=t.repostOf)!=null?V:t["repost-of"];o&&(s["bookmark-of"]=[String(o)]),p&&(s["like-of"]=[String(p)]),c&&(s["in-reply-to"]=[String(c)]),l&&(s["repost-of"]=[String(l)]),(p||l)&&!r||(s.content=r?[{html:r}]:[{html:""}]);let u=(q=(H=t.postType)!=null?H:t["post-type"])!=null?q:t.type;if(u==="article"||!u&&!!((W=t.title)!=null?W:t.name)){let h=(Y=(J=t.title)!=null?J:t.name)!=null?Y:e;s.name=[String(h)]}((Q=t.summary)!=null?Q:t.excerpt)&&(s.summary=[String((X=t.summary)!=null?X:t.excerpt)]);let b=(K=t.created)!=null?K:t.date;b&&(s.published=[new Date(String(b)).toISOString()]);let y=this.resolveArray((Z=t.tags)!=null?Z:t.category),v=this.extractGardenStage(y),E=y.filter(h=>!h.startsWith(_)&&h!=="garden");if(E.length>0&&(s.category=E),this.settings.mapGardenTags){let h=(tt=t.gardenStage)!=null?tt:v;h&&(s.gardenStage=[h])}let A=this.resolveArray((et=t["mp-syndicate-to"])!=null?et:t.mpSyndicateTo),S=[...new Set([...this.settings.defaultSyndicateTo,...A])];S.length>0&&(s["mp-syndicate-to"]=S);let x=(it=t.visibility)!=null?it:this.settings.defaultVisibility;x&&x!=="public"&&(s.visibility=[x]);let w=t.ai&&typeof t.ai=="object"?t.ai:{},C=(nt=t.aiTextLevel)!=null?nt:w.textLevel,M=(st=t.aiCodeLevel)!=null?st:w.codeLevel,B=(ot=(rt=t.aiTools)!=null?rt:w.aiTools)!=null?ot:w.tools,N=(ct=(at=t.aiDescription)!=null?at:w.aiDescription)!=null?ct:w.description;C!=null&&(s.aiTextLevel=[String(C)]),M!=null&&(s.aiCodeLevel=[String(M)]),B!=null&&(s.aiTools=[String(B)]),N!=null&&(s.aiDescription=[String(N)]);let j=this.resolvePhotoArray(t.photo);j.length>0?s.photo=j:n.length>0&&(s.photo=n.map(h=>({value:h})));for(let[h,ft]of Object.entries(t))h.startsWith("mp-")&&h!=="mp-url"&&h!=="mp-syndicate-to"&&(s[h]=this.resolveArray(ft));return s}resolvePhotoArray(t){return t?(Array.isArray(t)?t:[t]).map(n=>{var e,s;if(typeof n=="string")return{value:n};if(typeof n=="object"&&n!==null){let r=n,o=String((s=(e=r.url)!=null?e: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 i of t){let n=i.replace(/^#/,"");if(n.startsWith(_)){let e=n.slice(_.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,s=t,r=[...t.matchAll(n)];for(let p of r){let c=p[1];try{let l=await this.uploadLocalFile(c);l&&(i.push(l),s=s.replace(p[0],`![${c}](${l})`))}catch(l){console.warn(`[micropub] Failed to upload ${c}:`,l)}}let o=[...s.matchAll(e)];for(let p of o){let c=p[1],l=p[2];if(!l.startsWith("http"))try{let g=await this.uploadLocalFile(l);g&&(i.push(g),s=s.replace(p[0],`![${c}](${g})`))}catch(g){console.warn(`[micropub] Failed to upload ${l}:`,g)}}return{content:s,uploadedUrls:i}}async uploadLocalFile(t){let i=this.app.vault.getFiles().find(s=>s.name===t||s.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,mt.parseYaml)(i[1]))!=null?e:{}}catch(s){}return{frontmatter:n,body:i[2]}}async writeUrlToNote(t,i,n){let e=i.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/);if(!e){let o=`--- +`,p=new TextEncoder().encode(r),c=new TextEncoder().encode(o),l=new Uint8Array(t),g=new Uint8Array(p.length+l.length+c.length);g.set(p,0),g.set(l,p.length),g.set(c,p.length+l.length);let u=await(0,k.requestUrl)({url:e,method:"POST",headers:{...this.authHeaders(),"Content-Type":`multipart/form-data; boundary=${s}`},body:g.buffer,throw:!1});if(u.status===201||u.status===202){let v=((m=u.headers)==null?void 0:m.location)||((b=u.headers)==null?void 0:b.Location)||((y=u.json)==null?void 0:y.url);if(v)return v}throw new Error(`Media upload failed (HTTP ${u.status}): ${this.extractError(u.text)}`)}authHeaders(){return{Authorization:`Bearer ${this.getToken()}`}}extractLinkRel(t,i){var s;let n=new RegExp(`]+rel=["']${i}["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["']${i}["']`,"i"),e=t.match(n);return(s=e==null?void 0:e[1])!=null?s: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=Pt(require("crypto")),F=require("obsidian"),ht="https://svemagie.github.io/obsidian-micropub/",mt="https://svemagie.github.io/obsidian-micropub/callback",ft="create update media",At=300*1e3,P=null;function bt(a){if(!P)return;let{resolve:t,state:i}=P;P=null,t(a)}var D=class a{static async discoverEndpoints(t){let n=(await(0,F.requestUrl)({url:t,method:"GET"})).text,e=a.extractLinkRel(n,"authorization_endpoint"),s=a.extractLinkRel(n,"token_endpoint"),r=a.extractLinkRel(n,"micropub");if(!e)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:e,tokenEndpoint:s,micropubEndpoint:r}}static async signIn(t){var b,y,v,E,A,S;let{authorizationEndpoint:i,tokenEndpoint:n,micropubEndpoint:e}=await a.discoverEndpoints(t),s=a.base64url(R.randomBytes(16)),r=a.base64url(R.randomBytes(64)),o=a.base64url(R.createHash("sha256").update(r).digest()),p=new Promise((x,w)=>{let C=setTimeout(()=>{P=null,w(new Error("Sign-in timed out (5 min). Please try again."))},At);P={state:s,resolve:M=>{clearTimeout(C),x(M)}}}),c=new URL(i);c.searchParams.set("response_type","code"),c.searchParams.set("client_id",ht),c.searchParams.set("redirect_uri",mt),c.searchParams.set("state",s),c.searchParams.set("code_challenge",o),c.searchParams.set("code_challenge_method","S256"),c.searchParams.set("scope",ft),c.searchParams.set("me",t),window.open(c.toString());let l=await p;if(l.state!==s)throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");let g=l.code;if(!g)throw new Error((y=(b=l.error_description)!=null?b:l.error)!=null?y:"No authorization code received.");let u=await(0,F.requestUrl)({url:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"authorization_code",code:g,client_id:ht,redirect_uri:mt,code_verifier:r}).toString(),throw:!1}),m=u.json;if(!m.access_token)throw new Error((E=(v=m.error_description)!=null?v:m.error)!=null?E:`Token exchange failed (HTTP ${u.status})`);return{accessToken:m.access_token,scope:(A=m.scope)!=null?A:ft,me:(S=m.me)!=null?S: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 s;let n=new RegExp(`]+rel=["'][^"']*\\b${i}\\b[^"']*["'][^>]+href=["']([^"']+)["']|]+href=["']([^"']+)["'][^>]+rel=["'][^"']*\\b${i}\\b[^"']*["']`,"i"),e=t.match(n);return(s=e==null?void 0:e[1])!=null?s:e==null?void 0:e[2]}};var U=class extends d.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 d.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 d.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 d.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 d.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 d.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 d.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 s=>{this.plugin.settings.siteUrl=s.trim(),await this.plugin.saveSettings()})).addButton(e=>{e.setButtonText("Sign in").setCta().onClick(async()=>{let s=this.plugin.settings.siteUrl.trim();if(!s){new d.Notice("Enter your site URL first.");return}e.setDisabled(!0),e.setButtonText("Opening browser\u2026");try{let r=await D.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 p=await new T(()=>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 d.Notice(`\u2705 Signed in as ${r.me}`),this.display()}catch(r){new d.Notice(`Sign-in failed: ${String(r)}`,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 d.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 s=>{this.plugin.settings.accessToken=s.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 d.Notice("Set the Micropub endpoint and token first.");return}e.setDisabled(!0);try{await new T(()=>this.plugin.settings.micropubEndpoint,()=>this.plugin.settings.mediaEndpoint,()=>this.plugin.settings.accessToken).fetchConfig(),new d.Notice("\u2705 Token is valid!")}catch(s){new d.Notice(`Token check failed: ${String(s)}`)}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 s=e.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=e.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 d.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 yt=require("obsidian");var _="garden/",L=class{constructor(t,i){this.app=t;this.settings=i;this.client=new T(()=>i.micropubEndpoint,()=>i.mediaEndpoint,()=>i.accessToken)}async publish(t){let i=await this.app.vault.read(t),{frontmatter:n,body:e}=this.parseFrontmatter(i),s=n["mp-url"]!=null?String(n["mp-url"]):n.url!=null?String(n.url):void 0,{content:r,uploadedUrls:o}=await this.processImages(e),p=this.buildProperties(n,r,o,t.basename),c;if(s){let l={};for(let[g,u]of Object.entries(p))l[g]=Array.isArray(u)?u:[u];c=await this.client.updatePost(s,l)}else c=await this.client.createPost(p);return c.success&&c.url&&this.settings.writeUrlToFrontmatter&&await this.writeUrlToNote(t,i,c.url),c}buildProperties(t,i,n,e){var G,I,z,V,H,q,W,J,Y,Q,X,K,Z,tt,et,it,nt,st,rt,ot,at,ct,lt,pt,dt;let s={},r=i.trim(),o=(G=t.bookmarkOf)!=null?G:t["bookmark-of"],p=(I=t.likeOf)!=null?I:t["like-of"],c=(z=t.inReplyTo)!=null?z:t["in-reply-to"],l=(V=t.repostOf)!=null?V:t["repost-of"];o&&(s["bookmark-of"]=[String(o)]),p&&(s["like-of"]=[String(p)]),c&&(s["in-reply-to"]=[String(c)]),l&&(s["repost-of"]=[String(l)]),(p||l)&&!r||(s.content=r?[{html:r}]:[{html:""}]);let u=(q=(H=t.postType)!=null?H:t["post-type"])!=null?q:t.type;if(u==="article"||!u&&!!((W=t.title)!=null?W:t.name)){let h=(Y=(J=t.title)!=null?J:t.name)!=null?Y:e;s.name=[String(h)]}((Q=t.summary)!=null?Q:t.excerpt)&&(s.summary=[String((X=t.summary)!=null?X:t.excerpt)]);let b=(K=t.created)!=null?K:t.date;b&&(s.published=[new Date(String(b)).toISOString()]);let y=[...this.resolveArray(t.tags),...this.resolveArray(t.category)],v=this.extractGardenStage(y),E=y.filter(h=>!h.startsWith(_)&&h!=="garden");if(E.length>0&&(s.category=[...new Set(E)]),this.settings.mapGardenTags){let h=(Z=t.gardenStage)!=null?Z:v;h&&(s.gardenStage=[h])}let A=this.resolveArray((tt=t["mp-syndicate-to"])!=null?tt:t.mpSyndicateTo),S=[...new Set([...this.settings.defaultSyndicateTo,...A])];S.length>0&&(s["mp-syndicate-to"]=S);let x=(et=t.visibility)!=null?et:this.settings.defaultVisibility;x&&x!=="public"&&(s.visibility=[x]);let w=t.ai&&typeof t.ai=="object"?t.ai:{},C=(nt=(it=t["ai-text-level"])!=null?it:t.aiTextLevel)!=null?nt:w.textLevel,M=(rt=(st=t["ai-code-level"])!=null?st:t.aiCodeLevel)!=null?rt:w.codeLevel,B=(ct=(at=(ot=t["ai-tools"])!=null?ot:t.aiTools)!=null?at:w.aiTools)!=null?ct:w.tools,N=(dt=(pt=(lt=t["ai-description"])!=null?lt:t.aiDescription)!=null?pt:w.aiDescription)!=null?dt:w.description;C!=null&&(s["ai-text-level"]=[String(C)]),M!=null&&(s["ai-code-level"]=[String(M)]),B!=null&&(s["ai-tools"]=[String(B)]),N!=null&&(s["ai-description"]=[String(N)]);let j=this.resolvePhotoArray(t.photo);j.length>0?s.photo=j:n.length>0&&(s.photo=n.map(h=>({value:h})));for(let[h,wt]of Object.entries(t))h.startsWith("mp-")&&h!=="mp-url"&&h!=="mp-syndicate-to"&&(s[h]=this.resolveArray(wt));return s}resolvePhotoArray(t){return t?(Array.isArray(t)?t:[t]).map(n=>{var e,s;if(typeof n=="string")return{value:n};if(typeof n=="object"&&n!==null){let r=n,o=String((s=(e=r.url)!=null?e: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 i of t){let n=i.replace(/^#/,"");if(n.startsWith(_)){let e=n.slice(_.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,s=t,r=[...t.matchAll(n)];for(let p of r){let c=p[1];try{let l=await this.uploadLocalFile(c);l&&(i.push(l),s=s.replace(p[0],`![${c}](${l})`))}catch(l){console.warn(`[micropub] Failed to upload ${c}:`,l)}}let o=[...s.matchAll(e)];for(let p of o){let c=p[1],l=p[2];if(!l.startsWith("http"))try{let g=await this.uploadLocalFile(l);g&&(i.push(g),s=s.replace(p[0],`![${c}](${g})`))}catch(g){console.warn(`[micropub] Failed to upload ${l}:`,g)}}return{content:s,uploadedUrls:i}}async uploadLocalFile(t){let i=this.app.vault.getFiles().find(s=>s.name===t||s.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,yt.parseYaml)(i[1]))!=null?e:{}}catch(s){}return{frontmatter:n,body:i[2]}}async writeUrlToNote(t,i,n){let e=i.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/);if(!e){let o=`--- mp-url: "${n}" --- `;await this.app.vault.modify(t,o+i);return}let s=e[1],r=e[2];if(s.includes("mp-url:")){let o=s.replace(/mp-url:.*(\r?\n)/,`mp-url: "${n}"$1`);await this.app.vault.modify(t,o+r)}else{let o=s.replace(/(\r?\n---\r?\n)$/,` -mp-url: "${n}"$1`);await this.app.vault.modify(t,o+r)}}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=>{ht(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 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 U(this.app,this.settings).publish(t);if(i.hide(),e.success){let s=e.url?` -${e.url}`:"";new f.Notice(`\u2705 Published!${s}`,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({},pt,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}}; +mp-url: "${n}"$1`);await this.app.vault.modify(t,o+r)}}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=>{bt(t)}),this.addSettingTab(new U(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 s=e.url?` +${e.url}`:"";new f.Notice(`\u2705 Published!${s}`,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({},gt,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}}; diff --git a/src/Publisher.ts b/src/Publisher.ts index 9b276a0..84a7950 100644 --- a/src/Publisher.ts +++ b/src/Publisher.ts @@ -137,14 +137,19 @@ export class Publisher { props["published"] = [new Date(String(rawDate)).toISOString()]; } - // Categories from frontmatter `category` or `tags` (excluding garden/* tags) - const rawTags = this.resolveArray(fm["tags"] ?? fm["category"]); + // Categories from frontmatter `category` AND `tags` (excluding garden/* tags). + // Merge both fields — `tags` may contain garden/* stages while `category` + // holds the actual topic categories sent to Micropub. + const rawTags = [ + ...this.resolveArray(fm["tags"]), + ...this.resolveArray(fm["category"]), + ]; const gardenStageFromTags = this.extractGardenStage(rawTags); const normalTags = rawTags.filter( (t) => !t.startsWith(GARDEN_TAG_PREFIX) && t !== "garden", ); if (normalTags.length > 0) { - props["category"] = normalTags; + props["category"] = [...new Set(normalTags)]; } // Garden stage — prefer explicit `gardenStage` frontmatter property, @@ -178,22 +183,20 @@ export class Publisher { props["visibility"] = [visibility]; } - // AI disclosure — flatten nested `ai` object into individual top-level - // properties so Indiekit writes them as plain scalar frontmatter keys. - // Also support top-level `aiTextLevel`, `aiTools`, etc. set directly. - // Sending `ai: [{textLevel: "1"}]` makes Indiekit write a YAML array, - // but the template reads `aiTextLevel` / `aiCodeLevel` as top-level scalars. + // AI disclosure — kebab-case keys (ai-text-level, ai-tools, etc.) + // with camelCase fallback for backward compatibility. + // Also support nested `ai` object flattening. const aiObj = (fm["ai"] && typeof fm["ai"] === "object") ? fm["ai"] as Record : {}; - const aiTextLevel = fm["aiTextLevel"] ?? aiObj["textLevel"]; - const aiCodeLevel = fm["aiCodeLevel"] ?? aiObj["codeLevel"]; - const aiTools = fm["aiTools"] ?? aiObj["aiTools"] ?? aiObj["tools"]; - const aiDescription = fm["aiDescription"] ?? aiObj["aiDescription"] ?? aiObj["description"]; - if (aiTextLevel != null) props["aiTextLevel"] = [String(aiTextLevel)]; - if (aiCodeLevel != null) props["aiCodeLevel"] = [String(aiCodeLevel)]; - if (aiTools != null) props["aiTools"] = [String(aiTools)]; - if (aiDescription != null) props["aiDescription"] = [String(aiDescription)]; + const aiTextLevel = fm["ai-text-level"] ?? fm["aiTextLevel"] ?? aiObj["textLevel"]; + const aiCodeLevel = fm["ai-code-level"] ?? fm["aiCodeLevel"] ?? aiObj["codeLevel"]; + const aiTools = fm["ai-tools"] ?? fm["aiTools"] ?? aiObj["aiTools"] ?? aiObj["tools"]; + const aiDescription = fm["ai-description"] ?? fm["aiDescription"] ?? aiObj["aiDescription"] ?? aiObj["description"]; + if (aiTextLevel != null) props["ai-text-level"] = [String(aiTextLevel)]; + if (aiCodeLevel != null) props["ai-code-level"] = [String(aiCodeLevel)]; + if (aiTools != null) props["ai-tools"] = [String(aiTools)]; + if (aiDescription != null) props["ai-description"] = [String(aiDescription)]; // Photos: prefer structured photo array from frontmatter (with alt text), // fall back to uploaded local images.