diff --git a/apps/ios/EdgeEver/Resources/EditorBundle/index.html b/apps/ios/EdgeEver/Resources/EditorBundle/index.html
index 5338da09..a8905476 100644
--- a/apps/ios/EdgeEver/Resources/EditorBundle/index.html
+++ b/apps/ios/EdgeEver/Resources/EditorBundle/index.html
@@ -7,14 +7,14 @@
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
EdgeEver Editor
-
+`},"getStyles"),RT,nu=(RT=class{constructor(){this.d=""}moveTo(e,r){return this.d+=`M ${e} ${r} `,this}lineTo(e,r){return this.d+=`L ${e} ${r} `,this}horizontalTo(e){return this.d+=`H ${e} `,this}verticalTo(e){return this.d+=`V ${e} `,this}arcTo(e,r,n,i,s,a,o){return this.d+=`A ${e} ${r} ${n} ${i?1:0} ${s?1:0} ${a} ${o} `,this}build(){return this.d.trim()}},w(RT,"PathBuilder"),RT),_T,Nxr=(_T=class{constructor(e,r=aie()){this.textCache=new Map,this.svg=e,this.config=r}measureText(e){if(this.textCache.has(e))return this.textCache.get(e);const r=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(e),n=r.node().getBBox(),i={width:n.width,height:n.height};return r.remove(),this.textCache.set(e,i),i}renderTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-terminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s).attr("rx",10).attr("ry",10),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderNonTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-nonterminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderSequence(e,r){const n=r.map(u=>this.renderExpression(e,u));let i=0,s=0,a=0;for(const u of n)i+=u.dimensions.width,s=Math.max(s,u.dimensions.up),a=Math.max(a,u.dimensions.down);i+=(n.length-1)*this.config.horizontalSeparation;const o=e.append("g").attr("class","railroad-sequence");let l=0;for(let u=0;uthis.renderExpression(e,f));let i=0,s=0;for(const f of n)i=Math.max(i,f.dimensions.width),s+=f.dimensions.height;s+=(n.length-1)*this.config.verticalSeparation;const a=this.config.arcRadius,o=a*4,l=i+o,u=e.append("g").attr("class","railroad-choice");let h=0;const d=s/2;for(const f of n){const p=h,g=p+f.dimensions.up,m=a*2+(i-f.dimensions.width)/2;u.node().appendChild(f.element).setAttribute("transform",`translate(${m}, ${p})`);const v=new nu,x=g>d;g===d?v.moveTo(0,d).lineTo(m,g):v.moveTo(0,d).arcTo(a,a,0,!1,x,a,d+(x?a:-a)).lineTo(a,g-(x?a:-a)).arcTo(a,a,0,!1,!x,a*2,g).lineTo(m,g),u.append("path").attr("class","railroad-line").attr("d",v.build());const b=new nu,T=m+f.dimensions.width,C=l-a*2;g===d?b.moveTo(T,g).lineTo(l,d):b.moveTo(T,g).lineTo(C,g).arcTo(a,a,0,!1,!x,l-a,g+(x?-a:a)).lineTo(l-a,d+(x?a:-a)).arcTo(a,a,0,!1,x,l,d),u.append("path").attr("class","railroad-line").attr("d",b.build()),h+=f.dimensions.height+this.config.verticalSeparation}return{element:u.node(),dimensions:{width:l,height:s,up:d,down:s-d}}}renderOptional(e,r){const n=this.renderExpression(e,r),i=this.config.arcRadius,s=i*2,a=n.dimensions.width+i*4,o=n.dimensions.height+s,l=e.append("g").attr("class","railroad-optional"),u=i*2,h=s;l.node().appendChild(n.element).setAttribute("transform",`translate(${u}, ${h})`);const f=h+n.dimensions.up,p=new nu().moveTo(0,f).lineTo(i*2,f);l.append("path").attr("class","railroad-line").attr("d",p.build());const g=new nu().moveTo(u+n.dimensions.width,f).lineTo(a,f);l.append("path").attr("class","railroad-line").attr("d",g.build());const m=new nu().moveTo(0,f).arcTo(i,i,0,!1,!1,i,f-i).lineTo(i,i).arcTo(i,i,0,!1,!0,i*2,0).lineTo(a-i*2,0).arcTo(i,i,0,!1,!0,a-i,i).lineTo(a-i,f-i).arcTo(i,i,0,!1,!1,a,f);return l.append("path").attr("class","railroad-line").attr("d",m.build()),{element:l.node(),dimensions:{width:a,height:o,up:f,down:o-f}}}renderRepetition(e,r,n){const i=this.renderExpression(e,r),s=this.config.arcRadius,a=s*2,o=i.dimensions.width+s*4,l=n===0,u=i.dimensions.height+a+(l?a:0),h=e.append("g").attr("class","railroad-repetition"),d=s*2,f=l?a:0;h.node().appendChild(i.element).setAttribute("transform",`translate(${d}, ${f})`);const g=f+i.dimensions.up;h.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(0,g).lineTo(s*2,g).build()),h.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(d+i.dimensions.width,g).lineTo(o,g).build());const m=f+i.dimensions.height+s,y=new nu().moveTo(d+i.dimensions.width,g).arcTo(s,s,0,!1,!0,d+i.dimensions.width+s,g+s).lineTo(d+i.dimensions.width+s,m).arcTo(s,s,0,!1,!0,d+i.dimensions.width,m+s).lineTo(s*2,m+s).arcTo(s,s,0,!1,!0,s,m).lineTo(s,g+s).arcTo(s,s,0,!1,!0,s*2,g);if(h.append("path").attr("class","railroad-line").attr("d",y.build()),l){const v=new nu().moveTo(0,g).arcTo(s,s,0,!1,!1,s,g-s).lineTo(s,s).arcTo(s,s,0,!1,!0,s*2,0).lineTo(o-s*2,0).arcTo(s,s,0,!1,!0,o-s,s).lineTo(o-s,g-s).arcTo(s,s,0,!1,!1,o,g);h.append("path").attr("class","railroad-line").attr("d",v.build())}return{element:h.node(),dimensions:{width:o,height:u,up:g,down:u-g}}}renderSpecial(e,r){const n=this.measureText("? "+r+" ?"),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-special");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text("? "+r+" ?"),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderExpression(e,r){switch(r.type){case"terminal":return this.renderTerminal(e,r.value);case"nonterminal":return this.renderNonTerminal(e,r.name);case"sequence":return this.renderSequence(e,r.elements);case"choice":return this.renderChoice(e,r.alternatives);case"optional":return this.renderOptional(e,r.element);case"repetition":return this.renderRepetition(e,r.element,r.min);case"special":return this.renderSpecial(e,r.text);default:throw new Error(`Unknown node type: ${r.type}`)}}renderRule(e,r){const n=this.svg.append("g").attr("class","railroad-rule").attr("transform",`translate(0, ${r})`),i=e.name+" =",s=this.measureText(i).width+20,a=s+20,o=n.append("g"),l=this.renderExpression(o,e.definition),u=Math.max(20,l.dimensions.up),h=u-l.dimensions.up;return o.attr("transform",`translate(${a}, ${h})`),n.append("g").attr("class","railroad-rule-name-group").append("text").attr("class","railroad-rule-name").attr("x",0).attr("y",u).text(i),n.append("g").attr("class","railroad-start").append("circle").attr("cx",s).attr("cy",u).attr("r",this.config.markerRadius),n.append("g").attr("class","railroad-end").append("circle").attr("cx",a+l.dimensions.width+10).attr("cy",u).attr("r",this.config.markerRadius),n.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(s+this.config.markerRadius,u).lineTo(a,u).build()),n.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(a+l.dimensions.width,u).lineTo(a+l.dimensions.width+10-this.config.markerRadius,u).build()),{height:Math.max(40,h+l.dimensions.height+this.config.padding*2),width:a+l.dimensions.width+10+this.config.markerRadius}}renderDiagram(e){let r=this.config.padding,n=0;for(const i of e){const s=this.renderRule(i,r);r+=s.height+this.config.verticalSeparation,n=Math.max(n,s.width)}return{width:n+this.config.padding*2,height:r+this.config.padding}}},w(_T,"RailroadRenderer"),_T),Vye=w((t,e,r)=>{Fs(t,e.height,e.width,r),t.attr("viewBox",`0 0 ${e.width} ${e.height}`)},"configureRailroadSvgSize"),Dxr=w((t,e,r)=>{de.debug(`[Railroad] Rendering diagram
+`+t);try{const n=Xo(e);n.attr("class","railroad-diagram");const i=vr().railroad,s=(i==null?void 0:i.useMaxWidth)??!0,a=Qi.getRules();if(de.debug(`[Railroad] Rendering ${a.length} rules`),a.length===0){de.warn("[Railroad] No rules to render"),Vye(n,{height:100,width:200},s);return}const l=new Nxr(n,aie()).renderDiagram(a);Vye(n,l,s),de.debug("[Railroad] Render complete")}catch(n){throw de.error("[Railroad] Render error:",n),n}},"draw"),BP={draw:Dxr},Oxr=nne().Railroad.parser.LangiumParser,q2=w(t=>{switch(t.$type){case"RailroadTerminalExpr":return{type:"terminal",value:t.value};case"RailroadNonTerminalExpr":return{type:"nonterminal",name:t.name};case"RailroadSpecialExpr":return{type:"special",text:t.text};case"RailroadSequenceExpr":{const e=t.elements.map(q2);return e.length===1?e[0]:{type:"sequence",elements:e}}case"RailroadChoiceExpr":{const e=t.alternatives.map(q2);return e.length===1?e[0]:{type:"choice",alternatives:e}}case"RailroadOptionalExpr":return{type:"optional",element:q2(t.element)};case"RailroadOneOrMoreExpr":return{type:"repetition",element:q2(t.element),min:1,max:1/0};case"RailroadZeroOrMoreExpr":return{type:"repetition",element:q2(t.element),min:0,max:1/0};default:throw new Error(`Unsupported railroad expression: ${t.$type}`)}},"transformExpression"),Pxr=w(t=>({name:t.name,definition:q2(t.definition)}),"transformRule"),$xr=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(Pxr(e)))},"populateDb"),Bxr={parse:w(t=>{Qi.clear(),de.debug("[Railroad Parser] Starting Langium parse");const e=Oxr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[Railroad Parser] Parsed rules:",r.rules.length),$xr(r),de.debug("[Railroad Parser] Parse complete")},"parse"),parser:{yy:Qi}},Fxr={parser:Bxr,db:Qi,renderer:BP,styles:$P};const zxr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Fxr},Symbol.toStringTag,{value:"Module"}));var Vxr=ine().RailroadEbnf.parser.LangiumParser,XR=w(t=>{const e=t.alternatives.map(Hxr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformChoice"),Hxr=w(t=>{const e=t.elements.map(Wxr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),hje=w(t=>{switch(t.$type){case"EbnfTerminal":return{type:"terminal",value:t.value};case"EbnfNonTerminal":return{type:"nonterminal",name:t.name};case"EbnfSpecial":return{type:"special",text:t.text};case"EbnfGroup":return XR(t.element);case"EbnfOptional":return{type:"optional",element:XR(t.element)};case"EbnfRepetition":return{type:"repetition",element:XR(t.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${t.$type}`)}},"transformPrimary"),Gxr=w((t,e)=>{switch(e.$type){case"EbnfOptionalPostfix":return{type:"optional",element:t};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:t,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:t,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[t,{type:"terminal",value:"-"},hje(e.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${e.$type}`)}},"transformPostfix"),Wxr=w(t=>t.postfixes.reduce((e,r)=>Gxr(e,r),hje(t.base)),"transformTerm"),qxr=w(t=>({name:t.name,definition:XR(t.definition)}),"transformRule"),Uxr=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(qxr(e)))},"populateDb"),Yxr={parse:w(t=>{Qi.clear(),de.debug("[EBNF Parser] Starting Langium parse");const e=Vxr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[EBNF Parser] Parsed rules:",r.rules.length),Uxr(r),de.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:Qi}},jxr={parser:Yxr,db:Qi,renderer:BP,styles:$P};const Xxr=Object.freeze(Object.defineProperty({__proto__:null,diagram:jxr},Symbol.toStringTag,{value:"Module"}));var Kxr=sne().RailroadAbnf.parser.LangiumParser,jj=w(t=>{const e=t.alternatives.map(Zxr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformAlternation"),Zxr=w(t=>{const e=t.elements.map(Jxr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformConcatenation"),Qxr=w(t=>{if(t.includes("*")){const[r,n]=t.split("*"),i=r?parseInt(r,10):0,s=n?parseInt(n,10):1/0;return{min:i,max:s}}const e=parseInt(t,10);return{min:e,max:e}},"parseRepeat"),Jxr=w(t=>{const e=e4r(t.primary);if(!t.repeat)return e;const{min:r,max:n}=Qxr(t.repeat);return r===0&&n===1?{type:"optional",element:e}:{type:"repetition",element:e,min:r,max:n}},"transformElement"),e4r=w(t=>{switch(t.$type){case"AbnfStringLiteral":return{type:"terminal",value:t.value};case"AbnfNumVal":return{type:"terminal",value:t.value};case"AbnfRuleName":return{type:"nonterminal",name:t.name};case"AbnfGroup":return jj(t.element);case"AbnfOptionalGroup":return{type:"optional",element:jj(t.element)};default:throw new Error(`Unsupported ABNF primary node: ${t.$type}`)}},"transformPrimary"),t4r=w(t=>({name:t.name,definition:jj(t.definition)}),"transformRule"),r4r=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(t4r(e)))},"populateDb"),n4r={parse:w(t=>{Qi.clear(),de.debug("[ABNF Parser] Starting Langium parse");const e=Kxr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[ABNF Parser] Parsed rules:",r.rules.length),r4r(r),de.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:Qi}},i4r={parser:n4r,db:Qi,renderer:BP,styles:$P};const s4r=Object.freeze(Object.defineProperty({__proto__:null,diagram:i4r},Symbol.toStringTag,{value:"Module"}));var a4r=ane().RailroadPeg.parser.LangiumParser,dje=w(t=>{const e=t.alternatives.map(o4r);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformOrderedChoice"),o4r=w(t=>{const e=t.elements.map(l4r);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),l4r=w(t=>{const e=c4r(t.suffix);return t.operator?{type:"special",text:t.operator==="&"?`&${Hye(e)}`:`!${Hye(e)}`}:e},"transformPrefix"),Hye=w(t=>{switch(t.type){case"terminal":return`"${t.value}"`;case"nonterminal":return t.name;case"special":return t.text;default:return"(...)"}},"nodeToLabel"),c4r=w(t=>{const e=u4r(t.primary);if(!t.operator)return e;switch(t.operator){case"?":return{type:"optional",element:e};case"*":return{type:"repetition",element:e,min:0,max:1/0};case"+":return{type:"repetition",element:e,min:1,max:1/0};default:throw new Error(`Unsupported PEG suffix operator: ${t.operator}`)}},"transformSuffix"),u4r=w(t=>{switch(t.$type){case"PegLiteral":return{type:"terminal",value:t.value};case"PegIdentifier":return{type:"nonterminal",name:t.name};case"PegGroup":return dje(t.element);case"PegAny":return{type:"special",text:t.dot};default:throw new Error(`Unsupported PEG primary node: ${t.$type}`)}},"transformPrimary"),h4r=w(t=>({name:t.name,definition:dje(t.definition)}),"transformRule"),d4r=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(h4r(e)))},"populateDb"),f4r={parse:w(t=>{Qi.clear(),de.debug("[PEG Parser] Starting Langium parse");const e=a4r.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[PEG Parser] Parsed rules:",r.rules.length),d4r(r),de.debug("[PEG Parser] Parse complete")},"parse"),parser:{yy:Qi}},p4r={parser:f4r,db:Qi,renderer:BP,styles:$P};const g4r=Object.freeze(Object.defineProperty({__proto__:null,diagram:p4r},Symbol.toStringTag,{value:"Module"}));var m4r=1;function oie(){if(!(typeof globalThis>"u"))return globalThis}w(oie,"getCaptureGlobal");function fje(){var t;return!!((t=oie())!=null&&t.mermaidCaptureSizes)}w(fje,"shouldCaptureSizes");function pje(){return typeof location>"u"?"browser-dev":`${location.pathname}${location.search}`}w(pje,"capturedFromLocation");function gje(t,e){const r=oie();if(!r)return;const n=e.node(),i=(n&&"ownerSVGElement"in n?n.ownerSVGElement:null)??n,s=(i==null?void 0:i.id)??"(unknown)";r.mermaidCapturedSizes??(r.mermaidCapturedSizes=[]);const a={svgId:s,sizes:t};r.mermaidCapturedSizes.push(a),r.mermaidLastCapturedSizes=a}w(gje,"emitCapturedSizes");function mje(t,e){const r=[];for(const n of e.nodes)n.isGroup||r.push({id:n.id,width:n.width??0,height:n.height??0});r.length!==0&&gje({metadata:{captureVersion:m4r,capturedAt:new Date().toISOString(),capturedFrom:pje()},nodes:r},t)}w(mje,"captureNodeSizes");const y4r=Object.freeze(Object.defineProperty({__proto__:null,captureNodeSizes:mje,shouldCaptureSizes:fje},Symbol.toStringTag,{value:"Module"})),v4r=Object.freeze(Object.defineProperty({__proto__:null,InfoModule:_Ge,createInfoServices:LGe},Symbol.toStringTag,{value:"Module"})),b4r=Object.freeze(Object.defineProperty({__proto__:null,PacketModule:IGe,createPacketServices:NGe},Symbol.toStringTag,{value:"Module"})),x4r=Object.freeze(Object.defineProperty({__proto__:null,PieModule:DGe,createPieServices:OGe},Symbol.toStringTag,{value:"Module"})),w4r=Object.freeze(Object.defineProperty({__proto__:null,TreeViewModule:PGe,createTreeViewServices:$Ge},Symbol.toStringTag,{value:"Module"})),T4r=Object.freeze(Object.defineProperty({__proto__:null,ArchitectureModule:BGe,createArchitectureServices:FGe},Symbol.toStringTag,{value:"Module"})),C4r=Object.freeze(Object.defineProperty({__proto__:null,GitGraphModule:MGe,createGitGraphServices:RGe},Symbol.toStringTag,{value:"Module"})),k4r=Object.freeze(Object.defineProperty({__proto__:null,EventModelingModule:VGe,createEventModelingServices:HGe},Symbol.toStringTag,{value:"Module"})),S4r=Object.freeze(Object.defineProperty({__proto__:null,RadarModule:gGe,createRadarServices:mGe},Symbol.toStringTag,{value:"Module"})),E4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadModule:yGe,createRailroadServices:nne},Symbol.toStringTag,{value:"Module"})),A4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadEbnfModule:vGe,createRailroadEbnfServices:ine},Symbol.toStringTag,{value:"Module"})),M4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadAbnfModule:bGe,createRailroadAbnfServices:sne},Symbol.toStringTag,{value:"Module"})),R4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadPegModule:xGe,createRailroadPegServices:ane},Symbol.toStringTag,{value:"Module"})),_4r=Object.freeze(Object.defineProperty({__proto__:null,TreemapModule:TGe,createTreemapServices:CGe},Symbol.toStringTag,{value:"Module"})),L4r=Object.freeze(Object.defineProperty({__proto__:null,WardleyModule:kGe,createWardleyServices:SGe},Symbol.toStringTag,{value:"Module"})),I4r=Object.freeze(Object.defineProperty({__proto__:null,CynefinModule:EGe,createCynefinServices:AGe},Symbol.toStringTag,{value:"Module"}));
diff --git a/apps/ios/EditorBundle/index.html b/apps/ios/EditorBundle/index.html
index 5338da09..a8905476 100644
--- a/apps/ios/EditorBundle/index.html
+++ b/apps/ios/EditorBundle/index.html
@@ -7,14 +7,14 @@
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
EdgeEver Editor
-
+`},"getStyles"),RT,nu=(RT=class{constructor(){this.d=""}moveTo(e,r){return this.d+=`M ${e} ${r} `,this}lineTo(e,r){return this.d+=`L ${e} ${r} `,this}horizontalTo(e){return this.d+=`H ${e} `,this}verticalTo(e){return this.d+=`V ${e} `,this}arcTo(e,r,n,i,s,a,o){return this.d+=`A ${e} ${r} ${n} ${i?1:0} ${s?1:0} ${a} ${o} `,this}build(){return this.d.trim()}},w(RT,"PathBuilder"),RT),_T,Nxr=(_T=class{constructor(e,r=aie()){this.textCache=new Map,this.svg=e,this.config=r}measureText(e){if(this.textCache.has(e))return this.textCache.get(e);const r=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(e),n=r.node().getBBox(),i={width:n.width,height:n.height};return r.remove(),this.textCache.set(e,i),i}renderTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-terminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s).attr("rx",10).attr("ry",10),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderNonTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-nonterminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderSequence(e,r){const n=r.map(u=>this.renderExpression(e,u));let i=0,s=0,a=0;for(const u of n)i+=u.dimensions.width,s=Math.max(s,u.dimensions.up),a=Math.max(a,u.dimensions.down);i+=(n.length-1)*this.config.horizontalSeparation;const o=e.append("g").attr("class","railroad-sequence");let l=0;for(let u=0;uthis.renderExpression(e,f));let i=0,s=0;for(const f of n)i=Math.max(i,f.dimensions.width),s+=f.dimensions.height;s+=(n.length-1)*this.config.verticalSeparation;const a=this.config.arcRadius,o=a*4,l=i+o,u=e.append("g").attr("class","railroad-choice");let h=0;const d=s/2;for(const f of n){const p=h,g=p+f.dimensions.up,m=a*2+(i-f.dimensions.width)/2;u.node().appendChild(f.element).setAttribute("transform",`translate(${m}, ${p})`);const v=new nu,x=g>d;g===d?v.moveTo(0,d).lineTo(m,g):v.moveTo(0,d).arcTo(a,a,0,!1,x,a,d+(x?a:-a)).lineTo(a,g-(x?a:-a)).arcTo(a,a,0,!1,!x,a*2,g).lineTo(m,g),u.append("path").attr("class","railroad-line").attr("d",v.build());const b=new nu,T=m+f.dimensions.width,C=l-a*2;g===d?b.moveTo(T,g).lineTo(l,d):b.moveTo(T,g).lineTo(C,g).arcTo(a,a,0,!1,!x,l-a,g+(x?-a:a)).lineTo(l-a,d+(x?a:-a)).arcTo(a,a,0,!1,x,l,d),u.append("path").attr("class","railroad-line").attr("d",b.build()),h+=f.dimensions.height+this.config.verticalSeparation}return{element:u.node(),dimensions:{width:l,height:s,up:d,down:s-d}}}renderOptional(e,r){const n=this.renderExpression(e,r),i=this.config.arcRadius,s=i*2,a=n.dimensions.width+i*4,o=n.dimensions.height+s,l=e.append("g").attr("class","railroad-optional"),u=i*2,h=s;l.node().appendChild(n.element).setAttribute("transform",`translate(${u}, ${h})`);const f=h+n.dimensions.up,p=new nu().moveTo(0,f).lineTo(i*2,f);l.append("path").attr("class","railroad-line").attr("d",p.build());const g=new nu().moveTo(u+n.dimensions.width,f).lineTo(a,f);l.append("path").attr("class","railroad-line").attr("d",g.build());const m=new nu().moveTo(0,f).arcTo(i,i,0,!1,!1,i,f-i).lineTo(i,i).arcTo(i,i,0,!1,!0,i*2,0).lineTo(a-i*2,0).arcTo(i,i,0,!1,!0,a-i,i).lineTo(a-i,f-i).arcTo(i,i,0,!1,!1,a,f);return l.append("path").attr("class","railroad-line").attr("d",m.build()),{element:l.node(),dimensions:{width:a,height:o,up:f,down:o-f}}}renderRepetition(e,r,n){const i=this.renderExpression(e,r),s=this.config.arcRadius,a=s*2,o=i.dimensions.width+s*4,l=n===0,u=i.dimensions.height+a+(l?a:0),h=e.append("g").attr("class","railroad-repetition"),d=s*2,f=l?a:0;h.node().appendChild(i.element).setAttribute("transform",`translate(${d}, ${f})`);const g=f+i.dimensions.up;h.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(0,g).lineTo(s*2,g).build()),h.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(d+i.dimensions.width,g).lineTo(o,g).build());const m=f+i.dimensions.height+s,y=new nu().moveTo(d+i.dimensions.width,g).arcTo(s,s,0,!1,!0,d+i.dimensions.width+s,g+s).lineTo(d+i.dimensions.width+s,m).arcTo(s,s,0,!1,!0,d+i.dimensions.width,m+s).lineTo(s*2,m+s).arcTo(s,s,0,!1,!0,s,m).lineTo(s,g+s).arcTo(s,s,0,!1,!0,s*2,g);if(h.append("path").attr("class","railroad-line").attr("d",y.build()),l){const v=new nu().moveTo(0,g).arcTo(s,s,0,!1,!1,s,g-s).lineTo(s,s).arcTo(s,s,0,!1,!0,s*2,0).lineTo(o-s*2,0).arcTo(s,s,0,!1,!0,o-s,s).lineTo(o-s,g-s).arcTo(s,s,0,!1,!1,o,g);h.append("path").attr("class","railroad-line").attr("d",v.build())}return{element:h.node(),dimensions:{width:o,height:u,up:g,down:u-g}}}renderSpecial(e,r){const n=this.measureText("? "+r+" ?"),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-special");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text("? "+r+" ?"),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderExpression(e,r){switch(r.type){case"terminal":return this.renderTerminal(e,r.value);case"nonterminal":return this.renderNonTerminal(e,r.name);case"sequence":return this.renderSequence(e,r.elements);case"choice":return this.renderChoice(e,r.alternatives);case"optional":return this.renderOptional(e,r.element);case"repetition":return this.renderRepetition(e,r.element,r.min);case"special":return this.renderSpecial(e,r.text);default:throw new Error(`Unknown node type: ${r.type}`)}}renderRule(e,r){const n=this.svg.append("g").attr("class","railroad-rule").attr("transform",`translate(0, ${r})`),i=e.name+" =",s=this.measureText(i).width+20,a=s+20,o=n.append("g"),l=this.renderExpression(o,e.definition),u=Math.max(20,l.dimensions.up),h=u-l.dimensions.up;return o.attr("transform",`translate(${a}, ${h})`),n.append("g").attr("class","railroad-rule-name-group").append("text").attr("class","railroad-rule-name").attr("x",0).attr("y",u).text(i),n.append("g").attr("class","railroad-start").append("circle").attr("cx",s).attr("cy",u).attr("r",this.config.markerRadius),n.append("g").attr("class","railroad-end").append("circle").attr("cx",a+l.dimensions.width+10).attr("cy",u).attr("r",this.config.markerRadius),n.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(s+this.config.markerRadius,u).lineTo(a,u).build()),n.append("path").attr("class","railroad-line").attr("d",new nu().moveTo(a+l.dimensions.width,u).lineTo(a+l.dimensions.width+10-this.config.markerRadius,u).build()),{height:Math.max(40,h+l.dimensions.height+this.config.padding*2),width:a+l.dimensions.width+10+this.config.markerRadius}}renderDiagram(e){let r=this.config.padding,n=0;for(const i of e){const s=this.renderRule(i,r);r+=s.height+this.config.verticalSeparation,n=Math.max(n,s.width)}return{width:n+this.config.padding*2,height:r+this.config.padding}}},w(_T,"RailroadRenderer"),_T),Vye=w((t,e,r)=>{Fs(t,e.height,e.width,r),t.attr("viewBox",`0 0 ${e.width} ${e.height}`)},"configureRailroadSvgSize"),Dxr=w((t,e,r)=>{de.debug(`[Railroad] Rendering diagram
+`+t);try{const n=Xo(e);n.attr("class","railroad-diagram");const i=vr().railroad,s=(i==null?void 0:i.useMaxWidth)??!0,a=Qi.getRules();if(de.debug(`[Railroad] Rendering ${a.length} rules`),a.length===0){de.warn("[Railroad] No rules to render"),Vye(n,{height:100,width:200},s);return}const l=new Nxr(n,aie()).renderDiagram(a);Vye(n,l,s),de.debug("[Railroad] Render complete")}catch(n){throw de.error("[Railroad] Render error:",n),n}},"draw"),BP={draw:Dxr},Oxr=nne().Railroad.parser.LangiumParser,q2=w(t=>{switch(t.$type){case"RailroadTerminalExpr":return{type:"terminal",value:t.value};case"RailroadNonTerminalExpr":return{type:"nonterminal",name:t.name};case"RailroadSpecialExpr":return{type:"special",text:t.text};case"RailroadSequenceExpr":{const e=t.elements.map(q2);return e.length===1?e[0]:{type:"sequence",elements:e}}case"RailroadChoiceExpr":{const e=t.alternatives.map(q2);return e.length===1?e[0]:{type:"choice",alternatives:e}}case"RailroadOptionalExpr":return{type:"optional",element:q2(t.element)};case"RailroadOneOrMoreExpr":return{type:"repetition",element:q2(t.element),min:1,max:1/0};case"RailroadZeroOrMoreExpr":return{type:"repetition",element:q2(t.element),min:0,max:1/0};default:throw new Error(`Unsupported railroad expression: ${t.$type}`)}},"transformExpression"),Pxr=w(t=>({name:t.name,definition:q2(t.definition)}),"transformRule"),$xr=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(Pxr(e)))},"populateDb"),Bxr={parse:w(t=>{Qi.clear(),de.debug("[Railroad Parser] Starting Langium parse");const e=Oxr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[Railroad Parser] Parsed rules:",r.rules.length),$xr(r),de.debug("[Railroad Parser] Parse complete")},"parse"),parser:{yy:Qi}},Fxr={parser:Bxr,db:Qi,renderer:BP,styles:$P};const zxr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Fxr},Symbol.toStringTag,{value:"Module"}));var Vxr=ine().RailroadEbnf.parser.LangiumParser,XR=w(t=>{const e=t.alternatives.map(Hxr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformChoice"),Hxr=w(t=>{const e=t.elements.map(Wxr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),hje=w(t=>{switch(t.$type){case"EbnfTerminal":return{type:"terminal",value:t.value};case"EbnfNonTerminal":return{type:"nonterminal",name:t.name};case"EbnfSpecial":return{type:"special",text:t.text};case"EbnfGroup":return XR(t.element);case"EbnfOptional":return{type:"optional",element:XR(t.element)};case"EbnfRepetition":return{type:"repetition",element:XR(t.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${t.$type}`)}},"transformPrimary"),Gxr=w((t,e)=>{switch(e.$type){case"EbnfOptionalPostfix":return{type:"optional",element:t};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:t,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:t,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[t,{type:"terminal",value:"-"},hje(e.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${e.$type}`)}},"transformPostfix"),Wxr=w(t=>t.postfixes.reduce((e,r)=>Gxr(e,r),hje(t.base)),"transformTerm"),qxr=w(t=>({name:t.name,definition:XR(t.definition)}),"transformRule"),Uxr=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(qxr(e)))},"populateDb"),Yxr={parse:w(t=>{Qi.clear(),de.debug("[EBNF Parser] Starting Langium parse");const e=Vxr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[EBNF Parser] Parsed rules:",r.rules.length),Uxr(r),de.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:Qi}},jxr={parser:Yxr,db:Qi,renderer:BP,styles:$P};const Xxr=Object.freeze(Object.defineProperty({__proto__:null,diagram:jxr},Symbol.toStringTag,{value:"Module"}));var Kxr=sne().RailroadAbnf.parser.LangiumParser,jj=w(t=>{const e=t.alternatives.map(Zxr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformAlternation"),Zxr=w(t=>{const e=t.elements.map(Jxr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformConcatenation"),Qxr=w(t=>{if(t.includes("*")){const[r,n]=t.split("*"),i=r?parseInt(r,10):0,s=n?parseInt(n,10):1/0;return{min:i,max:s}}const e=parseInt(t,10);return{min:e,max:e}},"parseRepeat"),Jxr=w(t=>{const e=e4r(t.primary);if(!t.repeat)return e;const{min:r,max:n}=Qxr(t.repeat);return r===0&&n===1?{type:"optional",element:e}:{type:"repetition",element:e,min:r,max:n}},"transformElement"),e4r=w(t=>{switch(t.$type){case"AbnfStringLiteral":return{type:"terminal",value:t.value};case"AbnfNumVal":return{type:"terminal",value:t.value};case"AbnfRuleName":return{type:"nonterminal",name:t.name};case"AbnfGroup":return jj(t.element);case"AbnfOptionalGroup":return{type:"optional",element:jj(t.element)};default:throw new Error(`Unsupported ABNF primary node: ${t.$type}`)}},"transformPrimary"),t4r=w(t=>({name:t.name,definition:jj(t.definition)}),"transformRule"),r4r=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(t4r(e)))},"populateDb"),n4r={parse:w(t=>{Qi.clear(),de.debug("[ABNF Parser] Starting Langium parse");const e=Kxr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[ABNF Parser] Parsed rules:",r.rules.length),r4r(r),de.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:Qi}},i4r={parser:n4r,db:Qi,renderer:BP,styles:$P};const s4r=Object.freeze(Object.defineProperty({__proto__:null,diagram:i4r},Symbol.toStringTag,{value:"Module"}));var a4r=ane().RailroadPeg.parser.LangiumParser,dje=w(t=>{const e=t.alternatives.map(o4r);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformOrderedChoice"),o4r=w(t=>{const e=t.elements.map(l4r);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),l4r=w(t=>{const e=c4r(t.suffix);return t.operator?{type:"special",text:t.operator==="&"?`&${Hye(e)}`:`!${Hye(e)}`}:e},"transformPrefix"),Hye=w(t=>{switch(t.type){case"terminal":return`"${t.value}"`;case"nonterminal":return t.name;case"special":return t.text;default:return"(...)"}},"nodeToLabel"),c4r=w(t=>{const e=u4r(t.primary);if(!t.operator)return e;switch(t.operator){case"?":return{type:"optional",element:e};case"*":return{type:"repetition",element:e,min:0,max:1/0};case"+":return{type:"repetition",element:e,min:1,max:1/0};default:throw new Error(`Unsupported PEG suffix operator: ${t.operator}`)}},"transformSuffix"),u4r=w(t=>{switch(t.$type){case"PegLiteral":return{type:"terminal",value:t.value};case"PegIdentifier":return{type:"nonterminal",name:t.name};case"PegGroup":return dje(t.element);case"PegAny":return{type:"special",text:t.dot};default:throw new Error(`Unsupported PEG primary node: ${t.$type}`)}},"transformPrimary"),h4r=w(t=>({name:t.name,definition:dje(t.definition)}),"transformRule"),d4r=w(t=>{Fl(t,Qi),t.title&&Qi.setTitle(t.title),t.rules.map(e=>Qi.addRule(h4r(e)))},"populateDb"),f4r={parse:w(t=>{Qi.clear(),de.debug("[PEG Parser] Starting Langium parse");const e=a4r.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new j7(e);const r=e.value;de.debug("[PEG Parser] Parsed rules:",r.rules.length),d4r(r),de.debug("[PEG Parser] Parse complete")},"parse"),parser:{yy:Qi}},p4r={parser:f4r,db:Qi,renderer:BP,styles:$P};const g4r=Object.freeze(Object.defineProperty({__proto__:null,diagram:p4r},Symbol.toStringTag,{value:"Module"}));var m4r=1;function oie(){if(!(typeof globalThis>"u"))return globalThis}w(oie,"getCaptureGlobal");function fje(){var t;return!!((t=oie())!=null&&t.mermaidCaptureSizes)}w(fje,"shouldCaptureSizes");function pje(){return typeof location>"u"?"browser-dev":`${location.pathname}${location.search}`}w(pje,"capturedFromLocation");function gje(t,e){const r=oie();if(!r)return;const n=e.node(),i=(n&&"ownerSVGElement"in n?n.ownerSVGElement:null)??n,s=(i==null?void 0:i.id)??"(unknown)";r.mermaidCapturedSizes??(r.mermaidCapturedSizes=[]);const a={svgId:s,sizes:t};r.mermaidCapturedSizes.push(a),r.mermaidLastCapturedSizes=a}w(gje,"emitCapturedSizes");function mje(t,e){const r=[];for(const n of e.nodes)n.isGroup||r.push({id:n.id,width:n.width??0,height:n.height??0});r.length!==0&&gje({metadata:{captureVersion:m4r,capturedAt:new Date().toISOString(),capturedFrom:pje()},nodes:r},t)}w(mje,"captureNodeSizes");const y4r=Object.freeze(Object.defineProperty({__proto__:null,captureNodeSizes:mje,shouldCaptureSizes:fje},Symbol.toStringTag,{value:"Module"})),v4r=Object.freeze(Object.defineProperty({__proto__:null,InfoModule:_Ge,createInfoServices:LGe},Symbol.toStringTag,{value:"Module"})),b4r=Object.freeze(Object.defineProperty({__proto__:null,PacketModule:IGe,createPacketServices:NGe},Symbol.toStringTag,{value:"Module"})),x4r=Object.freeze(Object.defineProperty({__proto__:null,PieModule:DGe,createPieServices:OGe},Symbol.toStringTag,{value:"Module"})),w4r=Object.freeze(Object.defineProperty({__proto__:null,TreeViewModule:PGe,createTreeViewServices:$Ge},Symbol.toStringTag,{value:"Module"})),T4r=Object.freeze(Object.defineProperty({__proto__:null,ArchitectureModule:BGe,createArchitectureServices:FGe},Symbol.toStringTag,{value:"Module"})),C4r=Object.freeze(Object.defineProperty({__proto__:null,GitGraphModule:MGe,createGitGraphServices:RGe},Symbol.toStringTag,{value:"Module"})),k4r=Object.freeze(Object.defineProperty({__proto__:null,EventModelingModule:VGe,createEventModelingServices:HGe},Symbol.toStringTag,{value:"Module"})),S4r=Object.freeze(Object.defineProperty({__proto__:null,RadarModule:gGe,createRadarServices:mGe},Symbol.toStringTag,{value:"Module"})),E4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadModule:yGe,createRailroadServices:nne},Symbol.toStringTag,{value:"Module"})),A4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadEbnfModule:vGe,createRailroadEbnfServices:ine},Symbol.toStringTag,{value:"Module"})),M4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadAbnfModule:bGe,createRailroadAbnfServices:sne},Symbol.toStringTag,{value:"Module"})),R4r=Object.freeze(Object.defineProperty({__proto__:null,RailroadPegModule:xGe,createRailroadPegServices:ane},Symbol.toStringTag,{value:"Module"})),_4r=Object.freeze(Object.defineProperty({__proto__:null,TreemapModule:TGe,createTreemapServices:CGe},Symbol.toStringTag,{value:"Module"})),L4r=Object.freeze(Object.defineProperty({__proto__:null,WardleyModule:kGe,createWardleyServices:SGe},Symbol.toStringTag,{value:"Module"})),I4r=Object.freeze(Object.defineProperty({__proto__:null,CynefinModule:EGe,createCynefinServices:AGe},Symbol.toStringTag,{value:"Module"}));
diff --git a/apps/ios/EditorSource/src/main.ts b/apps/ios/EditorSource/src/main.ts
index f310ec09..bc2664af 100644
--- a/apps/ios/EditorSource/src/main.ts
+++ b/apps/ios/EditorSource/src/main.ts
@@ -5,7 +5,6 @@ import { Editor } from "@tiptap/core";
import Image from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder";
import CodeBlock from "@tiptap/extension-code-block";
-import { EdgeEverLink } from "@edgeever/shared/editor-link";
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { NodeSelection } from "@tiptap/pm/state";
import mermaid from "mermaid";
@@ -759,7 +758,6 @@ function buildExtensions(placeholder: string) {
table: { table: { resizable: false } },
markdown: true,
}),
- EdgeEverLink,
NativeAttachmentMetadata,
CodeBlock.configure({
languageClassPrefix: "language-",
diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx
index 54d3e152..2ef65d0a 100644
--- a/apps/web/src/components/EditorPane.tsx
+++ b/apps/web/src/components/EditorPane.tsx
@@ -192,6 +192,7 @@ import {
} from "@/lib/editor-external-link";
import { insertAiDraftAtTextCursor } from "@/lib/ai-draft-insertion";
import { createFileBatchQueue, processFileUploadBatch } from "@/lib/file-batch";
+import { resourceToMarkdown } from "@/lib/markdown-resource-paste";
import { MEMO_ID_REMAPPED_EVENT, MEMO_SYNC_ACKNOWLEDGED_EVENT } from "@/lib/sync-events";
import { useStandaloneMobileEditor } from "@/hooks/useStandaloneMobileEditor";
import { statusSettleMotion } from "@/lib/motion";
@@ -886,6 +887,39 @@ const RichEditorPane = ({
}
}, [focusMobileInputTarget, isMobileViewport, memo?.id, mobileDefaultEditMemoId, onMobileDefaultEditConsumed, readOnly]);
+ const uploadEditorResource = useCallback(async (targetMemoId: string, uploadFile: File, isImage: boolean) => {
+ try {
+ const resource = (await repository.uploadMemoResource(targetMemoId, uploadFile)).resource;
+ return { ...resource, url: toDesktopResourceUrl(resource.url) };
+ } catch (error) {
+ if (!isDesktopResourceRuntime()) throw error;
+ const listed = await repository.listResources().catch(() => ({ resources: [] as Array<{ memoId?: string; url: string; filename?: string | null; kind?: string | null; mimeType?: string | null; byteSize?: number }> }));
+ const existing = findMatchingMemoResource(
+ listed.resources.filter((item) => item.memoId === targetMemoId),
+ uploadFile.name,
+ isImage ? "image" : "attachment",
+ );
+ if (existing) {
+ return {
+ kind: isImage ? "image" as const : "attachment" as const,
+ filename: existing.filename || uploadFile.name,
+ mimeType: existing.mimeType || uploadFile.type || null,
+ byteSize: existing.byteSize ?? uploadFile.size,
+ url: toDesktopResourceUrl(existing.url),
+ };
+ }
+ const staged = await stageDesktopResource(targetMemoId, uploadFile);
+ if (!staged) throw error;
+ return {
+ kind: isImage ? "image" as const : "attachment" as const,
+ filename: uploadFile.name,
+ mimeType: uploadFile.type || null,
+ byteSize: uploadFile.size,
+ url: `edgeever-staged://${staged.id}`,
+ };
+ }
+ }, [repository]);
+
const insertResourceFiles = useCallback((files: File[]) => {
const currentMemo = memoRef.current;
const currentEditor = editorRef.current;
@@ -941,44 +975,7 @@ const RichEditorPane = ({
setImageUploadState("uploading");
if (placeholder) updateImageUploadPlaceholder(editorRef.current, placeholder,
t("editor.uploadState.uploading"));
- let resource: {
- kind: "image" | "attachment";
- filename: string | null;
- mimeType: string | null;
- byteSize: number;
- url: string;
- };
- try {
- const uploadedResource = (await repository.uploadMemoResource(targetMemoId, uploadFile)).resource;
- resource = { ...uploadedResource, url: toDesktopResourceUrl(uploadedResource.url) };
- } catch (error) {
- if (!isDesktopResourceRuntime()) throw error;
- const listed = await repository.listResources().catch(() => ({ resources: [] as Array<{ memoId?: string; url: string; filename?: string | null; kind?: string | null; mimeType?: string | null; byteSize?: number }> }));
- const existing = findMatchingMemoResource(
- listed.resources.filter((item) => item.memoId === targetMemoId),
- uploadFile.name,
- isImage ? "image" : "attachment",
- );
- if (existing) {
- resource = {
- kind: isImage ? "image" : "attachment",
- filename: existing.filename || uploadFile.name,
- mimeType: existing.mimeType || uploadFile.type || null,
- byteSize: existing.byteSize ?? uploadFile.size,
- url: toDesktopResourceUrl(existing.url),
- };
- } else {
- const staged = await stageDesktopResource(targetMemoId, uploadFile);
- if (!staged) throw error;
- resource = {
- kind: isImage ? "image" : "attachment",
- filename: uploadFile.name,
- mimeType: uploadFile.type || null,
- byteSize: uploadFile.size,
- url: `edgeever-staged://${staged.id}`,
- };
- }
- }
+ const resource = await uploadEditorResource(targetMemoId, uploadFile, isImage);
if (resource.kind === "image") {
imageReadiness.push(waitForImageSourceReady(resource.url));
}
@@ -1082,7 +1079,7 @@ const RichEditorPane = ({
});
});
return true;
- }, [queryClient, repository, resourceInsertionLimit, t]);
+ }, [queryClient, resourceInsertionLimit, t, uploadEditorResource]);
const pluginEmbedExtension = useMemo(() => createPluginEmbedExtension(pluginHost), [pluginHost]);
const inlineFieldExtension = useMemo(() => createInlineFieldExtension(i18n.language), [i18n.language]);
@@ -1282,6 +1279,32 @@ const RichEditorPane = ({
});
const useMarkdownSourceEditor = !useMobilePlainTextEditor && isMarkdownMode;
+ const uploadMarkdownPasteFiles = useCallback(async (files: File[]) => {
+ const targetMemoId = memoRef.current?.id;
+ if (!targetMemoId || effectiveReadOnly) return "";
+ setImageUploadState("uploading");
+ const results = await processFileUploadBatch(files, async (file) => {
+ const shouldCompress = SUPPORTED_PASTE_IMAGE_TYPES.has(file.type) && imageCompressionEnabledRef.current;
+ if (!shouldCompress) return file;
+ setImageUploadState("compressing");
+ return (await compressImageForUpload(file)).file;
+ }, async (uploadFile, file) => {
+ if (memoRef.current?.id !== targetMemoId) throw new Error("The active note changed during upload");
+ setImageUploadState("uploading");
+ return uploadEditorResource(targetMemoId, uploadFile, SUPPORTED_PASTE_IMAGE_TYPES.has(file.type));
+ });
+ if (results.some((result) => result.status === "fulfilled")) {
+ void queryClient.invalidateQueries({ queryKey: ["resources"] });
+ }
+ const failed = results.some((result) => result.status === "rejected");
+ setImageUploadState(failed ? "error" : "idle");
+ if (failed) window.setTimeout(() => setImageUploadState("idle"), 2200);
+ return results
+ .filter((result) => result.status === "fulfilled")
+ .map((result) => resourceToMarkdown(result.value, result.file.name))
+ .join("\n\n");
+ }, [effectiveReadOnly, queryClient, uploadEditorResource]);
+
useEffect(() => {
const syncPreference = (event?: Event) => {
const detail = event && event.type === AI_SPACE_SHORTCUT_CHANGED_EVENT
@@ -4085,8 +4108,10 @@ const RichEditorPane = ({
}>
HTMLElement | null;
@@ -38,8 +41,10 @@ export interface MarkdownSourceEditorRef {
}
export interface MarkdownSourceEditorProps {
+ memoId: string;
value: string;
onChange: (value: string) => void;
+ onPasteFiles?: (files: File[]) => Promise;
themeName: MarkdownThemeName;
readOnly?: boolean;
placeholder?: string;
@@ -101,8 +106,10 @@ const baseEditorTheme = EditorView.theme({
export const MarkdownSourceEditor = forwardRef(
(
{
+ memoId,
value,
onChange,
+ onPasteFiles,
themeName,
readOnly = false,
placeholder,
@@ -114,6 +121,9 @@ export const MarkdownSourceEditor = forwardRef {
const cmRef = useRef(null);
+ const memoIdRef = useRef(memoId);
+ memoIdRef.current = memoId;
+ const pendingPastesRef = useRef(new Set());
useImperativeHandle(
ref,
@@ -181,9 +191,48 @@ export const MarkdownSourceEditor = forwardRef {
+ if (!update.docChanged) return;
+ for (const pending of pendingPastesRef.current) {
+ pending.from = update.changes.mapPos(pending.from, -1);
+ pending.to = update.changes.mapPos(pending.to, 1);
+ }
+ }),
];
}, []);
+ const handlePasteCapture = useCallback((event: ReactClipboardEvent) => {
+ if (readOnly || !onPasteFiles) return;
+ const files = getResourceFilesFromDataTransfer(event.clipboardData);
+ if (!files.length) return;
+ const view = cmRef.current?.view;
+ if (!view) return;
+ event.preventDefault();
+ event.stopPropagation();
+ const selection = view.state.selection.main;
+ const pending: PendingPaste = {
+ from: selection.from,
+ to: selection.to,
+ originalText: view.state.doc.sliceString(selection.from, selection.to),
+ };
+ const targetMemoId = memoId;
+ pendingPastesRef.current.add(pending);
+ void onPasteFiles(files).then((markdown) => {
+ pendingPastesRef.current.delete(pending);
+ if (!markdown || cmRef.current?.view !== view || memoIdRef.current !== targetMemoId) return;
+ const unchanged = view.state.doc.sliceString(pending.from, pending.to) === pending.originalText;
+ const from = unchanged ? pending.from : pending.to;
+ const shouldMoveCursor = view.hasFocus && view.state.selection.main.from === pending.to
+ && view.state.selection.main.to === pending.to;
+ view.dispatch({
+ changes: { from, to: pending.to, insert: markdown },
+ ...(shouldMoveCursor ? { selection: { anchor: from + markdown.length }, scrollIntoView: true } : {}),
+ });
+ }).catch(() => {
+ pendingPastesRef.current.delete(pending);
+ });
+ }, [memoId, onPasteFiles, readOnly]);
+
const handleKeyDown = useCallback(
(event: React.KeyboardEvent | KeyboardEvent) => {
// Link shortcut: Cmd+K / Ctrl+K
@@ -248,6 +297,7 @@ export const MarkdownSourceEditor = forwardRef
{
+ test("formats uploaded images and attachments without breaking Markdown syntax", () => {
+ expect(resourceToMarkdown({ kind: "image", filename: "a[b].png", url: "/api/v1/resources/res_1/blob" }, "fallback.png"))
+ .toBe("![a\\[b\\].png](/api/v1/resources/res_1/blob)");
+ expect(resourceToMarkdown({ kind: "attachment", filename: null, url: "https://example.com/a(b).pdf" }, "report.pdf"))
+ .toBe("[report.pdf](https://example.com/a%28b%29.pdf)");
+ });
+});
diff --git a/apps/web/src/lib/markdown-resource-paste.ts b/apps/web/src/lib/markdown-resource-paste.ts
new file mode 100644
index 00000000..df85f723
--- /dev/null
+++ b/apps/web/src/lib/markdown-resource-paste.ts
@@ -0,0 +1,9 @@
+const escapeLabel = (value: string) => value
+ .replaceAll("\\", "\\\\")
+ .replace(/[\[\]\r\n]/g, (character) => character === "\r" || character === "\n" ? " " : `\\${character}`);
+
+export const resourceToMarkdown = (resource: { kind: "image" | "attachment"; url: string; filename: string | null }, fallbackName: string) => {
+ const label = escapeLabel(resource.filename || fallbackName || "file");
+ const url = resource.url.replaceAll("(", "%28").replaceAll(")", "%29");
+ return `${resource.kind === "image" ? "!" : ""}[${label}](${url})`;
+};
diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css
index bad43463..718ccbf8 100644
--- a/apps/web/src/styles/globals.css
+++ b/apps/web/src/styles/globals.css
@@ -1189,6 +1189,39 @@ textarea {
border-radius: 2px;
}
+.ProseMirror a.edgeever-empty-external-link,
+.markdown-content a.edgeever-empty-external-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 1.5em;
+ min-height: 1.5em;
+ padding: 0 0.25em;
+ border: 1px solid var(--brand-green-border);
+ border-radius: 0.35em;
+ background: var(--brand-green-soft);
+ color: var(--brand-green-text);
+ font-size: 0.9em;
+ font-weight: 700;
+ line-height: 1;
+ text-decoration: none;
+ vertical-align: baseline;
+}
+
+.ProseMirror a.edgeever-empty-external-link:hover,
+.markdown-content a.edgeever-empty-external-link:hover {
+ border-color: var(--brand-green);
+ background: var(--brand-green-muted);
+ color: var(--brand-green-text);
+ text-decoration: none;
+}
+
+.ProseMirror a.edgeever-empty-external-link:focus-visible,
+.markdown-content a.edgeever-empty-external-link:focus-visible {
+ outline: 2px solid var(--brand-green);
+ outline-offset: 2px;
+}
+
.ProseMirror a.edgeever-note-link,
.ProseMirror a[href^="#memo="] {
display: inline-flex;
diff --git a/packages/shared/src/content.test.mjs b/packages/shared/src/content.test.mjs
index 2263be41..1c9636ce 100644
--- a/packages/shared/src/content.test.mjs
+++ b/packages/shared/src/content.test.mjs
@@ -156,6 +156,23 @@ describe("file attachment Markdown compatibility", () => {
expect(doc.content[0]?.content?.[0]?.type).toBe("text");
});
+ test("preserves an empty external link through Markdown and JSON", () => {
+ const markdown = "Before [](https://example.com/path) and [named](https://example.com).";
+ const doc = markdownToDoc(markdown);
+ expect(doc.content[0]?.content?.[1]).toMatchObject({
+ type: "edgeeverEmptyExternalLink",
+ attrs: { href: "https://example.com/path" },
+ });
+ expect(docToMarkdown(doc)).toBe(markdown);
+ expect(markdownToDoc("`[](https://example.com)`").content[0]?.content?.[0]?.type).toBe("text");
+ });
+
+ test("recovers an empty link from older content JSON", () => {
+ const markdown = "Read [](https://example.com) here";
+ const oldDoc = markdownToDoc("Read here");
+ expect(resolveMemoContentDoc(oldDoc, markdown)).toEqual(markdownToDoc(markdown));
+ });
+
test("keeps video preview expanded unless compact display mode is stored", () => {
expect(resolveFileDisplayMode(undefined)).toBe("inline");
expect(resolveFileDisplayMode("inline")).toBe("inline");
diff --git a/packages/shared/src/content.ts b/packages/shared/src/content.ts
index 4e8f8a6f..a83d28c4 100644
--- a/packages/shared/src/content.ts
+++ b/packages/shared/src/content.ts
@@ -14,6 +14,7 @@ import {
import { projectNativeUnknownContentForMarkdown } from "./mobile-content-compatibility";
import { PluginEmbed, PLUGIN_EMBED_NODE_TYPE } from "./plugin-embed";
import { ImageGallery, IMAGE_GALLERY_NODE_TYPE, normalizeImageGalleries } from "./image-gallery";
+import { EMPTY_EXTERNAL_LINK_NODE_TYPE } from "./empty-external-link";
export { PluginEmbed, PLUGIN_EMBED_NODE_TYPE, pluginEmbedToMarkdown, normalizePluginEmbedAttributes } from "./plugin-embed";
export type { PluginEmbedAttributes } from "./plugin-embed";
@@ -169,6 +170,7 @@ export const resolveMemoContentDoc = (
docContainsNodeType(currentDoc, INLINE_MATH_NODE_TYPE)
|| docContainsNodeType(currentDoc, PDF_ATTACHMENT_NODE_TYPE)
|| docContainsNodeType(currentDoc, FILE_ATTACHMENT_NODE_TYPE)
+ || docContainsNodeType(currentDoc, EMPTY_EXTERNAL_LINK_NODE_TYPE)
|| docContainsNodeType(currentDoc, "details")
) {
return currentDoc;
@@ -185,6 +187,7 @@ export const resolveMemoContentDoc = (
|| docContainsNodeType(markdownDoc, MERGE_DIVIDER_NODE_TYPE)
|| docContainsNodeType(markdownDoc, BLOCK_MATH_NODE_TYPE)
|| docContainsNodeType(markdownDoc, INLINE_MATH_NODE_TYPE)
+ || docContainsNodeType(markdownDoc, EMPTY_EXTERNAL_LINK_NODE_TYPE)
|| docContainsNodeType(markdownDoc, "details")
|| !docToText(currentDoc)
? markdownDoc
diff --git a/packages/shared/src/document-extensions.ts b/packages/shared/src/document-extensions.ts
index 54ae7898..829e910a 100644
--- a/packages/shared/src/document-extensions.ts
+++ b/packages/shared/src/document-extensions.ts
@@ -10,6 +10,8 @@ import { ImageGallery } from "./image-gallery";
import { MergeDivider } from "./merge-divider";
import { PdfAttachment } from "./pdf-attachment";
import { PluginEmbed } from "./plugin-embed";
+import { EmptyExternalLink } from "./empty-external-link";
+import { EdgeEverLink } from "./editor-link";
export type CreateEdgeEverDocumentExtensionsOptions = {
mathematics: AnyExtension[];
@@ -36,7 +38,11 @@ const withOptional = (value: AnyExtension | false | undefined, fallback: AnyExte
export const createEdgeEverDocumentExtensions = (
options: CreateEdgeEverDocumentExtensionsOptions,
): AnyExtension[] => [
- options.starterKit === undefined ? StarterKit : StarterKit.configure(options.starterKit),
+ options.markdown
+ ? StarterKit.configure({ ...options.starterKit, link: false })
+ : options.starterKit === undefined ? StarterKit : StarterKit.configure(options.starterKit),
+ ...(options.markdown ? [EdgeEverLink] : []),
+ EmptyExternalLink,
TaskList,
TaskItem.configure({ nested: true }),
options.table === undefined ? TableKit : TableKit.configure(options.table),
diff --git a/packages/shared/src/editor-link.ts b/packages/shared/src/editor-link.ts
index 25f85d78..496af51b 100644
--- a/packages/shared/src/editor-link.ts
+++ b/packages/shared/src/editor-link.ts
@@ -1,4 +1,5 @@
import Link from "@tiptap/extension-link";
+import { EMPTY_EXTERNAL_LINK_NODE_TYPE } from "./empty-external-link";
/**
* Keep text entered at the end of a link outside that link while preserving
@@ -6,4 +7,16 @@ import Link from "@tiptap/extension-link";
*/
export const EdgeEverLink = Link.extend({
inclusive: false,
+ parseMarkdown(token, helpers) {
+ if (!token.tokens?.length && token.text === "" && typeof token.href === "string" && token.href) {
+ return {
+ type: EMPTY_EXTERNAL_LINK_NODE_TYPE,
+ attrs: { href: token.href, title: token.title || null },
+ };
+ }
+ return helpers.applyMark("link", helpers.parseInline(token.tokens || []), {
+ href: token.href,
+ title: token.title || null,
+ });
+ },
});
diff --git a/packages/shared/src/empty-external-link.ts b/packages/shared/src/empty-external-link.ts
new file mode 100644
index 00000000..ccf70b05
--- /dev/null
+++ b/packages/shared/src/empty-external-link.ts
@@ -0,0 +1,54 @@
+import { mergeAttributes, Node } from "@tiptap/core";
+
+export const EMPTY_EXTERNAL_LINK_NODE_TYPE = "edgeeverEmptyExternalLink" as const;
+
+const safeHref = (value: unknown) => {
+ if (typeof value !== "string") return "";
+ const href = value.trim();
+ return /^(?:https?:\/\/|\/|#)/i.test(href) ? href : "";
+};
+
+/** A visible, round-trippable target for Markdown links with no label. */
+export const EmptyExternalLink = Node.create({
+ name: EMPTY_EXTERNAL_LINK_NODE_TYPE,
+ group: "inline",
+ inline: true,
+ atom: true,
+ selectable: true,
+
+ addAttributes() {
+ return {
+ href: { default: "" },
+ title: { default: null },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: "a[data-edgeever-empty-link]" }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ const href = safeHref(HTMLAttributes.href);
+ return [
+ "a",
+ mergeAttributes(HTMLAttributes, {
+ href: href || undefined,
+ target: "_blank",
+ rel: "noopener noreferrer",
+ "data-edgeever-empty-link": "true",
+ "aria-label": href ? `Open link: ${href}` : "Open link",
+ class: "edgeever-empty-external-link",
+ }),
+ "↗",
+ ];
+ },
+
+ renderMarkdown(node) {
+ const href = safeHref(node.attrs?.href);
+ if (!href) return "";
+ const title = typeof node.attrs?.title === "string" && node.attrs.title
+ ? ` "${node.attrs.title.replaceAll('"', '\\"')}"`
+ : "";
+ return `[](${href}${title})`;
+ },
+});
diff --git a/packages/shared/src/mobile-content-compatibility.test.mjs b/packages/shared/src/mobile-content-compatibility.test.mjs
index 728fc870..0a5938b5 100644
--- a/packages/shared/src/mobile-content-compatibility.test.mjs
+++ b/packages/shared/src/mobile-content-compatibility.test.mjs
@@ -86,6 +86,18 @@ describe("native editor unknown-content compatibility", () => {
.toEqual(galleryDocument);
});
+ test("keeps empty links visible and serializable in native editors", () => {
+ const document = {
+ type: "doc",
+ content: [{
+ type: "paragraph",
+ content: [{ type: "edgeeverEmptyExternalLink", attrs: { href: "https://example.com", title: null } }],
+ }],
+ };
+ expect(prepareNativeEditorContent(document)).toEqual(document);
+ expect(docToMarkdown(document)).toBe("[](https://example.com)");
+ });
+
test("keeps Markdown generation alive by projecting unknown content to text", () => {
const markdown = docToMarkdown(futureDocument);
expect(markdown).toContain("保留主题块正文");
diff --git a/packages/shared/src/mobile-content-compatibility.ts b/packages/shared/src/mobile-content-compatibility.ts
index 3317d516..24b8b157 100644
--- a/packages/shared/src/mobile-content-compatibility.ts
+++ b/packages/shared/src/mobile-content-compatibility.ts
@@ -3,6 +3,7 @@ import type { TiptapDoc, TiptapMark, TiptapNode, TiptapTextNode } from "./conten
import { FILE_ATTACHMENT_NODE_TYPE } from "./file-attachment";
import { PDF_ATTACHMENT_NODE_TYPE } from "./pdf-attachment";
import { PLUGIN_EMBED_NODE_TYPE } from "./plugin-embed";
+import { EMPTY_EXTERNAL_LINK_NODE_TYPE } from "./empty-external-link";
export const UNSUPPORTED_BLOCK_NODE_TYPE = "edgeeverUnsupportedBlock" as const;
export const UNSUPPORTED_INLINE_NODE_TYPE = "edgeeverUnsupportedInline" as const;
@@ -38,6 +39,7 @@ export const NATIVE_EDITOR_NODE_TYPES = new Set([
"tableHeader",
"tableCell",
"edgeeverMergeDivider",
+ EMPTY_EXTERNAL_LINK_NODE_TYPE,
"inlineMath",
"blockMath",
...FALLBACK_NODE_TYPES,