diff --git a/package-lock.json b/package-lock.json
index 7b4701e4..7830a113 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,16 +5,16 @@
"requires": true,
"dependencies": {
"@mcschema/core": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/@mcschema/core/-/core-0.9.3.tgz",
- "integrity": "sha512-DtDncu+NE32OUDzjCv+IAA+owPXASEbR9WXRCikpDY/eTxpjiUkgrsjYzG9A8GhE/Hw60vbwbDuSrHjeTDYSTg=="
+ "version": "0.10.0-beta.6",
+ "resolved": "https://registry.npmjs.org/@mcschema/core/-/core-0.10.0-beta.6.tgz",
+ "integrity": "sha512-52QcHOBuATSL9dqI3fuNMexuJFwwF9NrtILs+IzkwAt/40e7VnR29doJW8P3JkNcZ2a9BaVtw5ExfbePj9rTFg=="
},
"@mcschema/java-1.16": {
- "version": "0.5.8",
- "resolved": "https://registry.npmjs.org/@mcschema/java-1.16/-/java-1.16-0.5.8.tgz",
- "integrity": "sha512-hbrDilc5fQK97x0MhbBiwHT+C4xCjsO7HPdgcZqvzhlsbcSkG2fz03Ew/myJJQyxYpShyZLq2QPl6OVtiCp40Q==",
+ "version": "0.5.9-beta.1",
+ "resolved": "https://registry.npmjs.org/@mcschema/java-1.16/-/java-1.16-0.5.9-beta.1.tgz",
+ "integrity": "sha512-Yh+Z6jH/GU/WgshsHfz6aRZLXGMldhaHgWfk+eWvY3f8ftrurNW+OUddCx3kwOZ4gADy5pivVhISB1yecpWKxQ==",
"requires": {
- "@mcschema/core": "^0.9.0"
+ "@mcschema/core": "^0.10.0-beta.3"
}
},
"@mcschema/locales": {
diff --git a/package.json b/package.json
index b066d318..f3803920 100644
--- a/package.json
+++ b/package.json
@@ -12,8 +12,8 @@
"author": "Misode",
"license": "MIT",
"dependencies": {
- "@mcschema/core": "^0.9.3",
- "@mcschema/java-1.16": "^0.5.8",
+ "@mcschema/core": "^0.10.0-beta.6",
+ "@mcschema/java-1.16": "^0.5.9-beta.1",
"@mcschema/locales": "^0.1.11",
"@types/google.analytics": "0.0.40",
"@types/split.js": "^1.4.0",
diff --git a/src/app/Mounter.ts b/src/app/Mounter.ts
new file mode 100644
index 00000000..02974514
--- /dev/null
+++ b/src/app/Mounter.ts
@@ -0,0 +1,89 @@
+import { ModelPath } from '@mcschema/core'
+
+const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
+
+export function hexId(length = 12) {
+ var arr = new Uint8Array(length / 2)
+ window.crypto.getRandomValues(arr)
+ return Array.from(arr, dec2hex).join('')
+}
+
+type Registry = {
+ [id: string]: (el: Element) => void
+}
+
+type NodeInjector = (path: ModelPath, mounter: Mounter) => string
+
+type TreeViewOptions = {
+ nodeInjector?: NodeInjector
+}
+
+export interface Mounter {
+ register(callback: (el: Element) => void): string
+ registerEvent(type: string, callback: (el: Element) => void): string
+ registerChange(callback: (el: Element) => void): string
+ registerClick(callback: (el: Element) => void): string
+ nodeInjector(path: ModelPath, mounter: Mounter): string
+ mount(el: HTMLElement): void
+}
+
+export class Mounter implements Mounter {
+ registry: Registry = {}
+ nodeInjector: NodeInjector
+
+ constructor(options?: TreeViewOptions) {
+ this.nodeInjector = options?.nodeInjector ?? (() => '')
+ }
+
+ /**
+ * Registers a callback and gives an ID
+ * @param callback function that is called when the element is mounted
+ * @returns the ID that should be applied to the data-id attribute
+ */
+ register(callback: (el: Element) => void): string {
+ const id = hexId()
+ this.registry[id] = callback
+ return id
+ }
+
+ /**
+ * Registers an event and gives an ID
+ * @param type event type
+ * @param callback function that is called when the event is fired
+ * @returns the ID that should be applied to the data-id attribute
+ */
+ registerEvent(type: string, callback: (el: Element) => void): string {
+ return this.register(el => {
+ el.addEventListener(type, evt => {
+ callback(el)
+ evt.stopPropagation()
+ })
+ })
+ }
+
+ /**
+ * Registers a change event and gives an ID
+ * @param callback function that is called when the event is fired
+ * @returns the ID that should be applied to the data-id attribute
+ */
+ registerChange(callback: (el: Element) => void): string {
+ return this.registerEvent('change', callback)
+ }
+
+ /**
+ * Registers a click event and gives an ID
+ * @param callback function that is called when the event is fired
+ * @returns the ID that should be applied to the data-id attribute
+ */
+ registerClick(callback: (el: Element) => void): string {
+ return this.registerEvent('click', callback)
+ }
+
+ mount(el: HTMLElement): void {
+ for (const id in this.registry) {
+ const element = el.querySelector(`[data-id="${id}"]`)
+ if (element !== null) this.registry[id](element)
+ }
+ this.registry = {}
+ }
+}
diff --git a/src/app/SourceView.ts b/src/app/SourceView.ts
index f8f88a35..42fd93af 100644
--- a/src/app/SourceView.ts
+++ b/src/app/SourceView.ts
@@ -1,5 +1,6 @@
import { DataModel, Path, ModelPath } from '@mcschema/core'
import { AbstractView } from './AbstractView'
+import { transformOutput } from './hooks/transformOutput'
type SourceViewOptions = {
indentation?: number | string,
@@ -27,7 +28,7 @@ export class SourceView extends AbstractView {
}
invalidated() {
- const transformed = this.model.schema.transform(new ModelPath(this.model), this.model.data)
+ const transformed = this.model.schema.hook(transformOutput, new ModelPath(this.model), this.model.data)
this.target.value = JSON.stringify(transformed, null, this.options?.indentation)
}
diff --git a/src/app/TreeView.ts b/src/app/TreeView.ts
index 33b4d83d..2d12bddc 100644
--- a/src/app/TreeView.ts
+++ b/src/app/TreeView.ts
@@ -1,5 +1,7 @@
-import { DataModel, ModelPath, Mounter } from '@mcschema/core'
+import { DataModel, ModelPath } from '@mcschema/core'
import { AbstractView } from './AbstractView'
+import { Mounter } from './Mounter'
+import { renderHtml } from './hooks/renderHtml'
type Registry = {
[id: string]: (el: Element) => void
@@ -39,7 +41,7 @@ export class TreeView extends AbstractView {
invalidated() {
const mounter = new Mounter({nodeInjector: this.nodeInjector})
const path = new ModelPath(this.model)
- const rendered = this.model.schema.render(path, this.model.data, mounter)
+ const rendered = this.model.schema.hook(renderHtml, path, this.model.data, mounter)
const category = this.model.schema.category(path)
if (rendered[1]) {
this.target.innerHTML = `
diff --git a/src/app/app.ts b/src/app/app.ts
index 001a9f28..1e4965dc 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -5,7 +5,6 @@ import {
locale,
LOCALES,
ModelPath,
- Mounter,
Path,
} from '@mcschema/core'
import { getCollections, getSchemas } from '@mcschema/java-1.16'
@@ -16,6 +15,7 @@ import { SourceView } from './SourceView'
import { ErrorsView } from './ErrorsView'
import config from '../config.json'
import { BiomeNoiseVisualizer } from './visualization/BiomeNoiseVisualizer'
+import { Mounter } from './Mounter'
const LOCAL_STORAGE_THEME = 'theme'
const LOCAL_STORAGE_LANGUAGE = 'language'
diff --git a/src/app/hooks/renderHtml.ts b/src/app/hooks/renderHtml.ts
new file mode 100644
index 00000000..9a9f808a
--- /dev/null
+++ b/src/app/hooks/renderHtml.ts
@@ -0,0 +1,241 @@
+import { locale, Hook, ModelPath, Path, StringHookParams, ValidationOption, EnumOption, INode, DataModel, MapNode, StringNode } from '@mcschema/core'
+import { Mounter } from '../Mounter'
+import { hexId } from '../utils'
+
+/**
+ * Secondary model used to remember the keys of a map
+ */
+const keysModel = new DataModel(MapNode(
+ StringNode(),
+ StringNode()
+), { historyMax: 0 })
+
+/**
+ * Renders the node and handles events to update the model
+ * @returns string HTML representation of this node using the given data
+ */
+export const renderHtml: Hook<[any, Mounter], [string, string, string]> = {
+ base() {
+ return ['', '', '']
+ },
+
+ boolean({ node }, path, value, mounter) {
+ const onFalse = mounter.registerClick(el => {
+ path.model.set(path, node.optional() && value === false ? undefined : false)
+ })
+ const onTrue = mounter.registerClick(el => {
+ path.model.set(path, node.optional() && value === true ? undefined : true)
+ })
+ return ['', `
+
`, '']
+ },
+
+ choice({ choices, config, switchNode }, path, value, mounter) {
+ const choice = switchNode.activeCase(path) ?? choices[0]
+ const pathWithContext = (config?.context) ?
+ new ModelPath(path.getModel(), new Path(path.getArray(), [config.context])) : path
+ const pathWithChoiceContext = config?.choiceContext ? new Path([], [config.choiceContext]) : config?.context ? new Path([], [config.context]) : path
+ const inject = choices.map(c => {
+ if (c.type === choice.type) {
+ return `
`
+ }
+ const buttonId = mounter.registerClick(el => {
+ path.model.set(path, c.change ? c.change(value) : c.node.default())
+ })
+ return `
`
+ }).join('')
+
+ const [prefix, suffix, body] = choice.node.hook(this, pathWithContext, value, mounter)
+ return [prefix, inject + suffix, body]
+ },
+
+ list({ children }, path, value, mounter) {
+ const onAdd = mounter.registerClick(el => {
+ if (!Array.isArray(value)) value = []
+ path.model.set(path, [children.default(), ...value])
+ })
+ const onAddBottom = mounter.registerClick(el => {
+ if (!Array.isArray(value)) value = []
+ path.model.set(path, [...value, children.default()])
+ })
+ const suffix = `
`
+ + mounter.nodeInjector(path, mounter)
+
+ let body = ''
+ if (Array.isArray(value)) {
+ body = value.map((childValue, index) => {
+ const removeId = mounter.registerClick(el => path.model.set(path.push(index), undefined))
+ const childPath = path.push(index).localePush('entry')
+ const category = children.category(childPath)
+ const [cPrefix, cSuffix, cBody] = children.hook(this, childPath, childValue, mounter)
+ return `
+
+ ${cBody ? `
${cBody}
` : ''}
+
`
+ }).join('')
+ if (value.length > 2) {
+ body += `
+
+
`
+ }
+ }
+ return ['', suffix, body]
+ },
+
+ map({ keys, children }, path, value, mounter) {
+ const keyPath = new ModelPath(keysModel, new Path([hashString(path.toString())]))
+ const onAdd = mounter.registerClick(el => {
+ const key = keyPath.get()
+ path.model.set(path.push(key), children.default())
+ })
+ const keyRendered = keys.hook(this, keyPath, keyPath.get() ?? '', mounter)
+ const suffix = keyRendered[1]
+ + `
`
+ + mounter.nodeInjector(path, mounter)
+ let body = ''
+ if (typeof value === 'object' && value !== undefined) {
+ body = Object.keys(value)
+ .map(key => {
+ const removeId = mounter.registerClick(el => path.model.set(path.push(key), undefined))
+ const childPath = path.modelPush(key)
+ const category = children.category(childPath)
+ const [cPrefix, cSuffix, cBody] = children.hook(this, childPath, value[key], mounter)
+ return `
+
+ ${cBody ? `
${cBody}
` : ''}
+
`
+ })
+ .join('')
+ }
+ return ['', suffix, body]
+ },
+
+ number({ integer, config }, path, value, mounter) {
+ const onChange = mounter.registerChange(el => {
+ const value = (el as HTMLInputElement).value
+ let parsed = config?.color
+ ? parseInt(value.slice(1), 16)
+ : integer ? parseInt(value) : parseFloat(value)
+ path.model.set(path, parsed)
+ })
+ if (config?.color) {
+ const hex = (value?.toString(16).padStart(6, '0') ?? '000000')
+ return ['', `
`, '']
+ }
+ return ['', `
`, '']
+ },
+
+ object({ node, getActiveFields, getChildModelPath }, path, value, mounter) {
+ let prefix = ''
+ if (node.optional()) {
+ if (value === undefined) {
+ prefix = `
`
+ } else {
+ prefix = `
`
+ }
+ }
+ let suffix = mounter.nodeInjector(path, mounter)
+ let body = ''
+ if (typeof value === 'object' && value !== undefined && (!(node.optional() && value === undefined))) {
+ const activeFields = getActiveFields(path)
+ body = Object.keys(activeFields)
+ .filter(k => activeFields[k].enabled(path))
+ .map(k => {
+ const field = activeFields[k]
+ const childPath = getChildModelPath(path, k)
+ const category = field.category(childPath)
+ const [cPrefix, cSuffix, cBody] = field.hook(this, childPath, value[k], mounter)
+ return `
+
+ ${cBody ? `
${cBody}
` : ''}
+
`
+ })
+ .join('')
+ }
+ return [prefix, suffix, body]
+ },
+
+ string(params, path, value, mounter) {
+ const inputId = mounter.register(el => {
+ (el as HTMLSelectElement).value = value ?? ''
+ el.addEventListener('change', evt => {
+ const newValue = (el as HTMLSelectElement).value
+ console.log("UPDATING NEW VALUE!", path.toString(), newValue)
+ path.model.set(path, newValue.length === 0 ? undefined : newValue)
+ evt.stopPropagation()
+ })
+ })
+ return ['', rawString(params, path, inputId), '']
+ }
+}
+
+function isEnum(value?: ValidationOption | EnumOption): value is EnumOption {
+ return !!(value as any)?.enum
+}
+
+function isValidator(value?: ValidationOption | EnumOption): value is ValidationOption {
+ return !!(value as any)?.validator
+}
+
+function rawString({ node, getValues, config }: { node: INode } & StringHookParams, path: ModelPath, inputId?: string) {
+ const values = getValues()
+ if (isEnum(config) && !config.additional) {
+ const contextPath = typeof config.enum === 'string' ?
+ new Path(path.getArray(), [config.enum]) : path
+ return selectRaw(node, contextPath, values, inputId)
+ }
+ if (config && isValidator(config)
+ && config.validator === 'resource'
+ && typeof config.params.pool === 'string'
+ && values.length > 0) {
+ const contextPath = new Path(path.getArray(), [config.params.pool])
+ if (contextPath.localePush(values[0]).strictLocale()) {
+ return selectRaw(node, contextPath, values, inputId)
+ }
+ }
+ const datalistId = hexId()
+ return `
+ ${values.length === 0 ? '' :
+ `
`}`
+}
+
+function selectRaw(node: INode, contextPath: Path, values: string[], inputId?: string) {
+ return `
`
+}
+
+function hashString(str: string) {
+ var hash = 0, i, chr;
+ for (i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + chr;
+ hash |= 0;
+ }
+ return hash;
+}
diff --git a/src/app/hooks/transformOutput.ts b/src/app/hooks/transformOutput.ts
new file mode 100644
index 00000000..eac78f57
--- /dev/null
+++ b/src/app/hooks/transformOutput.ts
@@ -0,0 +1,53 @@
+import { Hook } from '@mcschema/core'
+
+export const transformOutput: Hook<[any], any> = {
+ base({}, _, value) {
+ return value
+ },
+
+ boolean({}, _, value) {
+ return value
+ },
+
+ choice({ switchNode }, path, value) {
+ return switchNode.hook(this, path, value)
+ },
+
+ list({ children }, path, value) {
+ if (!Array.isArray(value)) return value
+ return value.map((obj, index) =>
+ children.hook(this, path.push(index), obj)
+ )
+ },
+
+ map({ children }, path, value) {
+ if (value === undefined) return undefined
+ let res: any = {}
+ Object.keys(value).forEach(f =>
+ res[f] = children.hook(this, path.push(f), value[f])
+ )
+ return res;
+ },
+
+ number({}, _, value) {
+ return value
+ },
+
+ object({ getActiveFields }, path, value) {
+ if (value === undefined || value === null || typeof value !== 'object') {
+ return value
+ }
+ let res: any = {}
+ const activeFields = getActiveFields(path)
+ Object.keys(activeFields)
+ .filter(k => activeFields[k].enabled(path))
+ .forEach(f => {
+ res[f] = activeFields[f].hook(this, path.push(f), value[f])
+ })
+ return res
+ },
+
+ string({}, _, value) {
+ return value
+ }
+}
diff --git a/src/app/utils.ts b/src/app/utils.ts
new file mode 100644
index 00000000..950fabdd
--- /dev/null
+++ b/src/app/utils.ts
@@ -0,0 +1,7 @@
+const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
+
+export function hexId(length = 12) {
+ var arr = new Uint8Array(length / 2)
+ window.crypto.getRandomValues(arr)
+ return Array.from(arr, dec2hex).join('')
+}