sdocs-dev 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/bin/sdocs-dev.js +303 -43
- package/package.json +9 -3
- package/public/brotli-wasm-v1.js +518 -0
- package/public/brotli_wasm_bg.wasm +0 -0
- package/public/css/layout.css +3 -4
- package/public/css/mobile.css +7 -0
- package/public/css/panel.css +1 -1
- package/public/css/rendered.css +184 -1
- package/public/images/examples.png +0 -0
- package/public/index.html +137 -42
- package/public/sdoc.md +542 -0
- package/public/sdocs-app.js +223 -66
- package/public/sdocs-charts.js +905 -0
- package/public/sdocs-controls.js +135 -53
- package/public/sdocs-export.js +939 -61
- package/public/sdocs-slugify.js +12 -0
- package/public/sdocs-state.js +14 -0
- package/public/sdocs-styles.js +180 -254
- package/public/sdocs-theme.js +21 -19
- package/public/sdocs-write.js +4 -1
- package/public/sw.js +6 -2
- package/public/vendor/purify.min.js +3 -0
- package/server.js +52 -3
- package/public/default.md +0 -255
package/public/sdocs-write.js
CHANGED
|
@@ -9,7 +9,7 @@ S.writeEl = writeEl;
|
|
|
9
9
|
// ── Enter / exit write mode ──────────────────────────
|
|
10
10
|
|
|
11
11
|
function enterWriteMode() {
|
|
12
|
-
var html = marked.parse(S.currentBody);
|
|
12
|
+
var html = DOMPurify.sanitize(marked.parse(S.currentBody));
|
|
13
13
|
writeEl.innerHTML = html || '<p><br></p>';
|
|
14
14
|
copyStyleVars();
|
|
15
15
|
setTimeout(updateToolbarState, 0);
|
|
@@ -280,6 +280,9 @@ function checkShortcuts() {
|
|
|
280
280
|
// ── Input handler + debounced sync ──────────────────
|
|
281
281
|
|
|
282
282
|
writeEl.addEventListener('input', function(e) {
|
|
283
|
+
// Content has diverged from the on-disk file — drop local paths.
|
|
284
|
+
if (S.invalidateLocalMeta) S.invalidateLocalMeta();
|
|
285
|
+
|
|
283
286
|
// Debounce sync
|
|
284
287
|
clearTimeout(S._writeSyncTimer);
|
|
285
288
|
S._writeSyncTimer = setTimeout(function() {
|
package/public/sw.js
CHANGED
|
@@ -9,6 +9,8 @@ var APP_SHELL = [
|
|
|
9
9
|
'/public/css/panel.css',
|
|
10
10
|
'/public/css/write.css',
|
|
11
11
|
'/public/css/mobile.css',
|
|
12
|
+
'/public/brotli-wasm-v1.js',
|
|
13
|
+
'/public/brotli_wasm_bg.wasm',
|
|
12
14
|
'/public/sdocs-yaml.js',
|
|
13
15
|
'/public/sdocs-styles.js',
|
|
14
16
|
'/public/sdocs-state.js',
|
|
@@ -16,12 +18,13 @@ var APP_SHELL = [
|
|
|
16
18
|
'/public/sdocs-controls.js',
|
|
17
19
|
'/public/sdocs-export.js',
|
|
18
20
|
'/public/sdocs-write.js',
|
|
21
|
+
'/public/sdocs-charts.js',
|
|
19
22
|
'/public/sdocs-app.js',
|
|
20
23
|
'/public/vendor/marked.min.js',
|
|
21
24
|
'/public/fonts/inter-400.woff2',
|
|
22
25
|
'/public/fonts/inter-500.woff2',
|
|
23
26
|
'/public/fonts/inter-600.woff2',
|
|
24
|
-
'/public/
|
|
27
|
+
'/public/sdoc.md',
|
|
25
28
|
];
|
|
26
29
|
|
|
27
30
|
// Pre-cache app shell on install
|
|
@@ -91,7 +94,8 @@ self.addEventListener('fetch', function (e) {
|
|
|
91
94
|
// No reload — stale-while-revalidate ensures next navigation gets fresh content.
|
|
92
95
|
self.addEventListener('message', function (e) {
|
|
93
96
|
if (e.data && e.data.type === 'check-update' && e.data.version) {
|
|
94
|
-
|
|
97
|
+
var qs = '?cohort=' + encodeURIComponent(e.data.cohort || '');
|
|
98
|
+
fetch('/version-check' + qs).then(function (res) {
|
|
95
99
|
return res.json();
|
|
96
100
|
}).then(function (data) {
|
|
97
101
|
if (data.version !== e.data.version) {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/*! @license DOMPurify 3.3.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.3/LICENSE */
|
|
2
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t){for(var n=arguments.length,o=new Array(n>2?n-2:0),r=2;r<n;r++)o[r-2]=arguments[r];return e.apply(t,o)}),s||(s=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];return new e(...n)});const u=D(Array.prototype.forEach),m=D(Array.prototype.lastIndexOf),p=D(Array.prototype.pop),f=D(Array.prototype.push),d=D(Array.prototype.splice),h=D(String.prototype.toLowerCase),g=D(String.prototype.toString),T=D(String.prototype.match),y=D(String.prototype.replace),E=D(String.prototype.indexOf),A=D(String.prototype.trim),_=D(Object.prototype.hasOwnProperty),b=D(RegExp.prototype.test),S=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;function D(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function R(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function w(e){for(let t=0;t<e.length;t++){_(e,t)||(e[t]=null)}return e}function C(t){const n=l(null);for(const[o,r]of e(t)){_(t,o)&&(Array.isArray(r)?n[o]=w(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=C(r):n[o]=r)}return n}function O(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return D(n.get);if("function"==typeof n.value)return D(n.value)}e=o(e)}return function(){return null}}const v=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),k=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),x=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),L=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),I=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),M=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),U=i(["#text"]),z=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),P=i(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),F=i(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),H=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),B=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),G=a(/<%[\w\W]*|[\w\W]*%>/gm),W=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:G,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:W});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.3.3",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:D,Element:w,NodeFilter:B,NamedNodeMap:G=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:W,DOMParser:Y,trustedTypes:j}=n,q=w.prototype,$=O(q,"cloneNode"),V=O(q,"remove"),re=O(q,"nextSibling"),ie=O(q,"childNodes"),ae=O(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:be}=Z;let{IS_ALLOWED_URI:Se}=Z,Ne=null;const De=R({},[...v,...k,...x,...I,...U]);let Re=null;const we=R({},[...z,...P,...F,...H]);let Ce=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Oe=null,ve=null;const ke=Object.seal(l(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let xe=!0,Le=!0,Ie=!1,Me=!0,Ue=!1,ze=!0,Pe=!1,Fe=!1,He=!1,Be=!1,Ge=!1,We=!1,Ye=!0,je=!1,Xe=!0,qe=!1,$e={},Ke=null;const Ve=R({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ze=null;const Je=R({},["audio","video","img","source","image","track"]);let Qe=null;const et=R({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),tt="http://www.w3.org/1998/Math/MathML",nt="http://www.w3.org/2000/svg",ot="http://www.w3.org/1999/xhtml";let rt=ot,it=!1,at=null;const lt=R({},[tt,nt,ot],g);let ct=R({},["mi","mo","mn","ms","mtext"]),st=R({},["annotation-xml"]);const ut=R({},["title","style","font","a","script"]);let mt=null;const pt=["application/xhtml+xml","text/html"];let ft=null,dt=null;const ht=r.createElement("form"),gt=function(e){return e instanceof RegExp||e instanceof Function},Tt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!dt||dt!==e){if(e&&"object"==typeof e||(e={}),e=C(e),mt=-1===pt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,ft="application/xhtml+xml"===mt?g:h,Ne=_(e,"ALLOWED_TAGS")?R({},e.ALLOWED_TAGS,ft):De,Re=_(e,"ALLOWED_ATTR")?R({},e.ALLOWED_ATTR,ft):we,at=_(e,"ALLOWED_NAMESPACES")?R({},e.ALLOWED_NAMESPACES,g):lt,Qe=_(e,"ADD_URI_SAFE_ATTR")?R(C(et),e.ADD_URI_SAFE_ATTR,ft):et,Ze=_(e,"ADD_DATA_URI_TAGS")?R(C(Je),e.ADD_DATA_URI_TAGS,ft):Je,Ke=_(e,"FORBID_CONTENTS")?R({},e.FORBID_CONTENTS,ft):Ve,Oe=_(e,"FORBID_TAGS")?R({},e.FORBID_TAGS,ft):C({}),ve=_(e,"FORBID_ATTR")?R({},e.FORBID_ATTR,ft):C({}),$e=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,xe=!1!==e.ALLOW_ARIA_ATTR,Le=!1!==e.ALLOW_DATA_ATTR,Ie=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Ue=e.SAFE_FOR_TEMPLATES||!1,ze=!1!==e.SAFE_FOR_XML,Pe=e.WHOLE_DOCUMENT||!1,Be=e.RETURN_DOM||!1,Ge=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ye=!1!==e.SANITIZE_DOM,je=e.SANITIZE_NAMED_PROPS||!1,Xe=!1!==e.KEEP_CONTENT,qe=e.IN_PLACE||!1,Se=e.ALLOWED_URI_REGEXP||X,rt=e.NAMESPACE||ot,ct=e.MATHML_TEXT_INTEGRATION_POINTS||ct,st=e.HTML_INTEGRATION_POINTS||st,Ce=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&>(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&>(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ue&&(Le=!1),Ge&&(Be=!0),$e&&(Ne=R({},U),Re=l(null),!0===$e.html&&(R(Ne,v),R(Re,z)),!0===$e.svg&&(R(Ne,k),R(Re,P),R(Re,H)),!0===$e.svgFilters&&(R(Ne,x),R(Re,P),R(Re,H)),!0===$e.mathMl&&(R(Ne,I),R(Re,F),R(Re,H))),_(e,"ADD_TAGS")||(ke.tagCheck=null),_(e,"ADD_ATTR")||(ke.attributeCheck=null),e.ADD_TAGS&&("function"==typeof e.ADD_TAGS?ke.tagCheck=e.ADD_TAGS:(Ne===De&&(Ne=C(Ne)),R(Ne,e.ADD_TAGS,ft))),e.ADD_ATTR&&("function"==typeof e.ADD_ATTR?ke.attributeCheck=e.ADD_ATTR:(Re===we&&(Re=C(Re)),R(Re,e.ADD_ATTR,ft))),e.ADD_URI_SAFE_ATTR&&R(Qe,e.ADD_URI_SAFE_ATTR,ft),e.FORBID_CONTENTS&&(Ke===Ve&&(Ke=C(Ke)),R(Ke,e.FORBID_CONTENTS,ft)),e.ADD_FORBID_CONTENTS&&(Ke===Ve&&(Ke=C(Ke)),R(Ke,e.ADD_FORBID_CONTENTS,ft)),Xe&&(Ne["#text"]=!0),Pe&&R(Ne,["html","head","body"]),Ne.table&&(R(Ne,["tbody"]),delete Oe.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw S('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw S('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),dt=e}},yt=R({},[...k,...x,...L]),Et=R({},[...I,...M]),At=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},_t=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Be||Ge)try{At(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},bt=function(e){let t=null,n=null;if(He)e="<remove></remove>"+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===mt&&rt===ot&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=le?le.createHTML(e):e;if(rt===ot)try{t=(new Y).parseFromString(o,mt)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(rt,"template",null);try{t.documentElement.innerHTML=it?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),rt===ot?pe.call(t,Pe?"html":"body")[0]:Pe?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},Nt=function(e){return e instanceof W&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof G)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Dt=function(e){return"function"==typeof D&&e instanceof D};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,dt)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),Nt(e))return At(e),!0;const n=ft(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),ze&&e.hasChildNodes()&&!Dt(e.firstElementChild)&&b(/<[/\w!]/g,e.innerHTML)&&b(/<[/\w!]/g,e.textContent))return At(e),!0;if(e.nodeType===ee)return At(e),!0;if(ze&&e.nodeType===te&&b(/<[/\w]/g,e.data))return At(e),!0;if(!(ke.tagCheck instanceof Function&&ke.tagCheck(n))&&(!Ne[n]||Oe[n])){if(!Oe[n]&&Ot(n)){if(Ce.tagNameCheck instanceof RegExp&&b(Ce.tagNameCheck,n))return!1;if(Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))return!1}if(Xe&&!Ke[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return At(e),!0}return e instanceof w&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:rt,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!at[e.namespaceURI]&&(e.namespaceURI===nt?t.namespaceURI===ot?"svg"===n:t.namespaceURI===tt?"svg"===n&&("annotation-xml"===o||ct[o]):Boolean(yt[n]):e.namespaceURI===tt?t.namespaceURI===ot?"math"===n:t.namespaceURI===nt?"math"===n&&st[o]:Boolean(Et[n]):e.namespaceURI===ot?!(t.namespaceURI===nt&&!st[o])&&!(t.namespaceURI===tt&&!ct[o])&&!Et[n]&&(ut[n]||!yt[n]):!("application/xhtml+xml"!==mt||!at[e.namespaceURI]))}(e)?(At(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!b(/<\/no(script|embed|frames)/i,e.innerHTML)?(Ue&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(At(e),!0)},Ct=function(e,t,n){if(ve[t])return!1;if(Ye&&("id"===t||"name"===t)&&(n in r||n in ht))return!1;if(Le&&!ve[t]&&b(ye,t));else if(xe&&b(Ee,t));else if(ke.attributeCheck instanceof Function&&ke.attributeCheck(t,e));else if(!Re[t]||ve[t]){if(!(Ot(e)&&(Ce.tagNameCheck instanceof RegExp&&b(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&b(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t,e))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&b(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(Qe[t]);else if(b(Se,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ze[e]){if(Ie&&!b(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Ot=function(e){return"annotation-xml"!==e&&T(e,be)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||Nt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Re,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=ft(a),m=c;let f="value"===a?m:A(m);if(n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),f=n.attrValue,!je||"id"!==s&&"name"!==s||(_t(a,e),f="user-content-"+f),ze&&b(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,f)){_t(a,e);continue}if("attributename"===s&&T(f,"href")){_t(a,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){_t(a,e);continue}if(!Me&&b(/\/>/i,f)){_t(a,e);continue}Ue&&u([he,ge,Te],(e=>{f=y(f,e," ")}));const d=ft(e.nodeName);if(Ct(d,s,f)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(d,s)){case"TrustedHTML":f=le.createHTML(f);break;case"TrustedScriptURL":f=le.createScriptURL(f)}if(f!==m)try{l?e.setAttributeNS(l,a,f):e.setAttribute(a,f),Nt(e)?At(e):p(o.removed)}catch(t){_t(a,e)}}else _t(a,e)}Rt(de.afterSanitizeAttributes,e,null)},kt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(it=!e,it&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Dt(e)){if("function"!=typeof e.toString)throw S("toString is not a function");if("string"!=typeof(e=e.toString()))throw S("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Fe||Tt(t),o.removed=[],"string"==typeof e&&(qe=!1),qe){if(e.nodeName){const t=ft(e.nodeName);if(!Ne[t]||Oe[t])throw S("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof D)n=bt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Be&&!Ue&&!Pe&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=bt(e),!n)return Be?null:We?ce:""}n&&He&&At(n.firstChild);const c=St(qe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&kt(i.content);if(qe)return e;if(Be){if(Ge)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Re.shadowroot||Re.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=Pe?n.outerHTML:n.innerHTML;return Pe&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&b(K,n.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+m),Ue&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){Tt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Fe=!0},o.clearConfig=function(){dt=null,Fe=!1},o.isValidAttribute=function(e,t,n){dt||Tt({});const o=ft(e),r=ft(t);return Ct(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re}));
|
|
3
|
+
//# sourceMappingURL=purify.min.js.map
|
package/server.js
CHANGED
|
@@ -3,6 +3,8 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
|
|
6
|
+
const analytics = require('./analytics/db');
|
|
7
|
+
|
|
6
8
|
const PORT = process.env.PORT || 3000;
|
|
7
9
|
|
|
8
10
|
// Auto-version: hash all non-font files in public/ at startup.
|
|
@@ -30,9 +32,13 @@ const MIME = {
|
|
|
30
32
|
'.md': 'text/plain',
|
|
31
33
|
'.smd': 'text/plain',
|
|
32
34
|
'.woff2': 'font/woff2',
|
|
35
|
+
'.wasm': 'application/wasm',
|
|
33
36
|
};
|
|
34
37
|
|
|
38
|
+
const DEV_MODE = process.env.SDOCS_DEV === '1' || process.env.NODE_ENV === 'development';
|
|
39
|
+
|
|
35
40
|
function cacheHeader(ext) {
|
|
41
|
+
if (DEV_MODE) return 'no-store';
|
|
36
42
|
if (ext === '.html') return 'no-cache';
|
|
37
43
|
if (ext === '.woff2') return 'public, max-age=31536000, immutable';
|
|
38
44
|
if (ext === '.css' || ext === '.js') return 'public, max-age=86400';
|
|
@@ -67,17 +73,21 @@ const server = http.createServer((req, res) => {
|
|
|
67
73
|
return;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
// Version check — used by service worker to detect updates
|
|
76
|
+
// Version check — used by service worker to detect updates + analytics
|
|
71
77
|
if (pathname === '/version-check') {
|
|
72
78
|
const v = url.searchParams.get('v') || '';
|
|
79
|
+
const cohort = url.searchParams.get('cohort') || '';
|
|
80
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
|
73
81
|
console.log([
|
|
74
82
|
new Date().toISOString(),
|
|
75
|
-
|
|
83
|
+
ip,
|
|
76
84
|
req.headers['user-agent'] || '',
|
|
77
85
|
req.headers['referer'] || '',
|
|
78
86
|
req.headers['accept-language'] || '',
|
|
79
87
|
v ? 'cached:' + v : 'no-cache',
|
|
88
|
+
cohort || '-',
|
|
80
89
|
].join(' | '));
|
|
90
|
+
try { analytics.logVisit(ip, cohort, req.headers['user-agent'] || '', req.headers['referer'] || ''); } catch (e) { /* analytics failure should not break version-check */ }
|
|
81
91
|
res.writeHead(200, {
|
|
82
92
|
'Content-Type': 'application/json',
|
|
83
93
|
'Cache-Control': 'no-cache',
|
|
@@ -86,12 +96,51 @@ const server = http.createServer((req, res) => {
|
|
|
86
96
|
return;
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
// Analytics dashboard (public)
|
|
100
|
+
if (pathname === '/analytics') {
|
|
101
|
+
serveFile(res, path.join(__dirname, 'analytics', 'dashboard.html'), { 'Cache-Control': 'no-cache' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Analytics JSON API (public)
|
|
106
|
+
if (pathname === '/analytics/data') {
|
|
107
|
+
try {
|
|
108
|
+
const { getRetentionData } = require('./analytics/query');
|
|
109
|
+
const data = getRetentionData();
|
|
110
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' });
|
|
111
|
+
res.end(JSON.stringify(data));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
114
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
if (pathname === '/' || pathname === '/new') {
|
|
90
120
|
const htmlPath = path.join(__dirname, 'public', 'index.html');
|
|
91
121
|
fs.readFile(htmlPath, 'utf8', (err, html) => {
|
|
92
122
|
if (err) { res.writeHead(500); res.end('Error'); return; }
|
|
123
|
+
const nonce = crypto.randomBytes(16).toString('base64');
|
|
93
124
|
html = html.replace('__APP_VERSION__', APP_VERSION);
|
|
94
|
-
|
|
125
|
+
html = html.replace('__SDOCS_DEV__', DEV_MODE ? '1' : '0');
|
|
126
|
+
html = html.replace(/__CSP_NONCE__/g, nonce);
|
|
127
|
+
const csp = [
|
|
128
|
+
"default-src 'self'",
|
|
129
|
+
"script-src 'self' 'nonce-" + nonce + "' 'wasm-unsafe-eval' https://cdn.jsdelivr.net",
|
|
130
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
131
|
+
"font-src 'self' https://fonts.gstatic.com",
|
|
132
|
+
"img-src 'self' data: https:",
|
|
133
|
+
"connect-src 'self' https://cdn.jsdelivr.net",
|
|
134
|
+
"frame-src 'none'",
|
|
135
|
+
"object-src 'none'",
|
|
136
|
+
].join('; ');
|
|
137
|
+
res.writeHead(200, {
|
|
138
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
139
|
+
'Cache-Control': 'no-cache',
|
|
140
|
+
'Content-Security-Policy': csp,
|
|
141
|
+
'X-Content-Type-Options': 'nosniff',
|
|
142
|
+
'X-Frame-Options': 'DENY',
|
|
143
|
+
});
|
|
95
144
|
res.end(html);
|
|
96
145
|
});
|
|
97
146
|
return;
|
package/public/default.md
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
styles:
|
|
3
|
-
fontFamily: Inter
|
|
4
|
-
baseFontSize: 16
|
|
5
|
-
lineHeight: 1.75
|
|
6
|
-
h1: { fontSize: 2.1, fontWeight: 700 }
|
|
7
|
-
h2: { fontSize: 1.55, fontWeight: 600 }
|
|
8
|
-
h3: { fontSize: 1.2, fontWeight: 600 }
|
|
9
|
-
p: { lineHeight: 1.75, marginBottom: 1 }
|
|
10
|
-
light:
|
|
11
|
-
background: "#ffffff"
|
|
12
|
-
color: "#1c1917"
|
|
13
|
-
dark:
|
|
14
|
-
background: "#2c2a26"
|
|
15
|
-
color: "#e7e5e2"
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
# Say hello to SmallDocs: A markdown-first replacement for Word & GDocs
|
|
19
|
-
|
|
20
|
-
If you're working with agents, a document written in markdown is <ins>officially</ins>* 407 times more useful than a document locked inside a `.docx` or `.gdoc` file format. Because of this, I believe Word and GDocs' days are numbered. (*I am the official.)
|
|
21
|
-
|
|
22
|
-
But while markdown is great for agents, it's a bit annoying for humans. Quickly and elegantly reading a `.md` file requires you to open your code editor and enter "preview" mode. Sharing a markdown file requires you to actually send the file to someone. They then have to download it and find the least annoying way to read it.
|
|
23
|
-
|
|
24
|
-
SmallDocs is an [open source](https://github.com/JoshInLisbon/SDocs) attempt at something different. It lets you (or your agent) easily, elegantly and <ins>100% privately</ins> **read**, **format**, **share** and **export** `.md` files.
|
|
25
|
-
|
|
26
|
-
Reading a `.md` file in SmallDocs feels just like this (you're reading markdown right now). And by playing with the styles, it can feel like [this](https://sdocs.dev/#md=fVbNbttGEL7zKQb2IQAhEaJky4oOKdIEboq2QFoHNXLTcHcobkXuMrtDyWpRoA_RJ-yTFLMkHcm1e5N2vpn55psfaTqdJoGPNYV1AlA6y7fYmPq4hh-dxwSgwEC3zvKd-Z3WkK8SgNpY-kBmW_Ea8mx1nQBUhJp8jHEe5WONxxKNh_cmtDUeI6BBvzX2W8fsmjXMshghX8Mf0bXPNM9Wk_j1fsh0M5vBnwKcnwPzF4GLp8DFOXA5AFvBPS1q8oRlni0iuDZ2J3hNynlk4-waOqvJS4CIKGqndl86x_Q0fz6BwnlN_t5ortYwRpSkUZgC1W7rXWf1Gi4uSyyX5ewiWpSrnZfHxWK-zLF_7DV7NK2KHHO8iEFHnR6NOLuez_WjcXFmXBZX8_lyNLZntoWeU16OtrH8F5Iqp2PV54VQSSutLyZfvW7UFeY0ep0r1kv0boSqq9fXS7yYPBVnQZquTmMui-vV1aqPqdHvnpM0x3yZP5WUZvpaXT8raTnD2c1LkuornC3xBUkH2s9KqpSaFbMXJT1P-rykc5wv8tVp-eds_lfSG1xeX8_-I-l8kVO-PI2Jq9erVSQ6nU6T5BI-VQQ_d4YYfqG9qzuZf3ClrLmx8IkeOElunZftQE1hAlwRaKe6hizDAQMg7E3osAb0bEpUDKXxgQGtBgTlLKOx5CWo0YQBAilndQb3BKpygeJGBSiodJ7gML4enNcholD_1gUmPSzwKfTRhH4bKYUslsTO1RAqbElHxlwZuzN2myXJT-h32h0sGLsnz0FsIYPvGTDswhrSQ4UMpox-kQQobKiv6ps0SS4v4d4bNnYL94Yr1zHceqNEuSSR5AUFhsMAESYBtAnYtoQ-g7dgHVPh3A60o2BfxcRwdB2wk9qleASmpq2RSRwOlREP9PrRxdOXzngB1kaRDZQNLWN6YKiwLwuCMP_SYW34CP_89TcYhi1xAGHthhrxmCXJG7gQ7kGhN8K_cbHDJgDWBzwGEKFH5YVsYPScwduSyQNXyJOo8lbksuBsfZRMUBAz-ewiZr9jaiuy8IOx2yS5l49H13konW8wJmsfi5gA7ckf-1YWpFxDMmynumbwWbyZfGMsjl_byll6FaLMAbBtRcLN3jQbCBRCHPA9ebi7-wDIMMemn5m-1yii9hWjUoIvapqMT63zjKcPQj9kMhSXcC-D89l18B0amyRTSNOPEW9E_jSNEnwtUBLuQl_loSJP8mPm5Wt0_ZV85Cob5F09uG-2hkGbstxAqNwhEgB6QMX1EeLkqgrtlnSM8c7VNRbDz9oQoSG_JYla1kZxX7An1H1Z1jEoZAzsXVsZFcPcOtWFwd26KH2BHhpCKzLLdLPHYQPOlPiBqE0S-aSdjG3d77t0W7oYOwZbUSQ1lslKCKzTM8jb-GdEei7DCJvLzQSoaSsMJsjTJsXA5E3YhXQzGe5ObQIP-Omm729g3ynuPEFt9hRA2iCnQVphOFBdZkmy2WziVexD45g6-frSosetx7aCg-EK0rRwtU7TmDc1jLVRaQyaJdOTQD0hpiZ5c_IaD3pMKmdFWL4fbysGeI-MSfJW7l5_skpTkziO_YLiCFXXSBskfYs-0PjeoKqMpXgQY7p4aaAx1nlp_56sIatoOAuvhE7ZWY2SHeV6mpKHlRoPfhgWETQyxqUH3Mr7BILyppUPQqQ_enIHZMZ7obTxJFOa9Udyk2mnHja9DEWNageFe-g7tcka3VssuJYsyLXMkn8B&theme=light), [this](https://sdocs.dev/#md=jVbdbuPKDb7XUxDKRQDDEWzH3nh90WB_uqc5OH9oUgS9W1pDWVOPZnSGVBy1KNCH6BP2SQqOrDjJZoFzY1iajxzyI_lRFxcXGUvviDcZQBW8fMHGun4D-S-dtxLyDGCLTF-Cl1v7T9rA_CoDcNbTX8juatnAvFhnADWhocgb-NdLN7-FtrWe8ylwiU7tp9Bg3Fn_MYiEZgOzYgX_Vg_z0Xi4aFGspunx_njR1Ww2ABcvgfPi3feQl6-Ri5fAdyNw-Qr4NqxV1KvcX6czL-ZDPs76veINlSGi2OA3kHfeUFQPecKUwdB48wby4eXWhXL_exckHW1DNBTvrZF6A8vpi2xm400sCuUWS-t3SulyCtYb8inGxRGlIWcAWtFyv4uh82YD-VmF1aqq8nRSBheivlyY-XZJw8uhMk9HV-UlkhliHavxdLil-Xp1Orx8cTgz75fr9Xg4EvQdxyM3r4Kd0VWF-fRk9f7y8pJwtHqLvE8jtJytl1WZT1_7vKQyvX7yudoul1fHQA3G_Vu0Ld8tF6vFK9qMMbNq9iZt5XK7qr5HW_Ue12b5HdpWRHg6_Ia2l47fpm0xm68uV89TNOvtsqI_RNt6uyqrd9_QNi_ni8vZc5-4fr8uZ-rz4uIiy87gM7HdefgtWl_a1hFDFSLcheAY7moU-AlZsuyuJtgSC0g6wUj-XEBqguCJ4WClTk9NYIGKULpIXMBdTf15pBNQ1GVF5GBiPT1Ywa2jCfzvP_8FZ_cJ2EMZOmfUf40Pei15MClMMoC-hyA1RThgX2TZ2RncCkaB-zGEL0TO-l2WfaQqRII-dHCIVggQ2PqdoyQQEKpUiSmg5wNFkNryBupwAK41AODQUPAEE413Aoea_BBfx8lrTGRcZ9mfID_SaBl8EPhHxwIHTdUKuBD2PGSH3qTkh8cCTlZ6rRU4hLjnIk983Ao9EPwYtpxlvwSB_DBwN3A7BmkHlgaTxG4ZGmJwKBRTAdKN1u_0GuWn0u5IalfAnx8o9lLrKTkmhdimddSQlwGiDJ-NnDLchyj1MW61-hJill3AZPIp-Moa8iVNhnLmN7D34QD0iKW4fqBDOYaDdQ5MyJPdbUtkRpPUcAdkqJAllQQ93AA9tlQKmcHiMyWdHG1-racp73MGBG9LAgldWQ_Yu9jxE_LnHgwKapKMFUFNkfLUQX-lJjwQ_M2LdXAj8DES7jnLEj0j5WqHUAaWAm6G61If2dQVYELJU0BoMfVhDN1u6Mdjk8HvHbFSOhp0TPGcobHeDHU6AVibPz9W-ECAxiTqrocyW709NyHNk2UgjB6ssJa-dfRopb_Oh8Kp37s6EsEPKMRPQ4FPWXFtW55qHzXatS0yAzqnKRBtsmxewGRywwogZvJi0V1PJnCfgkNoCLUTqs6B75otRR0szU1T42RnK_01Ns00PVqW62xxcmu9dFbsA6nbT-ifBk_nTIfCSh06gUhotOeU6evs8uRA0w6sSqIePisvx1kahGnQi3FyphAimCOojIRCwC2VFh2UyMTXWXZTPWOoQquS53vYoVCi6oBWuIAfQlCZqOSAQ3-0KJa8DLr0gVhqElsyfIgEOsO_tlpgdFn2KZhkoUlBE5JGoh9Kq2ol5Au48UKxwpKS4gKrEp6wchTX0tlyT8cmerDcoQOHPcVjI50-b07dU4am6bwtxyH_-vVr9gEO5NxFFWKDImSU6E6VABj79PmZ3x4rU2JULd5qWbQFizxTc-0brb5PX6wGhB6fGd9pr6YhMhErKeBn7LdU5Ony7K5vwy5iW_fT8TNpmgQz7a5x47jusYv2-WpR5VSJF0pNS4lRihuYpNlIqUQeXHEYCj-It21octwgtW2n6tHDT5aF_LDw0jIz6HcUQ8fQ1shpbWw768wzRVVGDqomQyC19Xv9p9GZ4KmAvw8PPkiR7kpW3KBzulIfKLIWJ6k3Ndtg7Bhj-7SWVeTIwwGlrFPOLYXWEWApHTrXH4elyLL7QWipB7vzqVnIOR7WYDoKEJPeFXCCssRup6tx2OTfWLCuBVv1z22Q9-lj4Vt0Igg8PWo8yqQuymM3VtZbrsk87dNI52lrpi3kqEoe0Jijyv4xvKCu1_RJ8H8&theme=dark) or even [this](https://sdocs.dev/#md=pVbdbtvMEb3nUwysiwCqKFiSJdkC2iBxkiZA2y-wnaa903B3KG603GF2llKUtkAfok_YJyl2KSqy41xVd-L87NkzZw6Z53km4WBJVhlAyS68w9rYwwruufWK4B6dwCwDKFDoHbtwb77TCibzDMAaR-_JbKqwgsl4mQFUhJp86vW42x0XHBjuLRYpVqPfGPeaQ-B6BZfjq1g8WcE_UlV3xnQ8HaW_n49nLC8v4V8xcfo4cTKe_yJx9jRx8iRzccxsYuKT-4x-QnmTchVr6vuuoGbH0qCiFLPGbWNMk2KPwbBbgWPXBQvLavu15ZDKC_aa_GejQ7WCq2O1hBiK_YzbxCNn82MkokrUFai2G8-t0yu4GJTzclniRYootuzjwwlOZ7Np97Bj9RS6VFe4oIvUtGfyR918Tsv5KTh7HKRFOVd9sL_oj8azxQ1O-nDP0WOwdE1Uzi9GP6oWenqtb_qq5wi6PR1AOKebi9HTnrq8KvG85xXO54vrrqdGv32Otks1mU4vn9CmrvWC5s_SttR6Vqpf0LZY0nV58wvaFkTLYvlL2h43fp62yWJ6OZueXxGX10WJ_w9tk8kEp49ou76-ucHUM8_zLBvA69ZYbdwGEG7_9AEeKgxwR9KQCgIfgsAnIS9Z9go2zBoU1zU6ncctgsBsQSpurYaSyII1WwIExW5HXtJmwN6EChC2jveW9IawsBTxWMJNS_Dff_8n5isjNIKKbFO2dgToNDjakY8xTaLIRZBjeE-eXghUvIfAUETwwI7GWTYYwEdvnDKNJZis4A3Db47goYqX-0zWZtnbHfkDSFscb9Fjr3AXYYtxG0sj4GJnuBVoWt-w0Bg-lHDgFhS6FwEiGm8Kgn2kCntGQDMJGBfRgJAL5BSNwIQXApojhsAMdauqcZat1-sCpcoG8EdmvQJlCX1_XiaaFUiFnuDu7as3f347rjWcfgNQ3ByO8ai0SISypikYvT4Wq4pqhJ9_A2i8cQHS2wAazw35YEiSElCvAOvCbFpupWvUeFYkcoYjz7kNTRt-n47O89PJkOeO88LzXsjHG2bZQ0XwyZlv0FTGsnBTHcBIJPFLKwEcS0C7MZhEkJhKGvv0t6fjnK7gnpyYKJ03VGJrg4zgXWst3LILnm132HfynCt2pdm0nTEDfWvImziMftoFAX1TZC25MIbXbYB9RQ7aqHNwRFHkqecINmZHYEJkOFRUJy1zG0CRp5rd4WyUiS5PDfvwaFw97-nGe_ZbSbf1hBpq1jQC3V2om4k8bZTn3axOjRLCCGhPuH0J3JCL4PqRoiP7cw-P-3Mwe3ShK0rv_5dQkU8XNdJN7g_wqtvuEEXu6WtrPAk8pjYuIEXeytaCEcCzkgoFSjSWNGAAEyQKr0Z_gC9cpPEOIE7sOE24D6i2WfZ3bn1PiAC5aJVx9zxwY5xhJ2O4rZiFuoFosqYgj4HsYZVlkzEMh78lfULJvsYwHCa6411rlpC2ldOwQaEQ7I2TbBrL_kq-YDHhcCz52hoKUBx6OCNY5_kuJdG6k0wcBelsFsvvsaRTrSYJvlUhygdVJEt6EjsOIzTDLruKpR_JJ7BO0bG-RIl6QB_aZgQWvx_AMiabLtlDNMZDSMZGVujJssxWvYGna380DUW3jhYeHT7NyJpd51bogBTLQQLVY_gQZ63oGEwseq6BQ0U-FUpy5gZFSKAzgiiDlCBjeM-OPYTKSLdDqMLZiiiMOx9Ioib_CUmkz3qUBG1ctyzPFnXe92zR787sXVKLLMonxw25cDIveAbAANAdouHpVpE_np6WIWr1rrUkifyeT7g1wXwnJ5VpsiyH4VCCjuZgujyNAYfDUcRF3vdPaxLBDUkqePvNhPQ1IFBjCOSP419frlOytCq67yh-XObR29LTuFatp9ThLwzGBYpER601nusmyHDY6bOjJG1mY5qu4j4Js_Wkj_Prk9d5_kXYrWN-GrBOuopL-oASCB5IQpbdta5byKSjMo2boDReAgRT06h74TsG9Ju2JhdkDJ87S2gacvKy-8aoOcZOy5mcWwIGOtrjjtDKudKxiOTGF398iaYXsj_KPOFJ_v0iYRhn_wM&theme=light):
|
|
27
|
-
|
|
28
|
-

|
|
29
|
-
|
|
30
|
-
Use SmallDoc's CLI for speed. Creating a SmallDoc for a `.md` file (+ automatically opening your browser to read it) is as simple as:
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
# npm i -g sdocs-dev
|
|
34
|
-
sdoc README.md
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## How SmallDocs work
|
|
38
|
-
|
|
39
|
-
### URLs
|
|
40
|
-
|
|
41
|
-
The URL format for SmallDocs is:
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
https://sdocs.dev/#md={compressed & encoded .md}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Your entire document (content and styles) lives in the URL hash.
|
|
48
|
-
|
|
49
|
-
To keep URLs as short as possible, SmallDocs compresses your markdown using [deflate](https://en.wikipedia.org/wiki/Deflate) (a standard compression algorithm built into every browser) and then encodes the result with [base64url](https://en.wikipedia.org/wiki/Base64#URL_applications) (a URL-safe variant of base64 that avoids characters like `+`, `/`, and `=` which would otherwise need percent-encoding).
|
|
50
|
-
|
|
51
|
-
The `mode` parameter controls which view opens. Valid values are `read` (clean reading view, style panel hidden), `style` (style panel visible), and `raw` (raw markdown editor). When sharing a link for someone to read, use `mode=read`:
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
https://sdocs.dev/#md=...&mode=read
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
You can also link directly to a section using the `sec` parameter. Click any heading's link icon to copy its section URL:
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
https://sdocs.dev/#md=...&sec=url-formatting
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The `sec` value is the heading text slugified (lowercased, spaces become hyphens, special characters stripped). The page will scroll to that section on load.
|
|
64
|
-
|
|
65
|
-
The `theme` parameter forces a specific theme: `theme=light` or `theme=dark`. This overrides the reader's system preference, which is useful when sharing a link where the document looks best in a particular theme.
|
|
66
|
-
|
|
67
|
-
### Privacy
|
|
68
|
-
|
|
69
|
-
Because the SmallDocs url format is:
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
https://sdocs.dev/#md={compressed & encoded .md}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Your document never hits the SDocs server.
|
|
76
|
-
|
|
77
|
-
This layer of privacy is built into how HTTP works. The hash fragment (everything after the `#` in a URL) is never sent to the server by the browser. It always stays entirely client-side:
|
|
78
|
-
|
|
79
|
-
> "The fragment is not sent to the server when the URI is requested; it is processed by the client" - [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment)
|
|
80
|
-
|
|
81
|
-
The [sdocs.dev](https://sdocs.dev) site is purely a rendering space. JavaScript reads `window.location.hash`, decompresses and decodes the content, and renders your `.md` locally. The server is about 60 lines of Node.js that serves static files — no database, no logging, no analytics.
|
|
82
|
-
|
|
83
|
-
### Formatting
|
|
84
|
-
|
|
85
|
-
SDocs adds basic styling to markdown files. You write your content in regular markdown and the styles live in a metadata block at the top of the file.
|
|
86
|
-
|
|
87
|
-
That metadata block is called [YAML front matter](https://jekyllrb.com/docs/front-matter/). It's a convention that started with [Jekyll](https://jekyllrb.com/) (the static site generator) back in 2008 and has since been adopted by [Hugo](https://gohugo.io/), [Gatsby](https://www.gatsbyjs.com/), [Obsidian](https://obsidian.md/), and most of the markdown ecosystem. It looks like a block of key-value pairs between two `---` lines at the top of your file:
|
|
88
|
-
|
|
89
|
-
```yaml
|
|
90
|
-
---
|
|
91
|
-
title: My Document
|
|
92
|
-
author: Someone
|
|
93
|
-
---
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
SDocs uses a `styles:` key with CSS properties written beneath it in YAML:
|
|
97
|
-
|
|
98
|
-
```yaml
|
|
99
|
-
---
|
|
100
|
-
styles:
|
|
101
|
-
fontFamily: Lora
|
|
102
|
-
baseFontSize: 17
|
|
103
|
-
h1: { fontSize: 2.3, fontWeight: 700 }
|
|
104
|
-
p: { lineHeight: 1.9, marginBottom: 1.2 }
|
|
105
|
-
...
|
|
106
|
-
---
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
(Click "**Raw**" — top left — to see the front matter for this file. See all available properties [here](https://sdocs.dev) or by running `npm i sdocs-dev; sdoc schema`.)
|
|
110
|
-
|
|
111
|
-
When a `Styled .md` file is rendered in the SmallDocs interface the specified styles are applied. If a plain `.md` file is rendered the default styles are applied.
|
|
112
|
-
|
|
113
|
-
#### Light & dark modes
|
|
114
|
-
|
|
115
|
-
You can nest styles in `light` and `dark` keys:
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
light:
|
|
119
|
-
background: "#fffaf5"
|
|
120
|
-
color: "#1a1a2e"
|
|
121
|
-
h1: { color: "#c0392b" }
|
|
122
|
-
dark:
|
|
123
|
-
background: "#1a1520"
|
|
124
|
-
color: "#e7e5e2"
|
|
125
|
-
h1: { color: "#ef6f5e" }
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
These will be used when you view the site in each mode.
|
|
129
|
-
|
|
130
|
-
Non-color properties (fonts, sizes, spacing) are shared across themes and live at the top level.
|
|
131
|
-
|
|
132
|
-
All color controls are in the **Colors** section of the style panel. The light/dark toggle at the top of that section lets you customize each theme independently. Colors cascade from general to specific — set `color` once and it flows to headings, paragraphs, and lists unless you override them individually.
|
|
133
|
-
|
|
134
|
-
### Drag & drop
|
|
135
|
-
|
|
136
|
-
Drag any `.md` file onto the editor to SmallDoc it instantly.
|
|
137
|
-
|
|
138
|
-
### Exports
|
|
139
|
-
|
|
140
|
-
SmallDocs can export your document in four formats:
|
|
141
|
-
|
|
142
|
-
- **Raw .md** — your markdown content with all front matter stripped. Plain markdown, compatible with anything.
|
|
143
|
-
- **PDF** — a styled PDF generated from the rendered view via the browser's print engine.
|
|
144
|
-
- **Word (.docx)** — a styled Word document generated from the rendered HTML.
|
|
145
|
-
- **Styled .md** — your markdown with the `styles:` front matter block included. This is the format SmallDocs reads back in, so your formatting is preserved.
|
|
146
|
-
|
|
147
|
-
### Collapsed headers
|
|
148
|
-
|
|
149
|
-
SmallDocs loads with all headers collapsed. This is done because it makes it easy to get an overview of the whole document.
|
|
150
|
-
|
|
151
|
-
If you expand a parent, all of its children expand too.
|
|
152
|
-
|
|
153
|
-
### Copy & paste
|
|
154
|
-
|
|
155
|
-
Every header has its own copy and paste button. This copies its content and all of its children's content. At the moment this is the fastest way to get SmallDoc content into your agent's context, but we're looking for novel ideas to make this better.
|
|
156
|
-
|
|
157
|
-
### Works offline
|
|
158
|
-
|
|
159
|
-
`https://sdocs.dev` uses extensive client side caching. If you've loaded the site once, you can visit it even when you're offline. If something has changed server side, we invalidate the cache and the next time you visit the site you'll get the latest version.
|
|
160
|
-
|
|
161
|
-
### Auto-save
|
|
162
|
-
|
|
163
|
-
Because the URL includes your full document and dynamically updates via JavaScript, every change you make is instantly preserved in the URL. This works when you're offline.
|
|
164
|
-
|
|
165
|
-
## The CLI
|
|
166
|
-
|
|
167
|
-
### Installation
|
|
168
|
-
|
|
169
|
-
SmallDocs has a command-line tool that lets you open, share, and style markdown files from the terminal. Install it once:
|
|
170
|
-
|
|
171
|
-
```
|
|
172
|
-
npm i -g sdocs-dev
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
This gives you the `sdoc` command.
|
|
176
|
-
|
|
177
|
-
### Open a file
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
sdoc README.md
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Your browser opens with the document styled and readable. That's it — one command to go from `.md` file to formatted document.
|
|
184
|
-
|
|
185
|
-
### Share a link
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
sdoc share README.md
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
This copies a shareable link to your clipboard.
|
|
192
|
-
|
|
193
|
-
You can also combine it with options:
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
sdoc share report.md --section "Results" # deep-link to a heading
|
|
197
|
-
sdoc share notes.md --write # link opens in write mode
|
|
198
|
-
sdoc share notes.md --dark # link opens in dark theme
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Start a new document
|
|
202
|
-
|
|
203
|
-
```
|
|
204
|
-
sdoc new
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Opens a blank document in write mode, ready to type a `h1`.
|
|
208
|
-
|
|
209
|
-
### Style schema
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
sdoc schema
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Prints every available style property with its type, default value, and description. This is designed to be readable by both humans and LLMs — so your agent can write YAML front matter for you.
|
|
216
|
-
|
|
217
|
-
### Modes
|
|
218
|
-
|
|
219
|
-
By default, files open in read mode. You can open in any mode:
|
|
220
|
-
|
|
221
|
-
```
|
|
222
|
-
sdoc README.md # read mode (default)
|
|
223
|
-
sdoc README.md --write # write mode (contentEditable editor)
|
|
224
|
-
sdoc README.md --style # style mode (styling panel visible)
|
|
225
|
-
sdoc README.md --raw # raw mode (plain markdown source)
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Pipe from stdin
|
|
229
|
-
|
|
230
|
-
Any command that outputs markdown can be piped directly into SmallDocs:
|
|
231
|
-
|
|
232
|
-
```
|
|
233
|
-
cat notes.md | sdoc # open in browser
|
|
234
|
-
cat notes.md | sdoc share # pipe to clipboard link
|
|
235
|
-
your-agent --output md | sdoc # pipe agent output to browser
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Default styles
|
|
239
|
-
|
|
240
|
-
If you find a style you like, use the "Save as Default" panel in the Style view to generate a command that saves your preferences to `~/.sdocs/styles.yaml`. The CLI automatically applies these defaults to every file you open — unless the file has its own styles, which always take priority.
|
|
241
|
-
|
|
242
|
-
```
|
|
243
|
-
sdoc defaults # view your current defaults
|
|
244
|
-
sdoc defaults --reset # remove them
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### For agents
|
|
248
|
-
|
|
249
|
-
The CLI is designed to work well in automated workflows. A few patterns:
|
|
250
|
-
|
|
251
|
-
- **Generate a styled doc**: have your agent write a `.md` file with YAML front matter, then `sdoc share file.md` to copy a shareable link
|
|
252
|
-
- **Learn the format**: `sdoc schema` gives your agent everything it needs to know about available style properties
|
|
253
|
-
- **Deep-link to context**: `sdoc share file.md --section "Heading"` creates a URL that scrolls straight to the relevant section
|
|
254
|
-
- **No auth, no API keys**: everything is client-side — the URL *is* the document
|
|
255
|
-
|