>>0).toString(36)}function X(t){if("string"==typeof t){const e=t;t=t=>t.matches(e)}const e=t;return function(){let t=Array.from(this.childNodes);return e&&(t=t.filter((t=>e(t)))),t}}function A(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function j(t,e){var n,r,i,a,s,o=new M(t),l=+t.value&&(o.value=t.value),h=[o];for(null==e&&(e=R);n=h.pop();)if(l&&(n.value=+n.data.value),(i=e(n.data))&&(s=i.length))for(n.children=new Array(s),a=s-1;a>=0;--a)h.push(r=n.children[a]=new M(i[a])),r.parent=n,r.depth=n.depth+1;return o.eachBefore($)}function R(t){return t.children}function O(t){t.data=t.data.data}function $(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function M(t){this.data=t,this.depth=this.height=0,this.parent=null}M.prototype=j.prototype={constructor:M,count:function(){return this.eachAfter(A)},each:function(t){var e,n,r,i,a=this,s=[a];do{for(e=s.reverse(),s=[];a=e.pop();)if(t(a),n=a.children)for(r=0,i=n.length;r=0;--n)i.push(e[n]);return this},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;t=n.pop(),e=r.pop();for(;t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){var t=[];return this.each((function(e){t.push(e)})),t},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return j(this).eachBefore(O)}};const T={version:"2.1.2"},{version:H}=T,B=Object.freeze({children:t=>t.children,nodeSize:t=>t.data.size,spacing:0});function _(t){const e=Object.assign({},B,t);function n(t){const n=e[t];return"function"==typeof n?n:()=>n}function r(t){const e=a(function(){const t=i(),e=n("nodeSize"),r=n("spacing");return class extends t{constructor(t){super(t),Object.assign(this,{x:0,y:0,relX:0,prelim:0,shift:0,change:0,lExt:this,lExtRelX:0,lThr:null,rExt:this,rExtRelX:0,rThr:null})}get size(){return e(this.data)}spacing(t){return r(this.data,t.data)}get x(){return this.data.x}set x(t){this.data.x=t}get y(){return this.data.y}set y(t){this.data.y=t}update(){return N(this),D(this),this}}}(),t,(t=>t.children));return e.update(),e.data}function i(){const t=n("nodeSize"),e=n("spacing");return class n extends j.prototype.constructor{constructor(t){super(t)}copy(){const t=a(this.constructor,this,(t=>t.children));return t.each((t=>t.data=t.data.data)),t}get size(){return t(this)}spacing(t){return e(this,t)}get nodes(){return this.descendants()}get xSize(){return this.size[0]}get ySize(){return this.size[1]}get top(){return this.y}get bottom(){return this.y+this.ySize}get left(){return this.x-this.xSize/2}get right(){return this.x+this.xSize/2}get root(){const t=this.ancestors();return t[t.length-1]}get numChildren(){return this.hasChildren?this.children.length:0}get hasChildren(){return!this.noChildren}get noChildren(){return null===this.children}get firstChild(){return this.hasChildren?this.children[0]:null}get lastChild(){return this.hasChildren?this.children[this.numChildren-1]:null}get extents(){return(this.children||[]).reduce(((t,e)=>n.maxExtents(t,e.extents)),this.nodeExtents)}get nodeExtents(){return{top:this.top,bottom:this.bottom,left:this.left,right:this.right}}static maxExtents(t,e){return{top:Math.min(t.top,e.top),bottom:Math.max(t.bottom,e.bottom),left:Math.min(t.left,e.left),right:Math.max(t.right,e.right)}}}}function a(t,e,n){const r=(e,i)=>{const a=new t(e);Object.assign(a,{parent:i,depth:null===i?0:i.depth+1,height:0,length:1});const s=n(e)||[];return a.children=0===s.length?null:s.map((t=>r(t,a))),a.children&&Object.assign(a,a.children.reduce(((t,e)=>({height:Math.max(t.height,e.height+1),length:t.length+e.length})),a)),a};return r(e,null)}return Object.assign(r,{nodeSize(t){return arguments.length?(e.nodeSize=t,r):e.nodeSize},spacing(t){return arguments.length?(e.spacing=t,r):e.spacing},children(t){return arguments.length?(e.children=t,r):e.children},hierarchy(t,n){const r=void 0===n?e.children:n;return a(i(),t,r)},dump(t){const e=n("nodeSize"),r=t=>n=>{const i=t+" ",a=t+" ",{x:s,y:o}=n,l=e(n),h=n.children||[],c=0===h.length?" ":`,${i}children: [${a}${h.map(r(a)).join(a)}${i}],${t}`;return`{ size: [${l.join(", ")}],${i}x: ${s}, y: ${o}${c}},`};return r("\n")(t)}}),r}_.version=H;const N=(t,e=0)=>(t.y=e,(t.children||[]).reduce(((e,n)=>{const[r,i]=e;N(n,t.y+t.ySize);const a=(0===r?n.lExt:n.rExt).bottom;0!==r&&W(t,r,i);return[r+1,Z(a,r,i)]}),[0,null]),L(t),G(t),t),D=(t,e,n)=>{void 0===e&&(e=-t.relX-t.prelim,n=0);const r=e+t.relX;return t.relX=r+t.prelim-n,t.prelim=0,t.x=n+t.relX,(t.children||[]).forEach((e=>D(e,r,t.x))),t},L=t=>{(t.children||[]).reduce(((t,e)=>{const[n,r]=t,i=n+e.shift,a=r+i+e.change;return e.relX+=a,[i,a]}),[0,0])},W=(t,e,n)=>{const r=t.children[e-1],i=t.children[e];let a=r,s=r.relX,o=i,l=i.relX,h=!0;for(;a&&o;){a.bottom>n.lowY&&(n=n.next);const r=s+a.prelim-(l+o.prelim)+a.xSize/2+o.xSize/2+a.spacing(o);(r>0||r<0&&h)&&(l+=r,F(i,r),P(t,e,n.index,r)),h=!1;const c=a.bottom,d=o.bottom;c<=d&&(a=V(a),a&&(s+=a.relX)),c>=d&&(o=I(o),o&&(l+=o.relX))}!a&&o?K(t,e,o,l):a&&!o&&Y(t,e,a,s)},F=(t,e)=>{t.relX+=e,t.lExtRelX+=e,t.rExtRelX+=e},P=(t,e,n,r)=>{const i=t.children[e],a=e-n;if(a>1){const e=r/a;t.children[n+1].shift+=e,i.shift-=e,i.change-=r-e}},I=t=>t.hasChildren?t.firstChild:t.lThr,V=t=>t.hasChildren?t.lastChild:t.rThr,K=(t,e,n,r)=>{const i=t.firstChild,a=i.lExt,s=t.children[e];a.lThr=n;const o=r-n.relX-i.lExtRelX;a.relX+=o,a.prelim-=o,i.lExt=s.lExt,i.lExtRelX=s.lExtRelX},Y=(t,e,n,r)=>{const i=t.children[e],a=i.rExt,s=t.children[e-1];a.rThr=n;const o=r-n.relX-i.rExtRelX;a.relX+=o,a.prelim-=o,i.rExt=s.rExt,i.rExtRelX=s.rExtRelX},G=t=>{if(t.hasChildren){const e=t.firstChild,n=t.lastChild,r=(e.prelim+e.relX-e.xSize/2+n.relX+n.prelim+n.xSize/2)/2;Object.assign(t,{prelim:r,lExt:e.lExt,lExtRelX:e.lExtRelX,rExt:n.rExt,rExtRelX:n.rExtRelX})}},Z=(t,e,n)=>{for(;null!==n&&t>=n.lowY;)n=n.next;return{lowY:t,index:e,next:n}},q=".markmap {\n --markmap-max-width: 9999px;\n --markmap-a-color: #0097e6;\n --markmap-a-hover-color: #00a8ff;\n --markmap-code-bg: #f0f0f0;\n --markmap-code-color: #555;\n --markmap-highlight-bg: #ffeaa7;\n --markmap-table-border: 1px solid currentColor;\n --markmap-font: 300 16px/20px sans-serif;\n --markmap-circle-open-bg: #fff;\n --markmap-text-color: #333;\n --markmap-highlight-node-bg: #ff02;\n\n font: var(--markmap-font);\n color: var(--markmap-text-color);\n}\n\n .markmap-link {\n fill: none;\n }\n\n .markmap-node > circle {\n cursor: pointer;\n }\n\n .markmap-foreign {\n display: inline-block;\n }\n\n .markmap-foreign p {\n margin: 0;\n }\n\n .markmap-foreign a {\n color: var(--markmap-a-color);\n }\n\n .markmap-foreign a:hover {\n color: var(--markmap-a-hover-color);\n }\n\n .markmap-foreign code {\n padding: 0.25em;\n font-size: calc(1em - 2px);\n color: var(--markmap-code-color);\n background-color: var(--markmap-code-bg);\n border-radius: 2px;\n }\n\n .markmap-foreign pre {\n margin: 0;\n }\n\n .markmap-foreign pre > code {\n display: block;\n }\n\n .markmap-foreign del {\n text-decoration: line-through;\n }\n\n .markmap-foreign em {\n font-style: italic;\n }\n\n .markmap-foreign strong {\n font-weight: bold;\n }\n\n .markmap-foreign mark {\n background: var(--markmap-highlight-bg);\n }\n\n .markmap-foreign table,\n .markmap-foreign th,\n .markmap-foreign td {\n border-collapse: collapse;\n border: var(--markmap-table-border);\n }\n\n .markmap-foreign img {\n display: inline-block;\n }\n\n .markmap-foreign svg {\n fill: currentColor;\n }\n\n .markmap-foreign > div {\n width: var(--markmap-max-width);\n text-align: left;\n }\n\n .markmap-foreign > div > div {\n display: inline-block;\n }\n\n .markmap-highlight rect {\n fill: var(--markmap-highlight-node-bg);\n }\n\n.markmap-dark .markmap {\n --markmap-code-bg: #1a1b26;\n --markmap-code-color: #ddd;\n --markmap-circle-open-bg: #444;\n --markmap-text-color: #eee;\n}\n",J=q,Q="g.markmap-node",U=e.linkHorizontal();function tt(t,n){return t[e.minIndex(t,n)]}function et(t){t.stopPropagation()}const nt=new class{constructor(){this.listeners=[]}tap(t){return this.listeners.push(t),()=>this.revoke(t)}revoke(t){const e=this.listeners.indexOf(t);e>=0&&this.listeners.splice(e,1)}revokeAll(){this.listeners.splice(0)}call(...t){for(const e of this.listeners)e(...t)}};class rt{constructor(t,i){this.options={...E},this._disposeList=[],this.handleZoom=t=>{const{transform:e}=t;this.g.attr("transform",e)},this.handlePan=t=>{t.preventDefault();const n=e.zoomTransform(this.svg.node()),r=n.translate(-t.deltaX/n.k,-t.deltaY/n.k);this.svg.call(this.zoom.transform,r)},this.handleClick=(t,e)=>{let n=this.options.toggleRecursively;(b?t.metaKey:t.ctrlKey)&&(n=!n),this.toggleNode(e,n)},this.ensureView=this.ensureVisible,this.svg=t.datum?t:e.select(t),this.styleNode=this.svg.append("style"),this.zoom=e.zoom().filter((t=>this.options.scrollForPan&&"wheel"===t.type?t.ctrlKey&&!t.button:!(t.ctrlKey&&"wheel"!==t.type||t.button))).on("zoom",this.handleZoom),this.setOptions(i),this.state={id:this.options.id||this.svg.attr("id")||(r+=1,`mm-${n}-${r}`),rect:{x1:0,y1:0,x2:0,y2:0}},this.g=this.svg.append("g"),this.g.append("g").attr("class","markmap-highlight"),this._observer=new ResizeObserver(function(t,e){const n={timer:0};function r(){n.timer&&(window.clearTimeout(n.timer),n.timer=0)}function i(){r(),n.args&&(n.result=t(...n.args))}return function(...t){return r(),n.args=t,n.timer=window.setTimeout(i,e),n.result}}((()=>{this.renderData()}),100)),this._disposeList.push(nt.tap((()=>{this.setData()})),(()=>this._observer.disconnect()))}getStyleContent(){const{style:t}=this.options,{id:e}=this.state,n="function"==typeof t?t(e):"";return[this.options.embedGlobalCSS&&q,n].filter(Boolean).join("\n")}updateStyle(){this.svg.attr("class",function(t,...e){const n=(t||"").split(" ").filter(Boolean);return e.forEach((t=>{t&&n.indexOf(t)<0&&n.push(t)})),n.join(" ")}(this.svg.attr("class"),"markmap",this.state.id));const t=this.getStyleContent();this.styleNode.text(t)}async toggleNode(t,e=!1){var n,r;const i=(null==(n=t.payload)?void 0:n.fold)?0:1;e?a(t,((t,e)=>{t.payload={...t.payload,fold:i},e()})):t.payload={...t.payload,fold:(null==(r=t.payload)?void 0:r.fold)?0:1},await this.renderData(t)}_initializeData(t){let e=0;const{color:n,initialExpandLevel:r}=this.options;let i=0,s=0;return a(t,((t,a,o)=>{var l,h,c,d;s+=1,t.children=null==(l=t.children)?void 0:l.map((t=>({...t}))),e+=1,t.state={...t.state,depth:s,id:e,rect:{x:0,y:0,width:0,height:0},size:[0,0]},t.state.key=[null==(h=null==o?void 0:o.state)?void 0:h.id,t.state.id].filter(Boolean).join(".")+C(t.content),t.state.path=[null==(c=null==o?void 0:o.state)?void 0:c.path,t.state.id].filter(Boolean).join("."),n(t);const p=2===(null==(d=t.payload)?void 0:d.fold);p?i+=1:(i||r>=0&&t.state.depth>=r)&&(t.payload={...t.payload,fold:1}),a(),p&&(i-=1),s-=1})),t}_relayout(){if(!this.state.data)return;this.g.selectAll(X(Q)).selectAll(X("foreignObject")).each((function(t){var e;const n=null==(e=this.firstChild)?void 0:e.firstChild,r=[n.scrollWidth,n.scrollHeight];t.state.size=r}));const{lineWidth:t,paddingX:n,spacingHorizontal:r,spacingVertical:i}=this.options,a=_({}).children((t=>{var e;if(!(null==(e=t.payload)?void 0:e.fold))return t.children})).nodeSize((t=>{const[e,i]=t.data.state.size;return[i,e+(e?2*n:0)+r]})).spacing(((e,n)=>(e.parent===n.parent?i:2*i)+t(e.data))),s=a.hierarchy(this.state.data);a(s);const o=s.descendants();o.forEach((t=>{t.data.state.rect={x:t.y,y:t.x-t.xSize/2,width:t.ySize-r,height:t.xSize}})),this.state.rect={x1:e.min(o,(t=>t.data.state.rect.x))||0,y1:e.min(o,(t=>t.data.state.rect.y))||0,x2:e.max(o,(t=>t.data.state.rect.x+t.data.state.rect.width))||0,y2:e.max(o,(t=>t.data.state.rect.y+t.data.state.rect.height))||0}}setOptions(t){this.options={...this.options,...t},this.options.zoom?this.svg.call(this.zoom):this.svg.on(".zoom",null),this.options.pan?this.svg.on("wheel",this.handlePan):this.svg.on("wheel",null)}async setData(t,e){e&&this.setOptions(e),t&&(this.state.data=this._initializeData(t)),this.state.data&&(this.updateStyle(),await this.renderData())}async setHighlight(t){this.state.highlight=t||void 0,await this.renderData()}_getHighlightRect(t){const n=this.svg.node(),r=4/e.zoomTransform(n).k,i={...t.state.rect};return i.x-=r,i.y-=r,i.width+=2*r,i.height+=2*r,i}async renderData(t){const{paddingX:e,autoFit:n,color:r,maxWidth:i,lineWidth:s}=this.options,o=this.state.data;if(!o)return;const l={},h={},c=[];a(o,((t,e,n)=>{var r;(null==(r=t.payload)?void 0:r.fold)||e(),l[t.state.id]=t,n&&(h[t.state.id]=n.state.id),c.push(t)}));const d={},p={},u=t=>{t&&!d[t.state.id]&&a(t,((e,n)=>{d[e.state.id]=t.state.id,n()}))},g=t=>p[d[t.state.id]]||o.state.rect,m=t=>(l[d[t.state.id]]||o).state.rect;p[o.state.id]=o.state.rect,t&&u(t);let{highlight:f}=this.state;f&&!l[f.state.id]&&(f=void 0);let v=this.g.selectAll(X("g.markmap-highlight")).selectAll(X("rect")).data(f?[this._getHighlightRect(f)]:[]).join("rect").attr("x",(t=>t.x)).attr("y",(t=>t.y)).attr("width",(t=>t.width)).attr("height",(t=>t.height));const y=this.g.selectAll(X(Q)).each((t=>{p[t.state.id]=t.state.rect})).data(c,(t=>t.state.key)),x=y.enter().append("g").attr("data-depth",(t=>t.state.depth)).attr("data-path",(t=>t.state.path)).each((t=>{u(l[h[t.state.id]])})),k=y.exit().each((t=>{u(l[h[t.state.id]])})),w=y.merge(x).attr("class",(t=>{var e;return["markmap-node",(null==(e=t.payload)?void 0:e.fold)&&"markmap-fold"].filter(Boolean).join(" ")})),b=w.selectAll(X("line")).data((t=>[t]),(t=>t.state.key)),z=b.enter().append("line").attr("stroke",(t=>r(t))).attr("stroke-width",0),S=b.merge(z),E=w.selectAll(X("circle")).data((t=>{var e;return(null==(e=t.children)?void 0:e.length)?[t]:[]}),(t=>t.state.key)),C=E.enter().append("circle").attr("stroke-width",0).attr("r",0).on("click",((t,e)=>this.handleClick(t,e))).on("mousedown",et).merge(E).attr("stroke",(t=>r(t))).attr("fill",(t=>{var e;return(null==(e=t.payload)?void 0:e.fold)&&t.children?r(t):"var(--markmap-circle-open-bg)"})),A=this._observer,j=w.selectAll(X("foreignObject")).data((t=>[t]),(t=>t.state.key)),R=j.enter().append("foreignObject").attr("class","markmap-foreign").attr("x",e).attr("y",0).style("opacity",0).on("mousedown",et).on("dblclick",et);R.append("xhtml:div").append("xhtml:div").html((t=>t.content)).attr("xmlns","http://www.w3.org/1999/xhtml"),R.each((function(){var t;const e=null==(t=this.firstChild)?void 0:t.firstChild;A.observe(e)}));const O=k.selectAll(X("foreignObject"));O.each((function(){var t;const e=null==(t=this.firstChild)?void 0:t.firstChild;A.unobserve(e)}));const $=R.merge(j),M=c.flatMap((t=>{var e;return(null==(e=t.payload)?void 0:e.fold)?[]:t.children.map((e=>({source:t,target:e})))})),T=this.g.selectAll(X("path.markmap-link")).data(M,(t=>t.target.state.key)),H=T.exit(),B=T.enter().insert("path","g").attr("class","markmap-link").attr("data-depth",(t=>t.target.state.depth)).attr("data-path",(t=>t.target.state.path)).attr("d",(t=>{const e=g(t.target),n=[e.x+e.width,e.y+e.height];return U({source:n,target:n})})).attr("stroke-width",0).merge(T);this.svg.style("--markmap-max-width",i?`${i}px`:null),await new Promise(requestAnimationFrame),this._relayout(),v=v.data(f?[this._getHighlightRect(f)]:[]).join("rect"),this.transition(v).attr("x",(t=>t.x)).attr("y",(t=>t.y)).attr("width",(t=>t.width)).attr("height",(t=>t.height)),x.attr("transform",(t=>{const e=g(t);return`translate(${e.x+e.width-t.state.rect.width},${e.y+e.height-t.state.rect.height})`})),this.transition(k).attr("transform",(t=>{const e=m(t);return`translate(${e.x+e.width-t.state.rect.width},${e.y+e.height-t.state.rect.height})`})).remove(),this.transition(w).attr("transform",(t=>`translate(${t.state.rect.x},${t.state.rect.y})`));const _=k.selectAll(X("line"));this.transition(_).attr("x1",(t=>t.state.rect.width)).attr("stroke-width",0),z.attr("x1",(t=>t.state.rect.width)).attr("x2",(t=>t.state.rect.width)),S.attr("y1",(t=>t.state.rect.height+s(t)/2)).attr("y2",(t=>t.state.rect.height+s(t)/2)),this.transition(S).attr("x1",-1).attr("x2",(t=>t.state.rect.width+2)).attr("stroke",(t=>r(t))).attr("stroke-width",s);const N=k.selectAll(X("circle"));this.transition(N).attr("r",0).attr("stroke-width",0),C.attr("cx",(t=>t.state.rect.width)).attr("cy",(t=>t.state.rect.height+s(t)/2)),this.transition(C).attr("r",6).attr("stroke-width","1.5"),this.transition(O).style("opacity",0),$.attr("width",(t=>Math.max(0,t.state.rect.width-2*e))).attr("height",(t=>t.state.rect.height)),this.transition($).style("opacity",1),this.transition(H).attr("d",(t=>{const e=m(t.target),n=[e.x+e.width,e.y+e.height+s(t.target)/2];return U({source:n,target:n})})).attr("stroke-width",0).remove(),this.transition(B).attr("stroke",(t=>r(t.target))).attr("stroke-width",(t=>s(t.target))).attr("d",(t=>{const e=t.source,n=t.target,r=[e.state.rect.x+e.state.rect.width,e.state.rect.y+e.state.rect.height+s(e)/2],i=[n.state.rect.x,n.state.rect.y+n.state.rect.height+s(n)/2];return U({source:r,target:i})})),n&&this.fit()}transition(t){const{duration:e}=this.options;return t.transition().duration(e)}async fit(t=this.options.maxInitialScale){const n=this.svg.node(),{width:r,height:a}=n.getBoundingClientRect(),{fitRatio:s}=this.options,{x1:o,y1:l,x2:h,y2:c}=this.state.rect,d=h-o,p=c-l,u=Math.min(r/d*s,a/p*s,t),g=e.zoomIdentity.translate((r-d*u)/2-o*u,(a-p*u)/2-l*u).scale(u);return this.transition(this.svg).call(this.zoom.transform,g).end().catch(i)}findElement(t){let e;return this.g.selectAll(X(Q)).each((function(n){n===t&&(e={data:n,g:this})})),e}async ensureVisible(t,n){var r;const a=null==(r=this.findElement(t))?void 0:r.data;if(!a)return;const s=this.svg.node(),o=s.getBoundingClientRect(),l=e.zoomTransform(s),[h,c]=[a.state.rect.x,a.state.rect.x+a.state.rect.width+2].map((t=>t*l.k+l.x)),[d,p]=[a.state.rect.y,a.state.rect.y+a.state.rect.height].map((t=>t*l.k+l.y)),u={left:0,right:0,top:0,bottom:0,...n},g=[u.left-h,o.width-u.right-c],m=[u.top-d,o.height-u.bottom-p],f=g[0]*g[1]>0?tt(g,Math.abs)/l.k:0,v=m[0]*m[1]>0?tt(m,Math.abs)/l.k:0;if(f||v){const t=l.translate(f,v);return this.transition(this.svg).call(this.zoom.transform,t).end().catch(i)}}async centerNode(t,n){var r;const a=null==(r=this.findElement(t))?void 0:r.data;if(!a)return;const s=this.svg.node(),o=s.getBoundingClientRect(),l=e.zoomTransform(s),h=(a.state.rect.x+a.state.rect.width/2)*l.k+l.x,c=(a.state.rect.y+a.state.rect.height/2)*l.k+l.y,d={left:0,right:0,top:0,bottom:0,...n},p=(d.left+o.width-d.right)/2,u=(d.top+o.height-d.bottom)/2,g=(p-h)/l.k,m=(u-c)/l.k;if(g||m){const t=l.translate(g,m);return this.transition(this.svg).call(this.zoom.transform,t).end().catch(i)}}async rescale(t){const n=this.svg.node(),{width:r,height:a}=n.getBoundingClientRect(),s=r/2,o=a/2,l=e.zoomTransform(n),h=l.translate((s-l.x)*(1-t)/l.k,(o-l.y)*(1-t)/l.k).scale(t);return this.transition(this.svg).call(this.zoom.transform,h).end().catch(i)}destroy(){this.svg.on(".zoom",null),this.svg.html(null),this._disposeList.forEach((t=>{t()}))}static create(t,e,n=null){const r=new rt(t,e);return n&&r.setData(n).then((()=>{r.fit()})),r}}t.Markmap=rt,t.childSelector=X,t.defaultColorFn=z,t.defaultOptions=E,t.deriveOptions=function(t){const n={},r={...t},{color:i,colorFreezeLevel:a,lineWidth:s}=r;if(1===(null==i?void 0:i.length)){const t=i[0];n.color=()=>t}else if(null==i?void 0:i.length){const t=e.scaleOrdinal(i);n.color=e=>t(`${e.state.path}`)}if(a){const t=n.color||E.color;n.color=e=>(e={...e,state:{...e.state,path:e.state.path.split(".").slice(0,a).join(".")}},t(e))}if(s){const t=Array.isArray(s)?s:[s,0,1];n.lineWidth=S(...t)}return["duration","fitRatio","initialExpandLevel","maxInitialScale","maxWidth","nodeMinHeight","paddingX","spacingHorizontal","spacingVertical"].forEach((t=>{const e=r[t];"number"==typeof e&&(n[t]=e)})),["zoom","pan"].forEach((t=>{const e=r[t];null!=e&&(n[t]=!!e)})),n},t.globalCSS=J,t.isMacintosh=b,t.lineWidthFactory=S,t.loadCSS=async function(t){await Promise.all(t.map((t=>async function(t){const e="stylesheet"===t.type&&t.data.href||"";if(t.loaded||(t.loaded=k[e]),!t.loaded){const n=s();t.loaded=n.promise,e&&(k[e]=t.loaded),"style"===t.type?(document.head.append(v("style",{textContent:t.data})),n.resolve()):e&&(document.head.append(v("link",{rel:"stylesheet",...t.data})),fetch(e).then((t=>{if(t.ok)return t.text();throw t})).then((()=>n.resolve()),n.reject))}await t.loaded}(t))))},t.loadJS=async function(t,e){t.forEach((t=>{var e;"script"===t.type&&(null==(e=t.data)?void 0:e.src)&&y(t.data.src)})),e={getMarkmap:()=>window.markmap,...e};for(const n of t)await w(n,e)},t.refreshHook=nt,t.simpleHash=C,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}(this.markmap=this.markmap||{},d3);
10 | //# sourceMappingURL=/sm/110abf9063f6ee4fb04c8b1324a7593e2b618205c89e4a3ba636f91260ce2b5d.map
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * config.js
3 | * 挂件默认设置和全局配置。
4 | *
5 | * 【如果修改后崩溃或运行不正常,请删除挂件重新下载,或更改前手动备份】
6 | * 请不要删除//双斜杠
7 | * 请不要删除//双斜杠注释前的英文逗号,(如果有)
8 | * 为true 或者 false的设置项,只能填这两者
9 | * 有英文双引号的设置项,只更改英文双引号内的内容,不要删除英文双引号。
10 | * */
11 |
12 | // dev warn: Main.js多处使用Object.assign浅拷贝对象!
13 | let custom_attr = {//这里列出的是挂件的默认设置,只在创建时写入到挂件中,挂件内属性custom-list-child-docs可覆盖此设置
14 | printMode: "0",//默认格式和输出位置,参数见本文件最下方,或参考modeName(在本文件中搜索)
15 | childListId: "",//子文档列表块id,由挂件自动生成,对应的块将会被本挂件更新,请避免自行修改
16 | listDepth: 1,//列出子文档的最大层级,仅支持数字,过多层级将导致性能或其他潜在问题
17 | auto: true, //创建挂件、打开挂件时是否自动更新,如果您关闭了安全模式、使用同步且目录列表插入文档,请勿设定为true
18 | listColumn: 1,//子文档列表列数,过多的列数将导致显示问题
19 | outlineDepth: 3,//大纲列出层级数,混合列出时此项只控制大纲部分
20 | targetId: "", //统计对象id,统计的目标应当为文档块或笔记本
21 | endDocOutline: false, // 一并列出叶子文档的大纲?(目录中包括最深层级文档的大纲?)影响性能、反应极慢,建议禁用(设置为false)。(i.e.混合列出)
22 | // 如果需要默认隐藏刷新按钮,请删除下面一行前的双斜杠
23 | // hideRefreshBtn: true,
24 | sortBy: 256, //排序模式,具体取值请参考本文件最下方的DOC_SORT_TYPES,默认值15为跟随文档树排序
25 | maxListCount: 0,//控制每个文档的子文档显示数量
26 | };
27 | // 全局设置
28 | let setting = {
29 | // 将列表写入文件时,此项控制挂件的宽
30 | width_2file: "20em",
31 | // 将列表写入文件时,此项控制挂件的高
32 | height_2file: "4em",
33 | // 将列表写入文件时,此项控制显示设置时挂件的高
34 | height_2file_setting: "9em",
35 |
36 | // 在挂件中显示自动刷新选项,设定true启用、false禁用【!自动刷新可能导致同步覆盖问题,详见README】
37 | showAutoBtn: true,
38 | // 在启动时显示所有设置项,设定true启用
39 | showSettingOnStartUp: false,
40 | // 显示搜索按钮
41 | showSearchBtn: true,
42 |
43 | // 安全模式【!建议开启,设定为true】:安全模式将禁止打开文档时自动刷新文档中的目录列表块
44 | // 可以避免此挂件自行刷新导致可能的同步覆盖问题。
45 | safeMode: false,
46 | // 安全模式PLUS【!可能因为思源版本更新而失效或导致bug,但部分情况下建议开启】
47 | // 避免在历史预览界面、编辑器只读时执行文档更改操作(目前允许挂件设置保存,请注意只读情况下设置保存的风险)
48 | // 【如果您使用自动插入助手,请启用此功能】
49 | safeModePlus: true,
50 |
51 | // 分列截断提示词(仅用于写入文档模式:url、引用块)
52 | divideIndentWord: "(续)",
53 |
54 | // 分列截断方式(仅用于写入文档模式:url、引用块
55 | // 为true: 多层级时,在缩进处截断,使每列行数相同,但层级>=2时体验不佳;
56 | // 为false,按照第一层级分列,每列行数不等
57 | divideColumnAtIndent: false,
58 |
59 | // 为true启用挂件内浮窗(挂件beta模式)
60 | floatWindowEnable: true,
61 |
62 | // 使用玄学的超级块创建方式。如果出现问题,请设置为false(测试中)
63 | superBlockBeta: true,
64 |
65 | // 混合列出时区分提示词(启用叶子文档大纲时,该提示词将被加载大纲的前面)
66 | outlineDistinguishingWords: "@",
67 |
68 | // 刷新列表后重写属性
69 | inheritAttrs: true,
70 |
71 | // 为true则一并写入文档icon Emoji
72 | emojiEnable: true,
73 | // 文档使用自定义emoji时,写入自定义emoji图片
74 | customEmojiEnable: true,
75 |
76 | // 在模式“默认”“挂件beta”下,使得挂件高度跟随目录长度自动调整
77 | autoHeight: false,
78 | // 将列表在挂件内展示、且启用自动高度,此项控制挂件的最小高度(单位px),若不限制,请设为undefined
79 | height_2widget_min: undefined,
80 | // 将列表在挂件内展示、且启用自动高度,此项控制挂件的最大高度(单位px),若不限制,请设为undefined
81 | height_2widget_max: undefined,
82 |
83 | // 【在插入挂件时表现不稳定,可能在第二次打开时设定、保存样式】挂件保存1次自身的显示样式,设置为undefined以禁用
84 | // issue #30 https://github.com/OpaqueGlass/listChildDocs/issues/30
85 | // 示例 "width: 2000px; height: 303px;"
86 | saveDefaultWidgetStyle: undefined,
87 |
88 | /* 挂件配置批量操作
89 | issue #31 https://github.com/OpaqueGlass/listChildDocs/issues/31
90 | !同步用户请注意:以下两个配置启用后挂件将在载入后更新挂件属性,未同步时可能导致同步覆盖
91 | */
92 | // 载入挂件后以配置文件为准重写独立设置
93 | overwriteIndependentSettings: false,
94 | // 载入挂件后移除独立设置
95 | removeIndependentSettings: false,
96 | // 重载/移除设置时一并删除文档中的原目录列表块;(如果重载为文档中模式,不会执行删除)
97 | deleteChildListBlockWhileReset: true,
98 | // 独立设置重载或移除白名单
99 | // 在这里列出的文档下的挂件,不会执行独立设置重载或移除
100 | // 示例["20220815001720-4xjvir0"]
101 | overwriteOrRemoveWhiteDocList: [],
102 |
103 | // 未完成功能 插入https:// 或 http:// 协议的emoji,
104 | webEmojiEnable: false,
105 |
106 | // 在目录列表第一个加入“../”(前往父文档)(仅挂件内目录),此设定项的类型为字符串,"true"(启用)"false"(禁用)"auto"(仅窄屏设备展示)
107 | backToParent: "auto",
108 |
109 | // 挂件内时,扩大点击响应范围为整行
110 | extendClickArea: true,
111 |
112 | // 适配挂件插入辅助(addChildDocLinkHelper.js)的属性检测模式,为所在文档插入属性(不建议一直开启,请开启此功能后几天关闭)
113 | // 默认情况下,无需打开此功能
114 | addChildDocLinkHelperEnable: false,
115 |
116 | // 首次创建目录块时插入的目录属性
117 | // 请注意,您写入的属性如果是自定义属性,应当以"custom-"开头,示例 "custom-type": "map"
118 | // 请不要写入"id","update"等块固有属性
119 | blockInitAttrs: {
120 |
121 | },
122 |
123 | // 在页签切换文档时自动刷新功能将在列出的操作系统上启用,不支持不显示页签的客户端
124 | // 若要禁用,值应当为[];如要在windows启用,["windows"];如要在多个操作系统上启用,示例:["linux", "windows"]
125 | includeOs: ["windows"],
126 |
127 | // 导图模式Markmap配置项,详见https://markmap.js.org/docs/json-options
128 | markmapConfig: {},
129 | // 导图模式:响应挂件大小变化
130 | markmapResizeHandlerEnable: true,
131 |
132 | // 按时间分组模式,显示日期的格式,yyyy将被替换为4位年,MM将被替换为两位数月份,dd将被替换为两位数日
133 | dateTemplate: "MM-dd",
134 | // 按时间分组模式,显示时间的格式,设置为""则不显示时间,HH将被替换为小时(24小时制),mm将被替换为分钟
135 | timeTemplate: "(HH:mm)",
136 |
137 | // 缓存只对挂件中显示的模式有效
138 | // 先载入缓存,再执行自动刷新
139 | loadCacheWhileAutoEnable: false,
140 | // 在自动刷新时也自动保存缓存(!同步用户请注意:多端同步未完成时保存缓存,可能导致同步覆盖)
141 | saveCacheWhileAutoEnable: false,
142 |
143 | // 右键重命名或删除操作
144 | deleteOrRenameEnable: true,
145 |
146 | // 使用Ctrl+F作为搜索快捷键(焦点在挂件内才生效)
147 | searchHotkeyEnable: false,
148 |
149 | // 悬停显示顶部按钮栏
150 | mouseoverButtonArea: false,
151 | };
152 | // 自动插入助手设置
153 | // 自动插入助手和挂件本体共用setting.safeModePlus(只读安全模式检查设置项),如果您使用自动插入助手,请启用此功能。
154 | let helperSettings = {
155 | // 文档中属性名称
156 | attrName: "custom-add-cdl-helper",
157 | // 模式为插入自定义时,插入的内容模板
158 | docLinkTemplate: "((%DOC_ID% '%DOC_NAME%'))",
159 | // 自动插入模式
160 | /* 【请只使用“插入挂件”模式。其他模式可能存在问题,请勿使用。】
161 | 插入挂件 【插入挂件将不重复插入(通过属性或文档为空判断)】
162 | 插入链接【可能有缺陷,不建议使用】
163 | 插入引用块【可能有缺陷,不建议使用】
164 | 插入自定义【可能有缺陷,不建议使用】 根据docLinkTemplate,插入自定义的内容
165 | */
166 | mode: "插入挂件",// 除非您了解这部分代码实现,请不要修改模式!Do not edit it unless you understand the codes.
167 | /* 通用 */
168 | // 插入在父文档结尾?若设置为undefined,则采用对应模式的默认设置
169 | insertAtEnd: undefined,
170 | // 在切换页签(而不是仅仅是打开)时也检查、执行自动插入
171 | switchTabEnable: false,
172 |
173 | /* 仅插入挂件模式 */
174 | // 检查文档是否为空?设置为false,将通过文档的属性判断是否插入过。
175 | checkEmptyDocInsertWidget: true,
176 | // 选择触发时机:
177 | /*
178 | // "open": 开启空白的父文档时;
179 | // "create": 在空白父文档下创建子文档时(不建议修改为create,在这种实现方式下,将持续获取WebSocket通信并判断是否进行了创建文档操作);
180 | */
181 | insertWidgetMoment: "open",
182 | // 要插入的挂件路径信息
183 | widgetPath: ["widgets/listChildDocs"],
184 |
185 | /* 插入链接/引用块/自定义模式 */
186 | // 当发现子文档被删除,移除对应的子文档链接?若设置为undefined,则采用对应模式的默认设置
187 | removeLinkEnable: false,
188 | // 当发现子文档文件名变化时,重写对应的子文档链接?若设置为undefined,则采用对应模式的默认设置
189 | renameLinkEnable: false,
190 | }
191 | //全局设置
192 | let token = "";//API鉴权token,可以不填的样子(在设置-关于中查看)
193 | let zh_CN = {
194 | refreshNeeded: "更新目录失败,找不到原有无序列表块,再次刷新将创建新块。",
195 | insertBlockFailed: "创建或更新无序列表块失败,请稍后刷新重试。",
196 | writeAttrFailed: "写入挂件属性失败,请稍后刷新重试。",
197 | getPathFailed: "查询当前文档所属路径失败,请稍后刷新重试。",
198 | noChildDoc: "似乎没有子文档@_@。",
199 | error: "错误:",
200 | updateTime: "更新时间:",
201 | modifywarn: "此块由listChildDocs挂件创建,若刷新列表,您的更改将会被覆盖。", // 不想显示这个提示的话,改成空字符串""就行
202 | getAttrFailed: "读取挂件属性失败。",
203 | wrongPrintMode: "错误的输出模式设定,已恢复默认值,请刷新重试。",
204 | // 模式提示词
205 | modeName0: "默认",
206 | modeName1: "挂件beta",
207 | modeName2: "url",
208 | modeName3: "引用块",
209 | modeName5: "1.1.默认",
210 | modeName4: "1.1.挂件",
211 | modeName6: "1.url",
212 | modeName7: "1.引用块",
213 | modeName8: "1.1.url",
214 | modeName9: "任务列表",
215 | modeName10: "导图",
216 | modeName11: "预览方格",
217 | modeName12: "按日期分组",
218 | // 界面元素鼠标悬停提示词
219 | refreshBtn: "[单击] 刷新\n[双击] 保存设置",
220 | depthList: "子文档展示层级\n设置为0就可以只显示大纲啦~\(≧▽≦)/~",
221 | searchBtnTitle: "显示搜索对话框",
222 | modeList: "挂件工作模式",
223 | autoBtn: "自动刷新",
224 | autoNotWork: "\n由于启用了安全模式(safeMode),自动刷新对当前工作模式无效。",
225 | targetIdTitle: "目标文档id\n从这里指定的文档或笔记本开始列出子文档,\n设定为/则从所有已开启的笔记本开始",
226 | disabledBtnHint: "\n因为不支持当前模式,我被禁用了T^T",
227 | endDocOutlineTitle: "启用后,对于目录列表中没有子文档的,将显示大纲",
228 | hideRefreshBtnTitle: "将刷新按钮搬运到设置中,防止误触",
229 | outlineDepthTitle: "大纲层级\n大纲层级和h1、h2等无关,以大纲面板显示的层次为准。",
230 | sortByTitle: "控制文档的排序方式\n请在思源v2.8.7及以上版本使用,较早的版本可能无法排序",
231 | maxListCountTitle: "每个文档的子文档显示数量(设置为0则显示全部)\n不支持思源2.8.5以下版本",
232 | // 错误提示词
233 | getAttrFailedAtInit: "读取挂件属性失败。如果是刚创建挂件,请稍后刷新重试。",
234 | startRefresh: "开始更新子文档列表---来自listChildDocs挂件的通知",
235 | widgetRefLink: "挂件beta",
236 | saved: "设置项已保存",
237 | columnBtn: "子文档展示列数",
238 | settingBtn: "显示/隐藏设置",
239 | // 界面提示词
240 | columnHint: "分列",
241 | depthHint: "层级",
242 | noOutline: "似乎没有文档大纲@_@。",
243 | outlineDepthHint: "大纲层级",
244 | endDocOutlineHint: "叶子文档大纲",
245 | targetIdhint: "目标文档id",
246 | hideRefreshBtnHint: "隐藏刷新按钮",
247 | sortByHint: "排序方式",
248 | maxListCountHint: "子文档最大数量",
249 | autoRefreshHint: "自动刷新",
250 | working: "执行中……",
251 | loadingCache: "载入缓存中",
252 | cacheLoaded: "已载入缓存",
253 | loadCacheFailed: "未能载入文档列表缓存",
254 | wrongTargetId: "错误的目标id。目标id应为存在的文档块id、开启的笔记本id或/",
255 | readonly: "检测到只读模式,已停止对文档的更改操作。",
256 | saveDefaultStyleFailed: "保存默认挂件样式设定失败,如反复出现此问题,请禁用saveDefaultWidgetStyle。",
257 | refreshFinish: "刷新完成",
258 | refreshReject: "刷新被拒绝",
259 | refreshFailed: "刷新出错",
260 | // 自动插入助手提示
261 | helperAddBlockMemo: "自动插入的子文档链接块:在此块下的编辑将在文档变化时被覆盖",
262 | queryFilePathFailed: "获取文档路径失败,文档可能刚创建",
263 | helperErrorHint: "helper执行时发生错误,如果可以,请向开发者反馈:",
264 | // 模式内部提示10
265 | mode10_allow_pan: "启用滚轮平移",
266 | mode10_allow_zoom: "启用(Alt+滚轮)缩放与拖拽移动",
267 | mode10_hint: "折叠、滚轮缩放与拖拽移动状态将在鼠标操作后自动暂存,之后手动保存挂件设置(或双击刷新按钮)才能持久化保存。
没有看到任何内容?点击“重置缩放、平移和折叠状态”按钮,然后手动保存挂件设置",
268 | mode10_reset: "重置缩放、平移和折叠状态",
269 | mode10_default_expand_level_hint: "默认展开层级数",
270 | // 模式内部提示12
271 | mode12_doc_num_text: "展示的文档数",
272 | mode12_update_hint: "按照更新时间排列",
273 | mode12_today: "(今天)",
274 | mode12_yesterday: "(昨天)",
275 | mode12_day_ago: "(%%天前)",
276 | mode12_week_day: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
277 | // 模式内部提示13
278 | mode13_cannot_select_folder: "此模式只适用于桌面端(electron 或 nodejs环境),当前无法选择目录。",
279 | mode13_select_folder: "指定本地目录",
280 | mode13_not_select_folder: "您似乎没有选择目录",
281 | mode13_show_what: "显示什么?",
282 | mode13_display_path_here: "[这里显示您选择的路径]",
283 | mode13_only_folder: "仅文件夹",
284 | mode13_only_file: "仅文件",
285 | mode13_show_all: "文件夹和文件",
286 | mode13_cannot_refresh: "此模式仅支持桌面端,移动端、Docker、浏览器环境无法更新。",
287 | mode13_not_select_folder_when_refresh: "您似乎没有选择目录。请在模式设置中指定本机目标路径。",
288 | mode13_trust_sysid: "在当前系统使用相同的目录路径",
289 | mode13_another_sys_warn: "和您选择目录时操作系统不匹配,继续刷新可能出现异常。
选择“确认”需要您为当前系统重新选择路径(其他系统路径仍然保留),选择“取消”则终止本次刷新。
",
290 | mode13_clear_all_path: "取消指定(当前系统)",
291 | mode13_error_while_select_folder: "选文件夹时出现错误,请重选",
292 | mode14_view_notfound: "没有在紧邻下方块找到数据库",
293 | mode14_first_use: "看起来这是您首次使用",
294 | mode14_first_use_content: "由于写入不可撤销、个人开发测试不周,因此请只用于对空白数据库的写入,如果包含大量其他内容,请做好备份(深克隆数据库、不是镜像数据库);
使用此模式,请先在挂件紧邻下方块(位于同一父块)使用斜杠菜单创建数据库;使用时,请避免连续刷新。
稍后手动保存挂件设置以忽略此提示
",
295 | // 对话框dialog
296 | dialog_canceled: "已取消",
297 | dialog_delete: "删除",
298 | dialog_delete_hint: "确定要删除所选文档“%%”吗?
请注意,如果有子文档,子文档也将被一并删除。",
299 | dialog_rename: "重命名",
300 | dialog_cancel: "取消",
301 | dialog_confirm: "确定",
302 | dialog_create_doc: "新建子文档",
303 | dialog_option: "已选择",
304 | dialog_search: "搜索",
305 | dialog_search_cancel: "清除高亮",
306 | dialog_search_panel: "搜索文档标题",
307 | dialog_search_nomatch: "无结果",
308 | doc_sort_type: {
309 | FILE_NAME_ASC: "名称字母升序",
310 | FILE_NAME_DESC: "名称字母降序",
311 | NAME_NAT_ASC: "名称自然升序",
312 | NAME_NAT_DESC: "名称自然降序",
313 | MODIFIED_TIME_ASC: "修改时间升序",
314 | MODIFIED_TIME_DESC: "修改时间降序",
315 | CREATED_TIME_ASC: "创建时间升序",
316 | CREATED_TIME_DESC: "创建时间降序",
317 | REF_COUNT_ASC: "引用次数升序",
318 | REF_COUNT_DESC: "引用次数降序",
319 | DOC_SIZE_ASC: "文档大小升序",
320 | DOC_SIZE_DESC: "文档大小降序",
321 | SUB_DOC_COUNT_ASC: "子文档数量升序",
322 | SUB_DOC_COUNT_DESC: "子文档数量降序",
323 | CUSTOM_SORT: "文档树自定义排序",
324 | UNASSIGNED: "跟随文档树排序",
325 | },
326 | // 弹层提示词
327 | removeDistinctSuccess: "成功删除%1%个挂件的独立设置。",
328 | removeDistinctFailed: "成功删除%1%个挂件的独立设置,失败%2%个。失败的挂件id分别是:%3%",
329 | removeOtherSuccess: "成功删除%1%个挂件",
330 | removeOtherFailed: "成功删除%1%个挂件,失败%2%个。失败的挂件id分别是:%3%",
331 | workResult: "结果",
332 | removeDistinctConfim: "确定要删除其他挂件的独立设置吗?
请注意:1. 挂件在文档中插入的列表也将被一并删除;
2. 受限于API查询数量限制,您可能需要多次执行此操作以确保完全删除。",
333 | removeCurrentDistinctConfim: "确定删除当前挂件独立设置吗?
请注意:1.删除后,当前挂件独立设置将跟随全局,直到下次保存独立设置。
2. 由当前挂件创建的子文档列表也将被一并删除",
334 | removeOtherConfim: "确定要删除其他挂件吗?
请注意:1. 挂件在文档中插入的列表不会被一并删除;
2. 受限于API查询数量限制,您可能需要多次执行此操作以确保完全删除。",
335 | removeFileConfirm: "确定要删除不使用的配置文件吗?
(如要删除所有配置文件,请前往工作空间/data/storage/listChildDocs手动删除)",
336 | removeFileSuccess: "成功删除%1%个配置文件。另有%2%个在使用中的配置文件未清理。",
337 | confirmTitle: "二次确认",
338 | configNameSet: "请输入配置名称",
339 | currentDoc: "当前文档",
340 | deletedSchema: "所选配置已删除",
341 | childDocsCreated: "已创建",
342 | };
343 | let en_US = {//先当他不存在 We don't fully support English yet.
344 | refreshNeeded: "Failed to refresh directory : couldn't find original directory list block. Click refresh button again to generate a new block. ",
345 | insertBlockFailed: "Failed to create or update the child-docs list block, please try again later. ",
346 | writeAttrFailed: "Failed to write widget properties, please try again later. ",
347 | getPathFailed: "Failed to get the path of current document, please try again later. ",
348 | noChildDoc: "There appears to be no child-docs.",
349 | error: "ERROR: ",
350 | updateTime: "Last update: ",
351 | modifywarn: "Created by listChildDocs widget. Your changes to this block will be overwritten when you click refresh button in the widget",
352 | getAttrFailed: "Failed to get widget properties.",
353 | wrongPrintMode: "Wrong output mode setting, default value restored, please refresh again.",
354 | // 模式提示词 Mode Name
355 | modeName0: "Default",
356 | modeName1: "Widget beta",
357 | modeName2: "siyuan url",
358 | modeName3: "ref block",
359 | modeName5: "1.1.Default",
360 | modeName4: "1.1.Widget",
361 | modeName6: "1.url",
362 | modeName7: "1.ref block",
363 | modeName8: "1.1.url",
364 | modeName9: "todo list",
365 | modeName10: "markmap",
366 | modeName11: "preview box",
367 | modeName12: "group by date",
368 | // 界面元素鼠标悬停提示词 hangover popup words
369 | refreshBtn: "[Click] Refresh\n[Double click] Save Settings",
370 | searchBtnTitle: "Show search dialog",
371 | depthList: "The number of display levels for the child docs",
372 | modeList: "Output mode",
373 | autoBtn: "'Auto' Refresh",
374 | autoNotWork: "\nNot available for current output mode, because safe mode is enabled",
375 | targetIdTitle: "Target doc id\nAlso accept notebookid, '/' as target id.",
376 | disabledBtnHint: "\nDisabled by current mode.",
377 | endDocOutlineTitle: "For the documents that have no subdocuments, display their outline.",
378 | hideRefreshBtnTitle: "Move refresh button into settings.",
379 | outlineDepthTitle: "The number of display levels for the doc outine. ",
380 | sortByTitle: "child docs sort mode\n available in siyuan v2.8.7 and later",
381 | maxListCountTitle: "Maximum number of subdocuments to be displayed for each document. If set to 0, all documents are displayed. Versions earlier than siyuan v2.8.5 are not supported.",
382 | refreshFinish: "Refreshed. ",
383 | refreshReject: "Refresh was rejected. ",
384 | refreshFailed: "An error occurred. ",
385 | // 错误提示词error warn
386 | getAttrFailedAtInit: "Failed to read widget properties. If you just created the widget, please ignore this error and refresh again later.",
387 | startRefresh: "Updating child-doc-list ... --- list child docs widget",
388 | widgetRefLink: "Widget beta",
389 | saved: "Settings have been saved",
390 | // 界面控件提示词 Hint words
391 | columnBtn: "Number of columns",
392 | settingBtn: "Show/hide settings",
393 | columnHint: "Column",
394 | depthHint: "Level",
395 | noOutline: "There appears to be no doc-outline.",
396 | outlineDepthHint: "Outline level",
397 | endDocOutlineHint: "Leaf document outline",
398 | targetIdhint: "Target document id",
399 | hideRefreshBtnHint: "Hide refresh button",
400 | sortByHint: "Sort Mode",
401 | maxListCountHint: "Maximum of sub-docs",
402 | autoRefreshHint: "Auto refresh",
403 | //
404 | working: "Running...",
405 | loadingCache: "Loading...",
406 | cacheLoaded: "Cache loaded.",
407 | loadCacheFailed: "Couldn't load doc-list cache.",
408 | wrongTargetId: "Wrong target doc id. The target id should be an existing document id, an open notebook id or /",
409 | readonly: "Work in read-only mode. Changes to the document are prohibited.",
410 | saveDefaultStyleFailed: "Failed to save default pendant style settings. If this problem occurs repeatedly, please disable saveDefaultWidgetStyle.",
411 | // addChildDocLinkHelper hint text
412 | helperAddBlockMemo: "Child-doc link block: the edits under this block will be overwritten when the child-docs changes.",
413 | queryFilePathFailed: "Failed to get the document path, the document may have just been created.",
414 | helperErrorHint: "An error occured during helper execution. If it's convenient for you, please give feedback to the developer.",
415 | // markmap
416 | mode10_allow_pan: "Enable Pan",
417 | mode10_allow_zoom: "Enable zoom(with Alt Key)",
418 | mode10_hint: "The collapse state will be temporarily saved automatically. To make it persistent, please manually save the widget settings.
Right-click inside the mind map to temporarily save the current zoom and pan state. Then manually save the widget settings to persist it.
Not seeing any content? Click the “Reset Zoom, Pan, and Collapse State” button, then manually refresh and save the widget settings",
419 | mode10_reset: "Reset Zoom, Pan, and Collapse State",
420 | mode10_default_expand_level_hint: "Default Expand Level",
421 | // hint text in mode
422 | mode12_doc_num_text: "the num of doc",
423 | mode12_update_hint: "Order by update time",
424 | mode12_today: "(today)",
425 | mode12_yesterday: "(yesterday)",
426 | mode12_day_ago: "(%% days ago)",
427 | mode12_week_day: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"],
428 | // 模式内部提示 tips in mode13
429 | mode13_cannot_select_folder: " This mode is only applicable in desktop environments (electron or nodejs), so you cannot select a directory at present.",
430 | mode13_select_folder: "Specify the local directory",
431 | mode13_show_what: "What should be included?",
432 | mode13_display_path_here: "[The path you selected should be displayed here",
433 | mode13_not_select_folder: "It seems you have not selected a directory.",
434 | mode13_only_folder: "Only folders",
435 | mode13_only_file: "Only files",
436 | mode13_show_all: "Folders and files",
437 | mode13_cannot_refresh: "This mode is only supported for desktop, so cannot be updated in mobile, Docker, or browser environments.",
438 | mode13_not_select_folder_when_refresh: "It seems you have not selected a directory. Please specify the local path in the mode settings.",
439 | mode13_trust_sysid: "Allow Refresh in Current System with Same Directory",
440 | mode13_another_sys_warn: "The operating system you selected does not match. Continuing the refresh may lead to exceptions. Clicking 'Confirm' requires you to reselect the path for the current system (other system paths will remain unchanged). Clicking 'Cancel' will abort the refresh.
",
441 | mode13_mode13_clear_all_path: "Unset Directory (For Current System)",
442 | mode13_error_while_select_folder: "An error occured during selecting folder, please try again",
443 | mode14_view_notfound: "No database found in the immediately adjacent block below.",
444 | mode14_first_use: "It looks like this is your first use",
445 | mode14_first_use_content: "Since write operations are irreversible and personal development testing is insufficient, please use this mode only for writing to a blank database. If the database contains a lot of other content, make sure to back it up first (Deep clone it, not mirror).
To use this mode, please first use the slash menu to create a database in the block immediately below the widget (within the same parent block). Please avoid refreshing continuously.
Save the widget settings manually later to ignore this prompt.
",
446 | // dialog
447 | dialog_canceled: "Canceled",
448 | dialog_delete: "Delete",
449 | dialog_delete_hint: "Are you sure you want to delete the selected document \"%%\"?
Please note that if there are subdocuments, they will also be deleted.",
450 | dialog_rename: "Rename",
451 | dialog_cancel: "Cancel",
452 | dialog_confirm: "OK",
453 | dialog_create_doc: "Create_child_doc",
454 | dialog_option: "Selected",
455 | dialog_search: "Search",
456 | dialog_search_cancel: "Clear",
457 | dialog_search_panel: "Search by Doc Name",
458 | dialog_search_nomatch: "No match",
459 | doc_sort_type: {
460 | FILE_NAME_ASC: "Name Alphabet ASC",
461 | FILE_NAME_DESC: "Name Alphabet DESC",
462 | NAME_NAT_ASC: "Name Natural ASC",
463 | NAME_NAT_DESC: "Name Natural DESC",
464 | MODIFIED_TIME_ASC: "Modified Time ASC",
465 | MODIFIED_TIME_DESC: "Modified Time DESC",
466 | CREATED_TIME_ASC: "Created Time ASC",
467 | CREATED_TIME_DESC: "Created Time DESC",
468 | REF_COUNT_ASC: "Ref Count ASC",
469 | REF_COUNT_DESC: "Ref Count DESC",
470 | DOC_SIZE_ASC: "Document Size ASC",
471 | DOC_SIZE_DESC: "Document Size DESC",
472 | SUB_DOC_COUNT_ASC: "Sub-docs Count ASC",
473 | SUB_DOC_COUNT_DESC: "Sub-docs Count DESC",
474 | CUSTOM_SORT: "Custom Sorting in the File Tree",
475 | UNASSIGNED: "Follow the File Tree"
476 | },
477 | // 弹层提示词
478 | removeDistinctSuccess: "Successfully deleted inpendent settings for %1% widgets.",
479 | removeDistinctFailed: "Successfully removed inpendent settings for %1% widgets, failed %2%. Failed widget ids are: %3%",
480 | removeOtherSuccess: "Successfully deleted %1% widgets",
481 | removeOtherFailed: "Successfully deleted %1% widgets, failed %2%. Failed widget ids are: %3%",
482 | workResult: "Result",
483 | removeDistinctConfim: "Are you sure you want to delete the inpendent settings for other widgets?
Notice that the list created by the widget will also be deleted.",
484 | removeCurrentDistinctConfim: "Are you sure you want to delete the current widget independent settings?
Note: 1. After deletion, the current widget inpendent settings will follow the global until the next time you save settings.
2. The child docs list created by the widget will also be deleted ",
485 | removeOtherConfim: "Are you sure you want to delete other widgets?
Notice that the list created by the widget will NOT be deleted.",
486 | removeFileConfirm: "Are you sure you want to delete unused config files?
(If you want to delete all config files, please go to workspace/data/storage/listChildDocs to delete them manually)",
487 | removeFileSuccess: "Successfully deleted %1% profiles. Another %2% active profiles were not cleaned.",
488 | confirmTitle: "Secondary Confirmation",
489 | configNameSet: "Please enter the configuration name",
490 | currentDoc: "Current Doc",
491 | deletedSchema: "The selected schema has been deleted.",
492 | childDocsCreated: "Created"
493 | };
494 | let language = zh_CN; // 使用的语言 the language in use. Only zh_CN and en_US are available.
495 | // ~~若思源设定非中文,则显示英文~~
496 | let siyuanLanguage;
497 | try{
498 | siyuanLanguage = window.top.siyuan.config.lang;
499 | }catch (err){
500 | console.warn("读取语言信息失败");
501 | }
502 | if (siyuanLanguage != "zh_CN" && siyuanLanguage != undefined) {
503 | language = en_US;
504 | }
505 |
506 |
507 | // 导入外部config.js 测试功能,如果您不清楚,请避免修改此部分;
508 |
509 | //注:下方的排序分类可能不会随着思源版本而及时更新
510 | const SORT_TYPES = {
511 | FILE_NAME_ASC: {type: 0, name: "文件名升序", englishName: "Name Alphabet ASC"},
512 | FILE_NAME_DESC: {type: 1, name: "文件名降序", englishName: "Name Alphabet DESC"},
513 | NAME_NAT_ASC: {type: 4, name: "名称自然升序", englishName: "Name Natural ASC"},
514 | NAME_NAT_DESC: {type: 5, name: "名称自然降序", englishName: "Name Natural DESC"},
515 | MODIFIED_TIME_ASC: {type: 2, name: "修改时间升序", englishName: "Modified Time ASC"},
516 | MODIFIED_TIME_DESC: {type: 3, name: "修改时间降序", englishName: "Modified Time DESC"},
517 | CREATED_TIME_ASC: {type: 9, name: "创建时间升序", englishName: "Created Time ASC"},
518 | CREATED_TIME_DESC: {type: 10, name: "创建时间降序", englishName: "Created Time DESC"},
519 | REF_COUNT_ASC: {type: 7, name: "引用次数升序", englishName: "Ref Count ASC"},
520 | REF_COUNT_DESC: {type: 8, name: "引用次数降序", englishName: "Ref Count DESC"},
521 | DOC_SIZE_ASC: {type: 11, name: "文档大小升序", englishName: "Document Size ASC"},
522 | DOC_SIZE_DESC: {type: 12, name: "文档大小降序", englishName: "Document Size DESC"},
523 | SUB_DOC_COUNT_ASC: {type: 13, name: "子文档数量升序", englishName: "Sub-docs Count ASC"},
524 | SUB_DOC_COUNT_DESC: {type: 14, name: "子文档数量降序", englishName: "Sub-docs Count DESC"},
525 | CUSTOM_SORT: {type: 6, name: "自定义排序", englishName: "Custom Sorting in the File Tree"},
526 | FOLLOW_DOC_TREE: {type: 256, name: "跟随文档树排序", englishName: "Follow Doc Tree Sorting"},
527 | };
528 |
529 |
530 | export {custom_attr, token, language, setting, helperSettings};
531 | /* printerMode参数
532 | 0 默认
533 | 1 挂件beta
534 | 2 url
535 | 3 引用块
536 | 4 1.1.挂件
537 | 5 1.1.默认
538 | 6 1.url
539 | 7 1.引用块
540 | 8 1.1.url
541 | 9 todo列表(任务列表)url
542 | 10 导图
543 | 11 预览方格
544 | 12 按时间分组
545 | */
--------------------------------------------------------------------------------
/src/API.js:
--------------------------------------------------------------------------------
1 | /**
2 | * API.js
3 | * 用于发送思源api请求。
4 | */
5 | import {token, setting} from "./config.js";
6 | import { isValidStr, logPush, warnPush, errorPush, debugPush } from "./common.js";
7 | /**向思源api发送请求
8 | * @param data 传递的信息(body)
9 | * @param url 请求的地址
10 | */
11 | export async function postRequest(data, url){
12 | let result;
13 | await fetch(url, {
14 | body: JSON.stringify(data),
15 | method: 'POST',
16 | headers: {
17 | "Authorization": "Token "+token,
18 | "Content-Type": "application/json"
19 | }
20 | }).then((response) => {
21 | result = response.json();
22 | });
23 | return result;
24 | }
25 |
26 | export async function checkResponse4Result(response){
27 | if (response.code != 0 || response.data == null){
28 | return null;
29 | }else{
30 | return response;
31 | }
32 | }
33 |
34 | /**
35 | * 检查请求是否成功,返回0、-1
36 | * @param {*} response
37 | * @returns 成功为0,失败为-1
38 | */
39 | export async function checkResponse(response){
40 | if (response.code == 0){
41 | return 0;
42 | }else{
43 | return -1;
44 | }
45 | }
46 |
47 | /**SQL(api)
48 | * @param sqlstmt SQL语句
49 | */
50 | export async function queryAPI(sqlstmt){
51 | let url = "/api/query/sql";
52 | let response = await postRequest({stmt: sqlstmt},url);
53 | if (response.code == 0 && response.data != null){
54 | return response.data;
55 | }
56 | if (response.msg != "") {
57 | throw new Error(`SQL ERROR: ${response.msg}`);
58 | }
59 |
60 | return null;
61 | }
62 |
63 | /**重建索引
64 | * @param docpath 需要重建索引的文档路径
65 | */
66 | export async function reindexDoc(docpath){
67 | let url = "/api/filetree/reindexTree";
68 | let response = await postRequest({path: docpath},url);
69 | return 0;
70 | }
71 |
72 | /**列出子文件(api)
73 | * @param notebookId 笔记本id
74 | * @param path 需要列出子文件的路径
75 | * @param maxListCount 子文档最大显示数量
76 | * @param sort 排序方式(类型号)
77 | */
78 | export async function getSubDocsAPI(notebookId, path, maxListCount = undefined, sort = undefined, showHidden = undefined){
79 | let url = "/api/filetree/listDocsByPath";
80 | let body = {
81 | "notebook": notebookId,
82 | "path": path,
83 | "ignoreMaxListHint": true,
84 | }
85 | if (maxListCount != undefined && maxListCount >= 0) {
86 | body["maxListCount"] = (maxListCount > 32 || maxListCount == 0) ? maxListCount : 32;
87 | }
88 | if (sort != undefined && sort != DOC_SORT_TYPES.FOLLOW_DOC_TREE && sort != DOC_SORT_TYPES.UNASSIGNED) {
89 | body["sort"] = sort;
90 | }else if (false){
91 | let sortMode = getNotebookSortModeF(notebookId);
92 | if (sortMode) body["sort"] = sortMode;
93 | }
94 | if (showHidden != undefined) {
95 | body["showHidden"] = showHidden;
96 | }
97 | let response = await postRequest(body, url);
98 | if (response.code != 0 || response.data == null){
99 | return new Array();
100 | }
101 |
102 | if (maxListCount > 32 || !maxListCount || maxListCount == 0) {
103 | return response.data.files;
104 | }else{
105 | return response.data.files.slice(0, maxListCount);
106 | }
107 | }
108 |
109 | /**
110 | * 添加属性(API)
111 | * @param attrs 属性对象
112 | * @param 挂件id
113 | * */
114 | export async function addblockAttrAPI(attrs, blockid){
115 | let url = "/api/attr/setBlockAttrs";
116 | let attr = {
117 | id: blockid,
118 | attrs: attrs
119 | }
120 | let result = await postRequest(attr, url);
121 | return checkResponse(result);
122 | }
123 |
124 | /**获取挂件块参数(API)
125 | * @param blockid
126 | * @return response 请访问result.data获取对应的属性
127 | */
128 | export async function getblockAttrAPI(blockid){
129 | let url = "/api/attr/getBlockAttrs";
130 | let response = await postRequest({id: blockid}, url);
131 | if (response.code != 0){
132 | throw Error("获取挂件块参数失败");
133 | }
134 | return response;
135 | }
136 |
137 | /**
138 | * 更新块(返回值有删减)
139 | * @param {String} text 更新写入的文本
140 | * @param {String} blockid 更新的块id
141 | * @param {String} textType 文本类型,markdown、dom可选
142 | * @returns 对象,为response.data[0].doOperations[0]的值,返回码为-1时也返回null
143 | */
144 | export async function updateBlockAPI(text, blockid, textType = "markdown"){
145 | let url = "/api/block/updateBlock";
146 | let data = {dataType: textType, data: text, id: blockid};
147 | let response = await postRequest(data, url);
148 | try{
149 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){
150 | return response.data[0].doOperations[0];
151 | }
152 | if (response.code == -1){
153 | warnPush("更新块失败", response.msg);
154 | return null;
155 | }
156 | }catch(err){
157 | errorPush(err);
158 | warnPush(response.msg);
159 | }
160 | return null;
161 | }
162 |
163 | /**
164 | * 插入块(返回值有删减)
165 | * @param {string} text 文本
166 | * @param {string} blockid 指定的块
167 | * @param {string} textType 插入的文本类型,"markdown" or "dom"
168 | * @param {string} addType 插入到哪里?默认插入为指定块之后,NEXT 为插入到指定块之前, PARENT 为插入为指定块的子块
169 | * @return 对象,为response.data[0].doOperations[0]的值,返回码为-1时也返回null
170 | */
171 | export async function insertBlockAPI(text, blockid, addType = "previousID", textType = "markdown", ){
172 | let url = "/api/block/insertBlock";
173 | let data = {dataType: textType, data: text};
174 | switch (addType) {
175 | case "parentID":
176 | case "PARENT":
177 | case "parentId": {
178 | data["parentID"] = blockid;
179 | break;
180 | }
181 | case "nextID":
182 | case "NEXT":
183 | case "nextId": {
184 | data["nextID"] = blockid;
185 | break;
186 | }
187 | case "previousID":
188 | case "PREVIOUS":
189 | case "previousId":
190 | default: {
191 | data["previousID"] = blockid;
192 | break;
193 | }
194 | }
195 | let response = await postRequest(data, url);
196 | try{
197 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){
198 | return response.data[0].doOperations[0];
199 | }
200 | if (response.code == -1){
201 | warnPush("插入块失败", response.msg);
202 | return null;
203 | }
204 | }catch(err){
205 | errorPush(err);
206 | warnPush(response.msg);
207 | }
208 | return null;
209 |
210 | }
211 |
212 | /**
213 | * 获取文档大纲
214 | * @param {string} docid 要获取的文档id
215 | * @returns {*} 响应的data部分,为outline对象数组
216 | */
217 | export async function getDocOutlineAPI(docid){
218 | let url = "/api/outline/getDocOutline";
219 | let data = {"id": docid};
220 | let response = await postRequest(data, url);
221 | if (response.code == 0){
222 | return response.data;
223 | }else{
224 | return null;
225 | }
226 | }
227 |
228 | /**
229 | * 插入为后置子块
230 | * @param {*} text 子块文本
231 | * @param {*} parentId 父块id
232 | * @param {*} textType 默认为"markdown"
233 | * @returns
234 | */
235 | export async function prependBlockAPI(text, parentId, textType = "markdown"){
236 | let url = "/api/block/prependBlock";
237 | let data = {"dataType": textType, "data": text, "parentID": parentId};
238 | let response = await postRequest(data, url);
239 | try{
240 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){
241 | return response.data[0].doOperations[0];
242 | }
243 | if (response.code == -1){
244 | warnPush("插入块失败", response.msg);
245 | return null;
246 | }
247 | }catch(err){
248 | errorPush(err);
249 | warnPush(response.msg);
250 | }
251 | return null;
252 |
253 | }
254 | /**
255 | * 插入为前置子块
256 | * @param {*} text 子块文本
257 | * @param {*} parentId 父块id
258 | * @param {*} textType 默认为markdown
259 | * @returns
260 | */
261 | export async function appendBlockAPI(text, parentId, textType = "markdown"){
262 | let url = "/api/block/appendBlock";
263 | let data = {"dataType": textType, "data": text, "parentID": parentId};
264 | let response = await postRequest(data, url);
265 | try{
266 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){
267 | return response.data[0].doOperations[0];
268 | }
269 | if (response.code == -1){
270 | warnPush("插入块失败", response.msg);
271 | return null;
272 | }
273 | }catch(err){
274 | errorPush(err);
275 | warnPush(response.msg);
276 | }
277 | return null;
278 |
279 | }
280 |
281 | /**
282 | * 推送普通消息
283 | * @param {string} msgText 推送的内容
284 | * @param {number} timeout 显示时间,单位毫秒
285 | * @return 0正常推送 -1 推送失败
286 | */
287 | export async function pushMsgAPI(msgText, timeout){
288 | let url = "/api/notification/pushMsg";
289 | let response = await postRequest({msg: msgText, timeout: timeout}, url);
290 | if (response.code != 0 || response.data == null || !isValidStr(response.data.id)){
291 | return -1;
292 | }
293 | return 0;
294 | }
295 |
296 | /**
297 | * 获取当前文档id(伪api)
298 | * 优先使用jquery查询
299 | */
300 | export async function getCurrentDocIdF(){
301 | let thisDocId;
302 | let thisWidgetId = getCurrentWidgetId();
303 |
304 | //依靠widgetId sql查,运行时最稳定方案(但挂件刚插入时查询不到!)
305 | if (isValidStr(thisWidgetId)){
306 | try {
307 | let queryResult = await queryAPI("SELECT root_id as parentId FROM blocks WHERE id = '" + thisWidgetId + "'");
308 | if (!(queryResult != null && queryResult.length == 1)) {
309 | debugPush("SQL查询失败", queryResult);
310 | }
311 | if (queryResult!= null && queryResult.length >= 1){
312 | logPush("获取当前文档idBy方案A"+queryResult[0].parentId);
313 | return queryResult[0].parentId;
314 | }
315 | } catch (error) {
316 | logPush("获取文档idBy方案A失败", error);
317 | }
318 | }
319 |
320 | try{
321 | if (isValidStr(thisWidgetId)){
322 | //通过获取挂件所在页面题头图的data-node-id获取文档id【安卓下跳转返回有问题,原因未知】
323 | let thisDocId = window.top.document.querySelector(`div.protyle-content:has(.iframe[data-node-id="${thisWidgetId}"]) .protyle-background`).getAttribute("data-node-id");
324 | if (isValidStr(thisDocId)){
325 | logPush("获取当前文档idBy方案B" + thisDocId);
326 | return thisDocId;
327 | }
328 | }
329 |
330 | }catch(err){
331 | warnPush(err);
332 | }
333 |
334 | // 移动端文档id获取
335 | if (isMobile()) {
336 | try {
337 | // 先前是因为移动端background id更新不及时,所以使用了文档icon获取的方法
338 | let temp;
339 | temp = window.top.document.querySelector(".protyle-breadcrumb .protyle-breadcrumb__item .popover__block[data-id]")?.getAttribute("data-id");
340 | let iconArray = window.top.document.querySelectorAll(".protyle-breadcrumb .protyle-breadcrumb__item .popover__block[data-id]");
341 | for (let i = 0; i < iconArray.length; i++) {
342 | let iconOne = iconArray[i];
343 | if (iconOne.children.length > 0
344 | && iconOne.children[0].getAttribute("xlink:href") == "#iconFile"){
345 | temp = iconOne.getAttribute("data-id");
346 | break;
347 | }
348 | }
349 | debugPush("文档图标获取当前文档id", temp);
350 | thisDocId = temp;
351 | }catch(e){
352 | warnPush("通过文档图标获取当前文档id失败", e);
353 | temp = null;
354 | }
355 | if (!thisDocId) {
356 | thisDocId = window.top.document.querySelector(".protyle.fn__flex-1:not(.fn__none) .protyle-background")?.getAttribute("data-node-id");
357 | debugPush("使用background的匹配值", thisDocId);
358 | }
359 | return thisDocId;
360 | }
361 |
362 | //widgetId不存在,则使用老方法(存在bug:获取当前展示的页面id(可能不是挂件所在的id))
363 | if (!isValidStr(thisWidgetId)){
364 | try{
365 | thisDocId = window.top.document.querySelector(".layout__wnd--active .protyle.fn__flex-1:not(.fn__none) .protyle-background").getAttribute("data-node-id");
366 | logPush("获取当前文档idBy方案C" + thisDocId);
367 | }catch(err){
368 | warnPush("获取当前文档id均失败");
369 | return null;
370 | }
371 | return thisDocId;
372 | }
373 | return null;
374 | }
375 |
376 | /**
377 | * 获取当前挂件id
378 | * @returns
379 | */
380 | export function getCurrentWidgetId(){
381 | try{
382 | // 预览模式
383 | if (window.frameElement.parentElement.getAttribute("id")) {
384 | return window.frameElement.parentElement.getAttribute("id");
385 | }
386 | if (!window.frameElement.parentElement.parentElement.dataset.nodeId) {
387 | return window.frameElement.parentElement.parentElement.dataset.id;
388 | }else{
389 | return window.frameElement.parentElement.parentElement.dataset.nodeId;
390 | }
391 | }catch(err){
392 | warnPush("getCurrentWidgetId window...nodeId方法失效");
393 | return null;
394 | }
395 | }
396 |
397 | /**
398 | * 检查运行的操作系统
399 | * @return true 可以运行,当前os在允许列表中
400 | */
401 | export function checkOs(){
402 | try{
403 | if (setting.includeOs.indexOf(window.top.siyuan.config.system.os.toLowerCase()) != -1){
404 | return true;
405 | }
406 | }catch(err){
407 | errorPush(err);
408 | warnPush("检查操作系统失败");
409 | }
410 |
411 | return false;
412 | }
413 | /**
414 | * 删除块
415 | * @param {*} blockid
416 | * @returns
417 | */
418 | export async function removeBlockAPI(blockid){
419 | let url = "/api/block/deleteBlock";
420 | let response = await postRequest({id: blockid}, url);
421 | if (response.code == 0){
422 | return true;
423 | }
424 | warnPush("删除块失败", response);
425 | return false;
426 | }
427 |
428 | /**
429 | * 获取块kramdown源码
430 | * @param {*} blockid
431 | * @returns kramdown文本
432 | */
433 | export async function getKramdown(blockid){
434 | let url = "/api/block/getBlockKramdown";
435 | let response = await postRequest({id: blockid}, url);
436 | if (response.code == 0 && response.data != null && "kramdown" in response.data){
437 | return response.data.kramdown;
438 | }
439 | return null;
440 | }
441 |
442 | /**
443 | * 获取笔记本列表
444 | * @returns
445 | "id": "20210817205410-2kvfpfn",
446 | "name": "测试笔记本",
447 | "icon": "1f41b",
448 | "sort": 0,
449 | "closed": false
450 |
451 | */
452 | export async function getNodebookList() {
453 | let url = "/api/notebook/lsNotebooks";
454 | let response = await postRequest({}, url);
455 | if (response.code == 0 && response.data != null && "notebooks" in response.data){
456 | return response.data.notebooks;
457 | }
458 | return null;
459 | }
460 |
461 | /**
462 | * 基于本地window.siyuan获得笔记本信息
463 | * @param {*} notebookId 为空获得所有笔记本信息
464 | * @returns
465 | */
466 | export function getNotebookInfoLocallyF(notebookId = undefined) {
467 | try {
468 | if (!notebookId) return window.top.siyuan.notebooks;
469 | for (let notebookInfo of window.top.siyuan.notebooks) {
470 | if (notebookInfo.id == notebookId) {
471 | return notebookInfo;
472 | }
473 | }
474 | return undefined;
475 | }catch(err) {
476 | errorPush(err);
477 | return undefined;
478 | }
479 | }
480 |
481 | /**
482 | * 获取笔记本排序规则
483 | * (为“跟随文档树“的,转为文档树排序
484 | * @param {*} notebookId 笔记本id,不传则为文档树排序
485 | * @returns
486 | */
487 | export function getNotebookSortModeF(notebookId = undefined) {
488 | try {
489 | let fileTreeSort = window.top.siyuan.config.fileTree.sort;
490 | if (!notebookId) return fileTreeSort;
491 | let notebookSortMode = getNotebookInfoLocallyF(notebookId).sortMode;
492 | if (notebookSortMode == DOC_SORT_TYPES.UNASSIGNED || notebookSortMode == DOC_SORT_TYPES.FOLLOW_DOC_TREE) {
493 | return fileTreeSort;
494 | }
495 | return notebookSortMode;
496 | }catch(err) {
497 | errorPush(err);
498 | return undefined;
499 | }
500 | }
501 |
502 | /**
503 | * 批量添加闪卡
504 | * @param {*} ids
505 | * @param {*} deckId 目标牌组Id
506 | * @param {*} oldCardsNum 原有牌组卡牌数(可选)
507 | * @returns (若未传入原卡牌数)添加后牌组内卡牌数, (若传入)返回实际添加的卡牌数; 返回null表示请求失败
508 | */
509 | export async function addRiffCards(ids, deckId, oldCardsNum = -1) {
510 | let url = "/api/riff/addRiffCards";
511 | let postBody = {
512 | deckID: deckId,
513 | blockIDs: ids
514 | };
515 | let response = await postRequest(postBody, url);
516 | if (response.code == 0 && response.data != null && "size" in response.data) {
517 | if (oldCardsNum < 0) {
518 | return response.data.size;
519 | }else{
520 | return response.data.size - oldCardsNum;
521 | }
522 | }
523 | warnPush("添加闪卡出错", response);
524 | return null;
525 | }
526 |
527 | /**
528 | * 批量移除闪卡
529 | * @param {*} ids
530 | * @param {*} deckId 目标牌组Id
531 | * @param {*} oldCardsNum 原有牌组卡牌数(可选)
532 | * @returns (若未传入原卡牌数)移除后牌组内卡牌数, (若传入)返回实际移除的卡牌数; 返回null表示请求失败
533 | */
534 | export async function removeRiffCards(ids, deckId, oldCardsNum = -1) {
535 | let url = "/api/riff/removeRiffCards";
536 | let postBody = {
537 | deckID: deckId,
538 | blockIDs: ids
539 | };
540 | let response = await postRequest(postBody, url);
541 | if (response.code == 0 && response.data != null && "size" in response.data) {
542 | if (oldCardsNum < 0) {
543 | return response.data.size;
544 | }else{
545 | return oldCardsNum - response.data.size;
546 | }
547 | }
548 | warnPush("移除闪卡出错", response);
549 | return null;
550 | }
551 |
552 | /**
553 | * 获取全部牌组信息
554 | * @returns 返回数组
555 | * [{"created":"2023-01-05 20:29:48",
556 | * "id":"20230105202948-xn12hz6",
557 | * "name":"Default Deck",
558 | * "size":1,
559 | * "updated":"2023-01-19 21:48:21"}]
560 | */
561 | export async function getRiffDecks() {
562 | let url = "/api/riff/getRiffDecks";
563 | let response = await postRequest({}, url);
564 | if (response.code == 0 && response.data != null) {
565 | return response.data;
566 | }
567 | return new Array();
568 | }
569 |
570 | /**
571 | * 获取文件内容或链接信息
572 | * @param {*} blockid 获取的文件id
573 | * @param {*} size 获取的块数
574 | * @param {*} mode 获取模式,0为获取html;1为
575 | */
576 | export async function getDoc(blockid, size = 5, mode = 0) {
577 | let url = "/api/filetree/getDoc";
578 | let response = await postRequest({id: blockid, mode: mode, size: size}, url);
579 | if (response.code == 0 && response.data != null) {
580 | return response.data;
581 | }
582 | return undefined;
583 | }
584 |
585 | /**
586 | * 获取文档导出预览
587 | * @param {*} docid
588 | * @returns
589 | */
590 | export async function getDocPreview(docid) {
591 | let url = "/api/export/preview";
592 | let response = await postRequest({id: docid}, url);
593 | if (response.code == 0 && response.data != null) {
594 | return response.data.html;
595 | }
596 | return "";
597 | }
598 | /**
599 | * 删除文档
600 | * @param {*} notebookid 笔记本id
601 | * @param {*} path 文档所在路径
602 | * @returns
603 | */
604 | export async function removeDocAPI(notebookid, path) {
605 | let url = "/api/filetree/removeDoc";
606 | let response = await postRequest({"notebook": notebookid, "path": path}, url);
607 | if (response.code == 0) {
608 | return response.code;
609 | }
610 | warnPush("删除文档时发生错误", response.msg);
611 | return response.code;
612 | }
613 | /**
614 | * 重命名文档
615 | * @param {*} notebookid 笔记本id
616 | * @param {*} path 文档所在路径
617 | * @param {*} title 新文档名
618 | * @returns
619 | */
620 | export async function renameDocAPI(notebookid, path, title) {
621 | let url = "/api/filetree/renameDoc";
622 | let response = await postRequest({"notebook": notebookid, "path": path, "title": title}, url);
623 | if (response.code == 0) {
624 | return response.code;
625 | }
626 | warnPush("重命名文档时发生错误", response.msg);
627 | return response.code;
628 | }
629 |
630 | export function isDarkMode() {
631 | if (window.top.siyuan) {
632 | return window.top.siyuan.config.appearance.mode == 1 ? true : false;
633 | } else {
634 | let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
635 | return isDarkMode;
636 | }
637 | }
638 |
639 | /**
640 | * 通过markdown创建文件
641 | * @param {*} notebookid 笔记本id
642 | * @param {*} hpath 示例 /父文档1/父文档2/你要新建的文档名
643 | * @param {*} md
644 | * @returns
645 | */
646 | export async function createDocWithMdAPI(notebookid, hpath, md) {
647 | let url = "/api/filetree/createDocWithMd";
648 | let response = await postRequest({"notebook": notebookid, "path": hpath, "markdown": md}, url);
649 | if (response.code == 0 && response.data != null) {
650 | return response.data.id;
651 | }
652 | return null;
653 | }
654 |
655 | /**
656 | *
657 | * @param {*} notebookid
658 | * @param {*} path 待创建的新文档path,即,最后应当为一个随机的id.sy
659 | * @param {*} title
660 | * @returns
661 | */
662 | export async function createDocWithPath(notebookid, path, title = "Untitled") {
663 | let url = "/api/filetree/createDoc";
664 | let response = await postRequest({"notebook": notebookid, "path": path, "md": "", "title": title}, url);
665 | if (response.code == 0) {
666 | return true;
667 | }
668 | return false;
669 | }
670 |
671 | /**
672 | * 将对象保存为JSON文件
673 | * @param {*} path
674 | * @param {*} object
675 | * @param {boolean} format
676 | * @returns
677 | */
678 | export async function putJSONFile(path, object, format = false) {
679 | const url = "/api/file/putFile";
680 | const pathSplited = path.split("/");
681 | let fileContent = "";
682 | if (format) {
683 | fileContent = JSON.stringify(object, null, 4);
684 | } else {
685 | fileContent = JSON.stringify(object);
686 | }
687 | // File的文件名实际上无关,但这里考虑到兼容,将上传文件按照路径进行了重命名
688 | const file = new File([fileContent], pathSplited[pathSplited.length - 1], {type: "text/plain"});
689 | const data = new FormData();
690 | data.append("path", path);
691 | data.append("isDir", false);
692 | data.append("modTime", new Date().valueOf());
693 | data.append("file", file);
694 | return fetch(url, {
695 | body: data,
696 | method: 'POST',
697 | headers: {
698 | "Authorization": "Token "+ token
699 | }
700 | }).then((response) => {
701 | return response.json();
702 | });
703 | }
704 |
705 | /**
706 | * 从JSON文件中读取对象
707 | * @param {*} path
708 | * @returns
709 | */
710 | export async function getJSONFile(path) {
711 | const url = "/api/file/getFile";
712 | let response = await postRequest({"path": path}, url);
713 | if (response.code == 404) {
714 | return null;
715 | }
716 | return response;
717 | }
718 |
719 | export async function getFileAPI(path) {
720 | const url = "/api/file/getFile";
721 | let data = {"path": path};
722 | let result;
723 | let response = await fetch(url, {
724 | body: JSON.stringify(data),
725 | method: 'POST',
726 | headers: {
727 | "Authorization": "Token "+token,
728 | "Content-Type": "application/json"
729 | }
730 | });
731 | result = await response.text();
732 | try {
733 | let jsonresult = JSON.parse(result);
734 | if (jsonresult.code == 404) {
735 | return null;
736 | }
737 | return result;
738 | } catch(err) {
739 |
740 | }
741 | return result;
742 | }
743 |
744 | /**
745 | * 列出工作空间下的文件
746 | * @param {*} path 例如"/data/20210808180117-6v0mkxr/20200923234011-ieuun1p.sy"
747 | * @returns isDir, isSymlink, name三个属性
748 | */
749 | export async function listFileAPI(path) {
750 | const url = "/api/file/readDir";
751 | let response = await postRequest({"path": path}, url);
752 | if (response.code == 0) {
753 | return response.data;
754 | }
755 | return [];
756 | }
757 |
758 | export async function removeFileAPI(path) {
759 | const url = "/api/file/removeFile";
760 | let response = await postRequest({"path": path}, url);
761 | if (response.code == 0) {
762 | return true;
763 | } else {
764 | return false;
765 | }
766 | }
767 |
768 | /**
769 | * 添加数据库行
770 | * @param {string} avID
771 | * @param {*} srcs 结构为 id 行id或绑定块id isDetached 是否不是绑定块(绑定块为false) content 非绑定块时的内容
772 | * @param {*} previousID 指定插入位置
773 | * @param {*} ignoreFillFilter 是否忽略填充过滤器(默认true)
774 | * @returns
775 | */
776 | export async function addAttributeViewBlocks(avID, srcs, previousID = undefined, ignoreFillFilter = undefined) {
777 | let url = "/api/av/addAttributeViewBlocks";
778 | let postBody = {
779 | avID,
780 | srcs,
781 | previousID,
782 | ignoreFillFilter
783 | };
784 | let response = await postRequest(postBody, url);
785 | if (response.code == 0) {
786 | return true;
787 | }
788 | warnPush("添加数据库行失败", response);
789 | return false;
790 | }
791 |
792 | // export async function addAttributeViewValues()
793 |
794 | export async function getAttributeView(id) {
795 | let url = "/api/av/getAttributeView";
796 | let postBody = {
797 | id: id,
798 | };
799 | let response = await postRequest(postBody, url);
800 | if (response.code == 0 && response.data != null) {
801 | return response.data.av;
802 | }
803 | return null;
804 | }
805 |
806 | export async function getAttributeViewPrimaryKeyValues(id, page=1, pageSize=32) {
807 | let url = "/api/av/getAttributeViewPrimaryKeyValues";
808 | let postBody = {
809 | id: id,
810 | page: page,
811 | pageSize: pageSize
812 | };
813 | let response = await postRequest(postBody, url);
814 | if (response.code == 0 && response.data != null) {
815 | return response.data;
816 | }
817 | return null;
818 | }
819 |
820 | /**
821 | * 自定义API:通过DatabaseId获取全部关联块id
822 | * @param {*} attributeViewId
823 | * @returns
824 | */
825 | export async function getBlockIdsFromDatabase(attributeViewId) {
826 | const pageSize = Number.MAX_SAFE_INTEGER;
827 | const avResponse = await getAttributeViewPrimaryKeyValues(attributeViewId, 1, pageSize);
828 | if (!avResponse) {
829 | throw new Error("获取属性视图失败: " + attributeViewId);
830 | }
831 | if (!avResponse.rows || !avResponse.rows.values) {
832 | return [];
833 | }
834 | return avResponse.rows.values.map(value => value.blockID).filter(key => key);
835 | }
836 |
837 | export function isMobile() {
838 | return window.top.document.getElementById("sidebar") ? true : false;
839 | };
840 |
841 | export const DOC_SORT_TYPES = {
842 | FILE_NAME_ASC: 0,
843 | FILE_NAME_DESC: 1,
844 | NAME_NAT_ASC: 4,
845 | NAME_NAT_DESC: 5,
846 | CREATED_TIME_ASC: 9,
847 | CREATED_TIME_DESC: 10,
848 | MODIFIED_TIME_ASC: 2,
849 | MODIFIED_TIME_DESC: 3,
850 | REF_COUNT_ASC: 7,
851 | REF_COUNT_DESC: 8,
852 | DOC_SIZE_ASC: 11,
853 | DOC_SIZE_DESC: 12,
854 | SUB_DOC_COUNT_ASC: 13,
855 | SUB_DOC_COUNT_DESC: 14,
856 | CUSTOM_SORT: 6,
857 | FOLLOW_DOC_TREE: 15,
858 | UNASSIGNED: 256,
859 | };
--------------------------------------------------------------------------------
/src/addChildDocLinkHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * addChildDocLink.js 全局监视文件创建/删除操作,向父文档插入文本内容
3 | * 此为历史遗留,现已废弃。
4 | * 此代码文件是listChildDocs的一部分,基于AGPL-3.0许可协议开源。(许可协议详见:https://www.gnu.org/licenses/agpl-3.0.txt,或本项目根目录/LICENSE文件)
5 | * THIS FILE IS A PART OF listChildDocs PROJECT, LICENSED UNDER AGPL-3.0 LICENSE (SEE AS https://www.gnu.org/licenses/agpl-3.0.txt).
6 | * @author OpaqueGlass
7 | *
8 | * 使用方法:
9 | * 设置-外观-代码片段-添加js片段: import("/widgets/listChildDocs/src/addChildDocLinkHelper.js");
10 | * 触发方式/触发条件:websocket message事件,cmd为"create" / "removeDoc"
11 | * 依赖listChildDocs挂件的部分代码,若未经修改,不能单独作为代码片段插入。
12 | *
13 | * 代码标记说明:
14 | * WARN: 警告,这些部分可能和其他js代码冲突,或导致性能问题;
15 | * TODO: 未完成的部分;
16 | * UNSTABLE: 不稳定的实现,可能跟随版本更新而失效;
17 | */
18 | import {
19 | queryAPI,
20 | getSubDocsAPI,
21 | addblockAttrAPI,
22 | getblockAttrAPI,
23 | appendBlockAPI,
24 | prependBlockAPI,
25 | getKramdown,
26 | removeBlockAPI,
27 | updateBlockAPI
28 | } from './API.js';
29 | import {
30 | helperSettings,
31 | language,
32 | setting
33 | } from './config.js';
34 | import {
35 | isSafelyUpdate,
36 | isValidStr
37 | } from './common.js';
38 | /* 全局变量和快速自定义设置 */
39 | let g_attrName = helperSettings.attrName;
40 | let g_docLinkTemplate = helperSettings.docLinkTemplate;
41 | // 将文本内容插入到文档末尾?
42 | let g_insertAtEnd = helperSettings.insertAtEnd;
43 | let g_mode = helperSettings.mode;
44 | let g_checkEmptyDocInsertWidget = helperSettings.checkEmptyDocInsertWidget;
45 | let g_removeLink = helperSettings.removeLinkEnable;
46 | let g_renameLink = helperSettings.renameLinkEnable;
47 | let CONSTANTS = {
48 | RANDOM_DELAY: 300, // 插入挂件的延迟最大值,300(之后会乘以10)对应最大延迟3秒
49 | OBSERVER_RANDOM_DELAY: 500, // 插入链接、引用块和自定义时,在OBSERVER_RANDOM_DELAY_ADD的基础上增加延时,单位毫秒
50 | OBSERVER_RANDOM_DELAY_ADD: 100, // 插入链接、引用块和自定义时,延时最小值,单位毫秒
51 | OBSERVER_RETRY_INTERVAL: 1000, // 找不到页签时,重试间隔
52 | }
53 | let g_observerRetryInterval;
54 | let g_observerStartupRefreshTimeout;
55 | let g_tabSwitchTimeout;
56 | let g_TIMER_LABLE_NAME_COMPARE = "acdlh子文件比对";
57 | let g_insertWidgetPath = helperSettings.widgetPath;
58 | /*
59 | 目前支持g_mode取值为
60 | 插入挂件 add_list_child_docs
61 | 插入链接 add_link
62 | 插入引用块 add_ref
63 | 插入自定义 add_custom
64 | */
65 | // let g_insertToParentDoc = true;
66 | // let g_insertWidgetToParent = true;
67 | const docInfoBlockTemplate = {
68 | docId: "", // 子文档id
69 | linkId: "", // 链接所在块id
70 | docName: "", // 文档名
71 | }
72 |
73 | /* ******************** 事件触发器(当发生事件时调用处理函数) ******************** */
74 |
75 | let g_mywebsocket = window.siyuan.ws.ws;
76 |
77 |
78 | /**
79 | * 页签变更触发器
80 | * 使用当前页面监视获得触发,不会和其他页面执行冲突。但无法处理多用户的情况。
81 | * WARN: UNSTABLE: 依赖页签栏、窗口元素。
82 | */
83 | let g_tabbarElement;
84 | // 处理找不到Element的情况,interval重试寻找
85 | let tabBarObserver = new MutationObserver((mutationList) =>{
86 | for (let mutation of mutationList) {
87 | // console.log("发现页签变化", mutation);
88 | // if (mutation.addedNodes.length > 0) {
89 | // setTimeout(() => {tabChangeHandler(mutation.addedNodes)}, Math.round(Math.random() * CONSTANTS.OBSERVER_RANDOM_DELAY) + CONSTANTS.OBSERVER_RANDOM_DELAY_ADD);
90 | // }
91 | // 由windowObserver代管。关闭页签后,tabBar移除重设,触发器锚定的元素丢失,不会触发
92 | }
93 | });
94 |
95 | /**处理分屏的情况:若页签栏刷新,则触发重设页签变更触发器
96 | * WARN: 依赖窗口变化
97 | * */
98 | let windowObserver = new MutationObserver((mutationList) => {
99 | for (let mutation of mutationList) {
100 | // console.log("发现窗口变化", mutation);
101 | if (mutation.removedNodes.length > 0 || mutation.addedNodes.length > 0) {
102 | // console.log("断开Observer");
103 | // tabBarObserver.disconnect();
104 | switchTabObserver.disconnect();
105 | clearInterval(g_observerRetryInterval);
106 | g_observerRetryInterval = setInterval(observerRetry, CONSTANTS.OBSERVER_RETRY_INTERVAL);
107 | }
108 |
109 | }
110 |
111 | });
112 |
113 | let switchTabObserver = new MutationObserver(async (mutationList) => {
114 | for (let mutation of mutationList) {
115 | // console.log("发现页签切换", mutation);
116 | clearTimeout(g_tabSwitchTimeout);
117 | g_tabSwitchTimeout = setTimeout(async () => {
118 | console.time(g_TIMER_LABLE_NAME_COMPARE);
119 | try{
120 | if (helperSettings.switchTabEnable) {
121 | if (g_mode == "插入挂件" || g_mode == "add_list_child_docs") {
122 | await tabChangeWidgetHandler([mutation.target]);
123 | }else{
124 | await tabChangeHandler([mutation.target]);
125 | }
126 | }else {
127 | if (g_mode == "插入挂件" || g_mode == "add_list_child_docs") {
128 | await tabChangeWidgetHandler(mutation.addedNodes);
129 | }else{
130 | await tabChangeHandler(mutation.addedNodes);
131 | }
132 | }
133 |
134 | }catch(err) {
135 | console.error(err);
136 | }
137 | console.timeEnd(g_TIMER_LABLE_NAME_COMPARE);
138 | }, Math.round(Math.random() * CONSTANTS.OBSERVER_RANDOM_DELAY) + CONSTANTS.OBSERVER_RANDOM_DELAY_ADD);
139 | }
140 | });
141 |
142 | // 窗口变化监视器设定
143 | let g_centerLayoutElement = window.siyuan.layout.centerLayout.element;
144 |
145 | // 只有移除link为启用时才执行
146 | function startObserver() {
147 | clearInterval(g_observerRetryInterval);
148 | g_observerRetryInterval = setInterval(observerRetry, CONSTANTS.OBSERVER_RETRY_INTERVAL);
149 | windowObserver.observe(g_centerLayoutElement, {childList: true});
150 | }
151 | /**
152 | * 重试页签监听
153 | */
154 | function observerRetry() {
155 | g_tabbarElement = window.siyuan.layout.centerLayout.element.querySelectorAll("[data-type='wnd'] ul.layout-tab-bar");
156 | if (g_tabbarElement.length > 0) {
157 | // console.log("重新监视页签变化");
158 | g_tabbarElement.forEach((element)=>{
159 | if (helperSettings.switchTabEnable) {
160 | switchTabObserver.observe(element, {"attributes": true, "attributeFilter": ["data-activetime"], subtree: true});
161 | }else{
162 | switchTabObserver.observe(element, {childList: true});
163 | }
164 | // 重启监听后立刻执行检查
165 | if (element.children.length > 0) {
166 | clearTimeout(g_observerStartupRefreshTimeout);
167 | g_observerStartupRefreshTimeout = setTimeout(async () => {
168 | console.time(g_TIMER_LABLE_NAME_COMPARE);
169 | try{
170 | if (g_mode == "插入挂件" || g_mode == "add_list_child_docs") {
171 | await tabChangeWidgetHandler(element.children);
172 | }else{
173 | await tabChangeHandler(element.children);
174 | }
175 | }catch (err) {
176 | console.error(err);
177 | }
178 | console.timeEnd(g_TIMER_LABLE_NAME_COMPARE);
179 | }, Math.round(Math.random() * CONSTANTS.OBSERVER_RANDOM_DELAY) + CONSTANTS.OBSERVER_RANDOM_DELAY_ADD);
180 | }
181 | });
182 | clearInterval(g_observerRetryInterval);
183 | }
184 | }
185 | /**
186 | * websocket message事件处理函数
187 | * 由于多个窗口的触发时间一致,这里通过随机延迟避开冲突。
188 | * @param {*} msg
189 | */
190 | function websocketEventHandler(msg) {
191 | try {
192 | if (msg && msg.data){
193 | let wsmessage = JSON.parse(msg.data);
194 | if (wsmessage.cmd == "create") {
195 | console.log(wsmessage);
196 | let random = Math.round(Math.random() * CONSTANTS.RANDOM_DELAY) * 10; // *10是为了扩大随机数之间的差距
197 | setTimeout(() => {addWidgetHandler(wsmessage.data)}, random);
198 | console.log("随机延迟", random);
199 | }
200 | // OR "transactions" "removeDoc"
201 | }
202 | }catch(err) {
203 | console.error(language["helperErrorHint"], err);
204 | g_mywebsocket.removeEventListener("message", websocketEventHandler);
205 | }
206 | }
207 | /* ******************** 事件处理(插入/移除执行函数)******************** */
208 |
209 | /**
210 | * 处理新建文档
211 | * @param msgdata websocket信息的data属性
212 | */
213 | async function createHandler(msgdata) {
214 | if (!isValidStr(msgdata)) return;
215 | let dividedPath = msgdata.path.split("/");
216 | let parentDocId = dividedPath[dividedPath.length - 2];
217 | let newDocId = msgdata.id;
218 | if (!isSafelyUpdate(parentDocId)) {
219 | console.log("只读模式,已停止操作");
220 | return;
221 | }
222 | // 笔记本根目录下文档不处理
223 | if (parentDocId == "") return;
224 | // 获取新创建的文档名
225 | let newDocName = "Untitled";
226 | for (let i = 0; i < msgdata.files.length; i++) {
227 | if (msgdata.files[i].path == msgdata.path) {
228 | newDocName = msgdata.files[i].name.substring(0, msgdata.files[i].name.length - 3);
229 | break;
230 | }
231 | }
232 | // 确定未被插入,生成插入链接属性信息
233 | let parentDocAttr = await getCustomAttr(parentDocId);
234 | console.log("获取到父文档属性", parentDocAttr);
235 | if (parentDocAttr && "docInfo" in parentDocAttr) {
236 | for (let docInfoItem of parentDocAttr.docInfo) {
237 | if (docInfoItem.docId == newDocId) {
238 | console.log("其他实例已经添加");
239 | return;
240 | }
241 | }
242 | }
243 | // 处理插入文档的文本信息,进行关键词替换
244 | let insertText;
245 | insertText = g_docLinkTemplate.replace(new RegExp("%DOC_ID%", "g"), msgdata.id)
246 | .replace(new RegExp("%DOC_NAME%", "g"), newDocName);
247 | console.log(insertText);
248 | let addResponse = null;
249 | if (g_insertAtEnd) {
250 | addResponse = await appendBlockAPI(insertText, parentDocId);
251 | }else{
252 | addResponse = await prependBlockAPI(insertText, parentDocId);
253 | }
254 |
255 | let childDocLinkId = addResponse.id;
256 | console.log(`helper已自动插入链接(${childDocLinkId})到父文档(${parentDocId})`);
257 | let newDocInfoBlock = Object.assign({}, docInfoBlockTemplate);
258 | newDocInfoBlock.docId = newDocId;
259 | newDocInfoBlock.linkId = childDocLinkId;
260 | if (parentDocAttr && "docInfo" in parentDocAttr) {
261 | parentDocAttr.docInfo.push(newDocInfoBlock);
262 | }else if (parentDocAttr){
263 | parentDocAttr["docInfo"] = [newDocInfoBlock];
264 | }else{
265 | parentDocAttr = {};
266 | parentDocAttr["docInfo"] = [newDocInfoBlock];
267 | }
268 |
269 | // 保存链接信息
270 | console.log("写入", parentDocAttr);
271 | await saveCustomAttr(parentDocId, parentDocAttr);
272 | }
273 |
274 |
275 | async function isDocEmpty(docId) {
276 | // 检查父文档是否为空
277 | // 获取父文档内容
278 | let parentDocContent = await getKramdown(docId);
279 | // 简化判断,过长的父文档内容必定有文本,不插入 // 作为参考,空文档的kramdown长度约为400
280 | if (parentDocContent.length > 1000) {
281 | console.log("父文档较长,认为非空,不插入挂件", parentDocContent.length);
282 | return;
283 | }
284 | // console.log(parentDocContent);
285 | // 清理ial和换行、空格
286 | let parentDocPlainText = parentDocContent;
287 | // 清理ial中的对象信息(例:文档块中的scrool字段),防止后面匹配ial出现遗漏
288 | parentDocPlainText = parentDocPlainText.replace(new RegExp('\\"{[^\n]*}\\"', "gm"), "\"\"")
289 | // console.log("替换内部对象中间结果", parentDocPlainText);
290 | // 清理ial
291 | parentDocPlainText = parentDocPlainText.replace(new RegExp('{:[^}]*}', "gm"), "");
292 | // 清理换行
293 | parentDocPlainText = parentDocPlainText.replace(new RegExp('\n', "gm"), "");
294 | // 清理空格
295 | parentDocPlainText = parentDocPlainText.replace(new RegExp(' +', "gm"), "");
296 | console.log(`父文档文本(+标记)为 ${parentDocPlainText}`);
297 | console.log(`父文档内容为空?${parentDocPlainText == ""}`);
298 | if (parentDocPlainText != "") return false;
299 | return true;
300 | }
301 |
302 | /**
303 | * 处理添加挂件
304 | * @param msgdata websocket信息的data属性
305 | */
306 | async function addWidgetHandler(msgdata) {
307 | if (!isValidStr(msgdata)) return;
308 | let dividedPath = msgdata.path.split("/");
309 | let parentDocId = dividedPath[dividedPath.length - 2];
310 | let newDocId = msgdata.id;
311 | if (!isSafelyUpdate(parentDocId, {"targetDoc": false})) {
312 | console.log("只读模式,已停止操作");
313 | return;
314 | }
315 | if (parentDocId == "") return;
316 | if (g_checkEmptyDocInsertWidget) {
317 | // 检查父文档是否为空
318 | if (!await isDocEmpty(parentDocId)) return;
319 | }else{
320 | // 获取父文档属性,判断是否插入过挂件
321 | let parentDocAttr = await getblockAttrAPI(parentDocId).data;
322 | if (parentDocAttr != undefined && "id" in parentDocAttr && g_attrName in parentDocAttr) {
323 | return;
324 | }
325 | }
326 | let addedWidgetIds = [];
327 | // 若未插入/文档为空,则插入挂件
328 | for (let widgetPath of g_insertWidgetPath) {
329 | let insertText = ``;
330 | let addResponse;
331 | if (g_insertAtEnd) {
332 | addResponse = await appendBlockAPI(insertText, parentDocId);
333 | }else{
334 | addResponse = await prependBlockAPI(insertText, parentDocId);
335 | }
336 | if (addResponse == null) {
337 | console.warn(`helper插入挂件失败`, widgetPath);
338 | }else{
339 | addedWidgetIds.push(addResponse.id);
340 | }
341 | }
342 |
343 | // 写入文档属性
344 | if (!g_checkEmptyDocInsertWidget) {
345 | let attr = {};
346 | attr[g_attrName] = "{}";
347 | await addblockAttrAPI(attr, parentDocId);
348 | }
349 | console.log(`helper已自动插入挂件块${addedWidgetIds},于父文档${parentDocId}`);
350 | }
351 |
352 |
353 | /**
354 | * 处理页签变化,对打开的空白父文档执行插入挂件操作
355 | * @param {*} addedNodes
356 | */
357 | async function tabChangeWidgetHandler(addedNodes) {
358 | let openDocIds = [];
359 | let safelyUpdateFlag = true;
360 | // WARN: UNSTABLE: 获取打开Tab的对应文档id
361 | // addedNodes.forEach(element => {
362 | // 重启监听后立刻执行检查时,传入的addedNodes类型为HTMLCollections,不支持forEach
363 | [].forEach.call(addedNodes, (element) => {
364 | let docDataId = element.getAttribute("data-id");
365 | // document.querySelector("div[data-id='7fadb0ac-e27d-4d2a-b910-0a8b5c185162']").querySelector(".protyle-background").getAttribute("data-node-id")
366 | if (document.querySelector(`div[data-id="${docDataId}"]`) == null) return;
367 | if (document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background") == null) return;
368 | let openDocId = document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background").getAttribute("data-node-id");
369 | if (!isSafelyUpdate(openDocId)) {
370 | safelyUpdateFlag = false;
371 | return;
372 | }
373 | openDocIds.push(openDocId);
374 | });
375 | if (!safelyUpdateFlag) {
376 | console.log("只读模式,已停止操作");
377 | return;
378 | }
379 | console.log("刚开启的页签文档id", openDocIds);
380 | if (openDocIds.length <= 0) return;
381 | for (let docId of openDocIds) {
382 | // 判断是否为父文档
383 | let queryResult = await queryAPI(`SELECT * FROM blocks WHERE path like '%${docId}/%' and type = "d"`);
384 | console.log("子文档信息", queryResult);
385 | if (!isValidStr(queryResult) || queryResult <= 0) {
386 | console.log("并非父文档");
387 | return;
388 | }
389 | // 判断父文档是否为空
390 | if (g_checkEmptyDocInsertWidget) {
391 | // 检查父文档是否为空
392 | if (!await isDocEmpty(docId)) return;
393 | }else{
394 | // 获取父文档属性,判断是否插入过挂件
395 | let parentDocAttr = await getblockAttrAPI(docId).data;
396 | if (parentDocAttr != undefined && "id" in parentDocAttr && g_attrName in parentDocAttr) {
397 | return;
398 | }
399 | }
400 | // 执行插入
401 | let addedWidgetIds = [];
402 | // 若未插入/文档为空,则插入挂件
403 | for (let widgetPath of g_insertWidgetPath) {
404 | let insertText = ``;
405 | let addResponse;
406 | if (g_insertAtEnd) {
407 | addResponse = await appendBlockAPI(insertText, docId);
408 | }else{
409 | addResponse = await prependBlockAPI(insertText, docId);
410 | }
411 | if (addResponse == null) {
412 | console.warn(`helper插入挂件失败`, widgetPath);
413 | }else{
414 | addedWidgetIds.push(addResponse.id);
415 | }
416 | }
417 |
418 | // 写入文档属性
419 | if (!g_checkEmptyDocInsertWidget) {
420 | let attr = {};
421 | attr[g_attrName] = "{}";
422 | await addblockAttrAPI(attr, docId);
423 | }
424 | console.log(`helper已自动插入挂件块${addedWidgetIds},于父文档${docId}`);
425 | }
426 | }
427 |
428 | /**
429 | * 处理页签节点变化
430 | * 本部分只检查并执行删除链接,不检查新增
431 | */
432 | // TODO: 打开页签时,刷新判断其下子文档变动,进行增加、移除操作(对文档属性,检查其在子文档中是否存在)
433 | async function tabChangeHandler(addedNodes) {
434 | let openDocIds = [];
435 | let safelyUpdateFlag = true;
436 | // WARN: UNSTABLE: 获取打开Tab的对应文档id
437 | // addedNodes.forEach(element => {
438 | // 重启监听后立刻执行检查时,传入的addedNodes类型为HTMLCollections,不支持forEach
439 | [].forEach.call(addedNodes, (element) => {
440 | let docDataId = element.getAttribute("data-id");
441 | // document.querySelector("div[data-id='7fadb0ac-e27d-4d2a-b910-0a8b5c185162']").querySelector(".protyle-background").getAttribute("data-node-id")
442 | if (document.querySelector(`div[data-id="${docDataId}"]`) == null) return;
443 | if (document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background") == null) return;
444 | let openDocId = document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background").getAttribute("data-node-id");
445 | if (!isSafelyUpdate(openDocId)) {
446 | safelyUpdateFlag = false;
447 | return;
448 | }
449 | openDocIds.push(openDocId);
450 | });
451 | if (!safelyUpdateFlag) {
452 | console.log("只读模式,已停止操作");
453 | return;
454 | }
455 | console.log("刚开启的页签文档id", openDocIds);
456 | if (openDocIds.length <= 0) return;
457 | for (let docId of openDocIds) {
458 | let queryResult = await queryAPI(`SELECT box, path FROM blocks WHERE id = '${docId}'`);
459 | if (queryResult == null || queryResult.length < 1) {
460 | console.warn("获取文档路径失败,文档可能刚创建");
461 | return;
462 | }
463 | let subDocInfoList = await getSubDocsAPI(queryResult[0].box, queryResult[0].path);
464 | console.log("API子文档信息", subDocInfoList);
465 | if (subDocInfoList == null) {
466 | console.warn("获取子文档无结果");
467 | return;
468 | }
469 | // 读取属性,获取原来的内容
470 | let docCustomAttr = await getCustomAttr(docId);
471 | if (!docCustomAttr || !("docInfo" in docCustomAttr)) {
472 | console.log("属性为空", docCustomAttr);
473 | docCustomAttr = {
474 | "docInfo": []
475 | };
476 | }
477 | // 由于赋值的是引用,修改会同步。
478 | let docInfos = docCustomAttr.docInfo;
479 | console.log("属性中的文件信息", docInfos);
480 | // 已经被删除的文档在属性列表中的下标(需要被移除的链接)
481 | let removeIndex = [];
482 | // 已经添加过链接的子文档在API请求列表中的下标
483 | let existDocSubDocIndex = [];
484 | // 需要修改文件名的文档在两个列表中的下标
485 | let renameNeededDocIndexBlockList = [];
486 | let needUpdateAttrFlag = false;
487 | docInfos.forEach(async (addedDocInfoBlock, index) => {
488 | let currentDocExistFlag = false;
489 | for (let [subDocIndex, subDocInfo] of subDocInfoList.entries()) {
490 | if (addedDocInfoBlock.docId == subDocInfo.id) {
491 | currentDocExistFlag = true;
492 | existDocSubDocIndex.push(subDocIndex);
493 | if (addedDocInfoBlock.docName != subDocInfo.name.substring(0, subDocInfo.name.length - 3)) {
494 | // renameIndexInfo 对象格式:
495 | renameNeededDocIndexBlockList.push({
496 | attrListIndex: index,
497 | subDocListIndex: subDocIndex
498 | });
499 | }
500 | break;
501 | }
502 | }
503 | if (!currentDocExistFlag) {
504 | removeIndex.push(index);
505 | await removeBlockAPI(addedDocInfoBlock.linkId);
506 | }
507 | });
508 | console.log("Remove Indexes", removeIndex);
509 | // 重命名(依赖原有属性中文件列表的顺序,在执行此部分之前,不要增加/删除docInfos数组中的元素)
510 | if (renameNeededDocIndexBlockList.length > 0 && g_renameLink) {
511 | console.log("需要重命名的链接", renameNeededDocIndexBlockList);
512 | for (let renameIndexInfo of renameNeededDocIndexBlockList) {
513 | let docName = subDocInfoList[renameIndexInfo.subDocListIndex].name;
514 | docName = docName.substring(0, docName.length - 3);
515 | docInfos[renameIndexInfo.attrListIndex].docName = docName;
516 | // 更新链接
517 | let updateText;
518 | updateText = g_docLinkTemplate.replace(new RegExp("%DOC_ID%", "g"), subDocInfoList[renameIndexInfo.subDocListIndex].id)
519 | .replace(new RegExp("%DOC_NAME%", "g"), docName);
520 | updateText += `\n{: memo=\"${language["helperAddBlockMemo"]}\"}`;
521 | let updateResponse = await updateBlockAPI(updateText, docInfos[renameIndexInfo.attrListIndex].linkId);
522 | // 更新失败的块,移除
523 | if (updateResponse == null) {
524 | console.warn(`对应文档${docInfos[renameIndexInfo.attrListIndex].docId}的子文档链接块${docInfos[renameIndexInfo.attrListIndex].linkId}更新失败,该块的记录将被移除,稍后重新创建。`);
525 | removeIndex.push(renameIndexInfo.attrListIndex);
526 | let removeInfoBlockIndex = existDocSubDocIndex.indexOf(renameIndexInfo.subDocListIndex);
527 | if (removeInfoBlockIndex != -1) {
528 | existDocSubDocIndex.splice(removeInfoBlockIndex, 1);
529 | }
530 | }
531 | }
532 | needUpdateAttrFlag = true;
533 | }else{
534 | if (g_renameLink) console.log("未发现子文档文档名变化");
535 | }
536 | // 删除
537 | if (removeIndex.length > 0 && g_removeLink) {
538 | let removedIds = [];
539 | for (let i = removeIndex.length - 1; i >= 0; i--) {
540 | removedIds.push(docInfos[i].docId);
541 | docInfos.splice(removeIndex[i], 1);
542 | }
543 | needUpdateAttrFlag = true;
544 | }else{
545 | if (g_removeLink) console.log("未发现文档被删除");
546 | }
547 |
548 | // 新增文档链接,这些文档链接直接插入到属性中文件列表,在此操作之后,不要删除docInfos中的元素
549 | if (existDocSubDocIndex.length != subDocInfoList.length) {
550 | for (let [index, subDocInfo] of subDocInfoList.entries()) {
551 | if (existDocSubDocIndex.indexOf(index) == -1) {
552 | let newDocInfoBlock = Object.assign({}, docInfoBlockTemplate);
553 | newDocInfoBlock.docId = subDocInfo.id;
554 | newDocInfoBlock.docName = subDocInfo.name.substring(0, subDocInfo.name.length - 3);
555 | let insertText;
556 | insertText = g_docLinkTemplate.replace(new RegExp("%DOC_ID%", "g"), newDocInfoBlock.docId)
557 | .replace(new RegExp("%DOC_NAME%", "g"), newDocInfoBlock.docName);
558 | insertText += `\n{: memo=\"${language["helperAddBlockMemo"]}\"}`;
559 | let addResponse;
560 | if (g_insertAtEnd) {
561 | addResponse = await appendBlockAPI(insertText, docId);
562 | }else{
563 | addResponse = await prependBlockAPI(insertText, docId);
564 | }
565 | let newLinkId = addResponse.id;
566 | newDocInfoBlock.linkId = newLinkId;
567 | docInfos.push(newDocInfoBlock);
568 | // console.log("插入新文档信息块", newDocInfoBlock);
569 | }
570 | }
571 | needUpdateAttrFlag = true;
572 | }else{
573 | console.log("未发现新增文档");
574 | }
575 | if (!needUpdateAttrFlag) return;
576 | console.log("修改后", docInfos);
577 | await saveCustomAttr(docId, docCustomAttr);
578 | }
579 |
580 | return;
581 | }
582 |
583 | /* ******************** 工具方法 ******************** */
584 |
585 |
586 | async function getCustomAttr(parentDocId) {
587 | let docAttrResponse = await getblockAttrAPI(parentDocId);
588 | docAttrResponse = docAttrResponse.data;
589 | if (docAttrResponse == undefined || !("id" in docAttrResponse) || !(g_attrName in docAttrResponse)) {
590 | console.log("未获取到父文档属性");
591 | return null;
592 | }
593 |
594 | return JSON.parse(docAttrResponse[g_attrName].replace(new RegExp(""", "g"), "\""));
595 | }
596 |
597 | async function saveCustomAttr(parentDocId, customAttr) {
598 | let attrString = JSON.stringify(customAttr);
599 | let attr = {};
600 | attr[g_attrName] = attrString;
601 | let response = await addblockAttrAPI(attr, parentDocId);
602 | }
603 |
604 | /* ******************** 模式切换 ******************** */
605 |
606 | console.log(`用户设定:插入到结尾?${g_insertAtEnd} 移除?${g_removeLink} 重命名?${g_renameLink}`);
607 | switch (g_mode) {
608 | case "插入挂件":
609 | case "add_list_child_docs":{
610 | if (!g_insertAtEnd) g_insertAtEnd = false;
611 | /*
612 | * 添加触发器,任意操作均触发,只在新建文件行为发生时执行;
613 | * WARN: 编辑过程中会高频触发,可能导致卡顿;
614 | */
615 | if (setting.safeModePlus && helperSettings.insertWidgetMoment == "create") g_mywebsocket.addEventListener("message", websocketEventHandler);
616 | if (setting.safeModePlus && helperSettings.insertWidgetMoment == "open") startObserver();
617 | break;
618 | }
619 |
620 | case "插入链接":
621 | case "add_link": {
622 | g_docLinkTemplate = "[%DOC_NAME%](siyuan://blocks/%DOC_ID%)";
623 | if (g_insertAtEnd == undefined || g_insertAtEnd == null) g_insertAtEnd = true;
624 | if (g_removeLink == undefined || g_removeLink == null) g_removeLink = true;
625 | if (g_renameLink == undefined || g_renameLink == null) g_renameLink = true;
626 | if (setting.safeModePlus) startObserver();
627 | break;
628 | }
629 |
630 | case "add_ref":
631 | case "插入引用块": {
632 | g_docLinkTemplate = "((%DOC_ID% '%DOC_NAME%'))";
633 | if (g_insertAtEnd == undefined || g_insertAtEnd == null) g_insertAtEnd = true;
634 | if (g_removeLink == undefined || g_removeLink == null) g_removeLink = true;
635 | if (g_renameLink == undefined || g_renameLink == null) g_renameLink = false;
636 | if (setting.safeModePlus) startObserver();
637 | break;
638 | }
639 |
640 | case "add_custom":
641 | case "插入自定义": {
642 | if (g_insertAtEnd == undefined || g_insertAtEnd == null) g_insertAtEnd = true;
643 | if (g_removeLink == undefined || g_removeLink == null) g_removeLink = false;
644 | if (g_renameLink == undefined || g_renameLink == null) g_renameLink = false;
645 | if (setting.safeModePlus) startObserver();
646 | break;
647 | }
648 |
649 | default: {
650 | console.error("不支持的模式,请检查模式设置是否正确 / Unsupported mode, check your input please.");
651 | }
652 | }
653 |
654 | if (!setting.safeModePlus){
655 | console.warn("自动插入助手只在开启只读安全模式(safeModePlus)的前提下运行");
656 | }
657 |
658 | console.log(`未设定值修改为默认后:插入到结尾?${g_insertAtEnd} 移除?${g_removeLink} 重命名?${g_renameLink}`);
--------------------------------------------------------------------------------