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):
+
+
+
+
+
+
+ => Get certificate
+
+
+
+ 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):
+
+
+
+
+
+
+ => Get certificate
+
+
+
+ Tokens
+
+
+ Passwords
+
+
+ Downloads
+
+
+ CAs
+ | Name | Certificate | Signed 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
+
+
+
+ {{" "}}
+
+
+
+
+ Active links
+ {{ d.text }}{{" "}}
+ `
+}
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 }} ({{ host.Template }})
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ error.code }}
+
{{ error.message }}
+
+
+
+ Not connected.
+
+
+
+ Store is new.
+ Option 1: initialize a new store
+
+ Option 2: upload a previously downloaded store
+
+
+
+
+ Store is not open.
+
+
+
+
+ Not logged in.
+ Invalid token
+
+
+
+
+
+
+ {{v.title}}
+
+ {{view.title}}
+
+
+
+
+
+
+
+
+
+
+
Config
+
+
+
Store
+
Download
+
+
+
+
+ Hosts from template
+
+
+
+
+
+
+
+
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. `