c&&t.push(new G(F(s,c),F(s,we(u,l,n))))}t.length||t.push(new G(f,f)),U(g,vi(d,y.ranges.slice(0,v).concat(t),v),{origin:"*mouse",scroll:!1}),d.scrollIntoView(e)}else{var h,r=m,i=ul(d,e,p.unit),e=r.anchor,e=0=n.to||o.linea.bottom?20:0)&&setTimeout(I(d,function(){u==i&&(l.scroller.scrollTop+=r,e(t))}),50))}:n)(e)}),i=I(d,n);d.state.selectingText=i,k(l.wrapper.ownerDocument,"mousemove",r),k(l.wrapper.ownerDocument,"mouseup",i)})(i,s,o,a)):Qe(e)==h.scroller&&D(e):2==n?(t&&Gi(c.doc,t),setTimeout(function(){return h.input.focus()},20)):3==n&&(Q?c.display.input.onContextMenu(e):Mr(c)))))}function ul(e,t,n){if("char"==n)return new G(t,t);if("word"==n)return e.findWordAt(t);if("line"==n)return new G(F(t.line,0),E(e.doc,F(t.line+1,0)));n=n(e,t);return new G(n.from,n.to)}function cl(e,t,n,r){var i,o;if(t.touches)i=t.touches[0].clientX,o=t.touches[0].clientY;else try{i=t.clientX,o=t.clientY}catch(e){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&D(t);var l=e.display,r=l.lineDiv.getBoundingClientRect();if(o>r.bottom||!Ye(e,n))return qe(t);o-=r.top-l.viewOffset;for(var s=0;s=i)return O(e,n,e,bt(e.doc,o),e.display.gutterSpecs[s].className,t),qe(t)}}function hl(e,t){return cl(e,t,"gutterClick",!0)}function dl(e,t){var n,r;An(e.display,t)||(r=t,Ye(n=e,"gutterContextMenu")&&cl(n,r,"gutterContextMenu",!1))||A(e,t,"contextmenu")||Q||e.display.input.onContextMenu(t)}function fl(e){e.display.wrapper.className=e.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+e.options.theme.replace(/(^|\s)\s*/g," cm-s-"),Yn(e)}ol.prototype.compare=function(e,t,n){return this.time+400>e&&0==P(t,this.pos)&&n==this.button};var pl={toString:function(){return"CodeMirror.Init"}},gl={},ml={};function vl(e,t,n){!t!=!(n&&n!=pl)&&(n=e.display.dragFunctions,(t=t?k:T)(e.display.scroller,"dragstart",n.start),t(e.display.scroller,"dragenter",n.enter),t(e.display.scroller,"dragover",n.over),t(e.display.scroller,"dragleave",n.leave),t(e.display.scroller,"drop",n.drop))}function yl(e){e.options.lineWrapping?(ie(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(ee(e.display.wrapper,"CodeMirror-wrap"),an(e)),pr(e),R(e),Yn(e),setTimeout(function(){return jr(e)},100)}function p(e,t){var n=this;if(!(this instanceof p))return new p(e,t);this.options=t=t?fe(t):{},fe(gl,t,!1);var r,i=t.value,o=("string"==typeof i?i=new f(i,t.mode,null,t.lineSeparator,t.direction):t.mode&&(i.modeOption=t.mode),this.doc=i,new p.inputStyles[t.inputStyle](this)),e=this.display=new hi(e,i,o,t),l=(fl(e.wrapper.CodeMirror=this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),$r(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new pe,keySeq:null,specialChars:null},t.autofocus&&!_&&e.input.focus(),w&&v<11&&setTimeout(function(){return n.display.input.reset(!0)},20),this),s=l.display;k(s.scroller,"mousedown",I(l,al)),k(s.scroller,"dblclick",w&&v<11?I(l,function(e){var t;A(l,e)||(!(t=gr(l,e))||hl(l,e)||An(l.display,e)||(D(e),e=l.findWordAt(t),Gi(l.doc,e.anchor,e.head)))}):function(e){return A(l,e)||D(e)}),k(s.scroller,"contextmenu",function(e){return dl(l,e)}),k(s.input.getField(),"contextmenu",function(e){s.scroller.contains(e.target)||dl(l,e)});var a,u={end:0};function c(){s.activeTouch&&(a=setTimeout(function(){return s.activeTouch=null},1e3),(u=s.activeTouch).end=+new Date)}function h(e,t){if(null==t.left)return 1;var n=t.left-e.left,t=t.top-e.top;return 400o.first?S(W(o,t-1).text,null,l):0:"add"==n?c=a+e.options.indentUnit:"subtract"==n?c=a-e.options.indentUnit:"number"==typeof n&&(c=a+n);var c=Math.max(0,c),h="",d=0;if(e.options.indentWithTabs)for(var f=Math.floor(c/l);f;--f)d+=l,h+="\t";if(dl,a=rt(t),u=null;if(s&&1l?"cut":"+input")});to(e.doc,f),b(e,"inputRead",e,f)}t&&!s&&kl(e,t),Pr(e),e.curOp.updateInput<2&&(e.curOp.updateInput=h),e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=-1}function Ll(e,t){var n=e.clipboardData&&e.clipboardData.getData("Text");return n&&(e.preventDefault(),t.isReadOnly()||t.options.disableInput||!t.hasFocus()||h(t,function(){return Sl(t,n,0,null,"paste")}),1)}function kl(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var n=e.doc.sel,r=n.ranges.length-1;0<=r;r--){var i=n.ranges[r];if(!(100=n.first+n.size||(r=new F(e,r.ch,r.sticky),!(s=W(n,e))))return;r=jo(l,n.cm,s,r.line,a)}else r=t;return 1}if("char"==o||"codepoint"==o)u();else if("column"==o)u(!0);else if("word"==o||"group"==o)for(var c=null,h="group"==o,d=n.cm&&n.cm.getHelper(r,"wordChars"),f=!0;!(i<0)||u(!f);f=!1){var p=s.text.charAt(r.ch)||"\n",p=Ne(p,d)?"w":h&&"\n"==p?"n":!h||/\s/.test(p)?null:"p";if(!h||f||p||(p="s"),c&&c!=p){i<0&&(i=1,u(),r.sticky="after");break}if(p&&(c=p),0=s.height){l.hitSide=!0;break}o+=5*n}return l}function r(e){this.cm=e,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new pe,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null}function Dl(e,t){var n=In(e,t.line);if(!n||n.hidden)return null;var r=W(e.doc,t.line),n=Rn(n,r,t.line),r=Ve(r,e.doc.direction),e="left",r=(r&&(e=Pe(r,t.ch)%2?"right":"left"),Kn(n.map,t.ch,e));return r.offset="right"==r.collapse?r.end:r.start,r}function Wl(e,t){return t&&(e.bad=!0),e}function Hl(e,t,n){var r;if(t==e.display.lineDiv){if(!(r=e.display.lineDiv.childNodes[n]))return Wl(e.clipPos(F(e.display.viewTo-1)),!0);t=null,n=0}else for(r=t;;r=r.parentNode){if(!r||r==e.display.lineDiv)return null;if(r.parentNode&&r.parentNode==e.display.lineDiv)break}for(var i=0;i=t.display.viewTo||n.line=t.display.viewFrom&&Dl(t,r)||{node:i[0].measure.map[2],offset:0},r=n.linet.firstLine()&&(i=F(i.line-1,W(t.doc,i.line-1).length)),r.ch==W(t.doc,r.line).text.length&&r.linen.viewTo-1)return!1;var o,l=i.line==n.viewFrom||0==(l=mr(t,i.line))?(e=H(n.view[0].line),n.view[0].node):(e=H(n.view[l].line),n.view[l-1].node.nextSibling),r=mr(t,r.line),n=r==n.view.length-1?(o=n.viewTo-1,n.lineDiv.lastChild):(o=H(n.view[r+1].line)-1,n.view[r+1].node.previousSibling);if(!l)return!1;for(var s=t.doc.splitLines(function(o,e,t,l,s){var n="",a=!1,u=o.doc.lineSeparator(),c=!1;function h(){a&&(n+=u,c&&(n+=u),a=c=!1)}function d(e){e&&(h(),n+=e)}for(;!function e(t){if(1==t.nodeType){var n=t.getAttribute("cm-text");if(n)d(n);else if(n=t.getAttribute("cm-marker"))(n=o.findMarks(F(l,0),F(s+1,0),(i=+n,function(e){return e.id==i}))).length&&(n=n[0].find(0))&&d(mt(o.doc,n.from,n.to).join(u));else if("false"!=t.getAttribute("contenteditable")&&(n=/^(pre|div|p|li|table|br)$/i.test(t.nodeName),/^br$/i.test(t.nodeName)||0!=t.textContent.length)){n&&h();for(var r=0;ri.ch&&p.charCodeAt(p.length-c-1)==g.charCodeAt(g.length-c-1);)u--,c++;s[s.length-1]=p.slice(0,p.length-c).replace(/^\u200b+/,""),s[0]=s[0].slice(u).replace(/\u200b+$/,"");r=F(e,u),l=F(o,a.length?z(a).length-c:0);return 1n&&(wl(this,i.head.line,e,!0),n=i.head.line,r==this.doc.sel.primIndex&&Pr(this));else{for(var o=i.from(),i=i.to(),l=Math.max(n,o.line),n=Math.min(this.lastLine(),i.line-(i.ch?0:1))+1,s=l;s>1;if((l?n[2*l-1]:0)>=o)i=l;else{if(!(n[2*l+1]l)&&e.top>t.offsetHeight?a=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=l&&(a=e.bottom),u+t.offsetWidth>o&&(u=o-t.offsetWidth)),t.style.top=a+"px",t.style.left=t.style.right="","right"==i?(u=s.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?u=0:"middle"==i&&(u=(s.sizer.clientWidth-t.offsetWidth)/2),t.style.left=u+"px"),n&&(r=this,l={left:u,top:a,right:u+t.offsetWidth,bottom:a+t.offsetHeight},null!=(l=Hr(r,l)).scrollTop&&Ir(r,l.scrollTop),null!=l.scrollLeft&&Gr(r,l.scrollLeft))},triggerOnKeyDown:t(nl),triggerOnKeyPress:t(il),triggerOnKeyUp:rl,triggerOnMouseDown:t(al),execCommand:function(e){if(Yo.hasOwnProperty(e))return Yo[e].call(null,this)},triggerElectric:t(function(e){kl(this,e)}),findPosH:function(e,t,n,r){for(var i=1,o=(t<0&&(i=-1,t=-t),E(this.doc,e)),l=0;li.keyCol)return e.skipToEnd(),"string";if(i.literal&&(i.literal=!1),e.sol()){if(i.keyCol=0,i.pair=!1,i.pairStart=!1,e.match("---"))return"def";if(e.match("..."))return"def";if(e.match(/\s*-\s+/))return"meta"}if(e.match(/^(\{|\}|\[|\])/))return"{"==t?i.inlinePairs++:"}"==t?i.inlinePairs--:"["==t?i.inlineList++:i.inlineList--,"meta";if(0)\s*/))return i.literal=!0,"meta";if(e.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==i.inlinePairs&&e.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(0'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)?(i.pair=!0,i.keyCol=e.indentation(),"atom"):i.pair&&e.match(/^:\s*/)?(i.pairStart=!0,"meta"):(i.pairStart=!1,i.escaped="\\"==t,e.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}},lineComment:"#",fold:"indent"}}),e.defineMIME("text/x-yaml","yaml"),e.defineMIME("text/yaml","yaml")});!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(p){"use strict";var h="CodeMirror-lint-markers",g="CodeMirror-lint-line-";function u(t){t.parentNode&&t.parentNode.removeChild(t)}function v(t,e,n,o){t=t,e=e,n=n,(i=document.createElement("div")).className="CodeMirror-lint-tooltip cm-s-"+t.options.theme,i.appendChild(n.cloneNode(!0)),(t.state.lint.options.selfContain?t.getWrapperElement():document.body).appendChild(i),p.on(document,"mousemove",a),a(e),null!=i.style.opacity&&(i.style.opacity=1);var i,r=i;function a(t){if(!i.parentNode)return p.off(document,"mousemove",a);var e=Math.max(0,t.clientY-i.offsetHeight-5),t=Math.max(0,Math.min(t.clientX+5,i.ownerDocument.defaultView.innerWidth-i.offsetWidth));i.style.top=e+"px",i.style.left=t+"px"}function l(){var t;p.off(o,"mouseout",l),r&&((t=r).parentNode&&(null==t.style.opacity&&u(t),t.style.opacity=0,setTimeout(function(){u(t)},600)),r=null)}var s=setInterval(function(){if(r)for(var t=o;;t=t.parentNode){if((t=t&&11==t.nodeType?t.host:t)==document.body)return;if(!t){l();break}}if(!r)return clearInterval(s)},400);p.on(o,"mouseout",l)}function a(s,t,e){for(var n in this.marked=[],(t=t instanceof Function?{getAnnotations:t}:t)&&!0!==t||(t={}),this.options={},this.linterOptions=t.options||{},o)this.options[n]=o[n];for(var n in t)o.hasOwnProperty(n)?null!=t[n]&&(this.options[n]=t[n]):t.options||(this.linterOptions[n]=t[n]);this.timeout=null,this.hasGutter=e,this.onMouseOver=function(t){var e=s,n=t.target||t.srcElement;if(/\bCodeMirror-lint-mark-/.test(n.className)){for(var n=n.getBoundingClientRect(),o=(n.left+n.right)/2,n=(n.top+n.bottom)/2,i=e.findMarksAt(e.coordsChar({left:o,top:n},"client")),r=[],a=0;al.clientHeight+1;setTimeout(function(){f=s.getScrollInfo()});0b&&(l.style.width=b-5+"px",k-=H.right-H.left-b),l.style.left=(m=Math.max(p.left-k-y,0))+"px"),e)for(var x=l.firstChild;x;x=x.nextSibling)x.style.paddingRight=s.display.nativeBarWidth+"px";s.addKeyMap(this.keyMap=function(t,n){var o={Up:function(){n.moveFocus(-1)},Down:function(){n.moveFocus(1)},PageUp:function(){n.moveFocus(1-n.menuSize(),!0)},PageDown:function(){n.moveFocus(n.menuSize()-1,!0)},Home:function(){n.setFocus(0)},End:function(){n.setFocus(n.length-1)},Enter:n.pick,Tab:n.pick,Esc:n.close},e=(/Mac/.test(navigator.platform)&&(o["Ctrl-P"]=function(){n.moveFocus(-1)},o["Ctrl-N"]=function(){n.moveFocus(1)}),t.options.customKeys),s=e?{}:o;function i(t,e){var i="string"!=typeof e?function(t){return e(t,n)}:o.hasOwnProperty(e)?o[e]:e;s[t]=i}if(e)for(var c in e)e.hasOwnProperty(c)&&i(c,e[c]);var r=t.options.extraKeys;if(r)for(var c in r)r.hasOwnProperty(c)&&i(c,r[c]);return s}(o,{moveFocus:function(t,e){i.changeActive(i.selectedHint+t,e)},setFocus:function(t){i.changeActive(t)},menuSize:function(){return i.screenAmount()},length:n.length,close:function(){o.close()},pick:function(){i.pick()},data:t})),o.options.closeOnUnfocus&&(s.on("blur",this.onBlur=function(){C=setTimeout(function(){o.close()},100)}),s.on("focus",this.onFocus=function(){clearTimeout(C)})),s.on("scroll",this.onScroll=function(){var t=s.getScrollInfo(),e=s.getWrapperElement().getBoundingClientRect(),i=(f=f||s.getScrollInfo(),g+f.top-t.top),n=i-(r.pageYOffset||(c.documentElement||c.body).scrollTop);if(v||(n+=l.offsetHeight),n<=e.top||n>=e.bottom)return o.close();l.style.top=i+"px",l.style.left=m+f.left-t.left+"px"}),T.on(l,"dblclick",function(t){t=O(l,t.target||t.srcElement);t&&null!=t.hintId&&(i.changeActive(t.hintId),i.pick())}),T.on(l,"click",function(t){t=O(l,t.target||t.srcElement);t&&null!=t.hintId&&(i.changeActive(t.hintId),o.options.completeOnSingleClick&&i.pick())}),T.on(l,"mousedown",function(){setTimeout(function(){s.focus()},20)});var S=this.getSelectedHintRange();return 0===S.from&&0===S.to||this.scrollToActive(),T.signal(t,"select",n[this.selectedHint],l.childNodes[this.selectedHint]),!0}function r(t,e,i,n){t.async?t(e,n,i):(t=t(e,i))&&t.then?t.then(n):n(t)}n.prototype={close:function(){this.active()&&(this.cm.state.completionActive=null,this.tick=null,this.options.updateOnCursorActivity&&this.cm.off("cursorActivity",this.activityFunc),this.widget&&this.data&&T.signal(this.data,"close"),this.widget&&this.widget.close(),T.signal(this.cm,"endCompletion",this.cm))},active:function(){return this.cm.state.completionActive==this},pick:function(t,e){var i=t.list[e],n=this;this.cm.operation(function(){i.hint?i.hint(n.cm,t,i):n.cm.replaceRange(M(i),i.from||t.from,i.to||t.to,"complete"),T.signal(t,"pick",i),n.cm.scrollIntoView()}),this.options.closeOnPick&&this.close()},cursorActivity:function(){this.debounce&&(s(this.debounce),this.debounce=0);var t,e=this.startPos,i=(this.data&&(e=this.data.from),this.cm.getCursor()),n=this.cm.getLine(i.line);i.line!=this.startPos.line||n.length-i.ch!=this.startLen-this.startPos.ch||i.ch=this.data.list.length?t=e?this.data.list.length-1:0:t<0&&(t=e?0:this.data.list.length-1),this.selectedHint!=t&&((e=this.hints.childNodes[this.selectedHint])&&(e.className=e.className.replace(" "+F,""),e.removeAttribute("aria-selected")),(e=this.hints.childNodes[this.selectedHint=t]).className+=" "+F,e.setAttribute("aria-selected","true"),this.completion.cm.getInputField().setAttribute("aria-activedescendant",e.id),this.scrollToActive(),T.signal(this.data,"select",this.data.list[this.selectedHint],e))},scrollToActive:function(){var t=this.getSelectedHintRange(),e=this.hints.childNodes[t.from],t=this.hints.childNodes[t.to],i=this.hints.firstChild;e.offsetTopthis.hints.scrollTop+this.hints.clientHeight&&(this.hints.scrollTop=t.offsetTop+t.offsetHeight-this.hints.clientHeight+i.offsetTop)},screenAmount:function(){return Math.floor(this.hints.clientHeight/this.hints.firstChild.offsetHeight)||1},getSelectedHintRange:function(){var t=this.completion.options.scrollMargin||0;return{from:Math.max(0,this.selectedHint-t),to:Math.min(this.data.list.length-1,this.selectedHint+t)}}},T.registerHelper("hint","auto",{resolve:function(t,e){var i,c=t.getHelpers(e,"hint");return c.length?((e=function(t,n,o){var s=function(t,e){if(!t.somethingSelected())return e;for(var i=[],n=0;n,]/,closeOnPick:!0,closeOnUnfocus:!0,updateOnCursorActivity:!0,completeOnSingleClick:!0,container:null,customKeys:null,extraKeys:null,paddingForScrollbar:!0,moveOnOverlap:!0};T.defineOption("hintOptions",null)});
/*!
* @toast-ui/editor
* @version 3.2.2 | Fri Feb 17 2023
@@ -7566,6 +7700,211 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
};
})();
+window.__ZDDC_SCHEMA__ = {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://zddc.varasys.io/schema/zddc.schema.json",
+ "title": ".zddc policy document",
+ "description": "Machine schema for the .zddc grammar (see GRAMMAR.md). Each property carries x-zddc-tier: 'structure' (the project shape an end user should not change — paths, WORM, tools, behaviors) or 'option' (the blanks an operator fills — role members, field-code vocabularies, names, labels). A form view renders option fields editable and structure fields read-only. NOTE: not all keys are valid at every level; the cascade + the per-location form decide relevance. Server-side validation still lives in validate.go (this draft-2020-12 schema uses $ref + patternProperties, which the in-tree validator does not yet support); the schema drives the form + client today.",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Human title for this directory.",
+ "x-zddc-tier": "option"
+ },
+ "created_by": {
+ "type": "string",
+ "description": "Email of the user who created this folder. Set by the server; audit only.",
+ "x-zddc-tier": "structure"
+ },
+ "admins": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Principals (emails, globs, or role names) who administer this subtree. Root admins are super-admins; deeper entries are subtree admins. Elevation-gated full bypass over scope.",
+ "x-zddc-tier": "option"
+ },
+ "roles": {
+ "type": "object",
+ "description": "Named principal groups referenced by acl/worm/admins. Membership UNIONS across the cascade. The operator fills the members.",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "members": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Email patterns (alice@x, *@acme.com, *) in this role."
+ },
+ "reset": {
+ "type": "boolean",
+ "description": "Stop the membership union here: ancestor definitions above this level are excluded."
+ }
+ }
+ },
+ "x-zddc-tier": "option"
+ },
+ "acl": {
+ "type": "object",
+ "description": "Access control for this level. permissions maps a principal (email/glob/role) to a verb string from r w c d a (empty string = explicit deny). inherit:false clamps the ACL level-walk so ancestor levels' grants do not apply.",
+ "additionalProperties": false,
+ "properties": {
+ "inherit": { "type": "boolean", "description": "false = this level's ACL does not inherit ancestor levels." },
+ "permissions": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": { "type": "string", "pattern": "^[rwcda]*$", "description": "Verb subset of r w c d a; empty = explicit deny." }
+ },
+ "additionalProperties": false
+ }
+ },
+ "x-zddc-tier": "structure"
+ },
+ "worm": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "WORM zone: write/delete/admin stripped for all; create survives only for the listed principals; admins bypass. Unions across the cascade.",
+ "x-zddc-tier": "structure"
+ },
+ "inherit": {
+ "type": "boolean",
+ "description": "false = stop the cascade here; everything below (ancestors + embedded defaults) is ignored. Makes a subtree a self-contained island.",
+ "x-zddc-tier": "structure"
+ },
+ "default_tool": {
+ "type": "string",
+ "enum": ["archive", "transmittal", "classifier", "browse", "tables", "landing", "form"],
+ "description": "Tool served at (no trailing slash). Sugar for views.dir.tool.",
+ "x-zddc-tier": "structure"
+ },
+ "dir_tool": {
+ "type": "string",
+ "enum": ["archive", "transmittal", "classifier", "browse", "tables", "landing", "form"],
+ "description": "Tool served at / (trailing slash). Sugar for views.dir_slash.tool; defaults to browse.",
+ "x-zddc-tier": "structure"
+ },
+ "views": {
+ "type": "object",
+ "description": "Per-URL-shape tool + supporting-config mapping.",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "tool": { "type": "string", "enum": ["archive", "transmittal", "classifier", "browse", "tables", "landing", "form"] },
+ "config": { "type": "string", "description": "Supporting-file name resolved under .zddc.d/ (no slashes)." }
+ }
+ },
+ "x-zddc-tier": "structure"
+ },
+ "available_tools": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Tools the apps subsystem may auto-serve here and below. Concat-dedupe union across the cascade.",
+ "x-zddc-tier": "structure"
+ },
+ "auto_own": {
+ "type": "boolean",
+ "description": "mkdir here writes a creator-owned .zddc (creator: rwcda).",
+ "x-zddc-tier": "structure"
+ },
+ "auto_own_fenced": {
+ "type": "boolean",
+ "description": "The auto-own .zddc is written with acl.inherit:false (private to its creator).",
+ "x-zddc-tier": "structure"
+ },
+ "auto_own_roles": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Roles also granted rwcda in the auto-own .zddc, alongside the creator.",
+ "x-zddc-tier": "structure"
+ },
+ "virtual": {
+ "type": "boolean",
+ "description": "Never materialise on disk; treat requests as virtual routes.",
+ "x-zddc-tier": "structure"
+ },
+ "drop_target": {
+ "type": "boolean",
+ "description": "This directory accepts drag-drop uploads (browse drop-zone). Leaf-only.",
+ "x-zddc-tier": "structure"
+ },
+ "party_source": {
+ "type": "string",
+ "description": "A new / here requires registration in /.yaml (e.g. 'ssr'). Leaf-only.",
+ "x-zddc-tier": "structure"
+ },
+ "history": {
+ "type": "boolean",
+ "description": "Snapshot text (markdown) edits to .history/ in this subtree with a server-stamped audit line.",
+ "x-zddc-tier": "structure"
+ },
+ "history_globs": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Which basenames get edit history (default [\"*.md\"]).",
+ "x-zddc-tier": "structure"
+ },
+ "convert": {
+ "type": "object",
+ "description": "Template variables for MD→{docx,html,pdf} conversion. Cascades leaf→root, per-key latest wins.",
+ "additionalProperties": false,
+ "properties": {
+ "client": { "type": "string" },
+ "project": { "type": "string" },
+ "contractor": { "type": "string" },
+ "project_number": { "type": "string" }
+ },
+ "x-zddc-tier": "option"
+ },
+ "field_codes": {
+ "type": "object",
+ "description": "Vocabularies for tracking-number / record field components. Map-merged per code across the cascade.",
+ "additionalProperties": { "type": "object" },
+ "x-zddc-tier": "option"
+ },
+ "records": {
+ "type": "object",
+ "description": "Per-record-type rules keyed by filename pattern (filename_format, field_defaults, locked, row_field, row_scope_fields, folder_fields).",
+ "additionalProperties": { "type": "object" },
+ "x-zddc-tier": "structure"
+ },
+ "display": {
+ "type": "object",
+ "description": "Human labels for child entries (on-disk name → label). Leaf-only.",
+ "additionalProperties": { "type": "string" },
+ "x-zddc-tier": "option"
+ },
+ "tables": {
+ "type": "object",
+ "description": "Legacy directory-of-YAML table views (stem → spec path).",
+ "additionalProperties": { "type": "string" },
+ "x-zddc-tier": "structure"
+ },
+ "received_path": {
+ "type": "string",
+ "description": "Links a workflow folder back to its canonical submittal in received/. Set by Plan Review.",
+ "x-zddc-tier": "structure"
+ },
+ "planned_review_date": {
+ "type": "string",
+ "description": "Doc-controller's committed review-completion date (YYYY-MM-DD), on the canonical submittal.",
+ "x-zddc-tier": "option"
+ },
+ "planned_response_date": {
+ "type": "string",
+ "description": "Doc-controller's committed response-issuance date (YYYY-MM-DD), on the canonical submittal.",
+ "x-zddc-tier": "option"
+ },
+ "paths": {
+ "type": "object",
+ "description": "Virtual sub-directory rules. Each key is a single path segment (literal or '*'); the value is a nested .zddc applied at the matching child directory. Recursive.",
+ "additionalProperties": { "$ref": "#" },
+ "x-zddc-tier": "structure"
+ }
+ }
+}
+;
+
// util.js — small browse-local helpers shared across the tool's modules.
//
// Consolidates copies that had drifted across modules: escapeHtml (some
@@ -7793,6 +8132,526 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
};
})();
+// yaml-complete.js — deterministic, schema-driven completion + hover docs for
+// the browse YAML editors (markdown front matter + .zddc). NO heuristics, no
+// AI: every candidate and doc string comes from a PROVIDER backed by the
+// converter's field list or the .zddc JSON Schema.
+//
+// A provider answers three questions about a position, identified by its key
+// PATH (the array of parent keys):
+// keysAt(path) → [{name, hint, values}] valid child keys here
+// valuesFor(path, key) → [string] | null enum/boolean values
+// describe(path, key) → string | null doc text (for hover)
+// The CodeMirror plumbing (indent→path, sibling scan, show-hint, hover) is
+// shared; only the provider differs between the flat front matter and the
+// nested .zddc schema. Requires CodeMirror 5 + the show-hint add-on.
+(function () {
+ 'use strict';
+ if (!window.app) window.app = {};
+ if (!window.app.modules) window.app.modules = {};
+
+ function indentOf(line) { var m = line.match(/^(\s*)/); return m ? m[1].length : 0; }
+ function isBlankOrComment(line) { return /^\s*$/.test(line) || /^\s*#/.test(line); }
+ function truncate(s, n) { s = String(s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
+
+ // Parent key-path for a line, derived from YAML indentation: walk upward
+ // collecting each "key:" line at a strictly smaller indent.
+ function pathAt(cm, lineNo) {
+ var path = [];
+ var target = indentOf(cm.getLine(lineNo));
+ for (var ln = lineNo - 1; ln >= 0 && target > 0; ln--) {
+ var line = cm.getLine(ln);
+ if (isBlankOrComment(line)) continue;
+ var ind = indentOf(line);
+ if (ind < target) {
+ var m = line.match(/^\s*([\w.\-]+)\s*:/);
+ if (m) { path.unshift(m[1]); target = ind; }
+ }
+ }
+ return path;
+ }
+
+ // Sibling keys already present at the same indent within this block, so we
+ // don't re-suggest a key the author already wrote.
+ function presentSiblings(cm, lineNo, indent) {
+ var present = {};
+ [-1, 1].forEach(function (dir) {
+ for (var ln = lineNo + dir; ln >= 0 && ln < cm.lineCount(); ln += dir) {
+ var line = cm.getLine(ln);
+ if (isBlankOrComment(line)) continue;
+ var ind = indentOf(line);
+ if (ind < indent) break; // left the block
+ if (ind === indent) {
+ var m = line.match(/^\s*([\w.\-]+)\s*:/);
+ if (m) present[m[1]] = true;
+ }
+ }
+ });
+ return present;
+ }
+
+ function keyItem(k, hinter) {
+ var item = {
+ text: k.name + ': ',
+ displayText: k.name + (k.hint ? ' — ' + truncate(k.hint, 64) : '')
+ };
+ // An enum key inserts "key: " then immediately opens its value menu.
+ if (k.values && k.values.length) {
+ item.hint = function (cmi, data, comp) {
+ cmi.replaceRange(comp.text, data.from, data.to);
+ setTimeout(function () { cmi.showHint({ hint: hinter, completeSingle: false }); }, 0);
+ };
+ }
+ return item;
+ }
+
+ function makeHinter(provider) {
+ function hinter(cm) {
+ var CM = window.CodeMirror;
+ if (!CM) return null;
+ var cur = cm.getCursor();
+ var before = cm.getLine(cur.line).slice(0, cur.ch);
+ var colon = before.indexOf(':');
+ var path = pathAt(cm, cur.line);
+
+ if (colon === -1) {
+ // KEY context.
+ var m = before.match(/^(\s*)([\w.\-]*)$/);
+ if (!m) return null;
+ var indent = m[1], typed = m[2];
+ var keys = provider.keysAt(path) || [];
+ if (!keys.length) return null;
+ var present = presentSiblings(cm, cur.line, indent.length);
+ var list = [];
+ keys.forEach(function (k) {
+ if (present[k.name]) return;
+ if (typed && k.name.indexOf(typed) !== 0) return;
+ list.push(keyItem(k, hinter));
+ });
+ if (!list.length) return null;
+ return { list: list, from: CM.Pos(cur.line, indent.length), to: cur };
+ }
+
+ // VALUE context.
+ var key = before.slice(0, colon).trim();
+ var values = provider.valuesFor(path, key) || [];
+ if (!values.length) return null;
+ var rest = before.slice(colon + 1);
+ var valTyped = rest.replace(/^\s*/, '');
+ var valStart = colon + 1 + (rest.length - valTyped.length);
+ var vlist = [];
+ values.forEach(function (v) {
+ if (valTyped && v.indexOf(valTyped) !== 0) return;
+ vlist.push({ text: v, displayText: v });
+ });
+ if (!vlist.length) return null;
+ return { list: vlist, from: CM.Pos(cur.line, valStart), to: cur };
+ }
+ return hinter;
+ }
+
+ // Lightweight hover docs: hover a "key:" → its schema description. No
+ // add-on — a debounced mousemove over the editor + a fixed-position tip.
+ function attachHover(cm, provider) {
+ var tip = null, timer = null;
+ function hide() { if (tip && tip.parentNode) tip.parentNode.removeChild(tip); tip = null; }
+ function show(text, x, y) {
+ hide();
+ tip = document.createElement('div');
+ tip.className = 'cm-doc-tip';
+ tip.textContent = text;
+ document.body.appendChild(tip);
+ tip.style.left = x + 'px';
+ tip.style.top = (y + 16) + 'px';
+ }
+ var wrap = cm.getWrapperElement();
+ wrap.addEventListener('mousemove', function (e) {
+ if (timer) clearTimeout(timer);
+ var ex = e.clientX, ey = e.clientY;
+ timer = setTimeout(function () {
+ if (!wrap.isConnected) { hide(); return; }
+ try {
+ var pos = cm.coordsChar({ left: ex, top: ey }, 'window');
+ var line = cm.getLine(pos.line) || '';
+ var m = line.match(/^\s*([\w.\-]+)\s*:/);
+ if (!m) { hide(); return; }
+ var keyStart = line.indexOf(m[1]);
+ if (pos.ch < keyStart || pos.ch > keyStart + m[1].length) { hide(); return; }
+ var doc = provider.describe(pathAt(cm, pos.line), m[1]);
+ if (doc) show(doc, ex, ey); else hide();
+ } catch (_e) { hide(); }
+ }, 350);
+ });
+ wrap.addEventListener('mouseleave', function () { if (timer) clearTimeout(timer); hide(); });
+ cm.on('cursorActivity', hide);
+ cm.on('changes', hide);
+ }
+
+ // Wire completion (Ctrl-Space + auto-trigger as you type) and hover docs
+ // onto a CodeMirror instance. opts.readOnly skips the typing trigger;
+ // opts.hover:false skips hover.
+ function attach(cm, provider, opts) {
+ opts = opts || {};
+ var hinter = makeHinter(provider);
+ var keys = Object.assign({}, cm.getOption('extraKeys') || {}, {
+ 'Ctrl-Space': function (c) { c.showHint({ hint: hinter, completeSingle: false }); }
+ });
+ cm.setOption('extraKeys', keys);
+ if (!opts.readOnly) {
+ cm.on('inputRead', function (c, change) {
+ if (!change.text || change.text.length !== 1) return; // skip paste/delete
+ if (!/[\w.\-]/.test(change.text[0])) return;
+ c.showHint({ hint: hinter, completeSingle: false });
+ });
+ }
+ if (opts.hover !== false) attachHover(cm, provider);
+ return hinter;
+ }
+
+ // ── Providers ───────────────────────────────────────────────────────────
+
+ // Flat: a fixed field list [{name, hint, values}] at the root, nothing
+ // nested (front matter). opts.exclude = names never suggested.
+ function flatProvider(getFields, opts) {
+ opts = opts || {};
+ var exclude = {};
+ (opts.exclude || []).forEach(function (n) { exclude[n] = true; });
+ function fields() { return getFields() || []; }
+ function find(name) {
+ var fs = fields();
+ for (var i = 0; i < fs.length; i++) if (fs[i].name === name) return fs[i];
+ return null;
+ }
+ return {
+ keysAt: function (path) {
+ if (path.length) return [];
+ return fields().filter(function (f) { return !exclude[f.name]; })
+ .map(function (f) { return { name: f.name, hint: f.hint, values: f.values }; });
+ },
+ valuesFor: function (path, key) {
+ if (path.length) return null;
+ var f = find(key); return f ? f.values : null;
+ },
+ describe: function (path, key) {
+ if (path.length) return null;
+ var f = find(key); return f ? f.hint : null;
+ }
+ };
+ }
+
+ // Schema: a JSON Schema (draft-2020-12 subset). Resolves nested key-paths
+ // through properties / additionalProperties / patternProperties and the
+ // recursive $ref:"#" .zddc uses for paths:. Keys = object property names;
+ // values = enum / boolean.
+ function schemaProvider(getSchema) {
+ function root() { return getSchema(); }
+ function deref(node) { return (node && node.$ref === '#') ? root() : node; }
+ function stepInto(node, seg) {
+ node = deref(node);
+ if (!node || node.type !== 'object') return null;
+ if (node.properties && node.properties[seg]) return node.properties[seg];
+ if (node.additionalProperties && typeof node.additionalProperties === 'object') {
+ return node.additionalProperties;
+ }
+ if (node.patternProperties) {
+ for (var p in node.patternProperties) {
+ if (Object.prototype.hasOwnProperty.call(node.patternProperties, p)) {
+ return node.patternProperties[p];
+ }
+ }
+ }
+ return null;
+ }
+ function containerAt(path) {
+ var node = deref(root());
+ for (var i = 0; i < path.length; i++) {
+ node = stepInto(node, path[i]);
+ if (!node) return null;
+ node = deref(node);
+ }
+ return node;
+ }
+ function valuesOf(node) {
+ node = deref(node);
+ if (!node) return null;
+ if (Array.isArray(node.enum)) return node.enum.map(String);
+ if (node.type === 'boolean') return ['true', 'false'];
+ return null;
+ }
+ function keyNodeAt(path, key) {
+ var c = containerAt(path);
+ if (!c || !c.properties) return null;
+ return c.properties[key] || null;
+ }
+ return {
+ keysAt: function (path) {
+ var c = containerAt(path);
+ if (!c || c.type !== 'object' || !c.properties) return [];
+ return Object.keys(c.properties).map(function (name) {
+ var n = deref(c.properties[name]) || {};
+ return { name: name, hint: n.description, values: valuesOf(n) };
+ });
+ },
+ valuesFor: function (path, key) { return valuesOf(keyNodeAt(path, key)); },
+ describe: function (path, key) {
+ var n = deref(keyNodeAt(path, key));
+ return n ? n.description : null;
+ }
+ };
+ }
+
+ window.app.modules.yamlComplete = {
+ attach: attach,
+ makeHinter: makeHinter,
+ flatProvider: flatProvider,
+ schemaProvider: schemaProvider
+ };
+})();
+
+// manage-access.js — guided "who can do what here" dialog. A task-first
+// front door for a folder's .zddc acl: the user picks people + friendly access
+// levels; we read the on-disk .zddc, merge ONLY the access bits (preserving
+// every other key), and PUT it. No YAML, no schema knowledge required. The raw
+// editor stays as the "Advanced" escape hatch.
+//
+// Friendly level → verbs (r read, w overwrite, c create, d delete, a admin):
+// View → r Contribute → rc
+// Edit → rwc Manage → admins: membership (not a verb string)
+// "Custom" preserves a hand-written verb string we don't recognise.
+(function (app) {
+ 'use strict';
+ if (!app || !app.modules) return;
+ var util = app.modules.util;
+
+ var LEVELS = [
+ { id: 'view', label: 'View', hint: 'read only', verbs: 'r' },
+ { id: 'contribute', label: 'Contribute', hint: 'read + add new files', verbs: 'rc' },
+ { id: 'edit', label: 'Edit', hint: 'read, overwrite, add', verbs: 'rwc' },
+ { id: 'manage', label: 'Manage', hint: 'full config + (elevated) bypass', verbs: null }
+ ];
+ function verbsOfLevel(id) {
+ for (var i = 0; i < LEVELS.length; i++) if (LEVELS[i].id === id) return LEVELS[i].verbs;
+ return null;
+ }
+ function levelOfVerbs(verbs) {
+ verbs = String(verbs || '');
+ if (verbs.indexOf('a') !== -1) return 'manage';
+ if (verbs.indexOf('w') !== -1) return 'edit';
+ if (verbs.indexOf('c') !== -1) return 'contribute';
+ if (verbs.indexOf('r') !== -1) return 'view';
+ return 'custom'; // empty (explicit deny) or non-standard
+ }
+
+ function dirUrl(dir) {
+ var u = dir || '/';
+ if (u.charAt(0) !== '/') u = '/' + u;
+ if (u.charAt(u.length - 1) !== '/') u += '/';
+ return u;
+ }
+
+ function el(tag, cls, text) {
+ var e = document.createElement(tag);
+ if (cls) e.className = cls;
+ if (text != null) e.textContent = text;
+ return e;
+ }
+
+ async function open(dir) {
+ if (!app.state || app.state.source !== 'server') {
+ toast('Access management needs the server.', 'error');
+ return;
+ }
+ var base = dirUrl(dir);
+ var zddcUrl = base + '.zddc';
+ var data = {}, etag = null;
+ try {
+ var r = await fetch(zddcUrl, { credentials: 'same-origin' });
+ if (r.ok) {
+ etag = r.headers.get('ETag');
+ var txt = await r.text();
+ try { data = (window.jsyaml && window.jsyaml.load(txt)) || {}; } catch (_e) { data = {}; }
+ } else if (r.status !== 404) {
+ throw new Error('HTTP ' + r.status);
+ }
+ } catch (e) {
+ toast('Could not read access rules: ' + (e.message || e), 'error');
+ return;
+ }
+ if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
+
+ // Build the principal → level model from admins (Manage) + acl.permissions.
+ var acl = (data.acl && typeof data.acl === 'object') ? data.acl : {};
+ var perms = (acl.permissions && typeof acl.permissions === 'object') ? acl.permissions : {};
+ var admins = Array.isArray(data.admins) ? data.admins : [];
+ var rows = [];
+ var seen = {};
+ admins.forEach(function (p) {
+ if (typeof p === 'string' && !seen[p]) { seen[p] = 1; rows.push({ principal: p, level: 'manage', custom: '' }); }
+ });
+ Object.keys(perms).forEach(function (p) {
+ if (seen[p]) return;
+ seen[p] = 1;
+ var lvl = levelOfVerbs(perms[p]);
+ rows.push({ principal: p, level: lvl, custom: lvl === 'custom' ? String(perms[p] || '') : '' });
+ });
+ var inherit = acl.inherit !== false;
+
+ renderModal(base, zddcUrl, data, etag, rows, inherit);
+ }
+
+ function toast(msg, kind) { if (window.zddc && window.zddc.toast) window.zddc.toast(msg, kind || 'info'); }
+
+ function renderModal(base, zddcUrl, data, etag, rows, inherit) {
+ var overlay = el('div', 'ma-overlay');
+ var box = el('div', 'ma-box');
+ overlay.appendChild(box);
+
+ box.appendChild(el('h2', 'ma-title', 'Manage access'));
+ var sub = el('p', 'ma-sub', 'Who can do what in ' + base + ' — changes here only.');
+ box.appendChild(sub);
+
+ var list = el('div', 'ma-list');
+ box.appendChild(list);
+
+ function addRow(model) {
+ var row = el('div', 'ma-row');
+ var who = el('input', 'ma-who');
+ who.type = 'text';
+ who.value = model.principal || '';
+ who.placeholder = 'email or *@domain or role name';
+ who.addEventListener('input', function () { model.principal = who.value.trim(); });
+
+ var sel = el('select', 'ma-level');
+ LEVELS.forEach(function (lv) {
+ var o = el('option', null, lv.label);
+ o.value = lv.id;
+ o.title = lv.hint;
+ sel.appendChild(o);
+ });
+ if (model.level === 'custom') {
+ var o2 = el('option', null, 'Custom');
+ o2.value = 'custom';
+ o2.title = 'verbs: ' + model.custom;
+ sel.appendChild(o2);
+ }
+ sel.value = model.level;
+ sel.addEventListener('change', function () { model.level = sel.value; });
+
+ var del = el('button', 'ma-del', '✕');
+ del.type = 'button';
+ del.title = 'Remove';
+ del.addEventListener('click', function () { row.remove(); model._removed = true; });
+
+ row.appendChild(who);
+ row.appendChild(sel);
+ row.appendChild(del);
+ list.appendChild(row);
+ return model;
+ }
+ rows.forEach(addRow);
+
+ var addBtn = el('button', 'ma-add', '+ Add person or group');
+ addBtn.type = 'button';
+ addBtn.addEventListener('click', function () {
+ var m = { principal: '', level: 'view', custom: '' };
+ rows.push(m);
+ addRow(m);
+ });
+ box.appendChild(addBtn);
+
+ var legend = el('p', 'ma-legend',
+ 'View = read · Contribute = add new files · Edit = overwrite + add · Manage = admin');
+ box.appendChild(legend);
+
+ // Inherit / make-private.
+ var inhWrap = el('label', 'ma-inherit');
+ var inhBox = el('input');
+ inhBox.type = 'checkbox';
+ inhBox.checked = inherit;
+ inhWrap.appendChild(inhBox);
+ inhWrap.appendChild(el('span', null, ' Inherit access from parent folders'));
+ box.appendChild(inhWrap);
+
+ var err = el('p', 'ma-err');
+ box.appendChild(err);
+
+ var actions = el('div', 'ma-actions');
+ var cancel = el('button', 'btn btn-sm btn-secondary', 'Cancel');
+ cancel.type = 'button';
+ var save = el('button', 'btn btn-sm btn-primary', 'Save');
+ save.type = 'button';
+ actions.appendChild(cancel);
+ actions.appendChild(save);
+ box.appendChild(actions);
+
+ function close() {
+ document.removeEventListener('keydown', onKey, true);
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
+ }
+ function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); close(); } }
+ document.addEventListener('keydown', onKey, true);
+ overlay.addEventListener('mousedown', function (e) { if (e.target === overlay) close(); });
+ cancel.addEventListener('click', close);
+
+ save.addEventListener('click', function () {
+ err.textContent = '';
+ // Rebuild perms + admins from the live rows (skip removed/blank).
+ var perms = {}, admins = [], bad = false;
+ rows.forEach(function (m) {
+ if (m._removed) return;
+ var p = (m.principal || '').trim();
+ if (!p) return;
+ if (m.level === 'manage') {
+ if (admins.indexOf(p) === -1) admins.push(p);
+ } else if (m.level === 'custom') {
+ perms[p] = m.custom; // preserve the hand-written string
+ } else {
+ perms[p] = verbsOfLevel(m.level);
+ }
+ });
+
+ // Merge into the existing doc, preserving every unmanaged key.
+ var out = {};
+ Object.keys(data).forEach(function (k) { out[k] = data[k]; });
+ var acl = (out.acl && typeof out.acl === 'object') ? Object.assign({}, out.acl) : {};
+ if (Object.keys(perms).length) acl.permissions = perms; else delete acl.permissions;
+ if (!inhBox.checked) acl.inherit = false; else delete acl.inherit;
+ if (Object.keys(acl).length) out.acl = acl; else delete out.acl;
+ if (admins.length) out.admins = admins; else delete out.admins;
+
+ var content;
+ try { content = window.jsyaml.dump(out); }
+ catch (e2) { err.textContent = 'Could not serialize: ' + (e2.message || e2); return; }
+
+ save.disabled = true;
+ save.textContent = 'Saving…';
+ var node = { url: zddcUrl, name: '.zddc', ext: '' };
+ util.saveFile(node, content, 'application/yaml; charset=utf-8', etag ? { etag: etag } : {})
+ .then(function () {
+ toast('Access updated for ' + base, 'success');
+ var ev = app.modules.events;
+ if (ev && ev.refreshListing) { try { ev.refreshListing(); } catch (_e) { /* ignore */ } }
+ close();
+ })
+ .catch(function (e3) {
+ save.disabled = false;
+ save.textContent = 'Save';
+ if (e3 && e3.status === 412) {
+ err.textContent = 'These rules changed on the server since you opened this. Close and reopen to get the latest, then redo your change.';
+ } else {
+ err.textContent = 'Save failed: ' + (e3 && e3.message ? e3.message : e3);
+ }
+ });
+ });
+
+ document.body.appendChild(overlay);
+ var first = box.querySelector('.ma-who');
+ if (first) first.focus();
+ }
+
+ app.modules.manageAccess = { open: open };
+})(window.app);
+
// conflict.js — shared conflict-resolution dialog for the browse tool.
//
// Surfaced when a save loses an optimistic-concurrency race: the file
@@ -8351,13 +9210,30 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
// ── admin / sub-admin tier ──
{
- // HIDDEN unless the user can actually edit access rules here
- // (admin verb 'a', or subtree/site admin) — not shown greyed.
+ // Guided "who can do what here" dialog — the front door for access.
+ // HIDDEN unless the user can administer here (admin verb 'a', or
+ // subtree/site admin).
id: 'manage-access', group: 'admin', surfaces: ['row', 'pane'],
- label: 'Edit access rules…',
+ label: 'Manage access…',
appliesTo: function (ctx) {
if (!isServer()) return false; // server-only tier
var typeOk = ctx.surface === 'pane' || appliesToFolderLike(ctx.node);
+ return typeOk && manageAccessGate(ctx).enabled
+ && !!(window.app.modules.manageAccess);
+ },
+ action: function (ctx) {
+ var m = window.app.modules.manageAccess;
+ if (m) m.open(ctx.dir);
+ }
+ },
+ {
+ // The raw-YAML escape hatch — same authority gate, demoted to
+ // "advanced" since the guided dialog covers the common case.
+ id: 'edit-zddc-raw', group: 'admin', surfaces: ['row', 'pane'],
+ label: 'Edit raw policy (.zddc)…',
+ appliesTo: function (ctx) {
+ if (!isServer()) return false;
+ var typeOk = ctx.surface === 'pane' || appliesToFolderLike(ctx.node);
return typeOk && manageAccessGate(ctx).enabled;
},
action: function (ctx) { openZddcEditor(ctx.dir); }
@@ -9506,7 +10382,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
function editorModules() {
var m = window.app.modules;
- return [m.markdown, m.yamledit, m.zddcform].filter(Boolean);
+ return [m.markdown, m.yamledit].filter(Boolean);
}
function disposeEditors() {
@@ -9538,6 +10414,9 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
disposeEditors();
var container = document.getElementById('previewBody');
if (container) container.innerHTML = '';
+ toggleTargetNode = null;
+ var tb = document.getElementById('previewViewToggle');
+ if (tb) tb.classList.add('hidden');
}
// Warn before a full page unload (reload / close / external nav) drops
@@ -9547,6 +10426,41 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
if (dirtyEditor()) { e.preventDefault(); e.returnValue = ''; }
});
+ // ── Rendered ⇄ Source toggle ─────────────────────────────────────────────
+ // Some types we can RENDER, not just edit (.html). Those show rendered by
+ // default (sandboxed — no scripts, no same-origin) with a toggle to the
+ // CodeMirror source view. Markdown has its own rendered/source toggle, so
+ // it's not here. Extend RENDERABLE to add more (svg already previews as an
+ // image; csv could render as a table later).
+ var RENDERABLE = { html: 1, htm: 1 };
+ function isRenderable(ext) { return !!RENDERABLE[(ext || '').toLowerCase()]; }
+ function nodeKey(node) { return (node && (node.url || node.name)) || ''; }
+ // Per-node mode; 'rendered' is the default. Only the node the user last
+ // toggled is remembered, so switching files resets to rendered.
+ var viewToggle = { key: null, mode: 'rendered' };
+ var toggleTargetNode = null;
+ function effectiveMode(node) {
+ return (viewToggle.key && viewToggle.key === nodeKey(node)) ? viewToggle.mode : 'rendered';
+ }
+ function ensureViewToggleBtn() {
+ var btn = document.getElementById('previewViewToggle');
+ if (btn) return btn;
+ var popout = document.getElementById('previewPopout');
+ if (!popout || !popout.parentNode) return null;
+ btn = document.createElement('button');
+ btn.id = 'previewViewToggle';
+ btn.type = 'button';
+ btn.className = 'btn btn-sm btn-secondary hidden';
+ popout.parentNode.insertBefore(btn, popout);
+ btn.addEventListener('click', function () {
+ if (!toggleTargetNode) return;
+ var next = effectiveMode(toggleTargetNode) === 'rendered' ? 'source' : 'rendered';
+ viewToggle = { key: nodeKey(toggleTargetNode), mode: next };
+ renderInline(toggleTargetNode, { toggle: true });
+ });
+ return btn;
+ }
+
// ── Inline rendering ────────────────────────────────────────────────────
// Bumped on every renderInline entry; a render that loses the race
@@ -9575,9 +10489,10 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
var dm = dirtyEditor();
if (dm) {
var cur = dm.currentNode ? dm.currentNode() : null;
- if (samePreviewNode(cur, node)) {
+ if (samePreviewNode(cur, node) && !opts.toggle) {
// Re-selecting the file we're already editing — don't reload
- // and clobber the in-progress edits.
+ // and clobber the in-progress edits. (A deliberate view toggle
+ // falls through to the discard prompt below.)
return;
}
if (opts.auto) {
@@ -9605,6 +10520,32 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
var ext = (node.ext || '').toLowerCase();
+ // Rendered ⇄ Source toggle button — shown only for renderable types.
+ var toggleBtn = ensureViewToggleBtn();
+ if (toggleBtn) {
+ if (isRenderable(ext)) {
+ toggleTargetNode = node;
+ toggleBtn.classList.remove('hidden');
+ toggleBtn.textContent = effectiveMode(node) === 'rendered' ? '⟨⟩ Source' : '◱ Preview';
+ } else {
+ toggleBtn.classList.add('hidden');
+ }
+ }
+
+ // Renderable types (.html) — show rendered by default, sandboxed for
+ // safety (no scripts, no same-origin). The toggle flips to source.
+ if (isRenderable(ext) && effectiveMode(node) === 'rendered') {
+ try {
+ var rinfo = await getBlobUrl(node);
+ if (seq !== renderSeq) return;
+ container.innerHTML = '';
+ } catch (e) {
+ renderError(container, e.message || String(e));
+ }
+ return;
+ }
+
// Markdown plugin (if loaded) takes over for .md / .markdown.
if ((ext === 'md' || ext === 'markdown') &&
window.app.modules.markdown &&
@@ -9617,39 +10558,27 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
return;
}
- // .zddc form view: a schema-driven form (option fields editable,
- // structure read-only) is the PRIMARY editor for .zddc files. It hands
- // off to the raw YAML editor on demand. Other YAML files skip it.
- var zddcForm = window.app.modules.zddcform;
- if (zddcForm && zddcForm.handles(node)) {
- try {
- await zddcForm.render(node, container, { getArrayBuffer: getArrayBuffer, getContentWithVersion: getContentWithVersion });
- } catch (e) {
- renderError(container, '.zddc form render failed: ' + (e.message || e));
- }
- return;
- }
-
- // YAML plugin: .yaml / .yml / .zddc / *.zddc.yaml route to a
- // CodeMirror 5 editor with js-yaml linting; .zddc files also
- // get a schema-aware lint pass.
+ // CodeMirror editor: the general editor for editable text files that
+ // aren't markdown — yaml/.zddc (schema lint + completion + hover) plus
+ // txt/csv/tsv/json/xml/html/css/js/… as a plaintext code editor.
+ // Guided dialogs (Manage access, …) are the front door for the common
+ // .zddc tasks; this is the full/raw edit surface.
var yamlMod = window.app.modules.yamledit;
if (yamlMod && yamlMod.handles(node)) {
try {
await yamlMod.render(node, container, { getArrayBuffer: getArrayBuffer, getContentWithVersion: getContentWithVersion });
} catch (e) {
- renderError(container, 'YAML render failed: ' + (e.message || e));
+ renderError(container, 'Editor failed: ' + (e.message || e));
}
return;
}
- // PDF / HTML → iframe.
- if (ext === 'pdf' || ext === 'html' || ext === 'htm') {
+ // PDF → iframe (HTML now routes to the editor above).
+ if (ext === 'pdf') {
try {
var info = await getBlobUrl(node);
if (seq !== renderSeq) return;
- var sandbox = (ext === 'pdf') ? '' : ' sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"';
- container.innerHTML = '';
+ container.innerHTML = '';
} catch (e) {
renderError(container, e.message || String(e));
}
@@ -9838,6 +10767,25 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
}
async function renderInPopup(node) {
+ // Editor-type files (markdown, yaml/.zddc, code text) can't be hosted
+ // in the lightweight popup window — they need the bundled editor. Pop
+ // them out as the FULL browse app deep-linked to the file, which loads
+ // the real editor in a new window. Server mode only; HTML keeps its
+ // rendered popup. Falls through to the lightweight popup otherwise.
+ var pext = (node.ext || '').toLowerCase();
+ var ym = window.app.modules.yamledit;
+ var isEditorType = pext === 'md' || pext === 'markdown'
+ || (ym && ym.handles && ym.handles(node) && pext !== 'html' && pext !== 'htm');
+ if (isEditorType && window.app.state.source === 'server' && node.url) {
+ var slash = node.url.lastIndexOf('/');
+ var pdir = slash >= 0 ? node.url.slice(0, slash + 1) : '/';
+ var pbase = slash >= 0 ? node.url.slice(slash + 1) : node.url;
+ var pp = new URLSearchParams();
+ try { pp.set('file', decodeURIComponent(pbase)); } catch (_e) { pp.set('file', pbase); }
+ if (window.app.state.showHidden) pp.set('hidden', '1');
+ window.open(pdir + '?' + pp.toString(), '_blank', 'noopener');
+ return;
+ }
var info;
try {
info = await getBlobUrl(node);
@@ -10012,30 +10960,38 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
// empty / unavailable. The promise dedupes concurrent fetches.
var fmPlaceholder = null;
var fmPlaceholderPromise = null;
+ // Recognised fields ([{name, hint, values}]) from the same /.api/frontmatter
+ // fetch — drives schema completion (keys + enum values). null = not loaded.
+ var fmFields = null;
- // applyFrontMatterPlaceholder sets the textarea placeholder to the server's
- // recognised-field hint, in server mode only. Async + best-effort: a failed
- // fetch leaves the pane blank (no placeholder), never an error.
- function applyFrontMatterPlaceholder(textarea) {
+ // applyFrontMatterHint populates a greyed caption (+ tooltip) with the
+ // server's recognised front-matter fields, in server mode only. Async +
+ // best-effort: a failed fetch leaves the caption hidden, never an error.
+ // (Replaces the old textarea placeholder — CodeMirror 5 has no built-in
+ // placeholder without an unvendored add-on. Arbitrary keys stay free.)
+ function applyFrontMatterHint(el) {
var st = window.app && window.app.state;
if (!st || st.source !== 'server') return;
- if (fmPlaceholder !== null) {
- textarea.placeholder = fmPlaceholder;
- return;
+ function paint() {
+ if (!el.isConnected) return; // user switched files before resolve
+ if (!fmPlaceholder) { el.style.display = 'none'; return; }
+ el.textContent = 'ⓘ Recognised front-matter keys (hover) — any other key is allowed';
+ el.title = fmPlaceholder;
+ el.style.display = '';
}
+ if (fmPlaceholder !== null) { paint(); return; }
if (!fmPlaceholderPromise) {
fmPlaceholderPromise = fetch('/.api/frontmatter', {
headers: { 'Accept': 'application/json' },
credentials: 'same-origin'
}).then(function (r) { return r.ok ? r.json() : null; })
- .then(function (j) { fmPlaceholder = (j && j.placeholder) || ''; })
- .catch(function () { fmPlaceholder = ''; });
+ .then(function (j) {
+ fmPlaceholder = (j && j.placeholder) || '';
+ fmFields = (j && j.fields) || [];
+ })
+ .catch(function () { fmPlaceholder = ''; fmFields = []; });
}
- fmPlaceholderPromise.then(function () {
- // Only apply if this textarea is still in the DOM (user may have
- // switched files before the fetch resolved).
- if (textarea.isConnected) textarea.placeholder = fmPlaceholder;
- });
+ fmPlaceholderPromise.then(paint);
}
// Lightweight YAML front-matter parser. Same envelope as mdedit's:
@@ -10357,22 +11313,19 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
fmHeader.textContent = 'YAML front matter';
var fmBody = document.createElement('div');
fmBody.className = 'md-side__body md-fm__body';
- var fmTextarea = document.createElement('textarea');
- fmTextarea.className = 'md-fm__textarea';
- fmTextarea.spellcheck = false;
- fmTextarea.autocapitalize = 'off';
- fmTextarea.autocomplete = 'off';
- // Placeholder: in server mode, hint the recognised front-matter keys
- // (doctype, numbering, …) as greyed text so authors can discover them.
- // It's placeholder-only — inserts nothing, vanishes on the first
- // keystroke — so arbitrary keys stay free and a file with no front
- // matter still renders as a genuinely empty pane. The text is fetched
- // from the server (/.api/frontmatter), the single source of truth, so
- // it never drifts from what the converter honours. file:// mode shows
- // no placeholder (conversion is server-only).
- fmTextarea.placeholder = '';
- applyFrontMatterPlaceholder(fmTextarea);
- fmBody.appendChild(fmTextarea);
+ // CodeMirror YAML editor host — mounted with the front-matter value
+ // once it's computed (sync-on-open) below. Same editor family as the
+ // .zddc previewer: syntax highlighting, line numbers, lint gutter.
+ var fmEditorHost = document.createElement('div');
+ fmEditorHost.className = 'md-fm__editor';
+ fmBody.appendChild(fmEditorHost);
+ // Recognised-keys hint (server mode): a greyed caption under the header
+ // whose tooltip carries the full "key: # hint" template from
+ // /.api/frontmatter. Replaces the old textarea placeholder.
+ var fmHint = document.createElement('div');
+ fmHint.className = 'md-fm__hint';
+ fmHint.style.display = 'none';
+ applyFrontMatterHint(fmHint);
// Rename cue: shown when the author edits an identity field
// (tracking_number / revision / status / title) away from the
// filename. The filename owns identity, so the cue offers an explicit
@@ -10380,12 +11333,16 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
// discarding the value. Populated by renderIdentityCue().
var fmWarn = document.createElement('div');
fmWarn.className = 'md-fm__warn';
- fmWarn.hidden = true;
+ // Visibility is controlled via style.display (toggled in
+ // renderIdentityCue), NOT the `hidden` attribute: an inline
+ // display:flex outranks [hidden]{display:none}, which would leave an
+ // empty box on screen whenever the cue has nothing to say.
fmWarn.style.cssText = 'color:#92400e;background:#fffbeb;border:1px solid '
+ '#fcd34d;border-radius:4px;padding:6px 8px;margin:0 0 4px;font-size:'
- + '0.78rem;line-height:1.5;display:flex;flex-wrap:wrap;align-items:'
- + 'center;gap:6px;';
+ + '0.78rem;line-height:1.5;flex-wrap:wrap;align-items:center;gap:6px;'
+ + 'display:none;';
fmSection.appendChild(fmHeader);
+ fmSection.appendChild(fmHint);
fmSection.appendChild(fmWarn);
fmSection.appendChild(fmBody);
sidebar.appendChild(fmSection);
@@ -10530,21 +11487,59 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
// even if we tweak whitespace in the YAML lines.
var initialParsed = parseFrontMatter(text);
var bodyText = initialParsed.body;
- // On open, mirror the filename-derived identity into the front matter
- // (the filename is the single source of truth; this keeps the values
- // baked in for the converter). No-op for non-ZDDC filenames. The dirty
+ // On open, RECONCILE existing front-matter identity keys with the
+ // filename (the single source of truth) — but never ADD them. A blank
+ // or new file opens blank (we don't inject a title etc.); a file whose
+ // author already wrote a now-stale title/revision/… gets corrected.
+ // The converter derives identity from the filename regardless, so
+ // there's nothing to "bake in" for an empty front matter. The dirty
// baseline stays the ON-DISK state, so a correction opens the buffer
// dirty and a save persists it.
var onDiskFM = stringifyFrontMatter(initialParsed.data);
var fid = filenameIdentity(node.name);
if (fid) {
for (var ik in fid) {
- if (Object.prototype.hasOwnProperty.call(fid, ik)) initialParsed.data[ik] = fid[ik];
+ if (Object.prototype.hasOwnProperty.call(fid, ik)
+ && Object.prototype.hasOwnProperty.call(initialParsed.data, ik)) {
+ initialParsed.data[ik] = fid[ik];
+ }
}
}
- fmTextarea.value = stringifyFrontMatter(initialParsed.data);
+ var syncedFM = stringifyFrontMatter(initialParsed.data);
var initialHash = await hashContent(assembleContent(onDiskFM, bodyText));
var writableMode = canSave(node);
+
+ // Front-matter YAML editor — CodeMirror, the same editor family as the
+ // .zddc previewer (syntax highlighting, line numbers, shared js-yaml
+ // lint gutter). Replaces the old