diff --git a/.obsidian/app.json b/.obsidian/app.json index 9e26dfe..d3817f7 100644 --- a/.obsidian/app.json +++ b/.obsidian/app.json @@ -1 +1,4 @@ -{} \ No newline at end of file +{ + "showLineNumber": false, + "readableLineLength": false +} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json index 9e26dfe..e730158 100644 --- a/.obsidian/appearance.json +++ b/.obsidian/appearance.json @@ -1 +1,6 @@ -{} \ No newline at end of file +{ + "baseFontSize": 12, + "translucency": true, + "accentColor": "#95b1ea", + "baseFontSizeAction": true +} \ No newline at end of file diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.obsidian/community-plugins.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json index 0faa60d..0d337c0 100644 --- a/.obsidian/core-plugins.json +++ b/.obsidian/core-plugins.json @@ -14,7 +14,7 @@ "templates": true, "note-composer": true, "command-palette": true, - "slash-command": false, + "slash-command": true, "editor-status": true, "bookmarks": true, "markdown-importer": false, @@ -22,7 +22,7 @@ "random-note": false, "outline": true, "word-count": true, - "slides": false, + "slides": true, "audio-recorder": false, "workspaces": false, "file-recovery": true, diff --git a/.obsidian/page-preview.json b/.obsidian/page-preview.json new file mode 100644 index 0000000..cd43461 --- /dev/null +++ b/.obsidian/page-preview.json @@ -0,0 +1,3 @@ +{ + "preview": true +} \ No newline at end of file diff --git a/.obsidian/plugins/mermaid-popup/main.js b/.obsidian/plugins/mermaid-popup/main.js new file mode 100644 index 0000000..dd9e952 --- /dev/null +++ b/.obsidian/plugins/mermaid-popup/main.js @@ -0,0 +1,3 @@ +"use strict";var obsidian=require("obsidian");class MermaidPopupSettingTab extends obsidian.PluginSettingTab{plugin;constructor(app,plugin){super(app,plugin);this.plugin=plugin}display(){const{containerEl:containerEl}=this;containerEl.empty();const tableContainer=containerEl.createDiv({cls:"setting-table"});const table=tableContainer.createEl("table");const tbody=table.createEl("table");const row_01_popup_sz_and_dg_h_title=tbody.createEl("tr");const row_02_popup_sz_and_dg_h_val=tbody.createEl("tr");const td_01_1_popup_sz_title=row_01_popup_sz_and_dg_h_title.createEl("td");let popup_sz_title=td_01_1_popup_sz_title.createEl("h2",{text:"Popup Size Init"});popup_sz_title.classList.add("config-text");const td_02_1_popup_sz=row_02_popup_sz_and_dg_h_val.createEl("td");new obsidian.Setting(td_02_1_popup_sz).setName("Choose the Popup Size").addDropdown((dropdown=>{let ddPopupSizeInit=this.plugin.settings.kvMapPopupSizeInit;for(const key in ddPopupSizeInit){dropdown.addOption(key,ddPopupSizeInit[key])}dropdown.setValue(this.plugin.settings.PopupSizeInitValue).onChange((async value=>{this.plugin.settings.PopupSizeInitValue=value;await this.plugin.saveSettings()}))}));let td_01_2_dg_h=row_01_popup_sz_and_dg_h_title.createEl("td");let td_02_1_dg_h_title=td_01_2_dg_h.createEl("h2",{text:"Original Target Height"});td_02_1_dg_h_title.classList.add("config-text");const td_02_2_dg_h_val=row_02_popup_sz_and_dg_h_val.createEl("td");td_02_2_dg_h_val.classList.add("ori_diagram_height");let dg_h_val=this.plugin.settings.DiagramHeightVal;let dg_h_min=this.plugin.settings.DiagramHeightMin;let dg_h_max=this.plugin.settings.DiagramHeightMax;let dg_h_step=this.plugin.settings.DiagramHeightStep;let dg_h_val_min=td_02_2_dg_h_val.createEl("p");dg_h_val_min.setText(dg_h_min);let dg_h_val_input=td_02_2_dg_h_val.createEl("input");dg_h_val_input.setAttribute("type","range");dg_h_val_input.setAttribute("min",dg_h_min);dg_h_val_input.setAttribute("max",dg_h_max);dg_h_val_input.setAttribute("step",dg_h_step);dg_h_val_input.setAttribute("value",dg_h_val);let dg_h_val_max=td_02_2_dg_h_val.createEl("p");dg_h_val_max.setText(dg_h_max);let dg_h_val_cur_title=td_02_2_dg_h_val.createEl("p",{text:"current:"});dg_h_val_cur_title.classList.add("ori_diagram_height_cur");let dg_h_val_cur=td_02_2_dg_h_val.createEl("p");dg_h_val_cur.classList.add("ori_diagram_height_val");dg_h_val_cur.setText(dg_h_val);dg_h_val_input.addEventListener("input",(event=>{const value=dg_h_val_input.value;dg_h_val_cur.setText(value+"");this.plugin.settings.DiagramHeightVal=value;this.plugin.saveSettings()}));this.setInfo(td_02_2_dg_h_val,"Click for tips on Original Target Height Setting.","Original Target Height Setting","Under proportional scaling, "+"adapt to the width of editor, "+"and then if the height is still greater than the value of 'Original Target Height',"+"it will adapt again. ");const row_1=tbody.createEl("tr");const row_2=tbody.createEl("tr");const td_1_1=row_1.createEl("td");let titleZoomRatio=td_1_1.createEl("h2",{text:"Zoom Ratio"});titleZoomRatio.classList.add("config-text");const td_2_1=row_2.createEl("td");new obsidian.Setting(td_2_1).setName("Choose the ratio for zooming in or out").addDropdown((dropdown=>{let ddZoomRatio=this.plugin.settings.kvMapZoomRatio;for(const key in ddZoomRatio){dropdown.addOption(key,ddZoomRatio[key])}dropdown.setValue(this.plugin.settings.ZoomRatioValue).onChange((async value=>{this.plugin.settings.ZoomRatioValue=value;await this.plugin.saveSettings()}))}));const td_1_2=row_1.createEl("td");let titleMoveStep=td_1_2.createEl("h2",{text:"Move Step"});titleMoveStep.classList.add("config-text");const td_2_2=row_2.createEl("td");new obsidian.Setting(td_2_2).setName("Choose the step for moving").addDropdown((dropdown=>{let ddZoomRatio=this.plugin.settings.kvMapMoveStep;for(const key in ddZoomRatio){dropdown.addOption(key,ddZoomRatio[key])}dropdown.setValue(this.plugin.settings.MoveStepValue).onChange((async value=>{this.plugin.settings.MoveStepValue=value;await this.plugin.saveSettings()}))}));const row=tbody.createEl("tr");const td_title_a=row.createEl("td");let titlePpBgA=td_title_a.createEl("h2",{text:"Popup Background Alpha Value"});titlePpBgA.classList.add("config-text");new obsidian.Setting(td_title_a).setName("Choose the alpha value").addDropdown((dropdown=>{let bgAlphaStep=this.plugin.settings.bgAlphaStep;for(const key in bgAlphaStep){dropdown.addOption(key,bgAlphaStep[key])}dropdown.setValue(this.plugin.settings.bgAlpha).onChange((async value=>{this.plugin.settings.bgAlpha=value;await this.plugin.saveSettings()}))}));this.addClass(td_title_a,"setting-item","setting-item-on-top-line");const td_title_blur=row.createEl("td");let titleBlur=td_title_blur.createEl("h2",{text:"Popup Background Blur"});titleBlur.classList.add("config-text");new obsidian.Setting(td_title_blur).setName("Enable blur").addToggle((toggle=>toggle.setValue(this.plugin.settings.bgIsBlur=="1"?true:false).onChange((async value=>{this.plugin.settings.bgIsBlur=value?"1":"0";await this.plugin.saveSettings()}))));this.addClass(td_title_blur,"setting-item","setting-item-on-top-line");let title_btn_pos=containerEl.createEl("h2",{text:"Open Popup Button Relative Position Init"});title_btn_pos.classList.add("config-text");const kvRow_open_btn=containerEl.createDiv({cls:"kv-row open_btn_pos"});this.slideInput(kvRow_open_btn,"x:",this.plugin.settings.open_btn_pos_x,(val=>{this.plugin.settings.open_btn_pos_x=val}),"1","500","px");this.setInfo(kvRow_open_btn,"Click for tips on Open Popup Button Relative Position Init Setting.","Open Popup Button Relative Position Init Setting","x represents the pixels to the right edge of the target container.");const kvRow_open_btn_y=containerEl.createDiv({cls:"kv-row open_btn_pos"});this.slideInput(kvRow_open_btn_y,"y:",this.plugin.settings.open_btn_pos_y,(val=>{this.plugin.settings.open_btn_pos_y=val}),"1","500","px");this.setInfo(kvRow_open_btn_y,"Click for tips on Open Popup Button Relative Position Init Setting.","Open Popup Button Relative Position Init Setting","y represents the pixels to the top edge of the target container.");let title=containerEl.createEl("h2",{text:"Add New Target"});title.classList.add("config-text");containerEl.createEl("p",{text:"This plugin supports customing target from mermaid, plantuml, graphviz, image and so on. "});const kvRow=containerEl.createDiv({cls:"kv-row"});const keyInput=kvRow.createEl("input",{type:"text",placeholder:"Input Key please"});const valueInput=kvRow.createEl("input",{type:"text",placeholder:"Input Class Name please"});const classname_fmt="classanme format: start with 'A-Za-z' or '-' and then 'A-Za-z0-9' or '-'";valueInput.setAttr("title",classname_fmt);const isContainer=kvRow.createEl("input",{type:"checkbox"});isContainer.setAttr("title","Please check it if the classname is the container of the object you want to control");isContainer.addClass("kv-chk");const saveButton=kvRow.createEl("button",{text:"save"});saveButton.onclick=async()=>{const key=keyInput.value.trim();const value=valueInput.value.trim();const chk=isContainer.checked;if(key&&value){if(this.plugin.settings.kvMapReserved[key]||this.plugin.settings.kvMap[key]||this.plugin.settings.kvMapDefault[key]){new obsidian.Notice("Target exists");return}if(!this.isValidClassname(value)){new obsidian.Notice(classname_fmt);return}this.plugin.settings.kvMap[key]=value+"|"+chk;await this.plugin.saveSettings();this.display();new obsidian.Notice(`Saved Target And Class Name: ${key} -> ${value}`);keyInput.value="";valueInput.value="";isContainer.value=""}else{new obsidian.Notice("Input Target and Class Name please")}};const resetButton=kvRow.createEl("button",{text:"reset"});resetButton.onclick=async()=>{const confirmed=resetButton.win.confirm("Confirm to reset? It could not be restored!");if(confirmed){this.plugin.settings.kvMap={};this.plugin.saveData(this.plugin.settings);new obsidian.Notice("reset success");this.display()}else{new obsidian.Notice("reset canceled")}};this.displayKvMap(containerEl);let titleConnect=containerEl.createEl("h2",{text:"How to work in other plugins"});titleConnect.classList.add("config-text-connect");containerEl.createEl("p",{text:"'.diagram-popup' is a reserved class for other plugins to work with."});containerEl.createEl("p",{text:"if you add it to the class list of your target container, it will get the functionality."})}isValidClassname(classname){return/^[A-Za-z-][A-Za-z0-9-]*$/.test(classname)}addClass(_container,_targetElementClass,_class){let dropdownElement=_container.querySelector("."+_targetElementClass);if(dropdownElement){dropdownElement.classList.add(_class)}}setInfo(containerEl,tip,title,msg){const addSettings=new obsidian.Setting(containerEl);addSettings.addExtraButton((extra=>{extra.setIcon("info");extra.setTooltip(tip);extra.onClick((()=>{let msgModal=new obsidian.Modal(this.app);msgModal.setTitle(title);msgModal.setContent(msg);msgModal.open()}));extra.extraSettingsEl.closest(".setting-item")?.classList.add("settings-icon")}))}slideInput(containerEl,title,value,saveVal,step="10",max="100",unit="%"){let input_title=containerEl.createEl("p");input_title.classList.add("open_btn_pos_slide_title");input_title.setText(title);let input_val_min=containerEl.createEl("p");input_val_min.setText("0");let input=containerEl.createEl("input");input.classList.add("open_btn_pos_slide_width");input.setAttribute("type","range");input.setAttribute("min","0");input.setAttribute("max",max);input.setAttribute("step",step);input.setAttribute("value",value);let input_val_max=containerEl.createEl("p");input_val_max.setText(max+unit);let input_val_cur_title=containerEl.createEl("p",{text:"current:"});input_val_cur_title.classList.add("open_btn_pos_cur_title");let input_val_cur=containerEl.createEl("p");input_val_cur.classList.add("open_btn_pos_cur_val");input_val_cur.setText(value);let input_val_cur_per=containerEl.createEl("p");input_val_cur_per.setText(unit);input_val_cur_per.classList.add("open_btn_pos_cur_per");input.addEventListener("input",(event=>{const value=input.value;input_val_cur.setText(value+"");saveVal(value);this.plugin.saveSettings()}))}displayKvMap(containerEl){const existingDisplay=containerEl.querySelector(".kv-display");if(existingDisplay)existingDisplay.remove();const kvDisplay=containerEl.createDiv({cls:"kv-display"});let mergedMap={...this.plugin.settings.kvMapReserved,...this.plugin.settings.kvMapDefault,...this.plugin.settings.kvMap};let kvEntries=Object.entries(mergedMap);if(kvEntries.length>0){const table=kvDisplay.createEl("table");const thead=table.createEl("thead");const headerRow=thead.createEl("tr");headerRow.createEl("th",{text:"Target"});headerRow.createEl("th",{text:"Class Name"});headerRow.createEl("th",{text:"Is Container"});headerRow.createEl("th",{text:"Actions"});const tbody=table.createEl("tbody");kvEntries.forEach((([key,value])=>{const row=tbody.createEl("tr");row.createEl("td",{text:key});let arrVal=value.split("|");let val=arrVal[0].replace(".","");let chk=arrVal[1]=="true"?"Y":"";row.createEl("td",{text:val});row.createEl("td",{text:chk});const actionsTd=row.createEl("td");if(Object.values(this.plugin.settings.kvMapDefault).includes(value))return;if(Object.values(this.plugin.settings.kvMapReserved).includes(value))return;const deleteButton=actionsTd.createEl("button",{text:"del"});deleteButton.addEventListener("click",(()=>{delete this.plugin.settings.kvMap[key];this.display();this.plugin.saveData(this.plugin.settings)}))}))}else{kvDisplay.setText("No Target Setting Saved")}}}const DEFAULT_SETTINGS={kvMap:{},kvMapDefault:{Mermaid:".mermaid"},kvMapReserved:{Reserved:".diagram-popup"},PopupSizeInitValue:"1.50",kvMapPopupSizeInit:{"1.00":"1.00",1.25:"1.25","1.50":"1.50",1.75:"1.75","2.00":"2.00",2.25:"2.25","2.50":"2.50",2.75:"2.75","3.00":"3.00"},DiagramHeightVal:"600",DiagramHeightMin:"50",DiagramHeightMax:"1500",DiagramHeightStep:"50",ZoomRatioValue:"0.2",kvMapZoomRatio:{.1:"0.1",.2:"0.2",.3:"0.3",.4:"0.4"},MoveStepValue:"30",kvMapMoveStep:{20:"20",30:"30",40:"40",50:"60",60:"60"},open_btn_pos_x:"35",open_btn_pos_y:"90",bgColorLight:"rgba(255,255,255, 0.5)",bgColorDark:"rgba(51,51,51, 0.5)",bgAlpha:"0.5",bgAlphaStep:{"0.0":"0.0",.1:"0.1",.2:"0.2",.3:"0.3",.4:"0.4",.5:"0.5",.6:"0.6",.7:"0.7",.8:"0.8",.9:"0.9","1.0":"1.0"},bgIsBlur:"1"};class MermaidPopupPlugin extends obsidian.Plugin{settings;observer_editting;observer_reading;class_editBlockBtn="edit-block-button";class_openPopupBtn="mermaid-popup-button";class_openPopupBtnReading="mermaid-popup-button-reading";class_openPopupBtn_container="mermaid-popup-button-container";class_openPopupBtnReading_container="mermaid-popup-button-reading-container";class_md_containerRead="markdown-reading-view";class_md_containerEdit="markdown-source-view";async onload(){console.log(`Loading ${this.manifest.name} ${this.manifest.version}`);await this.loadSettings();this.addSettingTab(new MermaidPopupSettingTab(this.app,this));this.registerEvent(this.app.workspace.on("layout-change",(()=>{let view=this.app.workspace.getActiveViewOfType(obsidian.MarkdownView);if(!view){this.RelaseWhenfileClose()}if(view){let container=view.containerEl;let targetArr=this.GetSettingsClassElementAll(container);if(targetArr==null||targetArr.length==0){this.RelaseWhenfileClose()}for(var i=0;i{let containerArr=this.GetSettingsClassElementAll(myView);for(var i=0;i{let containerArr=this.GetSettingsClassElementAll(myView);for(var i=0;i0){for(var j=0;j{if(!isDragging){evt.stopPropagation();this.openPopup(targetContainer)}isDragging=false}));popupButton.setCssStyles({display:"none"});this.makePopupButtonDisplay_WhenHoverOnContainer(popupButton,targetContainer)}isParentReading(ele){return this.isParent(ele,this.class_md_containerRead)}isParentEditting(ele){return this.isParent(ele,this.class_md_containerEdit)}isParent(ele,parentClass){return ele.closest(`.${parentClass}`)!==null}makePopupButtonDisplay_WhenHoverOnContainer(button,container){container.addEventListener("mouseenter",(()=>{button.setCssStyles({display:"block"})}));container.addEventListener("mouseleave",(()=>{button.setCssStyles({display:"none"})}))}getAppContainerRect(ele){return ele.doc.getElementsByClassName("app-container")[0].getBoundingClientRect()}setPopupBtnPos(btn){let x=this.settings.open_btn_pos_x;let y=this.settings.open_btn_pos_y;btn.setCssStyles({right:x+"px",top:y+"px"})}adjustDiagramWidthAndHeight_ToContainer(container,isInPopup=false){let coreDeepEle=this.getCoreDeepElement(container);if(!coreDeepEle){let coreEle=this.getCoreElement(container);if(!coreEle)return;let dg_h_val=parseInt(this.settings.DiagramHeightVal);if(dg_h_valcontainer_w_o){rate_by_width=container_w_o/coreDeep_w}let rate_by_height=1;let dg_h_val=parseInt(this.settings.DiagramHeightVal);if(coreDeep_h>dg_h_val){rate_by_height=dg_h_val/coreDeep_h}if(rate_by_width==1&&rate_by_height==1)return;let rate=rate_by_widthchild.tagName.toLowerCase()==="svg"));if(diagramSvg){return diagramSvg}let diagramImg=Array.from(container.children).find((child=>child.tagName.toLowerCase()==="img"));if(diagramImg)return diagramImg;return null}GetPosButtonToMermaid(eleBtn,eleDiv){const divRect=eleDiv.getBoundingClientRect();const buttonRect=eleBtn.getBoundingClientRect();const buttonRelativeTop=buttonRect.top-divRect.top;const buttonRelativeLeft=buttonRect.left-divRect.left;return{top:buttonRelativeTop,left:buttonRelativeLeft}}IsClassListContains_SettingsDiagramClass(ele){if(ele==null||ele.classList==null||ele.classList.length==0)return false;let classnameArr=this.GetSelectorAll();for(var i=0;i{if(!evt.ctrlKey)return;evt.stopPropagation();let targetElement=evt.target;let closestElement=this.GetSettingsClassElementClosest(targetElement);if(closestElement)this.openPopup(closestElement)};openPopup(containerElement){let _doc=containerElement.doc;const overlay=_doc.createElement("div");overlay.classList.add("popup-overlay");this.setPopupBgAlpha(overlay);this.setPopupBgBlur(overlay);let containerElementClone=containerElement.cloneNode(true);let containerElementInPopup=containerElementClone;let{popupButtonClass:popupButtonClass}=this.getOpenBtnInMd_Mark();let childElementArr=containerElementInPopup.querySelectorAll("."+popupButtonClass);if(childElementArr){childElementArr.forEach((child=>{let childEle=child;this.hideElement(childEle)}))}containerElementInPopup.classList.add("popup-content","draggable","resizable");let _buttonContainer=this.createButtonContainer(containerElementInPopup,overlay);overlay.appendChild(containerElementInPopup);overlay.appendChild(_buttonContainer);_doc.body.appendChild(overlay);this.adjustInPopup(containerElementInPopup);overlay.addEventListener("click",(evt=>{evt.doc.body.removeChild(overlay)}));containerElementInPopup.addEventListener("click",(evt=>{evt.stopPropagation()}));containerElementInPopup.doc.addEventListener("keydown",(evt=>{if(evt.key==="Escape"){if(containerElementInPopup.doc.body.contains(overlay))containerElementInPopup.doc.body.removeChild(overlay)}}));this.setPopupSize(containerElementInPopup,containerElement);this.makeDraggable(containerElementInPopup);containerElementInPopup.classList.add("resizable");containerElementInPopup.addEventListener("wheel",(evt=>{evt.preventDefault();const isOut=evt.deltaY>0;this.zoomPopupAtCursor(containerElementInPopup,isOut,evt)}))}hideElement(ele){ele.setCssStyles({display:"none"})}adjustInPopup(containerInPopupEle){let mark=this.getOpenBtnInMd_Mark();let btn_in_p=containerInPopupEle.querySelector("."+mark.popupButtonClass);if(btn_in_p==null)return;let coreEle_in_p=btn_in_p.nextElementSibling;if(coreEle_in_p==null)return;let btnAfterCore=coreEle_in_p.nextElementSibling;if(btnAfterCore){this.hideElement(btnAfterCore)}}setPopupBgBlur(_popupElement){if(!_popupElement)return;let bgIsBlur=this.settings.bgIsBlur;let cssBgIsBlur=bgIsBlur=="1"?"blur(10px)":"";_popupElement.setCssStyles({backdropFilter:cssBgIsBlur})}setPopupBgAlpha(_popupElement){if(!_popupElement)return;let alpha=this.settings.bgAlpha;let newBgColor;if(this.isThemeLight()){newBgColor=`rgba(255, 255, 255, ${alpha})`}else if(this.isThemeDark()){newBgColor=`rgba(51, 51, 51, ${alpha})`}_popupElement.setCssStyles({backgroundColor:newBgColor})}isThemeLight(){return document.body.classList.contains("theme-light")}isThemeDark(){return document.body.classList.contains("theme-dark")}setPopupSize(containerInPopup,container){let multiVal=parseFloat(this.settings.PopupSizeInitValue);if(typeof multiVal!="number"){return}let width_tar_md=this.getWidth(container);let height_tar_md=this.getHeight(container);let core=this.getCoreElement(container);let core_w=this.getWidth(core);let core_h=this.getHeight(core);let core_in_p=this.getCoreElement(containerInPopup);containerInPopup.setCssStyles({width:width_tar_md+"px",height:height_tar_md+"px",transform:`scale(${multiVal})`});core_in_p.setCssStyles({width:core_w+"px",height:core_h+"px"});let coreDeep_in_p=this.getCoreDeepElement(containerInPopup);if(coreDeep_in_p==null)return;let coreDeep_in_p_w=this.getWidth(coreDeep_in_p);if(coreDeep_in_p_w{evt.stopPropagation()}));zoomInButton.addEventListener("click",(evt=>{evt.stopPropagation();this.zoomPopup(_targetElementInPopup,false)}));zoomOutButton.addEventListener("click",(evt=>{evt.stopPropagation();this.zoomPopup(_targetElementInPopup,true)}));upButton.addEventListener("click",(evt=>{evt.stopPropagation();this.movePopup(_targetElementInPopup,0,-1)}));downButton.addEventListener("click",(evt=>{evt.stopPropagation();this.movePopup(_targetElementInPopup,0,1)}));leftButton.addEventListener("click",(evt=>{evt.stopPropagation();this.movePopup(_targetElementInPopup,-1,0)}));rightButton.addEventListener("click",(evt=>{evt.stopPropagation();this.movePopup(_targetElementInPopup,1,0)}));closeButton.addEventListener("click",(evt=>{evt.stopPropagation();evt.doc.body.removeChild(_overlay)}));return buttonContainer}movePopup(popup,dx,dy){const style=popup.win.getComputedStyle(popup);const matrix=style.transform==="none"?new DOMMatrix:new DOMMatrixReadOnly(style.transform);const newX=matrix.m41+(dx==0?dx:dx*parseInt(this.settings.MoveStepValue));const newY=matrix.m42+(dy==0?dy:dy*parseInt(this.settings.MoveStepValue));popup.setCssStyles({transform:`translate(${newX}px, ${newY}px) scale(${matrix.a})`})}zoomPopup(popup,isOut){this.zoomPopupCore(popup,isOut,1,1)}zoomPopupAtCursor(popup,isOut,evt){const popupRect=popup.getBoundingClientRect();const popupCenterX=popupRect.left+popupRect.width/2;const popupCenterY=popupRect.top+popupRect.height/2;const offsetX=evt.clientX-popupCenterX;const offsetY=evt.clientY-popupCenterY;this.zoomPopupCore(popup,isOut,offsetX,offsetY)}zoomPopupCore(popup,isOut,offsetX,offsetY){const style=popup.win.getComputedStyle(popup);const matrix=style.transform==="none"?new DOMMatrix:new DOMMatrixReadOnly(style.transform);const currentScale=matrix.a;let symbol=isOut?-1:1;const newScale=currentScale*(1+symbol*parseFloat(this.settings.ZoomRatioValue));const newX=matrix.m41-offsetX*symbol*parseFloat(this.settings.ZoomRatioValue);const newY=matrix.m42-offsetY*symbol*parseFloat(this.settings.ZoomRatioValue);popup.setCssStyles({transformOrigin:"center center",transform:`translate(${newX}px, ${newY}px) scale(${newScale})`})}makeDraggable(element){let isDragging=false;let startX=0;let startY=0;let initialX=0;let initialY=0;const mouseDownHandler=e=>{e.preventDefault();e.stopPropagation();isDragging=true;if(!e.target)return;const ele_target=e.target;const style=ele_target.win.getComputedStyle(element);const matrix=style.transform==="none"?new DOMMatrix:new DOMMatrixReadOnly(style.transform);startX=e.clientX-matrix.m41;startY=e.clientY-matrix.m42;e.doc.addEventListener("mousemove",mouseMoveHandler);e.doc.addEventListener("mouseup",mouseUpHandler);ele_target.closest(".popup-content")?.classList.add("dragging")};const mouseMoveHandler=e=>{if(!isDragging)return;if(!e.target)return;const ele_target=e.target;const style=ele_target.win.getComputedStyle(element);const matrix=style.transform==="none"?new DOMMatrix:new DOMMatrixReadOnly(style.transform);initialX=e.clientX-startX;initialY=e.clientY-startY;element.setCssStyles({transform:`translate(${initialX}px, ${initialY}px) scale(${matrix.a})`})};const mouseUpHandler=e=>{isDragging=false;e.doc.removeEventListener("mousemove",mouseMoveHandler);e.doc.removeEventListener("mouseup",mouseUpHandler);const ele_target=e.target;ele_target.closest(".popup-content")?.classList.remove("dragging")};element.addEventListener("mousedown",mouseDownHandler);let lastScale=1;let initialDistance=0;let t={scaleX:1,obliqueX:0,obliqueY:0,scaleY:1,translateX:0,translateY:0};element.addEventListener("touchstart",(e=>{e.stopPropagation();e.preventDefault();if(e.touches.length===1){const touch=e.touches[0];initialX=touch.clientX;initialY=touch.clientY}else if(e.touches.length===2){t=getTransform(element);lastScale=t.scaleX;initialX=e.touches[0].clientX;initialY=e.touches[0].clientY;initialDistance=getDistance(e.touches[0],e.touches[1])}}));element.addEventListener("touchmove",(e=>{e.stopPropagation();e.preventDefault();if(e.touches.length===1){t=getTransform(element);touch_move(e,t)}else if(e.touches.length===2){const distance=getDistance(e.touches[0],e.touches[1]);lastScale=distance/initialDistance*lastScale;initialDistance=distance;t=getTransform(element);t.scaleX=lastScale;t.scaleY=lastScale;touch_move(e,t)}}));const touch_move=(e,t)=>{const touch=e.touches[0];const deltaX=touch.clientX-initialX;const deltaY=touch.clientY-initialY;initialX=touch.clientX;initialY=touch.clientY;let tx=t.translateX+deltaX;let ty=t.translateY+deltaY;setTransform(element,tx,ty,t.scaleX)};const getTransform=touchArea=>{const transform=window.getComputedStyle(touchArea).getPropertyValue("transform");if(transform&&transform!=="none"){const match=transform.match(/matrix\((.+)\)/);if(match){const values=match[1].split(", ").map(parseFloat);if(values.length>=6){const scaleX=values[0];const obliqueX=values[1];const obliqueY=values[2];const scaleY=values[3];const translateX=values[4];const translateY=values[5];return{scaleX:scaleX,obliqueX:obliqueX,obliqueY:obliqueY,scaleY:scaleY,translateX:translateX,translateY:translateY}}}}return{scaleX:1,obliqueX:0,obliqueY:0,scaleY:1,translateX:0,translateY:0}};const setTransform=(touchArea,_translateX,_translateY,_scale)=>{let transform=`translate(${_translateX}px, ${_translateY}px) scale(${_scale})`;touchArea.setCssStyles({transform:transform})};const getDistance=(touch1,touch2)=>{const dx=touch1.clientX-touch2.clientX;const dy=touch1.clientY-touch2.clientY;return Math.sqrt(dx*dx+dy*dy)}}}module.exports=MermaidPopupPlugin; + +/* nosourcemap */ \ No newline at end of file diff --git a/.obsidian/plugins/mermaid-popup/manifest.json b/.obsidian/plugins/mermaid-popup/manifest.json new file mode 100644 index 0000000..fd70042 --- /dev/null +++ b/.obsidian/plugins/mermaid-popup/manifest.json @@ -0,0 +1,9 @@ +{ + "id": "mermaid-popup", + "name": "Diagram Popup", + "version": "0.2.69", + "minAppVersion": "0.12.0", + "description": "Show diagrams, from Mermaid, PlantUML, Graphviz and so on, in a draggable and zoomable popup", + "author": "ChenPengyuan", + "isDesktopOnly": false +} diff --git a/.obsidian/plugins/mermaid-popup/styles.css b/.obsidian/plugins/mermaid-popup/styles.css new file mode 100644 index 0000000..3cac3f7 --- /dev/null +++ b/.obsidian/plugins/mermaid-popup/styles.css @@ -0,0 +1,239 @@ + +.popup-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; /* Ensure the overlay is on top */ +} + +.popup-content { + display: flex; + justify-content: center; + align-items: center; + + z-index: 1001; + cursor: pointer; /* 悬停时鼠标变为手掌 */ +} + +.popup-content.dragging { + cursor: grabbing; /* 拖动时鼠标变为抓手 */ +} + +.popup-content > * { + max-width: 300% !important; +} + +.popup-content > img { + pointer-events: none +} + +.theme-light .popup-content { + background-color: var(--background-primary) !important; /* 白色背景 */ +} + +/* 深色模式下的弹窗背景 */ +.theme-dark .popup-content { + background-color: var(--background-primary) !important; /* 深灰色背景 */ +} + +/*开启弹窗按钮*/ +div.mermaid-popup-button, div.mermaid-popup-button-reading { + position: absolute !important; + right: 35px; + top: 4px; + color: var(--text-muted); + width: 30px; + height: 26px; + + padding: 3px 0px 3px 5px; + border-radius: 4px; + cursor: move; /* 鼠标显示为拖动图标 */ + z-index: 9999; + + background-color: rgba(206, 206, 206, 0.4); + border: 1px var(--text-muted) solid; +} + +div.mermaid-popup-button:hover, div.mermaid-popup-button-reading:hover { + background-color: rgb(92, 92, 92, 0.8); /* 鼠标悬停时颜色变化 */ + cursor:default; + border-color: rgba(206, 206, 206, 0.4); + color: rgba(206, 206, 206, 0.4); +} + +/*操作弹窗按钮*/ +.button-container { + position: absolute; + bottom: 50px; /* Adjusted for desired position */ + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 5px; + justify-content: center; + background: rgba(255, 255, 255, 0.7); /* Semi-transparent background for the button container */ + border-radius: 5px; + padding: 5px; + backdrop-filter: blur(5px); /* Background semi-blur */ + z-index: 1002; /* Ensure the button container is on top of the content */ +} +.control-button { + background: rgba(255, 255, 255, 0.7); /* Semi-transparent background */ + border: 1px solid #ccc; + border-radius: 5px; + padding: 5px; + cursor: pointer; + font-size: 16px; +} + +.control-button:hover { + background: rgba(255, 255, 255, 0.9); +} + +.arrow-up, .arrow-down, .arrow-left, .arrow-right, .zoom-in, .zoom-out, .close-popup { + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; +} + +/* 按钮的 hover 样式 */ +.arrow-up:hover, .arrow-down:hover, .arrow-left:hover, .arrow-right:hover, +.zoom-in:hover, .zoom-out:hover, .close-popup:hover { + background-color: var(--button-bg-hover); + color: var(--button-color-hover); +} + +.setting-item-on-top-line{ + border-top: 0px; +} + +/*以下是配置中已存图表来源的表格样式*/ + +/* 定义 kv-row 的 flex 布局样式 */ +.kv-row { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +/* 输入框样式,可以定义宽度和边距 */ +.kv-row input { + margin-right: 10px; + width: 200px; +} + +.kv-row .kv-chk { + margin-right: 10px; + width: 20px; +} + +/* 保存按钮的样式 */ +.kv-row button { + margin-right: 10px; +} + +/* 键值对显示区域的样式 */ +.kv-display { + margin-top: 20px; +} +/* 布局 */ +.setting-table table { + width: 100%; +} + +.setting-table td { + padding-right: 30px; + text-align: left; +} + +.setting-table .ori_diagram_height{ + display:ruby; +} + +.setting-table .ori_diagram_height_cur{ + margin-left: 30px; +} + +.setting-table .ori_diagram_height_val{ + width: 50px; +} + +.setting-table .settings-icon{ + display: block; + border-top: 0px; +} + +.open_btn_pos_slide_title{ + margin-right: 5px !important; +} + +.open_btn_pos_slide_width{ + width: 20% !important; +} +.open_btn_pos_cur_title{ + margin-left: 20px; +} +.open_btn_pos_cur_val{ + width: 30px; +} +.open_btn_pos_cur_per{ + margin-right: 30px; +} + +.open_btn_pos .settings-icon{ + display: block; + border-top: 0px; +} + +/* 表格样式 */ +.kv-display table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; + border: 2px solid #444 !important; /* 外边框 */ +} + +/* 表格标题栏样式 */ +.kv-display th { + background-color: var(--interactive-accent) !important; + padding: 10px; + border: 1px solid #444 !important; /* 表头的边框 */ + text-align: left; +} + +/* 表格行样式 */ +.kv-display td { + padding: 10px; + border: 1px solid #444 !important; /* 单元格边框 */ +} + +/* 表格行的背景颜色交替 */ +.kv-display tr:nth-child(even) { + background-color: var(--interactive-accent); +} + +/* 鼠标悬停时突出显示表格行 */ +.kv-display tr:hover { + background-color: #4a4a4a; +} + +.config-text { + border-bottom: 1px solid gray; /* 灰色横线 */ + padding-bottom: 10px; /* 给横线和文字之间添加一些间距 */ + margin-bottom: 10px; /* 横线与下面内容之间的间距 */ +} + +.config-text-connect{ + border-bottom: 1px solid gray; /* 灰色横线 */ + padding-bottom: 10px; /* 给横线和文字之间添加一些间距 */ + margin-bottom: 10px; /* 横线与下面内容之间的间距 */ + margin-top: 30px; +} + diff --git a/.obsidian/plugins/mermaid-tools/main.js b/.obsidian/plugins/mermaid-tools/main.js new file mode 100644 index 0000000..829d903 --- /dev/null +++ b/.obsidian/plugins/mermaid-tools/main.js @@ -0,0 +1,2633 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ + +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// main.ts +var main_exports = {}; +__export(main_exports, { + TRIDENT_ICON_NAME: () => TRIDENT_ICON_NAME, + default: () => MermaidPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian7 = require("obsidian"); + +// src/elements/sampleDiagrams.ts +var sampleDiagrams = { + EntityRelationshipDiagram: `erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + CUSTOMER }|..|{ DELIVERY-ADDRESS : uses`, + ClassDiagram: `class BankAccount + BankAccount : +String owner + BankAccount : +Bigdecimal balance + BankAccount : +deposit(amount) + BankAccount : +withdrawal(amount)`, + Flowchart: "flowchart LR\nStart --> Stop", + GanttChart: `gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2014-01-01, 30d + Another task :after a1 , 20d + section Another + Task in sec :2014-01-12 , 12d + another task : 24d`, + GitGraph: `gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit`, + PieChart: `pie title /r/obsidianmd posts by type + "Look at my awesome graph" : 85 + "Look at my cool dashboard" : 14 + "Moved from Notion, liking it" : 1`, + RequirementDiagram: ` requirementDiagram + + requirement test_req { + id: 1 + text: the test text. + risk: high + verifymethod: test + } + + element test_entity { + type: simulation + } + + test_entity - satisfies -> test_req`, + SequenceDiagram: `sequenceDiagram +Alice->>John: Hello John, how are you? +John-->>Alice: Great! +Alice-)John: See you later!`, + StateDiagram: `stateDiagram-v2 + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]`, + UserJourneyDiagram: `journey + title My working day + section Go to work + Make tea: 5: Me + Go upstairs: 3: Me + Do work: 1: Me, Cat + section Go home + Go downstairs: 5: Me + Sit down: 5: Me`, + Mindmap: `mindmap + Root + A + B + C`, + Timeline: `timeline + title History of Social Media Platform + 2002 : LinkedIn + 2004 : Facebook + : Google + 2005 : Youtube + 2006 : Twitter`, + QuadrantChart: `quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78]`, + C4Diagram: `C4Context + title System Context diagram for Internet Banking System + Enterprise_Boundary(b0, "BankBoundary0") { + Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") + Person(customerB, "Banking Customer B") + Person_Ext(customerC, "Banking Customer C", "desc") + + Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") + + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + + Enterprise_Boundary(b1, "BankBoundary") { + + SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") + + System_Boundary(b2, "BankBoundary2") { + System(SystemA, "Banking System A") + System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts. next line.") + } + + System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") + SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") + + Boundary(b3, "BankBoundary3", "boundary") { + SystemQueue(SystemF, "Banking System F Queue", "A system of the bank.") + SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") + } + } + } + + BiRel(customerA, SystemAA, "Uses") + BiRel(SystemAA, SystemE, "Uses") + Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") + Rel(SystemC, customerA, "Sends e-mails to") + + UpdateElementStyle(customerA, $fontColor="red", $bgColor="grey", $borderColor="red") + UpdateRelStyle(customerA, SystemAA, $textColor="blue", $lineColor="blue", $offsetX="5") + UpdateRelStyle(SystemAA, SystemE, $textColor="blue", $lineColor="blue", $offsetY="-10") + UpdateRelStyle(SystemAA, SystemC, $textColor="blue", $lineColor="blue", $offsetY="-40", $offsetX="-50") + UpdateRelStyle(SystemC, customerA, $textColor="red", $lineColor="red", $offsetX="-50", $offsetY="20") + + UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")`, + SankeyDiagram: `sankey-beta + + %% source,target,value + Electricity grid,Over generation / exports,104.453 + Electricity grid,Heating and cooling - homes,113.726 + Electricity grid,H2 conversion,27.14`, + XyChart: `xychart-beta + title "Sales Revenue" + x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] + y-axis "Revenue (in $)" 4000 --> 11000 + bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] + line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]`, + Packet: `packet-beta +title UDP Packet +0-15: "Source Port" +16-31: "Destination Port" +32-47: "Length" +48-63: "Checksum" +64-95: "Data (variable length)" +`, + Kanban: `kanban + Todo + [Create Documentation] + docs[Create Blog about the new diagram] + [In progress] + id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.] + id9[Ready for deploy] + id8[Design grammar]@{ assigned: 'knsv' } + id10[Ready for test] + id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' } + id66[last item]@{ priority: 'Very Low', assigned: 'knsv' } + id11[Done] + id5[define getData] + id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'} + id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' } + + id12[Can't reproduce] + id3[Weird flickering in Firefox]`, + Block: `block-beta +columns 1 + db(("DB")) + blockArrowId6<["   "]>(down) + block:ID + A + B["A wide one in the middle"] + C + end + space + D + ID --> D + C --> D + style B fill:#969,stroke:#333,stroke-width:4px +`, + Architecture: `architecture-beta + group api(cloud)[API] + + service db(database)[Database] in api + service disk1(disk)[Storage] in api + service disk2(disk)[Storage] in api + service server(server)[Server] in api + + db:L -- R:server + disk1:T -- B:server + disk2:T -- B:db +` +}; + +// src/elements/architecture.ts +var architectureElements = [ + { + id: crypto.randomUUID(), + categoryId: "architecture", + description: "a sample architecture diagram", + content: `architecture-beta + group api(cloud)[API] + + service db(database)[Database] in api + service disk1(disk)[Storage] in api + service disk2(disk)[Storage] in api + service server(server)[Server] in api + + db:L -- R:server + disk1:T -- B:server + disk2:T -- B:db +`, + sortingOrder: 0, + isPinned: false + } +]; + +// src/elements/blockDiagram.ts +var blockDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "block", + description: "a sample block diagram", + content: `block-beta +columns 1 + db(("DB")) + blockArrowId6<["   "]>(down) + block:ID + A + B["A wide one in the middle"] + C + end + space + D + ID --> D + C --> D + style B fill:#969,stroke:#333,stroke-width:4px +`, + sortingOrder: 0, + isPinned: false + } +]; + +// src/elements/c4Diagram.ts +var c4DiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "c4Diagram", + description: "sample C4 diagram (compatible with PlantUML)", + content: `C4Context + title System Context diagram for Internet Banking System + Enterprise_Boundary(b0, "BankBoundary0") { + Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") + Person(customerB, "Banking Customer B") + Person_Ext(customerC, "Banking Customer C", "desc") + + Person(customerD, "Banking Customer D", "A customer of the bank,
with personal bank accounts.") + + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + + Enterprise_Boundary(b1, "BankBoundary") { + + SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") + + System_Boundary(b2, "BankBoundary2") { + System(SystemA, "Banking System A") + System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts. next line.") + } + + System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") + SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.") + + Boundary(b3, "BankBoundary3", "boundary") { + SystemQueue(SystemF, "Banking System F Queue", "A system of the bank.") + SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.") + } + } + } + + BiRel(customerA, SystemAA, "Uses") + BiRel(SystemAA, SystemE, "Uses") + Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") + Rel(SystemC, customerA, "Sends e-mails to") + + UpdateElementStyle(customerA, $fontColor="red", $bgColor="grey", $borderColor="red") + UpdateRelStyle(customerA, SystemAA, $textColor="blue", $lineColor="blue", $offsetX="5") + UpdateRelStyle(SystemAA, SystemE, $textColor="blue", $lineColor="blue", $offsetY="-10") + UpdateRelStyle(SystemAA, SystemC, $textColor="blue", $lineColor="blue", $offsetY="-40", $offsetX="-50") + UpdateRelStyle(SystemC, customerA, $textColor="red", $lineColor="red", $offsetX="-50", $offsetY="20") + + UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")`, + sortingOrder: 1, + isPinned: false + } +]; + +// src/elements/classDiagram.ts +var classDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "sample class", + content: `class Duck{ + +String beakColor + +swim() + +quack() + }`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "sample class", + content: `class BankAccount + BankAccount : +String owner + BankAccount : +Bigdecimal balance + BankAccount : +deposit(amount) + BankAccount : +withdrawal(amount)`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "generic class", + content: `class Square~Shape~{ + int id + List~int~ position + setPoints(List~int~ points) + getPoints() List~int~ + } + + Square : -List~string~ messages + Square : +setMessages(List~string~ messages) + Square : +getMessages() List~string~`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "inheritance", + content: "classA <|-- classB", + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "composition", + content: "classC *-- classD", + sortingOrder: 4, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "aggregation", + content: "classE o-- classF", + sortingOrder: 5, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "association", + content: "classG <-- classH", + sortingOrder: 6, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "solid link", + content: "classI -- classJ", + sortingOrder: 7, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "dependency", + content: "classK <.. classL", + sortingOrder: 8, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "realization", + content: "classM <|.. classN", + sortingOrder: 9, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "dashed link", + content: "classO .. classP", + sortingOrder: 10, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "two-way relation", + content: "Animal <|--|> Zebra", + sortingOrder: 11, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "classDiagram", + description: "sample class diagram", + content: `classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + }`, + sortingOrder: 12, + isPinned: false + } +]; + +// src/elements/entityRelationshipDiagram.ts +var entityRelationshipDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "a sample entity relationship diagram", + content: `erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + CUSTOMER }|..|{ DELIVERY-ADDRESS : uses`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "an entity", + content: ` CUSTOMER { + string name + string custNumber + string sector + }`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "one-to-many relationship", + content: `A ||--|{ B : label`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "many-to-many relationship", + content: `A }|--|{ B : label`, + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "one-to-one relationship", + content: `A ||--|| B : label`, + sortingOrder: 4, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "many-to-one relationship", + content: `A }|--|| B : label`, + sortingOrder: 5, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "zero/one-to-one relationship", + content: `A |o--|| B : label`, + sortingOrder: 6, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "one-to-one/zero relationship", + content: `A ||--o| B : label`, + sortingOrder: 7, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "zero-or-more-to-one relationship", + content: `A }o--|| B : label`, + sortingOrder: 8, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "one-to-zero-or-more relationship", + content: `A ||--o{ B : label`, + sortingOrder: 9, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "zero-or-more-to-many relationship", + content: `A }o--|{ B : label`, + sortingOrder: 10, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "entityRelationshipDiagram", + description: "many-to-zero-or-more relationship", + content: `A }|--o{ B : label`, + sortingOrder: 11, + isPinned: false + } +]; + +// src/elements/flowchart.ts +var flowchartElements = [ + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "a simple flowchart with top to down direction", + content: `flowchart TD +Start --> Stop`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "a simple flowchart with left to right direction", + content: "flowchart LR\nStart --> Stop", + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "A node with round edges", + content: "id1(Some text)", + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "A stadium-shaped node", + content: "id1([Some text])", + sortingOrder: 4, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "A node in a cylindrical shape", + content: "id1[(Database)]", + sortingOrder: 5, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Circle", + content: "id1((Some text))", + sortingOrder: 6, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Rhombus", + content: "id1{Some text}", + sortingOrder: 7, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Hexagon", + content: "id1{{Some text}}", + sortingOrder: 8, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Parallelogram skewed right", + content: "id1[/Some text/]", + sortingOrder: 9, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Parallelogram skewed left", + content: "id1[\\Some text\\]", + sortingOrder: 10, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Trapezoid", + content: "A[/Some text\\]", + sortingOrder: 11, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Trapezoid upside down", + content: "A[\\Some text/]", + sortingOrder: 12, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Double circle node", + content: "id1(((Some text)))", + sortingOrder: 13, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "A link with arrow head", + content: "A-->B", + sortingOrder: 14, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "An open link", + content: "A --- B", + sortingOrder: 15, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Text on links", + content: "A-- This is the text! ---B", + sortingOrder: 16, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "A link with arrow head and text", + content: "A-->|text|B", + sortingOrder: 17, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Dotted link", + content: "A-.->B", + sortingOrder: 18, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Thick link", + content: "A ==> B", + sortingOrder: 19, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Invisible link", + content: "A ~~~ B", + sortingOrder: 20, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Link with circle edge", + content: "A --o B", + sortingOrder: 21, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Link with cross edge", + content: "A --x B", + sortingOrder: 22, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "flowchart", + description: "Subgraph", + content: "subgraph one\na1-->a2\nend", + sortingOrder: 14, + isPinned: false + } +]; + +// src/elements/ganntChart.ts +var ganttChartElements = [ + { + id: crypto.randomUUID(), + categoryId: "ganttChart", + description: "simple gantt chart", + content: `gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2014-01-01, 30d + Another task :after a1 , 20d + section Another + Task in sec :2014-01-12 , 12d + another task : 24d`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "ganttChart", + description: "rich gantt chart", + content: `gantt + dateFormat YYYY-MM-DD + title Adding GANTT diagram functionality to mermaid + excludes weekends + + section A section + Completed task :done, des1, 2014-01-06,2014-01-08 + Active task :active, des2, 2014-01-09, 3d + Future task : des3, after des2, 5d + Future task2 : des4, after des3, 5d + + section Critical tasks + Completed task in the critical line :crit, done, 2014-01-06,24h + Implement parser and jison :crit, done, after des1, 2d + Create tests for parser :crit, active, 3d + Future task in critical line :crit, 5d + Create tests for renderer :2d + Add to mermaid :1d + Functionality added :milestone, 2014-01-25, 0d + + section Documentation + Describe gantt syntax :active, a1, after des1, 3d + Add gantt diagram to demo page :after a1 , 20h + Add another diagram to demo page :doc1, after a1 , 48h + + section Last section + Describe gantt syntax :after doc1, 3d + Add gantt diagram to demo page :20h + Add another diagram to demo page :48h`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "ganttChart", + description: "milestones example", + content: `gantt + dateFormat HH:mm + axisFormat %H:%M + Initial milestone : milestone, m1, 17:49,2min + taska2 : 10min + taska3 : 5min + Final milestone : milestone, m2, 18:14, 2min`, + sortingOrder: 2, + isPinned: false + } +]; + +// src/elements/gitGraph.ts +var gitGraphElements = [ + { + id: crypto.randomUUID(), + categoryId: "gitGraph", + description: "simple git graph", + content: `gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "gitGraph", + description: "tagged commit", + content: `commit id: "Normal" tag: "v1.0.0"`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "gitGraph", + description: "reverse commit", + content: `commit id: "Reverse" type: REVERSE`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "gitGraph", + description: "highlighted commit", + content: `commit id: "Highlight" type: HIGHLIGHT`, + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "gitGraph", + description: "reverse commit", + content: `commit id: "Reverse" type: REVERSE`, + sortingOrder: 4, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "gitGraph", + description: "git graph with cherry-pick", + content: `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + commit id:"TWO" + cherry-pick id:"A" + commit id:"THREE" + checkout develop + commit id:"C"`, + sortingOrder: 5, + isPinned: false + } +]; + +// src/elements/kanban.ts +var kanbanElements = [ + { + id: crypto.randomUUID(), + categoryId: "kanban", + description: "a sample kanban diagram", + content: `kanban + Todo + [Create Documentation] + docs[Create Blog about the new diagram] + [In progress] + id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.] + id9[Ready for deploy] + id8[Design grammar]@{ assigned: 'knsv' } + id10[Ready for test] + id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' } + id66[last item]@{ priority: 'Very Low', assigned: 'knsv' } + id11[Done] + id5[define getData] + id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'} + id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' } + + id12[Can't reproduce] + id3[Weird flickering in Firefox]`, + sortingOrder: 0, + isPinned: false + } +]; + +// src/elements/mindMap.ts +var mindMapElements = [ + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "a simple mindmap", + content: `mindmap + Root + A + B + C`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "square", + content: `id[I am a square]`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "rounded square", + content: `id(I am a rounded square)`, + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "circle", + content: `id((I am a circle))`, + sortingOrder: 4, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "bang", + content: `id))I am a bang((`, + sortingOrder: 5, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "cloud", + content: `id)I am a cloud(`, + sortingOrder: 6, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "hexagon", + content: `id{{I am a hexagon}}`, + sortingOrder: 7, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "default", + content: `I am the default shape`, + sortingOrder: 8, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "mindmap", + description: "sample mindmap", + content: `mindmap + root((mindmap)) + Origins + Long history + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid`, + sortingOrder: 9, + isPinned: false + } +]; + +// src/elements/packet.ts +var packetElements = [ + { + id: crypto.randomUUID(), + categoryId: "packet", + description: "a sample packet diagram", + content: `packet-beta +title UDP Packet +0-15: "Source Port" +16-31: "Destination Port" +32-47: "Length" +48-63: "Checksum" +64-95: "Data (variable length)" +`, + sortingOrder: 0, + isPinned: false + } +]; + +// src/elements/pieChart.ts +var pieChartElements = [ + { + id: crypto.randomUUID(), + categoryId: "pieChart", + description: "sample pie chart", + content: `pie title /r/obsidianmd posts by type + "Graphs" : 85 + "Dashboards" : 14 + "Tips" : 1`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "pieChart", + description: "sample pie chart with values shown in legend", + content: `pie showData title /r/obsidianmd posts by type + "Graphs" : 85 + "Dashboards" : 14 + "Tips" : 1`, + sortingOrder: 1, + isPinned: false + } +]; + +// src/elements/quadrant.ts +var quadrantElements = [ + { + id: crypto.randomUUID(), + categoryId: "quadrantChart", + description: "sample quadrant chart", + content: `quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78]`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "quadrantChart", + description: "themed quadrant chart", + content: `%%{init: {"quadrantChart": {"chartWidth": 400, "chartHeight": 400}, "themeVariables": {"quadrant1TextFill": "#ff0000"} }}%% + quadrantChart + x-axis Urgent --> Not Urgent + y-axis Not Important --> "Important \u2764" + quadrant-1 Plan + quadrant-2 Do + quadrant-3 Delegate + quadrant-4 Delete`, + sortingOrder: 1, + isPinned: false + } +]; + +// src/elements/requirementDiagram.ts +var requirementDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "requirementDiagram", + description: "sample requirements diagram", + content: ` requirementDiagram + + requirement test_req { + id: 1 + text: the test text. + risk: high + verifymethod: test + } + + element test_entity { + type: simulation + } + + test_entity - satisfies -> test_req`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "requirementDiagram", + description: "sample requirements diagram", + content: `element customElement { + type: customType + docref: customDocRef + }`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "requirementDiagram", + description: "a requirement with high risk", + content: `functionalRequirement myReq { + id: reqId + text: someText + risk: High + verifymethod: analysis + }`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "requirementDiagram", + description: "sample requirements diagram", + content: `interfaceRequirement myReq2 { + id: reqId + text: someText + risk: Medium + verifymethod: demonstration + }`, + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "requirementDiagram", + description: "sample requirements diagram", + content: `designConstraint myReq3 { + id: reqId + text: someText + risk: Low + verifymethod: test + }`, + sortingOrder: 4, + isPinned: false + } +]; + +// src/elements/sankeyDiagram.ts +var sankeyDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "sankeyDiagram", + description: "", + content: `sankey-beta + %% source,target,value + Electricity grid,Over generation / exports,104.453 + Electricity grid,Heating and cooling - homes,113.726 + Electricity grid,H2 conversion,27.14`, + sortingOrder: 0, + isPinned: false + } +]; + +// src/elements/sequenceDiagram.ts +var sequenceDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "sequenceDiagram", + description: "a simple sequence diagram", + content: `sequenceDiagram +Alice->>John: Hello John, how are you? +John-->>Alice: Great! +Alice-)John: See you later!`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "sequenceDiagram", + description: "a simple sequence diagram with actors", + content: `sequenceDiagram +actor Alice +actor John +Alice->>John: Hello John, how are you? +John-->>Alice: Great! +Alice-)John: See you later!`, + sortingOrder: 1, + isPinned: false + } +]; + +// src/elements/stateDiagram.ts +var stateDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "a sample state diagram", + content: `stateDiagram-v2 + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "a sample state diagram with left-to-right direction", + content: `stateDiagram-v2 + direction LR + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "node with description", + content: `s2 : This is a state description`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "a transition", + content: `s1 --> s2`, + sortingOrder: 3, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "a transition with label", + content: `s1 --> s2: A transition`, + sortingOrder: 4, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "composite state", + content: ` + [*] --> First + state First { + [*] --> second + second --> [*] + }`, + sortingOrder: 5, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "diagram with choice", + content: `stateDiagram-v2 + state if_state <> + [*] --> IsPositive + IsPositive --> if_state + if_state --> False: if n < 0 + if_state --> True : if n >= 0`, + sortingOrder: 6, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "diagram with fork", + content: `stateDiagram-v2 + state fork_state <> + [*] --> fork_state + fork_state --> State2 + fork_state --> State3 + + state join_state <> + State2 --> join_state + State3 --> join_state + join_state --> State4 + State4 --> [*]`, + sortingOrder: 7, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "stateDiagram", + description: "a diagram with concurrency", + content: `stateDiagram-v2 + [*] --> Active + + state Active { + [*] --> NumLockOff + NumLockOff --> NumLockOn : EvNumLockPressed + NumLockOn --> NumLockOff : EvNumLockPressed + -- + [*] --> CapsLockOff + CapsLockOff --> CapsLockOn : EvCapsLockPressed + CapsLockOn --> CapsLockOff : EvCapsLockPressed + -- + [*] --> ScrollLockOff + ScrollLockOff --> ScrollLockOn : EvScrollLockPressed + ScrollLockOn --> ScrollLockOff : EvScrollLockPressed + }`, + sortingOrder: 8, + isPinned: false + } +]; + +// src/elements/timeline.ts +var timelineElements = [ + { + id: crypto.randomUUID(), + categoryId: "timeline", + description: "sample timeline", + content: `timeline + title History of Social Media Platform + 2002 : LinkedIn + 2004 : Facebook + : Google + 2005 : Youtube + 2006 : Twitter`, + sortingOrder: 1, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "timeline", + description: "timeline with grouping", + content: `timeline + title Timeline of Industrial Revolution + section 17th-20th century + Industry 1.0 : Machinery, Water power, Steam
power + Industry 2.0 : Electricity, Internal combustion engine, Mass production + Industry 3.0 : Electronics, Computers, Automation + section 21st century + Industry 4.0 : Internet, Robotics, Internet of Things + Industry 5.0 : Artificial intelligence, Big data,3D printing`, + sortingOrder: 2, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "timeline", + description: "timeline with Forest theme. see the docs for additional themes", + content: `%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%% + timeline + title History of Social Media Platform + 2002 : LinkedIn + 2004 : Facebook : Google + 2005 : Youtube + 2006 : Twitter + 2007 : Tumblr + 2008 : Instagram + 2010 : Pinterest`, + sortingOrder: 3, + isPinned: false + } +]; + +// src/elements/userJourneyDiagram.ts +var userJourneyDiagramElements = [ + { + id: crypto.randomUUID(), + categoryId: "userJourneyDiagram", + description: "a sample user journey diagram", + content: `journey + title My working day + section Go to work + Make tea: 5: Me + Go upstairs: 3: Me + Do work: 1: Me, Cat + section Go home + Go downstairs: 5: Me + Sit down: 5: Me`, + sortingOrder: 0, + isPinned: false + }, + { + id: crypto.randomUUID(), + categoryId: "userJourneyDiagram", + description: "a step in user journey", + content: ` Step Title: 5: ActorName`, + sortingOrder: 1, + isPinned: false + } +]; + +// src/elements/xyChart.ts +var xyChartElements = [ + { + id: crypto.randomUUID(), + categoryId: "xyChart", + description: "a sample XYChart diagram", + content: `xychart-beta + title "Sales Revenue" + x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] + y-axis "Revenue (in $)" 4000 --> 11000 + bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] + line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]`, + sortingOrder: 0, + isPinned: false + } +]; + +// src/elements/defaultElements.ts +var defaultElements = [ + ...flowchartElements, + ...sequenceDiagramElements, + ...classDiagramElements, + ...stateDiagramElements, + ...entityRelationshipDiagramElements, + ...userJourneyDiagramElements, + ...ganttChartElements, + ...pieChartElements, + ...requirementDiagramElements, + ...gitGraphElements, + ...mindMapElements, + ...timelineElements, + ...quadrantElements, + ...c4DiagramElements, + ...sankeyDiagramElements, + ...xyChartElements, + ...packetElements, + ...kanbanElements, + ...blockDiagramElements, + ...architectureElements +]; + +// src/core/defaultCategories.ts +var DEFAULT_CATEGORIES = [ + { + id: "flowchart", + name: "Flowchart", + defaultWrapping: "flowchart LR", + wrappings: ["flowchart LR", "flowchart TD"], + isCustom: false, + sortOrder: 0 + }, + { + id: "sequenceDiagram", + name: "SequenceDiagram", + defaultWrapping: "sequenceDiagram", + wrappings: null, + isCustom: false, + sortOrder: 1 + }, + { + id: "classDiagram", + name: "ClassDiagram", + defaultWrapping: "classDiagram", + wrappings: null, + isCustom: false, + sortOrder: 2 + }, + { + id: "stateDiagram", + name: "StateDiagram", + defaultWrapping: "stateDiagram-v2", + wrappings: null, + isCustom: false, + sortOrder: 3 + }, + { + id: "entityRelationshipDiagram", + name: "EntityRelationshipDiagram", + defaultWrapping: "erDiagram", + wrappings: null, + isCustom: false, + sortOrder: 4 + }, + { + id: "userJourneyDiagram", + name: "UserJourneyDiagram", + defaultWrapping: "journey", + wrappings: null, + isCustom: false, + sortOrder: 5 + }, + { + id: "ganttChart", + name: "GanttChart", + defaultWrapping: "gantt", + wrappings: null, + isCustom: false, + sortOrder: 6 + }, + { + id: "pieChart", + name: "PieChart", + defaultWrapping: "pie", + wrappings: null, + isCustom: false, + sortOrder: 7 + }, + { + id: "requirementDiagram", + name: "RequirementDiagram", + defaultWrapping: "requirementDiagram", + wrappings: null, + isCustom: false, + sortOrder: 8 + }, + { + id: "gitGraph", + name: "GitGraph", + defaultWrapping: "gitGraph", + wrappings: null, + isCustom: false, + sortOrder: 9 + }, + { + id: "mindmap", + name: "Mindmap", + defaultWrapping: "mindmap", + wrappings: ["mindmap"], + isCustom: false, + sortOrder: 10 + }, + { + id: "timeline", + name: "Timeline", + defaultWrapping: "timeline", + wrappings: null, + isCustom: false, + sortOrder: 11 + }, + { + id: "c4Diagram", + name: "C4Diagram", + defaultWrapping: "C4Context", + wrappings: null, + isCustom: false, + sortOrder: 12 + }, + { + id: "quadrantChart", + name: "QuadrantChart", + defaultWrapping: "quadrantChart", + wrappings: null, + isCustom: false, + sortOrder: 13 + }, + { + id: "sankeyDiagram", + name: "SankeyDiagram", + defaultWrapping: "sankey-beta", + wrappings: null, + isCustom: false, + sortOrder: 14 + }, + { + id: "xyChart", + name: "XyChart", + defaultWrapping: "xychart-beta", + wrappings: null, + isCustom: false, + sortOrder: 15 + }, + { + id: "kanban", + name: "Kanban", + defaultWrapping: "kanban", + wrappings: null, + isCustom: false, + sortOrder: 16 + }, + { + id: "architecture", + name: "Architecture", + defaultWrapping: "architecture-beta", + wrappings: null, + isCustom: false, + sortOrder: 17 + }, + { + id: "block", + name: "Block", + defaultWrapping: "block-beta", + wrappings: null, + isCustom: false, + sortOrder: 18 + }, + { + id: "packet", + name: "Packet", + defaultWrapping: "packet-beta", + wrappings: null, + isCustom: false, + sortOrder: 19 + } +]; + +// src/core/categoryService.ts +var CategoryService = class { + constructor() { + this.categories = []; + this.categories = [...DEFAULT_CATEGORIES]; + } + static getInstance() { + if (!CategoryService.instance) { + CategoryService.instance = new CategoryService(); + } + return CategoryService.instance; + } + getCategories() { + return [...this.categories].sort((a, b) => a.sortOrder - b.sortOrder); + } + getCategoryById(id) { + return this.categories.find((cat) => cat.id === id); + } + getCategoryByName(name) { + return this.categories.find((cat) => cat.name === name); + } + addCategory(category) { + if (this.categories.some((cat) => cat.id === category.id)) { + throw new Error(`Category with ID '${category.id}' already exists`); + } + this.categories.push(category); + } + updateCategory(category) { + const index = this.categories.findIndex((cat) => cat.id === category.id); + if (index === -1) { + throw new Error(`Category with ID '${category.id}' not found`); + } + this.categories[index] = category; + } + deleteCategory(id) { + var _a; + if (!((_a = this.getCategoryById(id)) == null ? void 0 : _a.isCustom)) { + throw new Error("Cannot delete default categories"); + } + this.categories = this.categories.filter((cat) => cat.id !== id); + } + loadCategories(customCategories, defaultCategorySortOrders = {}) { + const defaultCategories = DEFAULT_CATEGORIES.map((cat) => ({ + ...cat, + sortOrder: defaultCategorySortOrders[cat.id] !== void 0 ? defaultCategorySortOrders[cat.id] : cat.sortOrder + })); + const customCats = customCategories.filter((cat) => cat.isCustom); + this.categories = [...defaultCategories, ...customCats]; + } + getCustomCategories() { + return this.categories.filter((cat) => cat.isCustom); + } + getWrappingData(categoryId) { + var _a; + const category = this.getCategoryById(categoryId); + if (!category) + return null; + return { + defaultWrapping: category.defaultWrapping, + wrappings: (_a = category.wrappings) != null ? _a : null + }; + } + getNextSortOrder() { + return Math.max(...this.categories.map((cat) => cat.sortOrder), -1) + 1; + } +}; + +// src/core/elementService.ts +var MermaidElementService = class { + constructor() { + this.categoryService = CategoryService.getInstance(); + } + static DefaultElements() { + return defaultElements; + } + saveElement(element, plugin) { + const elementExists = plugin.settings.elements.some((el) => el.id === element.id); + if (elementExists) { + const index = plugin.settings.elements.findIndex((el) => el.id === element.id); + if (index !== -1) { + plugin.settings.elements[index] = element; + } + } else { + this.fixSortOrder(element, plugin); + plugin.settings.elements.push(element); + } + plugin.saveSettings(); + } + fixSortOrder(element, plugin) { + const elementsFromSameCategory = plugin.settings.elements.filter((el) => el.categoryId === element.categoryId); + if (elementsFromSameCategory.some((el) => el.sortingOrder === element.sortingOrder)) { + element.sortingOrder = elementsFromSameCategory.length; + } + } + getSampleDiagram(categoryId) { + const category = this.categoryService.getCategoryById(categoryId); + if (!category) { + console.warn(`[Mermaid Tools] No category found for ID: ${categoryId}, using default sample`); + return this.wrapForPastingIntoEditor(this.wrapWithMermaidBlock("flowchart TD\nStart --> End")); + } + const sampleKey = category.name; + const sample = sampleDiagrams[sampleKey]; + if (sample) { + return this.wrapForPastingIntoEditor(this.wrapWithMermaidBlock(sample)); + } + console.warn(`[Mermaid Tools] No sample diagram found for category: ${category.name}, using default sample`); + return this.wrapForPastingIntoEditor(this.wrapWithMermaidBlock("flowchart TD\nStart --> End")); + } + wrapForPastingIntoEditor(text) { + return `${text} +`; + } + wrapWithMermaidBlock(text) { + return `\`\`\`mermaid +${text} +\`\`\``; + } + wrapAsCompleteDiagram(element) { + const wrapping = this.categoryService.getWrappingData(element.categoryId); + if (!wrapping) { + console.warn(`[Mermaid Tools] No wrapping data found for category: ${element.categoryId}`); + return element.content; + } + const contentAlreadyWrapped = wrapping.wrappings ? wrapping.wrappings.some((w) => element.content.contains(w)) : element.content.contains(wrapping.defaultWrapping); + if (contentAlreadyWrapped) { + return element.content; + } + const wrappedContent = wrapping.defaultWrapping + "\n" + element.content; + const firstWord = wrapping.defaultWrapping.trim().split(/\s+/)[0]; + const validDiagramTypes = [ + "flowchart", + "graph", + "sequenceDiagram", + "classDiagram", + "stateDiagram-v2", + "erDiagram", + "journey", + "gantt", + "pie", + "requirementDiagram", + "gitGraph", + "mindmap", + "timeline", + "quadrantChart", + "C4Context", + "sankey-beta", + "xychart-beta", + "packet-beta", + "kanban", + "block-beta", + "architecture-beta" + ]; + if (!validDiagramTypes.includes(firstWord)) { + console.warn(`[Mermaid Tools] Potentially invalid diagram type "${firstWord}" in category ${element.categoryId}. This may cause rendering errors.`); + } + return wrappedContent; + } +}; + +// src/core/textEditorService.ts +var NoActiveCursorError = class extends Error { + constructor() { + super(); + this.message = "Mermaid Tools: Error getting cursor position. Make sure you are in editing mode and have an active cursor in file content."; + } +}; +var TextEditorService = class { + constructor() { + this._elementService = new MermaidElementService(); + } + insertTextAtCursor(editor, content) { + if (!editor) + throw new NoActiveCursorError(); + content = this._elementService.wrapForPastingIntoEditor(content); + const cursor = editor.getCursor(); + editor.replaceRange(content, cursor); + const lines = content.split("\n"); + const newCursor = { + line: cursor.line + lines.length - 1, + ch: lines.length === 1 ? cursor.ch + lines[0].length : lines[lines.length - 1].length + }; + editor.setCursor(newCursor); + editor.focus(); + } +}; + +// src/settings/settings.ts +var MermaidPluginSettings = class { + static DefaultSettings() { + const settings = new MermaidPluginSettings(); + settings.elements = defaultElements; + settings.customCategories = []; + settings.selectedCategoryId = "flowchart"; + settings.defaultCategorySortOrders = {}; + settings.categoryModifications = {}; + return settings; + } +}; + +// src/trident-icon.ts +var import_obsidian = require("obsidian"); +function addTridentIcon() { + (0, import_obsidian.addIcon)(TRIDENT_ICON_NAME, getSanitizedSvg()); +} +function getSanitizedSvg() { + var sanitized = (0, import_obsidian.sanitizeHTMLToDom)(tridentIcon); + var tempContainer = createDiv(); + tempContainer.appendChild(sanitized); + return tempContainer.innerHTML; +} +var tridentIcon = ` + +`; + +// src/ui/settingsTab.ts +var import_obsidian4 = require("obsidian"); + +// src/ui/editMermaidElementModal.ts +var import_obsidian2 = require("obsidian"); +var EditMermaidElementModal = class extends import_obsidian2.Modal { + constructor(app, _plugin, _mermaid, _element, _categoryService) { + super(app); + this._plugin = _plugin; + this._mermaid = _mermaid; + this._element = _element; + this._categoryService = _categoryService; + } + async onOpen() { + const { contentEl } = this; + contentEl.addClass("mermaid-tools-edit-element-modal"); + contentEl.createEl("h2", { text: "Edit element" }); + const renderContainerEl = contentEl.createDiv(); + const renderEl = renderContainerEl.createEl("pre", { text: "rendered diagram" }); + if (!this._mermaid) + this._mermaid = await (0, import_obsidian2.loadMermaid)(); + renderEl.id = "mermaid-edit-element-modal"; + const elementCategoryContainerEl = contentEl.createDiv(); + elementCategoryContainerEl.createEl("label", { text: "Category" }); + const elementCategoryEl = elementCategoryContainerEl.createEl("select"); + const categories = this._categoryService.getCategories(); + for (const category of categories) { + const option = elementCategoryEl.createEl("option", { text: category.name }); + option.value = category.id; + } + elementCategoryEl.value = this._element.categoryId; + elementCategoryEl.onchange = (e) => { + this._element.categoryId = elementCategoryEl.value; + }; + const elementDescriptionContainerEl = contentEl.createDiv(); + elementDescriptionContainerEl.createEl("label", { text: "Description" }); + const elementDescriptionEl = elementDescriptionContainerEl.createEl("input", { value: this._element.description, type: "text" }); + elementDescriptionEl.style.minWidth = "50%"; + elementDescriptionEl.onchange = (e) => { + this._element.description = elementDescriptionEl.value; + }; + const elementContentContainerEl = contentEl.createDiv(); + elementContentContainerEl.createEl("label", { text: "Content" }); + const elementContentEl = elementContentContainerEl.createEl("textarea", { text: this._element.content }); + elementContentEl.style.height = "200px"; + elementContentEl.style.width = "100%"; + elementContentEl.onchange = async (e) => { + this._element.content = elementContentEl.value; + const { svg: svg2 } = await this._mermaid.render(renderEl.id, this._plugin._mermaidElementService.wrapAsCompleteDiagram(this._element)); + renderEl.innerHTML = svg2; + renderContainerEl.appendChild(renderEl); + }; + const saveButtonEl = contentEl.createEl("button", { text: "Save" }); + saveButtonEl.onclick = (e) => { + this.save(); + }; + const { svg } = await this._mermaid.render(renderEl.id, this._plugin._mermaidElementService.wrapAsCompleteDiagram(this._element)); + renderEl.innerHTML = svg; + renderContainerEl.appendChild(renderEl); + } + save() { + this._plugin._mermaidElementService.saveElement(this._element, this._plugin); + this.close(); + } +}; + +// src/ui/editCategoryModal.ts +var import_obsidian3 = require("obsidian"); +var EditCategoryModal = class extends import_obsidian3.Modal { + constructor(app, plugin, existingCategory, onSave) { + super(app); + this.plugin = plugin; + this.categoryService = CategoryService.getInstance(); + this.onSave = onSave; + this.isNewCategory = !existingCategory; + if (existingCategory) { + this.category = { ...existingCategory }; + } else { + this.category = { + id: "", + name: "", + defaultWrapping: "", + wrappings: null, + isCustom: true, + sortOrder: this.categoryService.getNextSortOrder() + }; + } + } + onOpen() { + const { contentEl } = this; + contentEl.addClass("mermaid-tools-edit-category-modal"); + contentEl.createEl("h2", { + text: this.isNewCategory ? "Create Custom Category" : "Edit Category" + }); + new import_obsidian3.Setting(contentEl).setName("Category ID").setDesc("Unique identifier for this category (lowercase, no spaces)").addText((text) => text.setPlaceholder("my-custom-category").setValue(this.category.id).onChange((value) => { + this.category.id = value.toLowerCase().replace(/\s+/g, "-"); + })); + new import_obsidian3.Setting(contentEl).setName("Category Name").setDesc("Display name for this category").addText((text) => text.setPlaceholder("My Custom Category").setValue(this.category.name).onChange((value) => { + this.category.name = value; + })); + new import_obsidian3.Setting(contentEl).setName("Default Wrapping").setDesc('Default mermaid syntax to wrap elements (e.g., "flowchart TD", "sequenceDiagram")').addText((text) => text.setPlaceholder("flowchart TD").setValue(this.category.defaultWrapping).onChange((value) => { + this.category.defaultWrapping = value; + })); + new import_obsidian3.Setting(contentEl).setName("Additional Wrappings (Optional)").setDesc('Comma-separated list of alternative wrappings (e.g., "flowchart LR, flowchart TB")').addText((text) => text.setPlaceholder("flowchart LR, flowchart TB").setValue(this.category.wrappings ? this.category.wrappings.join(", ") : "").onChange((value) => { + if (value.trim()) { + this.category.wrappings = value.split(",").map((w) => w.trim()).filter((w) => w); + } else { + this.category.wrappings = null; + } + })); + new import_obsidian3.Setting(contentEl).setName("Sort Order").setDesc("Determines the order in which categories appear").addText((text) => text.setPlaceholder("0").setValue(this.category.sortOrder.toString()).onChange((value) => { + const num = parseInt(value); + if (!isNaN(num)) { + this.category.sortOrder = num; + } + })); + const buttonContainer = contentEl.createDiv("modal-button-container"); + buttonContainer.style.display = "flex"; + buttonContainer.style.justifyContent = "flex-end"; + buttonContainer.style.gap = "10px"; + buttonContainer.style.marginTop = "20px"; + const cancelButton = buttonContainer.createEl("button", { text: "Cancel" }); + cancelButton.onclick = () => this.close(); + const saveButton = buttonContainer.createEl("button", { + text: this.isNewCategory ? "Create" : "Save", + cls: "mod-cta" + }); + saveButton.onclick = () => this.save(); + } + save() { + if (!this.category.id.trim()) { + alert("Category ID is required"); + return; + } + if (!this.category.name.trim()) { + alert("Category name is required"); + return; + } + if (!this.category.defaultWrapping.trim()) { + alert("Default wrapping is required"); + return; + } + const commonDiagramTypes = [ + "flowchart", + "graph", + "sequenceDiagram", + "classDiagram", + "stateDiagram-v2", + "erDiagram", + "journey", + "gantt", + "pie", + "requirementDiagram", + "gitGraph", + "mindmap", + "timeline", + "quadrantChart", + "C4Context", + "sankey-beta", + "xychart-beta", + "packet-beta", + "kanban", + "block-beta", + "architecture-beta" + ]; + const wrapping = this.category.defaultWrapping.trim().split(/\s+/)[0]; + if (!commonDiagramTypes.includes(wrapping)) { + const shouldContinue = confirm(`Warning: "${wrapping}" is not a recognized Mermaid diagram type. This may cause rendering errors. Are you sure you want to continue?`); + if (!shouldContinue) { + return; + } + } + if (this.isNewCategory && this.categoryService.getCategoryById(this.category.id)) { + alert(`A category with ID '${this.category.id}' already exists`); + return; + } + try { + this.onSave(this.category); + this.close(); + } catch (error) { + alert(`Error saving category: ${error.message}`); + } + } +}; + +// src/ui/settingsTab.ts +var MermaidToolsSettingsTab = class extends import_obsidian4.PluginSettingTab { + constructor(_app, _plugin) { + super(_app, _plugin); + this._app = _app; + this._plugin = _plugin; + } + async display() { + await renderSettings(this.containerEl, this._plugin); + } +}; +async function renderSettings(containerEl, plugin) { + const mermaid = await (0, import_obsidian4.loadMermaid)(); + const categoryService = CategoryService.getInstance(); + categoryService.loadCategories(plugin.settings.customCategories, plugin.settings.defaultCategorySortOrders); + containerEl.empty(); + containerEl.createEl("h1", { text: "Mermaid Tools Settings" }); + containerEl.createEl("h2", { text: "Manage Elements & Categories" }); + const buttonsContainer = containerEl.createDiv(); + buttonsContainer.style.marginBottom = "20px"; + buttonsContainer.style.display = "flex"; + buttonsContainer.style.gap = "10px"; + const addElementButton = buttonsContainer.createEl("button", { text: "Add Element" }); + addElementButton.addClass("mod-cta"); + addElementButton.onclick = () => { + const newElement = { + id: crypto.randomUUID(), + description: "New element", + content: `flowchart TD +Start --> Stop`, + categoryId: "flowchart", + sortingOrder: 0, + isPinned: false + }; + const modal = new EditMermaidElementModal(plugin.app, plugin, mermaid, newElement, categoryService); + modal.open(); + modal.onClose = () => { + renderSettings(containerEl, plugin); + }; + }; + const addCategoryButton = buttonsContainer.createEl("button", { text: "Add Category" }); + addCategoryButton.addClass("mod-cta"); + addCategoryButton.onclick = () => { + const modal = new EditCategoryModal(plugin.app, plugin, null, (category) => { + try { + if (category.sortOrder === void 0 || category.sortOrder === null) { + category.sortOrder = categoryService.getNextSortOrder(); + } + categoryService.addCategory(category); + saveAllCategoryChanges(plugin, categoryService); + renderSettings(containerEl, plugin); + } catch (error) { + console.error("Error adding category:", error); + } + }); + modal.open(); + }; + createIntegratedCategorySection(containerEl, plugin, categoryService, mermaid); +} +function saveAllCategoryChanges(plugin, categoryService) { + plugin.settings.customCategories = categoryService.getCustomCategories(); + const defaultCategories = categoryService.getCategories().filter((cat) => !cat.isCustom); + defaultCategories.forEach((cat) => { + plugin.settings.defaultCategorySortOrders[cat.id] = cat.sortOrder; + }); + plugin.saveSettings(); +} +function createIntegratedCategorySection(containerEl, plugin, categoryService, mermaid) { + const allCategories = categoryService.getCategories().sort((a, b) => a.sortOrder - b.sortOrder); + allCategories.forEach((category) => { + const categoryContainer = containerEl.createDiv(); + categoryContainer.addClass("mermaid-tools-category-section"); + categoryContainer.style.marginBottom = "20px"; + categoryContainer.style.border = "1px solid var(--background-modifier-border)"; + categoryContainer.style.borderRadius = "8px"; + categoryContainer.style.padding = "15px"; + const categoryHeader = categoryContainer.createDiv(); + categoryHeader.style.display = "flex"; + categoryHeader.style.alignItems = "center"; + categoryHeader.style.justifyContent = "space-between"; + categoryHeader.style.marginBottom = "10px"; + categoryHeader.style.cursor = "pointer"; + const categoryTitle = categoryHeader.createDiv(); + categoryTitle.style.display = "flex"; + categoryTitle.style.alignItems = "center"; + categoryTitle.style.gap = "10px"; + const expandIcon = categoryTitle.createSpan(); + expandIcon.innerHTML = "\u25BC"; + expandIcon.style.fontSize = "12px"; + expandIcon.style.transition = "transform 0.2s"; + const categoryName = categoryTitle.createEl("h3", { text: category.name }); + categoryName.style.margin = "0"; + categoryName.style.fontSize = "16px"; + const categoryInfo = categoryTitle.createSpan(); + const elementCount = plugin.settings.elements.filter((el) => el.categoryId === category.id).length; + categoryInfo.textContent = `(${elementCount} elements)`; + categoryInfo.style.color = "var(--text-muted)"; + categoryInfo.style.fontSize = "12px"; + const categoryControls = categoryHeader.createDiv(); + categoryControls.style.display = "flex"; + categoryControls.style.gap = "2px"; + const addElementButton = categoryControls.createEl("button"); + addElementButton.title = "Add element to this category"; + addElementButton.style.background = "none"; + addElementButton.style.border = "none"; + addElementButton.style.cursor = "pointer"; + addElementButton.style.padding = "4px"; + addElementButton.style.display = "flex"; + addElementButton.style.alignItems = "center"; + addElementButton.style.borderRadius = "3px"; + addElementButton.innerHTML = ``; + addElementButton.onmouseenter = () => addElementButton.style.backgroundColor = "var(--background-modifier-hover)"; + addElementButton.onmouseleave = () => addElementButton.style.backgroundColor = "transparent"; + addElementButton.onclick = (e) => { + e.stopPropagation(); + const newElement = { + id: crypto.randomUUID(), + description: "New element", + content: `flowchart TD +Start --> Stop`, + categoryId: category.id, + sortingOrder: plugin.settings.elements.filter((el) => el.categoryId === category.id).length, + isPinned: false + }; + const modal = new EditMermaidElementModal(plugin.app, plugin, mermaid, newElement, categoryService); + modal.open(); + modal.onClose = () => { + renderSettings(containerEl, plugin); + }; + }; + const moveUpButton = categoryControls.createEl("button"); + moveUpButton.title = "Move category up"; + moveUpButton.style.background = "none"; + moveUpButton.style.border = "none"; + moveUpButton.style.cursor = "pointer"; + moveUpButton.style.padding = "4px"; + moveUpButton.style.display = "flex"; + moveUpButton.style.alignItems = "center"; + moveUpButton.style.borderRadius = "3px"; + moveUpButton.innerHTML = ``; + moveUpButton.onmouseenter = () => moveUpButton.style.backgroundColor = "var(--background-modifier-hover)"; + moveUpButton.onmouseleave = () => moveUpButton.style.backgroundColor = "transparent"; + moveUpButton.onclick = (e) => { + e.stopPropagation(); + const categories = categoryService.getCategories().sort((a, b) => a.sortOrder - b.sortOrder); + const currentIndex = categories.findIndex((cat) => cat.id === category.id); + if (currentIndex > 0) { + const temp = categories[currentIndex - 1].sortOrder; + categories[currentIndex - 1].sortOrder = category.sortOrder; + category.sortOrder = temp; + categoryService.updateCategory(categories[currentIndex - 1]); + categoryService.updateCategory(category); + saveAllCategoryChanges(plugin, categoryService); + renderSettings(containerEl, plugin); + } + }; + const moveDownButton = categoryControls.createEl("button"); + moveDownButton.title = "Move category down"; + moveDownButton.style.background = "none"; + moveDownButton.style.border = "none"; + moveDownButton.style.cursor = "pointer"; + moveDownButton.style.padding = "4px"; + moveDownButton.style.display = "flex"; + moveDownButton.style.alignItems = "center"; + moveDownButton.style.borderRadius = "3px"; + moveDownButton.innerHTML = ``; + moveDownButton.onmouseenter = () => moveDownButton.style.backgroundColor = "var(--background-modifier-hover)"; + moveDownButton.onmouseleave = () => moveDownButton.style.backgroundColor = "transparent"; + moveDownButton.onclick = (e) => { + e.stopPropagation(); + const categories = categoryService.getCategories().sort((a, b) => a.sortOrder - b.sortOrder); + const currentIndex = categories.findIndex((cat) => cat.id === category.id); + if (currentIndex < categories.length - 1) { + const temp = categories[currentIndex + 1].sortOrder; + categories[currentIndex + 1].sortOrder = category.sortOrder; + category.sortOrder = temp; + categoryService.updateCategory(categories[currentIndex + 1]); + categoryService.updateCategory(category); + saveAllCategoryChanges(plugin, categoryService); + renderSettings(containerEl, plugin); + } + }; + const editButton = categoryControls.createEl("button"); + editButton.title = "Edit category"; + editButton.style.background = "none"; + editButton.style.border = "none"; + editButton.style.cursor = "pointer"; + editButton.style.padding = "4px"; + editButton.style.display = "flex"; + editButton.style.alignItems = "center"; + editButton.style.borderRadius = "3px"; + editButton.innerHTML = ``; + editButton.onmouseenter = () => editButton.style.backgroundColor = "var(--background-modifier-hover)"; + editButton.onmouseleave = () => editButton.style.backgroundColor = "transparent"; + editButton.onclick = (e) => { + e.stopPropagation(); + const modal = new EditCategoryModal(plugin.app, plugin, category, (updatedCategory) => { + try { + categoryService.updateCategory(updatedCategory); + saveAllCategoryChanges(plugin, categoryService); + renderSettings(containerEl, plugin); + } catch (error) { + console.error("Error updating category:", error); + } + }); + modal.open(); + }; + const deleteButton = categoryControls.createEl("button"); + deleteButton.title = "Delete category"; + deleteButton.style.background = "none"; + deleteButton.style.border = "none"; + deleteButton.style.cursor = "pointer"; + deleteButton.style.padding = "4px"; + deleteButton.style.display = "flex"; + deleteButton.style.alignItems = "center"; + deleteButton.style.borderRadius = "3px"; + deleteButton.innerHTML = ``; + deleteButton.onmouseenter = () => deleteButton.style.backgroundColor = "var(--background-modifier-hover)"; + deleteButton.onmouseleave = () => deleteButton.style.backgroundColor = "transparent"; + deleteButton.onclick = (e) => { + e.stopPropagation(); + const elementsInCategory = plugin.settings.elements.filter((el) => el.categoryId === category.id); + if (elementsInCategory.length > 0) { + alert(`Cannot delete category '${category.name}' because it contains ${elementsInCategory.length} element(s). Please move or delete these elements first.`); + return; + } + const confirmMessage = category.isCustom ? `Are you sure you want to delete the category '${category.name}'?` : `Are you sure you want to delete the default category '${category.name}'? This action cannot be undone.`; + if (confirm(confirmMessage)) { + try { + categoryService.deleteCategory(category.id); + if (category.isCustom) { + plugin.settings.customCategories = categoryService.getCustomCategories(); + } + plugin.saveSettings(); + renderSettings(containerEl, plugin); + } catch (error) { + console.error("Error deleting category:", error); + if (!category.isCustom) { + alert(`Cannot delete default category: ${error.message}`); + } + } + } + }; + const elementsContainer = categoryContainer.createDiv(); + elementsContainer.addClass("mermaid-tools-elements-container"); + elementsContainer.style.display = "none"; + let isCollapsed = true; + categoryHeader.onclick = () => { + isCollapsed = !isCollapsed; + elementsContainer.style.display = isCollapsed ? "none" : "block"; + expandIcon.style.transform = isCollapsed ? "rotate(-90deg)" : "rotate(0deg)"; + }; + expandIcon.style.transform = "rotate(-90deg)"; + renderCategoryElements(category, plugin, elementsContainer, mermaid, categoryService); + }); +} +function renderCategoryElements(category, plugin, parentEl, mermaid, categoryService) { + const elements = plugin.settings.elements.filter((e) => e.categoryId === category.id).sort((a, b) => a.sortingOrder - b.sortingOrder); + if (elements.length === 0) { + const emptyMessage = parentEl.createDiv(); + emptyMessage.textContent = "No elements in this category"; + emptyMessage.style.color = "var(--text-muted)"; + emptyMessage.style.fontStyle = "italic"; + emptyMessage.style.padding = "10px"; + return; + } + elements.forEach((element, index) => { + const settingContainer = parentEl.createDiv("mermaid-tools-element-container"); + settingContainer.style.marginBottom = "10px"; + settingContainer.style.padding = "10px"; + settingContainer.style.backgroundColor = "var(--background-secondary)"; + settingContainer.style.borderRadius = "5px"; + const setting = new import_obsidian4.Setting(settingContainer); + setting.setName(element.description); + setting.addExtraButton((cb) => { + cb.setIcon("edit").setTooltip("edit element").onClick(() => { + const modal = new EditMermaidElementModal(plugin.app, plugin, mermaid, element, categoryService); + modal.open(); + modal.onClose = () => { + const settingsContainer = parentEl.closest(".vertical-tab-content"); + if (settingsContainer) + renderSettings(settingsContainer, plugin); + }; + }); + }); + setting.addExtraButton((cb) => { + cb.setIcon("copy").setTooltip("create a duplicate of this element").onClick(() => { + const duplicate = { + id: crypto.randomUUID(), + categoryId: element.categoryId, + description: element.description + " (copy)", + content: element.content, + sortingOrder: plugin.settings.elements.filter((el) => el.categoryId === element.categoryId).length, + isPinned: element.isPinned + }; + plugin._mermaidElementService.saveElement(duplicate, plugin); + plugin.saveSettings(); + const settingsContainer = parentEl.closest(".vertical-tab-content"); + if (settingsContainer) + renderSettings(settingsContainer, plugin); + }); + }); + setting.addExtraButton((cb) => { + cb.setIcon("arrow-up").setTooltip("move element up in the sidebar").onClick(() => { + if (index > 0) { + const temp = elements[index - 1].sortingOrder; + elements[index - 1].sortingOrder = element.sortingOrder; + element.sortingOrder = temp; + plugin.settings.elements = plugin.settings.elements.filter((el) => el.categoryId !== category.id).concat(elements); + plugin.saveSettings(); + const settingsContainer = parentEl.closest(".vertical-tab-content"); + if (settingsContainer) + renderSettings(settingsContainer, plugin); + } + }); + }); + setting.addExtraButton((cb) => { + cb.setIcon("arrow-down").setTooltip("move element down in the sidebar").onClick(() => { + if (index < elements.length - 1) { + const temp = elements[index + 1].sortingOrder; + elements[index + 1].sortingOrder = element.sortingOrder; + element.sortingOrder = temp; + plugin.settings.elements = plugin.settings.elements.filter((el) => el.categoryId !== category.id).concat(elements); + plugin.saveSettings(); + const settingsContainer = parentEl.closest(".vertical-tab-content"); + if (settingsContainer) + renderSettings(settingsContainer, plugin); + } + }); + }); + setting.addExtraButton((cb) => { + cb.setIcon("trash-2").setTooltip("delete element").onClick(() => { + plugin.settings.elements = plugin.settings.elements.filter((e) => e.id !== element.id); + plugin.saveSettings(); + const settingsContainer = parentEl.closest(".vertical-tab-content"); + if (settingsContainer) + renderSettings(settingsContainer, plugin); + }); + }); + }); +} + +// src/ui/toolbarView/mermaidToolbarView.ts +var import_obsidian6 = require("obsidian"); + +// src/ui/toolbarView/viewHelpers.ts +var import_obsidian5 = require("obsidian"); +var TOOLBAR_ELEMENT_CLASS_NAME = "mermaid-toolbar-element"; +var TOOLBAR_ELEMENTS_CONTAINER_CLASS_NAME = "mermaid-toolbar-elements-container"; +var TOOLBAR_ELEMENTS_CONTAINER_ID = "mermaid-toolbar-elements-container-id"; +async function createMermaidToolbar(topRowButtons, items, selectedCategoryId, onCategoryChanged, onElementClick, categoryService) { + const container = document.createElement("div"); + const topRow = container.createDiv(); + topRow.addClass("mermaid-toolbar-top-row"); + const elementsContainer = container.createDiv(); + elementsContainer.addClass(TOOLBAR_ELEMENTS_CONTAINER_CLASS_NAME); + elementsContainer.setAttr("id", TOOLBAR_ELEMENTS_CONTAINER_ID); + createDropdown(topRow, elementsContainer, items, selectedCategoryId, onCategoryChanged, onElementClick, categoryService); + createTopRowBtns(topRow, topRowButtons); + await recreateElementsSection(elementsContainer, selectedCategoryId, items, onElementClick, categoryService); + return container; +} +function createTopRowBtns(parentEl, buttons) { + buttons.forEach((btn) => { + const b = new import_obsidian5.ButtonComponent(parentEl).setClass("clickable-icon").setIcon(btn.iconName).setTooltip(btn.tooltip).onClick(btn.callback); + }); +} +function createDropdown(parentEl, elementsContainer, items, selectedCategoryId, onSelectionChanged, onElClick, categoryService) { + const categories = categoryService.getCategories(); + const dropdown = new import_obsidian5.DropdownComponent(parentEl); + categories.forEach((category) => { + dropdown.addOption(category.id, category.name); + }); + dropdown.setValue(selectedCategoryId); + dropdown.onChange((val) => { + onSelectionChanged(val); + recreateElementsSection(elementsContainer, val, items, onElClick, categoryService); + }); +} +async function recreateElementsSection(sectionContainer, categoryId, items, onElClick, categoryService) { + sectionContainer.innerHTML = ""; + const elemService = new MermaidElementService(); + const mermaid = await (0, import_obsidian5.loadMermaid)(); + const filteredSortedItems = items.filter((i) => i.categoryId === categoryId).sort((a, b) => a.sortingOrder - b.sortingOrder); + filteredSortedItems.forEach(async (elem, index) => { + const el = createToolbarElement(sectionContainer); + el.id = `mermaid-toolbar-element-${elem.categoryId}-${index}`; + const diagram = elemService.wrapAsCompleteDiagram(elem); + console.log(mermaid.detectType(diagram)); + const { svg } = await mermaid.render(el.id, diagram); + el.title = elem.description; + el.innerHTML = svg; + el.onclick = (e) => onElClick(elem.content); + sectionContainer.appendChild(el); + }); +} +function createToolbarElement(parentEl) { + const itemEl = parentEl.createEl("pre"); + itemEl.addClass(TOOLBAR_ELEMENT_CLASS_NAME); + return itemEl; +} + +// src/ui/toolbarView/mermaidToolbarButtons.ts +var MermaidToolbarButton = class { + constructor(tooltip, iconName, callback) { + this.tooltip = tooltip; + this.iconName = iconName; + this.callback = callback; + } +}; + +// src/ui/toolbarView/mermaidToolbarView.ts +var _MermaidToolbarView = class extends import_obsidian6.ItemView { + constructor(leaf, plugin) { + super(leaf); + this.topRowButtons = [ + new MermaidToolbarButton("insert Mermaid code block with sample diagram", "code-2", () => this.insertTextAtCursor(this._plugin._mermaidElementService.getSampleDiagram(this._plugin.settings.selectedCategoryId))), + new MermaidToolbarButton("open Mermaid.js documentation web page", "external-link", () => window.open("https://mermaid.js.org/intro/")), + new MermaidToolbarButton("open settings", "settings", () => { + this.app.setting.open(); + this.app.setting.openTabById("mermaid-tools"); + }) + ]; + this._plugin = plugin; + this.items = plugin.settings.elements; + this.categoryService = CategoryService.getInstance(); + this.categoryService.loadCategories(plugin.settings.customCategories, plugin.settings.defaultCategorySortOrders); + this.containerEl.children[1].addClass("mermaid-toolbar-container"); + } + async onOpen() { + await this.recreateToolbar(this._plugin.settings.selectedCategoryId); + } + async onClose() { + } + async recreateToolbar(selectedCategoryId) { + const container = this.containerEl.children[1]; + container.empty(); + const toolbarElement = await createMermaidToolbar(this.topRowButtons, this.items, selectedCategoryId, async (newCategoryId) => { + this._plugin.settings.selectedCategoryId = newCategoryId; + this._plugin.saveSettings(); + await this.recreateToolbar(this._plugin.settings.selectedCategoryId); + }, (text) => this.insertTextAtCursor(text), this.categoryService); + container.appendChild(toolbarElement); + } + insertTextAtCursor(text) { + this._plugin.insertTextAtCursor(text); + } + getViewType() { + return _MermaidToolbarView.VIEW_TYPE; + } + getDisplayText() { + return _MermaidToolbarView.VIEW_DESCRIPTION; + } + getIcon() { + return TRIDENT_ICON_NAME; + } +}; +var MermaidToolbarView = _MermaidToolbarView; +MermaidToolbarView.VIEW_TYPE = "mermaid-toolbar-view"; +MermaidToolbarView.VIEW_DESCRIPTION = "Mermaid Toolbar"; + +// main.ts +var TRIDENT_ICON_NAME = "trident-custom"; +var MermaidPlugin = class extends import_obsidian7.Plugin { + constructor() { + super(...arguments); + this._mermaidElementService = new MermaidElementService(); + this._textEditorService = new TextEditorService(); + } + async onload() { + await this.loadSettings(); + addTridentIcon(); + this.registerView(MermaidToolbarView.VIEW_TYPE, (leaf) => new MermaidToolbarView(leaf, this)); + this.app.workspace.on("active-leaf-change", (leaf) => { + var _a, _b; + this.activeEditor = (_b = (_a = this.app.workspace.activeEditor) == null ? void 0 : _a.editor) != null ? _b : this.activeEditor; + }); + this.addRibbonIcon(TRIDENT_ICON_NAME, "Open Mermaid Toolbar", () => { + this.activateView(); + }); + this.addCommand({ + id: "open-toolbar", + name: "Open Toolbar View", + callback: () => { + this.activateView(); + } + }); + this.addSettingTab(new MermaidToolsSettingsTab(this.app, this)); + } + async onunload() { + this.app.workspace.detachLeavesOfType(MermaidToolbarView.VIEW_TYPE); + } + async loadSettings() { + this.settings = Object.assign({}, MermaidPluginSettings.DefaultSettings(), await this.loadData()); + this.addNewCategories(); + } + addNewCategories() { + if (!this.settings.elements.some((x) => x.categoryId === "mindmap")) { + this.settings.elements.push(...mindMapElements); + console.log("[Mermaid Tools] added Mindmap elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "timeline")) { + this.settings.elements.push(...timelineElements); + console.log("[Mermaid Tools] added Timeline elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "quadrantChart")) { + this.settings.elements.push(...quadrantElements); + console.log("[Mermaid Tools] added QuadrantChart elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "c4Diagram")) { + this.settings.elements.push(...c4DiagramElements); + console.log("[Mermaid Tools] added C4 diagram elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "packet")) { + this.settings.elements.push(...packetElements); + console.log("[Mermaid Tools] added Packet elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "kanban")) { + this.settings.elements.push(...kanbanElements); + console.log("[Mermaid Tools] added Kanban elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "block")) { + this.settings.elements.push(...blockDiagramElements); + console.log("[Mermaid Tools] added Block elements"); + } + if (!this.settings.elements.some((x) => x.categoryId === "architecture")) { + this.settings.elements.push(...architectureElements); + console.log("[Mermaid Tools] added Architecture diagram elements"); + } + } + async saveSettings() { + await this.saveData(this.settings); + await this.activateView(); + } + async activateView() { + var _a; + this.app.workspace.detachLeavesOfType(MermaidToolbarView.VIEW_TYPE); + if (this.app.workspace === null) + return; + await ((_a = this.app.workspace.getRightLeaf(false)) == null ? void 0 : _a.setViewState({ + type: MermaidToolbarView.VIEW_TYPE, + active: true + })); + this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(MermaidToolbarView.VIEW_TYPE)[0]); + } + insertTextAtCursor(text) { + this._textEditorService.insertTextAtCursor(this.activeEditor, text); + } +}; + +/* nosourcemap */ \ No newline at end of file diff --git a/.obsidian/plugins/mermaid-tools/manifest.json b/.obsidian/plugins/mermaid-tools/manifest.json new file mode 100644 index 0000000..6a4fd0e --- /dev/null +++ b/.obsidian/plugins/mermaid-tools/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "mermaid-tools", + "name": "Mermaid Tools", + "version": "1.3.0", + "minAppVersion": "1.4.0", + "description": "Improved Mermaid.js experience for Obsidian: visual toolbar with common elements & more", + "author": "dartungar", + "authorUrl": "https://dartungar.com", + "fundingUrl": "https://www.paypal.com/paypalme/dartungar", + "isDesktopOnly": false +} diff --git a/.obsidian/plugins/mermaid-tools/styles.css b/.obsidian/plugins/mermaid-tools/styles.css new file mode 100644 index 0000000..e117528 --- /dev/null +++ b/.obsidian/plugins/mermaid-tools/styles.css @@ -0,0 +1,149 @@ +.mermaid-toolbar-container, .mermaid-toolbar-container * { + max-width: 100%; + max-height: 100%; +} + +.mermaid-toolbar-top-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; +} + +.mermaid-toolbar-elements-container { + padding-top: 1rem; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.mermaid-toolbar-element { + font-size: var(--font-ui-small); + cursor: pointer; + padding: 2px 2px 2px 5px; + border-radius: 3px; + flex: 1 0 auto; +} + +.mermaid-toolbar-element:hover { + background-color: var(--interactive-hover); +} + +.mermaid-tools-element-category-header::before { + content: "▼ "; + font-size: 70%; + padding-bottom: 2px; +} + +.mermaid-tools-element-category-header.collapsed::before { + content: "▶ "; + font-size: 70%; + padding-bottom: 2px; +} + +.mermaid-tools-element-container { + padding-top: 6px; + border-bottom: var(--border-width) solid var(--color-base-35); +} + +.mermaid-tools-edit-element-modal > div { + margin-bottom: 0.5rem; +} + +.mermaid-tools-edit-element-modal label { + margin-right: 1rem; +} + +/* Custom Category Management Styles */ +.mermaid-tools-category-management { + margin-bottom: 2rem; + padding: 1rem; + border: 1px solid var(--color-base-25); + border-radius: 8px; + background-color: var(--color-base-00); +} + +.mermaid-tools-category-management h3 { + margin-top: 1rem; + margin-bottom: 0.5rem; + color: var(--text-accent); +} + +.mermaid-tools-category-management button.mod-cta { + margin-bottom: 1rem; +} + +/* Edit Category Modal Styles */ +.mermaid-tools-edit-category-modal { + min-width: 500px; +} + +.mermaid-tools-edit-category-modal .setting-item { + padding: 8px 0; + border: none; +} + +.mermaid-tools-edit-category-modal .setting-item-info { + flex-grow: 1; + margin-right: 12px; +} + +.mermaid-tools-edit-category-modal .setting-item-name { + font-weight: 600; + color: var(--text-normal); +} + +.mermaid-tools-edit-category-modal .setting-item-description { + color: var(--text-muted); + font-size: var(--font-ui-smaller); +} + +.mermaid-tools-edit-category-modal input, +.mermaid-tools-edit-category-modal textarea { + width: 100%; + padding: 4px 8px; + border: 1px solid var(--color-base-30); + border-radius: 4px; + background-color: var(--color-base-00); + color: var(--text-normal); +} + +.mermaid-tools-edit-category-modal input:focus, +.mermaid-tools-edit-category-modal textarea:focus { + border-color: var(--color-accent); + outline: none; + box-shadow: 0 0 0 2px var(--color-accent-2); +} + +.modal-button-container { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--color-base-25); +} + +.modal-button-container button { + padding: 6px 16px; + border: 1px solid var(--color-base-30); + border-radius: 4px; + background-color: var(--color-base-10); + color: var(--text-normal); + cursor: pointer; + font-size: var(--font-ui-small); +} + +.modal-button-container button:hover { + background-color: var(--color-base-20); +} + +.modal-button-container button.mod-cta { + background-color: var(--color-accent); + color: var(--text-on-accent); + border-color: var(--color-accent); +} + +.modal-button-container button.mod-cta:hover { + background-color: var(--color-accent-hover); +} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index b7216c6..44fa639 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -4,24 +4,39 @@ "type": "split", "children": [ { - "id": "6bd8d5cd0b9fe171", + "id": "fab387a4144c5e24", "type": "tabs", "children": [ { - "id": "c5205a20b44266cb", + "id": "91f1f775b766a709", "type": "leaf", "state": { "type": "markdown", "state": { - "file": "README.md", + "file": "AUDIO/KV2.md", "mode": "source", "source": false }, "icon": "lucide-file", - "title": "README" + "title": "KV2" + } + }, + { + "id": "f4b542a9ab6d55a4", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "PhenixRTS/frontend-draining/19 days draining.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "19 days draining" } } - ] + ], + "currentTab": 1 } ], "direction": "vertical" @@ -53,7 +68,7 @@ "state": { "type": "search", "state": { - "query": "", + "query": "dec", "matchingCase": false, "explainSearch": false, "collapseAll": false, @@ -74,11 +89,12 @@ "title": "Bookmarks" } } - ] + ], + "currentTab": 1 } ], "direction": "horizontal", - "width": 300 + "width": 200 }, "right": { "id": "755a200fbb7115db", @@ -166,10 +182,25 @@ "bases:Create new base": false } }, - "active": "c5205a20b44266cb", + "active": "f4b542a9ab6d55a4", "lastOpenFiles": [ - "ESP32/Setting up ESP32.md", + "PhenixRTS/frontend-draining/THE QUERY.md", + "PhenixRTS/frontend-draining/19 days draining.md", + "AUDIO/KV2.md", + "WebSocket Lifecycle Sequence Diagram.md", + "AUDIO", + "Untitled.md", + "Untitled 2.md", "README.md", + "PhenixRTS/chat/Demo.md", + "PhenixRTS/frontend-draining/Potential causes.md", + "PhenixRTS/chat", + "PhenixRTS/frontend-draining/WebSocket Lifecycle Sequence Diagram.md", + "ZSH/Documentation.md", + "ZSH", + "ESP32/Setting up ESP32 Development Environment.md", + "PhenixRTS/frontend-draining", + "PhenixRTS", "ESP32" ] } \ No newline at end of file diff --git a/AUDIO/KV2.md b/AUDIO/KV2.md new file mode 100644 index 0000000..9ef8e56 --- /dev/null +++ b/AUDIO/KV2.md @@ -0,0 +1,89 @@ +This business plan outlines the acquisition of a flagship **Kv2 Audio VHD (Very High Definition)** system. + +This configuration is designed for high-impact electronic music events, outdoor festivals, and large-scale premium corporate AV. With 12 double-18" subwoofers, this system is capable + + of delivering extreme sound pressure levels (SPL) while maintaining the audiophile clarity Kv2 is famous for. + +### 1. Executive Summary +**Objective:** To acquire a turnkey high-performance audio system capable of covering audiences of 2,000–5,000 people with distinct "club-like" pressure and fidelity. +**Total Estimated Investment:** **$230,000 – $260,000** +**Primary Target Market:** Electronic Music Festivals, High-End Touring DJs, Large Nightclub Installs, and Boutique Production Rental. + +### 2. Equipment Configuration & Capital Expenditure (CapEx) +The VHD system is unique because it requires specific proprietary amplification/control units to function. + +**The "Main" System Ratio:** +For electronic music, a ratio of 1 Top to 3 Subs is aggressive and desirable. +* **Tops:** 4x VHD2.0 (Long throw mid/highs) +* **Subs:** 12x VHD2.18J (The dual 18" low frequency units) +* **Monitors:** A "Texas Headphones" style DJ booth (2x EX12 Tops + 2x EX2.5 MkII Subs). + +| Item | Qty | Unit Estimate ($) | Total Estimate ($) | Notes | +| :----------------------- | :------- | :----------------------- | :----------------- | :-------------------------------------- | +| **Main PA Tops** | | | | | +| VHD2.0 Mid/High Cabinet | 4 | $9,500 | $38,000 | 2 per side (Main L/R) | +| VHD2000 Amp/Controller | 4 | $7,000 | $28,000 | 1 amp per top | +| **Main PA Subs** | | | | | +| VHD2.18J Dual 18" Sub | 12 | $6,500 | $78,000 | 6 per side | +| VHD3200 Amp/Controller | 6 | $6,000 | $36,000 | 1 amp drives 2 subs | +| **DJ Monitoring** | | | | | +| EX12 Active Top | 2 | $3,200 | $6,400 | High output active top | +| EX2.5 MkII Active Sub | 2 | $4,500 | $9,000 | High output active sub | +| **Infrastructure** | | | | | +| Cabling (EP6 Heavy Duty) | 1 | $8,000 | $8,000 | VHD requires specific heavy gauge cable | +| Power Distro (3-Phase) | 1 | $5,000 | $5,000 | Amps require significant current | +| Cases & Rigging | 1 | $15,000 | $15,000 | Touring grade protection | +| Total CapEx | $223,400 | (Excluding tax/shipping) | | | + +### 3. Market Analysis & Strategy + +#### The Competitive Advantage +Unlike competitors using Line Arrays (L-Acoustics, d&b), the Kv2 VHD is a **Point Source** system. +1. **Setup Time:** A VHD stack can be set up by 2 people in 20 minutes. Line arrays require fly-bars, motors, and complex angle calculations. +2. **Sound Quality:** Electronic music purists prefer point source for its immediate transient response and lack of comb filtering. +3. **Throw:** One VHD2.0 cabinet can throw intelligible audio over 300 ft. + +#### Rental Rates (Revenue Model) +Industry standard rental rates are typically 3% to 5% of the equipment value per show day. + +* **Full System Rate (Festival Package):** $6,500 – $8,500 per day +* **Split System Rate (2 Smaller Rooms):** $3,500 per room/day +* **Dry Hire (B2B cross-rental):** $4,000 per day + +### 4. Financial Projections (ROI) + +**Assumptions:** +* **Average Rental Rate:** $5,500 (blended rate allowing for discounts/packages) +* **Crew/Logistics Cost per show:** -$1,500 (trucking and system tech) +* **Net Profit per Show:** $4,000 + +**Utilization Strategy:** +We aim for 4 major deployments per month (weekends). + +**Monthly Revenue Calculation:** +\[ \text{Monthly Net} = 4 \text{ shows} \times \$4,000 = \$16,000 \] + +**Break-Even Analysis:** +\[ \text{Break Even Point} = \frac{\text{Total CapEx}}{\text{Net Profit per Show}} \] + +\[ \frac{225,000}{4,000} \approx 56.25 \text{ Shows} \] + +At 4 shows a month, the system pays for itself in roughly **14 months**. + +### 5. Operational Logistics + +* **Power Requirements:** The VHD3200 amps are hungry. You will need a dedicated 3 +* phase power distro (likely 60A or 100A 3-phase) to run 12 subs and tops without tripping breakers on bass drops. +* **Transport:** This system will **not** fit in a cargo van. The VHD2.18J is massive (approx 3.5 ft wide). You will need a **16ft or 24ft Box Truck** with a lift gate. +* **Weight:** A VHD2.18J weighs nearly 200 lbs. Everything must be on wheels. + +### 6. Risk Assessment + +* **Rider Acceptability:** Kv2 is a boutique brand. Some touring engineers for pop/rock acts may demand L-Acoustics or d&b exclusively. + * *Mitigation:* Market heavily to the **Electronic/Techno/House** scenes where Kv2 is considered "God-tier" audio. +* **Scalability:** Unlike line arrays, you cannot simply "add more boxes" to widen coverage indefinitely due to comb filtering. + * *Mitigation:* This system is designed for focused, long-throw energy. It is perfect for dance floors, not wide, dispersed crowds. + +### 7. Conclusion +Purchasing 12 VHD2.18J units creates a "Bass-First" system that outperforms almost any line array of equivalent cost in terms of raw visceral impact. By targeting the niche electronic music market, this asset can command premium rental rates with lower setup labor costs than traditional arrays. + diff --git a/ESP32/Setting up ESP32.md b/ESP32/Setting up ESP32 Development Environment.md similarity index 100% rename from ESP32/Setting up ESP32.md rename to ESP32/Setting up ESP32 Development Environment.md diff --git a/PhenixRTS/.DS_Store b/PhenixRTS/.DS_Store new file mode 100644 index 0000000..0f21033 Binary files /dev/null and b/PhenixRTS/.DS_Store differ diff --git a/PhenixRTS/chat/Demo.md b/PhenixRTS/chat/Demo.md new file mode 100644 index 0000000..a75a2a2 --- /dev/null +++ b/PhenixRTS/chat/Demo.md @@ -0,0 +1,20 @@ + +```sh +curl https://pcast-stg.phenixrts.com/pcast/channel/us-central%23phenixrts.com-alex.zinn%23frontendWebsocket.LnQnpYeK26hH/message \ +-u "${APPLICATION_ID}:${SECRET_STG}" \ +-H "Accept: application/json" \ +-H "Content-Type: application/json" \ +-X PUT \ +-d '{ + "message": { + "from": { + "screenName": "Me", + "role": "Moderator", + "lastUpdate": 0 + }, + "mimeType": "text/plain", + "message": "This is my chat message", + "tags": ["my-tag", "my-other-tag"] + } + }' +``` \ No newline at end of file diff --git a/PhenixRTS/frontend-draining/19 days draining.md b/PhenixRTS/frontend-draining/19 days draining.md new file mode 100644 index 0000000..0c605ee --- /dev/null +++ b/PhenixRTS/frontend-draining/19 days draining.md @@ -0,0 +1,227 @@ +Hostname: `frontend-us-northeast-3-vm4w` +InstanceId: `us-northeast#us-east4-c.Iqb8nNAA` + + +```SQL +DECLARE + hostName STRING DEFAULT "frontend-us-northeast-3-vm4w"; +DECLARE + lookbackDays INT64 DEFAULT 41; +DECLARE + start_time TIMESTAMP DEFAULT TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -lookbackDays DAY); + + ----------------------------------------------------------------- + -- Step 1: Find the most recent log message indicating draining connections for a specific host +WITH LatestDrainLog AS ( + SELECT Message + FROM `phenix-pcast.pcast_logs_us.syslog` + WHERE + Timestamp > start_time + AND Facility = 'platform' + AND HostName = hostName + AND Message LIKE 'Websocket connectionids preventing drain%' + ORDER BY Timestamp DESC LIMIT 1 +-- Step 2: Extract all connection IDs from that single log message +DrainingConnectionIds AS ( + SELECT connectionId + FROM + LatestDrainLog, + UNNEST(REGEXP_EXTRACT_ALL(Message, r"'([^']*)'")) AS connectionId ) +), +-- Step 3: Find all logs that associate sessions with connections +SessionConnections AS ( + Timestamp + SELECT + REGEXP_EXTRACT(Message, r'\] \[(.*?)\] Session started with connection') AS sessionId, + REGEXP_EXTRACT(Message, r'connection \[(.*?)\] and roles') AS connectionId, + FROM `phenix-pcast.pcast_logs_us.syslog` + WHERE Timestamp > start_time + AND Facility = 'platform' + AND Message LIKE '%Session started with connection%' + AND REGEXP_EXTRACT(Message, r'\] \[(.*?)\] Session started with connection') IS NOT NULL + AND REGEXP_EXTRACT(Message, r'connection \[(.*?)\] and roles') IS NOT NULL + ), + -- Pattern 2: "Session [sessionId] has a new connection [connectionId], previously [oldConnectionId]" +SessionNewConnections AS ( +SELECT + Timestamp + REGEXP_EXTRACT(Message, r'Session \[(.*?)\] has a new connection') AS sessionId, + REGEXP_EXTRACT(Message, r'connection \[(.*?)\], previously') AS connectionId +FROM `phenix-pcast.pcast_logs_us.syslog` +WHERE + Timestamp > start_time + AND Facility = 'platform' + AND Message LIKE '%Session%has a new connection%' + AND REGEXP_EXTRACT(Message, r'Session \[(.*?)\] has a new connection') IS NOT NULL + AND REGEXP_EXTRACT(Message, r'connection \[(.*?)\], previously') IS NOT NULL +AllSessionConnections AS ( + SELECT Timestamp, sessionId, connectionId + FROM SessionConnections + UNION DISTINCT + SELECT Timestamp, sessionId, connectionId + FROM SessionNewConnections ) +) +SELECT * +FROM AllSessionConnections +ORDER BY Timestmap + +``` + + + + + + +------ + +```SQL +DECLARE TargetHostName STRING DEFAULT "frontend-us-northeast-3-vm4w"; +DECLARE TargetInstanceId STRING DEFAULT "us-northeast#us-east4-c.Iqb8nNAA"; +DECLARE TargetConnectionId STRING DEFAULT "us-central#ZQiUCdymrZHbmeF12NhUZQ8xZZXxviWD"; +------------ +With ConnectionIdsPreventingDrain AS ( +SELECT + Message +FROM + `phenix-pcast.pcast_logs_us.syslog` +WHERE + Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -2 MINUTE) + AND Facility = 'platform' + AND HostName = hostName + AND Message LIKE 'Websocket connectionids preventing drain%' +ORDER BY + Timestamp DESC +LIMIT + 1 + ) +``` + + + + +```SQL +DECLARE HostName STRING DEFAULT "frontend-us-northeast-3-vm4w"; + +CREATE TEMPORARY FUNCTION GET_METRIC_VALUE(statusJson STRING, metricName STRING) RETURNS FLOAT64 AS ( + COALESCE( + ( + SELECT + CAST( + JSON_EXTRACT_SCALAR(metric, '$.value') AS FLOAT64 + ) + FROM + UNNEST( + JSON_EXTRACT_ARRAY(JSON_EXTRACT(statusJson, '$.load')) + ) AS metric + WHERE + JSON_EXTRACT_SCALAR(metric, '$.name') = metricName + LIMIT + 1 + ), + 0 + ) +); +-- WITH LatestInstanceMetricForHost AS ( + SELECT + Timestamp, + Status, + (GET_METRIC_VALUE(Status, 'uptime/os/seconds') / 3600 ) AS UptimeHours, + (GET_METRIC_VALUE(Status, 'status/seconds') / 3600 ) AS DrainingHours + FROM `phenix-pcast.pcast.InstanceMetrics` + WHERE Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -1 MINUTE) + AND Hostname = Hostname + QUALIFY ROW_NUMBER() OVER (PARTITION BY InstanceId ORDER BY Timestamp DESC) = 1 + ORDER BY Timestamp DESC + LIMIT 1 + +``` + + + + + +```SQL +DECLARE TargetInstanceId STRING DEFAULT "us-northeast#us-east4-c.Iqb8nNAA"; + +CREATE TEMPORARY FUNCTION GET_METRIC_VALUE(statusJson STRING, metricName STRING) RETURNS FLOAT64 AS ( + COALESCE( + ( + SELECT + CAST( + JSON_EXTRACT_SCALAR(metric, '$.value') AS FLOAT64 + ) + FROM + UNNEST( + JSON_EXTRACT_ARRAY(JSON_EXTRACT(statusJson, '$.load')) + ) AS metric + WHERE + JSON_EXTRACT_SCALAR(metric, '$.name') = metricName + LIMIT + 1 + ), + 0 + + + ) +); + +SELECT + Timestamp, + Status, + InstanceId, + HostName, + Health, + HealthAlert, + FORMAT('%.2f', (GET_METRIC_VALUE(Status, 'uptime/os/seconds') / 3600 )) AS UptimeHours, + FORMAT('%.2f', (GET_METRIC_VALUE(Status, 'status/seconds') / 3600 )) AS DrainingHours, + GET_METRIC_VALUE(Status, 'connections/open') AS connectionsOpen, + GET_METRIC_VALUE(Status, 'clients') AS clients, + GET_METRIC_VALUE(Status, 'clients/subscriptions') AS clientsSubscriptions, + GET_METRIC_VALUE(Status, 'clients/replay/events') AS clientsReplayEvents, + GET_METRIC_VALUE(Status, 'mq/incoming/pending') AS mqIncomingPending, + GET_METRIC_VALUE(Status, 'mq/outgoing/pending') AS mqOutgoingPending, + GET_METRIC_VALUE(Status, 'mq/incoming/rate') AS mqIncomingRate, + +FROM `phenix-pcast.pcast.InstanceMetrics` +WHERE Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -1 MINUTE) +AND InstanceId = TargetInstanceId +QUALIFY ROW_NUMBER() OVER (PARTITION BY InstanceId ORDER BY Timestamp DESC) = 1 +ORDER BY Timestamp DESC +LIMIT 1 + +``` + + +Using +```SQL +SELECT + Timestamp, + Category, + Severity, + Message, + HostName, + Region, + Zone, +FROM + `phenix-pcast.pcast_logs_us.syslog` +WHERE + Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -90 DAY) + + AND Facility = 'platform' + AND Service = 'frontend' + AND Message LIKE "%Drain instance%" + AND HostName = 'frontend-us-northeast-3-vm4w' +ORDER BY + Timestamp + +``` + +`HostName`: `frontend-us-northeast-3-vm4w` + +Went into draining +`2025-11-03 19:49:37.012998 UTC` - `[us-northeast#us-east4-c.Iqb8nNAA] Drain instance (undoable=[false])` + +`Skipping ping as previous ping is still pending since [1760474289916]` + 1760474289916 --> `2025-10-14T20:38:09.916Z` + + diff --git a/PhenixRTS/frontend-draining/Potential causes.md b/PhenixRTS/frontend-draining/Potential causes.md new file mode 100644 index 0000000..2a8dfd9 --- /dev/null +++ b/PhenixRTS/frontend-draining/Potential causes.md @@ -0,0 +1,37 @@ + +- Close path can loop forever if peer never finishes closing. When `close()` is called we just invoke `socket.close()` and wait for a future `disconnectDelegate` If the FIN/ACK never arrives, we keep the entry and never fall back to terminate() (despite adding that method). Consider scheduling a timeout so that a close automatically escalates to terminate() and disconnectDelegate cleanup. + + +```sql +-- Check for send errors during drain +SELECT + timestamp, + message + FROM + `phenix-pcast.pcast_logs_us.syslog` +WHERE +Timestamp > TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -10 DAY) + AND Facility = 'platform' + AND HostName = "frontend-australia-southeast-1-k3ms" + AND ( + message LIKE '%Failed to send%' + OR message LIKE '%not opened%' + OR message LIKE '%not found%' + ) +ORDER BY timestamp DESC +LIMIT 100; +``` + + +| id | Timestamp | Message | Hostname | Thread | +| :-- | :----------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | :----- | +| 0 | 2025-11-22 14:09:32.453398 UTC | [australia-southeast#WOkAeFYOsT3gWMwVbTLpcHYqIEYttN9k] Rejecting message [chat.RoomEvent] for closed client: Client MQ adapter for australia-southeast#WOkAeFYOsT3gWMwVbTLpcHYqIEYttN9k not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 1 | 2025-11-22 14:04:56.692624 UTC | [australia-southeast#f0AOMhXqggfsFmog5FYfDOfHzl4guVTp] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#f0AOMhXqggfsFmog5FYfDOfHzl4guVTp not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 2 | 2025-11-22 14:04:55.976509 UTC | [australia-southeast#01let42yTTGJJViCmDNG5YkfPAhr1Fk5] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#01let42yTTGJJViCmDNG5YkfPAhr1Fk5 not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 3 | 2025-11-22 14:03:48.854516 UTC | [australia-southeast#x1o42d2VicP5Cfq9pYH1k9eW6sKi5djC] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#x1o42d2VicP5Cfq9pYH1k9eW6sKi5djC not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 4 | 2025-11-22 10:24:52.469294 UTC | [australia-southeast#TVhflpoNzOK6dSloQCFqoYcY1ttbiOiM] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#TVhflpoNzOK6dSloQCFqoYcY1ttbiOiM not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 5 | 2025-11-22 10:00:42.335501 UTC | [australia-southeast#Kb5C372wLecwyTihj1yO1jZT5t6ghNmU] Rejecting message [chat.RoomEvent] for closed client: Client MQ adapter for australia-southeast#Kb5C372wLecwyTihj1yO1jZT5t6ghNmU not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 6 | 2025-11-22 06:46:44.278841 UTC | [australia-southeast#f4cOuMd2wMGQ7v8DWLVYbCu6andAXeKO] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#f4cOuMd2wMGQ7v8DWLVYbCu6andAXeKO not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 7 | 2025-11-22 06:37:02.677132 UTC | [australia-southeast#vKYJCpFY1rXi4VT0Rn1A5ufFvbHuUNX4] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#vKYJCpFY1rXi4VT0Rn1A5ufFvbHuUNX4 not found | frontend-australia-southeast-1-k3ms | PID:35 | +| 8 | 2025-11-22 06:36:11.151866 UTC | [australia-southeast#S8vqusQFBG4s7zK4IS1hlqIg5ADM4KwB] Rejecting message [pcast.StreamEnded] for closed client: Client MQ adapter for australia-southeast#S8vqusQFBG4s7zK4IS1hlqIg5ADM4KwB not found | frontend-australia-southeast-1-k3ms | PID:35 | + diff --git a/PhenixRTS/frontend-draining/THE QUERY.md b/PhenixRTS/frontend-draining/THE QUERY.md new file mode 100644 index 0000000..312ce92 --- /dev/null +++ b/PhenixRTS/frontend-draining/THE QUERY.md @@ -0,0 +1,35 @@ + +```SQL +WITH DrainingConnectionIds AS ( + SELECT DISTINCT + connectionId + FROM + `phenix-pcast.pcast_logs_us.syslog`, + UNNEST(REGEXP_EXTRACT_ALL(Message, r"'([^']+)'")) AS connectionId + WHERE + Timestamp >= TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -2 MINUTE) + AND Message LIKE "%Websocket connectionids preventing drain%" +), +SessionStartLogs AS ( + SELECT + REGEXP_EXTRACT(Message, r"\[([^\]]+)\]") AS ApplicationId, + REGEXP_EXTRACT(Message, r"connection \[([^\]]+)\]") AS ConnectionId, + Message + FROM + `phenix-pcast.pcast_logs_us.syslog` + WHERE + Timestamp >= TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -90 DAY) + AND Message LIKE '%Session started with connection%' +) +SELECT + s.ApplicationId, + s.ConnectionId, + REGEXP_EXTRACT_ALL(s.Message, r"connection \[(\]]+)\]")[SAFE_OFFSET(1)] AS SessionId +FROM + SessionStartLogs AS s +INNER JOIN + DrainingConnectionIds AS d +ON + s.ConnectionId = d.connectionId + +``` \ No newline at end of file diff --git a/PhenixRTS/frontend-draining/WebSocket Lifecycle Sequence Diagram.md b/PhenixRTS/frontend-draining/WebSocket Lifecycle Sequence Diagram.md new file mode 100644 index 0000000..cffef65 --- /dev/null +++ b/PhenixRTS/frontend-draining/WebSocket Lifecycle Sequence Diagram.md @@ -0,0 +1,190 @@ +**** +```mermaid +sequenceDiagram + participant Client + participant HTTPServer + participant WebSocketServer + participant ClientMQ + participant ClientMQWorker + participant MQWorker + participant PingInterval as Ping Interval
(20s) + + Note over Client,ClientMQWorker: === Connection Establishment === + Client->>HTTPServer: HTTP Upgrade Request + HTTPServer->>WebSocketServer: upgrade event + WebSocketServer->>WebSocketServer: handleUpgrade() + WebSocketServer->>WebSocketServer: Generate connection.id (32 chars) + WebSocketServer->>WebSocketServer: Get remoteAddress (from X-Forwarded-For) + WebSocketServer->>WebSocketServer: Setup event handlers
(message, close, pong, error) + WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers) + ClientMQAdapter->>ClientMQAdapter: Set __connectionStart
Set __connectionId
Set __meta {headers} + ClientMQAdapter->>ClientMQAdapter: Register in _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = true + ClientMQAdapter->>ClientMQ: emit('connect', connectionId) + ClientMQ->>ClientMQ: Register adapter in _adaptersById + ClientMQ->>ClientMQWorker: emit('connect', connectionId) + ClientMQWorker->>ClientMQWorker: Increment _connectionsTotal
Increment _connectionsOpen + alt Connection limit exceeded (>2000) + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + Note over ClientMQAdapter: See "Server-Initiated Close" below + end + + Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) === + Client->>WebSocketServer: Message (binary or base64) + WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data) + ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol) + alt Unsupported message format + ClientMQAdapter->>ClientMQAdapter: Drop message, log warning + else Socket not connected + ClientMQAdapter->>ClientMQAdapter: Drop message, log warning + else Message type = 'Request' + ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta) + alt Request timeout (>15s) + MQWorker-->>ClientMQAdapter: TimeoutError + ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'} + ClientMQAdapter->>Client: Response (mq.Error) + else Request failed + MQWorker-->>ClientMQAdapter: Error + ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'} + ClientMQAdapter->>Client: Response (mq.Error) + else Request succeeded + MQWorker-->>ClientMQAdapter: {type, payload, wallTime} + ClientMQAdapter->>ClientMQAdapter: Build Response message
(messageType: 'Response', requestId, type, payload) + alt Socket not open + ClientMQAdapter->>ClientMQAdapter: Drop response, log debug + else Socket open + ClientMQAdapter->>Client: Response (binary or base64) + ClientMQAdapter->>ClientMQ: emit('request', connectionId, data) + ClientMQ->>ClientMQWorker: emit('request', connectionId, data) + ClientMQWorker->>ClientMQWorker: Handle subscription updates
(JoinRoom, LeaveRoom, etc.) + end + end + else Message type = 'Response' + alt Request handler not found + ClientMQAdapter->>ClientMQAdapter: Log warning, ignore + else Request handler exists + ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise + end + else Message type = 'Event' + ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported) + end + + Note over Client,ClientMQWorker: === Server-Initiated Request === + ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message) + ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message) + ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter) + ClientMQAdapter->>ClientMQAdapter: Register in _requests map + alt Socket not found + ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found' + else Socket not connected + ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened' + else Socket valid + ClientMQAdapter->>Client: Request (binary or base64) + ClientMQAdapter->>ClientMQAdapter: Start timeout (15s) + Client->>WebSocketServer: Response message + WebSocketServer->>ClientMQAdapter: messageDelegate (Response) + ClientMQAdapter->>ClientMQAdapter: Resolve request promise + ClientMQAdapter-->>ClientMQWorker: Response message + alt Response timeout + ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout + end + end + + Note over Client,ClientMQWorker: === Heartbeat/Ping Flow === + loop Every 20 seconds + PingInterval->>ClientMQAdapter: Ping interval trigger + ClientMQAdapter->>ClientMQAdapter: Iterate all sockets + alt Previous ping still pending + ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning + else Socket has ping() method + alt No pong received (lastPong < lastPing) + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + ClientMQ->>ClientMQWorker: emit('timeout', connectionId) + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + Note over ClientMQAdapter: See "Server-Initiated Close" below + else Pong received + ClientMQAdapter->>Client: ping() + Client->>WebSocketServer: pong() + WebSocketServer->>ClientMQAdapter: pongDelegate(socket) + ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()
Calculate RTT + ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...) + end + else Socket has no ping() method + alt Last ping > 2x heartbeatInterval ago + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + Note over ClientMQAdapter: See timeout flow above + end + end + end + + Note over Client,ClientMQWorker: === Server-Initiated Close === + alt Close reason: timeout + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + else Close reason: drain + ClientMQWorker->>ClientMQ: stop(3333, 'drain') + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each] + else Close reason: connection limit + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + else Close reason: heartbeat terminate + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + else Close reason: send error + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + end + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + alt Socket not found + ClientMQAdapter->>ClientMQAdapter: Log warning, return false + else Connection already closed (>5s ago) + ClientMQAdapter->>ClientMQAdapter: Force disconnect + ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Connection valid + ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now() + ClientMQAdapter->>Client: socket.close(code, reason) + Client->>WebSocketServer: close event + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + alt Multiple close events + ClientMQAdapter->>ClientMQAdapter: Log warning, return early + else Socket already disconnected + alt Previously forcefully closed + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Already disconnected + ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected') + end + else Normal disconnect + ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = false + ClientMQAdapter->>ClientMQAdapter: Calculate connection duration + ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) + ClientMQ->>ClientMQ: Remove from _adaptersById + ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) + ClientMQWorker->>ClientMQWorker: Decrement _connectionsOpen + ClientMQWorker->>ClientMQWorker: Clean up room subscriptions + ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions + ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) + end + end + + Note over Client,ClientMQWorker: === Client-Initiated Close === + Client->>WebSocketServer: close connection + WebSocketServer->>WebSocketServer: close event (reasonCode, description) + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + Note over ClientMQAdapter: Same disconnect flow as above + + Note over Client,ClientMQWorker: === Error Handling === + alt Socket error + Client->>WebSocketServer: error event + WebSocketServer->>WebSocketServer: Log error + else Handler error + WebSocketServer->>WebSocketServer: Log error, continue + else Send error (not opened) + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...) + Note over ClientMQAdapter: See "Server-Initiated Close" above + else Send error (not found/closed) + ClientMQWorker->>ClientMQWorker: Return {status: 'closed'} + end +``` + diff --git a/Untitled.md b/Untitled.md new file mode 100644 index 0000000..b6b6a13 --- /dev/null +++ b/Untitled.md @@ -0,0 +1,278 @@ + + +```mermaid +sequenceDiagram + participant Client + participant HTTPServer + participant WebSocketServer + participant ClientMQWebsocketAdapter as ClientMQAdapter + participant ClientMQ + participant ClientMQWorker + participant MQWorker + participant PingInterval as Ping Interval
(20s) + + Note over Client,ClientMQWorker: === Connection Establishment === + activate Client + Client->>HTTPServer: HTTP Upgrade Request + activate HTTPServer + HTTPServer->>WebSocketServer: upgrade event + deactivate HTTPServer + + activate WebSocketServer + WebSocketServer->>WebSocketServer: handleUpgrade() + WebSocketServer->>WebSocketServer: ... + WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers) + deactivate WebSocketServer + + activate ClientMQAdapter + ClientMQAdapter->>ClientMQ: emit('connect', connectionId) + deactivate ClientMQAdapter + + activate ClientMQ + ClientMQ->>ClientMQ: Register adapter in _adaptersById + ClientMQ->>ClientMQWorker: emit('connect', connectionId) + deactivate ClientMQ + + activate ClientMQWorker + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION + ClientMQWorker->>ClientMQWorker: _connectionsTotal++
_connectionsOpen++ + end + deactivate ClientMQWorker + deactivate Client + + Note over Client,ClientMQWorker: === Heartbeat/Ping Flow === + loop Every 20 seconds + activate PingInterval + PingInterval->>ClientMQAdapter: Ping interval trigger + deactivate PingInterval + + activate ClientMQAdapter + ClientMQAdapter->>ClientMQAdapter: Iterate all sockets + alt Previous ping still pending + ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning + else Socket has ping() method + alt No pong received (lastPong < lastPing) + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + deactivate ClientMQAdapter + + activate ClientMQ + ClientMQ->>ClientMQWorker: emit('timeout', connectionId) + deactivate ClientMQ + + activate ClientMQWorker + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + deactivate ClientMQWorker + + Note over ClientMQAdapter: See "Server-Initiated Close" below + else Pong received + ClientMQAdapter->>Client: ping() + activate Client + Client->>WebSocketServer: pong() + deactivate Client + + activate WebSocketServer + WebSocketServer->>ClientMQAdapter: pongDelegate(socket) + deactivate WebSocketServer + + ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()
Calculate RTT + ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...) + end + else Socket has no ping() method + alt Last ping > 2x heartbeatInterval ago + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + Note over ClientMQAdapter: See timeout flow above + end + end + end + deactivate ClientMQAdapter + + Note over Client,ClientMQWorker: === Server-Initiated Close === + activate ClientMQWorker + alt Close reason: timeout + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + deactivate ClientMQWorker + activate ClientMQ + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + deactivate ClientMQ + activate ClientMQAdapter + else Close reason: drain + ClientMQWorker->>ClientMQ: stop(3333, 'drain') + deactivate ClientMQWorker + activate ClientMQ + loop For each connection + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + activate ClientMQAdapter + Note over ClientMQAdapter: Full close processing happens
for each connection in loop + alt Socket not found + ClientMQAdapter->>ClientMQAdapter: Log warning, return false + else Connection already closed (>5s ago) + ClientMQAdapter->>ClientMQAdapter: Force disconnect + ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Connection valid + ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now() + ClientMQAdapter->>WebSocketServer: socket.close(code, reason) + activate WebSocketServer + WebSocketServer->>Client: Send Close Frame + activate Client + deactivate Client + activate Client + Client->>WebSocketServer: Send Close Frame (acknowledgment) + deactivate Client + WebSocketServer->>WebSocketServer: 'close' event fires + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + deactivate WebSocketServer + ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById + ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) + activate ClientMQ + ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) + deactivate ClientMQ + activate ClientMQWorker + ClientMQWorker->>ClientMQWorker: _connectionsOpen-- + ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) + activate MQWorker + deactivate MQWorker + deactivate ClientMQWorker + end + deactivate ClientMQAdapter + end + deactivate ClientMQ + + activate ClientMQAdapter + Note over ClientMQAdapter: All connections processed in loop above
Processing code below will be no-op (socket not found) + else Close reason: connection limit + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + deactivate ClientMQWorker + activate ClientMQ + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + deactivate ClientMQ + activate ClientMQAdapter + else Close reason: heartbeat terminate + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + deactivate ClientMQWorker + activate ClientMQ + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + deactivate ClientMQ + activate ClientMQAdapter + else Close reason: send error + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + deactivate ClientMQWorker + activate ClientMQ + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + deactivate ClientMQ + activate ClientMQAdapter + end + Note over ClientMQAdapter: Processing for single connection close
(ClientMQAdapter active for non-drain cases only) + alt Socket not found + ClientMQAdapter->>ClientMQAdapter: Log warning, return false + deactivate ClientMQAdapter + else Connection already closed (>5s ago) + ClientMQAdapter->>ClientMQAdapter: Force disconnect + ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + deactivate ClientMQAdapter + activate ClientMQ + deactivate ClientMQ + else Connection valid + ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now() + rect rgb(200, 255, 200) + Note over ClientMQAdapter,Client: 📤 SERVER SENDS CLOSE FRAME + ClientMQAdapter->>WebSocketServer: socket.close(code, reason) + activate WebSocketServer + WebSocketServer->>Client: Send Close Frame (code, reason) + activate Client + Note over Client: Client receives server's close frame + deactivate Client + end + rect rgb(255, 200, 200) + Note over Client,WebSocketServer: 📥 CLIENT MUST RESPOND WITH CLOSE FRAME
(WebSocket Protocol Requirement) + alt Client sends close frame + activate Client + Client->>WebSocketServer: Send Close Frame (acknowledgment) + deactivate Client + Note over WebSocketServer: Platform receives client's close frame + else Client does NOT send close frame + Note over ClientMQAdapter: ⏱️ After 5s timeout (closeTimeout)
Connection marked as forcefully closed + Note over ClientMQAdapter: Next disconnect will use
disconnect-after-forcefull-close path + end + end + rect rgb(200, 200, 255) + Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING + WebSocketServer->>WebSocketServer: 'close' event fires
(reasonCode, description) + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + deactivate WebSocketServer + alt Multiple close events + ClientMQAdapter->>ClientMQAdapter: Log warning, return early
(_connectionsOpen NOT decremented) + deactivate ClientMQAdapter + else Socket already disconnected + alt Previously forcefully closed + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
(_connectionsOpen NOT decremented) + deactivate ClientMQAdapter + activate ClientMQ + deactivate ClientMQ + else Already disconnected + ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
(_connectionsOpen NOT decremented) + deactivate ClientMQAdapter + activate ClientMQ + deactivate ClientMQ + end + else Normal disconnect + ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = false + ClientMQAdapter->>ClientMQAdapter: Calculate connection duration + ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) + deactivate ClientMQAdapter + activate ClientMQ + ClientMQ->>ClientMQ: Remove from _adaptersById + ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) + deactivate ClientMQ + activate ClientMQWorker + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
⚠️ Only happens after receiving client's close frame + ClientMQWorker->>ClientMQWorker: _connectionsOpen-- + end + ClientMQWorker->>ClientMQWorker: Clean up room subscriptions + ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions + ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) + activate MQWorker + deactivate MQWorker + deactivate ClientMQWorker + end + end + end + + Note over Client,ClientMQWorker: === Error Handling === + alt Socket error + activate Client + Client->>WebSocketServer: error event + deactivate Client + activate WebSocketServer + WebSocketServer->>WebSocketServer: Log error + deactivate WebSocketServer + else Handler error + activate WebSocketServer + WebSocketServer->>WebSocketServer: Log error, continue + deactivate WebSocketServer + else Send error (not opened) + activate ClientMQWorker + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + deactivate ClientMQWorker + activate ClientMQ + deactivate ClientMQ + activate ClientMQWorker + ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...) + activate MQWorker + deactivate MQWorker + deactivate ClientMQWorker + Note over ClientMQAdapter: See "Server-Initiated Close" above + else Send error (not found/closed) + activate ClientMQWorker + ClientMQWorker->>ClientMQWorker: Return {status: 'closed'} + deactivate ClientMQWorker + end +``` + + diff --git a/WebSocket Lifecycle Sequence Diagram.md b/WebSocket Lifecycle Sequence Diagram.md new file mode 100644 index 0000000..86ef2c1 --- /dev/null +++ b/WebSocket Lifecycle Sequence Diagram.md @@ -0,0 +1,508 @@ + + + +```mermaid +sequenceDiagram + participant Client + participant HTTPServer + participant WebSocketServer + participant ClientMQ + participant ClientMQWorker + participant MQWorker + participant PingInterval as Ping Interval
(20s) + + Note over Client,ClientMQWorker: === Connection Establishment === + Client->>HTTPServer: HTTP Upgrade Request + HTTPServer->>WebSocketServer: upgrade event + WebSocketServer->>WebSocketServer: handleUpgrade() + WebSocketServer->>WebSocketServer: Generate connection.id (32 chars) + WebSocketServer->>WebSocketServer: Get remoteAddress (from X-Forwarded-For) + WebSocketServer->>WebSocketServer: Setup event handlers
(message, close, pong, error) + WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers) + ClientMQAdapter->>ClientMQAdapter: Set __connectionStart
Set __connectionId
Set __meta {headers} + ClientMQAdapter->>ClientMQAdapter: Register in _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = true + ClientMQAdapter->>ClientMQ: emit('connect', connectionId) + ClientMQ->>ClientMQ: Register adapter in _adaptersById + ClientMQ->>ClientMQWorker: emit('connect', connectionId) + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION + ClientMQWorker->>ClientMQWorker: _connectionsTotal++
_connectionsOpen++ + end + alt Connection limit exceeded (>2000) + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + Note over ClientMQAdapter: See "Server-Initiated Close" below + end + + Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) === + Client->>WebSocketServer: Message (binary or base64) + WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data) + ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol) + alt Unsupported message format + ClientMQAdapter->>ClientMQAdapter: Drop message, log warning + else Socket not connected + ClientMQAdapter->>ClientMQAdapter: Drop message, log warning + else Message type = 'Request' + ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta) + alt Request timeout (>15s) + MQWorker-->>ClientMQAdapter: TimeoutError + ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'} + ClientMQAdapter->>Client: Response (mq.Error) + else Request failed + MQWorker-->>ClientMQAdapter: Error + ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'} + ClientMQAdapter->>Client: Response (mq.Error) + else Request succeeded + MQWorker-->>ClientMQAdapter: {type, payload, wallTime} + ClientMQAdapter->>ClientMQAdapter: Build Response message
(messageType: 'Response', requestId, type, payload) + alt Socket not open + ClientMQAdapter->>ClientMQAdapter: Drop response, log debug + else Socket open + ClientMQAdapter->>Client: Response (binary or base64) + ClientMQAdapter->>ClientMQ: emit('request', connectionId, data) + ClientMQ->>ClientMQWorker: emit('request', connectionId, data) + ClientMQWorker->>ClientMQWorker: Handle subscription updates
(JoinRoom, LeaveRoom, etc.) + end + end + else Message type = 'Response' + alt Request handler not found + ClientMQAdapter->>ClientMQAdapter: Log warning, ignore + else Request handler exists + ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise + end + else Message type = 'Event' + ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported) + end + + Note over Client,ClientMQWorker: === Server-Initiated Request === + ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message) + ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message) + ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter) + ClientMQAdapter->>ClientMQAdapter: Register in _requests map + alt Socket not found + ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found' + else Socket not connected + ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened' + else Socket valid + ClientMQAdapter->>Client: Request (binary or base64) + ClientMQAdapter->>ClientMQAdapter: Start timeout (15s) + Client->>WebSocketServer: Response message + WebSocketServer->>ClientMQAdapter: messageDelegate (Response) + ClientMQAdapter->>ClientMQAdapter: Resolve request promise + ClientMQAdapter-->>ClientMQWorker: Response message + alt Response timeout + ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout + end + end + + Note over Client,ClientMQWorker: === Heartbeat/Ping Flow === + loop Every 20 seconds + PingInterval->>ClientMQAdapter: Ping interval trigger + ClientMQAdapter->>ClientMQAdapter: Iterate all sockets + alt Previous ping still pending + ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning + else Socket has ping() method + alt No pong received (lastPong < lastPing) + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + ClientMQ->>ClientMQWorker: emit('timeout', connectionId) + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + Note over ClientMQAdapter: See "Server-Initiated Close" below + else Pong received + ClientMQAdapter->>Client: ping() + Client->>WebSocketServer: pong() + WebSocketServer->>ClientMQAdapter: pongDelegate(socket) + ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()
Calculate RTT + ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...) + end + else Socket has no ping() method + alt Last ping > 2x heartbeatInterval ago + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + Note over ClientMQAdapter: See timeout flow above + end + end + end + + Note over Client,ClientMQWorker: === Server-Initiated Close === + alt Close reason: timeout + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + else Close reason: drain + ClientMQWorker->>ClientMQ: stop(3333, 'drain') + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each] + else Close reason: connection limit + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + else Close reason: heartbeat terminate + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + else Close reason: send error + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + end + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + alt Socket not found + ClientMQAdapter->>ClientMQAdapter: Log warning, return false + else Connection already closed (>5s ago) + ClientMQAdapter->>ClientMQAdapter: Force disconnect + ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Connection valid + ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now() + ClientMQAdapter->>Client: socket.close(code, reason) + Client->>WebSocketServer: close event + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + alt Multiple close events + ClientMQAdapter->>ClientMQAdapter: Log warning, return early + else Socket already disconnected + alt Previously forcefully closed + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Already disconnected + ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected') + end + else Normal disconnect + ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = false + ClientMQAdapter->>ClientMQAdapter: Calculate connection duration + ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) + ClientMQ->>ClientMQ: Remove from _adaptersById + ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION + ClientMQWorker->>ClientMQWorker: _connectionsOpen-- + end + ClientMQWorker->>ClientMQWorker: Clean up room subscriptions + ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions + ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) + end + end + + Note over Client,ClientMQWorker: === Client-Initiated Close === + Client->>WebSocketServer: close connection + WebSocketServer->>WebSocketServer: close event (reasonCode, description) + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + Note over ClientMQAdapter: Same disconnect flow as above + + Note over Client,ClientMQWorker: === Error Handling === + alt Socket error + Client->>WebSocketServer: error event + WebSocketServer->>WebSocketServer: Log error + else Handler error + WebSocketServer->>WebSocketServer: Log error, continue + else Send error (not opened) + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...) + Note over ClientMQAdapter: See "Server-Initiated Close" above + else Send error (not found/closed) + ClientMQWorker->>ClientMQWorker: Return {status: 'closed'} + end +``` + + + + +## connectionsOpen + +```mermaid +sequenceDiagram + participant Client + participant HTTPServer + participant WebSocketServer + participant ClientMQ + participant ClientMQWorker + participant MQWorker + participant PingInterval as Ping Interval
(20s) + + Note over Client,ClientMQWorker: === Connection Establishment === + Client->>HTTPServer: HTTP Upgrade Request + HTTPServer->>WebSocketServer: upgrade event + WebSocketServer->>WebSocketServer: handleUpgrade() + ClientMQAdapter->>ClientMQ: emit('connect', connectionId) + ClientMQ->>ClientMQWorker: emit('connect', connectionId) + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION + ClientMQWorker->>ClientMQWorker: _connectionsTotal++
_connectionsOpen++ + end + + Note over Client,ClientMQWorker: === Server-Initiated Close === + alt Close reason: timeout + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + end + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + alt Socket not found + ClientMQAdapter->>ClientMQAdapter: Log warning, return false + else Connection already closed (>5s ago) + ClientMQAdapter->>ClientMQAdapter: Force disconnect + ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Connection valid + ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now() + ClientMQAdapter->>Client: socket.close(code, reason) + Client->>WebSocketServer: close event + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + alt Multiple close events + ClientMQAdapter->>ClientMQAdapter: Log warning, return early + else Socket already disconnected + alt Previously forcefully closed + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Already disconnected + ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected') + end + else Normal disconnect + ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = false + ClientMQAdapter->>ClientMQAdapter: Calculate connection duration + ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) + ClientMQ->>ClientMQ: Remove from _adaptersById + ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION + ClientMQWorker->>ClientMQWorker: _connectionsOpen-- + end + ClientMQWorker->>ClientMQWorker: Clean up room subscriptions + ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions + ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) + end + end + + Note over Client,ClientMQWorker: === Error Handling === + alt Socket error + Client->>WebSocketServer: error event + WebSocketServer->>WebSocketServer: Log error + else Handler error + WebSocketServer->>WebSocketServer: Log error, continue + else Send error (not opened) + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...) + Note over ClientMQAdapter: See "Server-Initiated Close" above + else Send error (not found/closed) + ClientMQWorker->>ClientMQWorker: Return {status: 'closed'} + end +``` + + + + +# WebSocket Lifecycle Sequence Diagram + + +```mermaid +sequenceDiagram + participant Client + participant HTTPServer + participant WebSocketServer + participant ClientMQWebsocketAdapter as ClientMQAdapter + participant ClientMQ + participant ClientMQWorker + participant MQWorker + participant PingInterval as Ping Interval
(20s) + + Note over Client,ClientMQWorker: === Connection Establishment === + Client->>HTTPServer: HTTP Upgrade Request + HTTPServer->>WebSocketServer: upgrade event + WebSocketServer->>+ClientMQAdapter: connectDelegate(socket, headers) + ClientMQAdapter->>ClientMQAdapter: Set __connectionStart
Set __connectionId
Set __meta {headers} + ClientMQAdapter->>ClientMQAdapter: Register in _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = true + ClientMQAdapter->>-ClientMQ: emit('connect', connectionId) + ClientMQ->>ClientMQ: Register adapter in _adaptersById + ClientMQ->>ClientMQWorker: emit('connect', connectionId) + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION + ClientMQWorker->>ClientMQWorker: _connectionsTotal++
_connectionsOpen++ + end + alt Connection limit exceeded (>2000) + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + Note over ClientMQAdapter: See "Server-Initiated Close" below + end + + Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) === + Client->>WebSocketServer: Message (binary or base64) + WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data) + ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol) + alt Unsupported message format + ClientMQAdapter->>ClientMQAdapter: Drop message, log warning + else Socket not connected + ClientMQAdapter->>ClientMQAdapter: Drop message, log warning + else Message type = 'Request' + ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta) + alt Request timeout (>15s) + MQWorker-->>ClientMQAdapter: TimeoutError + ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'} + ClientMQAdapter->>Client: Response (mq.Error) + else Request failed + MQWorker-->>ClientMQAdapter: Error + ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'} + ClientMQAdapter->>Client: Response (mq.Error) + else Request succeeded + MQWorker-->>ClientMQAdapter: {type, payload, wallTime} + ClientMQAdapter->>ClientMQAdapter: Build Response message
(messageType: 'Response', requestId, type, payload) + alt Socket not open + ClientMQAdapter->>ClientMQAdapter: Drop response, log debug + else Socket open + ClientMQAdapter->>Client: Response (binary or base64) + ClientMQAdapter->>ClientMQ: emit('request', connectionId, data) + ClientMQ->>ClientMQWorker: emit('request', connectionId, data) + ClientMQWorker->>ClientMQWorker: Handle subscription updates
(JoinRoom, LeaveRoom, etc.) + end + end + else Message type = 'Response' + alt Request handler not found + ClientMQAdapter->>ClientMQAdapter: Log warning, ignore + else Request handler exists + ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise + end + else Message type = 'Event' + ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported) + end + + Note over Client,ClientMQWorker: === Server-Initiated Request === + ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message) + ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message) + ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter) + ClientMQAdapter->>ClientMQAdapter: Register in _requests map + alt Socket not found + ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found' + else Socket not connected + ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened' + else Socket valid + ClientMQAdapter->>Client: Request (binary or base64) + ClientMQAdapter->>ClientMQAdapter: Start timeout (15s) + Client->>WebSocketServer: Response message + WebSocketServer->>ClientMQAdapter: messageDelegate (Response) + ClientMQAdapter->>ClientMQAdapter: Resolve request promise + ClientMQAdapter-->>ClientMQWorker: Response message + alt Response timeout + ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout + end + end + + Note over Client,ClientMQWorker: === Heartbeat/Ping Flow === + loop Every 20 seconds + PingInterval->>ClientMQAdapter: Ping interval trigger + ClientMQAdapter->>ClientMQAdapter: Iterate all sockets + alt Previous ping still pending + ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning + else Socket has ping() method + alt No pong received (lastPong < lastPing) + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + ClientMQ->>ClientMQWorker: emit('timeout', connectionId) + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + Note over ClientMQAdapter: See "Server-Initiated Close" below + else Pong received + ClientMQAdapter->>Client: ping() + Client->>WebSocketServer: pong() + WebSocketServer->>ClientMQAdapter: pongDelegate(socket) + ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()
Calculate RTT + ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...) + end + else Socket has no ping() method + alt Last ping > 2x heartbeatInterval ago + ClientMQAdapter->>ClientMQAdapter: Detect timeout + ClientMQAdapter->>ClientMQ: emit('timeout', connectionId) + Note over ClientMQAdapter: See timeout flow above + end + end + end + + Note over Client,ClientMQWorker: === Server-Initiated Close === + alt Close reason: timeout + ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') + else Close reason: drain + ClientMQWorker->>ClientMQ: stop(3333, 'drain') + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each] + else Close reason: connection limit + ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') + else Close reason: heartbeat terminate + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + else Close reason: send error + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + end + ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) + alt Socket not found + ClientMQAdapter->>ClientMQAdapter: Log warning, return false + else Connection already closed (>5s ago) + ClientMQAdapter->>ClientMQAdapter: Force disconnect + ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') + else Connection valid + ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now() + rect rgb(200, 255, 200) + Note over ClientMQAdapter,Client: 📤 SERVER SENDS CLOSE FRAME + ClientMQAdapter->>WebSocketServer: socket.close(code, reason) + WebSocketServer->>Client: Send Close Frame (code, reason) + Note over Client: Client receives server's close frame + end + rect rgb(255, 200, 200) + Note over Client,WebSocketServer: 📥 CLIENT MUST RESPOND WITH CLOSE FRAME
(WebSocket Protocol Requirement) + alt Client sends close frame + Client->>WebSocketServer: Send Close Frame (acknowledgment) + Note over WebSocketServer: Platform receives client's close frame + else Client does NOT send close frame + Note over ClientMQAdapter: ⏱️ After 5s timeout (closeTimeout)
Connection marked as forcefully closed + Note over ClientMQAdapter: Next disconnect will use
disconnect-after-forcefull-close path + end + end + rect rgb(200, 200, 255) + Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING + WebSocketServer->>WebSocketServer: 'close' event fires
(reasonCode, description) + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + alt Multiple close events + ClientMQAdapter->>ClientMQAdapter: Log warning, return early
(_connectionsOpen NOT decremented) + else Socket already disconnected + alt Previously forcefully closed + ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
(_connectionsOpen NOT decremented) + else Already disconnected + ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
(_connectionsOpen NOT decremented) + end + else Normal disconnect + ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById + ClientMQAdapter->>ClientMQAdapter: Set __connected = false + ClientMQAdapter->>ClientMQAdapter: Calculate connection duration + ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) + ClientMQ->>ClientMQ: Remove from _adaptersById + ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) + rect rgb(255, 255, 200) + Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
⚠️ Only happens after receiving client's close frame + ClientMQWorker->>ClientMQWorker: _connectionsOpen-- + end + ClientMQWorker->>ClientMQWorker: Clean up room subscriptions + ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions + ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) + end + end + end + + Note over Client,ClientMQWorker: === Client-Initiated Close === + rect rgb(255, 200, 200) + Note over Client,WebSocketServer: 📥 CLIENT SENDS CLOSE FRAME + Client->>WebSocketServer: Send Close Frame (code, reason) + Note over WebSocketServer: Platform receives client's close frame + end + rect rgb(200, 255, 200) + Note over WebSocketServer,Client: 📤 SERVER RESPONDS WITH CLOSE FRAME
(WebSocket Protocol Requirement) + WebSocketServer->>Client: Send Close Frame (acknowledgment) + end + rect rgb(200, 200, 255) + Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING + WebSocketServer->>WebSocketServer: 'close' event fires
(reasonCode, description) + WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) + Note over ClientMQAdapter: Same disconnect flow as server-initiated
(_connectionsOpen decremented after close event) + end + + Note over Client,ClientMQWorker: === Error Handling === + alt Socket error + Client->>WebSocketServer: error event + WebSocketServer->>WebSocketServer: Log error + else Handler error + WebSocketServer->>WebSocketServer: Log error, continue + else Send error (not opened) + ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') + ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...) + Note over ClientMQAdapter: See "Server-Initiated Close" above + else Send error (not found/closed) + ClientMQWorker->>ClientMQWorker: Return {status: 'closed'} + end +``` + + diff --git a/ZSH/Documentation.md b/ZSH/Documentation.md new file mode 100644 index 0000000..85984df --- /dev/null +++ b/ZSH/Documentation.md @@ -0,0 +1,107 @@ +[source](https://zsh.sourceforge.io/Guide/zshguide02.html) + +### Options +The usual way to set and unset options +```zsh +setopt # sets an option +unsetopt # unsets the option + +# Note +set -o +# is the equivelant to +setopt + +# NOTE: +set # without the `-o` does something else -- sets the positional paramters +``` + +both `~/.zshrc` and `~/.zshenv` run for every shell + +`~/.zshrc` is executed upon a shell starting for every **interactive** shell + + +### Parameters + +Simple parameters can be assigned +```zsh +foo='This is a parameter' +``` + +Note no spaces between the `foo` and `=` +Single quotes, as here, are the nuclear option of quotes: everything up to another single quote is treated as a simple string --- newlines, equal signs, unprintable characters, the lot, in this example all would be assigned to the variable; for example, + + ```zsh + foo='This is a parameter. + This is still the same parameter.' + ``` + +So they're the best thing to use until you know what you're doing with double quotes, which have extra effects. Sometimes you don't need them, for example: + +``` +foo=oneword +``` + +because there's nothing in `oneword` to confuse the shell; but you could still put quotes there anyway. + + +#### Parameter Expansion + +```zsh +foo='This is a parameter' + +print -- '$foo is "'$foo'"' +``` + +prints +```zsh +$foo is "This is a parameter" +``` +expansion happens anywhere the parameter is not quoted; it doesn't have to be on its own, just separated from anything which might make it look like a different parameter. This is one of those things that can help make shell scripts look so barbaric. + +Note: why the `---` after the `print`? + because **print**, like many UNIX commands, can take options after it which begin with a -` `--` says that there are no more options; so if what you're trying to print begins with a ``-`', it will still print out. + +### Arrays + +There is a special type of parameter called an **array** which zsh inherited from both ksh and csh. This is a slightly shaky marriage, since some of the things those two shells do with them are not compatible, and zsh has elements of both, so you need to be careful if you've used arrays in either. The option `KSH_ARRAYS` is something you can set to make them behave more like they do in ksh, but a lot of zsh users write functions and scripts assuming it isn't set, so it can be dangerous. + +Unlike normal parameters (known as **scalars**), arrays have more than one word in them. In the examples above, we made the parameter `$foo` get a string with spaces in, but the spaces weren't significant. If we'd done + + foo=(This is a parameter.) + +(note the absence of quotes), it would have created an array. Again, there must be no space between the ``=`' and the `(', though inside the parentheses spaces separate words just like they do on a command line. The difference isn't obvious if you try and print it --- it looks just the same --- but now try this: + + print -- ${foo[4]} + +and you get ``parameter.`'. The array stores the words separately, and you can retrieve them separately by putting the number of the element of the array in square brackets. Note also the braces ``{...}`' --- zsh doesn't always require them, but they make things much clearer when things get complicated, and it's never wrong to put them in: you could have said ``${foo}`' when you wanted to print out the complete parameter, and it would be treated identically to ``$foo`'. The braces simply screen off the expansion from whatever else might be lying around to confuse the shell. It's useful too in expressions like ``${foo}s`' to keep the ``s`' from being part of the parameter name; and, finally, with `KSH_ARRAYS` set, the braces are compulsory, though unfortunately arrays are indexed from 0 in that case. + +You can use quotes when defining arrays; as before, this protects against the shell thinking the spaces are between different elements of the array. Try: + + foo=('first element' 'second element') + print -- ${foo[2]} + +Arrays are useful when the shell needs to keep a whole series of different things together, so we'll meet some you may want to put in a startup file. Users of ksh will have noticed that things are a bit different in zsh, but for now I'll just assume you're using the normal zsh way of doing things. + +### Compatibility Options +- `SH_WORD_SPLT` - Split string variables into arrays. ZSH Default: `not set` +- `NO_BANG_HIST` +- `BSD_ECHO` (sh only) +- `IGNORE_BRACES` +- `INTERACTIVE_COMMENTS` +- `KSH_OPTION_PRINT` +- `NO_MULTIOS`  +- `POSIX_BUILTINS` +- `PROMPT_BANG` +- `SINGLE_LINE_ZLE` + + +**`BG_NICE`& `NOTIFY`** + +All UNIX shells allow you to start a _background_ job by putting `&` at the end of the line; then the shell doesn't wait for the job to finish, so you can type something else. In zsh, such jobs are usually run at a lower priority (a `higher nice value` in UNIX-speak), so that they don't use so much of the processor's time as foreground jobs (all the others, without the &`) do. This is so that jobs like editing or using the shell don't get slowed down, which can be highly annoying. You can turn this feature off by setting `NO_BG_NICE` + +When a background job finishes, zsh usually tells you immediately by printing a message, which interrupts whatever you're doing. You can stop this by setting `NO_NOTIFY`. Actually, this is an option in most versions of ksh, too, but it's a little less annoying in zsh because if it happens while you're typing something else to the shell, the shell will reprint the line you were on as far as you've got. For example: + + +### HUP + - `SIGINT` - what the shell interprets `^C` (CNTRL + C) + - `SIGHUP` - \ No newline at end of file