diff --git a/main.js b/main.js index 8069cc6..52098f1 100644 --- a/main.js +++ b/main.js @@ -3,7 +3,7 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ -var de=Object.create;var M=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var ge=Object.getPrototypeOf,me=Object.prototype.hasOwnProperty;var fe=(h,f)=>{for(var e in f)M(h,e,{get:f[e],enumerable:!0})},te=(h,f,e,t)=>{if(f&&typeof f=="object"||typeof f=="function")for(let n of ue(f))!me.call(h,n)&&n!==e&&M(h,n,{get:()=>f[n],enumerable:!(t=he(f,n))||t.enumerable});return h};var B=(h,f,e)=>(e=h!=null?de(ge(h)):{},te(f||!h||!h.__esModule?M(e,"default",{value:h,enumerable:!0}):e,h)),we=h=>te(M({},"__esModule",{value:!0}),h);var xe={};fe(xe,{default:()=>O});module.exports=we(xe);var u=require("obsidian"),ne=require("child_process"),k=B(require("fs")),L=B(require("path")),se=require("util"),ie=B(require("os")),ae=require("crypto"),oe=require("url"),ve=(0,se.promisify)(k.mkdtemp),$="r-environment-view",D="r-help-view",ye={rExecutablePath:"/usr/local/bin/R",rstudioPandocPath:"/opt/homebrew/bin/"},U=class extends u.ItemView{constructor(e){super(e);this.environmentData=[];this.noteTitle=""}getViewType(){return $}getDisplayText(){return"R Environment"}getIcon(){return"table"}async onOpen(){console.log("REnvironmentView opened"),this.containerEl.empty(),this.render()}async onClose(){console.log("REnvironmentView closed")}updateEnvironmentData(e,t){console.log(`Updating environment data for note: ${e}`,t),this.noteTitle=e,this.environmentData=t,this.render()}render(){console.log("REnvironmentView render called with data:",this.environmentData),this.containerEl.empty();let e=document.createElement("div");e.style.padding="10px",e.style.overflowY="auto";let t=document.createElement("h5");t.textContent=`R environment for ${this.noteTitle}`,t.style.fontFamily='"Poppins", sans-serif',t.style.fontSize="18px",t.style.fontWeight="600",t.style.marginBottom="15px",t.style.padding="10px",t.style.borderRadius="8px",t.style.textAlign="center",t.classList.add("theme-aware-title"),e.appendChild(t);let n=document.createElement("table");n.style.width="100%",n.style.borderCollapse="separate",n.style.borderSpacing="0",n.style.fontFamily="'Monaco', 'monospace'",n.style.whiteSpace="nowrap",n.style.overflow="hidden",n.style.borderRadius="12px",n.style.tableLayout="fixed",n.style.border="1px solid rgba(200, 200, 200, 0.3)",n.classList.add("theme-aware-table");let s=document.createElement("tr");["Name","Type","Size","Value"].forEach((r,a)=>{let i=document.createElement("th");i.textContent=r,i.style.padding="12px",i.style.textAlign="left",i.style.fontFamily='"Poppins", sans-serif',i.style.fontSize="12px",i.style.fontWeight="600",i.style.borderBottom="2px solid rgba(200, 200, 200, 0.5)",i.style.borderRight="1px solid rgba(200, 200, 200, 0.3)",r==="Type"&&(i.style.width="90px"),r==="Size"&&(i.style.width="80px"),r==="Name"&&(i.style.width="60px"),s.appendChild(i)}),n.appendChild(s),this.environmentData.forEach(r=>{let a=document.createElement("tr");a.style.transition="background-color 0.3s",a.style.borderRadius="12px",a.classList.add("theme-aware-row"),a.addEventListener("mouseover",()=>{a.style.backgroundColor="var(--hover-background-color)"}),a.addEventListener("mouseout",()=>{a.style.backgroundColor="var(--row-background-color)"});let i=(R,C="left")=>{let g=document.createElement("td");return g.textContent=R,g.style.padding="12px",g.style.borderBottom="1px solid rgba(200, 200, 200, 0.5)",g.style.borderRight="1px solid rgba(200, 200, 200, 0.3)",g.style.textAlign=C,g.style.fontSize="12px",g.style.overflow="hidden",g.style.textOverflow="ellipsis",g.classList.add("theme-aware-cell"),C==="left"&&R===r.value&&(g.style.width="65%"),g},d=i(r.name);a.appendChild(d);let m=i(Array.isArray(r.type)?r.type.join(", "):r.type);a.appendChild(m);function y(R){let C=["B","KB","MB","GB","TB"];if(R==0)return"0 Byte";let g=Math.floor(Math.log(R)/Math.log(1024));return(R/Math.pow(1024,g)).toFixed(1)+" "+C[g]}let c=i(y(r.size),"right");a.appendChild(c);let x=Array.isArray(r.value)?r.value.slice(0,5).join(", ")+" ...":r.value.toString(),v=i(x);v.style.whiteSpace="nowrap",v.style.width="65%",a.appendChild(v),n.appendChild(a)}),e.appendChild(n),this.containerEl.appendChild(e);let w=document.createElement("style");w.textContent=` +var he=Object.create;var W=Object.defineProperty;var ge=Object.getOwnPropertyDescriptor;var me=Object.getOwnPropertyNames;var fe=Object.getPrototypeOf,we=Object.prototype.hasOwnProperty;var ve=(f,y)=>{for(var e in y)W(f,e,{get:y[e],enumerable:!0})},oe=(f,y,e,t)=>{if(y&&typeof y=="object"||typeof y=="function")for(let n of me(y))!we.call(f,n)&&n!==e&&W(f,n,{get:()=>y[n],enumerable:!(t=ge(y,n))||t.enumerable});return f};var I=(f,y,e)=>(e=f!=null?he(fe(f)):{},oe(y||!f||!f.__esModule?W(e,"default",{value:f,enumerable:!0}):e,f)),ye=f=>oe(W({},"__esModule",{value:!0}),f);var Ee={};ve(Ee,{default:()=>V});module.exports=ye(Ee);var p=require("obsidian"),B=require("child_process"),b=I(require("fs")),x=I(require("path")),ie=require("util"),ae=I(require("os")),re=require("crypto"),le=require("url"),xe=(0,ie.promisify)(b.mkdtemp),_="r-environment-view",O="r-help-view",be={rExecutablePath:"/usr/local/bin/R",rstudioPandocPath:"/opt/homebrew/bin/",quartoExecutablePath:"/usr/local/bin/quarto"},z=class extends p.ItemView{constructor(e){super(e);this.environmentData=[];this.noteTitle=""}getViewType(){return _}getDisplayText(){return"R Environment"}getIcon(){return"table"}async onOpen(){console.log("REnvironmentView opened"),this.containerEl.empty(),this.render()}async onClose(){console.log("REnvironmentView closed")}updateEnvironmentData(e,t){console.log(`Updating environment data for note: ${e}`,t),this.noteTitle=e,this.environmentData=t,this.render()}render(){console.log("REnvironmentView render called with data:",this.environmentData),this.containerEl.empty();let e=document.createElement("div");e.style.padding="10px",e.style.overflowY="auto";let t=document.createElement("h5");t.textContent=`R environment for ${this.noteTitle}`,t.style.fontFamily='"Poppins", sans-serif',t.style.fontSize="18px",t.style.fontWeight="600",t.style.marginBottom="15px",t.style.padding="10px",t.style.borderRadius="8px",t.style.textAlign="center",t.classList.add("theme-aware-title"),e.appendChild(t);let n=document.createElement("table");n.style.width="100%",n.style.borderCollapse="separate",n.style.borderSpacing="0",n.style.fontFamily="'Monaco', 'monospace'",n.style.whiteSpace="nowrap",n.style.overflow="hidden",n.style.borderRadius="12px",n.style.tableLayout="fixed",n.style.border="1px solid rgba(200, 200, 200, 0.3)",n.classList.add("theme-aware-table");let s=document.createElement("tr");["Name","Type","Size","Value"].forEach((a,r)=>{let i=document.createElement("th");i.textContent=a,i.style.padding="12px",i.style.textAlign="left",i.style.fontFamily='"Poppins", sans-serif',i.style.fontSize="12px",i.style.fontWeight="600",i.style.borderBottom="2px solid rgba(200, 200, 200, 0.5)",i.style.borderRight="1px solid rgba(200, 200, 200, 0.3)",a==="Type"&&(i.style.width="90px"),a==="Size"&&(i.style.width="80px"),a==="Name"&&(i.style.width="60px"),s.appendChild(i)}),n.appendChild(s),this.environmentData.forEach(a=>{let r=document.createElement("tr");r.style.transition="background-color 0.3s",r.style.borderRadius="12px",r.classList.add("theme-aware-row"),r.addEventListener("mouseover",()=>{r.style.backgroundColor="var(--hover-background-color)"}),r.addEventListener("mouseout",()=>{r.style.backgroundColor="var(--row-background-color)"});let i=(E,$="left")=>{let m=document.createElement("td");return m.textContent=E,m.style.padding="12px",m.style.borderBottom="1px solid rgba(200, 200, 200, 0.5)",m.style.borderRight="1px solid rgba(200, 200, 200, 0.3)",m.style.textAlign=$,m.style.fontSize="12px",m.style.overflow="hidden",m.style.textOverflow="ellipsis",m.classList.add("theme-aware-cell"),$==="left"&&E===a.value&&(m.style.width="65%"),m},h=i(a.name);r.appendChild(h);let g=i(Array.isArray(a.type)?a.type.join(", "):a.type);r.appendChild(g);function u(E){let $=["B","KB","MB","GB","TB"];if(E==0)return"0 Byte";let m=Math.floor(Math.log(E)/Math.log(1024));return(E/Math.pow(1024,m)).toFixed(1)+" "+$[m]}let w=i(u(a.size),"right");r.appendChild(w);let v=Array.isArray(a.value)?a.value.slice(0,5).join(", ")+" ...":a.value.toString(),P=i(v);P.style.whiteSpace="nowrap",P.style.width="65%",r.appendChild(P),n.appendChild(r)}),e.appendChild(n),this.containerEl.appendChild(e);let c=document.createElement("style");c.textContent=` .theme-aware-title, .theme-aware-table, .theme-aware-cell, .theme-aware-row { color: var(--text-normal); background: var(--background-primary); @@ -17,7 +17,7 @@ var de=Object.create;var M=Object.defineProperty;var he=Object.getOwnPropertyDes .theme-aware-table th { color: var(--text-muted); } - `,document.head.appendChild(w)}},j=class extends u.ItemView{constructor(e){super(e);this.helpContent=""}getViewType(){return D}getDisplayText(){return"R Help"}getIcon(){return"info"}async onOpen(){console.log("RHelpView opened"),this.contentEl.empty(),this.render()}async onClose(){console.log("RHelpView closed")}updateHelpContent(e){console.log("Updating help content in RHelpView with content:",e),this.helpContent=e,this.render()}render(){console.log("RHelpView render called with help content:",this.helpContent),this.contentEl.empty();let e=document.createElement("div");e.style.padding="1px",e.style.overflowY="auto",e.style.fontFamily="sans-serif",e.innerHTML=this.helpContent;let t=document.createElement("style");t.innerHTML=` + `,document.head.appendChild(c)}},j=class extends p.ItemView{constructor(e){super(e);this.helpContent=""}getViewType(){return O}getDisplayText(){return"R Help"}getIcon(){return"info"}async onOpen(){console.log("RHelpView opened"),this.contentEl.empty(),this.render()}async onClose(){console.log("RHelpView closed")}updateHelpContent(e){console.log("Updating help content in RHelpView with content:",e),this.helpContent=e,this.render()}render(){console.log("RHelpView render called with help content:",this.helpContent),this.contentEl.empty();let e=document.createElement("div");e.style.padding="1px",e.style.overflowY="auto",e.style.fontFamily="sans-serif",e.innerHTML=this.helpContent;let t=document.createElement("style");t.innerHTML=` code { font-family: 'Monaco', 'Courier New', monospace; font-size: 0.95em; @@ -25,20 +25,18 @@ var de=Object.create;var M=Object.defineProperty;var he=Object.getOwnPropertyDes padding: 2px 4px; border-radius: 4px; } - `,e.appendChild(t),this.contentEl.appendChild(e)}},z=class extends u.PluginSettingTab{constructor(e,t){super(e,t);this.plugin=t}display(){let{containerEl:e}=this;e.empty(),e.createEl("h2",{text:"R Integration Settings"});function t(n){return navigator.platform.includes("Win")?n.replace(/\\/g,"\\\\").replace(/\//g,"\\\\"):n}new u.Setting(e).setName("Path to R Executable").setDesc("Specify the full path to your R executable.").addText(n=>n.setPlaceholder("/usr/local/bin/R").setValue(t(this.plugin.settings.rExecutablePath)).onChange(async s=>{let o=t(s.trim());console.log("R Executable Path changed to: "+o),this.plugin.settings.rExecutablePath=o,await this.plugin.saveSettings(),new u.Notice("R executable path updated successfully.")})),new u.Setting(e).setName("Path to RStudio Pandoc").setDesc("Specify the full path to your RStudio Pandoc installation.").addText(n=>n.setPlaceholder("/opt/homebrew/bin/").setValue(t(this.plugin.settings.rstudioPandocPath)).onChange(async s=>{let o=t(s.trim());console.log("RStudio Pandoc Path changed to: "+o),this.plugin.settings.rstudioPandocPath=o,await this.plugin.saveSettings(),new u.Notice("RStudio Pandoc path updated successfully.")}))}},O=class extends u.Plugin{constructor(){super(...arguments);this.rProcesses=new Map}generateUniqueId(e){return(0,ae.createHash)("sha256").update(e.toString()).digest("hex").substring(0,8)}async loadSettings(){this.settings=Object.assign({},ye,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}async onload(){console.log("Loading R Code Evaluator Plugin"),await this.loadSettings(),this.addSettingTab(new z(this.app,this)),this.registerView($,e=>new U(e)),this.registerView(D,e=>new j(e)),this.app.workspace.onLayoutReady(()=>{if(console.log("Workspace is ready, adding R Environment and Help views"),this.app.workspace.getLeavesOfType($).length===0){let e=this.app.workspace.getRightLeaf(!1);e?e.setViewState({type:$,active:!0}).then(()=>{console.log("REnvironmentView added to the right pane")}).catch(t=>{console.error("Failed to add REnvironmentView to the right pane:",t)}):console.error("Failed to obtain the right workspace leaf for REnvironmentView.")}else console.log("REnvironmentView already exists in the workspace");if(this.app.workspace.getLeavesOfType(D).length===0){let e=this.app.workspace.getRightLeaf(!0);e?e.setViewState({type:D,active:!0}).then(()=>{console.log("RHelpView added to the right pane")}).catch(t=>{console.error("Failed to add RHelpView to the right pane:",t)}):console.error("Failed to obtain the right workspace leaf for RHelpView.")}else console.log("RHelpView already exists in the workspace")}),this.addCommand({id:"run-current-code-chunk",name:"Run Current Code Chunk",editorCallback:(e,t)=>{if(!t.file){new u.Notice("No file associated with the current view."),console.error("No file associated with the current view.");return}let n=t.file.basename;this.runCurrentCodeChunk(e,t,n)},hotkeys:[{modifiers:["Mod"],key:"r"}]}),console.log("R Code Evaluator Plugin loaded successfully")}onunload(){console.log("Unloading R Code Evaluator Plugin"),this.rProcesses.forEach((e,t)=>{console.log(`Terminating R process for note: ${t}`),e.kill()}),this.rProcesses.clear(),this.app.workspace.getLeavesOfType($).forEach(e=>{console.log("Detaching REnvironmentView from workspace"),e.detach()}),console.log("R Code Evaluator Plugin unloaded successfully")}runCurrentCodeChunk(e,t,n){var a;let s=e.getCursor(),{startLine:o,endLine:p,code:w,existingLabel:r}=this.getCurrentCodeChunk(e,s.line);if(w){let i=r||this.generateUniqueId(o),d=(a=t.file)==null?void 0:a.path;if(d){r||this.insertLabel(e,o,i),console.log(`Running current code chunk in note: ${d} with ID: ${i}`);let m=/\?\s*\w+/.test(w)||/help\s*\(\s*\w+\s*\)/.test(w);this.runRCodeInSession(d,w,n,i,m).then(({result:y,imagePaths:c,widgetPaths:x,helpContent:v})=>{console.log("R code executed successfully"),m?new u.Notice("Help content updated in the sidebar."):this.insertOutputWithCallout(e,p,y,c,x,i)}).catch(y=>{console.error("Error executing R code:",y),this.insertOutputWithCallout(e,p,`Error: -${y}`,[],[],i)})}else new u.Notice("No file associated with the current view."),console.error("No file associated with the current view.")}else new u.Notice("No R code chunk found at the cursor position."),console.log("No R code chunk found at the cursor position.")}getCurrentCodeChunk(e,t){let n=e.lineCount(),s=t,o=t,p=null;for(;s>=0&&!e.getLine(s).startsWith("```r");)s--;if(s<0)return{startLine:-1,endLine:-1,code:"",existingLabel:null};for(o=s+1;o=n)return{startLine:-1,endLine:-1,code:"",existingLabel:null};let w=[];for(let d=s+1;d [!OUTPUT]+ {#output-${p}} -> -`,r="> ",a=w,i=n.trim().split(` -`).map(c=>r+c);a+=i.join(` -`),s.forEach(c=>{let v=`![center| 480 ](${`${c}`})`;a+=` -${r} ${v}`}),o.forEach(c=>{let v=``;a+=` -${r} ${v}`}),a+=` -> -`;let d=-1,m=-1,y=e.lineCount();for(let c=0;c [!OUTPUT]+ {#output-${p}}`){for(d=c,m=c;m+1 ")&&v.trim()!=="")break;m++}break}if(d!==-1&&m!==-1){let c={line:d,ch:0},x={line:m+1,ch:0};e.replaceRange(a+` -`,c,x),console.log(`Replaced existing output callout for ID: ${p}`)}else{let c={line:t+1,ch:0};e.replaceRange(` -`+a+` -`,c),console.log(`Inserted new output callout for ID: ${p}`)}}getRProcess(e){let t=this.rProcesses.get(e);return t||(t=this.startRProcess(e)),t}startRProcess(e){let t=this.settings.rExecutablePath||"/usr/local/bin/R";if(console.log(`Starting R process for note: ${e} using executable: ${t}`),!k.existsSync(t))throw new u.Notice(`R executable not found at ${t}. Please update the path in settings.`),console.error(`R executable not found at ${t}.`),new Error(`R executable not found at ${t}.`);let n={...process.env},s=(0,ne.spawn)(t,["--vanilla","--quiet","--slave"],{stdio:"pipe",env:n});s.on("error",p=>{console.error(`Failed to start R process for ${e}:`,p)});let o=` + `,e.appendChild(t),this.contentEl.appendChild(e)}},q=class extends p.PluginSettingTab{constructor(e,t){super(e,t);this.plugin=t}display(){let{containerEl:e}=this;e.empty(),e.createEl("h2",{text:"R Integration Settings"});function t(n){return navigator.platform.includes("Win")?n.replace(/\\/g,"\\\\").replace(/\//g,"\\\\"):n}new p.Setting(e).setName("Path to R Executable").setDesc("Specify the full path to your R executable.").addText(n=>n.setPlaceholder("/usr/local/bin/R").setValue(t(this.plugin.settings.rExecutablePath)).onChange(async s=>{let l=t(s.trim());console.log("R Executable Path changed to: "+l),this.plugin.settings.rExecutablePath=l,await this.plugin.saveSettings(),new p.Notice("R executable path updated successfully.")})),new p.Setting(e).setName("Path to RStudio Pandoc").setDesc("Specify the full path to your RStudio Pandoc installation.").addText(n=>n.setPlaceholder("/opt/homebrew/bin/").setValue(t(this.plugin.settings.rstudioPandocPath)).onChange(async s=>{let l=t(s.trim());console.log("RStudio Pandoc Path changed to: "+l),this.plugin.settings.rstudioPandocPath=l,await this.plugin.saveSettings(),new p.Notice("RStudio Pandoc path updated successfully.")})),new p.Setting(e).setName("Quarto Executable Path").setDesc("Specify the full path to your Quarto executable. Example: /usr/local/bin/quarto").addText(n=>n.setPlaceholder("/usr/local/bin/quarto").setValue(this.plugin.settings.quartoExecutablePath).onChange(async s=>{console.log("Quarto Executable Path changed to: "+s),this.plugin.settings.quartoExecutablePath=s.trim(),await this.plugin.saveSettings(),new p.Notice("Quarto executable path updated successfully.")}))}},V=class extends p.Plugin{constructor(){super(...arguments);this.rProcesses=new Map}generateUniqueId(e){return(0,re.createHash)("sha256").update(e.toString()).digest("hex").substring(0,8)}async loadSettings(){this.settings=Object.assign({},be,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}async onload(){console.log("Loading R Code Evaluator Plugin"),await this.loadSettings(),this.addSettingTab(new q(this.app,this)),this.registerView(_,e=>new z(e)),this.registerView(O,e=>new j(e)),this.app.workspace.onLayoutReady(()=>{if(console.log("Workspace is ready, adding R Environment and Help views"),this.app.workspace.getLeavesOfType(_).length===0){let e=this.app.workspace.getRightLeaf(!1);e?e.setViewState({type:_,active:!0}).then(()=>{console.log("REnvironmentView added to the right pane")}).catch(t=>{console.error("Failed to add REnvironmentView to the right pane:",t)}):console.error("Failed to obtain the right workspace leaf for REnvironmentView.")}else console.log("REnvironmentView already exists in the workspace");if(this.app.workspace.getLeavesOfType(O).length===0){let e=this.app.workspace.getRightLeaf(!0);e?e.setViewState({type:O,active:!0}).then(()=>{console.log("RHelpView added to the right pane")}).catch(t=>{console.error("Failed to add RHelpView to the right pane:",t)}):console.error("Failed to obtain the right workspace leaf for RHelpView.")}else console.log("RHelpView already exists in the workspace")}),this.addCommand({id:"run-current-code-chunk",name:"Run Current Code Chunk",editorCallback:(e,t)=>{if(!t.file){new p.Notice("No file associated with the current view."),console.error("No file associated with the current view.");return}let n=t.file.basename;this.runCurrentCodeChunk(e,t,n)},hotkeys:[{modifiers:["Mod"],key:"r"}]}),this.addCommand({id:"export-note-with-quarto",name:"Export Note with Quarto",callback:()=>this.exportNoteWithQuarto()}),console.log("R Code Evaluator Plugin loaded successfully")}onunload(){console.log("Unloading R Code Evaluator Plugin"),this.rProcesses.forEach((e,t)=>{console.log(`Terminating R process for note: ${t}`),e.kill()}),this.rProcesses.clear(),this.app.workspace.getLeavesOfType(_).forEach(e=>{console.log("Detaching REnvironmentView from workspace"),e.detach()}),console.log("R Code Evaluator Plugin unloaded successfully")}runCurrentCodeChunk(e,t,n){var h;let s=e.getCursor(),{startLine:l,endLine:o,code:c,existingLabel:a,options:r}=this.getCurrentCodeChunk(e,s.line),i=o;if(c){let g=a||this.generateUniqueId(l),u=(h=t.file)==null?void 0:h.path;if(u){if(!a){let v=this.insertLabel(e,l,g);i+=v}console.log(`Running current code chunk in note: ${u} with ID: ${g}`);let w=/\?\s*\w+/.test(c)||/help\s*\(\s*\w+\s*\)/.test(c);this.runRCodeInSession(u,c,n,g,w,r).then(({result:v,imagePaths:P,widgetPaths:E,helpContent:$})=>{if(console.log("R code executed successfully"),w)new p.Notice("Help content updated in the sidebar.");else{let m=r.include=="false",M=r.output=="false";(v||P.length>0||E.length>0)&&this.insertOutputWithCallout(e,i,v,P,E,g,r),M&&this.removeOutputCallout(e,g),m&&this.removeOutputCallout(e,g)}}).catch(v=>{console.error("Error executing R code:",v),r.error!=="false"&&r.include!=="false"&&r.output!=="false"?this.insertOutputWithCallout(e,i,`Error: +${v}`,[],[],g,r):this.removeOutputCallout(e,g)})}else new p.Notice("No file associated with the current view."),console.error("No file associated with the current view.")}else new p.Notice("No R code chunk found at the cursor position."),console.log("No R code chunk found at the cursor position.")}getCurrentCodeChunk(e,t){let n=e.lineCount(),s=t,l=t,o=null,c={};for(;s>=0&&!this.isCodeChunkStart(e.getLine(s));)s--;if(s<0)return{startLine:-1,endLine:-1,code:"",existingLabel:null,options:{}};for(l=s+1;l=n)return{startLine:-1,endLine:-1,code:"",existingLabel:null,options:{}};o=this.parseChunkLabel(e,s),c=this.parseChunkOptions(e,s);let a=[];for(let i=s+1;i"> "+w);a.push(...u)}if(s.forEach(u=>{let v=`![center|480](${`${u}`})`;a.push(`> ${v}`)}),l.forEach(u=>{let w=``;a.push(`> ${w}`)}),a.length===0){console.log("No output, images, or widgets to insert. Skipping callout insertion.");return}let r=`> [!OUTPUT]+ {#output-${o}} +`;r+=a.join(` +`)+` +`,r+=`> +`;let i=-1,h=-1,g=e.lineCount();for(let u=0;u [!OUTPUT]+ {#output-${o}}`){for(i=u,h=u;h+1 ")&&v.trim()!=="")break;h++}break}if(i!==-1&&h!==-1){let u={line:i,ch:0},w={line:h+1,ch:0};e.replaceRange(r+` +`,u,w),console.log(`Replaced existing output callout for ID: ${o}`)}else{let u={line:t+1,ch:0};e.replaceRange(` +`+r+` +`,u),console.log(`Inserted new output callout for ID: ${o}`)}}removeOutputCallout(e,t){console.log(`Removing output callout for ID: ${t} if it exists`);let n=-1,s=-1,l=e.lineCount();for(let o=0;o [!OUTPUT]+ {#output-${t}}`){for(n=o,s=o;s+1 ")&&a.trim()!=="")break;s++}break}if(n!==-1&&s!==-1){let o={line:n,ch:0},c={line:s+1,ch:0};e.replaceRange("",o,c),console.log(`Removed existing output callout for ID: ${t}`)}else console.log(`No existing output callout found for ID: ${t}`)}getRProcess(e){let t=this.rProcesses.get(e);return t||(t=this.startRProcess(e)),t}startRProcess(e){let t=this.settings.rExecutablePath||"/usr/local/bin/R";if(console.log(`Starting R process for note: ${e} using executable: ${t}`),!b.existsSync(t))throw new p.Notice(`R executable not found at ${t}. Please update the path in settings.`),console.error(`R executable not found at ${t}.`),new Error(`R executable not found at ${t}.`);let n={...process.env},s=(0,B.spawn)(t,["--vanilla","--quiet","--slave"],{stdio:"pipe",env:n});s.on("error",o=>{console.error(`Failed to start R process for ${e}:`,o)});let l=` library(jsonlite) if (!exists("user_env")) { user_env <- new.env() @@ -49,23 +47,34 @@ if (!exists("user_env")) { options(browser='false') options(bitmapType = 'cairo') options(device = function(...) jpeg(filename = tempfile(), width=800, height=600, ...)) - `;return s.stdin.write(o+` -`),this.rProcesses.set(e,s),console.log(`R process started and stored for note: ${e}`),s}async runRCodeInSession(e,t,n,s,o){console.log("runRCodeInSession called for note:",e,"with ID:",s);let p=this.getRProcess(e);return new Promise(async(w,r)=>{let a="",i="",d=await ve(L.join(ie.tmpdir(),"rplots-")),m=process.platform==="win32"?d.replace(/\\/g,"\\\\"):d.replace(/\\/g,"/"),y=L.join(m,`help_${s}.txt`),c=process.platform==="win32"?y.replace(/\\/g,"\\\\"):y.replace(/\\/g,"/"),x=`__END_OF_OUTPUT__${Date.now()}__`,v="__PLOT_PATH__",R=`__ENVIRONMENT_DATA__${Date.now()}__`,C="__WIDGET_PATH__",g=` + `;return s.stdin.write(l+` +`),this.rProcesses.set(e,s),console.log(`R process started and stored for note: ${e}`),s}async runRCodeInSession(e,t,n,s,l,o){console.log("runRCodeInSession called for note:",e,"with ID:",s);let c=this.getRProcess(e);return new Promise(async(a,r)=>{let i="",h="",g=await xe(x.join(ae.tmpdir(),"rplots-")),u=process.platform==="win32"?g.replace(/\\/g,"\\\\"):g.replace(/\\/g,"/"),w=x.join(u,`help_${s}.txt`),v=process.platform==="win32"?w.replace(/\\/g,"\\\\"):w.replace(/\\/g,"/"),P=`__END_OF_OUTPUT__${Date.now()}__`,E="__PLOT_PATH__",$=`__ENVIRONMENT_DATA__${Date.now()}__`,m="__WIDGET_PATH__",M=` + opts <- list( + echo = ${o.echo!=="false"?"TRUE":"FALSE"}, + warning = ${o.warning!=="false"?"TRUE":"FALSE"}, + error = ${o.error!=="false"?"TRUE":"FALSE"}, + include = ${o.include!=="false"?"TRUE":"FALSE"}, + output = ${o.output!=="false"?"TRUE":"FALSE"} + ) + `,Y=` library(evaluate) library(jsonlite) library(htmlwidgets) Sys.setenv(RSTUDIO_PANDOC='${this.settings.rstudioPandocPath}') + +${M} + # Define our custom print function custom_print_htmlwidget <- function(x, ..., viewer = NULL) { # Generate a unique filename widgetFileName <- paste0("widget_${s}_",".html") - widgetFilePath <- file.path("${m}", widgetFileName) + widgetFilePath <- file.path("${u}", widgetFileName) # Save the widget to the file saveWidget(x, widgetFilePath, selfcontained = TRUE) # Output a marker to indicate the widget was saved - cat("${C}", widgetFileName, "\\n", sep="") + cat("${m}", widgetFileName, "\\n", sep="") } # Replace the original function in the 'htmlwidgets' namespace @@ -156,7 +165,7 @@ Using the first match ...")) if(type == "text") { pkgname <- basename(dirname(dirname(file))) - tools::Rd2HTML(.getHelpFile(file), out = "${c}", + tools::Rd2HTML(.getHelpFile(file), out = "${v}", package = pkgname) } @@ -182,56 +191,61 @@ for (res in results) { if (inherits(res, "source")) { # Ignore source elements } else if (inherits(res, "warning")) { - outputs <- c(outputs, paste("Warning:", conditionMessage(res))) + if (opts$warning && opts$include) { + outputs <- c(outputs, paste("Warning:", conditionMessage(res))) + } } else if (inherits(res, "message")) { - outputs <- c(outputs, conditionMessage(res)) + if (opts$output && opts$include) { + outputs <- c(outputs, res$message) + } } else if (inherits(res, "error")) { - outputs <- c(outputs, paste("Error:", conditionMessage(res))) + if (opts$error && opts$include) { + outputs <- c(outputs, paste("Error:", conditionMessage(res))) + } } else if (inherits(res, "character")) { - - val_str <- res - outputs <- c(outputs, val_str) - + if (opts$output && opts$include) { + outputs <- c(outputs, res) + } } else if (inherits(res, "recordedplot")) { - # Save the plot to a file using uniqueId - timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") - plotFileName <- paste0("plot_${s}_", length(imagePaths) + 1, "_", timestamp, ".jpg") - plotFilePath <- file.path("${m}", plotFileName) - jpeg(filename=plotFilePath, width=800, height=600) - replayPlot(res) - dev.off() - imagePaths <- c(imagePaths, plotFileName) + if (opts$output && opts$include) { + # Save the plot to a file using uniqueId + timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") + plotFileName <- paste0("plot_${s}_", length(imagePaths) + 1, "_", timestamp, ".jpg") + plotFilePath <- file.path("${u}", plotFileName) + jpeg(filename=plotFilePath, width=800, height=600) + replayPlot(res) + dev.off() + imagePaths <- c(imagePaths, plotFileName) + } } } -# Output the collected outputs -if (length(outputs) > 0) { - cat(paste(outputs, collapse = "\\n"), "\\n") -} - - # Attempt to retrieve the last animation, if any -if (requireNamespace("gganimate", quietly = TRUE)) { - anim <- try(gganimate::last_animation(), silent = TRUE) - - - +if (opts$output && opts$include) { + if (requireNamespace("gganimate", quietly = TRUE)) { + anim <- try(gganimate::last_animation(), silent = TRUE) if (is.character(anim[1])) { - if(file.info(anim[1])$mtime > timecheck){ - # 'anim' is a file path to the GIF - timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") - animFileName <- paste0("animation_${s}_", timestamp, ".gif") - animFilePath <- file.path("${m}", animFileName) # Corrected line - file.copy(anim[1], animFilePath) - imagePaths <- c(imagePaths, animFileName) - }} - - + if (file.info(anim[1])$mtime > timecheck) { + timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") + animFileName <- paste0("animation_${s}_", timestamp, ".gif") + animFilePath <- file.path("${u}", animFileName) + file.copy(anim[1], animFilePath) + imagePaths <- c(imagePaths, animFileName) + } + } + } +} + +# Output the collected outputs +if (opts$output && opts$include && length(outputs) > 0) { + cat(paste(outputs, collapse = "\\n"), "\\n") } # Output image markers -for (img in imagePaths) { - cat("${v}", img, "\\n", sep="") +if (opts$output && opts$include) { + for (img in imagePaths) { + cat("${E}", img, "\\n", sep="") + } } # Output the environment data @@ -254,9 +268,17 @@ env_list <- lapply(vars, function(var_name) { env_json <- toJSON(env_list, auto_unbox = TRUE) -cat("${R}\\n") +cat("${$}\\n") cat(env_json) -cat("\\n${x}\\n") +cat("\\n${P}\\n") `;console.log(`Wrapped code sent to R: -`,g);let Q=async W=>{var G,J;let F=W.toString();if(console.log("Received data chunk:",F),a+=F,a.includes(x)){console.log("Marker detected in R output"),p.stdout.off("data",Q),p.stderr.off("data",Y);let P="",V="";if(o){try{let b=await k.promises.readFile(y,"utf8");console.log("Read help content:",b),V=b}catch(b){console.error("Failed to read help content:",b),V="Failed to retrieve help content."}let l=(G=this.app.workspace.getLeavesOfType(D)[0])==null?void 0:G.view;l?l.updateHelpContent(V):console.log("RHelpView not found in the workspace")}let H="";if(a.includes(R)){let l=a.split(R);P=l[0].trim(),H=l[1].split(x)[0].trim()}else P=a.split(x)[0].trim();console.log("Result before processing:",P),console.log("Environment data:",H);let q=v.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),S=[],re=new RegExp(`${q}(.*)`,"g"),N;for(;(N=re.exec(P))!==null;)if(console.log("Image regex match:",N),N[1]){let l=N[1].trim();S.push(l)}else console.error("No image file name captured in regex match:",N);P=P.replace(new RegExp(`${q}.*`,"g"),"").trim();for(let l of S){let b=L.join(d,l);try{let E=await k.promises.readFile(b),_=`plots/${l}`;this.app.vault.getAbstractFileByPath("plots")||(await this.app.vault.createFolder("plots"),console.log('Created "plots" folder in the vault'));let A=this.app.vault.getAbstractFileByPath(_);A?(await this.app.vault.modifyBinary(A,E),console.log(`Image file updated in vault: ${_}`)):(await this.app.vault.createBinary(_,E),console.log(`Image file created in vault: ${_}`)),S[S.indexOf(l)]=_}catch(E){console.error(`Error handling image file ${l}:`,E)}}let T=[],K=C.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),le=new RegExp(`${K}(.*)`,"g"),I;for(;(I=le.exec(P))!==null;)if(I[1]){let l=I[1].trim();T.push(l)}P=P.replace(new RegExp(`${K}.*`,"g"),"").trim();for(let l of T){let b=L.join(d,l);try{let E=await k.promises.readFile(b,"utf8"),_=`widgets/${l}`,Z=this.app.vault.adapter.getBasePath();this.app.vault.getAbstractFileByPath("widgets")||await this.app.vault.createFolder("widgets");let ee=this.app.vault.getAbstractFileByPath(_);ee?await this.app.vault.modify(ee,E):await this.app.vault.create(_,E);let ce=L.join(Z,_),pe=(0,oe.pathToFileURL)(ce).href;T[T.indexOf(l)]=pe}catch(E){console.error(`Error handling widget file ${l}:`,E)}}try{console.log(`Temporary directory ${d} removed`)}catch(l){console.error(`Error removing temporary directory ${d}:`,l)}let X=(J=this.app.workspace.getLeavesOfType($)[0])==null?void 0:J.view;if(X){let l=[];try{l=JSON.parse(H),console.log("Parsed environment variables:",l)}catch(b){console.error("Failed to parse environment data JSON:",b)}X.updateEnvironmentData(n,l)}else console.log("REnvironmentView not found in the workspace");a="",i="",i?r(i.trim()):w({result:P,imagePaths:S,widgetPaths:T,helpContent:V})}},Y=W=>{let F=W.toString();console.error("Received error chunk from R:",F),i+=F};p.stdout.on("data",Q),p.stderr.on("data",Y),p.stdin.write(g+` -`),console.log("Wrapped R code sent to the R process")})}}; +`,Y);let G=async A=>{var K,Z;let F=A.toString();if(console.log("Received data chunk:",F),i+=F,i.includes(P)){console.log("Marker detected in R output"),c.stdout.off("data",G),c.stderr.off("data",J);let k="",D="";if(l){try{let R=await b.promises.readFile(w,"utf8");console.log("Read help content:",R),D=R}catch(R){console.error("Failed to read help content:",R),D="Failed to retrieve help content."}let d=(K=this.app.workspace.getLeavesOfType(O)[0])==null?void 0:K.view;d?d.updateHelpContent(D):console.log("RHelpView not found in the workspace")}let U="";if(i.includes($)){let d=i.split($);k=d[0].trim(),U=d[1].split(P)[0].trim()}else k=i.split(P)[0].trim();console.log("Result before processing:",k),console.log("Environment data:",U);let X=E.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),S=[],ce=new RegExp(`${X}(.*)`,"g"),N;for(;(N=ce.exec(k))!==null;)if(console.log("Image regex match:",N),N[1]){let d=N[1].trim();S.push(d)}else console.error("No image file name captured in regex match:",N);k=k.replace(new RegExp(`${X}.*`,"g"),"").trim();for(let d of S){let R=x.join(g,d);try{let C=await b.promises.readFile(R),L=`plots/${d}`;this.app.vault.getAbstractFileByPath("plots")||(await this.app.vault.createFolder("plots"),console.log('Created "plots" folder in the vault'));let Q=this.app.vault.getAbstractFileByPath(L);Q?(await this.app.vault.modifyBinary(Q,C),console.log(`Image file updated in vault: ${L}`)):(await this.app.vault.createBinary(L,C),console.log(`Image file created in vault: ${L}`)),S[S.indexOf(d)]=L}catch(C){console.error(`Error handling image file ${d}:`,C)}}let T=[],ee=m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pe=new RegExp(`${ee}(.*)`,"g"),H;for(;(H=pe.exec(k))!==null;)if(H[1]){let d=H[1].trim();T.push(d)}k=k.replace(new RegExp(`${ee}.*`,"g"),"").trim();for(let d of T){let R=x.join(g,d);try{let C=await b.promises.readFile(R,"utf8"),L=`widgets/${d}`,ne=this.app.vault.adapter.getBasePath();this.app.vault.getAbstractFileByPath("widgets")||await this.app.vault.createFolder("widgets");let se=this.app.vault.getAbstractFileByPath(L);se?await this.app.vault.modify(se,C):await this.app.vault.create(L,C);let ue=x.join(ne,L),de=(0,le.pathToFileURL)(ue).href;T[T.indexOf(d)]=de}catch(C){console.error(`Error handling widget file ${d}:`,C)}}try{console.log(`Temporary directory ${g} removed`)}catch(d){console.error(`Error removing temporary directory ${g}:`,d)}let te=(Z=this.app.workspace.getLeavesOfType(_)[0])==null?void 0:Z.view;if(te){let d=[];try{d=JSON.parse(U),console.log("Parsed environment variables:",d)}catch(R){console.error("Failed to parse environment data JSON:",R)}te.updateEnvironmentData(n,d)}else console.log("REnvironmentView not found in the workspace");i="",h="",h?r(h.trim()):a({result:k,imagePaths:S,widgetPaths:T,helpContent:D})}},J=A=>{let F=A.toString();console.error("Received error chunk from R:",F),h+=F};c.stdout.on("data",G),c.stderr.on("data",J),c.stdin.write(Y+` +`),console.log("Wrapped R code sent to the R process")})}addFrontMatter(e,t){return e.startsWith(`--- +`)?e:`--- +title: "${t.basename}" +author: "Author Name" +date: today +format: html +--- + +`+e}sanitizeFileName(e){return e.replace(/[^a-zA-Z0-9-_]/g,"_")}stripOutputCallouts(e){let t=/> \[!OUTPUT\]\+ {#output-[\w]+}\n(?:>.*\n)*>/gm;return e.replace(t,"")}async savePreparedNote(e,t){let n=x.basename(t,".md"),l=this.sanitizeFileName(n)+"_quarto.qmd",o,c=this.app.vault.adapter;if(c instanceof p.FileSystemAdapter)o=c.getBasePath();else throw new p.Notice("Unable to determine vault base path. Export failed."),new Error("Vault adapter is not a FileSystemAdapter.");let a=x.join(o,"Exports");b.existsSync(a)||b.mkdirSync(a);let r=x.join(a,l);return await b.promises.writeFile(r,e,"utf8"),r}async renderWithQuarto(e){return new Promise((t,n)=>{let s=this.settings.quartoExecutablePath||"quarto";if(!b.existsSync(s)){new p.Notice(`Quarto executable not found at ${s}. Please update the path in settings.`),console.error(`Quarto executable not found at ${s}.`),n(new Error(`Quarto executable not found at ${s}.`));return}let l=this.settings.rExecutablePath||"Rscript",o={...process.env};o.QUARTO_R=l;let c=x.dirname(l);o.PATH=`${c}${x.delimiter}${o.PATH}`;let a=(0,B.spawn)(s,["render",e],{stdio:["ignore","pipe","pipe"],env:o}),r="";a.stderr.on("data",i=>{r+=i.toString()}),a.on("error",i=>{new p.Notice("Failed to start Quarto rendering process."),console.error(`Failed to start Quarto rendering process: ${i}`),n(i)}),a.on("exit",(i,h)=>{console.log(`Quarto render process exited with code: ${i}, signal: ${h}`),i===0?(new p.Notice("Quarto rendering completed successfully."),t()):(console.error(`Quarto stderr: ${r}`),new p.Notice("Quarto rendering failed. Click for details.",1e4),n(new Error(`Quarto exited with code ${i}`)))})})}async exportNoteWithQuarto(){let e=this.app.workspace.getActiveFile();if(!e){new p.Notice("No active note to export.");return}try{let n=await this.app.vault.read(e);n=this.addFrontMatter(n,e),n=this.stripOutputCallouts(n);let s=await this.savePreparedNote(n,e.path);await this.renderWithQuarto(s),new p.Notice("Note exported and rendered with Quarto successfully.")}catch(t){console.error("Failed to export note with Quarto:",t),new p.Notice("Failed to export note with Quarto.")}}}; diff --git a/main.ts b/main.ts index 7646c65..6710cd3 100644 --- a/main.ts +++ b/main.ts @@ -16,7 +16,7 @@ import { WorkspaceLeaf, PluginSettingTab, // Added Setting, // Added - TextComponent, + FileSystemAdapter, } from 'obsidian'; import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; import * as fs from 'fs'; @@ -38,12 +38,14 @@ const VIEW_TYPE_R_HELP = 'r-help-view'; interface MyPluginSettings { rExecutablePath: string; rstudioPandocPath: string; // New property + quartoExecutablePath: string; // Add this line } // Define default settings const DEFAULT_SETTINGS: MyPluginSettings = { rExecutablePath: '/usr/local/bin/R', // Default path to R executable rstudioPandocPath: '/opt/homebrew/bin/', // OS-specific default path + quartoExecutablePath: '/usr/local/bin/quarto', }; // Create the REnvironmentView class @@ -235,8 +237,6 @@ class REnvironmentView extends ItemView { document.head.appendChild(style); } } - - // Add this new class after REnvironmentView class class RHelpView extends ItemView { @@ -322,49 +322,69 @@ class MyPluginSettingTab extends PluginSettingTab { containerEl.createEl('h2', { text: 'R Integration Settings' }); - new Setting(containerEl) - .setName('Path to R Executable') - .setDesc('Specify the full path to your R executable.') - .addText((text) => - text - .setPlaceholder('/usr/local/bin/R') - .setValue(this.plugin.settings.rExecutablePath) - .onChange(async (value: string) => { // Added type annotation - const trimmedValue = value.trim(); - console.log('R Executable Path changed to: ' + trimmedValue); - // Validate the path - if (fs.existsSync(trimmedValue) && fs.statSync(trimmedValue).isFile()) { - this.plugin.settings.rExecutablePath = trimmedValue; - await this.plugin.saveSettings(); - new Notice('R executable path updated successfully.'); - } else { - new Notice('Invalid R executable path. Please enter a valid path.'); - console.error('Invalid R executable path provided:', trimmedValue); - } - }) - ); - - new Setting(containerEl) - .setName('Path to RStudio Pandoc') - .setDesc('Specify the full path to your RStudio Pandoc installation.') - .addText((text: TextComponent) => // Added type annotation - text - .setPlaceholder('/opt/homebrew/bin/') - .setValue(this.plugin.settings.rstudioPandocPath) - .onChange(async (value: string) => { // Added type annotation - const trimmedValue = value.trim(); - console.log('RStudio Pandoc Path changed to: ' + trimmedValue); - // Validate the path - if (fs.existsSync(trimmedValue) && fs.statSync(trimmedValue).isDirectory()) { - this.plugin.settings.rstudioPandocPath = trimmedValue; - await this.plugin.saveSettings(); - new Notice('RStudio Pandoc path updated successfully.'); - } else { - new Notice('Invalid RStudio Pandoc path. Please enter a valid directory path.'); - console.error('Invalid RStudio Pandoc path provided:', trimmedValue); - } - }) - ); +// Function to process path for Windows compatibility with type annotation +function formatPathForWindows(path: string): string { + if (navigator.platform.includes('Win')) { + // First, replace all single backslashes with double backslashes + // Then, replace all forward slashes with double backslashes + return path.replace(/\\/g, '\\\\').replace(/\//g, '\\\\'); + } + return path; +} + + +// Setting for R Executable Path +new Setting(containerEl) + .setName('Path to R Executable') + .setDesc('Specify the full path to your R executable.') + .addText((text) => + text + .setPlaceholder('/usr/local/bin/R') + .setValue(formatPathForWindows(this.plugin.settings.rExecutablePath)) + .onChange(async (value) => { + const formattedValue = formatPathForWindows(value.trim()); + console.log('R Executable Path changed to: ' + formattedValue); + + this.plugin.settings.rExecutablePath = formattedValue; + await this.plugin.saveSettings(); + new Notice('R executable path updated successfully.'); + }) + ); + +// Setting for RStudio Pandoc Path +new Setting(containerEl) + .setName('Path to RStudio Pandoc') + .setDesc('Specify the full path to your RStudio Pandoc installation.') + .addText((text) => + text + .setPlaceholder('/opt/homebrew/bin/') + .setValue(formatPathForWindows(this.plugin.settings.rstudioPandocPath)) + .onChange(async (value) => { + const formattedValue = formatPathForWindows(value.trim()); + console.log('RStudio Pandoc Path changed to: ' + formattedValue); + + this.plugin.settings.rstudioPandocPath = formattedValue; + await this.plugin.saveSettings(); + new Notice('RStudio Pandoc path updated successfully.'); + }) + ); + + // Setting for Quarto Executable Path + new Setting(containerEl) + .setName('Quarto Executable Path') + .setDesc('Specify the full path to your Quarto executable. Example: /usr/local/bin/quarto') + .addText((text) => + text + .setPlaceholder('/usr/local/bin/quarto') + .setValue(this.plugin.settings.quartoExecutablePath) + .onChange(async (value) => { + console.log('Quarto Executable Path changed to: ' + value); + this.plugin.settings.quartoExecutablePath = value.trim(); + await this.plugin.saveSettings(); + new Notice('Quarto executable path updated successfully.'); + }) + ); + } @@ -477,6 +497,14 @@ if (this.app.workspace.getLeavesOfType(VIEW_TYPE_R_HELP).length === 0) { }, ], }); + + this.addCommand({ + id: 'export-note-with-quarto', + name: 'Export Note with Quarto', + callback: () => this.exportNoteWithQuarto(), + }); + + console.log('R Code Evaluator Plugin loaded successfully'); } @@ -501,115 +529,284 @@ if (this.app.workspace.getLeavesOfType(VIEW_TYPE_R_HELP).length === 0) { console.log('R Code Evaluator Plugin unloaded successfully'); } - // Run code chunk in editing mode - runCurrentCodeChunk(editor: Editor, view: MarkdownView, noteTitle: string) { - const cursor = editor.getCursor(); - const { startLine, endLine, code, existingLabel } = this.getCurrentCodeChunk(editor, cursor.line); - - if (code) { - const uniqueId = existingLabel || this.generateUniqueId(startLine); // Use existing label or generate new based on position - const notePath = view.file?.path; - if (notePath) { - if (!existingLabel) { - // Insert the generated label into the code chunk - this.insertLabel(editor, startLine, uniqueId); - } - - console.log(`Running current code chunk in note: ${notePath} with ID: ${uniqueId}`); - - // Updated regular expressions to detect help requests anywhere in the code - const isHelpRequest = - /\?\s*\w+/.test(code) || // Detects patterns like "?functionName" - /help\s*\(\s*\w+\s*\)/.test(code); // Detects patterns like "help(functionName)" +// Run code chunk in editing mode +runCurrentCodeChunk(editor: Editor, view: MarkdownView, noteTitle: string) { + const cursor = editor.getCursor(); + const { startLine, endLine: originalEndLine, code, existingLabel, options } = this.getCurrentCodeChunk(editor, cursor.line); + + let endLine = originalEndLine; // We'll adjust this if necessary + + if (code) { + const uniqueId = existingLabel || this.generateUniqueId(startLine); // Use existing label or generate new based on position + const notePath = view.file?.path; + if (notePath) { + if (!existingLabel) { + // Insert the generated label into the code chunk + const linesInserted = this.insertLabel(editor, startLine, uniqueId); + // Adjust endLine since we've inserted new lines + endLine += linesInserted; + } + + console.log(`Running current code chunk in note: ${notePath} with ID: ${uniqueId}`); + + // Determine if it's a help request + const isHelpRequest = + /\?\s*\w+/.test(code) || // Detects patterns like "?functionName" + /help\s*\(\s*\w+\s*\)/.test(code); // Detects patterns like "help(functionName)" + + this.runRCodeInSession(notePath, code, noteTitle, uniqueId, isHelpRequest, options) // Pass the options + .then(({ result, imagePaths, widgetPaths, helpContent }) => { + console.log('R code executed successfully'); + + if (isHelpRequest) { + // Help content is already handled inside runRCodeInSession + new Notice('Help content updated in the sidebar.'); + } else { + // Handle options + const includeOption = options['include'] == 'false'; + const outputOption = options['output'] == 'false'; + + - this.runRCodeInSession(notePath, code, noteTitle, uniqueId, isHelpRequest) // Pass the flag - .then(({ result, imagePaths, widgetPaths, helpContent }) => { - console.log('R code executed successfully'); - - if (isHelpRequest) { - // Help content is already handled inside runRCodeInSession - new Notice('Help content updated in the sidebar.'); - } else { - // Insert output and images into the editor - this.insertOutputWithCallout(editor, endLine, result, imagePaths, widgetPaths, uniqueId); + if (result || imagePaths.length > 0 || widgetPaths.length > 0) { + // Insert output and images into the editor if output is not suppressed and there's content + this.insertOutputWithCallout(editor, endLine, result, imagePaths, widgetPaths, uniqueId, options); + } + if(outputOption){ + // Output is suppressed or there's no content + // Remove existing output callout if present + this.removeOutputCallout(editor, uniqueId); + } + + if (includeOption) { + // Include is false, remove existing output callout if present + this.removeOutputCallout(editor, uniqueId); } - }) - .catch((err) => { - console.error('Error executing R code:', err); - this.insertOutputWithCallout(editor, endLine, `Error:\n${err}`, [], [], uniqueId); - }); - } else { - new Notice('No file associated with the current view.'); - console.error('No file associated with the current view.'); - } + + } + }) + .catch((err) => { + console.error('Error executing R code:', err); + // Check if errors should be included + if (options['error'] !== 'false' && options['include'] !== 'false' && options['output'] !== 'false') { + this.insertOutputWithCallout(editor, endLine, `Error:\n${err}`, [], [], uniqueId, options); + } else { + // Remove existing output callout if present + this.removeOutputCallout(editor, uniqueId); + } + }); } else { - new Notice('No R code chunk found at the cursor position.'); - console.log('No R code chunk found at the cursor position.'); + new Notice('No file associated with the current view.'); + console.error('No file associated with the current view.'); } + } else { + new Notice('No R code chunk found at the cursor position.'); + console.log('No R code chunk found at the cursor position.'); } - +} - // Get the current code chunk based on cursor position - getCurrentCodeChunk( - editor: Editor, - cursorLine: number - ): { startLine: number; endLine: number; code: string; existingLabel: string | null } { - const totalLines = editor.lineCount(); - let startLine = cursorLine; - let endLine = cursorLine; - let existingLabel: string | null = null; - // Find the start of the code chunk - while (startLine >= 0 && !editor.getLine(startLine).startsWith('```r')) { - startLine--; - } - // If not found, return invalid - if (startLine < 0) { - return { startLine: -1, endLine: -1, code: '', existingLabel: null }; +// Get the current code chunk based on cursor position +getCurrentCodeChunk( + editor: Editor, + cursorLine: number +): { + startLine: number; + endLine: number; + code: string; + existingLabel: string | null; + options: { [key: string]: string }; +} { + const totalLines = editor.lineCount(); + let startLine = cursorLine; + let endLine = cursorLine; + let existingLabel: string | null = null; + let options: { [key: string]: string } = {}; + + // Find the start of the code chunk + while (startLine >= 0 && !this.isCodeChunkStart(editor.getLine(startLine))) { + startLine--; + } + + // If not found, return invalid + if (startLine < 0) { + return { startLine: -1, endLine: -1, code: '', existingLabel: null, options: {} }; + } + + // Find the end of the code chunk + endLine = startLine + 1; + while (endLine < totalLines && !editor.getLine(endLine).startsWith('```')) { + endLine++; + } + + if (endLine >= totalLines) { + // No closing ``` + return { startLine: -1, endLine: -1, code: '', existingLabel: null, options: {} }; + } + + // Parse the label and options from the code chunk + existingLabel = this.parseChunkLabel(editor, startLine); + options = this.parseChunkOptions(editor, startLine); + + // Extract the code chunk content, skipping options lines + const codeLines = []; + for (let i = startLine + 1; i < endLine; i++) { + const line = editor.getLine(i); + // Skip lines starting with '#|' as they are options + if (!line.trim().startsWith('#|')) { + codeLines.push(line); } + } + const code = codeLines.join('\n'); + + console.log(`Found code chunk from line ${startLine} to ${endLine} with label: ${existingLabel}`); + return { startLine, endLine, code, existingLabel, options }; +} + +// Helper method to check if a line is the start of a code chunk +isCodeChunkStart(line: string): boolean { + // Remove leading whitespace + line = line.trim(); + // Match lines that start with ```r or ```{r} + return /^```{?r/.test(line); +} + - // Find the end of the code chunk - endLine = startLine + 1; - while (endLine < totalLines && !editor.getLine(endLine).startsWith('```')) { - endLine++; +// Helper method to parse options from the code chunk +parseChunkOptions(editor: Editor, startLine: number): { [key: string]: string } { + const options: { [key: string]: string } = {}; + let lineIndex = startLine + 1; + const totalLines = editor.lineCount(); + + while (lineIndex < totalLines) { + const line = editor.getLine(lineIndex).trim(); + if (line.startsWith('#|')) { + // Extract the option key and value + const optionMatch = line.match(/^#\|\s*(\w+)\s*:\s*(.*)$/); + if (optionMatch) { + const key = optionMatch[1]; + let value = optionMatch[2]; + + // Remove surrounding quotes if present + value = value.replace(/^["']|["']$/g, ''); + + options[key] = value; + } + lineIndex++; + } else if (line === '' || line.startsWith('#')) { + // Skip empty lines or comments + lineIndex++; + } else { + // Reached the end of options + break; } + } + + return options; +} + + +// Helper method to parse the label from the code chunk options +parseChunkLabel(editor: Editor, startLine: number): string | null { + const fenceLine = editor.getLine(startLine).trim(); + + // Check if Quarto code chunk with options in comments + if (fenceLine.startsWith('```{r')) { + let lineIndex = startLine + 1; + const totalLines = editor.lineCount(); + + while (lineIndex < totalLines) { + const line = editor.getLine(lineIndex).trim(); + if (line.startsWith('#|')) { + // Extract the option key and value + const optionMatch = line.match(/^#\|\s*(\w+)\s*:\s*(.*)$/); + if (optionMatch) { + const key = optionMatch[1]; + let value = optionMatch[2]; - if (endLine >= totalLines) { - // No closing ``` - return { startLine: -1, endLine: -1, code: '', existingLabel: null }; + // Remove surrounding quotes if present + value = value.replace(/^["']|["']$/g, ''); + + if (key === 'label') { + return value; + } + } + lineIndex++; + } else if (line === '' || line.startsWith('#')) { + // Skip empty lines or comments + lineIndex++; + } else { + // Reached the end of options + break; + } } + } + + // Check for R Markdown style label inside braces + const labelMatch = fenceLine.match(/\{.*(#[^\s\}]+).*\}/); + if (labelMatch) { + const label = labelMatch[1].substring(1); // Remove the '#' character + return label; + } + + return null; +} + +// Insert the generated label into the code chunk +insertLabel(editor: Editor, startLine: number, uniqueId: string): number { + const fenceLine = editor.getLine(startLine).trim(); + const isQuartoChunk = fenceLine.startsWith('```{r'); - // Extract the code chunk content - const codeLines = []; - for (let i = startLine + 1; i < endLine; i++) { - codeLines.push(editor.getLine(i)); + if (isQuartoChunk) { + // Insert label into Quarto chunk options as a `#| label: ...` line + let insertPosition = startLine + 1; + const totalLines = editor.lineCount(); + + // Find the position after existing `#|` options + while (insertPosition < totalLines) { + const line = editor.getLine(insertPosition).trim(); + if (line.startsWith('#|')) { + insertPosition++; + } else { + break; + } } - const code = codeLines.join('\n'); - // Check for existing label in the code chunk fence line, e.g., ```r {#label} - const fenceLine = editor.getLine(startLine); - const labelMatch = fenceLine.match(/\{#([^\}]+)\}/); - if (labelMatch) { - existingLabel = labelMatch[1]; + // Insert the label option + editor.replaceRange(`#| label: ${uniqueId}\n`, { line: insertPosition, ch: 0 }); + console.log(`Inserted label "#| label: ${uniqueId}" into code chunk at line ${insertPosition}`); + return 1; // We inserted one line + } else { + // Insert label into R Markdown chunk options inside the braces + const match = fenceLine.match(/^(```{r)(.*)(})?$/); + if (!match) { + // Not a valid code chunk start line + return 0; // Return 0 since no lines were inserted } - console.log(`Found code chunk from line ${startLine} to ${endLine} with label: ${existingLabel}`); - return { startLine, endLine, code, existingLabel }; - } + const start = match[1]; // '```{r' + const options = match[2] || ''; // existing options + const end = match[3] ? '}' : ''; // closing '}' - // Insert the generated label into the code chunk - insertLabel(editor: Editor, startLine: number, uniqueId: string) { - const fenceLine = editor.getLine(startLine); - if (fenceLine.includes('{#')) { - // Already has a label, do not insert - return; + // Append label option + let newOptions; + if (options.trim() === '') { + newOptions = ` {#${uniqueId}}`; + } else { + newOptions = options + ` {#${uniqueId}}`; } - // Insert the label at the end of the fence line - const newFenceLine = fenceLine.replace(/```r/, `\`\`\`r {#${uniqueId}}`); + + // Reconstruct the fence line + const newFenceLine = `${start}${newOptions}${end || '}'}`; + editor.setLine(startLine, newFenceLine); - console.log(`Inserted label {#${uniqueId}} into code chunk at line ${startLine}`); + console.log(`Inserted label "{#${uniqueId}}" into code chunk at line ${startLine}`); + return 0; // No new lines were inserted } +} + + // Insert or replace the output callout with images insertOutputWithCallout( @@ -618,36 +815,91 @@ insertOutputWithCallout( output: string, imagePaths: string[], widgetPaths: string[], - uniqueId: string + uniqueId: string, + options: { [key: string]: string } // Add this parameter ) { console.log('Inserting or updating output callout and images into the editor'); + + + // Prepare the content lines + const contentLines: string[] = []; + + // Prepare the output content, prefixing each line with '> ' + if (output && output.trim() !== '') { + const outputLines = output.trim().split('\n').map((line) => '> ' + line); + contentLines.push(...outputLines); + } + + // Add new image and animation links inside the callout + imagePaths.forEach((imagePath) => { + const vaultImagePath = `${imagePath}`; // Path in the vault + const imageMarkdown = `![center|480](${vaultImagePath})`; + contentLines.push(`> ${imageMarkdown}`); + }); + + widgetPaths.forEach((widgetPath) => { + const widgetMarkdown = ``; + contentLines.push(`> ${widgetMarkdown}`); + }); + + // If there's no content, avoid inserting an empty callout + if (contentLines.length === 0) { + console.log('No output, images, or widgets to insert. Skipping callout insertion.'); + return; + } // Define the callout block with unique ID - const calloutStart = `> [!OUTPUT]+ {#output-${uniqueId}}\n> \n`; - const calloutContentPrefix = '> '; // Each line of content should start with '> ' - let outputContent = calloutStart; - - // Prepare the output content, prefixing each line with '> ' - const outputLines = output.trim().split('\n').map((line) => calloutContentPrefix + line); - outputContent += outputLines.join('\n'); - - // Add new image and animation links inside the callout - imagePaths.forEach((imagePath) => { - const vaultImagePath = `${imagePath}`; // Path in the vault - const imageMarkdown = `![center| 480 ](${vaultImagePath})`; - outputContent += `\n${calloutContentPrefix} ${imageMarkdown}`; - }); - - widgetPaths.forEach((widgetPath) => { - const vaultWidgetPath = `${widgetPath}`; // Path in the vault - const widgetMarkdown = ``; - outputContent += `\n${calloutContentPrefix} ${widgetMarkdown}`; - }); + let outputContent = `> [!OUTPUT]+ {#output-${uniqueId}}\n`; + // Append content lines to the outputContent + outputContent += contentLines.join('\n') + '\n'; // Ensure there's a newline after the callout content - outputContent += '\n> \n'; + outputContent += '> \n'; + + + // Read the current content to check for existing output + let existingOutputStart = -1; + let existingOutputEnd = -1; + const totalLines = editor.lineCount(); + + for (let i = 0; i < totalLines; i++) { + const line = editor.getLine(i); + if (line.trim() === `> [!OUTPUT]+ {#output-${uniqueId}}`) { + existingOutputStart = i; + // Find the end of the callout block + existingOutputEnd = i; + while (existingOutputEnd + 1 < totalLines) { + const nextLine = editor.getLine(existingOutputEnd + 1); + // Check if the next line is part of the callout + if (!nextLine.startsWith('> ') && nextLine.trim() !== '') { + break; + } + existingOutputEnd++; + } + break; + } + } + + if (existingOutputStart !== -1 && existingOutputEnd !== -1) { + // Replace the existing callout block + const from = { line: existingOutputStart, ch: 0 }; + const to = { line: existingOutputEnd + 1, ch: 0 }; + editor.replaceRange(outputContent + '\n', from, to); + console.log(`Replaced existing output callout for ID: ${uniqueId}`); + } else { + // Insert the new callout block after the code chunk + const insertPosition = { line: endLine + 1, ch: 0 }; + // Insert a leading newline to separate from the code chunk + editor.replaceRange('\n' + outputContent + '\n', insertPosition); + console.log(`Inserted new output callout for ID: ${uniqueId}`); + } +} - // Read the current content to check for existing output +// Remove the output callout with the given unique ID +removeOutputCallout(editor: Editor, uniqueId: string) { + console.log(`Removing output callout for ID: ${uniqueId} if it exists`); + + // Find the existing callout let existingOutputStart = -1; let existingOutputEnd = -1; const totalLines = editor.lineCount(); @@ -671,22 +923,17 @@ insertOutputWithCallout( } if (existingOutputStart !== -1 && existingOutputEnd !== -1) { - // Replace the existing callout block + // Remove the existing callout block const from = { line: existingOutputStart, ch: 0 }; const to = { line: existingOutputEnd + 1, ch: 0 }; - editor.replaceRange(outputContent + '\n', from, to); - console.log(`Replaced existing output callout for ID: ${uniqueId}`); + editor.replaceRange('', from, to); + console.log(`Removed existing output callout for ID: ${uniqueId}`); } else { - // Insert the new callout block after the code chunk - const insertPosition = { line: endLine + 1, ch: 0 }; - // Insert a leading newline to separate from the code chunk - editor.replaceRange('\n' + outputContent + '\n', insertPosition); - console.log(`Inserted new output callout for ID: ${uniqueId}`); + console.log(`No existing output callout found for ID: ${uniqueId}`); } } - // Get or create R process for the note getRProcess(notePath: string): ChildProcessWithoutNullStreams { let rProcess = this.rProcesses.get(notePath); @@ -743,13 +990,14 @@ options(device = function(...) jpeg(filename = tempfile(), width=800, height=600 return rProcess; } - async runRCodeInSession( - notePath: string, - code: string, - noteTitle: string, - uniqueId: string, - isHelpRequest: boolean // New parameter - ): Promise<{ result: string; imagePaths: string[]; widgetPaths: string[]; helpContent: string }> { + async runRCodeInSession( + notePath: string, + code: string, + noteTitle: string, + uniqueId: string, + isHelpRequest: boolean, + options: { [key: string]: string } // New parameter + ): Promise<{ result: string; imagePaths: string[]; widgetPaths: string[]; helpContent: string }> { @@ -762,18 +1010,37 @@ options(device = function(...) jpeg(filename = tempfile(), width=800, height=600 // Create a temporary directory for R to output plots const tempDir = await mkdtempAsync(path.join(os.tmpdir(), 'rplots-')); - const tempDirEscaped = tempDir.replace(/\\/g, '/'); // Ensure forward slashes + // const tempDirEscaped = tempDir.replace(/\\/g, '/'); // Ensure forward slashes + + const tempDirEscaped = process.platform === 'win32' + ? tempDir.replace(/\\/g, '\\\\') + :tempDir.replace(/\\/g, '/'); + // Generate a temp file path for the help content const tempHelpFilePath = path.join(tempDirEscaped, `help_${uniqueId}.txt`); - const tempHelpFilePathR = tempHelpFilePath.replace(/\\/g, '/'); + //const tempHelpFilePathR = tempHelpFilePath.replace(/\\/g, '/'); + + const tempHelpFilePathR = process.platform === 'win32' + ? tempHelpFilePath.replace(/\\/g, '\\\\') + :tempHelpFilePath .replace(/\\/g, '/'); + + const marker = `__END_OF_OUTPUT__${Date.now()}__`; const imageMarker = `__PLOT_PATH__`; const envMarker = `__ENVIRONMENT_DATA__${Date.now()}__`; const widgetMarker = `__WIDGET_PATH__`; // Define the widget marker - + const optsCode = ` + opts <- list( + echo = ${options['echo'] !== 'false' ? 'TRUE' : 'FALSE'}, + warning = ${options['warning'] !== 'false' ? 'TRUE' : 'FALSE'}, + error = ${options['error'] !== 'false' ? 'TRUE' : 'FALSE'}, + include = ${options['include'] !== 'false' ? 'TRUE' : 'FALSE'}, + output = ${options['output'] !== 'false' ? 'TRUE' : 'FALSE'} + ) + `; // Prepare code to send to R using the 'evaluate' package const wrappedCode = ` @@ -782,6 +1049,9 @@ library(jsonlite) library(htmlwidgets) Sys.setenv(RSTUDIO_PANDOC='${this.settings.rstudioPandocPath}') + +${optsCode} + # Define our custom print function custom_print_htmlwidget <- function(x, ..., viewer = NULL) { # Generate a unique filename @@ -881,7 +1151,7 @@ print.help_files_with_topic <- function(x, ...) if(type == "text") { pkgname <- basename(dirname(dirname(file))) - tools::Rd2HTML(.getHelpFile(file), out = "${tempHelpFilePath}", + tools::Rd2HTML(.getHelpFile(file), out = "${tempHelpFilePathR}", package = pkgname) } @@ -907,56 +1177,61 @@ for (res in results) { if (inherits(res, "source")) { # Ignore source elements } else if (inherits(res, "warning")) { - outputs <- c(outputs, paste("Warning:", conditionMessage(res))) + if (opts$warning && opts$include) { + outputs <- c(outputs, paste("Warning:", conditionMessage(res))) + } } else if (inherits(res, "message")) { - outputs <- c(outputs, conditionMessage(res)) + if (opts$output && opts$include) { + outputs <- c(outputs, res$message) + } } else if (inherits(res, "error")) { - outputs <- c(outputs, paste("Error:", conditionMessage(res))) + if (opts$error && opts$include) { + outputs <- c(outputs, paste("Error:", conditionMessage(res))) + } } else if (inherits(res, "character")) { - - val_str <- res - outputs <- c(outputs, val_str) - + if (opts$output && opts$include) { + outputs <- c(outputs, res) + } } else if (inherits(res, "recordedplot")) { - # Save the plot to a file using uniqueId - timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") - plotFileName <- paste0("plot_${uniqueId}_", length(imagePaths) + 1, "_", timestamp, ".jpg") - plotFilePath <- file.path("${tempDirEscaped}", plotFileName) - jpeg(filename=plotFilePath, width=800, height=600) - replayPlot(res) - dev.off() - imagePaths <- c(imagePaths, plotFileName) + if (opts$output && opts$include) { + # Save the plot to a file using uniqueId + timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") + plotFileName <- paste0("plot_${uniqueId}_", length(imagePaths) + 1, "_", timestamp, ".jpg") + plotFilePath <- file.path("${tempDirEscaped}", plotFileName) + jpeg(filename=plotFilePath, width=800, height=600) + replayPlot(res) + dev.off() + imagePaths <- c(imagePaths, plotFileName) + } } } -# Output the collected outputs -if (length(outputs) > 0) { - cat(paste(outputs, collapse = "\\n"), "\\n") -} - - # Attempt to retrieve the last animation, if any -if (requireNamespace("gganimate", quietly = TRUE)) { - anim <- try(gganimate::last_animation(), silent = TRUE) - - - +if (opts$output && opts$include) { + if (requireNamespace("gganimate", quietly = TRUE)) { + anim <- try(gganimate::last_animation(), silent = TRUE) if (is.character(anim[1])) { - if(file.info(anim[1])$mtime > timecheck){ - # 'anim' is a file path to the GIF - timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") - animFileName <- paste0("animation_${uniqueId}_", timestamp, ".gif") - animFilePath <- file.path("${tempDirEscaped}", animFileName) # Corrected line - file.copy(anim[1], animFilePath) - imagePaths <- c(imagePaths, animFileName) - }} - - + if (file.info(anim[1])$mtime > timecheck) { + timestamp <- format(Sys.time(), "%Y%m%d%H%M%S") + animFileName <- paste0("animation_${uniqueId}_", timestamp, ".gif") + animFilePath <- file.path("${tempDirEscaped}", animFileName) + file.copy(anim[1], animFilePath) + imagePaths <- c(imagePaths, animFileName) + } + } + } +} + +# Output the collected outputs +if (opts$output && opts$include && length(outputs) > 0) { + cat(paste(outputs, collapse = "\\n"), "\\n") } # Output image markers -for (img in imagePaths) { - cat("${imageMarker}", img, "\\n", sep="") +if (opts$output && opts$include) { + for (img in imagePaths) { + cat("${imageMarker}", img, "\\n", sep="") + } } # Output the environment data @@ -1190,4 +1465,161 @@ for (const imageFileName of imagePaths) { console.log('Wrapped R code sent to the R process'); }); } + + // Implement quarto export: + + + addFrontMatter(content: string, activeFile: TFile): string { + if (content.startsWith('---\n')) { + // Front matter already exists + return content; + } else { + const title = activeFile.basename; + const frontMatter = `--- +title: "${title}" +author: "Author Name" +date: today +format: html +---\n\n`; + return frontMatter + content; + } +} + +sanitizeFileName(fileName: string): string { + // Replace spaces and other invalid characters with underscores + return fileName.replace(/[^a-zA-Z0-9-_]/g, '_'); +} + + +stripOutputCallouts(content: string): string { + // Use a regular expression to match and remove output callouts + const outputCalloutRegex = /> \[!OUTPUT\]\+ {#output-[\w]+}\n(?:>.*\n)*>/gm; + return content.replace(outputCalloutRegex, ''); +} + +// save note to quarto +async savePreparedNote(content: string, originalFilePath: string): Promise { + const baseName = path.basename(originalFilePath, '.md'); + const sanitizedBaseName = this.sanitizeFileName(baseName); + const fileName = sanitizedBaseName + '_quarto.qmd'; // Use .qmd extension for Quarto + + // Get the base path of the vault + let basePath: string; + const adapter = this.app.vault.adapter; + + if (adapter instanceof FileSystemAdapter) { + basePath = adapter.getBasePath(); + } else { + new Notice('Unable to determine vault base path. Export failed.'); + throw new Error('Vault adapter is not a FileSystemAdapter.'); + } + + const exportFolderPath = path.join(basePath, 'Exports'); + + // Ensure the Exports folder exists + if (!fs.existsSync(exportFolderPath)) { + fs.mkdirSync(exportFolderPath); + } + + const exportFilePath = path.join(exportFolderPath, fileName); + + // Write the content to the export file + await fs.promises.writeFile(exportFilePath, content, 'utf8'); + + return exportFilePath; +} + +async renderWithQuarto(exportFilePath: string) { + return new Promise((resolve, reject) => { + const quartoExecutable = this.settings.quartoExecutablePath || 'quarto'; + + // Check if the Quarto executable exists + if (!fs.existsSync(quartoExecutable)) { + new Notice(`Quarto executable not found at ${quartoExecutable}. Please update the path in settings.`); + console.error(`Quarto executable not found at ${quartoExecutable}.`); + reject(new Error(`Quarto executable not found at ${quartoExecutable}.`)); + return; + } + + // Get R executable path from settings + const rExecutablePath = this.settings.rExecutablePath || 'Rscript'; + + // Set up environment variables + const env = { ...process.env }; + env.QUARTO_R = rExecutablePath; + + // Optional: Ensure Rscript is in PATH + const rscriptDir = path.dirname(rExecutablePath); + env.PATH = `${rscriptDir}${path.delimiter}${env.PATH}`; + + // Spawn the Quarto render process with the updated environment + const renderProcess = spawn( + quartoExecutable, + ['render', exportFilePath], + { stdio: ['ignore', 'pipe', 'pipe'], env: env } + ); + + let stderrData = ''; // Variable to accumulate stderr output + + // Capture stderr output + renderProcess.stderr.on('data', (data) => { + stderrData += data.toString(); + }); + + renderProcess.on('error', (err) => { + new Notice('Failed to start Quarto rendering process.'); + console.error(`Failed to start Quarto rendering process: ${err}`); + reject(err); + }); + + renderProcess.on('exit', (code, signal) => { + console.log(`Quarto render process exited with code: ${code}, signal: ${signal}`); + if (code === 0) { + new Notice('Quarto rendering completed successfully.'); + resolve(); + } else { + // Quarto exited with an error, present stderr output + console.error(`Quarto stderr: ${stderrData}`); + new Notice('Quarto rendering failed. Click for details.', 10000); + reject(new Error(`Quarto exited with code ${code}`)); + } + }); + }); +} + + +// export note with quarto +async exportNoteWithQuarto() { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + new Notice('No active note to export.'); + return; + } + + try { + const originalContent = await this.app.vault.read(activeFile); + let content = originalContent; + + // Add front matter + content = this.addFrontMatter(content, activeFile); + + // Strip output callouts + content = this.stripOutputCallouts(content); + + // Save the prepared note + const exportFilePath = await this.savePreparedNote(content, activeFile.path); + + // Optionally, render with Quarto + await this.renderWithQuarto(exportFilePath); + + // Open the rendered file or provide further instructions + new Notice('Note exported and rendered with Quarto successfully.'); + + } catch (err) { + console.error('Failed to export note with Quarto:', err); + new Notice('Failed to export note with Quarto.'); + } } + +} + diff --git a/manifest.json b/manifest.json index 992465b..c003051 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1 @@ -{ - "id": "ridian", - "name": "Ridian", - "version": "0.0.1", - "minAppVersion": "0.15.0", - "description": "Execute R code blocks and display outputs and plots within Obsidian.", - "author": "Michel Nivard", - "authorUrl": "https://github.com/MichelNivard/Ridian", - "isDesktopOnly": true -} +{"id":"ridian","name":"Ridian","version":"0.0.2","minAppVersion":"0.15.0","description":"Execute R code blocks and display outputs and plots within Obsidian.","author":"Michel Nivard","authorUrl":"https://github.com/MichelNivard/Ridian","isDesktopOnly":true} \ No newline at end of file diff --git a/package.json b/package.json index aa16d65..a2ad032 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ridian", - "version": "0.0.1", + "name": "Ridian", + "version": "0.0.2", "description": "Execute R code blocks and display outputs and plots within Obsidian.", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 4fde155..74d6d03 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "0.0.1": "0.15.0" + "0.0.2": "0.15.0" }