>>3;const h=1<<1+(7&r);c&&(a.localColorTable=lo(t,h));const d=t=>{const{r:s,g:n,b:r}=(c?a.localColorTable:e.globalColorTable)[t];return{r:s,g:n,b:r,a:t===o(null)?i?~~((s+n+r)/3):0:255}},u=(()=>{try{return new ImageData(a.width,a.height,{colorSpace:"srgb"})}catch(t){if(t instanceof DOMException&&"IndexSizeError"===t.name)return null;throw t}})();if(null==u)throw new EvalError("GIF frame size is to large");const p=t.nextByte(),f=t.readSubBlocksBin(),v=1<{const i=t>>>3,s=7&t;return(f[i]+(f[i+1]<<8)+(f[i+2]<<16)&(1<>>s};if(l){for(let i=0,o=p+1,r=0,c=[[0]],l=0;l<4;l++){if(ao[l]=c.length?c.push(c[s].concat(c[s][0])):s!==v&&c.push(c[s].concat(c[i][0]));for(let s=0;s=a.height))break}n?.(t.pos/(t.data.length-1),s(!1)+1,u,{x:a.left,y:a.top},{width:e.width,height:e.height})}a.image=u,a.bitmap=await createImageBitmap(u)}else{for(let t=0,e=p+1,i=0,s=[[0]],o=-4;;){const n=t;if(t=m(i,e),i+=e,t===v){e=p+1,s.length=v+2;for(let t=0;t=s.length?s.push(s[n].concat(s[n][0])):n!==v&&s.push(s[n].concat(s[t][0]));for(let e=0;e=1<>>5,o.disposalMethod=(28&n)>>>2,o.userInputDelayFlag=2==(2&n);const a=1==(1&n);o.delayTime=10*t.nextTwoBytes();const r=t.nextByte();a&&s(r),t.pos++;break}case 255:{t.pos++;const i={identifier:t.getString(8),authenticationCode:t.getString(3),data:t.readSubBlocksBin()};e.applicationExtensions.push(i);break}case 254:e.comments.push([i(!1),t.readSubBlocks()]);break;case 1:if(0===e.globalColorTable.length)throw new EvalError("plain text extension without global color table");t.pos++,e.frames[i(!1)].plainTextData={left:t.nextTwoBytes(),top:t.nextTwoBytes(),width:t.nextTwoBytes(),height:t.nextTwoBytes(),charSize:{width:t.nextTwoBytes(),height:t.nextTwoBytes()},foregroundColor:t.nextByte(),backgroundColor:t.nextByte(),text:t.readSubBlocks()};break;default:t.skipSubBlocks()}}(t,e,s,o);break;default:throw new EvalError("undefined block found")}return!1}const uo=/(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d.]+%?\))|currentcolor/gi;async function po(t){return new Promise((e=>{t.loading=!0;const i=new Image;t.element=i,i.addEventListener("load",(()=>{t.loading=!1,e()})),i.addEventListener("error",(()=>{t.element=void 0,t.error=!0,t.loading=!1,$().error(`${f} loading image: ${t.source}`),e()})),i.src=t.source}))}async function fo(t){if("gif"===t.type){t.loading=!0;try{t.gifData=await async function(t,e,i){i||(i=!1);const s=await fetch(t);if(!s.ok&&404===s.status)throw new EvalError("file not found");const o=await s.arrayBuffer(),n={width:0,height:0,totalTime:0,colorRes:0,pixelAspectRatio:0,frames:[],sortFlag:!1,globalColorTable:[],backgroundImage:new ImageData(1,1,{colorSpace:"srgb"}),comments:[],applicationExtensions:[]},a=new co(new Uint8ClampedArray(o));if("GIF89a"!==a.getString(6))throw new Error("not a supported GIF file");n.width=a.nextTwoBytes(),n.height=a.nextTwoBytes();const r=a.nextByte(),c=128==(128&r);n.colorRes=(112&r)>>>4,n.sortFlag=8==(8&r);const l=1<<1+(7&r),h=a.nextByte();n.pixelAspectRatio=a.nextByte(),0!==n.pixelAspectRatio&&(n.pixelAspectRatio=(n.pixelAspectRatio+15)/64),c&&(n.globalColorTable=lo(a,l));const d=(()=>{try{return new ImageData(n.width,n.height,{colorSpace:"srgb"})}catch(t){if(t instanceof DOMException&&"IndexSizeError"===t.name)return null;throw t}})();if(null==d)throw new Error("GIF frame size is to large");const{r:u,g:p,b:f}=n.globalColorTable[h];d.data.set(c?[u,p,f,255]:[0,0,0,0]);for(let t=4;t(t&&(m=!0),v),b=t=>(null!=t&&(y=t),y);try{do{m&&(n.frames.push({left:0,top:0,width:0,height:0,disposalMethod:0,image:new ImageData(1,1,{colorSpace:"srgb"}),plainTextData:null,userInputDelayFlag:!1,delayTime:0,sortFlag:!1,localColorTable:[],reserved:0,GCreserved:0}),v++,y=-1,m=!1)}while(!await ho(a,n,i,g,b,e));n.frames.length--;for(const t of n.frames){if(t.userInputDelayFlag&&0===t.delayTime){n.totalTime=1/0;break}n.totalTime+=t.delayTime}return n}catch(t){if(t instanceof EvalError)throw new Error(`error while parsing frame ${v} "${t.message}"`);throw t}}(t.source),t.gifLoopCount=function(t){for(const e of t.applicationExtensions)if(e.identifier+e.authenticationCode==="NETSCAPE2.0")return e.data[1]+(e.data[2]<<8);return NaN}(t.gifData)??0,0===t.gifLoopCount&&(t.gifLoopCount=1/0)}catch{t.error=!0}t.loading=!1}else await po(t)}async function vo(t){if("svg"!==t.type)return void await po(t);t.loading=!0;const e=await fetch(t.source);e.ok?t.svgData=await e.text():($().error(`${f} Image not found`),t.error=!0),t.loading=!1}function mo(t,e,i,s){const o=function(t,e,i){const{svgData:s}=t;if(!s)return"";const o=Zt(e,i);if(s.includes("fill"))return s.replace(uo,(()=>o));const n=s.indexOf(">");return`${s.substring(0,n)} fill="${o}"${s.substring(n)}`}(t,i,s.opacity?.value??1),n={color:i,gif:e.gif,data:{...t,svgData:o},loaded:!1,ratio:e.width/e.height,replaceColor:e.replaceColor??e.replace_color,source:e.src};return new Promise((e=>{const i=new Blob([o],{type:"image/svg+xml"}),s=URL||window.URL||window.webkitURL||window,a=s.createObjectURL(i),r=new Image;r.addEventListener("load",(()=>{n.loaded=!0,n.element=r,e(n),s.revokeObjectURL(a)})),r.addEventListener("error",(async()=>{s.revokeObjectURL(a);const i={...t,error:!1,loading:!0};await po(i),n.loaded=!0,n.element=i.element,e(n)})),r.src=a}))}class yo{constructor(t){this.loadImageShape=async t=>{if(!this._engine.loadImage)throw new Error(`${f} image shape not initialized`);await this._engine.loadImage({gif:t.gif,name:t.name,replaceColor:t.replaceColor??t.replace_color??!1,src:t.src})},this._engine=t}addImage(t){this._engine.images||(this._engine.images=[]),this._engine.images.push(t)}draw(t,e,i,s,o){const n=e.image,a=n?.element;if(n){if(t.globalAlpha=s,n.gif&&n.gifData){const s=new OffscreenCanvas(n.gifData.width,n.gifData.height),a=s.getContext("2d");if(!a)throw new Error("could not create offscreen canvas context");a.imageSmoothingQuality="low",a.imageSmoothingEnabled=!1,a.clearRect(0,0,s.width,s.height),void 0===e.gifLoopCount&&(e.gifLoopCount=n.gifLoopCount??0);let r=e.gifFrame??0;const c={x:.5*-n.gifData.width,y:.5*-n.gifData.height},l=n.gifData.frames[r];if(void 0===e.gifTime&&(e.gifTime=0),!l.bitmap)return;switch(t.scale(i/n.gifData.width,i/n.gifData.height),l.disposalMethod){case 4:case 5:case 6:case 7:case 0:a.drawImage(l.bitmap,l.left,l.top),t.drawImage(s,c.x,c.y),a.clearRect(0,0,s.width,s.height);break;case 1:a.drawImage(l.bitmap,l.left,l.top),t.drawImage(s,c.x,c.y);break;case 2:a.drawImage(l.bitmap,l.left,l.top),t.drawImage(s,c.x,c.y),a.clearRect(0,0,s.width,s.height),0===n.gifData.globalColorTable.length?a.putImageData(n.gifData.frames[0].image,c.x+l.left,c.y+l.top):a.putImageData(n.gifData.backgroundImage,c.x,c.y);break;case 3:{const e=a.getImageData(0,0,s.width,s.height);a.drawImage(l.bitmap,l.left,l.top),t.drawImage(s,c.x,c.y),a.clearRect(0,0,s.width,s.height),a.putImageData(e,0,0)}}if(e.gifTime+=o.value,e.gifTime>l.delayTime){if(e.gifTime-=l.delayTime,++r>=n.gifData.frames.length){if(--e.gifLoopCount<=0)return;r=0,a.clearRect(0,0,s.width,s.height)}e.gifFrame=r}t.scale(n.gifData.width/i,n.gifData.height/i)}else if(a){const e=n.ratio,s={x:-i,y:-i};t.drawImage(a,s.x,s.y,2*i,2*i/e)}t.globalAlpha=1}}getSidesCount(){return 12}async init(t){const e=t.actualOptions;if(e.preload&&this._engine.loadImage)for(const t of e.preload)await this._engine.loadImage(t)}loadShape(t){if("image"!==t.shape&&"images"!==t.shape)return;this._engine.images||(this._engine.images=[]);const e=t.shapeData;this._engine.images.find((t=>t.name===e.name||t.source===e.src))||this.loadImageShape(e).then((()=>{this.loadShape(t)}))}particleInit(t,e){if("image"!==e.shape&&"images"!==e.shape)return;this._engine.images||(this._engine.images=[]);const i=this._engine.images,s=e.shapeData,o=e.getFillColor(),n=i.find((t=>t.name===s.name||t.source===s.src));if(!n)return;const a=s.replaceColor??s.replace_color??n.replaceColor;n.loading?setTimeout((()=>{this.particleInit(t,e)})):(async()=>{let t;t=n.svgData&&o?await mo(n,s,o,e):{color:o,data:n,element:n.element,gif:n.gif,gifData:n.gifData,gifLoopCount:n.gifLoopCount,loaded:!0,ratio:s.width&&s.height?s.width/s.height:n.ratio??1,replaceColor:a,source:s.src},t.ratio||(t.ratio=1);const i={image:t,fill:s.fill??e.fill,close:s.close??e.close};e.image=i.image,e.fill=i.fill,e.close=i.close})()}}class go{constructor(){this.src="",this.gif=!1}load(t){t&&(void 0!==t.gif&&(this.gif=t.gif),void 0!==t.height&&(this.height=t.height),void 0!==t.name&&(this.name=t.name),void 0!==t.replaceColor&&(this.replaceColor=t.replaceColor),void 0!==t.src&&(this.src=t.src),void 0!==t.width&&(this.width=t.width))}}class bo{constructor(t){this.id="imagePreloader",this._engine=t}getPlugin(){return{}}loadOptions(t,e){if(!e||!e.preload)return;t.preload||(t.preload=[]);const i=t.preload;for(const t of e.preload){const e=i.find((e=>e.name===t.name||e.src===t.src));if(e)e.load(t);else{const e=new go;e.load(t),i.push(e)}}}needsPlugin(){return!0}}async function wo(t,e=!0){!function(t){t.loadImage||(t.loadImage=async e=>{if(!e.name&&!e.src)throw new Error(`${f} no image source provided`);if(t.images||(t.images=[]),!t.images.find((t=>t.name===e.name||t.source===e.src)))try{const i={gif:e.gif??!1,name:e.name??e.src,source:e.src,type:e.src.substring(e.src.length-3),error:!1,loading:!0,replaceColor:e.replaceColor,ratio:e.width&&e.height?e.width/e.height:void 0};t.images.push(i);const s=e.gif?fo:e.replaceColor?vo:po;await s(i)}catch{throw new Error(`${f} ${e.name??e.src} not found`)}})}(t);const i=new bo(t);await t.addPlugin(i,e),await t.addShape(["image","images"],new yo(t),e)}class xo extends St{constructor(){super(),this.sync=!1}load(t){t&&(super.load(t),void 0!==t.sync&&(this.sync=t.sync))}}class _o extends St{constructor(){super(),this.random.minimumValue=1e-4,this.sync=!1}load(t){t&&(super.load(t),void 0!==t.sync&&(this.sync=t.sync))}}class ko{constructor(){this.count=0,this.delay=new xo,this.duration=new _o}load(t){t&&(void 0!==t.count&&(this.count=t.count),this.delay.load(t.delay),this.duration.load(t.duration))}}class zo{constructor(t){this.container=t}init(t){const e=this.container,i=t.options.life;i&&(t.life={delay:e.retina.reduceFactor?P(i.delay.value)*(i.delay.sync?1:_())/e.retina.reduceFactor*1e3:0,delayTime:0,duration:e.retina.reduceFactor?P(i.duration.value)*(i.duration.sync?1:_())/e.retina.reduceFactor*1e3:0,time:0,count:i.count},t.life.duration<=0&&(t.life.duration=-1),t.life.count<=0&&(t.life.count=-1),t.life&&(t.spawning=t.life.delay>0))}isEnabled(t){return!t.destroyed}loadOptions(t,...e){t.life||(t.life=new ko);for(const i of e)t.life.load(i?.life)}update(t,e){if(!this.isEnabled(t)||!t.life)return;const i=t.life;let s=!1;if(t.spawning){if(i.delayTime+=e.value,!(i.delayTime>=t.life.delay))return;s=!0,t.spawning=!1,i.delayTime=0,i.time=0}if(-1===i.duration)return;if(t.spawning)return;if(s?i.time=0:i.time+=e.value,i.time0&&t.life.count--,0===t.life.count)return void t.destroy();const o=this.container.canvas.size,n=O(0,o.width),a=O(0,o.width);t.position.x=M(n),t.position.y=M(a),t.spawning=!0,i.delayTime=0,i.time=0,t.reset();const r=t.options.life;r&&(i.delay=1e3*P(r.delay.value),i.duration=1e3*P(r.duration.value))}}class Mo{draw(t,e,i){const s=e.shapeData;t.moveTo(-i/2,0),t.lineTo(i/2,0),t.lineCap=s?.cap??"butt"}getSidesCount(){return 1}}class Po{init(){}isEnabled(t){return!j()&&!t.destroyed&&t.container.actualOptions.interactivity.events.onHover.parallax.enable}move(t){const e=t.container,i=e.actualOptions.interactivity.events.onHover.parallax;if(j()||!i.enable)return;const s=i.force,o=e.interactivity.mouse.position;if(!o)return;const n=e.canvas.size,a=n.width/2,r=n.height/2,c=i.smooth,l=t.getRadius()/s,h=(o.x-a)*l,d=(o.y-r)*l,{offset:u}=t;u.x+=(h-u.x)/c,u.y+=(d-u.y)/c}}class Co extends fs{constructor(t){super(t)}clear(){}init(){}async interact(t){const e=this.container,i=t.retina.attractDistance??e.retina.attractDistance,s=t.getPosition(),o=e.particles.quadTree.queryCircle(s,i);for(const e of o){if(t===e||!e.options.move.attract.enable||e.destroyed||e.spawning)continue;const i=e.getPosition(),{dx:o,dy:n}=R(s,i),a=t.options.move.attract.rotate,r=o/(1e3*a.x),c=n/(1e3*a.y),l=e.size.value/t.size.value,h=1/l;t.velocity.x-=r*l,t.velocity.y-=c*l,e.velocity.x+=r*h,e.velocity.y+=c*h}}isEnabled(t){return t.options.move.attract.enable}reset(){}}function So(t,e,i,s,o,n){const a=k(t.options.collisions.absorb.speed*o.factor/10,0,s);t.size.value+=a/2,i.size.value-=a,s<=n&&(i.size.value=0,i.destroy())}const Oo=t=>{void 0===t.collisionMaxSpeed&&(t.collisionMaxSpeed=P(t.options.collisions.maxSpeed)),t.velocity.length>t.collisionMaxSpeed&&(t.velocity.length=t.collisionMaxSpeed)};function To(t,e){lt(ct(t),ct(e)),Oo(t),Oo(e)}function Ro(t,e,i,s){switch(t.options.collisions.mode){case"absorb":!function(t,e,i,s){const o=t.getRadius(),n=e.getRadius();void 0===o&&void 0!==n?t.destroy():void 0!==o&&void 0===n?e.destroy():void 0!==o&&void 0!==n&&(o>=n?So(t,0,e,n,i,s):So(e,0,t,o,i,s))}(t,e,i,s);break;case"bounce":To(t,e);break;case"destroy":!function(t,e){t.unbreakable||e.unbreakable||To(t,e),void 0===t.getRadius()&&void 0!==e.getRadius()?t.destroy():void 0!==t.getRadius()&&void 0===e.getRadius()?e.destroy():void 0!==t.getRadius()&&void 0!==e.getRadius()&&(t.getRadius()>=e.getRadius()?e:t).destroy()}(t,e)}}class Do extends fs{constructor(t){super(t)}clear(){}init(){}async interact(t,e){if(t.destroyed||t.spawning)return;const i=this.container,s=t.getPosition(),o=t.getRadius(),n=i.particles.quadTree.queryCircle(s,2*o);for(const a of n){if(t===a||!a.options.collisions.enable||t.options.collisions.mode!==a.options.collisions.mode||a.destroyed||a.spawning)continue;const n=a.getPosition(),r=a.getRadius();if(Math.abs(Math.round(s.z)-Math.round(n.z))>o+r)continue;D(s,n)>o+r||Ro(t,a,e,i.retina.pixelRatio)}}isEnabled(t){return t.options.collisions.enable}reset(){}}class Eo extends Ki{constructor(t,e,i,s){super(t,e,i),this.canvasSize=s,this.canvasSize={...s}}contains(t){const{width:e,height:i}=this.canvasSize,{x:s,y:o}=t;return super.contains(t)||super.contains({x:s-e,y:o})||super.contains({x:s-e,y:o-i})||super.contains({x:s,y:o-i})}intersects(t){if(super.intersects(t))return!0;const e=t,i=t,s={x:t.position.x-this.canvasSize.width,y:t.position.y-this.canvasSize.height};if(void 0!==i.radius){const t=new Ki(s.x,s.y,2*i.radius);return super.intersects(t)}if(void 0!==e.size){const t=new Qi(s.x,s.y,2*e.size.width,2*e.size.height);return super.intersects(t)}return!1}}class Io{constructor(){this.blur=5,this.color=new zt,this.color.value="#000",this.enable=!1}load(t){t&&(void 0!==t.blur&&(this.blur=t.blur),this.color=zt.create(this.color,t.color),void 0!==t.enable&&(this.enable=t.enable))}}class Lo{constructor(){this.enable=!1,this.frequency=1}load(t){t&&(void 0!==t.color&&(this.color=zt.create(this.color,t.color)),void 0!==t.enable&&(this.enable=t.enable),void 0!==t.frequency&&(this.frequency=t.frequency),void 0!==t.opacity&&(this.opacity=t.opacity))}}class Ao{constructor(){this.blink=!1,this.color=new zt,this.color.value="#fff",this.consent=!1,this.distance=100,this.enable=!1,this.frequency=1,this.opacity=1,this.shadow=new Io,this.triangles=new Lo,this.width=1,this.warp=!1}load(t){t&&(void 0!==t.id&&(this.id=t.id),void 0!==t.blink&&(this.blink=t.blink),this.color=zt.create(this.color,t.color),void 0!==t.consent&&(this.consent=t.consent),void 0!==t.distance&&(this.distance=t.distance),void 0!==t.enable&&(this.enable=t.enable),void 0!==t.frequency&&(this.frequency=t.frequency),void 0!==t.opacity&&(this.opacity=t.opacity),this.shadow.load(t.shadow),this.triangles.load(t.triangles),void 0!==t.width&&(this.width=t.width),void 0!==t.warp&&(this.warp=t.warp))}}function Fo(t,e,i,s,o){const{dx:n,dy:a,distance:r}=R(t,e);if(!o||r<=i)return r;const c={x:Math.abs(n),y:Math.abs(a)},l=Math.min(c.x,s.width-c.x),h=Math.min(c.y,s.height-c.y);return Math.sqrt(l**2+h**2)}class Bo extends fs{constructor(t){super(t),this._setColor=t=>{if(!t.options.links)return;const e=this.linkContainer,i=t.options.links;let s=void 0===i.id?e.particles.linksColor:e.particles.linksColors.get(i.id);if(s)return;s=te(i.color,i.blink,i.consent),void 0===i.id?e.particles.linksColor=s:e.particles.linksColors.set(i.id,s)},this.linkContainer=t}clear(){}init(){this.linkContainer.particles.linksColor=void 0,this.linkContainer.particles.linksColors=new Map}async interact(t){if(!t.options.links)return;t.links=[];const e=t.getPosition(),i=this.container,s=i.canvas.size;if(e.x<0||e.y<0||e.x>s.width||e.y>s.height)return;const o=t.options.links,n=o.opacity,a=t.retina.linksDistance??0,r=o.warp,c=r?new Eo(e.x,e.y,a,s):new Ki(e.x,e.y,a),l=i.particles.quadTree.query(c);for(const i of l){const c=i.options.links;if(t===i||!c?.enable||o.id!==c.id||i.spawning||i.destroyed||!i.links||t.links.some((t=>t.destination===i))||i.links.some((e=>e.destination===t)))continue;const l=i.getPosition();if(l.x<0||l.y<0||l.x>s.width||l.y>s.height)continue;const h=Fo(e,l,a,s,r&&c.warp);if(h>a)continue;const d=(1-h/a)*n;this._setColor(t),t.links.push({destination:i,opacity:d})}}isEnabled(t){return!!t.options.links?.enable}loadParticlesOptions(t,...e){t.links||(t.links=new Ao);for(const i of e)t.links.load(i?.links??i?.lineLinked??i?.line_linked)}reset(){}}function qo(t,e){const i=((s=t.map((t=>t.id))).sort(((t,e)=>t-e)),s.join("_"));var s;let o=e.get(i);return void 0===o&&(o=_(),e.set(i,o)),o}class Vo{constructor(t){this.container=t,this._drawLinkLine=(t,e)=>{const i=t.options.links;if(!i?.enable)return;const s=this.container,o=s.actualOptions,n=e.destination,a=t.getPosition(),r=n.getPosition();let c=e.opacity;s.canvas.draw((e=>{let l;const h=t.options.twinkle?.lines;if(h?.enable){const t=h.frequency,e=Vt(h.color);_(){const s=t.options.links;if(!s?.enable)return;const o=s.triangles;if(!o.enable)return;const n=this.container,a=n.actualOptions,r=e.destination,c=i.destination,l=o.opacity??(e.opacity+i.opacity)/2;l<=0||n.canvas.draw((e=>{const i=t.getPosition(),h=r.getPosition(),d=c.getPosition(),u=t.retina.linksDistance??0;if(D(i,h)>u||D(d,h)>u||D(d,i)>u)return;let p=Vt(o.color);if(!p){const e=void 0!==s.id?n.particles.linksColors.get(s.id):n.particles.linksColor;p=Kt(t,r,e)}p&&function(t){const{context:e,pos1:i,pos2:s,pos3:o,backgroundMask:n,colorTriangle:a,opacityTriangle:r}=t;mi(e,i,s,o),n.enable&&(e.globalCompositeOperation=n.composite),e.fillStyle=Jt(a,r),e.fill()}({context:e,pos1:i,pos2:h,pos3:d,backgroundMask:a.backgroundMask,colorTriangle:p,opacityTriangle:l})}))},this._drawTriangles=(t,e,i,s)=>{const o=i.destination;if(!t.links?.triangles.enable||!o.options.links?.triangles.enable)return;const n=o.links?.filter((t=>{const e=this._getLinkFrequency(o,t.destination);return o.options.links&&e<=o.options.links.frequency&&s.findIndex((e=>e.destination===t.destination))>=0}));if(n?.length)for(const s of n){const n=s.destination;this._getTriangleFrequency(e,o,n)>t.links.triangles.frequency||this._drawLinkTriangle(e,i,s)}},this._getLinkFrequency=(t,e)=>qo([t,e],this._freqs.links),this._getTriangleFrequency=(t,e,i)=>qo([t,e,i],this._freqs.triangles),this._freqs={links:new Map,triangles:new Map}}drawParticle(t,e){const{links:i,options:s}=e;if(!i||i.length<=0)return;const o=i.filter((t=>s.links&&this._getLinkFrequency(e,t.destination)<=s.links.frequency));for(const t of o)this._drawTriangles(s,e,t,o),t.opacity>0&&(e.retina.linksWidth??0)>0&&this._drawLinkLine(e,t)}async init(){this._freqs.links=new Map,this._freqs.triangles=new Map}particleCreated(t){if(t.links=[],!t.options.links)return;const e=this.container.retina.pixelRatio,{retina:i}=t,{distance:s,width:o}=t.options.links;i.linksDistance=s*e,i.linksWidth=o*e}particleDestroyed(t){t.links=[]}}class Ho{constructor(){this.id="links"}getPlugin(t){return new Vo(t)}loadOptions(){}needsPlugin(){return!0}}async function Uo(t,e=!0){await async function(t,e=!0){await t.addInteractor("particlesLinks",(t=>new Bo(t)),e)}(t,e),await async function(t,e=!0){const i=new Ho;await t.addPlugin(i,e)}(t,e)}class Wo{draw(t,e,i){const s=this.getCenter(e,i),o=this.getSidesData(e,i),n=o.count.numerator*o.count.denominator,a=o.count.numerator/o.count.denominator,r=180*(a-2)/a,c=Math.PI-Math.PI*r/180;if(t){t.beginPath(),t.translate(s.x,s.y),t.moveTo(0,0);for(let e=0;e0?"counter-clockwise":"clockwise"}switch(i){case"counter-clockwise":case"counterClockwise":t.rotate.status="decreasing";break;case"clockwise":t.rotate.status="increasing"}const s=e.animation;s.enable&&(t.rotate.decay=1-P(s.decay),t.rotate.velocity=P(s.speed)/360*this.container.retina.reduceFactor,s.sync||(t.rotate.velocity*=_())),t.rotation=t.rotate.value}isEnabled(t){const e=t.options.rotate;return!!e&&(!t.destroyed&&!t.spawning&&e.animation.enable&&!e.path)}loadOptions(t,...e){t.rotate||(t.rotate=new Xo);for(const i of e)t.rotate.load(i?.rotate)}update(t,e){this.isEnabled(t)&&(!function(t,e){const i=t.rotate,s=t.options.rotate;if(!i||!s)return;const o=s.animation,n=(i.velocity??0)*e.factor,a=2*Math.PI,r=i.decay??1;o.enable&&("increasing"===i.status?(i.value+=n,i.value>a&&(i.value-=a)):(i.value-=n,i.value<0&&(i.value+=a)),i.velocity&&1!==r&&(i.velocity*=r))}(t,e),t.rotation=t.rotate?.value??0)}}const Jo=Math.sqrt(2);class Zo{draw(t,e,i){const s=i/Jo,o=2*s;t.rect(-s,-s,o,o)}getSidesCount(){return 4}}class Qo{draw(t,e,i){const s=e.sides,o=e.starInset??2;t.moveTo(0,0-i);for(let e=0;e0&&(e.loops??0)>(e.maxLoops??0))return;if(e.time||(e.time=0),(e.delayTime??0)>0&&e.time<(e.delayTime??0)&&(e.time+=t.value),(e.delayTime??0)>0&&e.time<(e.delayTime??0))return;const n=M(i.offset),a=(e.velocity??0)*t.factor+3.6*n,r=e.decay??1;o&&"increasing"!==e.status?(e.value-=a,e.value<0&&(e.loops||(e.loops=0),e.loops++,e.status="increasing",e.value+=e.value)):(e.value+=a,e.value>s&&(e.loops||(e.loops=0),e.loops++,o&&(e.status="decreasing",e.value-=e.value%s))),e.velocity&&1!==r&&(e.velocity*=r),e.value>s&&(e.value%=s)}class tn{constructor(t){this.container=t}init(t){const e=this.container,i=t.options,s=ut(i.stroke,t.id,i.reduceDuplicates);t.strokeWidth=P(s.width)*e.retina.pixelRatio,t.strokeOpacity=P(s.opacity??1),t.strokeAnimation=s.color?.animation;const o=Wt(s.color)??t.getFillColor();o&&(t.strokeColor=ie(o,t.strokeAnimation,e.retina.reduceFactor))}isEnabled(t){const e=t.strokeAnimation,{strokeColor:i}=t;return!t.destroyed&&!t.spawning&&!!e&&(void 0!==i?.h.value&&i.h.enable||void 0!==i?.s.value&&i.s.enable||void 0!==i?.l.value&&i.l.enable)}update(t,e){this.isEnabled(t)&&function(t,e){if(!t.strokeColor||!t.strokeAnimation)return;const{h:i,s,l:o}=t.strokeColor,{h:n,s:a,l:r}=t.strokeAnimation;i&&Ko(e,i,n,360,!1),s&&Ko(e,s,a,100,!0),o&&Ko(e,o,r,100,!0)}(t,e)}}const en=["text","character","char"];class sn{draw(t,e,i,s){const o=e.shapeData;if(void 0===o)return;const n=o.value;if(void 0===n)return;void 0===e.text&&(e.text=ut(n,e.randomIndexData));const a=e.text,r=o.style??"",c=o.weight??"400",l=2*Math.round(i),h=o.font??"Verdana",d=e.fill,u=a.length*i/2;t.font=`${r} ${c} ${l}px "${h}"`;const p={x:-u,y:i/2};t.globalAlpha=s,d?t.fillText(a,p.x,p.y):t.strokeText(a,p.x,p.y),t.globalAlpha=1}getSidesCount(){return 12}async init(t){const e=t.actualOptions;if(en.find((t=>J(t,e.particles.shape.type)))){const t=en.map((t=>e.particles.shape.options[t])).find((t=>!!t)),i=[];dt(t,(t=>{i.push(Z(t.font,t.weight))})),await Promise.all(i)}}particleInit(t,e){if(!e.shape||!en.includes(e.shape))return;const i=e.shapeData;if(void 0===i)return;const s=i.value;void 0!==s&&(e.text=ut(s,e.randomIndexData))}}async function on(t,e=!0){ys(t),await async function(t,e=!0){await t.addMover("parallax",(()=>new Po),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalAttract",(e=>new Ds(t,e)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalBounce",(t=>new Is(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalBubble",(t=>new qs(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalConnect",(t=>new $s(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalGrab",(t=>new Xs(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalPause",(t=>new Ys(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalPush",(t=>new Zs(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalRemove",(t=>new Ks(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalRepulse",(e=>new so(t,e)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalSlow",(t=>new no(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("particlesAttract",(t=>new Co(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("particlesCollisions",(t=>new Do(t)),e)}(t,!1),await Uo(t,!1),await async function(){b("ease-in-quad",(t=>t**2)),b("ease-out-quad",(t=>1-(1-t)**2)),b("ease-in-out-quad",(t=>t<.5?2*t**2:1-(-2*t+2)**2/2))}(),await wo(t,!1),await async function(t,e=!0){await t.addShape("line",new Mo,e)}(t,!1),await jo(t,!1),await async function(t,e=!0){await t.addShape(["edge","square"],new Zo,e)}(t,!1),await async function(t,e=!0){await t.addShape("star",new Qo,e)}(t,!1),await async function(t,e=!0){await t.addShape(en,new sn,e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("life",(t=>new zo(t)),e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("rotate",(t=>new Yo(t)),e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("strokeColor",(t=>new tn(t)),e)}(t,!1),await Ts(t,e)}class nn{constructor(){this.enable=!1,this.speed=0,this.decay=0,this.sync=!1}load(t){t&&(void 0!==t.enable&&(this.enable=t.enable),void 0!==t.speed&&(this.speed=O(t.speed)),void 0!==t.decay&&(this.decay=O(t.decay)),void 0!==t.sync&&(this.sync=t.sync))}}class an extends St{constructor(){super(),this.animation=new nn,this.direction="clockwise",this.enable=!1,this.value=0}load(t){super.load(t),t&&(this.animation.load(t.animation),void 0!==t.direction&&(this.direction=t.direction),void 0!==t.enable&&(this.enable=t.enable))}}class rn{constructor(t){this.container=t}getTransformValues(t){const e=t.tilt?.enable&&t.tilt;return{b:e?Math.cos(e.value)*e.cosDirection:void 0,c:e?Math.sin(e.value)*e.sinDirection:void 0}}init(t){const e=t.options.tilt;if(!e)return;t.tilt={enable:e.enable,value:P(e.value)*Math.PI/180,sinDirection:_()>=.5?1:-1,cosDirection:_()>=.5?1:-1};let i=e.direction;if("random"===i){i=Math.floor(2*_())>0?"counter-clockwise":"clockwise"}switch(i){case"counter-clockwise":case"counterClockwise":t.tilt.status="decreasing";break;case"clockwise":t.tilt.status="increasing"}const s=t.options.tilt?.animation;s?.enable&&(t.tilt.decay=1-P(s.decay),t.tilt.velocity=P(s.speed)/360*this.container.retina.reduceFactor,s.sync||(t.tilt.velocity*=_()))}isEnabled(t){const e=t.options.tilt?.animation;return!t.destroyed&&!t.spawning&&!!e?.enable}loadOptions(t,...e){t.tilt||(t.tilt=new an);for(const i of e)t.tilt.load(i?.tilt)}update(t,e){this.isEnabled(t)&&function(t,e){if(!t.tilt||!t.options.tilt)return;const i=t.options.tilt.animation,s=(t.tilt.velocity??0)*e.factor,o=2*Math.PI,n=t.tilt.decay??1;i.enable&&("increasing"===t.tilt.status?(t.tilt.value+=s,t.tilt.value>o&&(t.tilt.value-=o)):(t.tilt.value-=s,t.tilt.value<0&&(t.tilt.value+=o)),t.tilt.velocity&&1!==n&&(t.tilt.velocity*=n))}(t,e)}}class cn{constructor(){this.enable=!1,this.frequency=.05,this.opacity=1}load(t){t&&(void 0!==t.color&&(this.color=zt.create(this.color,t.color)),void 0!==t.enable&&(this.enable=t.enable),void 0!==t.frequency&&(this.frequency=t.frequency),void 0!==t.opacity&&(this.opacity=O(t.opacity)))}}class ln{constructor(){this.lines=new cn,this.particles=new cn}load(t){t&&(this.lines.load(t.lines),this.particles.load(t.particles))}}class hn{getColorStyles(t,e,i,s){const o=t.options.twinkle;if(!o)return{};const n=o.particles,a=n.enable&&_()a&&(s.angle-=a),r.x+=n*Math.cos(s.angle),r.y+=n*Math.abs(Math.sin(s.angle))}(t,e)}}async function fn(t,e=!0){await async function(t,e=!0){await t.addParticleUpdater("destroy",(e=>new Je(t,e)),e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("roll",(()=>new fi),e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("tilt",(t=>new rn(t)),e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("twinkle",(()=>new hn),e)}(t,!1),await async function(t,e=!0){await t.addParticleUpdater("wobble",(t=>new pn(t)),e)}(t,!1),await async function(t,e=!0){await t.addInteractor("externalTrail",(t=>new di(t)),e)}(t,!1),await async function(t,e=!0){await t.addPlugin(new ae,e)}(t,!1),await async function(t,e=!0){t.emitterShapeManager||(t.emitterShapeManager=new ni(t)),t.addEmitterShape||(t.addEmitterShape=(e,i)=>{t.emitterShapeManager?.addShape(e,i)});const i=new ci(t);await t.addPlugin(i,e),t.addEmitterShape("circle",new Ze),t.addEmitterShape("square",new ri)}(t,!1),await on(t,e)}return fn(vs),e})()));
\ No newline at end of file
diff --git a/src/ui/static/js/utils/file.manager.js b/src/ui/static/js/utils/file.manager.js
index 4ada55857..c01fa3501 100644
--- a/src/ui/static/js/utils/file.manager.js
+++ b/src/ui/static/js/utils/file.manager.js
@@ -1,656 +1,656 @@
-class FolderNav {
- constructor(prefix) {
- this.prefix = prefix;
- this.breadContainer = document.querySelector(
- `[data-${this.prefix}-breadcrumb]`
- );
- this.container = document.querySelector(`[data-${this.prefix}-container]`);
- this.listContainer = document.querySelector(
- `[data-${this.prefix}-folders]`
- );
- this.els = document.querySelectorAll(`div[data-${this.prefix}-element]`);
- this.files = document.querySelectorAll(
- `div[data-${this.prefix}-element][data-_type='file']`
- );
- this.addFileEl = document.querySelector(`[data-${this.prefix}-add-file]`);
- this.addFolderEl = document.querySelector(
- `[data-${this.prefix}-add-folder]`
- );
- this.initSorted();
- this.initNav();
- }
-
- //sorted elements to get folders first
- initSorted() {
- this.files.forEach((file) => {
- this.listContainer.append(file);
- });
- }
-
- initNav() {
- this.container.addEventListener("click", (e) => {
- //GO ON NESTED FOLDER
- try {
- if (e.target.closest("div").getAttribute("data-_type") === "folder") {
- //avoid logic on action btn click
- const folder = e.target.closest("div[data-_type='folder']");
- this.updatedNested(folder);
- }
- } catch (err) {}
- //BREACRUMB ITEM
- try {
- if (
- e.target
- .closest("li")
- .hasAttribute(`data-${this.prefix}-breadcrumb-item`) &&
- !e.target.closest("li").hasAttribute(`data-${this.prefix}-back`) &&
- e.target.closest("li").nextSibling !== null
- ) {
- const breadItem = e.target.closest("li");
- this.updateBread(breadItem);
- }
- } catch (err) {}
- //BREADCRUMB BACK LOGIC
- try {
- if (
- e.target.closest("li").hasAttribute(`data-${this.prefix}-back`) &&
- +this.breadContainer.lastElementChild.getAttribute("data-level") !== 0
- ) {
- //back is like clicking on last prev element
- const prevItem =
- this.breadContainer.lastElementChild.previousElementSibling;
- this.updateBread(prevItem);
- }
- } catch (err) {}
- });
- }
-
- //go to nested folder element
- updatedNested(folder) {
- const [folderPath, folderLvl, folderTxt] = this.getElAtt(folder);
- //hidden all
- this.hiddenConfEls();
- //show every files in folder
- this.showCurrentFolderEls(folderPath, +folderLvl);
- //update breadcrumb
- this.appendBreadItem(folderPath, folderLvl, folderTxt);
- //update actions
- this.updateActions(folder);
- }
-
- //update clicked bread and check for allow add conf
- updateBread(item) {
- const [prevPath, prevLvl, prevTxt] = this.getElAtt(item);
- this.hiddenConfEls();
- //show every files in folder
- this.showCurrentFolderEls(prevPath, +prevLvl);
- //remove useless bread
- this.removeBreadElByLvl(+prevLvl);
- const folder = document.querySelector(
- `div[data-${this.prefix}-element][data-path='${item.getAttribute(
- "data-path"
- )}']`
- );
- this.updateActions(folder);
- }
-
- //check if file/folder can be created on folder
- updateActions(folder) {
- //by default
- this.hideAddConf();
- //check if folder allow add file/folder
- const isAddFile = folder.getAttribute("data-can-create-file");
- const isAddFolder = folder.getAttribute("data-can-create-folder");
- isAddFile === "True" ? this.addFileEl.classList.remove("hidden") : "";
- isAddFolder === "True" ? this.addFolderEl.classList.remove("hidden") : "";
- }
-
- hideAddConf() {
- this.addFileEl.classList.add("hidden");
- this.addFolderEl.classList.add("hidden");
- }
-
- showCurrentFolderEls(path, lvl) {
- const nestedEl = document.querySelectorAll(
- `div[data-path^="${path}/"][data-level="${+lvl + 1}"]`
- );
- for (let i = 0; i < nestedEl.length; i++) {
- const el = nestedEl[i];
- el.setAttribute("data-current-el", "");
- el.classList.remove("hidden");
- }
- }
-
- //remove the bread items with a higher level than
- //the clicked bread item
- removeBreadElByLvl(lvl) {
- const breadcrumbItem = this.breadContainer.querySelectorAll(
- `[data-${this.prefix}-breadcrumb-item]`
- );
- breadcrumbItem.forEach((item) => {
- if (
- item.hasAttribute("data-level") &&
- +item.getAttribute("data-level") > lvl
- )
- item.remove();
- });
- }
-
- //retrieve path, level and text
- getElAtt(el) {
- const newPath = el.getAttribute("data-path");
- const newLvl = el.getAttribute("data-level");
- const newTxt = el.getAttribute("data-name");
- return [newPath, newLvl, newTxt];
- }
-
- //hidden all folders
- hiddenConfEls() {
- this.els = document.querySelectorAll(`div[data-${this.prefix}-element]`);
- this.els.forEach((el) => {
- el.classList.add("hidden");
- el.removeAttribute("data-current-el");
- });
- }
-
- //add a bread item as last child with needed info
- appendBreadItem(path, level, name) {
- //create item el
- const itemEl = document.createElement("li");
- itemEl.className = "leading-normal text-sm";
- //set item atts
- const itemAtt = [
- ["data-path", path],
- [`data-${this.prefix}-breadcrumb-item`, ""],
- ["data-level", level],
- ["data-name", name],
- ];
- for (let i = 0; i < itemAtt.length; i++) {
- itemEl.setAttribute(`${itemAtt[i][0]}`, `${itemAtt[i][1]}`);
- }
- //create nested btn el
- const nestedBtnEl = document.createElement("button");
- nestedBtnEl.className =
- "ml-2 dark:text-gray-500 text-gray-600 after:float-right after:pl-2 after:text-gray-600 dark:after:text-gray-500 after:content-['/']";
- itemEl.appendChild(nestedBtnEl);
- nestedBtnEl.setAttribute("type", "button");
- nestedBtnEl.textContent = name;
- this.breadContainer.appendChild(itemEl);
- }
-}
-
-class FolderDropdown {
- constructor(prefix) {
- this.prefix = prefix;
- this.container = document.querySelector(`[data-${this.prefix}-container]`);
- this.dropEls = document.querySelectorAll(
- `[data-${this.prefix}-action-dropdown]`
- );
- this.init();
- }
-
- init() {
- let prevActionBtn = ""; //compare with curr to hide or not prev
- this.container.addEventListener("click", (e) => {
- //remove when none click
- try {
- if (
- !e.target
- .closest("div")
- .hasAttribute(`data-${this.prefix}-action-button`)
- ) {
- this.hideDropEls();
- }
- } catch (err) {}
- //show dropdown actions for folders
- try {
- if (
- e.target
- .closest("div")
- .hasAttribute(`data-${this.prefix}-action-button`)
- ) {
- const dropEl = e.target
- .closest(`div[data-${this.prefix}-element]`)
- .querySelector(`[data-${this.prefix}-action-dropdown]`);
- //avoid multiple dropdown
- if (prevActionBtn === "") prevActionBtn = dropEl;
- if (prevActionBtn !== dropEl) this.hideDropEls();
- this.toggleDrop(dropEl);
- prevActionBtn = dropEl;
- }
- } catch (err) {}
- //hide dropdown clicking an action
- try {
- if (
- e.target
- .closest("button")
- .hasAttribute(`data-${this.prefix}-action-dropdown-btn`)
- ) {
- const att = e.target
- .closest("button")
- .getAttribute(`data-${this.prefix}-action-dropdown-btn`);
- const dropEl = document.querySelector(
- `[data-${this.prefix}-action-dropdown="${att}"]`
- );
- this.hideDrop(dropEl);
- }
- } catch (err) {}
- });
- }
-
- //UTILS
-
- toggleDrop(dropEl) {
- dropEl.classList.toggle("hidden");
- dropEl.classList.toggle("flex");
- }
-
- hideDrop(dropEl) {
- dropEl.classList.add("hidden");
- dropEl.classList.remove("flex");
- }
-
- hideDropEls() {
- this.dropEls.forEach((drop) => {
- this.hideDrop(drop);
- });
- }
-}
-
-class FolderEditor {
- constructor() {
- this.editor = ace.edit("editor");
- this.darkMode = document.querySelector("[data-dark-toggle]");
- this.initEditor();
- this.listenDarkToggle();
- }
-
- initEditor() {
- //editor options
- this.editor.setShowPrintMargin(false);
- this.setDarkMode();
- }
-
- //listen to dark toggle button to change theme
- listenDarkToggle() {
- this.darkMode.addEventListener("click", (e) => {
- this.darkMode.checked
- ? this.changeDarkMode(true)
- : this.changeDarkMode(false);
- });
- }
-
- setDarkMode() {
- document.querySelector("html").className.includes("dark")
- ? this.editor.setTheme("ace/theme/dracula")
- : this.editor.setTheme("ace/theme/dawn");
- }
-
- //change theme according to mode
- changeDarkMode(bool) {
- bool
- ? this.editor.setTheme("ace/theme/dracula")
- : this.editor.setTheme("ace/theme/dawn");
- }
-
- readOnlyBool(bool) {
- this.editor.setReadOnly(bool);
- }
-}
-
-class FolderModal {
- constructor(prefix) {
- this.prefix = prefix;
- //container
- this.container = document.querySelector(`[data-${this.prefix}-container]`);
- //add service/file elements
- this.breadContainer = document.querySelector(
- `[data-${this.prefix}-breadcrumb]`
- );
- this.addConfContainer = document.querySelector(
- `[data-${this.prefix}-add-container]`
- );
- //modal DOM elements
- this.form = document.querySelector(`[data-${this.prefix}-modal-form]`);
- this.modalEl = document.querySelector(`[data-${this.prefix}-modal]`);
- this.modalTitle = this.modalEl.querySelector(
- `[data-${this.prefix}-modal-title]`
- );
- this.modalPath = this.modalEl.querySelector(
- `[data-${this.prefix}-modal-path]`
- );
- this.modalEditor = this.modalEl.querySelector(
- `[data-${this.prefix}-modal-editor]`
- );
- this.modalPathPrev = this.modalPath.querySelector(
- `p[data-${this.prefix}-modal-path-prefix]`
- );
- this.modalPathName = this.modalPath.querySelector("input");
- this.modalPathSuffix = this.modalPath.querySelector(
- `p[data-${this.prefix}-modal-path-suffix]`
- );
-
- this.modalSubmit = this.modalEl.querySelector(
- `[data-${this.prefix}-modal-submit]`
- );
- //hidden input for backend
- this.modalInpPath = this.modalEl.querySelector("#path");
- this.modalInpOperation = this.modalEl.querySelector("#operation");
- this.modalInpType = this.modalEl.querySelector("#_type");
- this.modalInpOldName = this.modalEl.querySelector("#old_name");
- this.modalTxtarea = this.modalEl.querySelector("#content");
- //HANDLERS
- //modal and values logic after clicking add file/folder button
- this.initAddConfig();
- //modal and values logic after clicking actions buttons
- this.initActionToModal();
- //modal element logic
- this.initModal();
- //modal submit check and filter before submit
- this.initForm();
- }
-
- //HANDLERS
- initAddConfig() {
- this.addConfContainer.addEventListener("click", (e) => {
- //add folder
- try {
- if (
- e.target.closest("li").hasAttribute(`data-${this.prefix}-add-folder`)
- ) {
- this.setModal(
- "new",
- this.getPathFromBread(),
- "folder",
- "",
- "",
- this.getLevelFromBread()
- );
- }
- } catch (err) {}
- //add file
- try {
- if (
- e.target.closest("li").hasAttribute(`data-${this.prefix}-add-file`)
- ) {
- this.setModal(
- "new",
- this.getPathFromBread(),
- "file",
- "",
- "",
- this.getLevelFromBread()
- );
- }
- } catch (err) {}
- });
- }
-
- initActionToModal() {
- this.container.addEventListener("click", (e) => {
- //click on file logic
- try {
- if (e.target.closest("div").getAttribute("data-_type") == "file") {
- const btnEl = e.target.closest("div").querySelector("button[value]");
- const [action, path, type, content, name, level] =
- this.getInfoFromActionBtn(btnEl);
- this.setModal(action, path, type, content, name, level);
- this.showModal();
- }
- } catch (err) {}
- //set data of folder and show modal unless it's download btn
- try {
- if (
- e.target
- .closest("button")
- .hasAttribute(`data-${this.prefix}-action-dropdown-btn`) &&
- e.target.closest("button").getAttribute("value") !== "download"
- ) {
- const btnEl = e.target.closest("button");
- const [action, path, type, content, name, level] =
- this.getInfoFromActionBtn(btnEl);
- this.setModal(action, path, type, content, name, level);
- this.showModal();
- }
- } catch (err) {}
- //download btn logic
- try {
- if (
- e.target
- .closest("button")
- .hasAttribute(`data-${this.prefix}-action-dropdown-btn`) &&
- e.target.closest("button").getAttribute("value") === "download"
- ) {
- const btnEl = e.target.closest("button");
- const [action, path, type, content, name, level] =
- this.getInfoFromActionBtn(btnEl);
- this.download(name, content);
- }
- } catch (err) {}
- });
- }
-
- initModal() {
- this.modalEl.addEventListener("click", (e) => {
- //close modal logic
- try {
- if (
- e.target
- .closest("button")
- .hasAttribute(`data-${this.prefix}-modal-close`)
- ) {
- this.closeModal();
- }
- } catch (err) {}
- });
- }
-
- download(filename, text) {
- var element = document.createElement("a");
- element.setAttribute(
- "href",
- "data:text/plain;charset=utf-8," + encodeURIComponent(text)
- );
- element.setAttribute("download", filename);
-
- element.style.display = "none";
- document.body.appendChild(element);
-
- element.click();
-
- document.body.removeChild(element);
- }
-
- initForm() {
- this.form.addEventListener("submit", (e) => {
- e.preventDefault();
- //submit nothing case
- if (this.modalInpOperation.value === "view") {
- return this.closeModal();
- }
- //else set data to input and request
- this.setDataForRequest();
- });
- }
-
- //get data of custom inputs and set it on submit input
- setDataForRequest() {
- //set path to input
- if (this.modalInpType === "folder") {
- const prevPath = this.modalPathPrev.textContent;
- const name = this.modalPathName.value;
- this.modalInpPath.value = `${prevPath}${name}`;
- }
- //set textarea value from editor
- const newTextarea = ace.edit("editor").getValue();
- this.modalTxtarea.value = newTextarea;
- this.form.submit();
- }
-
- //for add file/folder btn
- //get path of last bread element
- getPathFromBread() {
- const path = this.breadContainer.lastElementChild.getAttribute("data-path");
- return `${path}/`;
- }
-
- getLevelFromBread() {
- const level =
- this.breadContainer.lastElementChild.getAttribute("data-level");
- return level;
- }
- //set all needed data from btn action and folder info
- setModal(action, path, type, content, name, level) {
- //title
- this.modalTitle.textContent = `${action} ${type}`;
- this.setInpt(action, path, type, name);
- this.setEditor(type, content);
- this.setSubmitTxt(action);
- this.setPath(action, path, type, name, level);
- this.setDisabled(action);
- this.showModal();
- }
-
- //for hidden input to send on backend
- //on form submit, check for update values before send request
- setInpt(action, path, type, name) {
- this.modalInpPath.value =
- type === "file" && this.prefix === "configs"
- ? path.replace(".conf", "")
- : path;
- this.modalInpType.value = type;
- this.modalInpOperation.value = action;
- this.modalInpOldName.value =
- type === "file" && this.prefix === "configs"
- ? name.replace(".conf", "")
- : name;
- }
-
- //path is empty if new one, else show current name
- setPath(action, path, type) {
- let [prevPath, name] = this.separatePath(path);
- //remove conf if file type
- this.modalPathSuffix.textContent =
- type === "file" && this.prefix === "configs" ? ".conf" : "";
- name =
- type === "file" && this.prefix === "configs"
- ? name.replace(".conf", "")
- : name;
-
- if (action === "new") {
- this.modalPathPrev.textContent = `${path}`;
- this.modalPathName.value = ``;
- }
-
- if (action !== "new") {
- this.modalPathPrev.textContent = `${prevPath}`;
- this.modalPathName.value = `${name}`;
- }
- }
-
- //separate name and previous of path for DOM elements
- separatePath(path) {
- const splitPath = path.split("/");
- const nme = splitPath[splitPath.length - 1];
- const prev = path.replace(nme, "");
- return [prev, nme];
- }
-
- //disabled for view and delete actions
- setDisabled(action) {
- action === "view" || action === "delete" || action === "download"
- ? this.disabledDOMInpt(true)
- : this.disabledDOMInpt(false);
- }
-
- //submit text depending action
- setSubmitTxt(action) {
- this.delSubmitBtnType();
- if (action === "new") {
- this.modalSubmit.textContent = "add";
- this.setSubmitBtnType("valid-btn");
- return;
- }
- if (action === "view") {
- this.modalSubmit.textContent = "ok";
- this.setSubmitBtnType("valid-btn");
- return;
- }
- if (action === "edit") {
- this.setSubmitBtnType("edit-btn");
- this.modalSubmit.textContent = "edit";
- return;
- }
-
- if (action === "delete") {
- this.setSubmitBtnType("delete-btn");
- this.modalSubmit.textContent = "delete";
- return;
- }
- if (action === "download") {
- this.setSubmitBtnType("info-btn");
- this.modalSubmit.textContent = "download";
- return;
- }
- }
-
- setSubmitBtnType(btnType) {
- this.modalSubmit.classList.add(btnType);
- }
-
- delSubmitBtnType() {
- this.modalSubmit.classList.remove(
- "delete-btn",
- "valid-btn",
- "edit-btn",
- "info-btn"
- );
- }
-
- //show only if type file and display text
- setEditor(type, content) {
- //SHOW LOGIC
- if (type === "folder") this.modalEditor.classList.add("hidden");
-
- if (type === "file") this.modalEditor.classList.remove("hidden");
-
- ace.edit("editor").setValue(content);
- }
-
- //get all needed info when clicking on action btn
- getInfoFromActionBtn(btnEl) {
- const action = btnEl.getAttribute("value");
- const name = btnEl.getAttribute(`data-${this.prefix}-action-dropdown-btn`);
- const folder = btnEl.closest(`[data-${this.prefix}-element]`);
- const level = folder.getAttribute("data-level");
- const path = folder.getAttribute("data-path");
- const type = folder.getAttribute("data-_type");
- let content;
- try {
- content = folder
- .querySelector(`[data-${this.prefix}-content]`)
- .getAttribute("data-value");
- } catch (err) {
- content = "";
- }
- return [action, path, type, content, name, level];
- }
-
- //UTILS
- disabledDOMInpt(bool) {
- this.modalPathName.disabled = bool;
- ace.edit("editor").setReadOnly(bool);
- }
-
- closeModal() {
- this.modalEl.classList.add("hidden");
- this.modalEl.classList.remove("flex");
- }
-
- showModal() {
- this.modalEl.classList.add("flex");
- this.modalEl.classList.remove("hidden");
- }
-}
-
-export { FolderNav, FolderModal, FolderEditor, FolderDropdown };
+class FolderNav {
+ constructor(prefix) {
+ this.prefix = prefix;
+ this.breadContainer = document.querySelector(
+ `[data-${this.prefix}-breadcrumb]`,
+ );
+ this.container = document.querySelector(`[data-${this.prefix}-container]`);
+ this.listContainer = document.querySelector(
+ `[data-${this.prefix}-folders]`,
+ );
+ this.els = document.querySelectorAll(`div[data-${this.prefix}-element]`);
+ this.files = document.querySelectorAll(
+ `div[data-${this.prefix}-element][data-_type='file']`,
+ );
+ this.addFileEl = document.querySelector(`[data-${this.prefix}-add-file]`);
+ this.addFolderEl = document.querySelector(
+ `[data-${this.prefix}-add-folder]`,
+ );
+ this.initSorted();
+ this.initNav();
+ }
+
+ //sorted elements to get folders first
+ initSorted() {
+ this.files.forEach((file) => {
+ this.listContainer.append(file);
+ });
+ }
+
+ initNav() {
+ this.container.addEventListener("click", (e) => {
+ //GO ON NESTED FOLDER
+ try {
+ if (e.target.closest("div").getAttribute("data-_type") === "folder") {
+ //avoid logic on action btn click
+ const folder = e.target.closest("div[data-_type='folder']");
+ this.updatedNested(folder);
+ }
+ } catch (err) {}
+ //BREACRUMB ITEM
+ try {
+ if (
+ e.target
+ .closest("li")
+ .hasAttribute(`data-${this.prefix}-breadcrumb-item`) &&
+ !e.target.closest("li").hasAttribute(`data-${this.prefix}-back`) &&
+ e.target.closest("li").nextSibling !== null
+ ) {
+ const breadItem = e.target.closest("li");
+ this.updateBread(breadItem);
+ }
+ } catch (err) {}
+ //BREADCRUMB BACK LOGIC
+ try {
+ if (
+ e.target.closest("li").hasAttribute(`data-${this.prefix}-back`) &&
+ +this.breadContainer.lastElementChild.getAttribute("data-level") !== 0
+ ) {
+ //back is like clicking on last prev element
+ const prevItem =
+ this.breadContainer.lastElementChild.previousElementSibling;
+ this.updateBread(prevItem);
+ }
+ } catch (err) {}
+ });
+ }
+
+ //go to nested folder element
+ updatedNested(folder) {
+ const [folderPath, folderLvl, folderTxt] = this.getElAtt(folder);
+ //hidden all
+ this.hiddenConfEls();
+ //show every files in folder
+ this.showCurrentFolderEls(folderPath, +folderLvl);
+ //update breadcrumb
+ this.appendBreadItem(folderPath, folderLvl, folderTxt);
+ //update actions
+ this.updateActions(folder);
+ }
+
+ //update clicked bread and check for allow add conf
+ updateBread(item) {
+ const [prevPath, prevLvl, prevTxt] = this.getElAtt(item);
+ this.hiddenConfEls();
+ //show every files in folder
+ this.showCurrentFolderEls(prevPath, +prevLvl);
+ //remove useless bread
+ this.removeBreadElByLvl(+prevLvl);
+ const folder = document.querySelector(
+ `div[data-${this.prefix}-element][data-path='${item.getAttribute(
+ "data-path",
+ )}']`,
+ );
+ this.updateActions(folder);
+ }
+
+ //check if file/folder can be created on folder
+ updateActions(folder) {
+ //by default
+ this.hideAddConf();
+ //check if folder allow add file/folder
+ const isAddFile = folder.getAttribute("data-can-create-file");
+ const isAddFolder = folder.getAttribute("data-can-create-folder");
+ isAddFile === "True" ? this.addFileEl.classList.remove("hidden") : "";
+ isAddFolder === "True" ? this.addFolderEl.classList.remove("hidden") : "";
+ }
+
+ hideAddConf() {
+ this.addFileEl.classList.add("hidden");
+ this.addFolderEl.classList.add("hidden");
+ }
+
+ showCurrentFolderEls(path, lvl) {
+ const nestedEl = document.querySelectorAll(
+ `div[data-path^="${path}/"][data-level="${+lvl + 1}"]`,
+ );
+ for (let i = 0; i < nestedEl.length; i++) {
+ const el = nestedEl[i];
+ el.setAttribute("data-current-el", "");
+ el.classList.remove("hidden");
+ }
+ }
+
+ //remove the bread items with a higher level than
+ //the clicked bread item
+ removeBreadElByLvl(lvl) {
+ const breadcrumbItem = this.breadContainer.querySelectorAll(
+ `[data-${this.prefix}-breadcrumb-item]`,
+ );
+ breadcrumbItem.forEach((item) => {
+ if (
+ item.hasAttribute("data-level") &&
+ +item.getAttribute("data-level") > lvl
+ )
+ item.remove();
+ });
+ }
+
+ //retrieve path, level and text
+ getElAtt(el) {
+ const newPath = el.getAttribute("data-path");
+ const newLvl = el.getAttribute("data-level");
+ const newTxt = el.getAttribute("data-name");
+ return [newPath, newLvl, newTxt];
+ }
+
+ //hidden all folders
+ hiddenConfEls() {
+ this.els = document.querySelectorAll(`div[data-${this.prefix}-element]`);
+ this.els.forEach((el) => {
+ el.classList.add("hidden");
+ el.removeAttribute("data-current-el");
+ });
+ }
+
+ //add a bread item as last child with needed info
+ appendBreadItem(path, level, name) {
+ //create item el
+ const itemEl = document.createElement("li");
+ itemEl.className = "leading-normal text-sm";
+ //set item atts
+ const itemAtt = [
+ ["data-path", path],
+ [`data-${this.prefix}-breadcrumb-item`, ""],
+ ["data-level", level],
+ ["data-name", name],
+ ];
+ for (let i = 0; i < itemAtt.length; i++) {
+ itemEl.setAttribute(`${itemAtt[i][0]}`, `${itemAtt[i][1]}`);
+ }
+ //create nested btn el
+ const nestedBtnEl = document.createElement("button");
+ nestedBtnEl.className =
+ "ml-2 dark:text-gray-500 text-gray-600 after:float-right after:pl-2 after:text-gray-600 dark:after:text-gray-500 after:content-['/']";
+ itemEl.appendChild(nestedBtnEl);
+ nestedBtnEl.setAttribute("type", "button");
+ nestedBtnEl.textContent = name;
+ this.breadContainer.appendChild(itemEl);
+ }
+}
+
+class FolderDropdown {
+ constructor(prefix) {
+ this.prefix = prefix;
+ this.container = document.querySelector(`[data-${this.prefix}-container]`);
+ this.dropEls = document.querySelectorAll(
+ `[data-${this.prefix}-action-dropdown]`,
+ );
+ this.init();
+ }
+
+ init() {
+ let prevActionBtn = ""; //compare with curr to hide or not prev
+ this.container.addEventListener("click", (e) => {
+ //remove when none click
+ try {
+ if (
+ !e.target
+ .closest("div")
+ .hasAttribute(`data-${this.prefix}-action-button`)
+ ) {
+ this.hideDropEls();
+ }
+ } catch (err) {}
+ //show dropdown actions for folders
+ try {
+ if (
+ e.target
+ .closest("div")
+ .hasAttribute(`data-${this.prefix}-action-button`)
+ ) {
+ const dropEl = e.target
+ .closest(`div[data-${this.prefix}-element]`)
+ .querySelector(`[data-${this.prefix}-action-dropdown]`);
+ //avoid multiple dropdown
+ if (prevActionBtn === "") prevActionBtn = dropEl;
+ if (prevActionBtn !== dropEl) this.hideDropEls();
+ this.toggleDrop(dropEl);
+ prevActionBtn = dropEl;
+ }
+ } catch (err) {}
+ //hide dropdown clicking an action
+ try {
+ if (
+ e.target
+ .closest("button")
+ .hasAttribute(`data-${this.prefix}-action-dropdown-btn`)
+ ) {
+ const att = e.target
+ .closest("button")
+ .getAttribute(`data-${this.prefix}-action-dropdown-btn`);
+ const dropEl = document.querySelector(
+ `[data-${this.prefix}-action-dropdown="${att}"]`,
+ );
+ this.hideDrop(dropEl);
+ }
+ } catch (err) {}
+ });
+ }
+
+ //UTILS
+
+ toggleDrop(dropEl) {
+ dropEl.classList.toggle("hidden");
+ dropEl.classList.toggle("flex");
+ }
+
+ hideDrop(dropEl) {
+ dropEl.classList.add("hidden");
+ dropEl.classList.remove("flex");
+ }
+
+ hideDropEls() {
+ this.dropEls.forEach((drop) => {
+ this.hideDrop(drop);
+ });
+ }
+}
+
+class FolderEditor {
+ constructor() {
+ this.editor = ace.edit("editor");
+ this.darkMode = document.querySelector("[data-dark-toggle]");
+ this.initEditor();
+ this.listenDarkToggle();
+ }
+
+ initEditor() {
+ //editor options
+ this.editor.setShowPrintMargin(false);
+ this.setDarkMode();
+ }
+
+ //listen to dark toggle button to change theme
+ listenDarkToggle() {
+ this.darkMode.addEventListener("click", (e) => {
+ this.darkMode.checked
+ ? this.changeDarkMode(true)
+ : this.changeDarkMode(false);
+ });
+ }
+
+ setDarkMode() {
+ document.querySelector("html").className.includes("dark")
+ ? this.editor.setTheme("ace/theme/dracula")
+ : this.editor.setTheme("ace/theme/dawn");
+ }
+
+ //change theme according to mode
+ changeDarkMode(bool) {
+ bool
+ ? this.editor.setTheme("ace/theme/dracula")
+ : this.editor.setTheme("ace/theme/dawn");
+ }
+
+ readOnlyBool(bool) {
+ this.editor.setReadOnly(bool);
+ }
+}
+
+class FolderModal {
+ constructor(prefix) {
+ this.prefix = prefix;
+ //container
+ this.container = document.querySelector(`[data-${this.prefix}-container]`);
+ //add service/file elements
+ this.breadContainer = document.querySelector(
+ `[data-${this.prefix}-breadcrumb]`,
+ );
+ this.addConfContainer = document.querySelector(
+ `[data-${this.prefix}-add-container]`,
+ );
+ //modal DOM elements
+ this.form = document.querySelector(`[data-${this.prefix}-modal-form]`);
+ this.modalEl = document.querySelector(`[data-${this.prefix}-modal]`);
+ this.modalTitle = this.modalEl.querySelector(
+ `[data-${this.prefix}-modal-title]`,
+ );
+ this.modalPath = this.modalEl.querySelector(
+ `[data-${this.prefix}-modal-path]`,
+ );
+ this.modalEditor = this.modalEl.querySelector(
+ `[data-${this.prefix}-modal-editor]`,
+ );
+ this.modalPathPrev = this.modalPath.querySelector(
+ `p[data-${this.prefix}-modal-path-prefix]`,
+ );
+ this.modalPathName = this.modalPath.querySelector("input");
+ this.modalPathSuffix = this.modalPath.querySelector(
+ `p[data-${this.prefix}-modal-path-suffix]`,
+ );
+
+ this.modalSubmit = this.modalEl.querySelector(
+ `[data-${this.prefix}-modal-submit]`,
+ );
+ //hidden input for backend
+ this.modalInpPath = this.modalEl.querySelector("#path");
+ this.modalInpOperation = this.modalEl.querySelector("#operation");
+ this.modalInpType = this.modalEl.querySelector("#_type");
+ this.modalInpOldName = this.modalEl.querySelector("#old_name");
+ this.modalTxtarea = this.modalEl.querySelector("#content");
+ //HANDLERS
+ //modal and values logic after clicking add file/folder button
+ this.initAddConfig();
+ //modal and values logic after clicking actions buttons
+ this.initActionToModal();
+ //modal element logic
+ this.initModal();
+ //modal submit check and filter before submit
+ this.initForm();
+ }
+
+ //HANDLERS
+ initAddConfig() {
+ this.addConfContainer.addEventListener("click", (e) => {
+ //add folder
+ try {
+ if (
+ e.target.closest("li").hasAttribute(`data-${this.prefix}-add-folder`)
+ ) {
+ this.setModal(
+ "new",
+ this.getPathFromBread(),
+ "folder",
+ "",
+ "",
+ this.getLevelFromBread(),
+ );
+ }
+ } catch (err) {}
+ //add file
+ try {
+ if (
+ e.target.closest("li").hasAttribute(`data-${this.prefix}-add-file`)
+ ) {
+ this.setModal(
+ "new",
+ this.getPathFromBread(),
+ "file",
+ "",
+ "",
+ this.getLevelFromBread(),
+ );
+ }
+ } catch (err) {}
+ });
+ }
+
+ initActionToModal() {
+ this.container.addEventListener("click", (e) => {
+ //click on file logic
+ try {
+ if (e.target.closest("div").getAttribute("data-_type") == "file") {
+ const btnEl = e.target.closest("div").querySelector("button[value]");
+ const [action, path, type, content, name, level] =
+ this.getInfoFromActionBtn(btnEl);
+ this.setModal(action, path, type, content, name, level);
+ this.showModal();
+ }
+ } catch (err) {}
+ //set data of folder and show modal unless it's download btn
+ try {
+ if (
+ e.target
+ .closest("button")
+ .hasAttribute(`data-${this.prefix}-action-dropdown-btn`) &&
+ e.target.closest("button").getAttribute("value") !== "download"
+ ) {
+ const btnEl = e.target.closest("button");
+ const [action, path, type, content, name, level] =
+ this.getInfoFromActionBtn(btnEl);
+ this.setModal(action, path, type, content, name, level);
+ this.showModal();
+ }
+ } catch (err) {}
+ //download btn logic
+ try {
+ if (
+ e.target
+ .closest("button")
+ .hasAttribute(`data-${this.prefix}-action-dropdown-btn`) &&
+ e.target.closest("button").getAttribute("value") === "download"
+ ) {
+ const btnEl = e.target.closest("button");
+ const [action, path, type, content, name, level] =
+ this.getInfoFromActionBtn(btnEl);
+ this.download(name, content);
+ }
+ } catch (err) {}
+ });
+ }
+
+ initModal() {
+ this.modalEl.addEventListener("click", (e) => {
+ //close modal logic
+ try {
+ if (
+ e.target
+ .closest("button")
+ .hasAttribute(`data-${this.prefix}-modal-close`)
+ ) {
+ this.closeModal();
+ }
+ } catch (err) {}
+ });
+ }
+
+ download(filename, text) {
+ var element = document.createElement("a");
+ element.setAttribute(
+ "href",
+ "data:text/plain;charset=utf-8," + encodeURIComponent(text),
+ );
+ element.setAttribute("download", filename);
+
+ element.style.display = "none";
+ document.body.appendChild(element);
+
+ element.click();
+
+ document.body.removeChild(element);
+ }
+
+ initForm() {
+ this.form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ //submit nothing case
+ if (this.modalInpOperation.value === "view") {
+ return this.closeModal();
+ }
+ //else set data to input and request
+ this.setDataForRequest();
+ });
+ }
+
+ //get data of custom inputs and set it on submit input
+ setDataForRequest() {
+ //set path to input
+ if (this.modalInpType === "folder") {
+ const prevPath = this.modalPathPrev.textContent;
+ const name = this.modalPathName.value;
+ this.modalInpPath.value = `${prevPath}${name}`;
+ }
+ //set textarea value from editor
+ const newTextarea = ace.edit("editor").getValue();
+ this.modalTxtarea.value = newTextarea;
+ this.form.submit();
+ }
+
+ //for add file/folder btn
+ //get path of last bread element
+ getPathFromBread() {
+ const path = this.breadContainer.lastElementChild.getAttribute("data-path");
+ return `${path}/`;
+ }
+
+ getLevelFromBread() {
+ const level =
+ this.breadContainer.lastElementChild.getAttribute("data-level");
+ return level;
+ }
+ //set all needed data from btn action and folder info
+ setModal(action, path, type, content, name, level) {
+ //title
+ this.modalTitle.textContent = `${action} ${type}`;
+ this.setInpt(action, path, type, name);
+ this.setEditor(type, content);
+ this.setSubmitTxt(action);
+ this.setPath(action, path, type, name, level);
+ this.setDisabled(action);
+ this.showModal();
+ }
+
+ //for hidden input to send on backend
+ //on form submit, check for update values before send request
+ setInpt(action, path, type, name) {
+ this.modalInpPath.value =
+ type === "file" && this.prefix === "configs"
+ ? path.replace(".conf", "")
+ : path;
+ this.modalInpType.value = type;
+ this.modalInpOperation.value = action;
+ this.modalInpOldName.value =
+ type === "file" && this.prefix === "configs"
+ ? name.replace(".conf", "")
+ : name;
+ }
+
+ //path is empty if new one, else show current name
+ setPath(action, path, type) {
+ let [prevPath, name] = this.separatePath(path);
+ //remove conf if file type
+ this.modalPathSuffix.textContent =
+ type === "file" && this.prefix === "configs" ? ".conf" : "";
+ name =
+ type === "file" && this.prefix === "configs"
+ ? name.replace(".conf", "")
+ : name;
+
+ if (action === "new") {
+ this.modalPathPrev.textContent = `${path}`;
+ this.modalPathName.value = ``;
+ }
+
+ if (action !== "new") {
+ this.modalPathPrev.textContent = `${prevPath}`;
+ this.modalPathName.value = `${name}`;
+ }
+ }
+
+ //separate name and previous of path for DOM elements
+ separatePath(path) {
+ const splitPath = path.split("/");
+ const nme = splitPath[splitPath.length - 1];
+ const prev = path.replace(nme, "");
+ return [prev, nme];
+ }
+
+ //disabled for view and delete actions
+ setDisabled(action) {
+ action === "view" || action === "delete" || action === "download"
+ ? this.disabledDOMInpt(true)
+ : this.disabledDOMInpt(false);
+ }
+
+ //submit text depending action
+ setSubmitTxt(action) {
+ this.delSubmitBtnType();
+ if (action === "new") {
+ this.modalSubmit.textContent = "add";
+ this.setSubmitBtnType("valid-btn");
+ return;
+ }
+ if (action === "view") {
+ this.modalSubmit.textContent = "ok";
+ this.setSubmitBtnType("valid-btn");
+ return;
+ }
+ if (action === "edit") {
+ this.setSubmitBtnType("edit-btn");
+ this.modalSubmit.textContent = "edit";
+ return;
+ }
+
+ if (action === "delete") {
+ this.setSubmitBtnType("delete-btn");
+ this.modalSubmit.textContent = "delete";
+ return;
+ }
+ if (action === "download") {
+ this.setSubmitBtnType("info-btn");
+ this.modalSubmit.textContent = "download";
+ return;
+ }
+ }
+
+ setSubmitBtnType(btnType) {
+ this.modalSubmit.classList.add(btnType);
+ }
+
+ delSubmitBtnType() {
+ this.modalSubmit.classList.remove(
+ "delete-btn",
+ "valid-btn",
+ "edit-btn",
+ "info-btn",
+ );
+ }
+
+ //show only if type file and display text
+ setEditor(type, content) {
+ //SHOW LOGIC
+ if (type === "folder") this.modalEditor.classList.add("hidden");
+
+ if (type === "file") this.modalEditor.classList.remove("hidden");
+
+ ace.edit("editor").setValue(content);
+ }
+
+ //get all needed info when clicking on action btn
+ getInfoFromActionBtn(btnEl) {
+ const action = btnEl.getAttribute("value");
+ const name = btnEl.getAttribute(`data-${this.prefix}-action-dropdown-btn`);
+ const folder = btnEl.closest(`[data-${this.prefix}-element]`);
+ const level = folder.getAttribute("data-level");
+ const path = folder.getAttribute("data-path");
+ const type = folder.getAttribute("data-_type");
+ let content;
+ try {
+ content = folder
+ .querySelector(`[data-${this.prefix}-content]`)
+ .getAttribute("data-value");
+ } catch (err) {
+ content = "";
+ }
+ return [action, path, type, content, name, level];
+ }
+
+ //UTILS
+ disabledDOMInpt(bool) {
+ this.modalPathName.disabled = bool;
+ ace.edit("editor").setReadOnly(bool);
+ }
+
+ closeModal() {
+ this.modalEl.classList.add("hidden");
+ this.modalEl.classList.remove("flex");
+ }
+
+ showModal() {
+ this.modalEl.classList.add("flex");
+ this.modalEl.classList.remove("hidden");
+ }
+}
+
+export { FolderNav, FolderModal, FolderEditor, FolderDropdown };
diff --git a/src/ui/static/js/utils/form.js b/src/ui/static/js/utils/form.js
index 5e6591263..1c50da357 100644
--- a/src/ui/static/js/utils/form.js
+++ b/src/ui/static/js/utils/form.js
@@ -1,262 +1,263 @@
-class Checkbox {
- constructor() {
- this.init();
- }
-
- init() {
- window.addEventListener("click", (e) => {
- //prevent default checkbox behavior
- try {
- //case a related checkbox element is clicked and checkbox is enabled
- if (
- e.target.closest("div").hasAttribute("data-checkbox-handler") &&
- !e.target
- .closest("div")
- .querySelector('input[type="checkbox"]')
- .hasAttribute("disabled")
- ) {
- //get related checkbox
- const checkboxEl = e.target
- .closest("div")
- .querySelector('input[type="checkbox"]');
-
- const prevValue = checkboxEl.getAttribute("value");
-
- //set attribute value for new state
- prevValue === "no"
- ? checkboxEl.setAttribute("value", "yes")
- : checkboxEl.setAttribute("value", "no");
-
- //set custom input hidden value
- const newValue = checkboxEl.getAttribute("value");
- newValue === "yes"
- ? checkboxEl.setAttribute("aria-checked", "true")
- : checkboxEl.setAttribute("aria-checked", "false");
-
- //force checked for submit
- checkboxEl.checked = true;
- }
- } catch (err) {}
- });
- }
-}
-
-class Select {
- constructor() {
- this.init();
- }
-
- init() {
- window.addEventListener("click", (e) => {
- //CASE NO BTN SELECT CLICKED
- try {
- if (!e.target.closest("button")) {
- const selectEls = document.querySelectorAll(
- "div[data-setting-select-dropdown]"
- );
- selectEls.forEach((select) => {
- select.classList.add("hidden");
- select.classList.remove("flex");
- });
- const btnEls = document.querySelectorAll(
- "button[data-setting-select]"
- );
- btnEls.forEach((btn) => {
- const dropdownChevron = btn.querySelector(
- `svg[data-setting-select]`
- );
- dropdownChevron.classList.remove("rotate-180");
- });
- }
- } catch (err) {}
- //SELECT BTN LOGIC
- try {
- if (
- e.target.closest("button").hasAttribute(`data-setting-select`) &&
- !e.target.closest("button").hasAttribute(`disabled`)
- ) {
- const btnEl = e.target.closest("button");
- this.toggleSelectBtn(btnEl);
- }
- } catch (err) {}
- //SELECT DROPDOWN BTN LOGIC
- try {
- if (
- e.target
- .closest("button")
- .hasAttribute(`data-setting-select-dropdown-btn`)
- ) {
- const btn = e.target.closest(
- `button[data-setting-select-dropdown-btn]`
- );
- const btnValue = btn.getAttribute("value");
-
- //add new value to custom
- const selectCustom = btn
- .closest("div[data-select-container]")
- .querySelector(`button[data-setting-select]`);
-
- selectCustom.querySelector(`[data-setting-select-text]`).textContent =
- btnValue;
- //add selected to new value
-
- //change style
- const dropdownEl = btn.closest(`div[data-setting-select-dropdown]`);
- dropdownEl.classList.add("hidden");
- dropdownEl.classList.remove("flex");
-
- //reset dropdown btns
- const btnEls = dropdownEl.querySelectorAll("button");
-
- btnEls.forEach((btn) => {
- btn.classList.remove("active");
- });
- //highlight clicked btn
- btn.classList.add("active");
-
- //close dropdown
- const dropdownChevron = selectCustom.querySelector(
- `svg[data-setting-select]`
- );
- dropdownChevron.classList.remove("rotate-180");
-
- //update real select element
- const realSel = btn
- .closest("div[data-setting-container]")
- .querySelector("select");
- this.updateSelected(realSel, btnValue);
- }
- } catch (err) {}
- });
- }
-
- updateSelected(selectEl, selectedValue) {
- const options = selectEl.querySelectorAll("option");
- //remove selected to all
- options.forEach((option) => {
- option.removeAttribute("selected");
- option.selected = false;
- });
- //select new one
- const newOption = selectEl.querySelector(
- `option[value="${selectedValue}"]`
- );
- newOption.selected = true;
- newOption.setAttribute("selected", "");
- }
-
- toggleSelectBtn(btn) {
- //toggle dropdown
- const dropdownEl = btn
- .closest("div")
- .querySelector(`[data-setting-select-dropdown]`);
- const dropdownChevron = btn.querySelector(`svg[data-setting-select]`);
- dropdownEl.classList.toggle("hidden");
- dropdownEl.classList.toggle("flex");
- dropdownChevron.classList.toggle("rotate-180");
- }
-}
-
-class Password {
- constructor() {
- this.init();
- }
-
- init() {
- window.addEventListener("click", (e) => {
- try {
- if (e.target.closest("button").hasAttribute("data-setting-password")) {
- const btn = e.target.closest("button");
- const action = btn.getAttribute("data-setting-password");
- const inp = btn
- .closest("[data-setting-container]")
- .querySelector("input");
- this.setValDisplay(action, inp);
- this.hiddenBtns(btn);
- this.showOppositeBtn(btn, action);
- }
- } catch (err) {}
- });
- }
-
- showOppositeBtn(btnEl, action) {
- const btnEls = this.getBtns(btnEl);
- const opposite = action === "visible" ? "invisible" : "visible";
-
- btnEls.forEach((btn) => {
- const action = btn.getAttribute("data-setting-password");
-
- if (action === opposite) {
- btn.classList.add("flex");
- btn.classList.remove("hidden");
- }
- });
- }
-
- setValDisplay(action, inp) {
- if (action === "visible") inp.setAttribute("type", "text");
- if (action === "invisible") inp.setAttribute("type", "password");
- }
-
- hiddenBtns(btnEl) {
- const btnEls = this.getBtns(btnEl);
-
- btnEls.forEach((btn) => {
- btn.classList.add("hidden");
- btn.classList.remove("flex");
- });
- }
-
- getBtns(btnEl) {
- return btnEl
- .closest("[data-setting-container]")
- .querySelectorAll("button[data-setting-password]");
- }
-}
-
-class DisabledPop {
- constructor() {
- this.init();
- }
-
- init() {
- window.addEventListener("pointerover", (e) => {
- //for checkbox and regular inputs
- if (e.target.tagName === "INPUT") {
- const el = e.target;
- this.showPopup(el, "input");
- }
- //for select custom
- if (
- e.target.tagName === "BUTTON" &&
- e.target.hasAttribute("data-setting-select")
- ) {
- const el = e.target;
- this.showPopup(el, "select");
- }
- });
-
- window.addEventListener("pointerout", (e) => {
- try {
- const popupEl = e.target
- .closest("div")
- .querySelector("div[data-disabled-info]");
- popupEl.remove();
- } catch (err) {}
- });
- }
-
- showPopup(el, type = "input") {
- if (!el.hasAttribute("disabled")) return;
- const method = el.getAttribute("data-default-method");
- const popupHTML = `
-
-
disabled by ${method}
-
`;
- el.insertAdjacentHTML("beforebegin", popupHTML);
- }
-}
-
-export { Checkbox, Select, Password, DisabledPop };
+class Checkbox {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("click", (e) => {
+ //prevent default checkbox behavior
+ try {
+ //case a related checkbox element is clicked and checkbox is enabled
+ if (
+ e.target.closest("div").hasAttribute("data-checkbox-handler") &&
+ !e.target
+ .closest("div")
+ .querySelector('input[type="checkbox"]')
+ .hasAttribute("disabled")
+ ) {
+ //get related checkbox
+ const checkboxEl = e.target
+ .closest("div")
+ .querySelector('input[type="checkbox"]');
+
+ const prevValue = checkboxEl.getAttribute("value");
+
+ //set attribute value for new state
+ prevValue === "no"
+ ? checkboxEl.setAttribute("value", "yes")
+ : checkboxEl.setAttribute("value", "no");
+
+ //set custom input hidden value
+ const newValue = checkboxEl.getAttribute("value");
+ newValue === "yes"
+ ? checkboxEl.setAttribute("aria-checked", "true")
+ : checkboxEl.setAttribute("aria-checked", "false");
+
+ //force checked for submit
+ checkboxEl.checked = true;
+ }
+ } catch (err) {}
+ });
+ }
+}
+
+class Select {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("click", (e) => {
+ //CASE NO BTN SELECT CLICKED
+ try {
+ if (!e.target.closest("button")) {
+ const selectEls = document.querySelectorAll(
+ "div[data-setting-select-dropdown]"
+ );
+ selectEls.forEach((select) => {
+ select.classList.add("hidden");
+ select.classList.remove("flex");
+ });
+ const btnEls = document.querySelectorAll(
+ "button[data-setting-select]"
+ );
+ btnEls.forEach((btn) => {
+ const dropdownChevron = btn.querySelector(
+ `svg[data-setting-select]`
+ );
+ dropdownChevron.classList.remove("rotate-180");
+ });
+ }
+ } catch (err) {}
+ //SELECT BTN LOGIC
+ try {
+ if (
+ e.target.closest("button").hasAttribute(`data-setting-select`) &&
+ !e.target.closest("button").hasAttribute(`disabled`)
+ ) {
+ const btnEl = e.target.closest("button");
+ this.toggleSelectBtn(btnEl);
+ }
+ } catch (err) {}
+ //SELECT DROPDOWN BTN LOGIC
+ try {
+ if (
+ e.target
+ .closest("button")
+ .hasAttribute(`data-setting-select-dropdown-btn`)
+ ) {
+ const btn = e.target.closest(
+ `button[data-setting-select-dropdown-btn]`
+ );
+ const btnValue = btn.getAttribute("value");
+
+ //add new value to custom
+ const selectCustom = btn
+ .closest("div[data-select-container]")
+ .querySelector(`button[data-setting-select]`);
+
+ selectCustom.querySelector(`[data-setting-select-text]`).textContent =
+ btnValue;
+ //add selected to new value
+
+ //change style
+ const dropdownEl = btn.closest(`div[data-setting-select-dropdown]`);
+ dropdownEl.classList.add("hidden");
+ dropdownEl.classList.remove("flex");
+
+ //reset dropdown btns
+ const btnEls = dropdownEl.querySelectorAll("button");
+
+ btnEls.forEach((btn) => {
+ btn.classList.remove("active");
+ });
+ //highlight clicked btn
+ btn.classList.add("active");
+
+ //close dropdown
+ const dropdownChevron = selectCustom.querySelector(
+ `svg[data-setting-select]`
+ );
+ dropdownChevron.classList.remove("rotate-180");
+
+ //update real select element
+ const realSel = btn
+ .closest("div[data-setting-container]")
+ .querySelector("select");
+ this.updateSelected(realSel, btnValue);
+ }
+ } catch (err) {}
+ });
+ }
+
+ updateSelected(selectEl, selectedValue) {
+ const options = selectEl.querySelectorAll("option");
+ //remove selected to all
+ options.forEach((option) => {
+ option.removeAttribute("selected");
+ option.selected = false;
+ });
+ //select new one
+ const newOption = selectEl.querySelector(
+ `option[value="${selectedValue}"]`
+ );
+ newOption.selected = true;
+ newOption.setAttribute("selected", "");
+ }
+
+ toggleSelectBtn(btn) {
+ //toggle dropdown
+ const dropdownEl = btn
+ .closest("div")
+ .querySelector(`[data-setting-select-dropdown]`);
+ const dropdownChevron = btn.querySelector(`svg[data-setting-select]`);
+ dropdownEl.classList.toggle("hidden");
+ dropdownEl.classList.toggle("flex");
+ dropdownChevron.classList.toggle("rotate-180");
+ }
+}
+
+class Password {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("click", (e) => {
+ try {
+ if (e.target.closest("button").hasAttribute("data-setting-password")) {
+ const btn = e.target.closest("button");
+ const action = btn.getAttribute("data-setting-password");
+ const inp = btn
+ .closest("[data-setting-container]")
+ .querySelector("input");
+ this.setValDisplay(action, inp);
+ this.hiddenBtns(btn);
+ this.showOppositeBtn(btn, action);
+ }
+ } catch (err) {}
+ });
+ }
+
+ showOppositeBtn(btnEl, action) {
+ const btnEls = this.getBtns(btnEl);
+ const opposite = action === "visible" ? "invisible" : "visible";
+
+ btnEls.forEach((btn) => {
+ const action = btn.getAttribute("data-setting-password");
+
+ if (action === opposite) {
+ btn.classList.add("flex");
+ btn.classList.remove("hidden");
+ }
+ });
+ }
+
+ setValDisplay(action, inp) {
+ if (action === "visible") inp.setAttribute("type", "text");
+ if (action === "invisible") inp.setAttribute("type", "password");
+ }
+
+ hiddenBtns(btnEl) {
+ const btnEls = this.getBtns(btnEl);
+
+ btnEls.forEach((btn) => {
+ btn.classList.add("hidden");
+ btn.classList.remove("flex");
+ });
+ }
+
+ getBtns(btnEl) {
+ return btnEl
+ .closest("[data-setting-container]")
+ .querySelectorAll("button[data-setting-password]");
+ }
+}
+
+class DisabledPop {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("pointerover", (e) => {
+ //for checkbox and regular inputs
+ if (e.target.tagName === "INPUT") {
+ const el = e.target;
+ this.showPopup(el, "input");
+ }
+ //for select custom
+ if (
+ e.target.tagName === "BUTTON" &&
+ e.target.hasAttribute("data-setting-select")
+ ) {
+ const el = e.target;
+ this.showPopup(el, "select");
+ }
+ });
+
+ window.addEventListener("pointerout", (e) => {
+ try {
+ const popupEl = e.target
+ .closest("div")
+ .querySelector("div[data-disabled-info]");
+ popupEl.remove();
+ } catch (err) {}
+ });
+ }
+
+ showPopup(el, type = "input") {
+ if (!el.hasAttribute("disabled")) return;
+ const method = el.getAttribute("data-default-method");
+ const popupHTML = `
+
+
disabled by ${method}
+
`;
+ let cleanHTML = DOMPurify.sanitize(popupHTML);
+ el.insertAdjacentHTML("beforebegin", cleanHTML);
+ }
+}
+
+export { Checkbox, Select, Password, DisabledPop };
diff --git a/src/ui/static/js/utils/purify/purify.min.js b/src/ui/static/js/utils/purify/purify.min.js
new file mode 100644
index 000000000..7a4da7686
--- /dev/null
+++ b/src/ui/static/js/utils/purify/purify.min.js
@@ -0,0 +1,3 @@
+/*! @license DOMPurify 3.0.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.6/LICENSE */
+!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,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=N(Array.prototype.forEach),m=N(Array.prototype.pop),f=N(Array.prototype.push),p=N(String.prototype.toLowerCase),d=N(String.prototype.toString),h=N(String.prototype.match),g=N(String.prototype.replace),T=N(String.prototype.indexOf),y=N(String.prototype.trim),E=N(RegExp.prototype.test),A=(_=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:p;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 S(t){const n=l(null);for(const[o,i]of e(t))void 0!==r(t,o)&&(n[o]=i);return n}function R(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return N(n.get);if("function"==typeof n.value)return N(n.value)}e=o(e)}return function(e){return console.warn("fallback value for",e),null}}const w=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","section","select","shadow","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"]),D=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),L=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"]),v=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"]),x=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"]),k=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),C=i(["#text"]),O=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","face","for","headers","height","hidden","high","href","hreflang","id","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","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),I=i(["accent-height","accumulate","additive","alignment-baseline","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","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","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","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","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","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"]),M=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"]),U=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),P=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),F=a(/<%[\w\W]*|[\w\W]*%>/gm),H=a(/\${[\w\W]*}/gm),z=a(/^data-[\-\w.\u00B7-\uFFFF]/),B=a(/^aria-[\-\w]+$/),W=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),G=a(/^(?:\w+script|data):/i),Y=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),j=a(/^html$/i);var q=Object.freeze({__proto__:null,MUSTACHE_EXPR:P,ERB_EXPR:F,TMPLIT_EXPR:H,DATA_ATTR:z,ARIA_ATTR:B,IS_ALLOWED_URI:W,IS_SCRIPT_OR_DATA:G,ATTR_WHITESPACE:Y,DOCTYPE_NAME:j});const X=function(){return"undefined"==typeof window?null:window},K=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}};var V=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:X();const o=e=>t(e);if(o.version="3.0.6",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:_,Node:N,Element:P,NodeFilter:F,NamedNodeMap:H=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:z,DOMParser:B,trustedTypes:G}=n,Y=P.prototype,V=R(Y,"cloneNode"),$=R(Y,"nextSibling"),Z=R(Y,"childNodes"),J=R(Y,"parentNode");if("function"==typeof _){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let Q,ee="";const{implementation:te,createNodeIterator:ne,createDocumentFragment:oe,getElementsByTagName:re}=r,{importNode:ie}=a;let ae={};o.isSupported="function"==typeof e&&"function"==typeof J&&te&&void 0!==te.createHTMLDocument;const{MUSTACHE_EXPR:le,ERB_EXPR:ce,TMPLIT_EXPR:se,DATA_ATTR:ue,ARIA_ATTR:me,IS_SCRIPT_OR_DATA:fe,ATTR_WHITESPACE:pe}=q;let{IS_ALLOWED_URI:de}=q,he=null;const ge=b({},[...w,...D,...L,...x,...C]);let Te=null;const ye=b({},[...O,...I,...M,...U]);let Ee=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}})),Ae=null,_e=null,Ne=!0,be=!0,Se=!1,Re=!0,we=!1,De=!1,Le=!1,ve=!1,xe=!1,ke=!1,Ce=!1,Oe=!0,Ie=!1;const Me="user-content-";let Ue=!0,Pe=!1,Fe={},He=null;const ze=b({},["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 Be=null;const We=b({},["audio","video","img","source","image","track"]);let Ge=null;const Ye=b({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),je="http://www.w3.org/1998/Math/MathML",qe="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml";let Ke=Xe,Ve=!1,$e=null;const Ze=b({},[je,qe,Xe],d);let Je=null;const Qe=["application/xhtml+xml","text/html"],et="text/html";let tt=null,nt=null;const ot=r.createElement("form"),rt=function(e){return e instanceof RegExp||e instanceof Function},it=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!nt||nt!==e){if(e&&"object"==typeof e||(e={}),e=S(e),Je=Je=-1===Qe.indexOf(e.PARSER_MEDIA_TYPE)?et:e.PARSER_MEDIA_TYPE,tt="application/xhtml+xml"===Je?d:p,he="ALLOWED_TAGS"in e?b({},e.ALLOWED_TAGS,tt):ge,Te="ALLOWED_ATTR"in e?b({},e.ALLOWED_ATTR,tt):ye,$e="ALLOWED_NAMESPACES"in e?b({},e.ALLOWED_NAMESPACES,d):Ze,Ge="ADD_URI_SAFE_ATTR"in e?b(S(Ye),e.ADD_URI_SAFE_ATTR,tt):Ye,Be="ADD_DATA_URI_TAGS"in e?b(S(We),e.ADD_DATA_URI_TAGS,tt):We,He="FORBID_CONTENTS"in e?b({},e.FORBID_CONTENTS,tt):ze,Ae="FORBID_TAGS"in e?b({},e.FORBID_TAGS,tt):{},_e="FORBID_ATTR"in e?b({},e.FORBID_ATTR,tt):{},Fe="USE_PROFILES"in e&&e.USE_PROFILES,Ne=!1!==e.ALLOW_ARIA_ATTR,be=!1!==e.ALLOW_DATA_ATTR,Se=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Re=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,we=e.SAFE_FOR_TEMPLATES||!1,De=e.WHOLE_DOCUMENT||!1,xe=e.RETURN_DOM||!1,ke=e.RETURN_DOM_FRAGMENT||!1,Ce=e.RETURN_TRUSTED_TYPE||!1,ve=e.FORCE_BODY||!1,Oe=!1!==e.SANITIZE_DOM,Ie=e.SANITIZE_NAMED_PROPS||!1,Ue=!1!==e.KEEP_CONTENT,Pe=e.IN_PLACE||!1,de=e.ALLOWED_URI_REGEXP||W,Ke=e.NAMESPACE||Xe,Ee=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&rt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ee.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&rt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ee.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ee.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),we&&(be=!1),ke&&(xe=!0),Fe&&(he=b({},[...C]),Te=[],!0===Fe.html&&(b(he,w),b(Te,O)),!0===Fe.svg&&(b(he,D),b(Te,I),b(Te,U)),!0===Fe.svgFilters&&(b(he,L),b(Te,I),b(Te,U)),!0===Fe.mathMl&&(b(he,x),b(Te,M),b(Te,U))),e.ADD_TAGS&&(he===ge&&(he=S(he)),b(he,e.ADD_TAGS,tt)),e.ADD_ATTR&&(Te===ye&&(Te=S(Te)),b(Te,e.ADD_ATTR,tt)),e.ADD_URI_SAFE_ATTR&&b(Ge,e.ADD_URI_SAFE_ATTR,tt),e.FORBID_CONTENTS&&(He===ze&&(He=S(He)),b(He,e.FORBID_CONTENTS,tt)),Ue&&(he["#text"]=!0),De&&b(he,["html","head","body"]),he.table&&(b(he,["tbody"]),delete Ae.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');Q=e.TRUSTED_TYPES_POLICY,ee=Q.createHTML("")}else void 0===Q&&(Q=K(G,c)),null!==Q&&"string"==typeof ee&&(ee=Q.createHTML(""));i&&i(e),nt=e}},at=b({},["mi","mo","mn","ms","mtext"]),lt=b({},["foreignobject","desc","title","annotation-xml"]),ct=b({},["title","style","font","a","script"]),st=b({},D);b(st,L),b(st,v);const ut=b({},x);b(ut,k);const mt=function(e){let t=J(e);t&&t.tagName||(t={namespaceURI:Ke,tagName:"template"});const n=p(e.tagName),o=p(t.tagName);return!!$e[e.namespaceURI]&&(e.namespaceURI===qe?t.namespaceURI===Xe?"svg"===n:t.namespaceURI===je?"svg"===n&&("annotation-xml"===o||at[o]):Boolean(st[n]):e.namespaceURI===je?t.namespaceURI===Xe?"math"===n:t.namespaceURI===qe?"math"===n&<[o]:Boolean(ut[n]):e.namespaceURI===Xe?!(t.namespaceURI===qe&&!lt[o])&&(!(t.namespaceURI===je&&!at[o])&&(!ut[n]&&(ct[n]||!st[n]))):!("application/xhtml+xml"!==Je||!$e[e.namespaceURI]))},ft=function(e){f(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},pt=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&&!Te[e])if(xe||ke)try{ft(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},dt=function(e){let t=null,n=null;if(ve)e=" "+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===Je&&Ke===Xe&&(e=''+e+"");const o=Q?Q.createHTML(e):e;if(Ke===Xe)try{t=(new B).parseFromString(o,Je)}catch(e){}if(!t||!t.documentElement){t=te.createDocument(Ke,"template",null);try{t.documentElement.innerHTML=Ve?ee:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Ke===Xe?re.call(t,De?"html":"body")[0]:De?t.documentElement:i},ht=function(e){return ne.call(e.ownerDocument||e,e,F.SHOW_ELEMENT|F.SHOW_COMMENT|F.SHOW_TEXT,null)},gt=function(e){return e instanceof z&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof H)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Tt=function(e){return"function"==typeof N&&e instanceof N},yt=function(e,t,n){ae[e]&&u(ae[e],(e=>{e.call(o,t,n,nt)}))},Et=function(e){let t=null;if(yt("beforeSanitizeElements",e,null),gt(e))return ft(e),!0;const n=tt(e.nodeName);if(yt("uponSanitizeElement",e,{tagName:n,allowedTags:he}),e.hasChildNodes()&&!Tt(e.firstElementChild)&&E(/<[/\w]/g,e.innerHTML)&&E(/<[/\w]/g,e.textContent))return ft(e),!0;if(!he[n]||Ae[n]){if(!Ae[n]&&_t(n)){if(Ee.tagNameCheck instanceof RegExp&&E(Ee.tagNameCheck,n))return!1;if(Ee.tagNameCheck instanceof Function&&Ee.tagNameCheck(n))return!1}if(Ue&&!He[n]){const t=J(e)||e.parentNode,n=Z(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o)t.insertBefore(V(n[o],!0),$(e))}}return ft(e),!0}return e instanceof P&&!mt(e)?(ft(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!E(/<\/no(script|embed|frames)/i,e.innerHTML)?(we&&3===e.nodeType&&(t=e.textContent,u([le,ce,se],(e=>{t=g(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),yt("afterSanitizeElements",e,null),!1):(ft(e),!0)},At=function(e,t,n){if(Oe&&("id"===t||"name"===t)&&(n in r||n in ot))return!1;if(be&&!_e[t]&&E(ue,t));else if(Ne&&E(me,t));else if(!Te[t]||_e[t]){if(!(_t(e)&&(Ee.tagNameCheck instanceof RegExp&&E(Ee.tagNameCheck,e)||Ee.tagNameCheck instanceof Function&&Ee.tagNameCheck(e))&&(Ee.attributeNameCheck instanceof RegExp&&E(Ee.attributeNameCheck,t)||Ee.attributeNameCheck instanceof Function&&Ee.attributeNameCheck(t))||"is"===t&&Ee.allowCustomizedBuiltInElements&&(Ee.tagNameCheck instanceof RegExp&&E(Ee.tagNameCheck,n)||Ee.tagNameCheck instanceof Function&&Ee.tagNameCheck(n))))return!1}else if(Ge[t]);else if(E(de,g(n,pe,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Be[e]){if(Se&&!E(fe,g(n,pe,"")));else if(n)return!1}else;return!0},_t=function(e){return e.indexOf("-")>0},Nt=function(e){yt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Te};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=tt(a);let f="value"===a?c:y(c);if(n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,yt("uponSanitizeAttribute",e,n),f=n.attrValue,n.forceKeepAttr)continue;if(pt(a,e),!n.keepAttr)continue;if(!Re&&E(/\/>/i,f)){pt(a,e);continue}we&&u([le,ce,se],(e=>{f=g(f,e," ")}));const p=tt(e.nodeName);if(At(p,s,f)){if(!Ie||"id"!==s&&"name"!==s||(pt(a,e),f=Me+f),Q&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(p,s)){case"TrustedHTML":f=Q.createHTML(f);break;case"TrustedScriptURL":f=Q.createScriptURL(f)}try{l?e.setAttributeNS(l,a,f):e.setAttribute(a,f),m(o.removed)}catch(e){}}}yt("afterSanitizeAttributes",e,null)},bt=function e(t){let n=null;const o=ht(t);for(yt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)yt("uponSanitizeShadowNode",n,null),Et(n)||(n.content instanceof s&&e(n.content),Nt(n));yt("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(Ve=!e,Ve&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Tt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Le||it(t),o.removed=[],"string"==typeof e&&(Pe=!1),Pe){if(e.nodeName){const t=tt(e.nodeName);if(!he[t]||Ae[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof N)n=dt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!xe&&!we&&!De&&-1===e.indexOf("<"))return Q&&Ce?Q.createHTML(e):e;if(n=dt(e),!n)return xe?null:Ce?ee:""}n&&ve&&ft(n.firstChild);const c=ht(Pe?e:n);for(;i=c.nextNode();)Et(i)||(i.content instanceof s&&bt(i.content),Nt(i));if(Pe)return e;if(xe){if(ke)for(l=oe.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Te.shadowroot||Te.shadowrootmode)&&(l=ie.call(a,l,!0)),l}let m=De?n.outerHTML:n.innerHTML;return De&&he["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&E(j,n.ownerDocument.doctype.name)&&(m="\n"+m),we&&u([le,ce,se],(e=>{m=g(m,e," ")})),Q&&Ce?Q.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};it(e),Le=!0},o.clearConfig=function(){nt=null,Le=!1},o.isValidAttribute=function(e,t,n){nt||it({});const o=tt(e),r=tt(t);return At(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(ae[e]=ae[e]||[],f(ae[e],t))},o.removeHook=function(e){if(ae[e])return m(ae[e])},o.removeHooks=function(e){ae[e]&&(ae[e]=[])},o.removeAllHooks=function(){ae={}},o}();return V}));
+//# sourceMappingURL=purify.min.js.map
diff --git a/src/ui/static/js/utils/purify/purify.min.js.map b/src/ui/static/js/utils/purify/purify.min.js.map
new file mode 100644
index 000000000..c0e565f5b
--- /dev/null
+++ b/src/ui/static/js/utils/purify/purify.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"purify.min.js","sources":["src/utils.js","src/tags.js","src/attrs.js","src/regexp.js","src/purify.js"],"sourcesContent":["const {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n} = Object;\n\nlet { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports\nlet { apply, construct } = typeof Reflect !== 'undefined' && Reflect;\n\nif (!freeze) {\n freeze = function (x) {\n return x;\n };\n}\n\nif (!seal) {\n seal = function (x) {\n return x;\n };\n}\n\nif (!apply) {\n apply = function (fun, thisValue, args) {\n return fun.apply(thisValue, args);\n };\n}\n\nif (!construct) {\n construct = function (Func, args) {\n return new Func(...args);\n };\n}\n\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayIndexOf = unapply(Array.prototype.indexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySlice = unapply(Array.prototype.slice);\n\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\n\nconst regExpTest = unapply(RegExp.prototype.test);\n\nconst typeErrorCreate = unconstruct(TypeError);\n\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param {Function} func - The function to be wrapped and called.\n * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(func) {\n return (thisArg, ...args) => apply(func, thisArg, args);\n}\n\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param {Function} func - The constructor function to be wrapped and called.\n * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(func) {\n return (...args) => construct(func, args);\n}\n\n/**\n * Add properties to a lookup table\n *\n * @param {Object} set - The set to which elements will be added.\n * @param {Array} array - The array containing elements to be added to the set.\n * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns {Object} The modified set with added elements.\n */\nfunction addToSet(set, array, transformCaseFunc = stringToLowerCase) {\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n\n let l = array.length;\n while (l--) {\n let element = array[l];\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n array[l] = lcElement;\n }\n\n element = lcElement;\n }\n }\n\n set[element] = true;\n }\n\n return set;\n}\n\n/**\n * Shallow clone an object\n *\n * @param {Object} object - The object to be cloned.\n * @returns {Object} A new object that copies the original.\n */\nexport function clone(object) {\n const newObject = create(null);\n\n for (const [property, value] of entries(object)) {\n if (getOwnPropertyDescriptor(object, property) !== undefined) {\n newObject[property] = value;\n }\n }\n\n return newObject;\n}\n\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param {Object} object - The object to look up the getter function in its prototype chain.\n * @param {String} prop - The property name for which to find the getter function.\n * @returns {Function} The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter(object, prop) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n\n object = getPrototypeOf(object);\n }\n\n function fallbackValue(element) {\n console.warn('fallback value for', element);\n return null;\n }\n\n return fallbackValue;\n}\n\nexport {\n // Array\n arrayForEach,\n arrayIndexOf,\n arrayPop,\n arrayPush,\n arraySlice,\n // Object\n entries,\n freeze,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n isFrozen,\n setPrototypeOf,\n seal,\n create,\n // RegExp\n regExpTest,\n // String\n stringIndexOf,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringTrim,\n // Errors\n typeErrorCreate,\n // Other\n lookupGetter,\n addToSet,\n // Reflect\n unapply,\n unconstruct,\n};\n","import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'a',\n 'abbr',\n 'acronym',\n 'address',\n 'area',\n 'article',\n 'aside',\n 'audio',\n 'b',\n 'bdi',\n 'bdo',\n 'big',\n 'blink',\n 'blockquote',\n 'body',\n 'br',\n 'button',\n 'canvas',\n 'caption',\n 'center',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'content',\n 'data',\n 'datalist',\n 'dd',\n 'decorator',\n 'del',\n 'details',\n 'dfn',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'element',\n 'em',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'font',\n 'footer',\n 'form',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hgroup',\n 'hr',\n 'html',\n 'i',\n 'img',\n 'input',\n 'ins',\n 'kbd',\n 'label',\n 'legend',\n 'li',\n 'main',\n 'map',\n 'mark',\n 'marquee',\n 'menu',\n 'menuitem',\n 'meter',\n 'nav',\n 'nobr',\n 'ol',\n 'optgroup',\n 'option',\n 'output',\n 'p',\n 'picture',\n 'pre',\n 'progress',\n 'q',\n 'rp',\n 'rt',\n 'ruby',\n 's',\n 'samp',\n 'section',\n 'select',\n 'shadow',\n 'small',\n 'source',\n 'spacer',\n 'span',\n 'strike',\n 'strong',\n 'style',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'template',\n 'textarea',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'tt',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'wbr',\n]);\n\n// SVG\nexport const svg = freeze([\n 'svg',\n 'a',\n 'altglyph',\n 'altglyphdef',\n 'altglyphitem',\n 'animatecolor',\n 'animatemotion',\n 'animatetransform',\n 'circle',\n 'clippath',\n 'defs',\n 'desc',\n 'ellipse',\n 'filter',\n 'font',\n 'g',\n 'glyph',\n 'glyphref',\n 'hkern',\n 'image',\n 'line',\n 'lineargradient',\n 'marker',\n 'mask',\n 'metadata',\n 'mpath',\n 'path',\n 'pattern',\n 'polygon',\n 'polyline',\n 'radialgradient',\n 'rect',\n 'stop',\n 'style',\n 'switch',\n 'symbol',\n 'text',\n 'textpath',\n 'title',\n 'tref',\n 'tspan',\n 'view',\n 'vkern',\n]);\n\nexport const svgFilters = freeze([\n 'feBlend',\n 'feColorMatrix',\n 'feComponentTransfer',\n 'feComposite',\n 'feConvolveMatrix',\n 'feDiffuseLighting',\n 'feDisplacementMap',\n 'feDistantLight',\n 'feDropShadow',\n 'feFlood',\n 'feFuncA',\n 'feFuncB',\n 'feFuncG',\n 'feFuncR',\n 'feGaussianBlur',\n 'feImage',\n 'feMerge',\n 'feMergeNode',\n 'feMorphology',\n 'feOffset',\n 'fePointLight',\n 'feSpecularLighting',\n 'feSpotLight',\n 'feTile',\n 'feTurbulence',\n]);\n\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nexport const svgDisallowed = freeze([\n 'animate',\n 'color-profile',\n 'cursor',\n 'discard',\n 'font-face',\n 'font-face-format',\n 'font-face-name',\n 'font-face-src',\n 'font-face-uri',\n 'foreignobject',\n 'hatch',\n 'hatchpath',\n 'mesh',\n 'meshgradient',\n 'meshpatch',\n 'meshrow',\n 'missing-glyph',\n 'script',\n 'set',\n 'solidcolor',\n 'unknown',\n 'use',\n]);\n\nexport const mathMl = freeze([\n 'math',\n 'menclose',\n 'merror',\n 'mfenced',\n 'mfrac',\n 'mglyph',\n 'mi',\n 'mlabeledtr',\n 'mmultiscripts',\n 'mn',\n 'mo',\n 'mover',\n 'mpadded',\n 'mphantom',\n 'mroot',\n 'mrow',\n 'ms',\n 'mspace',\n 'msqrt',\n 'mstyle',\n 'msub',\n 'msup',\n 'msubsup',\n 'mtable',\n 'mtd',\n 'mtext',\n 'mtr',\n 'munder',\n 'munderover',\n 'mprescripts',\n]);\n\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nexport const mathMlDisallowed = freeze([\n 'maction',\n 'maligngroup',\n 'malignmark',\n 'mlongdiv',\n 'mscarries',\n 'mscarry',\n 'msgroup',\n 'mstack',\n 'msline',\n 'msrow',\n 'semantics',\n 'annotation',\n 'annotation-xml',\n 'mprescripts',\n 'none',\n]);\n\nexport const text = freeze(['#text']);\n","import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'accept',\n 'action',\n 'align',\n 'alt',\n 'autocapitalize',\n 'autocomplete',\n 'autopictureinpicture',\n 'autoplay',\n 'background',\n 'bgcolor',\n 'border',\n 'capture',\n 'cellpadding',\n 'cellspacing',\n 'checked',\n 'cite',\n 'class',\n 'clear',\n 'color',\n 'cols',\n 'colspan',\n 'controls',\n 'controlslist',\n 'coords',\n 'crossorigin',\n 'datetime',\n 'decoding',\n 'default',\n 'dir',\n 'disabled',\n 'disablepictureinpicture',\n 'disableremoteplayback',\n 'download',\n 'draggable',\n 'enctype',\n 'enterkeyhint',\n 'face',\n 'for',\n 'headers',\n 'height',\n 'hidden',\n 'high',\n 'href',\n 'hreflang',\n 'id',\n 'inputmode',\n 'integrity',\n 'ismap',\n 'kind',\n 'label',\n 'lang',\n 'list',\n 'loading',\n 'loop',\n 'low',\n 'max',\n 'maxlength',\n 'media',\n 'method',\n 'min',\n 'minlength',\n 'multiple',\n 'muted',\n 'name',\n 'nonce',\n 'noshade',\n 'novalidate',\n 'nowrap',\n 'open',\n 'optimum',\n 'pattern',\n 'placeholder',\n 'playsinline',\n 'poster',\n 'preload',\n 'pubdate',\n 'radiogroup',\n 'readonly',\n 'rel',\n 'required',\n 'rev',\n 'reversed',\n 'role',\n 'rows',\n 'rowspan',\n 'spellcheck',\n 'scope',\n 'selected',\n 'shape',\n 'size',\n 'sizes',\n 'span',\n 'srclang',\n 'start',\n 'src',\n 'srcset',\n 'step',\n 'style',\n 'summary',\n 'tabindex',\n 'title',\n 'translate',\n 'type',\n 'usemap',\n 'valign',\n 'value',\n 'width',\n 'xmlns',\n 'slot',\n]);\n\nexport const svg = freeze([\n 'accent-height',\n 'accumulate',\n 'additive',\n 'alignment-baseline',\n 'ascent',\n 'attributename',\n 'attributetype',\n 'azimuth',\n 'basefrequency',\n 'baseline-shift',\n 'begin',\n 'bias',\n 'by',\n 'class',\n 'clip',\n 'clippathunits',\n 'clip-path',\n 'clip-rule',\n 'color',\n 'color-interpolation',\n 'color-interpolation-filters',\n 'color-profile',\n 'color-rendering',\n 'cx',\n 'cy',\n 'd',\n 'dx',\n 'dy',\n 'diffuseconstant',\n 'direction',\n 'display',\n 'divisor',\n 'dur',\n 'edgemode',\n 'elevation',\n 'end',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'filter',\n 'filterunits',\n 'flood-color',\n 'flood-opacity',\n 'font-family',\n 'font-size',\n 'font-size-adjust',\n 'font-stretch',\n 'font-style',\n 'font-variant',\n 'font-weight',\n 'fx',\n 'fy',\n 'g1',\n 'g2',\n 'glyph-name',\n 'glyphref',\n 'gradientunits',\n 'gradienttransform',\n 'height',\n 'href',\n 'id',\n 'image-rendering',\n 'in',\n 'in2',\n 'k',\n 'k1',\n 'k2',\n 'k3',\n 'k4',\n 'kerning',\n 'keypoints',\n 'keysplines',\n 'keytimes',\n 'lang',\n 'lengthadjust',\n 'letter-spacing',\n 'kernelmatrix',\n 'kernelunitlength',\n 'lighting-color',\n 'local',\n 'marker-end',\n 'marker-mid',\n 'marker-start',\n 'markerheight',\n 'markerunits',\n 'markerwidth',\n 'maskcontentunits',\n 'maskunits',\n 'max',\n 'mask',\n 'media',\n 'method',\n 'mode',\n 'min',\n 'name',\n 'numoctaves',\n 'offset',\n 'operator',\n 'opacity',\n 'order',\n 'orient',\n 'orientation',\n 'origin',\n 'overflow',\n 'paint-order',\n 'path',\n 'pathlength',\n 'patterncontentunits',\n 'patterntransform',\n 'patternunits',\n 'points',\n 'preservealpha',\n 'preserveaspectratio',\n 'primitiveunits',\n 'r',\n 'rx',\n 'ry',\n 'radius',\n 'refx',\n 'refy',\n 'repeatcount',\n 'repeatdur',\n 'restart',\n 'result',\n 'rotate',\n 'scale',\n 'seed',\n 'shape-rendering',\n 'specularconstant',\n 'specularexponent',\n 'spreadmethod',\n 'startoffset',\n 'stddeviation',\n 'stitchtiles',\n 'stop-color',\n 'stop-opacity',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-miterlimit',\n 'stroke-opacity',\n 'stroke',\n 'stroke-width',\n 'style',\n 'surfacescale',\n 'systemlanguage',\n 'tabindex',\n 'targetx',\n 'targety',\n 'transform',\n 'transform-origin',\n 'text-anchor',\n 'text-decoration',\n 'text-rendering',\n 'textlength',\n 'type',\n 'u1',\n 'u2',\n 'unicode',\n 'values',\n 'viewbox',\n 'visibility',\n 'version',\n 'vert-adv-y',\n 'vert-origin-x',\n 'vert-origin-y',\n 'width',\n 'word-spacing',\n 'wrap',\n 'writing-mode',\n 'xchannelselector',\n 'ychannelselector',\n 'x',\n 'x1',\n 'x2',\n 'xmlns',\n 'y',\n 'y1',\n 'y2',\n 'z',\n 'zoomandpan',\n]);\n\nexport const mathMl = freeze([\n 'accent',\n 'accentunder',\n 'align',\n 'bevelled',\n 'close',\n 'columnsalign',\n 'columnlines',\n 'columnspan',\n 'denomalign',\n 'depth',\n 'dir',\n 'display',\n 'displaystyle',\n 'encoding',\n 'fence',\n 'frame',\n 'height',\n 'href',\n 'id',\n 'largeop',\n 'length',\n 'linethickness',\n 'lspace',\n 'lquote',\n 'mathbackground',\n 'mathcolor',\n 'mathsize',\n 'mathvariant',\n 'maxsize',\n 'minsize',\n 'movablelimits',\n 'notation',\n 'numalign',\n 'open',\n 'rowalign',\n 'rowlines',\n 'rowspacing',\n 'rowspan',\n 'rspace',\n 'rquote',\n 'scriptlevel',\n 'scriptminsize',\n 'scriptsizemultiplier',\n 'selection',\n 'separator',\n 'separators',\n 'stretchy',\n 'subscriptshift',\n 'supscriptshift',\n 'symmetric',\n 'voffset',\n 'width',\n 'xmlns',\n]);\n\nexport const xml = freeze([\n 'xlink:href',\n 'xml:id',\n 'xlink:title',\n 'xml:space',\n 'xmlns:xlink',\n]);\n","import { seal } from './utils.js';\n\n// eslint-disable-next-line unicorn/better-regex\nexport const MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\nexport const ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\nexport const TMPLIT_EXPR = seal(/\\${[\\w\\W]*}/gm);\nexport const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\nexport const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nexport const IS_ALLOWED_URI = seal(\n /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nexport const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nexport const ATTR_WHITESPACE = seal(\n /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nexport const DOCTYPE_NAME = seal(/^html$/i);\n","import * as TAGS from './tags.js';\nimport * as ATTRS from './attrs.js';\nimport * as EXPRESSIONS from './regexp.js';\nimport {\n addToSet,\n clone,\n entries,\n freeze,\n arrayForEach,\n arrayPop,\n arrayPush,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringIndexOf,\n stringTrim,\n regExpTest,\n typeErrorCreate,\n lookupGetter,\n create,\n} from './utils.js';\n\nconst getGlobal = function () {\n return typeof window === 'undefined' ? null : window;\n};\n\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory.\n * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function (trustedTypes, purifyHostElement) {\n if (\n typeof trustedTypes !== 'object' ||\n typeof trustedTypes.createPolicy !== 'function'\n ) {\n return null;\n }\n\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n },\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn(\n 'TrustedTypes policy ' + policyName + ' could not be created.'\n );\n return null;\n }\n};\n\nfunction createDOMPurify(window = getGlobal()) {\n const DOMPurify = (root) => createDOMPurify(root);\n\n /**\n * Version label, exposed for easier checks\n * if DOMPurify is up to date or not\n */\n DOMPurify.version = VERSION;\n\n /**\n * Array of elements that DOMPurify removed during sanitation.\n * Empty if nothing was removed.\n */\n DOMPurify.removed = [];\n\n if (!window || !window.document || window.document.nodeType !== 9) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n\n return DOMPurify;\n }\n\n let { document } = window;\n\n const originalDocument = document;\n const currentScript = originalDocument.currentScript;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes,\n } = window;\n\n const ElementPrototype = Element.prototype;\n\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n\n let trustedTypesPolicy;\n let emptyHTML = '';\n\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName,\n } = document;\n const { importNode } = originalDocument;\n\n let hooks = {};\n\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported =\n typeof entries === 'function' &&\n typeof getParentNode === 'function' &&\n implementation &&\n implementation.createHTMLDocument !== undefined;\n\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n } = EXPRESSIONS;\n\n let { IS_ALLOWED_URI } = EXPRESSIONS;\n\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [\n ...TAGS.html,\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.mathMl,\n ...TAGS.text,\n ]);\n\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [\n ...ATTRS.html,\n ...ATTRS.svg,\n ...ATTRS.mathMl,\n ...ATTRS.xml,\n ]);\n\n /*\n * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false,\n },\n })\n );\n\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (§7.3.3)\n * - DOM Tree Accessors (§3.1.5)\n * - Form Element Parent-Child Relations (§4.10.3)\n * - Iframe srcdoc / Nested WindowProxies (§4.8.5)\n * - HTMLCollection (§4.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES = {};\n\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, [\n 'annotation-xml',\n 'audio',\n 'colgroup',\n 'desc',\n 'foreignobject',\n 'head',\n 'iframe',\n 'math',\n 'mi',\n 'mn',\n 'mo',\n 'ms',\n 'mtext',\n 'noembed',\n 'noframes',\n 'noscript',\n 'plaintext',\n 'script',\n 'style',\n 'svg',\n 'template',\n 'thead',\n 'title',\n 'video',\n 'xmp',\n ]);\n\n /* Tags that are safe for data: URIs */\n let DATA_URI_TAGS = null;\n const DEFAULT_DATA_URI_TAGS = addToSet({}, [\n 'audio',\n 'video',\n 'img',\n 'source',\n 'image',\n 'track',\n ]);\n\n /* Attributes safe for values like \"javascript:\" */\n let URI_SAFE_ATTRIBUTES = null;\n const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, [\n 'alt',\n 'class',\n 'for',\n 'id',\n 'label',\n 'name',\n 'pattern',\n 'placeholder',\n 'role',\n 'summary',\n 'title',\n 'value',\n 'style',\n 'xmlns',\n ]);\n\n const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n /* Document namespace */\n let NAMESPACE = HTML_NAMESPACE;\n let IS_EMPTY_INPUT = false;\n\n /* Allowed XHTML+XML namespaces */\n let ALLOWED_NAMESPACES = null;\n const DEFAULT_ALLOWED_NAMESPACES = addToSet(\n {},\n [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE],\n stringToString\n );\n\n /* Parsing of strict XHTML documents */\n let PARSER_MEDIA_TYPE = null;\n const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n let transformCaseFunc = null;\n\n /* Keep a reference to config to pass to hooks */\n let CONFIG = null;\n\n /* Ideally, do not touch anything below this line */\n /* ______________________________________________ */\n\n const formElement = document.createElement('form');\n\n const isRegexOrFunction = function (testValue) {\n return testValue instanceof RegExp || testValue instanceof Function;\n };\n\n /**\n * _parseConfig\n *\n * @param {Object} cfg optional config literal\n */\n // eslint-disable-next-line complexity\n const _parseConfig = function (cfg = {}) {\n if (CONFIG && CONFIG === cfg) {\n return;\n }\n\n /* Shield configuration object from tampering */\n if (!cfg || typeof cfg !== 'object') {\n cfg = {};\n }\n\n /* Shield configuration object from prototype pollution */\n cfg = clone(cfg);\n\n PARSER_MEDIA_TYPE =\n // eslint-disable-next-line unicorn/prefer-includes\n SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1\n ? (PARSER_MEDIA_TYPE = DEFAULT_PARSER_MEDIA_TYPE)\n : (PARSER_MEDIA_TYPE = cfg.PARSER_MEDIA_TYPE);\n\n // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n transformCaseFunc =\n PARSER_MEDIA_TYPE === 'application/xhtml+xml'\n ? stringToString\n : stringToLowerCase;\n\n /* Set configuration parameters */\n ALLOWED_TAGS =\n 'ALLOWED_TAGS' in cfg\n ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)\n : DEFAULT_ALLOWED_TAGS;\n ALLOWED_ATTR =\n 'ALLOWED_ATTR' in cfg\n ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc)\n : DEFAULT_ALLOWED_ATTR;\n ALLOWED_NAMESPACES =\n 'ALLOWED_NAMESPACES' in cfg\n ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString)\n : DEFAULT_ALLOWED_NAMESPACES;\n URI_SAFE_ATTRIBUTES =\n 'ADD_URI_SAFE_ATTR' in cfg\n ? addToSet(\n clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent\n cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_URI_SAFE_ATTRIBUTES;\n DATA_URI_TAGS =\n 'ADD_DATA_URI_TAGS' in cfg\n ? addToSet(\n clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent\n cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_DATA_URI_TAGS;\n FORBID_CONTENTS =\n 'FORBID_CONTENTS' in cfg\n ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc)\n : DEFAULT_FORBID_CONTENTS;\n FORBID_TAGS =\n 'FORBID_TAGS' in cfg\n ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc)\n : {};\n FORBID_ATTR =\n 'FORBID_ATTR' in cfg\n ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)\n : {};\n USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false;\n ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n IN_PLACE = cfg.IN_PLACE || false; // Default false\n IS_ALLOWED_URI = cfg.ALLOWED_URI_REGEXP || EXPRESSIONS.IS_ALLOWED_URI;\n NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)\n ) {\n CUSTOM_ELEMENT_HANDLING.tagNameCheck =\n cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n }\n\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)\n ) {\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck =\n cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n }\n\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements ===\n 'boolean'\n ) {\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements =\n cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n }\n\n if (SAFE_FOR_TEMPLATES) {\n ALLOW_DATA_ATTR = false;\n }\n\n if (RETURN_DOM_FRAGMENT) {\n RETURN_DOM = true;\n }\n\n /* Parse profile info */\n if (USE_PROFILES) {\n ALLOWED_TAGS = addToSet({}, [...TAGS.text]);\n ALLOWED_ATTR = [];\n if (USE_PROFILES.html === true) {\n addToSet(ALLOWED_TAGS, TAGS.html);\n addToSet(ALLOWED_ATTR, ATTRS.html);\n }\n\n if (USE_PROFILES.svg === true) {\n addToSet(ALLOWED_TAGS, TAGS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n\n if (USE_PROFILES.svgFilters === true) {\n addToSet(ALLOWED_TAGS, TAGS.svgFilters);\n addToSet(ALLOWED_ATTR, ATTRS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n\n if (USE_PROFILES.mathMl === true) {\n addToSet(ALLOWED_TAGS, TAGS.mathMl);\n addToSet(ALLOWED_ATTR, ATTRS.mathMl);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n }\n\n /* Merge configuration parameters */\n if (cfg.ADD_TAGS) {\n if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n ALLOWED_TAGS = clone(ALLOWED_TAGS);\n }\n\n addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n }\n\n if (cfg.ADD_ATTR) {\n if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n ALLOWED_ATTR = clone(ALLOWED_ATTR);\n }\n\n addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n }\n\n if (cfg.ADD_URI_SAFE_ATTR) {\n addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n }\n\n if (cfg.FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n\n addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n }\n\n /* Add #text in case KEEP_CONTENT is set to true */\n if (KEEP_CONTENT) {\n ALLOWED_TAGS['#text'] = true;\n }\n\n /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n if (WHOLE_DOCUMENT) {\n addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n }\n\n /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n if (ALLOWED_TAGS.table) {\n addToSet(ALLOWED_TAGS, ['tbody']);\n delete FORBID_TAGS.tbody;\n }\n\n if (cfg.TRUSTED_TYPES_POLICY) {\n if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n throw typeErrorCreate(\n 'TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.'\n );\n }\n\n if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n throw typeErrorCreate(\n 'TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.'\n );\n }\n\n // Overwrite existing TrustedTypes policy.\n trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n\n // Sign local variables required by `sanitize`.\n emptyHTML = trustedTypesPolicy.createHTML('');\n } else {\n // Uninitialized policy, attempt to initialize the internal dompurify policy.\n if (trustedTypesPolicy === undefined) {\n trustedTypesPolicy = _createTrustedTypesPolicy(\n trustedTypes,\n currentScript\n );\n }\n\n // If creating the internal policy succeeded sign internal variables.\n if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n emptyHTML = trustedTypesPolicy.createHTML('');\n }\n }\n\n // Prevent further manipulation of configuration.\n // Not available in IE8, Safari 5, etc.\n if (freeze) {\n freeze(cfg);\n }\n\n CONFIG = cfg;\n };\n\n const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, [\n 'mi',\n 'mo',\n 'mn',\n 'ms',\n 'mtext',\n ]);\n\n const HTML_INTEGRATION_POINTS = addToSet({}, [\n 'foreignobject',\n 'desc',\n 'title',\n 'annotation-xml',\n ]);\n\n // Certain elements are allowed in both SVG and HTML\n // namespace. We need to specify them explicitly\n // so that they don't get erroneously deleted from\n // HTML namespace.\n const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, [\n 'title',\n 'style',\n 'font',\n 'a',\n 'script',\n ]);\n\n /* Keep track of all possible SVG and MathML tags\n * so that we can perform the namespace checks\n * correctly. */\n const ALL_SVG_TAGS = addToSet({}, TAGS.svg);\n addToSet(ALL_SVG_TAGS, TAGS.svgFilters);\n addToSet(ALL_SVG_TAGS, TAGS.svgDisallowed);\n\n const ALL_MATHML_TAGS = addToSet({}, TAGS.mathMl);\n addToSet(ALL_MATHML_TAGS, TAGS.mathMlDisallowed);\n\n /**\n * @param {Element} element a DOM element whose namespace is being checked\n * @returns {boolean} Return false if the element has a\n * namespace that a spec-compliant parser would never\n * return. Return true otherwise.\n */\n const _checkValidNamespace = function (element) {\n let parent = getParentNode(element);\n\n // In JSDOM, if we're inside shadow DOM, then parentNode\n // can be null. We just simulate parent in this case.\n if (!parent || !parent.tagName) {\n parent = {\n namespaceURI: NAMESPACE,\n tagName: 'template',\n };\n }\n\n const tagName = stringToLowerCase(element.tagName);\n const parentTagName = stringToLowerCase(parent.tagName);\n\n if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n return false;\n }\n\n if (element.namespaceURI === SVG_NAMESPACE) {\n // The only way to switch from HTML namespace to SVG\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'svg';\n }\n\n // The only way to switch from MathML to SVG is via`\n // svg if parent is either or MathML\n // text integration points.\n if (parent.namespaceURI === MATHML_NAMESPACE) {\n return (\n tagName === 'svg' &&\n (parentTagName === 'annotation-xml' ||\n MATHML_TEXT_INTEGRATION_POINTS[parentTagName])\n );\n }\n\n // We only allow elements that are defined in SVG\n // spec. All others are disallowed in SVG namespace.\n return Boolean(ALL_SVG_TAGS[tagName]);\n }\n\n if (element.namespaceURI === MATHML_NAMESPACE) {\n // The only way to switch from HTML namespace to MathML\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'math';\n }\n\n // The only way to switch from SVG to MathML is via\n // and HTML integration points\n if (parent.namespaceURI === SVG_NAMESPACE) {\n return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n }\n\n // We only allow elements that are defined in MathML\n // spec. All others are disallowed in MathML namespace.\n return Boolean(ALL_MATHML_TAGS[tagName]);\n }\n\n if (element.namespaceURI === HTML_NAMESPACE) {\n // The only way to switch from SVG to HTML is via\n // HTML integration points, and from MathML to HTML\n // is via MathML text integration points\n if (\n parent.namespaceURI === SVG_NAMESPACE &&\n !HTML_INTEGRATION_POINTS[parentTagName]\n ) {\n return false;\n }\n\n if (\n parent.namespaceURI === MATHML_NAMESPACE &&\n !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]\n ) {\n return false;\n }\n\n // We disallow tags that are specific for MathML\n // or SVG and should never appear in HTML namespace\n return (\n !ALL_MATHML_TAGS[tagName] &&\n (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])\n );\n }\n\n // For XHTML and XML documents that support custom namespaces\n if (\n PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&\n ALLOWED_NAMESPACES[element.namespaceURI]\n ) {\n return true;\n }\n\n // The code should never reach this place (this means\n // that the element somehow got namespace that is not\n // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n // Return false just in case.\n return false;\n };\n\n /**\n * _forceRemove\n *\n * @param {Node} node a DOM node\n */\n const _forceRemove = function (node) {\n arrayPush(DOMPurify.removed, { element: node });\n try {\n // eslint-disable-next-line unicorn/prefer-dom-node-remove\n node.parentNode.removeChild(node);\n } catch (_) {\n node.remove();\n }\n };\n\n /**\n * _removeAttribute\n *\n * @param {String} name an Attribute name\n * @param {Node} node a DOM node\n */\n const _removeAttribute = function (name, node) {\n try {\n arrayPush(DOMPurify.removed, {\n attribute: node.getAttributeNode(name),\n from: node,\n });\n } catch (_) {\n arrayPush(DOMPurify.removed, {\n attribute: null,\n from: node,\n });\n }\n\n node.removeAttribute(name);\n\n // We void attribute values for unremovable \"is\"\" attributes\n if (name === 'is' && !ALLOWED_ATTR[name]) {\n if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n try {\n _forceRemove(node);\n } catch (_) {}\n } else {\n try {\n node.setAttribute(name, '');\n } catch (_) {}\n }\n }\n };\n\n /**\n * _initDocument\n *\n * @param {String} dirty a string of dirty markup\n * @return {Document} a DOM, filled with the dirty markup\n */\n const _initDocument = function (dirty) {\n /* Create a HTML document */\n let doc = null;\n let leadingWhitespace = null;\n\n if (FORCE_BODY) {\n dirty = ' ' + dirty;\n } else {\n /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n leadingWhitespace = matches && matches[0];\n }\n\n if (\n PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&\n NAMESPACE === HTML_NAMESPACE\n ) {\n // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n dirty =\n '' +\n dirty +\n '';\n }\n\n const dirtyPayload = trustedTypesPolicy\n ? trustedTypesPolicy.createHTML(dirty)\n : dirty;\n /*\n * Use the DOMParser API by default, fallback later if needs be\n * DOMParser not work for svg when has multiple root element.\n */\n if (NAMESPACE === HTML_NAMESPACE) {\n try {\n doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n } catch (_) {}\n }\n\n /* Use createHTMLDocument in case DOMParser is not available */\n if (!doc || !doc.documentElement) {\n doc = implementation.createDocument(NAMESPACE, 'template', null);\n try {\n doc.documentElement.innerHTML = IS_EMPTY_INPUT\n ? emptyHTML\n : dirtyPayload;\n } catch (_) {\n // Syntax error if dirtyPayload is invalid xml\n }\n }\n\n const body = doc.body || doc.documentElement;\n\n if (dirty && leadingWhitespace) {\n body.insertBefore(\n document.createTextNode(leadingWhitespace),\n body.childNodes[0] || null\n );\n }\n\n /* Work on whole document or just its body */\n if (NAMESPACE === HTML_NAMESPACE) {\n return getElementsByTagName.call(\n doc,\n WHOLE_DOCUMENT ? 'html' : 'body'\n )[0];\n }\n\n return WHOLE_DOCUMENT ? doc.documentElement : body;\n };\n\n /**\n * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n *\n * @param {Node} root The root element or node to start traversing on.\n * @return {NodeIterator} The created NodeIterator\n */\n const _createNodeIterator = function (root) {\n return createNodeIterator.call(\n root.ownerDocument || root,\n root,\n // eslint-disable-next-line no-bitwise\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT,\n null\n );\n };\n\n /**\n * _isClobbered\n *\n * @param {Node} elm element to check for clobbering attacks\n * @return {Boolean} true if clobbered, false if safe\n */\n const _isClobbered = function (elm) {\n return (\n elm instanceof HTMLFormElement &&\n (typeof elm.nodeName !== 'string' ||\n typeof elm.textContent !== 'string' ||\n typeof elm.removeChild !== 'function' ||\n !(elm.attributes instanceof NamedNodeMap) ||\n typeof elm.removeAttribute !== 'function' ||\n typeof elm.setAttribute !== 'function' ||\n typeof elm.namespaceURI !== 'string' ||\n typeof elm.insertBefore !== 'function' ||\n typeof elm.hasChildNodes !== 'function')\n );\n };\n\n /**\n * Checks whether the given object is a DOM node.\n *\n * @param {Node} object object to check whether it's a DOM node\n * @return {Boolean} true is object is a DOM node\n */\n const _isNode = function (object) {\n return typeof Node === 'function' && object instanceof Node;\n };\n\n /**\n * _executeHook\n * Execute user configurable hooks\n *\n * @param {String} entryPoint Name of the hook's entry point\n * @param {Node} currentNode node to work on with the hook\n * @param {Object} data additional hook parameters\n */\n const _executeHook = function (entryPoint, currentNode, data) {\n if (!hooks[entryPoint]) {\n return;\n }\n\n arrayForEach(hooks[entryPoint], (hook) => {\n hook.call(DOMPurify, currentNode, data, CONFIG);\n });\n };\n\n /**\n * _sanitizeElements\n *\n * @protect nodeName\n * @protect textContent\n * @protect removeChild\n *\n * @param {Node} currentNode to check for permission to exist\n * @return {Boolean} true if node was killed, false if left alive\n */\n const _sanitizeElements = function (currentNode) {\n let content = null;\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeElements', currentNode, null);\n\n /* Check if element is clobbered or can clobber */\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Now let's check the element's type and name */\n const tagName = transformCaseFunc(currentNode.nodeName);\n\n /* Execute a hook if present */\n _executeHook('uponSanitizeElement', currentNode, {\n tagName,\n allowedTags: ALLOWED_TAGS,\n });\n\n /* Detect mXSS attempts abusing namespace confusion */\n if (\n currentNode.hasChildNodes() &&\n !_isNode(currentNode.firstElementChild) &&\n regExpTest(/<[/\\w]/g, currentNode.innerHTML) &&\n regExpTest(/<[/\\w]/g, currentNode.textContent)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Remove element if anything forbids its presence */\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n\n /* Keep content except for bad-listed elements */\n if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n\n if (childNodes && parentNode) {\n const childCount = childNodes.length;\n\n for (let i = childCount - 1; i >= 0; --i) {\n parentNode.insertBefore(\n cloneNode(childNodes[i], true),\n getNextSibling(currentNode)\n );\n }\n }\n }\n\n _forceRemove(currentNode);\n return true;\n }\n\n /* Check whether element has a valid namespace */\n if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Make sure that older browsers don't get fallback-tag mXSS */\n if (\n (tagName === 'noscript' ||\n tagName === 'noembed' ||\n tagName === 'noframes') &&\n regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Sanitize element content to be template-safe */\n if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {\n /* Get the element's text content */\n content = currentNode.textContent;\n\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n content = stringReplace(content, expr, ' ');\n });\n\n if (currentNode.textContent !== content) {\n arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });\n currentNode.textContent = content;\n }\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeElements', currentNode, null);\n\n return false;\n };\n\n /**\n * _isValidAttribute\n *\n * @param {string} lcTag Lowercase tag name of containing element.\n * @param {string} lcName Lowercase attribute name.\n * @param {string} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid, otherwise false.\n */\n // eslint-disable-next-line complexity\n const _isValidAttribute = function (lcTag, lcName, value) {\n /* Make sure attribute cannot clobber */\n if (\n SANITIZE_DOM &&\n (lcName === 'id' || lcName === 'name') &&\n (value in document || value in formElement)\n ) {\n return false;\n }\n\n /* Allow valid data-* attributes: At least one character after \"-\"\n (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n We don't need to check the value; it's always URI safe. */\n if (\n ALLOW_DATA_ATTR &&\n !FORBID_ATTR[lcName] &&\n regExpTest(DATA_ATTR, lcName)\n ) {\n // This attribute is safe\n } else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {\n // This attribute is safe\n /* Otherwise, check the name is permitted */\n } else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n if (\n // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n (_isBasicCustomElement(lcTag) &&\n ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag)) ||\n (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag))) &&\n ((CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName)) ||\n (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)))) ||\n // Alternative, second condition checks if it's an `is`-attribute, AND\n // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n (lcName === 'is' &&\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements &&\n ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)) ||\n (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))))\n ) {\n // If user has supplied a regexp or function in CUSTOM_ELEMENT_HANDLING.tagNameCheck, we need to also allow derived custom elements using the same tagName test.\n // Additionally, we need to allow attributes passing the CUSTOM_ELEMENT_HANDLING.attributeNameCheck user has configured, as custom elements can define these at their own discretion.\n } else {\n return false;\n }\n /* Check value is safe. First, is attr inert? If so, is safe */\n } else if (URI_SAFE_ATTRIBUTES[lcName]) {\n // This attribute is safe\n /* Check no script, data or unknown possibly unsafe URI\n unless we know URI values are safe for that attribute */\n } else if (\n regExpTest(IS_ALLOWED_URI, stringReplace(value, ATTR_WHITESPACE, ''))\n ) {\n // This attribute is safe\n /* Keep image data URIs alive if src/xlink:href is allowed */\n /* Further prevent gadget XSS for dynamically built script tags */\n } else if (\n (lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') &&\n lcTag !== 'script' &&\n stringIndexOf(value, 'data:') === 0 &&\n DATA_URI_TAGS[lcTag]\n ) {\n // This attribute is safe\n /* Allow unknown protocols: This provides support for links that\n are handled by protocol handlers which may be unknown ahead of\n time, e.g. fb:, spotify: */\n } else if (\n ALLOW_UNKNOWN_PROTOCOLS &&\n !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))\n ) {\n // This attribute is safe\n /* Check for binary attributes */\n } else if (value) {\n return false;\n } else {\n // Binary attributes are safe at this point\n /* Anything else, presume unsafe, do not add it back */\n }\n\n return true;\n };\n\n /**\n * _isBasicCustomElement\n * checks if at least one dash is included in tagName, and it's not the first char\n * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n *\n * @param {string} tagName name of the tag of the node to sanitize\n * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n */\n const _isBasicCustomElement = function (tagName) {\n return tagName.indexOf('-') > 0;\n };\n\n /**\n * _sanitizeAttributes\n *\n * @protect attributes\n * @protect nodeName\n * @protect removeAttribute\n * @protect setAttribute\n *\n * @param {Node} currentNode to sanitize\n */\n const _sanitizeAttributes = function (currentNode) {\n /* Execute a hook if present */\n _executeHook('beforeSanitizeAttributes', currentNode, null);\n\n const { attributes } = currentNode;\n\n /* Check if we have attributes; if not we might have a text node */\n if (!attributes) {\n return;\n }\n\n const hookEvent = {\n attrName: '',\n attrValue: '',\n keepAttr: true,\n allowedAttributes: ALLOWED_ATTR,\n };\n let l = attributes.length;\n\n /* Go backwards over all attributes; safely remove bad ones */\n while (l--) {\n const attr = attributes[l];\n const { name, namespaceURI, value: attrValue } = attr;\n const lcName = transformCaseFunc(name);\n\n let value = name === 'value' ? attrValue : stringTrim(attrValue);\n\n /* Execute a hook if present */\n hookEvent.attrName = lcName;\n hookEvent.attrValue = value;\n hookEvent.keepAttr = true;\n hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n _executeHook('uponSanitizeAttribute', currentNode, hookEvent);\n value = hookEvent.attrValue;\n /* Did the hooks approve of the attribute? */\n if (hookEvent.forceKeepAttr) {\n continue;\n }\n\n /* Remove attribute */\n _removeAttribute(name, currentNode);\n\n /* Did the hooks approve of the attribute? */\n if (!hookEvent.keepAttr) {\n continue;\n }\n\n /* Work around a security issue in jQuery 3.0 */\n if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n\n /* Sanitize attribute content to be template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n value = stringReplace(value, expr, ' ');\n });\n }\n\n /* Is `value` valid for this attribute? */\n const lcTag = transformCaseFunc(currentNode.nodeName);\n if (!_isValidAttribute(lcTag, lcName, value)) {\n continue;\n }\n\n /* Full DOM Clobbering protection via namespace isolation,\n * Prefix id and name attributes with `user-content-`\n */\n if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n // Remove the attribute with this value\n _removeAttribute(name, currentNode);\n\n // Prefix the value and later re-create the attribute with the sanitized value\n value = SANITIZE_NAMED_PROPS_PREFIX + value;\n }\n\n /* Handle attributes that require Trusted Types */\n if (\n trustedTypesPolicy &&\n typeof trustedTypes === 'object' &&\n typeof trustedTypes.getAttributeType === 'function'\n ) {\n if (namespaceURI) {\n /* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */\n } else {\n switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n case 'TrustedHTML': {\n value = trustedTypesPolicy.createHTML(value);\n break;\n }\n\n case 'TrustedScriptURL': {\n value = trustedTypesPolicy.createScriptURL(value);\n break;\n }\n\n default: {\n break;\n }\n }\n }\n }\n\n /* Handle invalid data-* attribute set by try-catching it */\n try {\n if (namespaceURI) {\n currentNode.setAttributeNS(namespaceURI, name, value);\n } else {\n /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n currentNode.setAttribute(name, value);\n }\n\n arrayPop(DOMPurify.removed);\n } catch (_) {}\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeAttributes', currentNode, null);\n };\n\n /**\n * _sanitizeShadowDOM\n *\n * @param {DocumentFragment} fragment to iterate over recursively\n */\n const _sanitizeShadowDOM = function (fragment) {\n let shadowNode = null;\n const shadowIterator = _createNodeIterator(fragment);\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeShadowDOM', fragment, null);\n\n while ((shadowNode = shadowIterator.nextNode())) {\n /* Execute a hook if present */\n _executeHook('uponSanitizeShadowNode', shadowNode, null);\n\n /* Sanitize tags and elements */\n if (_sanitizeElements(shadowNode)) {\n continue;\n }\n\n /* Deep shadow DOM detected */\n if (shadowNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(shadowNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(shadowNode);\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeShadowDOM', fragment, null);\n };\n\n /**\n * Sanitize\n * Public method providing core sanitation functionality\n *\n * @param {String|Node} dirty string or DOM node\n * @param {Object} cfg object\n */\n // eslint-disable-next-line complexity\n DOMPurify.sanitize = function (dirty, cfg = {}) {\n let body = null;\n let importedNode = null;\n let currentNode = null;\n let returnNode = null;\n /* Make sure we have a string to sanitize.\n DO NOT return early, as this will return the wrong type if\n the user has requested a DOM object rather than a string */\n IS_EMPTY_INPUT = !dirty;\n if (IS_EMPTY_INPUT) {\n dirty = '';\n }\n\n /* Stringify, in case dirty is an object */\n if (typeof dirty !== 'string' && !_isNode(dirty)) {\n if (typeof dirty.toString === 'function') {\n dirty = dirty.toString();\n if (typeof dirty !== 'string') {\n throw typeErrorCreate('dirty is not a string, aborting');\n }\n } else {\n throw typeErrorCreate('toString is not a function');\n }\n }\n\n /* Return dirty HTML if DOMPurify cannot run */\n if (!DOMPurify.isSupported) {\n return dirty;\n }\n\n /* Assign config vars */\n if (!SET_CONFIG) {\n _parseConfig(cfg);\n }\n\n /* Clean up removed elements */\n DOMPurify.removed = [];\n\n /* Check if dirty is correctly typed for IN_PLACE */\n if (typeof dirty === 'string') {\n IN_PLACE = false;\n }\n\n if (IN_PLACE) {\n /* Do some early pre-sanitization to avoid unsafe root nodes */\n if (dirty.nodeName) {\n const tagName = transformCaseFunc(dirty.nodeName);\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n throw typeErrorCreate(\n 'root node is forbidden and cannot be sanitized in-place'\n );\n }\n }\n } else if (dirty instanceof Node) {\n /* If dirty is a DOM element, append to an empty document to avoid\n elements being stripped by the parser */\n body = _initDocument('');\n importedNode = body.ownerDocument.importNode(dirty, true);\n if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {\n /* Node is already a body, use as is */\n body = importedNode;\n } else if (importedNode.nodeName === 'HTML') {\n body = importedNode;\n } else {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n body.appendChild(importedNode);\n }\n } else {\n /* Exit directly if we have nothing to do */\n if (\n !RETURN_DOM &&\n !SAFE_FOR_TEMPLATES &&\n !WHOLE_DOCUMENT &&\n // eslint-disable-next-line unicorn/prefer-includes\n dirty.indexOf('<') === -1\n ) {\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? trustedTypesPolicy.createHTML(dirty)\n : dirty;\n }\n\n /* Initialize the document to work on */\n body = _initDocument(dirty);\n\n /* Check we have a DOM node from the data */\n if (!body) {\n return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n }\n }\n\n /* Remove first element node (ours) if FORCE_BODY is set */\n if (body && FORCE_BODY) {\n _forceRemove(body.firstChild);\n }\n\n /* Get node iterator */\n const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\n /* Now start iterating over the created document */\n while ((currentNode = nodeIterator.nextNode())) {\n /* Sanitize tags and elements */\n if (_sanitizeElements(currentNode)) {\n continue;\n }\n\n /* Shadow DOM detected, sanitize it */\n if (currentNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(currentNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(currentNode);\n }\n\n /* If we sanitized `dirty` in-place, return it. */\n if (IN_PLACE) {\n return dirty;\n }\n\n /* Return sanitized string or DOM */\n if (RETURN_DOM) {\n if (RETURN_DOM_FRAGMENT) {\n returnNode = createDocumentFragment.call(body.ownerDocument);\n\n while (body.firstChild) {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n returnNode.appendChild(body.firstChild);\n }\n } else {\n returnNode = body;\n }\n\n if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n /*\n AdoptNode() is not used because internal state is not reset\n (e.g. the past names map of a HTMLFormElement), this is safe\n in theory but we would rather not risk another attack vector.\n The state that is cloned by importNode() is explicitly defined\n by the specs.\n */\n returnNode = importNode.call(originalDocument, returnNode, true);\n }\n\n return returnNode;\n }\n\n let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n /* Serialize doctype if allowed */\n if (\n WHOLE_DOCUMENT &&\n ALLOWED_TAGS['!doctype'] &&\n body.ownerDocument &&\n body.ownerDocument.doctype &&\n body.ownerDocument.doctype.name &&\n regExpTest(EXPRESSIONS.DOCTYPE_NAME, body.ownerDocument.doctype.name)\n ) {\n serializedHTML =\n '\\n' + serializedHTML;\n }\n\n /* Sanitize final string template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n serializedHTML = stringReplace(serializedHTML, expr, ' ');\n });\n }\n\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? trustedTypesPolicy.createHTML(serializedHTML)\n : serializedHTML;\n };\n\n /**\n * Public method to set the configuration once\n * setConfig\n *\n * @param {Object} cfg configuration object\n */\n DOMPurify.setConfig = function (cfg = {}) {\n _parseConfig(cfg);\n SET_CONFIG = true;\n };\n\n /**\n * Public method to remove the configuration\n * clearConfig\n *\n */\n DOMPurify.clearConfig = function () {\n CONFIG = null;\n SET_CONFIG = false;\n };\n\n /**\n * Public method to check if an attribute value is valid.\n * Uses last set config, if any. Otherwise, uses config defaults.\n * isValidAttribute\n *\n * @param {String} tag Tag name of containing element.\n * @param {String} attr Attribute name.\n * @param {String} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.\n */\n DOMPurify.isValidAttribute = function (tag, attr, value) {\n /* Initialize shared config vars if necessary. */\n if (!CONFIG) {\n _parseConfig({});\n }\n\n const lcTag = transformCaseFunc(tag);\n const lcName = transformCaseFunc(attr);\n return _isValidAttribute(lcTag, lcName, value);\n };\n\n /**\n * AddHook\n * Public method to add DOMPurify hooks\n *\n * @param {String} entryPoint entry point for the hook to add\n * @param {Function} hookFunction function to execute\n */\n DOMPurify.addHook = function (entryPoint, hookFunction) {\n if (typeof hookFunction !== 'function') {\n return;\n }\n\n hooks[entryPoint] = hooks[entryPoint] || [];\n arrayPush(hooks[entryPoint], hookFunction);\n };\n\n /**\n * RemoveHook\n * Public method to remove a DOMPurify hook at a given entryPoint\n * (pops it from the stack of hooks if more are present)\n *\n * @param {String} entryPoint entry point for the hook to remove\n * @return {Function} removed(popped) hook\n */\n DOMPurify.removeHook = function (entryPoint) {\n if (hooks[entryPoint]) {\n return arrayPop(hooks[entryPoint]);\n }\n };\n\n /**\n * RemoveHooks\n * Public method to remove all DOMPurify hooks at a given entryPoint\n *\n * @param {String} entryPoint entry point for the hooks to remove\n */\n DOMPurify.removeHooks = function (entryPoint) {\n if (hooks[entryPoint]) {\n hooks[entryPoint] = [];\n }\n };\n\n /**\n * RemoveAllHooks\n * Public method to remove all DOMPurify hooks\n */\n DOMPurify.removeAllHooks = function () {\n hooks = {};\n };\n\n return DOMPurify;\n}\n\nexport default createDOMPurify();\n"],"names":["entries","setPrototypeOf","isFrozen","getPrototypeOf","getOwnPropertyDescriptor","Object","freeze","seal","create","apply","construct","Reflect","x","fun","thisValue","args","Func","arrayForEach","unapply","Array","prototype","forEach","arrayPop","pop","arrayPush","push","stringToLowerCase","String","toLowerCase","stringToString","toString","stringMatch","match","stringReplace","replace","stringIndexOf","indexOf","stringTrim","trim","regExpTest","RegExp","test","typeErrorCreate","func","TypeError","_len2","arguments","length","_key2","thisArg","_len","_key","addToSet","set","array","transformCaseFunc","l","element","lcElement","clone","object","newObject","property","value","undefined","lookupGetter","prop","desc","get","fallbackValue","console","warn","html","svg","svgFilters","svgDisallowed","mathMl","mathMlDisallowed","text","xml","MUSTACHE_EXPR","ERB_EXPR","TMPLIT_EXPR","DATA_ATTR","ARIA_ATTR","IS_ALLOWED_URI","IS_SCRIPT_OR_DATA","ATTR_WHITESPACE","DOCTYPE_NAME","getGlobal","window","_createTrustedTypesPolicy","trustedTypes","purifyHostElement","createPolicy","suffix","ATTR_NAME","hasAttribute","getAttribute","policyName","createHTML","createScriptURL","scriptUrl","_","purify","createDOMPurify","DOMPurify","root","version","VERSION","removed","document","nodeType","isSupported","originalDocument","currentScript","DocumentFragment","HTMLTemplateElement","Node","Element","NodeFilter","NamedNodeMap","MozNamedAttrMap","HTMLFormElement","DOMParser","ElementPrototype","cloneNode","getNextSibling","getChildNodes","getParentNode","template","createElement","content","ownerDocument","trustedTypesPolicy","emptyHTML","implementation","createNodeIterator","createDocumentFragment","getElementsByTagName","importNode","hooks","createHTMLDocument","EXPRESSIONS","ALLOWED_TAGS","DEFAULT_ALLOWED_TAGS","TAGS","ALLOWED_ATTR","DEFAULT_ALLOWED_ATTR","ATTRS","CUSTOM_ELEMENT_HANDLING","tagNameCheck","writable","configurable","enumerable","attributeNameCheck","allowCustomizedBuiltInElements","FORBID_TAGS","FORBID_ATTR","ALLOW_ARIA_ATTR","ALLOW_DATA_ATTR","ALLOW_UNKNOWN_PROTOCOLS","ALLOW_SELF_CLOSE_IN_ATTR","SAFE_FOR_TEMPLATES","WHOLE_DOCUMENT","SET_CONFIG","FORCE_BODY","RETURN_DOM","RETURN_DOM_FRAGMENT","RETURN_TRUSTED_TYPE","SANITIZE_DOM","SANITIZE_NAMED_PROPS","SANITIZE_NAMED_PROPS_PREFIX","KEEP_CONTENT","IN_PLACE","USE_PROFILES","FORBID_CONTENTS","DEFAULT_FORBID_CONTENTS","DATA_URI_TAGS","DEFAULT_DATA_URI_TAGS","URI_SAFE_ATTRIBUTES","DEFAULT_URI_SAFE_ATTRIBUTES","MATHML_NAMESPACE","SVG_NAMESPACE","HTML_NAMESPACE","NAMESPACE","IS_EMPTY_INPUT","ALLOWED_NAMESPACES","DEFAULT_ALLOWED_NAMESPACES","PARSER_MEDIA_TYPE","SUPPORTED_PARSER_MEDIA_TYPES","DEFAULT_PARSER_MEDIA_TYPE","CONFIG","formElement","isRegexOrFunction","testValue","Function","_parseConfig","cfg","ADD_URI_SAFE_ATTR","ADD_DATA_URI_TAGS","ALLOWED_URI_REGEXP","ADD_TAGS","ADD_ATTR","table","tbody","TRUSTED_TYPES_POLICY","MATHML_TEXT_INTEGRATION_POINTS","HTML_INTEGRATION_POINTS","COMMON_SVG_AND_HTML_ELEMENTS","ALL_SVG_TAGS","ALL_MATHML_TAGS","_checkValidNamespace","parent","tagName","namespaceURI","parentTagName","Boolean","_forceRemove","node","parentNode","removeChild","remove","_removeAttribute","name","attribute","getAttributeNode","from","removeAttribute","setAttribute","_initDocument","dirty","doc","leadingWhitespace","matches","dirtyPayload","parseFromString","documentElement","createDocument","innerHTML","body","insertBefore","createTextNode","childNodes","call","_createNodeIterator","SHOW_ELEMENT","SHOW_COMMENT","SHOW_TEXT","_isClobbered","elm","nodeName","textContent","attributes","hasChildNodes","_isNode","_executeHook","entryPoint","currentNode","data","hook","_sanitizeElements","allowedTags","firstElementChild","_isBasicCustomElement","i","expr","_isValidAttribute","lcTag","lcName","_sanitizeAttributes","hookEvent","attrName","attrValue","keepAttr","allowedAttributes","attr","forceKeepAttr","getAttributeType","setAttributeNS","_sanitizeShadowDOM","fragment","shadowNode","shadowIterator","nextNode","sanitize","importedNode","returnNode","appendChild","firstChild","nodeIterator","shadowroot","shadowrootmode","serializedHTML","outerHTML","doctype","setConfig","clearConfig","isValidAttribute","tag","addHook","hookFunction","removeHook","removeHooks","removeAllHooks"],"mappings":";0OAAA,MAAMA,QACJA,EADIC,eAEJA,EAFIC,SAGJA,EAHIC,eAIJA,EAJIC,yBAKJA,GACEC,OAEJ,IAAIC,OAAEA,EAAFC,KAAUA,EAAVC,OAAgBA,GAAWH,QAC3BI,MAAEA,EAAFC,UAASA,GAAiC,oBAAZC,SAA2BA,QAExDL,IACHA,EAAS,SAAUM,GACjB,OAAOA,CACR,GAGEL,IACHA,EAAO,SAAUK,GACf,OAAOA,CACR,GAGEH,IACHA,EAAQ,SAAUI,EAAKC,EAAWC,GAChC,OAAOF,EAAIJ,MAAMK,EAAWC,EAC7B,GAGEL,IACHA,EAAY,SAAUM,EAAMD,GAC1B,OAAO,IAAIC,KAAQD,EACpB,GAGH,MAAME,EAAeC,EAAQC,MAAMC,UAAUC,SAEvCC,EAAWJ,EAAQC,MAAMC,UAAUG,KACnCC,EAAYN,EAAQC,MAAMC,UAAUK,MAGpCC,EAAoBR,EAAQS,OAAOP,UAAUQ,aAC7CC,EAAiBX,EAAQS,OAAOP,UAAUU,UAC1CC,EAAcb,EAAQS,OAAOP,UAAUY,OACvCC,EAAgBf,EAAQS,OAAOP,UAAUc,SACzCC,EAAgBjB,EAAQS,OAAOP,UAAUgB,SACzCC,EAAanB,EAAQS,OAAOP,UAAUkB,MAEtCC,EAAarB,EAAQsB,OAAOpB,UAAUqB,MAEtCC,GAkBeC,EAlBeC,UAmB3B,WAAA,IAAA,IAAAC,EAAAC,UAAAC,OAAIhC,EAAJ,IAAAI,MAAA0B,GAAAG,EAAA,EAAAA,EAAAH,EAAAG,IAAIjC,EAAJiC,GAAAF,UAAAE,GAAA,OAAatC,EAAUiC,EAAM5B,EAA7B,GADT,IAAqB4B,EAVrB,SAASzB,EAAQyB,GACf,OAAO,SAACM,GAAD,IAAA,IAAAC,EAAAJ,UAAAC,OAAahC,EAAb,IAAAI,MAAA+B,EAAA,EAAAA,EAAA,EAAA,GAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAapC,EAAboC,EAAA,GAAAL,UAAAK,GAAA,OAAsB1C,EAAMkC,EAAMM,EAASlC,EAA3C,CACR,CAoBD,SAASqC,EAASC,EAAKC,GAAOC,IAAAA,yDAAoB7B,EAC5CzB,GAIFA,EAAeoD,EAAK,MAGtB,IAAIG,EAAIF,EAAMP,OACPS,KAAAA,KAAK,CACV,IAAIC,EAAUH,EAAME,GACpB,GAAuB,iBAAZC,EAAsB,CAC/B,MAAMC,EAAYH,EAAkBE,GAChCC,IAAcD,IAEXvD,EAASoD,KACZA,EAAME,GAAKE,GAGbD,EAAUC,EAEb,CAEDL,EAAII,IAAW,CAChB,CAED,OAAOJ,CACR,CAQM,SAASM,EAAMC,GACpB,MAAMC,EAAYrD,EAAO,MAEpB,IAAA,MAAOsD,EAAUC,KAAU/D,EAAQ4D,QACaI,IAA/C5D,EAAyBwD,EAAQE,KACnCD,EAAUC,GAAYC,GAI1B,OAAOF,CACR,CASD,SAASI,EAAaL,EAAQM,GACrBN,KAAW,OAAXA,GAAiB,CACtB,MAAMO,EAAO/D,EAAyBwD,EAAQM,GAE9C,GAAIC,EAAM,CACJA,GAAAA,EAAKC,IACP,OAAOlD,EAAQiD,EAAKC,KAGtB,GAA0B,mBAAfD,EAAKJ,MACd,OAAO7C,EAAQiD,EAAKJ,MAEvB,CAEDH,EAASzD,EAAeyD,EACzB,CAOD,OALSS,SAAcZ,GAErB,OADAa,QAAQC,KAAK,qBAAsBd,GAC5B,IACR,CAGF,CC3JM,MAAMe,EAAOlE,EAAO,CACzB,IACA,OACA,UACA,UACA,OACA,UACA,QACA,QACA,IACA,MACA,MACA,MACA,QACA,aACA,OACA,KACA,SACA,SACA,UACA,SACA,OACA,OACA,MACA,WACA,UACA,OACA,WACA,KACA,YACA,MACA,UACA,MACA,SACA,MACA,MACA,KACA,KACA,UACA,KACA,WACA,aACA,SACA,OACA,SACA,OACA,KACA,KACA,KACA,KACA,KACA,KACA,OACA,SACA,SACA,KACA,OACA,IACA,MACA,QACA,MACA,MACA,QACA,SACA,KACA,OACA,MACA,OACA,UACA,OACA,WACA,QACA,MACA,OACA,KACA,WACA,SACA,SACA,IACA,UACA,MACA,WACA,IACA,KACA,KACA,OACA,IACA,OACA,UACA,SACA,SACA,QACA,SACA,SACA,OACA,SACA,SACA,QACA,MACA,UACA,MACA,QACA,QACA,KACA,WACA,WACA,QACA,KACA,QACA,OACA,KACA,QACA,KACA,IACA,KACA,MACA,QACA,QAIWmE,EAAMnE,EAAO,CACxB,MACA,IACA,WACA,cACA,eACA,eACA,gBACA,mBACA,SACA,WACA,OACA,OACA,UACA,SACA,OACA,IACA,QACA,WACA,QACA,QACA,OACA,iBACA,SACA,OACA,WACA,QACA,OACA,UACA,UACA,WACA,iBACA,OACA,OACA,QACA,SACA,SACA,OACA,WACA,QACA,OACA,QACA,OACA,UAGWoE,EAAapE,EAAO,CAC/B,UACA,gBACA,sBACA,cACA,mBACA,oBACA,oBACA,iBACA,eACA,UACA,UACA,UACA,UACA,UACA,iBACA,UACA,UACA,cACA,eACA,WACA,eACA,qBACA,cACA,SACA,iBAOWqE,EAAgBrE,EAAO,CAClC,UACA,gBACA,SACA,UACA,YACA,mBACA,iBACA,gBACA,gBACA,gBACA,QACA,YACA,OACA,eACA,YACA,UACA,gBACA,SACA,MACA,aACA,UACA,QAGWsE,EAAStE,EAAO,CAC3B,OACA,WACA,SACA,UACA,QACA,SACA,KACA,aACA,gBACA,KACA,KACA,QACA,UACA,WACA,QACA,OACA,KACA,SACA,QACA,SACA,OACA,OACA,UACA,SACA,MACA,QACA,MACA,SACA,aACA,gBAKWuE,EAAmBvE,EAAO,CACrC,UACA,cACA,aACA,WACA,YACA,UACA,UACA,SACA,SACA,QACA,YACA,aACA,iBACA,cACA,SAGWwE,EAAOxE,EAAO,CAAC,UCrRfkE,EAAOlE,EAAO,CACzB,SACA,SACA,QACA,MACA,iBACA,eACA,uBACA,WACA,aACA,UACA,SACA,UACA,cACA,cACA,UACA,OACA,QACA,QACA,QACA,OACA,UACA,WACA,eACA,SACA,cACA,WACA,WACA,UACA,MACA,WACA,0BACA,wBACA,WACA,YACA,UACA,eACA,OACA,MACA,UACA,SACA,SACA,OACA,OACA,WACA,KACA,YACA,YACA,QACA,OACA,QACA,OACA,OACA,UACA,OACA,MACA,MACA,YACA,QACA,SACA,MACA,YACA,WACA,QACA,OACA,QACA,UACA,aACA,SACA,OACA,UACA,UACA,cACA,cACA,SACA,UACA,UACA,aACA,WACA,MACA,WACA,MACA,WACA,OACA,OACA,UACA,aACA,QACA,WACA,QACA,OACA,QACA,OACA,UACA,QACA,MACA,SACA,OACA,QACA,UACA,WACA,QACA,YACA,OACA,SACA,SACA,QACA,QACA,QACA,SAGWmE,EAAMnE,EAAO,CACxB,gBACA,aACA,WACA,qBACA,SACA,gBACA,gBACA,UACA,gBACA,iBACA,QACA,OACA,KACA,QACA,OACA,gBACA,YACA,YACA,QACA,sBACA,8BACA,gBACA,kBACA,KACA,KACA,IACA,KACA,KACA,kBACA,YACA,UACA,UACA,MACA,WACA,YACA,MACA,OACA,eACA,YACA,SACA,cACA,cACA,gBACA,cACA,YACA,mBACA,eACA,aACA,eACA,cACA,KACA,KACA,KACA,KACA,aACA,WACA,gBACA,oBACA,SACA,OACA,KACA,kBACA,KACA,MACA,IACA,KACA,KACA,KACA,KACA,UACA,YACA,aACA,WACA,OACA,eACA,iBACA,eACA,mBACA,iBACA,QACA,aACA,aACA,eACA,eACA,cACA,cACA,mBACA,YACA,MACA,OACA,QACA,SACA,OACA,MACA,OACA,aACA,SACA,WACA,UACA,QACA,SACA,cACA,SACA,WACA,cACA,OACA,aACA,sBACA,mBACA,eACA,SACA,gBACA,sBACA,iBACA,IACA,KACA,KACA,SACA,OACA,OACA,cACA,YACA,UACA,SACA,SACA,QACA,OACA,kBACA,mBACA,mBACA,eACA,cACA,eACA,cACA,aACA,eACA,mBACA,oBACA,iBACA,kBACA,oBACA,iBACA,SACA,eACA,QACA,eACA,iBACA,WACA,UACA,UACA,YACA,mBACA,cACA,kBACA,iBACA,aACA,OACA,KACA,KACA,UACA,SACA,UACA,aACA,UACA,aACA,gBACA,gBACA,QACA,eACA,OACA,eACA,mBACA,mBACA,IACA,KACA,KACA,QACA,IACA,KACA,KACA,IACA,eAGWsE,EAA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"}
\ No newline at end of file
diff --git a/src/ui/static/js/utils/purify/src/attrs.js b/src/ui/static/js/utils/purify/src/attrs.js
new file mode 100644
index 000000000..09f2a0fb2
--- /dev/null
+++ b/src/ui/static/js/utils/purify/src/attrs.js
@@ -0,0 +1,362 @@
+import { freeze } from './utils.js';
+
+export const html = freeze([
+ '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',
+ 'face',
+ 'for',
+ 'headers',
+ 'height',
+ 'hidden',
+ 'high',
+ 'href',
+ 'hreflang',
+ 'id',
+ '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',
+ 'pattern',
+ 'placeholder',
+ 'playsinline',
+ 'poster',
+ 'preload',
+ 'pubdate',
+ 'radiogroup',
+ 'readonly',
+ 'rel',
+ 'required',
+ 'rev',
+ 'reversed',
+ 'role',
+ 'rows',
+ 'rowspan',
+ 'spellcheck',
+ 'scope',
+ 'selected',
+ 'shape',
+ 'size',
+ 'sizes',
+ 'span',
+ 'srclang',
+ 'start',
+ 'src',
+ 'srcset',
+ 'step',
+ 'style',
+ 'summary',
+ 'tabindex',
+ 'title',
+ 'translate',
+ 'type',
+ 'usemap',
+ 'valign',
+ 'value',
+ 'width',
+ 'xmlns',
+ 'slot',
+]);
+
+export const svg = freeze([
+ 'accent-height',
+ 'accumulate',
+ 'additive',
+ 'alignment-baseline',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+]);
+
+export const mathMl = freeze([
+ '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',
+]);
+
+export const xml = freeze([
+ 'xlink:href',
+ 'xml:id',
+ 'xlink:title',
+ 'xml:space',
+ 'xmlns:xlink',
+]);
diff --git a/src/ui/static/js/utils/purify/src/purify.js b/src/ui/static/js/utils/purify/src/purify.js
new file mode 100644
index 000000000..f6511db6d
--- /dev/null
+++ b/src/ui/static/js/utils/purify/src/purify.js
@@ -0,0 +1,1637 @@
+import * as TAGS from './tags.js';
+import * as ATTRS from './attrs.js';
+import * as EXPRESSIONS from './regexp.js';
+import {
+ addToSet,
+ clone,
+ entries,
+ freeze,
+ arrayForEach,
+ arrayPop,
+ arrayPush,
+ stringMatch,
+ stringReplace,
+ stringToLowerCase,
+ stringToString,
+ stringIndexOf,
+ stringTrim,
+ regExpTest,
+ typeErrorCreate,
+ lookupGetter,
+ create,
+} from './utils.js';
+
+const getGlobal = function () {
+ return typeof window === 'undefined' ? null : window;
+};
+
+/**
+ * Creates a no-op policy for internal use only.
+ * Don't export this function outside this module!
+ * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory.
+ * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
+ * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types
+ * are not supported or creating the policy failed).
+ */
+const _createTrustedTypesPolicy = function (trustedTypes, purifyHostElement) {
+ if (
+ typeof trustedTypes !== 'object' ||
+ typeof trustedTypes.createPolicy !== 'function'
+ ) {
+ return null;
+ }
+
+ // Allow the callers to control the unique policy name
+ // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
+ // Policy creation with duplicate names throws in Trusted Types.
+ let suffix = null;
+ const ATTR_NAME = 'data-tt-policy-suffix';
+ if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
+ suffix = purifyHostElement.getAttribute(ATTR_NAME);
+ }
+
+ const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
+
+ try {
+ return trustedTypes.createPolicy(policyName, {
+ createHTML(html) {
+ return html;
+ },
+ createScriptURL(scriptUrl) {
+ return scriptUrl;
+ },
+ });
+ } catch (_) {
+ // Policy creation failed (most likely another DOMPurify script has
+ // already run). Skip creating the policy, as this will only cause errors
+ // if TT are enforced.
+ console.warn(
+ 'TrustedTypes policy ' + policyName + ' could not be created.'
+ );
+ return null;
+ }
+};
+
+function createDOMPurify(window = getGlobal()) {
+ const DOMPurify = (root) => createDOMPurify(root);
+
+ /**
+ * Version label, exposed for easier checks
+ * if DOMPurify is up to date or not
+ */
+ DOMPurify.version = VERSION;
+
+ /**
+ * Array of elements that DOMPurify removed during sanitation.
+ * Empty if nothing was removed.
+ */
+ DOMPurify.removed = [];
+
+ if (!window || !window.document || window.document.nodeType !== 9) {
+ // Not running in a browser, provide a factory function
+ // so that you can pass your own Window
+ DOMPurify.isSupported = false;
+
+ return DOMPurify;
+ }
+
+ let { document } = window;
+
+ const originalDocument = document;
+ const currentScript = originalDocument.currentScript;
+ const {
+ DocumentFragment,
+ HTMLTemplateElement,
+ Node,
+ Element,
+ NodeFilter,
+ NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
+ HTMLFormElement,
+ DOMParser,
+ trustedTypes,
+ } = window;
+
+ const ElementPrototype = Element.prototype;
+
+ const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
+ const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
+ const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
+ const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
+
+ // As per issue #47, the web-components registry is inherited by a
+ // new document created via createHTMLDocument. As per the spec
+ // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
+ // a new empty registry is used when creating a template contents owner
+ // document, so we use that as our parent document to ensure nothing
+ // is inherited.
+ if (typeof HTMLTemplateElement === 'function') {
+ const template = document.createElement('template');
+ if (template.content && template.content.ownerDocument) {
+ document = template.content.ownerDocument;
+ }
+ }
+
+ let trustedTypesPolicy;
+ let emptyHTML = '';
+
+ const {
+ implementation,
+ createNodeIterator,
+ createDocumentFragment,
+ getElementsByTagName,
+ } = document;
+ const { importNode } = originalDocument;
+
+ let hooks = {};
+
+ /**
+ * Expose whether this browser supports running the full DOMPurify.
+ */
+ DOMPurify.isSupported =
+ typeof entries === 'function' &&
+ typeof getParentNode === 'function' &&
+ implementation &&
+ implementation.createHTMLDocument !== undefined;
+
+ const {
+ MUSTACHE_EXPR,
+ ERB_EXPR,
+ TMPLIT_EXPR,
+ DATA_ATTR,
+ ARIA_ATTR,
+ IS_SCRIPT_OR_DATA,
+ ATTR_WHITESPACE,
+ } = EXPRESSIONS;
+
+ let { IS_ALLOWED_URI } = EXPRESSIONS;
+
+ /**
+ * We consider the elements and attributes below to be safe. Ideally
+ * don't add any new ones but feel free to remove unwanted ones.
+ */
+
+ /* allowed element names */
+ let ALLOWED_TAGS = null;
+ const DEFAULT_ALLOWED_TAGS = addToSet({}, [
+ ...TAGS.html,
+ ...TAGS.svg,
+ ...TAGS.svgFilters,
+ ...TAGS.mathMl,
+ ...TAGS.text,
+ ]);
+
+ /* Allowed attribute names */
+ let ALLOWED_ATTR = null;
+ const DEFAULT_ALLOWED_ATTR = addToSet({}, [
+ ...ATTRS.html,
+ ...ATTRS.svg,
+ ...ATTRS.mathMl,
+ ...ATTRS.xml,
+ ]);
+
+ /*
+ * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.
+ * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
+ * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
+ * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
+ */
+ let CUSTOM_ELEMENT_HANDLING = Object.seal(
+ create(null, {
+ tagNameCheck: {
+ writable: true,
+ configurable: false,
+ enumerable: true,
+ value: null,
+ },
+ attributeNameCheck: {
+ writable: true,
+ configurable: false,
+ enumerable: true,
+ value: null,
+ },
+ allowCustomizedBuiltInElements: {
+ writable: true,
+ configurable: false,
+ enumerable: true,
+ value: false,
+ },
+ })
+ );
+
+ /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
+ let FORBID_TAGS = null;
+
+ /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
+ let FORBID_ATTR = null;
+
+ /* Decide if ARIA attributes are okay */
+ let ALLOW_ARIA_ATTR = true;
+
+ /* Decide if custom data attributes are okay */
+ let ALLOW_DATA_ATTR = true;
+
+ /* Decide if unknown protocols are okay */
+ let ALLOW_UNKNOWN_PROTOCOLS = false;
+
+ /* Decide if self-closing tags in attributes are allowed.
+ * Usually removed due to a mXSS issue in jQuery 3.0 */
+ let ALLOW_SELF_CLOSE_IN_ATTR = true;
+
+ /* Output should be safe for common template engines.
+ * This means, DOMPurify removes data attributes, mustaches and ERB
+ */
+ let SAFE_FOR_TEMPLATES = false;
+
+ /* Decide if document with ... should be returned */
+ let WHOLE_DOCUMENT = false;
+
+ /* Track whether config is already set on this instance of DOMPurify. */
+ let SET_CONFIG = false;
+
+ /* Decide if all elements (e.g. style, script) must be children of
+ * document.body. By default, browsers might move them to document.head */
+ let FORCE_BODY = false;
+
+ /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
+ * string (or a TrustedHTML object if Trusted Types are supported).
+ * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
+ */
+ let RETURN_DOM = false;
+
+ /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
+ * string (or a TrustedHTML object if Trusted Types are supported) */
+ let RETURN_DOM_FRAGMENT = false;
+
+ /* Try to return a Trusted Type object instead of a string, return a string in
+ * case Trusted Types are not supported */
+ let RETURN_TRUSTED_TYPE = false;
+
+ /* Output should be free from DOM clobbering attacks?
+ * This sanitizes markups named with colliding, clobberable built-in DOM APIs.
+ */
+ let SANITIZE_DOM = true;
+
+ /* Achieve full DOM Clobbering protection by isolating the namespace of named
+ * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
+ *
+ * HTML/DOM spec rules that enable DOM Clobbering:
+ * - Named Access on Window (§7.3.3)
+ * - DOM Tree Accessors (§3.1.5)
+ * - Form Element Parent-Child Relations (§4.10.3)
+ * - Iframe srcdoc / Nested WindowProxies (§4.8.5)
+ * - HTMLCollection (§4.2.10.2)
+ *
+ * Namespace isolation is implemented by prefixing `id` and `name` attributes
+ * with a constant string, i.e., `user-content-`
+ */
+ let SANITIZE_NAMED_PROPS = false;
+ const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';
+
+ /* Keep element content when removing element? */
+ let KEEP_CONTENT = true;
+
+ /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
+ * of importing it into a new Document and returning a sanitized copy */
+ let IN_PLACE = false;
+
+ /* Allow usage of profiles like html, svg and mathMl */
+ let USE_PROFILES = {};
+
+ /* Tags to ignore content of when KEEP_CONTENT is true */
+ let FORBID_CONTENTS = null;
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, [
+ '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',
+ ]);
+
+ /* Tags that are safe for data: URIs */
+ let DATA_URI_TAGS = null;
+ const DEFAULT_DATA_URI_TAGS = addToSet({}, [
+ 'audio',
+ 'video',
+ 'img',
+ 'source',
+ 'image',
+ 'track',
+ ]);
+
+ /* Attributes safe for values like "javascript:" */
+ let URI_SAFE_ATTRIBUTES = null;
+ const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, [
+ 'alt',
+ 'class',
+ 'for',
+ 'id',
+ 'label',
+ 'name',
+ 'pattern',
+ 'placeholder',
+ 'role',
+ 'summary',
+ 'title',
+ 'value',
+ 'style',
+ 'xmlns',
+ ]);
+
+ const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
+ const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
+ /* Document namespace */
+ let NAMESPACE = HTML_NAMESPACE;
+ let IS_EMPTY_INPUT = false;
+
+ /* Allowed XHTML+XML namespaces */
+ let ALLOWED_NAMESPACES = null;
+ const DEFAULT_ALLOWED_NAMESPACES = addToSet(
+ {},
+ [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE],
+ stringToString
+ );
+
+ /* Parsing of strict XHTML documents */
+ let PARSER_MEDIA_TYPE = null;
+ const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];
+ const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';
+ let transformCaseFunc = null;
+
+ /* Keep a reference to config to pass to hooks */
+ let CONFIG = null;
+
+ /* Ideally, do not touch anything below this line */
+ /* ______________________________________________ */
+
+ const formElement = document.createElement('form');
+
+ const isRegexOrFunction = function (testValue) {
+ return testValue instanceof RegExp || testValue instanceof Function;
+ };
+
+ /**
+ * _parseConfig
+ *
+ * @param {Object} cfg optional config literal
+ */
+ // eslint-disable-next-line complexity
+ const _parseConfig = function (cfg = {}) {
+ if (CONFIG && CONFIG === cfg) {
+ return;
+ }
+
+ /* Shield configuration object from tampering */
+ if (!cfg || typeof cfg !== 'object') {
+ cfg = {};
+ }
+
+ /* Shield configuration object from prototype pollution */
+ cfg = clone(cfg);
+
+ PARSER_MEDIA_TYPE =
+ // eslint-disable-next-line unicorn/prefer-includes
+ SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1
+ ? (PARSER_MEDIA_TYPE = DEFAULT_PARSER_MEDIA_TYPE)
+ : (PARSER_MEDIA_TYPE = cfg.PARSER_MEDIA_TYPE);
+
+ // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
+ transformCaseFunc =
+ PARSER_MEDIA_TYPE === 'application/xhtml+xml'
+ ? stringToString
+ : stringToLowerCase;
+
+ /* Set configuration parameters */
+ ALLOWED_TAGS =
+ 'ALLOWED_TAGS' in cfg
+ ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)
+ : DEFAULT_ALLOWED_TAGS;
+ ALLOWED_ATTR =
+ 'ALLOWED_ATTR' in cfg
+ ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc)
+ : DEFAULT_ALLOWED_ATTR;
+ ALLOWED_NAMESPACES =
+ 'ALLOWED_NAMESPACES' in cfg
+ ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString)
+ : DEFAULT_ALLOWED_NAMESPACES;
+ URI_SAFE_ATTRIBUTES =
+ 'ADD_URI_SAFE_ATTR' in cfg
+ ? addToSet(
+ clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent
+ cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent
+ transformCaseFunc // eslint-disable-line indent
+ ) // eslint-disable-line indent
+ : DEFAULT_URI_SAFE_ATTRIBUTES;
+ DATA_URI_TAGS =
+ 'ADD_DATA_URI_TAGS' in cfg
+ ? addToSet(
+ clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent
+ cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent
+ transformCaseFunc // eslint-disable-line indent
+ ) // eslint-disable-line indent
+ : DEFAULT_DATA_URI_TAGS;
+ FORBID_CONTENTS =
+ 'FORBID_CONTENTS' in cfg
+ ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc)
+ : DEFAULT_FORBID_CONTENTS;
+ FORBID_TAGS =
+ 'FORBID_TAGS' in cfg
+ ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc)
+ : {};
+ FORBID_ATTR =
+ 'FORBID_ATTR' in cfg
+ ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)
+ : {};
+ USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false;
+ ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
+ ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
+ ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
+ ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
+ SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
+ WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
+ RETURN_DOM = cfg.RETURN_DOM || false; // Default false
+ RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
+ RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
+ FORCE_BODY = cfg.FORCE_BODY || false; // Default false
+ SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
+ SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
+ KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
+ IN_PLACE = cfg.IN_PLACE || false; // Default false
+ IS_ALLOWED_URI = cfg.ALLOWED_URI_REGEXP || EXPRESSIONS.IS_ALLOWED_URI;
+ NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
+ CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
+ if (
+ cfg.CUSTOM_ELEMENT_HANDLING &&
+ isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)
+ ) {
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck =
+ cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
+ }
+
+ if (
+ cfg.CUSTOM_ELEMENT_HANDLING &&
+ isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)
+ ) {
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck =
+ cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
+ }
+
+ if (
+ cfg.CUSTOM_ELEMENT_HANDLING &&
+ typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements ===
+ 'boolean'
+ ) {
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements =
+ cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
+ }
+
+ if (SAFE_FOR_TEMPLATES) {
+ ALLOW_DATA_ATTR = false;
+ }
+
+ if (RETURN_DOM_FRAGMENT) {
+ RETURN_DOM = true;
+ }
+
+ /* Parse profile info */
+ if (USE_PROFILES) {
+ ALLOWED_TAGS = addToSet({}, [...TAGS.text]);
+ ALLOWED_ATTR = [];
+ if (USE_PROFILES.html === true) {
+ addToSet(ALLOWED_TAGS, TAGS.html);
+ addToSet(ALLOWED_ATTR, ATTRS.html);
+ }
+
+ if (USE_PROFILES.svg === true) {
+ addToSet(ALLOWED_TAGS, TAGS.svg);
+ addToSet(ALLOWED_ATTR, ATTRS.svg);
+ addToSet(ALLOWED_ATTR, ATTRS.xml);
+ }
+
+ if (USE_PROFILES.svgFilters === true) {
+ addToSet(ALLOWED_TAGS, TAGS.svgFilters);
+ addToSet(ALLOWED_ATTR, ATTRS.svg);
+ addToSet(ALLOWED_ATTR, ATTRS.xml);
+ }
+
+ if (USE_PROFILES.mathMl === true) {
+ addToSet(ALLOWED_TAGS, TAGS.mathMl);
+ addToSet(ALLOWED_ATTR, ATTRS.mathMl);
+ addToSet(ALLOWED_ATTR, ATTRS.xml);
+ }
+ }
+
+ /* Merge configuration parameters */
+ if (cfg.ADD_TAGS) {
+ if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
+ }
+
+ addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
+ }
+
+ if (cfg.ADD_ATTR) {
+ if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
+ }
+
+ addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
+ }
+
+ if (cfg.ADD_URI_SAFE_ATTR) {
+ addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
+ }
+
+ if (cfg.FORBID_CONTENTS) {
+ if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
+ FORBID_CONTENTS = clone(FORBID_CONTENTS);
+ }
+
+ addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
+ }
+
+ /* Add #text in case KEEP_CONTENT is set to true */
+ if (KEEP_CONTENT) {
+ ALLOWED_TAGS['#text'] = true;
+ }
+
+ /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
+ if (WHOLE_DOCUMENT) {
+ addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
+ }
+
+ /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
+ if (ALLOWED_TAGS.table) {
+ addToSet(ALLOWED_TAGS, ['tbody']);
+ delete FORBID_TAGS.tbody;
+ }
+
+ if (cfg.TRUSTED_TYPES_POLICY) {
+ if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
+ throw typeErrorCreate(
+ 'TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'
+ );
+ }
+
+ if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
+ throw typeErrorCreate(
+ 'TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'
+ );
+ }
+
+ // Overwrite existing TrustedTypes policy.
+ trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
+
+ // Sign local variables required by `sanitize`.
+ emptyHTML = trustedTypesPolicy.createHTML('');
+ } else {
+ // Uninitialized policy, attempt to initialize the internal dompurify policy.
+ if (trustedTypesPolicy === undefined) {
+ trustedTypesPolicy = _createTrustedTypesPolicy(
+ trustedTypes,
+ currentScript
+ );
+ }
+
+ // If creating the internal policy succeeded sign internal variables.
+ if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
+ emptyHTML = trustedTypesPolicy.createHTML('');
+ }
+ }
+
+ // Prevent further manipulation of configuration.
+ // Not available in IE8, Safari 5, etc.
+ if (freeze) {
+ freeze(cfg);
+ }
+
+ CONFIG = cfg;
+ };
+
+ const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, [
+ 'mi',
+ 'mo',
+ 'mn',
+ 'ms',
+ 'mtext',
+ ]);
+
+ const HTML_INTEGRATION_POINTS = addToSet({}, [
+ 'foreignobject',
+ 'desc',
+ 'title',
+ 'annotation-xml',
+ ]);
+
+ // Certain elements are allowed in both SVG and HTML
+ // namespace. We need to specify them explicitly
+ // so that they don't get erroneously deleted from
+ // HTML namespace.
+ const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, [
+ 'title',
+ 'style',
+ 'font',
+ 'a',
+ 'script',
+ ]);
+
+ /* Keep track of all possible SVG and MathML tags
+ * so that we can perform the namespace checks
+ * correctly. */
+ const ALL_SVG_TAGS = addToSet({}, TAGS.svg);
+ addToSet(ALL_SVG_TAGS, TAGS.svgFilters);
+ addToSet(ALL_SVG_TAGS, TAGS.svgDisallowed);
+
+ const ALL_MATHML_TAGS = addToSet({}, TAGS.mathMl);
+ addToSet(ALL_MATHML_TAGS, TAGS.mathMlDisallowed);
+
+ /**
+ * @param {Element} element a DOM element whose namespace is being checked
+ * @returns {boolean} Return false if the element has a
+ * namespace that a spec-compliant parser would never
+ * return. Return true otherwise.
+ */
+ const _checkValidNamespace = function (element) {
+ let parent = getParentNode(element);
+
+ // In JSDOM, if we're inside shadow DOM, then parentNode
+ // can be null. We just simulate parent in this case.
+ if (!parent || !parent.tagName) {
+ parent = {
+ namespaceURI: NAMESPACE,
+ tagName: 'template',
+ };
+ }
+
+ const tagName = stringToLowerCase(element.tagName);
+ const parentTagName = stringToLowerCase(parent.tagName);
+
+ if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
+ return false;
+ }
+
+ if (element.namespaceURI === SVG_NAMESPACE) {
+ // The only way to switch from HTML namespace to SVG
+ // is via . If it happens via any other tag, then
+ // it should be killed.
+ if (parent.namespaceURI === HTML_NAMESPACE) {
+ return tagName === 'svg';
+ }
+
+ // The only way to switch from MathML to SVG is via`
+ // svg if parent is either or MathML
+ // text integration points.
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
+ return (
+ tagName === 'svg' &&
+ (parentTagName === 'annotation-xml' ||
+ MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
+ );
+ }
+
+ // We only allow elements that are defined in SVG
+ // spec. All others are disallowed in SVG namespace.
+ return Boolean(ALL_SVG_TAGS[tagName]);
+ }
+
+ if (element.namespaceURI === MATHML_NAMESPACE) {
+ // The only way to switch from HTML namespace to MathML
+ // is via . If it happens via any other tag, then
+ // it should be killed.
+ if (parent.namespaceURI === HTML_NAMESPACE) {
+ return tagName === 'math';
+ }
+
+ // The only way to switch from SVG to MathML is via
+ // and HTML integration points
+ if (parent.namespaceURI === SVG_NAMESPACE) {
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
+ }
+
+ // We only allow elements that are defined in MathML
+ // spec. All others are disallowed in MathML namespace.
+ return Boolean(ALL_MATHML_TAGS[tagName]);
+ }
+
+ if (element.namespaceURI === HTML_NAMESPACE) {
+ // The only way to switch from SVG to HTML is via
+ // HTML integration points, and from MathML to HTML
+ // is via MathML text integration points
+ if (
+ parent.namespaceURI === SVG_NAMESPACE &&
+ !HTML_INTEGRATION_POINTS[parentTagName]
+ ) {
+ return false;
+ }
+
+ if (
+ parent.namespaceURI === MATHML_NAMESPACE &&
+ !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
+ ) {
+ return false;
+ }
+
+ // We disallow tags that are specific for MathML
+ // or SVG and should never appear in HTML namespace
+ return (
+ !ALL_MATHML_TAGS[tagName] &&
+ (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
+ );
+ }
+
+ // For XHTML and XML documents that support custom namespaces
+ if (
+ PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&
+ ALLOWED_NAMESPACES[element.namespaceURI]
+ ) {
+ return true;
+ }
+
+ // The code should never reach this place (this means
+ // that the element somehow got namespace that is not
+ // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).
+ // Return false just in case.
+ return false;
+ };
+
+ /**
+ * _forceRemove
+ *
+ * @param {Node} node a DOM node
+ */
+ const _forceRemove = function (node) {
+ arrayPush(DOMPurify.removed, { element: node });
+ try {
+ // eslint-disable-next-line unicorn/prefer-dom-node-remove
+ node.parentNode.removeChild(node);
+ } catch (_) {
+ node.remove();
+ }
+ };
+
+ /**
+ * _removeAttribute
+ *
+ * @param {String} name an Attribute name
+ * @param {Node} node a DOM node
+ */
+ const _removeAttribute = function (name, node) {
+ try {
+ arrayPush(DOMPurify.removed, {
+ attribute: node.getAttributeNode(name),
+ from: node,
+ });
+ } catch (_) {
+ arrayPush(DOMPurify.removed, {
+ attribute: null,
+ from: node,
+ });
+ }
+
+ node.removeAttribute(name);
+
+ // We void attribute values for unremovable "is"" attributes
+ if (name === 'is' && !ALLOWED_ATTR[name]) {
+ if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
+ try {
+ _forceRemove(node);
+ } catch (_) {}
+ } else {
+ try {
+ node.setAttribute(name, '');
+ } catch (_) {}
+ }
+ }
+ };
+
+ /**
+ * _initDocument
+ *
+ * @param {String} dirty a string of dirty markup
+ * @return {Document} a DOM, filled with the dirty markup
+ */
+ const _initDocument = function (dirty) {
+ /* Create a HTML document */
+ let doc = null;
+ let leadingWhitespace = null;
+
+ if (FORCE_BODY) {
+ dirty = ' ' + dirty;
+ } else {
+ /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
+ const matches = stringMatch(dirty, /^[\r\n\t ]+/);
+ leadingWhitespace = matches && matches[0];
+ }
+
+ if (
+ PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&
+ NAMESPACE === HTML_NAMESPACE
+ ) {
+ // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
+ dirty =
+ '' +
+ dirty +
+ '';
+ }
+
+ const dirtyPayload = trustedTypesPolicy
+ ? trustedTypesPolicy.createHTML(dirty)
+ : dirty;
+ /*
+ * Use the DOMParser API by default, fallback later if needs be
+ * DOMParser not work for svg when has multiple root element.
+ */
+ if (NAMESPACE === HTML_NAMESPACE) {
+ try {
+ doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
+ } catch (_) {}
+ }
+
+ /* Use createHTMLDocument in case DOMParser is not available */
+ if (!doc || !doc.documentElement) {
+ doc = implementation.createDocument(NAMESPACE, 'template', null);
+ try {
+ doc.documentElement.innerHTML = IS_EMPTY_INPUT
+ ? emptyHTML
+ : dirtyPayload;
+ } catch (_) {
+ // Syntax error if dirtyPayload is invalid xml
+ }
+ }
+
+ const body = doc.body || doc.documentElement;
+
+ if (dirty && leadingWhitespace) {
+ body.insertBefore(
+ document.createTextNode(leadingWhitespace),
+ body.childNodes[0] || null
+ );
+ }
+
+ /* Work on whole document or just its body */
+ if (NAMESPACE === HTML_NAMESPACE) {
+ return getElementsByTagName.call(
+ doc,
+ WHOLE_DOCUMENT ? 'html' : 'body'
+ )[0];
+ }
+
+ return WHOLE_DOCUMENT ? doc.documentElement : body;
+ };
+
+ /**
+ * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
+ *
+ * @param {Node} root The root element or node to start traversing on.
+ * @return {NodeIterator} The created NodeIterator
+ */
+ const _createNodeIterator = function (root) {
+ return createNodeIterator.call(
+ root.ownerDocument || root,
+ root,
+ // eslint-disable-next-line no-bitwise
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT,
+ null
+ );
+ };
+
+ /**
+ * _isClobbered
+ *
+ * @param {Node} elm element to check for clobbering attacks
+ * @return {Boolean} true if clobbered, false if safe
+ */
+ const _isClobbered = function (elm) {
+ return (
+ elm instanceof HTMLFormElement &&
+ (typeof elm.nodeName !== 'string' ||
+ typeof elm.textContent !== 'string' ||
+ typeof elm.removeChild !== 'function' ||
+ !(elm.attributes instanceof NamedNodeMap) ||
+ typeof elm.removeAttribute !== 'function' ||
+ typeof elm.setAttribute !== 'function' ||
+ typeof elm.namespaceURI !== 'string' ||
+ typeof elm.insertBefore !== 'function' ||
+ typeof elm.hasChildNodes !== 'function')
+ );
+ };
+
+ /**
+ * Checks whether the given object is a DOM node.
+ *
+ * @param {Node} object object to check whether it's a DOM node
+ * @return {Boolean} true is object is a DOM node
+ */
+ const _isNode = function (object) {
+ return typeof Node === 'function' && object instanceof Node;
+ };
+
+ /**
+ * _executeHook
+ * Execute user configurable hooks
+ *
+ * @param {String} entryPoint Name of the hook's entry point
+ * @param {Node} currentNode node to work on with the hook
+ * @param {Object} data additional hook parameters
+ */
+ const _executeHook = function (entryPoint, currentNode, data) {
+ if (!hooks[entryPoint]) {
+ return;
+ }
+
+ arrayForEach(hooks[entryPoint], (hook) => {
+ hook.call(DOMPurify, currentNode, data, CONFIG);
+ });
+ };
+
+ /**
+ * _sanitizeElements
+ *
+ * @protect nodeName
+ * @protect textContent
+ * @protect removeChild
+ *
+ * @param {Node} currentNode to check for permission to exist
+ * @return {Boolean} true if node was killed, false if left alive
+ */
+ const _sanitizeElements = function (currentNode) {
+ let content = null;
+
+ /* Execute a hook if present */
+ _executeHook('beforeSanitizeElements', currentNode, null);
+
+ /* Check if element is clobbered or can clobber */
+ if (_isClobbered(currentNode)) {
+ _forceRemove(currentNode);
+ return true;
+ }
+
+ /* Now let's check the element's type and name */
+ const tagName = transformCaseFunc(currentNode.nodeName);
+
+ /* Execute a hook if present */
+ _executeHook('uponSanitizeElement', currentNode, {
+ tagName,
+ allowedTags: ALLOWED_TAGS,
+ });
+
+ /* Detect mXSS attempts abusing namespace confusion */
+ if (
+ currentNode.hasChildNodes() &&
+ !_isNode(currentNode.firstElementChild) &&
+ regExpTest(/<[/\w]/g, currentNode.innerHTML) &&
+ regExpTest(/<[/\w]/g, currentNode.textContent)
+ ) {
+ _forceRemove(currentNode);
+ return true;
+ }
+
+ /* Remove element if anything forbids its presence */
+ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
+ /* Check if we have a custom element to handle */
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
+ if (
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
+ regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
+ ) {
+ return false;
+ }
+
+ if (
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
+ ) {
+ return false;
+ }
+ }
+
+ /* Keep content except for bad-listed elements */
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
+ const parentNode = getParentNode(currentNode) || currentNode.parentNode;
+ const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
+
+ if (childNodes && parentNode) {
+ const childCount = childNodes.length;
+
+ for (let i = childCount - 1; i >= 0; --i) {
+ parentNode.insertBefore(
+ cloneNode(childNodes[i], true),
+ getNextSibling(currentNode)
+ );
+ }
+ }
+ }
+
+ _forceRemove(currentNode);
+ return true;
+ }
+
+ /* Check whether element has a valid namespace */
+ if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
+ _forceRemove(currentNode);
+ return true;
+ }
+
+ /* Make sure that older browsers don't get fallback-tag mXSS */
+ if (
+ (tagName === 'noscript' ||
+ tagName === 'noembed' ||
+ tagName === 'noframes') &&
+ regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)
+ ) {
+ _forceRemove(currentNode);
+ return true;
+ }
+
+ /* Sanitize element content to be template-safe */
+ if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
+ /* Get the element's text content */
+ content = currentNode.textContent;
+
+ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {
+ content = stringReplace(content, expr, ' ');
+ });
+
+ if (currentNode.textContent !== content) {
+ arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
+ currentNode.textContent = content;
+ }
+ }
+
+ /* Execute a hook if present */
+ _executeHook('afterSanitizeElements', currentNode, null);
+
+ return false;
+ };
+
+ /**
+ * _isValidAttribute
+ *
+ * @param {string} lcTag Lowercase tag name of containing element.
+ * @param {string} lcName Lowercase attribute name.
+ * @param {string} value Attribute value.
+ * @return {Boolean} Returns true if `value` is valid, otherwise false.
+ */
+ // eslint-disable-next-line complexity
+ const _isValidAttribute = function (lcTag, lcName, value) {
+ /* Make sure attribute cannot clobber */
+ if (
+ SANITIZE_DOM &&
+ (lcName === 'id' || lcName === 'name') &&
+ (value in document || value in formElement)
+ ) {
+ return false;
+ }
+
+ /* Allow valid data-* attributes: At least one character after "-"
+ (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
+ XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
+ We don't need to check the value; it's always URI safe. */
+ if (
+ ALLOW_DATA_ATTR &&
+ !FORBID_ATTR[lcName] &&
+ regExpTest(DATA_ATTR, lcName)
+ ) {
+ // This attribute is safe
+ } else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {
+ // This attribute is safe
+ /* Otherwise, check the name is permitted */
+ } else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
+ if (
+ // First condition does a very basic check if a) it's basically a valid custom element tagname AND
+ // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
+ // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
+ (_isBasicCustomElement(lcTag) &&
+ ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
+ regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag)) ||
+ (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag))) &&
+ ((CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp &&
+ regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName)) ||
+ (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function &&
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)))) ||
+ // Alternative, second condition checks if it's an `is`-attribute, AND
+ // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
+ (lcName === 'is' &&
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements &&
+ ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
+ regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)) ||
+ (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))))
+ ) {
+ // If user has supplied a regexp or function in CUSTOM_ELEMENT_HANDLING.tagNameCheck, we need to also allow derived custom elements using the same tagName test.
+ // Additionally, we need to allow attributes passing the CUSTOM_ELEMENT_HANDLING.attributeNameCheck user has configured, as custom elements can define these at their own discretion.
+ } else {
+ return false;
+ }
+ /* Check value is safe. First, is attr inert? If so, is safe */
+ } else if (URI_SAFE_ATTRIBUTES[lcName]) {
+ // This attribute is safe
+ /* Check no script, data or unknown possibly unsafe URI
+ unless we know URI values are safe for that attribute */
+ } else if (
+ regExpTest(IS_ALLOWED_URI, stringReplace(value, ATTR_WHITESPACE, ''))
+ ) {
+ // This attribute is safe
+ /* Keep image data URIs alive if src/xlink:href is allowed */
+ /* Further prevent gadget XSS for dynamically built script tags */
+ } else if (
+ (lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') &&
+ lcTag !== 'script' &&
+ stringIndexOf(value, 'data:') === 0 &&
+ DATA_URI_TAGS[lcTag]
+ ) {
+ // This attribute is safe
+ /* Allow unknown protocols: This provides support for links that
+ are handled by protocol handlers which may be unknown ahead of
+ time, e.g. fb:, spotify: */
+ } else if (
+ ALLOW_UNKNOWN_PROTOCOLS &&
+ !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))
+ ) {
+ // This attribute is safe
+ /* Check for binary attributes */
+ } else if (value) {
+ return false;
+ } else {
+ // Binary attributes are safe at this point
+ /* Anything else, presume unsafe, do not add it back */
+ }
+
+ return true;
+ };
+
+ /**
+ * _isBasicCustomElement
+ * checks if at least one dash is included in tagName, and it's not the first char
+ * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
+ *
+ * @param {string} tagName name of the tag of the node to sanitize
+ * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
+ */
+ const _isBasicCustomElement = function (tagName) {
+ return tagName.indexOf('-') > 0;
+ };
+
+ /**
+ * _sanitizeAttributes
+ *
+ * @protect attributes
+ * @protect nodeName
+ * @protect removeAttribute
+ * @protect setAttribute
+ *
+ * @param {Node} currentNode to sanitize
+ */
+ const _sanitizeAttributes = function (currentNode) {
+ /* Execute a hook if present */
+ _executeHook('beforeSanitizeAttributes', currentNode, null);
+
+ const { attributes } = currentNode;
+
+ /* Check if we have attributes; if not we might have a text node */
+ if (!attributes) {
+ return;
+ }
+
+ const hookEvent = {
+ attrName: '',
+ attrValue: '',
+ keepAttr: true,
+ allowedAttributes: ALLOWED_ATTR,
+ };
+ let l = attributes.length;
+
+ /* Go backwards over all attributes; safely remove bad ones */
+ while (l--) {
+ const attr = attributes[l];
+ const { name, namespaceURI, value: attrValue } = attr;
+ const lcName = transformCaseFunc(name);
+
+ let value = name === 'value' ? attrValue : stringTrim(attrValue);
+
+ /* Execute a hook if present */
+ hookEvent.attrName = lcName;
+ hookEvent.attrValue = value;
+ hookEvent.keepAttr = true;
+ hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
+ _executeHook('uponSanitizeAttribute', currentNode, hookEvent);
+ value = hookEvent.attrValue;
+ /* Did the hooks approve of the attribute? */
+ if (hookEvent.forceKeepAttr) {
+ continue;
+ }
+
+ /* Remove attribute */
+ _removeAttribute(name, currentNode);
+
+ /* Did the hooks approve of the attribute? */
+ if (!hookEvent.keepAttr) {
+ continue;
+ }
+
+ /* Work around a security issue in jQuery 3.0 */
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
+ _removeAttribute(name, currentNode);
+ continue;
+ }
+
+ /* Sanitize attribute content to be template-safe */
+ if (SAFE_FOR_TEMPLATES) {
+ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {
+ value = stringReplace(value, expr, ' ');
+ });
+ }
+
+ /* Is `value` valid for this attribute? */
+ const lcTag = transformCaseFunc(currentNode.nodeName);
+ if (!_isValidAttribute(lcTag, lcName, value)) {
+ continue;
+ }
+
+ /* Full DOM Clobbering protection via namespace isolation,
+ * Prefix id and name attributes with `user-content-`
+ */
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
+ // Remove the attribute with this value
+ _removeAttribute(name, currentNode);
+
+ // Prefix the value and later re-create the attribute with the sanitized value
+ value = SANITIZE_NAMED_PROPS_PREFIX + value;
+ }
+
+ /* Handle attributes that require Trusted Types */
+ if (
+ trustedTypesPolicy &&
+ typeof trustedTypes === 'object' &&
+ typeof trustedTypes.getAttributeType === 'function'
+ ) {
+ if (namespaceURI) {
+ /* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */
+ } else {
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
+ case 'TrustedHTML': {
+ value = trustedTypesPolicy.createHTML(value);
+ break;
+ }
+
+ case 'TrustedScriptURL': {
+ value = trustedTypesPolicy.createScriptURL(value);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+
+ /* Handle invalid data-* attribute set by try-catching it */
+ try {
+ if (namespaceURI) {
+ currentNode.setAttributeNS(namespaceURI, name, value);
+ } else {
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
+ currentNode.setAttribute(name, value);
+ }
+
+ arrayPop(DOMPurify.removed);
+ } catch (_) {}
+ }
+
+ /* Execute a hook if present */
+ _executeHook('afterSanitizeAttributes', currentNode, null);
+ };
+
+ /**
+ * _sanitizeShadowDOM
+ *
+ * @param {DocumentFragment} fragment to iterate over recursively
+ */
+ const _sanitizeShadowDOM = function (fragment) {
+ let shadowNode = null;
+ const shadowIterator = _createNodeIterator(fragment);
+
+ /* Execute a hook if present */
+ _executeHook('beforeSanitizeShadowDOM', fragment, null);
+
+ while ((shadowNode = shadowIterator.nextNode())) {
+ /* Execute a hook if present */
+ _executeHook('uponSanitizeShadowNode', shadowNode, null);
+
+ /* Sanitize tags and elements */
+ if (_sanitizeElements(shadowNode)) {
+ continue;
+ }
+
+ /* Deep shadow DOM detected */
+ if (shadowNode.content instanceof DocumentFragment) {
+ _sanitizeShadowDOM(shadowNode.content);
+ }
+
+ /* Check attributes, sanitize if necessary */
+ _sanitizeAttributes(shadowNode);
+ }
+
+ /* Execute a hook if present */
+ _executeHook('afterSanitizeShadowDOM', fragment, null);
+ };
+
+ /**
+ * Sanitize
+ * Public method providing core sanitation functionality
+ *
+ * @param {String|Node} dirty string or DOM node
+ * @param {Object} cfg object
+ */
+ // eslint-disable-next-line complexity
+ DOMPurify.sanitize = function (dirty, cfg = {}) {
+ let body = null;
+ let importedNode = null;
+ let currentNode = null;
+ let returnNode = null;
+ /* Make sure we have a string to sanitize.
+ DO NOT return early, as this will return the wrong type if
+ the user has requested a DOM object rather than a string */
+ IS_EMPTY_INPUT = !dirty;
+ if (IS_EMPTY_INPUT) {
+ dirty = '';
+ }
+
+ /* Stringify, in case dirty is an object */
+ if (typeof dirty !== 'string' && !_isNode(dirty)) {
+ if (typeof dirty.toString === 'function') {
+ dirty = dirty.toString();
+ if (typeof dirty !== 'string') {
+ throw typeErrorCreate('dirty is not a string, aborting');
+ }
+ } else {
+ throw typeErrorCreate('toString is not a function');
+ }
+ }
+
+ /* Return dirty HTML if DOMPurify cannot run */
+ if (!DOMPurify.isSupported) {
+ return dirty;
+ }
+
+ /* Assign config vars */
+ if (!SET_CONFIG) {
+ _parseConfig(cfg);
+ }
+
+ /* Clean up removed elements */
+ DOMPurify.removed = [];
+
+ /* Check if dirty is correctly typed for IN_PLACE */
+ if (typeof dirty === 'string') {
+ IN_PLACE = false;
+ }
+
+ if (IN_PLACE) {
+ /* Do some early pre-sanitization to avoid unsafe root nodes */
+ if (dirty.nodeName) {
+ const tagName = transformCaseFunc(dirty.nodeName);
+ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
+ throw typeErrorCreate(
+ 'root node is forbidden and cannot be sanitized in-place'
+ );
+ }
+ }
+ } else if (dirty instanceof Node) {
+ /* If dirty is a DOM element, append to an empty document to avoid
+ elements being stripped by the parser */
+ body = _initDocument('');
+ importedNode = body.ownerDocument.importNode(dirty, true);
+ if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {
+ /* Node is already a body, use as is */
+ body = importedNode;
+ } else if (importedNode.nodeName === 'HTML') {
+ body = importedNode;
+ } else {
+ // eslint-disable-next-line unicorn/prefer-dom-node-append
+ body.appendChild(importedNode);
+ }
+ } else {
+ /* Exit directly if we have nothing to do */
+ if (
+ !RETURN_DOM &&
+ !SAFE_FOR_TEMPLATES &&
+ !WHOLE_DOCUMENT &&
+ // eslint-disable-next-line unicorn/prefer-includes
+ dirty.indexOf('<') === -1
+ ) {
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE
+ ? trustedTypesPolicy.createHTML(dirty)
+ : dirty;
+ }
+
+ /* Initialize the document to work on */
+ body = _initDocument(dirty);
+
+ /* Check we have a DOM node from the data */
+ if (!body) {
+ return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';
+ }
+ }
+
+ /* Remove first element node (ours) if FORCE_BODY is set */
+ if (body && FORCE_BODY) {
+ _forceRemove(body.firstChild);
+ }
+
+ /* Get node iterator */
+ const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
+
+ /* Now start iterating over the created document */
+ while ((currentNode = nodeIterator.nextNode())) {
+ /* Sanitize tags and elements */
+ if (_sanitizeElements(currentNode)) {
+ continue;
+ }
+
+ /* Shadow DOM detected, sanitize it */
+ if (currentNode.content instanceof DocumentFragment) {
+ _sanitizeShadowDOM(currentNode.content);
+ }
+
+ /* Check attributes, sanitize if necessary */
+ _sanitizeAttributes(currentNode);
+ }
+
+ /* If we sanitized `dirty` in-place, return it. */
+ if (IN_PLACE) {
+ return dirty;
+ }
+
+ /* Return sanitized string or DOM */
+ if (RETURN_DOM) {
+ if (RETURN_DOM_FRAGMENT) {
+ returnNode = createDocumentFragment.call(body.ownerDocument);
+
+ while (body.firstChild) {
+ // eslint-disable-next-line unicorn/prefer-dom-node-append
+ returnNode.appendChild(body.firstChild);
+ }
+ } else {
+ returnNode = body;
+ }
+
+ if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
+ /*
+ AdoptNode() is not used because internal state is not reset
+ (e.g. the past names map of a HTMLFormElement), this is safe
+ in theory but we would rather not risk another attack vector.
+ The state that is cloned by importNode() is explicitly defined
+ by the specs.
+ */
+ returnNode = importNode.call(originalDocument, returnNode, true);
+ }
+
+ return returnNode;
+ }
+
+ let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
+
+ /* Serialize doctype if allowed */
+ if (
+ WHOLE_DOCUMENT &&
+ ALLOWED_TAGS['!doctype'] &&
+ body.ownerDocument &&
+ body.ownerDocument.doctype &&
+ body.ownerDocument.doctype.name &&
+ regExpTest(EXPRESSIONS.DOCTYPE_NAME, body.ownerDocument.doctype.name)
+ ) {
+ serializedHTML =
+ '\n' + serializedHTML;
+ }
+
+ /* Sanitize final string template-safe */
+ if (SAFE_FOR_TEMPLATES) {
+ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {
+ serializedHTML = stringReplace(serializedHTML, expr, ' ');
+ });
+ }
+
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE
+ ? trustedTypesPolicy.createHTML(serializedHTML)
+ : serializedHTML;
+ };
+
+ /**
+ * Public method to set the configuration once
+ * setConfig
+ *
+ * @param {Object} cfg configuration object
+ */
+ DOMPurify.setConfig = function (cfg = {}) {
+ _parseConfig(cfg);
+ SET_CONFIG = true;
+ };
+
+ /**
+ * Public method to remove the configuration
+ * clearConfig
+ *
+ */
+ DOMPurify.clearConfig = function () {
+ CONFIG = null;
+ SET_CONFIG = false;
+ };
+
+ /**
+ * Public method to check if an attribute value is valid.
+ * Uses last set config, if any. Otherwise, uses config defaults.
+ * isValidAttribute
+ *
+ * @param {String} tag Tag name of containing element.
+ * @param {String} attr Attribute name.
+ * @param {String} value Attribute value.
+ * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.
+ */
+ DOMPurify.isValidAttribute = function (tag, attr, value) {
+ /* Initialize shared config vars if necessary. */
+ if (!CONFIG) {
+ _parseConfig({});
+ }
+
+ const lcTag = transformCaseFunc(tag);
+ const lcName = transformCaseFunc(attr);
+ return _isValidAttribute(lcTag, lcName, value);
+ };
+
+ /**
+ * AddHook
+ * Public method to add DOMPurify hooks
+ *
+ * @param {String} entryPoint entry point for the hook to add
+ * @param {Function} hookFunction function to execute
+ */
+ DOMPurify.addHook = function (entryPoint, hookFunction) {
+ if (typeof hookFunction !== 'function') {
+ return;
+ }
+
+ hooks[entryPoint] = hooks[entryPoint] || [];
+ arrayPush(hooks[entryPoint], hookFunction);
+ };
+
+ /**
+ * RemoveHook
+ * Public method to remove a DOMPurify hook at a given entryPoint
+ * (pops it from the stack of hooks if more are present)
+ *
+ * @param {String} entryPoint entry point for the hook to remove
+ * @return {Function} removed(popped) hook
+ */
+ DOMPurify.removeHook = function (entryPoint) {
+ if (hooks[entryPoint]) {
+ return arrayPop(hooks[entryPoint]);
+ }
+ };
+
+ /**
+ * RemoveHooks
+ * Public method to remove all DOMPurify hooks at a given entryPoint
+ *
+ * @param {String} entryPoint entry point for the hooks to remove
+ */
+ DOMPurify.removeHooks = function (entryPoint) {
+ if (hooks[entryPoint]) {
+ hooks[entryPoint] = [];
+ }
+ };
+
+ /**
+ * RemoveAllHooks
+ * Public method to remove all DOMPurify hooks
+ */
+ DOMPurify.removeAllHooks = function () {
+ hooks = {};
+ };
+
+ return DOMPurify;
+}
+
+export default createDOMPurify();
diff --git a/src/ui/static/js/utils/purify/src/regexp.js b/src/ui/static/js/utils/purify/src/regexp.js
new file mode 100644
index 000000000..3c9f75fef
--- /dev/null
+++ b/src/ui/static/js/utils/purify/src/regexp.js
@@ -0,0 +1,16 @@
+import { seal } from './utils.js';
+
+// eslint-disable-next-line unicorn/better-regex
+export const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
+export const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
+export const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm);
+export const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
+export const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
+export const IS_ALLOWED_URI = seal(
+ /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
+);
+export const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
+export const ATTR_WHITESPACE = seal(
+ /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
+);
+export const DOCTYPE_NAME = seal(/^html$/i);
diff --git a/src/ui/static/js/utils/purify/src/tags.js b/src/ui/static/js/utils/purify/src/tags.js
new file mode 100644
index 000000000..853c7308a
--- /dev/null
+++ b/src/ui/static/js/utils/purify/src/tags.js
@@ -0,0 +1,280 @@
+import { freeze } from './utils.js';
+
+export const html = freeze([
+ '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',
+ 'section',
+ 'select',
+ 'shadow',
+ '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',
+]);
+
+// SVG
+export const svg = freeze([
+ 'svg',
+ 'a',
+ 'altglyph',
+ 'altglyphdef',
+ 'altglyphitem',
+ 'animatecolor',
+ 'animatemotion',
+ 'animatetransform',
+ 'circle',
+ 'clippath',
+ 'defs',
+ 'desc',
+ 'ellipse',
+ 'filter',
+ 'font',
+ 'g',
+ 'glyph',
+ 'glyphref',
+ 'hkern',
+ 'image',
+ 'line',
+ 'lineargradient',
+ 'marker',
+ 'mask',
+ 'metadata',
+ 'mpath',
+ 'path',
+ 'pattern',
+ 'polygon',
+ 'polyline',
+ 'radialgradient',
+ 'rect',
+ 'stop',
+ 'style',
+ 'switch',
+ 'symbol',
+ 'text',
+ 'textpath',
+ 'title',
+ 'tref',
+ 'tspan',
+ 'view',
+ 'vkern',
+]);
+
+export const svgFilters = freeze([
+ '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',
+]);
+
+// List of SVG elements that are disallowed by default.
+// We still need to know them so that we can do namespace
+// checks properly in case one wants to add them to
+// allow-list.
+export const svgDisallowed = freeze([
+ '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',
+]);
+
+export const mathMl = freeze([
+ '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',
+]);
+
+// Similarly to SVG, we want to know all MathML elements,
+// even those that we disallow by default.
+export const mathMlDisallowed = freeze([
+ 'maction',
+ 'maligngroup',
+ 'malignmark',
+ 'mlongdiv',
+ 'mscarries',
+ 'mscarry',
+ 'msgroup',
+ 'mstack',
+ 'msline',
+ 'msrow',
+ 'semantics',
+ 'annotation',
+ 'annotation-xml',
+ 'mprescripts',
+ 'none',
+]);
+
+export const text = freeze(['#text']);
diff --git a/src/ui/static/js/utils/purify/src/utils.js b/src/ui/static/js/utils/purify/src/utils.js
new file mode 100644
index 000000000..a658514f8
--- /dev/null
+++ b/src/ui/static/js/utils/purify/src/utils.js
@@ -0,0 +1,193 @@
+const {
+ entries,
+ setPrototypeOf,
+ isFrozen,
+ getPrototypeOf,
+ getOwnPropertyDescriptor,
+} = Object;
+
+let { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports
+let { apply, construct } = typeof Reflect !== 'undefined' && Reflect;
+
+if (!freeze) {
+ freeze = function (x) {
+ return x;
+ };
+}
+
+if (!seal) {
+ seal = function (x) {
+ return x;
+ };
+}
+
+if (!apply) {
+ apply = function (fun, thisValue, args) {
+ return fun.apply(thisValue, args);
+ };
+}
+
+if (!construct) {
+ construct = function (Func, args) {
+ return new Func(...args);
+ };
+}
+
+const arrayForEach = unapply(Array.prototype.forEach);
+const arrayIndexOf = unapply(Array.prototype.indexOf);
+const arrayPop = unapply(Array.prototype.pop);
+const arrayPush = unapply(Array.prototype.push);
+const arraySlice = unapply(Array.prototype.slice);
+
+const stringToLowerCase = unapply(String.prototype.toLowerCase);
+const stringToString = unapply(String.prototype.toString);
+const stringMatch = unapply(String.prototype.match);
+const stringReplace = unapply(String.prototype.replace);
+const stringIndexOf = unapply(String.prototype.indexOf);
+const stringTrim = unapply(String.prototype.trim);
+
+const regExpTest = unapply(RegExp.prototype.test);
+
+const typeErrorCreate = unconstruct(TypeError);
+
+/**
+ * Creates a new function that calls the given function with a specified thisArg and arguments.
+ *
+ * @param {Function} func - The function to be wrapped and called.
+ * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.
+ */
+function unapply(func) {
+ return (thisArg, ...args) => apply(func, thisArg, args);
+}
+
+/**
+ * Creates a new function that constructs an instance of the given constructor function with the provided arguments.
+ *
+ * @param {Function} func - The constructor function to be wrapped and called.
+ * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.
+ */
+function unconstruct(func) {
+ return (...args) => construct(func, args);
+}
+
+/**
+ * Add properties to a lookup table
+ *
+ * @param {Object} set - The set to which elements will be added.
+ * @param {Array} array - The array containing elements to be added to the set.
+ * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.
+ * @returns {Object} The modified set with added elements.
+ */
+function addToSet(set, array, transformCaseFunc = stringToLowerCase) {
+ if (setPrototypeOf) {
+ // Make 'in' and truthy checks like Boolean(set.constructor)
+ // independent of any properties defined on Object.prototype.
+ // Prevent prototype setters from intercepting set as a this value.
+ setPrototypeOf(set, null);
+ }
+
+ let l = array.length;
+ while (l--) {
+ let element = array[l];
+ if (typeof element === 'string') {
+ const lcElement = transformCaseFunc(element);
+ if (lcElement !== element) {
+ // Config presets (e.g. tags.js, attrs.js) are immutable.
+ if (!isFrozen(array)) {
+ array[l] = lcElement;
+ }
+
+ element = lcElement;
+ }
+ }
+
+ set[element] = true;
+ }
+
+ return set;
+}
+
+/**
+ * Shallow clone an object
+ *
+ * @param {Object} object - The object to be cloned.
+ * @returns {Object} A new object that copies the original.
+ */
+export function clone(object) {
+ const newObject = create(null);
+
+ for (const [property, value] of entries(object)) {
+ if (getOwnPropertyDescriptor(object, property) !== undefined) {
+ newObject[property] = value;
+ }
+ }
+
+ return newObject;
+}
+
+/**
+ * This method automatically checks if the prop is function or getter and behaves accordingly.
+ *
+ * @param {Object} object - The object to look up the getter function in its prototype chain.
+ * @param {String} prop - The property name for which to find the getter function.
+ * @returns {Function} The getter function found in the prototype chain or a fallback function.
+ */
+function lookupGetter(object, prop) {
+ while (object !== null) {
+ const desc = getOwnPropertyDescriptor(object, prop);
+
+ if (desc) {
+ if (desc.get) {
+ return unapply(desc.get);
+ }
+
+ if (typeof desc.value === 'function') {
+ return unapply(desc.value);
+ }
+ }
+
+ object = getPrototypeOf(object);
+ }
+
+ function fallbackValue(element) {
+ console.warn('fallback value for', element);
+ return null;
+ }
+
+ return fallbackValue;
+}
+
+export {
+ // Array
+ arrayForEach,
+ arrayIndexOf,
+ arrayPop,
+ arrayPush,
+ arraySlice,
+ // Object
+ entries,
+ freeze,
+ getPrototypeOf,
+ getOwnPropertyDescriptor,
+ isFrozen,
+ setPrototypeOf,
+ seal,
+ create,
+ // RegExp
+ regExpTest,
+ // String
+ stringIndexOf,
+ stringMatch,
+ stringReplace,
+ stringToLowerCase,
+ stringToString,
+ stringTrim,
+ // Errors
+ typeErrorCreate,
+ // Other
+ lookupGetter,
+ addToSet,
+ // Reflect
+ unapply,
+ unconstruct,
+};
diff --git a/src/ui/static/js/utils/settings.js b/src/ui/static/js/utils/settings.js
index de107d222..b7d8c2c63 100644
--- a/src/ui/static/js/utils/settings.js
+++ b/src/ui/static/js/utils/settings.js
@@ -1,239 +1,239 @@
-class Popover {
- constructor() {
- this.init();
- }
-
- init() {
- window.addEventListener("pointerover", (e) => {
- //POPOVER LOGIC
- try {
- if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
- this.showPopover(e.target);
- }
- } catch (err) {}
- });
-
- window.addEventListener("pointerout", (e) => {
- //POPOVER LOGIC
- try {
- if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
- this.hidePopover(e.target);
- }
- } catch (err) {}
- });
- }
-
- showPopover(el) {
- const btn = el.closest("svg");
- //toggle curr popover
- const popover = btn.parentElement.querySelector(`[data-popover-content]`);
- popover.classList.remove("hidden");
- }
-
- hidePopover(el) {
- const btn = el.closest("svg");
- //toggle curr popover
- const popover = btn.parentElement.querySelector(`[data-popover-content]`);
- popover.classList.add("hidden");
- }
-}
-
-class Tabs {
- constructor() {
- this.init();
- }
-
- init() {
- window.addEventListener("click", (e) => {
- try {
- if (
- e.target.closest("button").hasAttribute("data-tab-handler") ||
- e.target.closest("button").hasAttribute("data-tab-handler-mobile")
- ) {
- //get needed data
- const tab = e.target.closest("button");
- const tabAtt =
- tab.getAttribute("data-tab-handler") ||
- tab.getAttribute("data-tab-handler-mobile");
- const container = tab.closest("div[data-service-content]");
- // change style
- this.resetTabsStyle(container);
- this.highlightClicked(container, tabAtt);
- //show content
- this.hideAllSettings(container);
- this.showSettingClicked(container, tabAtt);
- //close dropdown and change btn textcontent on mobile
- this.setDropBtnText(container, tabAtt);
- this.closeDropdown(container);
- }
- } catch (err) {}
-
- try {
- if (e.target.closest("button").hasAttribute("data-tab-dropdown-btn")) {
- const dropBtn = e.target.closest("button");
- const container = dropBtn.closest("div[data-service-content]");
- this.toggleDropdown(container);
- }
- } catch (err) {}
- });
- }
-
- resetTabsStyle(container) {
- //reset desktop style
- const tabsDesktop = container.querySelectorAll("button[data-tab-handler]");
- tabsDesktop.forEach((tab) => {
- tab.classList.remove("active");
- });
- //reset mobile style
- const tabsMobile = container.querySelectorAll(
- "button[data-tab-handler-mobile]"
- );
- tabsMobile.forEach((tab) => {
- tab.classList.remove("active");
- });
- }
-
- highlightClicked(container, tabAtt) {
- //desktop case
- const tabDesktop = container.querySelector(
- `button[data-tab-handler='${tabAtt}']`
- );
- tabDesktop.classList.add("active");
-
- //mobile case
- const tabMobile = container.querySelector(
- `button[data-tab-handler-mobile='${tabAtt}']`
- );
- tabMobile.classList.add("active");
- }
-
- hideAllSettings(container) {
- const plugins = container.querySelectorAll("[data-plugin-item]");
- plugins.forEach((plugin) => {
- plugin.classList.add("hidden");
- });
- }
-
- showSettingClicked(container, tabAtt) {
- const plugin = container.querySelector(`[data-plugin-item='${tabAtt}']`);
- plugin.classList.remove("hidden");
- }
-
- setDropBtnText(container, tabAtt) {
- const dropBtn = container.querySelector("[data-tab-dropdown-btn]");
- dropBtn.querySelector("span").textContent = tabAtt;
- }
-
- closeDropdown(container) {
- const dropdown = container.querySelector("[data-tab-dropdown]");
- dropdown.classList.add("hidden");
- dropdown.classList.remove("flex");
- }
-
- toggleDropdown(container) {
- const dropdown = container.querySelector("[data-tab-dropdown]");
- dropdown.classList.toggle("hidden");
- dropdown.classList.toggle("flex");
- }
-}
-
-class FormatValue {
- constructor() {
- this.inputs = document.querySelectorAll("input");
- this.init();
- }
-
- init() {
- this.inputs.forEach((inp) => {
- try {
- inp.setAttribute("value", inp.getAttribute("value").trim());
- inp.value = inp.value.trim();
- } catch (err) {}
- });
- }
-}
-
-class FilterSettings {
- constructor(inputID, container) {
- this.input = document.querySelector(`input#${inputID}`);
- //DESKTOP
- this.container = document.querySelector(container);
- this.deskTabs = this.container.querySelectorAll(`[data-tab-handler]`);
- this.init();
- }
-
- init() {
- this.input.addEventListener("input", () => {
- this.resetFilter();
- //get inp format
- const inpValue = this.input.value.trim().toLowerCase();
- //loop all tabs
- this.deskTabs.forEach((tab) => {
- //get settings of tabs except multiples
- const settings = this.getSettingsFromTab(tab);
-
- //compare total count to currCount to determine
- //if tabs need to be hidden
- const settingCount = settings.length;
- let hiddenCount = 0;
- settings.forEach((setting) => {
- try {
- const title = setting
- .querySelector("h5")
- .textContent.trim()
- .toLowerCase();
- if (!title.includes(inpValue)) {
- setting.classList.add("hidden");
- hiddenCount++;
- }
- } catch (err) {}
- });
- //case no setting match, hidden tab and content
- if (settingCount === hiddenCount) {
- const tabName = tab.getAttribute(`data-tab-handler`);
- //hide mobile and desk tabs
- tab.classList.add("hidden");
- this.container
- .querySelector(`[data-tab-handler-mobile="${tabName}"]`)
- .classList.add("hidden");
- this.container
- .querySelector(`[data-plugin-item=${tabName}]`)
- .querySelector("[data-setting-header]")
- .classList.add("hidden");
- }
- });
- });
- }
-
- resetFilter() {
- this.deskTabs.forEach((tab) => {
- const tabName = tab.getAttribute(`data-tab-handler`);
- //hide mobile and desk tabs
- tab.classList.remove("hidden");
- this.container
- .querySelector(`[data-tab-handler-mobile="${tabName}"]`)
- .classList.remove("hidden");
- this.container
- .querySelector(`[data-plugin-item=${tabName}]`)
- .querySelector("[data-setting-header]")
- .classList.remove("hidden");
- const settings = this.getSettingsFromTab(tab);
- settings.forEach((setting) => {
- setting.classList.remove("hidden");
- });
- });
- }
-
- getSettingsFromTab(tabEl) {
- const tabName = tabEl.getAttribute(`data-tab-handler`);
- const settingContainer = this.container
- .querySelector(`[data-plugin-item="${tabName}"]`)
- .querySelector(`[data-plugin-settings]`);
- const settings = settingContainer.querySelectorAll(
- "[data-setting-container]"
- );
- return settings;
- }
-}
-
-export { Popover, Tabs, FormatValue, FilterSettings };
+class Popover {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("pointerover", (e) => {
+ //POPOVER LOGIC
+ try {
+ if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
+ this.showPopover(e.target);
+ }
+ } catch (err) {}
+ });
+
+ window.addEventListener("pointerout", (e) => {
+ //POPOVER LOGIC
+ try {
+ if (e.target.closest("svg").hasAttribute(`data-popover-btn`)) {
+ this.hidePopover(e.target);
+ }
+ } catch (err) {}
+ });
+ }
+
+ showPopover(el) {
+ const btn = el.closest("svg");
+ //toggle curr popover
+ const popover = btn.parentElement.querySelector(`[data-popover-content]`);
+ popover.classList.remove("hidden");
+ }
+
+ hidePopover(el) {
+ const btn = el.closest("svg");
+ //toggle curr popover
+ const popover = btn.parentElement.querySelector(`[data-popover-content]`);
+ popover.classList.add("hidden");
+ }
+}
+
+class Tabs {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("click", (e) => {
+ try {
+ if (
+ e.target.closest("button").hasAttribute("data-tab-handler") ||
+ e.target.closest("button").hasAttribute("data-tab-handler-mobile")
+ ) {
+ //get needed data
+ const tab = e.target.closest("button");
+ const tabAtt =
+ tab.getAttribute("data-tab-handler") ||
+ tab.getAttribute("data-tab-handler-mobile");
+ const container = tab.closest("div[data-service-content]");
+ // change style
+ this.resetTabsStyle(container);
+ this.highlightClicked(container, tabAtt);
+ //show content
+ this.hideAllSettings(container);
+ this.showSettingClicked(container, tabAtt);
+ //close dropdown and change btn textcontent on mobile
+ this.setDropBtnText(container, tabAtt);
+ this.closeDropdown(container);
+ }
+ } catch (err) {}
+
+ try {
+ if (e.target.closest("button").hasAttribute("data-tab-dropdown-btn")) {
+ const dropBtn = e.target.closest("button");
+ const container = dropBtn.closest("div[data-service-content]");
+ this.toggleDropdown(container);
+ }
+ } catch (err) {}
+ });
+ }
+
+ resetTabsStyle(container) {
+ //reset desktop style
+ const tabsDesktop = container.querySelectorAll("button[data-tab-handler]");
+ tabsDesktop.forEach((tab) => {
+ tab.classList.remove("active");
+ });
+ //reset mobile style
+ const tabsMobile = container.querySelectorAll(
+ "button[data-tab-handler-mobile]",
+ );
+ tabsMobile.forEach((tab) => {
+ tab.classList.remove("active");
+ });
+ }
+
+ highlightClicked(container, tabAtt) {
+ //desktop case
+ const tabDesktop = container.querySelector(
+ `button[data-tab-handler='${tabAtt}']`,
+ );
+ tabDesktop.classList.add("active");
+
+ //mobile case
+ const tabMobile = container.querySelector(
+ `button[data-tab-handler-mobile='${tabAtt}']`,
+ );
+ tabMobile.classList.add("active");
+ }
+
+ hideAllSettings(container) {
+ const plugins = container.querySelectorAll("[data-plugin-item]");
+ plugins.forEach((plugin) => {
+ plugin.classList.add("hidden");
+ });
+ }
+
+ showSettingClicked(container, tabAtt) {
+ const plugin = container.querySelector(`[data-plugin-item='${tabAtt}']`);
+ plugin.classList.remove("hidden");
+ }
+
+ setDropBtnText(container, tabAtt) {
+ const dropBtn = container.querySelector("[data-tab-dropdown-btn]");
+ dropBtn.querySelector("span").textContent = tabAtt;
+ }
+
+ closeDropdown(container) {
+ const dropdown = container.querySelector("[data-tab-dropdown]");
+ dropdown.classList.add("hidden");
+ dropdown.classList.remove("flex");
+ }
+
+ toggleDropdown(container) {
+ const dropdown = container.querySelector("[data-tab-dropdown]");
+ dropdown.classList.toggle("hidden");
+ dropdown.classList.toggle("flex");
+ }
+}
+
+class FormatValue {
+ constructor() {
+ this.inputs = document.querySelectorAll("input");
+ this.init();
+ }
+
+ init() {
+ this.inputs.forEach((inp) => {
+ try {
+ inp.setAttribute("value", inp.getAttribute("value").trim());
+ inp.value = inp.value.trim();
+ } catch (err) {}
+ });
+ }
+}
+
+class FilterSettings {
+ constructor(inputID, container) {
+ this.input = document.querySelector(`input#${inputID}`);
+ //DESKTOP
+ this.container = document.querySelector(container);
+ this.deskTabs = this.container.querySelectorAll(`[data-tab-handler]`);
+ this.init();
+ }
+
+ init() {
+ this.input.addEventListener("input", () => {
+ this.resetFilter();
+ //get inp format
+ const inpValue = this.input.value.trim().toLowerCase();
+ //loop all tabs
+ this.deskTabs.forEach((tab) => {
+ //get settings of tabs except multiples
+ const settings = this.getSettingsFromTab(tab);
+
+ //compare total count to currCount to determine
+ //if tabs need to be hidden
+ const settingCount = settings.length;
+ let hiddenCount = 0;
+ settings.forEach((setting) => {
+ try {
+ const title = setting
+ .querySelector("h5")
+ .textContent.trim()
+ .toLowerCase();
+ if (!title.includes(inpValue)) {
+ setting.classList.add("hidden");
+ hiddenCount++;
+ }
+ } catch (err) {}
+ });
+ //case no setting match, hidden tab and content
+ if (settingCount === hiddenCount) {
+ const tabName = tab.getAttribute(`data-tab-handler`);
+ //hide mobile and desk tabs
+ tab.classList.add("hidden");
+ this.container
+ .querySelector(`[data-tab-handler-mobile="${tabName}"]`)
+ .classList.add("hidden");
+ this.container
+ .querySelector(`[data-plugin-item=${tabName}]`)
+ .querySelector("[data-setting-header]")
+ .classList.add("hidden");
+ }
+ });
+ });
+ }
+
+ resetFilter() {
+ this.deskTabs.forEach((tab) => {
+ const tabName = tab.getAttribute(`data-tab-handler`);
+ //hide mobile and desk tabs
+ tab.classList.remove("hidden");
+ this.container
+ .querySelector(`[data-tab-handler-mobile="${tabName}"]`)
+ .classList.remove("hidden");
+ this.container
+ .querySelector(`[data-plugin-item=${tabName}]`)
+ .querySelector("[data-setting-header]")
+ .classList.remove("hidden");
+ const settings = this.getSettingsFromTab(tab);
+ settings.forEach((setting) => {
+ setting.classList.remove("hidden");
+ });
+ });
+ }
+
+ getSettingsFromTab(tabEl) {
+ const tabName = tabEl.getAttribute(`data-tab-handler`);
+ const settingContainer = this.container
+ .querySelector(`[data-plugin-item="${tabName}"]`)
+ .querySelector(`[data-plugin-settings]`);
+ const settings = settingContainer.querySelectorAll(
+ "[data-setting-container]",
+ );
+ return settings;
+ }
+}
+
+export { Popover, Tabs, FormatValue, FilterSettings };
diff --git a/src/ui/static/webfonts/bootstrap-icons.css b/src/ui/static/webfonts/bootstrap-icons.css
index facb2077f..7a092af21 100644
--- a/src/ui/static/webfonts/bootstrap-icons.css
+++ b/src/ui/static/webfonts/bootstrap-icons.css
@@ -1,6 +1,7 @@
@font-face {
font-family: "bootstrap-icons";
- src: url("./bootstrap-icons.woff2?524846017b983fc8ded9325d94ed40f3")
+ src:
+ url("./bootstrap-icons.woff2?524846017b983fc8ded9325d94ed40f3")
format("woff2"),
url("./bootstrap-icons.woff?524846017b983fc8ded9325d94ed40f3")
format("woff");
diff --git a/src/ui/templates/head.html b/src/ui/templates/head.html
index c9a4ed862..90f58f9e4 100644
--- a/src/ui/templates/head.html
+++ b/src/ui/templates/head.html
@@ -18,7 +18,8 @@
-
+
+
{% if current_endpoint == "global_config" %}
@@ -33,7 +34,7 @@
{% elif current_endpoint == "logs" %}
-
+