diff --git a/html/ui/Cluster-703dcdca97841304.js b/html/ui/Cluster-703dcdca97841304.js new file mode 100644 index 0000000..2436c51 --- /dev/null +++ b/html/ui/Cluster-703dcdca97841304.js @@ -0,0 +1,131 @@ +const Cluster = { + components: { Downloads, GetCopy }, + props: [ 'cluster', 'token', 'state' ], + data() { + return { + signReqValidity: "1d", + sshSignReq: { + PubKey: "", + Principal: "root", + }, + sshUserCert: null, + kubeSignReq: { + CSR: "", + User: "", + Group: "system:masters", + }, + kubeUserCert: null, + }; + }, + methods: { + sshCASign() { + event.preventDefault(); + fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, { + method: 'POST', + body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }), + headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, + }).then((resp) => { + if (resp.ok) { + resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) }) + } else { + resp.json().then((resp) => alert('failed to sign: '+resp.message)) + } + }) + .catch((e) => { alert('failed to sign: '+e); }) + }, + kubeCASign() { + event.preventDefault(); + fetch(`/clusters/${this.cluster.Name}/kube/sign`, { + method: 'POST', + body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }), + headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, + }).then((resp) => { + if (resp.ok) { + resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) }) + } else { + resp.json().then((resp) => alert('failed to sign: '+resp.message)) + } + }) + .catch((e) => { alert('failed to sign: '+e); }) + }, + readFile(e, onload) { + const file = e.target.files[0]; + if (!file) { return; } + const reader = new FileReader(); + reader.onload = () => { onload(reader.result) }; + reader.onerror = () => { alert("error reading file"); }; + reader.readAsText(file); + }, + loadPubKey(e) { + this.readFile(e, (v) => { + this.sshSignReq.PubKey = v; + }); + }, + loadCSR(e) { + this.readFile(e, (v) => { + this.kubeSignReq.CSR = v; + }); + }, + }, + template: ` +

Access

+ +

Allow cluster access from a public key

+ +

Grant SSH access

+ +

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

+

User:

+

Public key (OpenSSH format):
+ + +

+ +

+ +

+ +

Grant Kubernetes API access

+ +

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

+

User: (by default, from the CSR)

+

Group:

+

Certificate signing request (PEM format):
+ + +

+ +

+ +

+ +

Tokens

+ + +

Passwords

+ + +

Downloads

+ + +

CAs

+ + + + + +
NameCertificateSigned certificates
{{ ca.Name }}
+ +` +} diff --git a/html/ui/Downloads-c9374f19f52c46d.js b/html/ui/Downloads-c9374f19f52c46d.js new file mode 100644 index 0000000..0e64c8e --- /dev/null +++ b/html/ui/Downloads-c9374f19f52c46d.js @@ -0,0 +1,70 @@ +const Downloads = { + props: [ 'kind', 'name', 'token', 'state' ], + data() { + return { createDisabled: false, selectedAssets: {} } + }, + computed: { + availableAssets() { + return { + cluster: ['addons'], + host: [ + "kernel", + "initrd", + "bootstrap.tar", + "boot.img.lz4", + "boot.img.gz", + "boot.qcow2", + "boot.vmdk", + "boot.img", + "boot.iso", + "boot.tar", + "boot-efi.tar", + "config", + "bootstrap-config", + "ipxe", + ], + }[this.kind] + }, + downloads() { + return Object.entries(this.state.Downloads) + .filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name }) + .map(e => { + const token= e[0]; + return { + text: token.substring(0, 5) + '...', + url: '/public/downloads/'+token+"/", + } + }) + }, + assets() { + return this.availableAssets.filter(a => this.selectedAssets[a]) + }, + }, + methods: { + createToken() { + event.preventDefault() + this.createDisabled = true + + fetch('/authorize-download', { + method: 'POST', + body: JSON.stringify({Kind: this.kind, Name: this.name, Assets: this.assets}), + headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, + }).then((resp) => resp.json()) + .then((token) => { this.selectedAssets = {}; this.createDisabled = false }) + .catch((e) => { alert('failed to create link'); this.createDisabled = false }) + }, + }, + template: ` +

Available assets

+

+ +

+

+ ` +} diff --git a/html/ui/GetCopy-7e3c9678f9647d40.js b/html/ui/GetCopy-7e3c9678f9647d40.js new file mode 100644 index 0000000..aed4707 --- /dev/null +++ b/html/ui/GetCopy-7e3c9678f9647d40.js @@ -0,0 +1,32 @@ +const GetCopy = { + props: [ 'name', 'href', 'token' ], + data() { return {showCopied: false} }, + template: `
copied!
{{name}} 🗐
`, + methods: { + fetch() { + event.preventDefault() + return fetch(this.href, { + method: 'GET', + headers: { 'Authorization': 'Bearer ' + this.token }, + }) + }, + handleFetchError(e) { + console.log("failed to get value:", e) + alert('failed to get value') + }, + fetchAndSave() { + this.fetch().then(resp => resp.blob()).then((value) => { + window.open(URL.createObjectURL(value), "_blank") + }).catch(this.handleFetchError) + }, + fetchAndCopy() { + this.fetch() + .then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text()) + .then((value) => { + window.navigator.clipboard.writeText(value) + this.showCopied = true + setTimeout(() => { this.showCopied = false }, 1000) + }).catch(this.handleFetchError) + }, + }, +} diff --git a/html/ui/Host-61916516a854adff.js b/html/ui/Host-61916516a854adff.js new file mode 100644 index 0000000..95871d8 --- /dev/null +++ b/html/ui/Host-61916516a854adff.js @@ -0,0 +1,14 @@ +const Host = { + components: { Downloads }, + props: [ 'host', 'token', 'state' ], + template: ` +

Cluster: {{ host.Cluster }}

+

IPs: + + {{ ip }}{{" "}} + +

+

Downloads

+ +` +} diff --git a/html/ui/app-492d80052b1d2654.js b/html/ui/app-492d80052b1d2654.js new file mode 100644 index 0000000..764c04b --- /dev/null +++ b/html/ui/app-492d80052b1d2654.js @@ -0,0 +1,262 @@ + +import { createApp } from './vue.esm-browser.js'; + +createApp({ + components: { Cluster, Host }, + data() { + return { + forms: { + store: {}, + storeUpload: {}, + delKey: {}, + hostFromTemplate: {}, + hostFromTemplateDel: "", + }, + view: "", + viewFilter: "", + session: {}, + error: null, + publicState: null, + serverVersion: null, + uiHash: null, + watchingState: false, + state: null, + } + }, + mounted() { + this.session = JSON.parse(sessionStorage.state || "{}") + this.watchPublicState() + }, + watch: { + session: { + deep: true, + handler(v) { + sessionStorage.state = JSON.stringify(v) + + if (v.token && !this.watchingState) { + this.watchState() + this.watchingState = true + } + } + }, + publicState: { + deep: true, + handler(v) { + if (v) { + this.serverVersion = v.ServerVersion + if (this.uiHash && v.UIHash != this.uiHash) { + console.log("reloading") + location.reload() + } else { + this.uiHash = v.UIHash + } + } + }, + } + }, + + computed: { + views() { + var views = [{type: "actions", name: "admin", title: "Admin actions"}]; + + (this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`})); + (this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`})); + + return views.filter((v) => v.type != "host" || v.name.includes(this.viewFilter)); + }, + viewObj() { + if (this.view) { + if (this.view.type == "cluster") { + return this.state.Clusters.find((c) => c.Name == this.view.name); + } + if (this.view.type == "host") { + return this.state.Hosts.find((h) => h.Name == this.view.name); + } + } + return undefined; + }, + hostsFromTemplate() { + return (this.state.Hosts||[]).filter((h) => h.Template); + }, + }, + + methods: { + any(array) { + return array && array.length != 0; + }, + copyText(text) { + event.preventDefault() + window.navigator.clipboard.writeText(text) + }, + setToken() { + event.preventDefault() + this.session.token = this.forms.setToken + this.forms.setToken = null + }, + uploadStore() { + event.preventDefault() + this.apiPost('/public/store.tar', this.$refs.storeUpload.files[0], (v) => { + this.forms.store = {} + }, "application/tar") + }, + namedPassphrase(name, passphrase) { + return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)} + }, + storeAddKey() { + this.apiPost('/store/add-key', this.namedPassphrase(), (v) => { + this.forms.store = {} + }) + }, + storeDelKey() { + event.preventDefault() + + let name = this.forms.delKey.name + + if (!confirm("Remove key named "+JSON.stringify(name)+"?")) { + return + } + this.apiPost('/store/delete-key', name , (v) => { + this.forms.delKey = {} + }) + }, + unlockStore() { + this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => { + this.forms.store = {} + + if (v) { + this.session.token = v + if (!this.watchingState) { + this.watchState() + this.watchingState = true + } + } + }) + }, + uploadConfig() { + this.apiPost('/configs', this.$refs.configUpload.files[0], (v) => {}, "text/vnd.yaml") + }, + hostFromTemplateAdd() { + let v = this.forms.hostFromTemplate; + this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} }); + }, + hostFromTemplateDel() { + event.preventDefault() + + let v = this.forms.hostFromTemplateDel; + if (!confirm("delete host template instance "+v+"?")) { + return + } + this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" }); + }, + apiPost(action, data, onload, contentType = 'application/json') { + event.preventDefault() + + if (data === undefined) { + throw("action " + action + ": no data") + } + + /* TODO + fetch(action, { + method: 'POST', + body: JSON.stringify(data), + }) + .then((response) => response.json()) + .then((result) => onload) + // */ + + var xhr = new XMLHttpRequest() + + xhr.responseType = 'json' + // TODO spinner, pending action notification, or something + xhr.onerror = () => { + // this.actionResults.splice(idx, 1, {...item, done: true, failed: true }) + } + xhr.onload = (r) => { + if (xhr.status != 200) { + this.error = xhr.response + return + } + // this.actionResults.splice(idx, 1, {...item, done: true, resp: xhr.responseText}) + this.error = null + if (onload) { + onload(xhr.response) + } + } + + xhr.open("POST", action) + xhr.setRequestHeader('Accept', 'application/json') + xhr.setRequestHeader('Content-Type', contentType) + if (this.session.token) { + xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token) + } + + if (contentType == "application/json") { + xhr.send(JSON.stringify(data)) + } else { + xhr.send(data) + } + }, + apiDelete(action, data, onload) { + event.preventDefault() + + var xhr = new XMLHttpRequest() + xhr.onload = (r) => { + if (xhr.status != 200) { + this.error = xhr.response + return + } + this.error = null + if (onload) { + onload(xhr.response) + } + } + xhr.open("DELETE", action) + xhr.setRequestHeader('Accept', 'application/json') + if (this.session.token) { + xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token) + } + xhr.send() + }, + download(url) { + event.target.target = '_blank' + event.target.href = this.downloadLink(url) + }, + downloadLink(url) { + // TODO once-shot download link + return url + '?token=' + this.session.token + }, + watchPublicState() { + this.watchStream('publicState', '/public-state') + }, + watchState() { + this.watchStream('state', '/state', true) + }, + watchStream(field, path, withToken) { + let evtSrc = new EventSource(path + (withToken ? '?token='+this.session.token : '')); + evtSrc.onmessage = (e) => { + let update = JSON.parse(e.data) + + console.log("watch "+path+":", update) + + if (update.err) { + console.log("watch error from server:", err) + } + if (update.set) { + this[field] = update.set + } + if (update.p) { // patch + new jsonpatch.JSONPatch(update.p, true).apply(this[field]) + } + } + evtSrc.onerror = (e) => { + // console.log("event source " + path + " error:", e) + if (evtSrc) evtSrc.close() + + this[field] = null + + window.setTimeout(() => { this.watchStream(field, path, withToken) }, 1000) + } + }, + } + +}).mount('#app'); diff --git a/html/ui/favicon.ico b/html/ui/favicon.ico new file mode 100644 index 0000000..f04b731 Binary files /dev/null and b/html/ui/favicon.ico differ diff --git a/html/ui/index.html b/html/ui/index.html new file mode 100644 index 0000000..2814983 --- /dev/null +++ b/html/ui/index.html @@ -0,0 +1,381 @@ + + + +Direktil Local Server + + + + + + + + + + + + + + +
+
+ +
+ + Logged in + + + + server {{ serverVersion || '-----' }} + ui {{ uiHash || '-----' }} + + 🗲 +
+
+ +
+ +
{{ error.code }}
+
{{ error.message }}
+
+ + + + + + + + + + +
+ + diff --git a/html/ui/jsonpatch.min-942279a1c4209cc2.js b/html/ui/jsonpatch.min-942279a1c4209cc2.js new file mode 100644 index 0000000..6af589d --- /dev/null +++ b/html/ui/jsonpatch.min-942279a1c4209cc2.js @@ -0,0 +1,36 @@ +/* @preserve + * JSONPatch.js + * + * A Dharmafly project written by Thomas Parslow + * and released with the kind permission of + * NetDev. + * + * Copyright 2011-2013 Thomas Parslow. All rights reserved. + * Permission is hereby granted,y free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Implements the JSON Patch IETF RFC 6902 as specified at: + * + * http://tools.ietf.org/html/rfc6902 + * + * Also implements the JSON Pointer IETF RFC 6901 as specified at: + * + * http://tools.ietf.org/html/rfc6901 + * + */ +!function(root,factory){"object"==typeof exports?factory(module.exports):"function"==typeof define&&define.amd?define(["exports"],factory):(root.jsonpatch={},root.returnExports=factory(root.jsonpatch))}(this,function(exports){function InvalidPatch(message){Error.call(this,message),this.message=message}function PatchApplyError(message){Error.call(this,message),this.message=message}function clone(o){var cloned,key;if(isArray(o))return o.slice();if(null===o)return o;if("object"==typeof o){cloned={};for(key in o)Object.hasOwnProperty.call(o,key)&&(cloned[key]=o[key]);return cloned}return o}function deepEqual(a,b){var key;if(a===b)return!0;if(typeof a!=typeof b)return!1;if("object"!=typeof a)return!1;var aIsArray=isArray(a),bIsArray=isArray(b);if(aIsArray!==bIsArray)return!1;if(!aIsArray){for(key in a)if(Object.hasOwnProperty(a,key)&&(!Object.hasOwnProperty(b,key)||!deepEqual(a[key],b[key])))return!1;for(key in b)if(Object.hasOwnProperty(b,key)&&!Object.hasOwnProperty(a,key))return!1;return!0}if(a.length!=b.length)return!1;for(var i=0;inode.length)throw new PatchApplyError("Add operation must not attempt to create a sparse array!");node.splice(lastSegment,0,clone(value))}else node[lastSegment]=clone(value);return node},mutate)},JSONPointer.prototype.remove=function(doc,mutate){return 0===this.length?void 0:this._action(doc,function(node,lastSegment){if(!Object.hasOwnProperty.call(node,lastSegment))throw new PatchApplyError("Remove operation must point to an existing value!");return isArray(node)?node.splice(lastSegment,1):delete node[lastSegment],node},mutate)},JSONPointer.prototype.replace=function(doc,value,mutate){return 0===this.length?value:this._action(doc,function(node,lastSegment){if(!Object.hasOwnProperty.call(node,lastSegment))throw new PatchApplyError("Replace operation must point to an existing value!");return isArray(node)?node.splice(lastSegment,1,clone(value)):node[lastSegment]=clone(value),node},mutate)},JSONPointer.prototype.get=function(doc){var value;return 0===this.length?doc:(this._action(doc,function(node,lastSegment){return value=node[lastSegment],node},!0),value)},JSONPointer.prototype.subsetOf=function(otherPointer){if(this.length<=otherPointer.length)return!1;for(var i=0;i !!map[val.toLowerCase()] : val => !!map[val]; +} + +/** + * dev only flag -> name mapping + */ +const PatchFlagNames = { + [1 /* PatchFlags.TEXT */]: `TEXT`, + [2 /* PatchFlags.CLASS */]: `CLASS`, + [4 /* PatchFlags.STYLE */]: `STYLE`, + [8 /* PatchFlags.PROPS */]: `PROPS`, + [16 /* PatchFlags.FULL_PROPS */]: `FULL_PROPS`, + [32 /* PatchFlags.HYDRATE_EVENTS */]: `HYDRATE_EVENTS`, + [64 /* PatchFlags.STABLE_FRAGMENT */]: `STABLE_FRAGMENT`, + [128 /* PatchFlags.KEYED_FRAGMENT */]: `KEYED_FRAGMENT`, + [256 /* PatchFlags.UNKEYED_FRAGMENT */]: `UNKEYED_FRAGMENT`, + [512 /* PatchFlags.NEED_PATCH */]: `NEED_PATCH`, + [1024 /* PatchFlags.DYNAMIC_SLOTS */]: `DYNAMIC_SLOTS`, + [2048 /* PatchFlags.DEV_ROOT_FRAGMENT */]: `DEV_ROOT_FRAGMENT`, + [-1 /* PatchFlags.HOISTED */]: `HOISTED`, + [-2 /* PatchFlags.BAIL */]: `BAIL` +}; + +/** + * Dev only + */ +const slotFlagsText = { + [1 /* SlotFlags.STABLE */]: 'STABLE', + [2 /* SlotFlags.DYNAMIC */]: 'DYNAMIC', + [3 /* SlotFlags.FORWARDED */]: 'FORWARDED' +}; + +const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + + 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + + 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'; +const isGloballyWhitelisted = /*#__PURE__*/ makeMap(GLOBALS_WHITE_LISTED); + +const range = 2; +function generateCodeFrame(source, start = 0, end = source.length) { + // Split the content into individual lines but capture the newline sequence + // that separated each line. This is important because the actual sequence is + // needed to properly take into account the full line length for offset + // comparison + let lines = source.split(/(\r?\n)/); + // Separate the lines and newline sequences into separate arrays for easier referencing + const newlineSequences = lines.filter((_, idx) => idx % 2 === 1); + lines = lines.filter((_, idx) => idx % 2 === 0); + let count = 0; + const res = []; + for (let i = 0; i < lines.length; i++) { + count += + lines[i].length + + ((newlineSequences[i] && newlineSequences[i].length) || 0); + if (count >= start) { + for (let j = i - range; j <= i + range || end > count; j++) { + if (j < 0 || j >= lines.length) + continue; + const line = j + 1; + res.push(`${line}${' '.repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`); + const lineLength = lines[j].length; + const newLineSeqLength = (newlineSequences[j] && newlineSequences[j].length) || 0; + if (j === i) { + // push underline + const pad = start - (count - (lineLength + newLineSeqLength)); + const length = Math.max(1, end > count ? lineLength - pad : end - start); + res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length)); + } + else if (j > i) { + if (end > count) { + const length = Math.max(Math.min(end - count, lineLength), 1); + res.push(` | ` + '^'.repeat(length)); + } + count += lineLength + newLineSeqLength; + } + } + break; + } + } + return res.join('\n'); +} + +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) + ? parseStringStyle(item) + : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } + else if (isString(value)) { + return value; + } + else if (isObject(value)) { + return value; + } +} +const listDelimiterRE = /;(?![^(]*\))/g; +const propertyDelimiterRE = /:([^]+)/; +const styleCommentRE = /\/\*.*?\*\//gs; +function parseStringStyle(cssText) { + const ret = {}; + cssText + .replace(styleCommentRE, '') + .split(listDelimiterRE) + .forEach(item => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function normalizeClass(value) { + let res = ''; + if (isString(value)) { + res = value; + } + else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + ' '; + } + } + } + else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + ' '; + } + } + } + return res.trim(); +} +function normalizeProps(props) { + if (!props) + return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; +} + +// These tag configs are shared between compiler-dom and runtime-dom, so they +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +const HTML_TAGS = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' + + 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' + + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' + + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' + + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' + + 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' + + 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' + + 'option,output,progress,select,textarea,details,dialog,menu,' + + 'summary,template,blockquote,iframe,tfoot'; +// https://developer.mozilla.org/en-US/docs/Web/SVG/Element +const SVG_TAGS = 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' + + 'defs,desc,discard,ellipse,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,filter,' + + 'foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,' + + 'mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,' + + 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' + + 'text,textPath,title,tspan,unknown,use,view'; +const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'; +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `true` flag. + */ +const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS); +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `true` flag. + */ +const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS); +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `true` flag. + */ +const isVoidTag = /*#__PURE__*/ makeMap(VOID_TAGS); + +/** + * On the client we only need to offer special cases for boolean attributes that + * have different names from their corresponding dom properties: + * - itemscope -> N/A + * - allowfullscreen -> allowFullscreen + * - formnovalidate -> formNoValidate + * - ismap -> isMap + * - nomodule -> noModule + * - novalidate -> noValidate + * - readonly -> readOnly + */ +const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; +const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs); +/** + * Boolean attributes should be included if the value is truthy or ''. + * e.g. `