diff --git a/index.html b/index.html index 3516d22a7..a6cb2cd6e 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,7 @@ diff --git a/src/bootstrap.ts b/src/bootstrap.ts new file mode 100644 index 000000000..c10ac2197 --- /dev/null +++ b/src/bootstrap.ts @@ -0,0 +1,95 @@ +import {Program} from "./runtime/dsl"; +import {Watcher} from "./watchers/watcher"; +import "./watchers/html"; + +let prog = new Program("test"); +Watcher.attach("html", prog); + +prog + .block("simple block", ({find, record, lib}) => { + let person = find("P"); + let potato = find("potato"); + let nameElem; + return [ + nameElem = record("html/element", {tagname: "span", text: person.name}), + record("html/element", {tagname: "section", potato}).add("child" + "ren", nameElem) + ] + }); + +prog.test(0, [ + [2, "tag", "html/element"], + [2, "tagname", "div"], + [2, "children", 3], + [2, "sort", 1], + + [3, "tag", "html/element"], + [3, "tagname", "span"], + [3, "text", "Woo hoo!"], + [3, "style", 4], + + [4, "color", "red"], + [4, "background", "pink"], + + [5, "tag", "html/element"], + [5, "tagname", "div"], + [5, "style", 6], + [5, "children", 7], + [5, "sort", 3], + + [6, "border", "3px solid green"], + + [7, "tag", "html/element"], + [7, "tagname", "span"], + [7, "text", "meep moop"], +]); + +prog.test(1, [ + [3, "style", 4, 0, -1] +]); + +prog.test(2, [ + [3, "style", 4, 0, 1], + [4, "font-size", "4em"], + [4, "background", "pink", 0, -1] +]); + +prog.test(3, [ + [8, "tag", "html/element"], + [8, "tagname", "div"], + [8, "style", 4], + [8, "text", "Jeff (from accounting)"], + [8, "sort", 0] +]); + +// prog +// .test(0, [ +// [1, "tag", "P"], +// [1, "name", "Jeff"], + +// [2, "tag", "P"], +// [2, "name", "KERY"], + +// [3, "tag", "P"], +// [3, "name", "RAB"], + +// [4, "tag", "potato"], +// [4, "kind", "idaho"], + +// [5, "tag", "potato"], +// [5, "kind", "irish gold"], + +// ]); + +// prog +// .test(1, [ +// [1, "tag", "P", 0, -1], +// [2, "tag", "P", 0, -1] + +// ]); + +// prog +// .test(2, [ +// [1, "tag", "P", 0, 1], +// ]); + +console.log(prog); diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts index 63aa34c11..ae1684712 100644 --- a/src/runtime/dsl.ts +++ b/src/runtime/dsl.ts @@ -8,7 +8,7 @@ declare var Proxy:new (obj:any, proxy:any) => any; declare var Symbol:any; import {RawValue, Register, isRegister, GlobalInterner, Scan, IGNORE_REG, ID, - InsertNode, Node, Constraint, FunctionConstraint, Change, concatArray} from "./runtime"; + InsertNode, WatchNode, Node, Constraint, FunctionConstraint, Change, concatArray} from "./runtime"; import * as runtime from "./runtime"; import * as indexes from "./indexes"; @@ -20,6 +20,11 @@ var CURRENT_ID = 0; // Utils //-------------------------------------------------------------------- +function toArray(x:T|T[]):T[] { + if(x.constructor === Array) return x as T[]; + return [x as T]; +} + function maybeIntern(value:(RawValue|Register)):Register|ID { if(value === undefined || value === null) throw new Error("Trying to intern an undefined"); if(isRegister(value)) return value; @@ -210,7 +215,9 @@ class DSLRecord { __output: boolean = false; /** If a record is an output, it needs an id by default unless its modifying an existing record. */ __needsId: boolean = true; - __fields: any; + + __fields: {[field:string]: (RawValue|DSLNode)[]}; + __dynamicFields: [DSLVariable|string, DSLNode[]][] = []; constructor(public __block:DSLBlock, tags:string[], initialAttributes:any, entityVariable?:DSLVariable) { this.__id = CURRENT_ID++; let fields:any = {tag: tags}; @@ -296,13 +303,18 @@ class DSLRecord { }) } - add(attributeName:string, value:DSLNode) { + add(attributeName:string|DSLVariable, values:DSLNode|DSLNode[]) { if(this.__block !== this.__block.program.contextStack[0]) { throw new Error("Adds and removes may only happen in the root block."); } - let record = new DSLRecord(this.__block, [], {[attributeName]: value}, this.__record); + values = toArray(values); + + let record = new DSLRecord(this.__block, [], {}, this.__record); record.__output = true; this.__block.records.push(record); + + record.__dynamicFields.push([attributeName, values]); + return this; } @@ -336,14 +348,32 @@ class DSLRecord { } toInserts() { + let program = this.__block.program; let inserts:(Constraint|Node)[] = []; - let e = maybeIntern(toValue(this.__record)); + let e = maybeIntern(this.__record.value); + for(let field in this.__fields) { for(let dslValue of this.__fields[field]) { let value = toValue(dslValue) as (RawValue | Register); - inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern("my-awesome-node"))) + if(this.__block.watcher) { + inserts.push(new WatchNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id)) + } else { + inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++))) + } + } + for(let [dslField, dslValues] of this.__dynamicFields) { + let field = toValue(dslField) as (RawValue | Register); + for(let dslValue of dslValues) { + let value = toValue(dslValue) as (RawValue | Register); + if(this.__block.watcher) { + inserts.push(new WatchNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id)) + } else { + inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++))) + } + } } } + return inserts; } @@ -382,6 +412,7 @@ class DSLRecord { //-------------------------------------------------------------------- type DSLCompilable = DSLRecord | DSLFunction; +export type BlockFunction = (block:DSLBlock) => any; class DSLBlock { __id:number; @@ -401,7 +432,7 @@ class DSLBlock { lib = this.generateLib(); - constructor(public name:string, public creationFunction:(block:DSLBlock) => any, public readonly program:Program, mangle = true) { + constructor(public name:string, public creationFunction:BlockFunction, public readonly program:Program, mangle = true, public readonly watcher = false) { this.__id = CURRENT_ID++; let neueFunc = creationFunction; if(mangle) { @@ -1096,6 +1127,10 @@ export class Program { blocks:DSLBlock[] = []; runtimeBlocks:runtime.Block[] = []; index:indexes.Index; + nodeCount = 0; + + protected _exporter?:runtime.Exporter; + protected _lastWatch?:number; /** Represents the hierarchy of blocks currently being compiled into runtime nodes. */ contextStack:DSLBlock[] = []; @@ -1104,22 +1139,41 @@ export class Program { this.index = new indexes.HashIndex(); } - block(name:string, func:(block:DSLBlock) => any) { + block(name:string, func:BlockFunction) { let block = new DSLBlock(name, func, this); block.prepare(); this.blocks.push(block); this.runtimeBlocks.push(block.block); + + return this; + } + + watch(name:string, func:BlockFunction) { + if(!this._exporter) this._exporter = new runtime.Exporter(); + let block = new DSLBlock(name, func, this, true, true); + block.prepare(); + this.blocks.push(block); + this.runtimeBlocks.push(block.block); + this._lastWatch = block.__id; + return this; + } + + asDiffs(handler:runtime.DiffConsumer) { + if(!this._exporter || !this._lastWatch) throw new Error("Must have at least one watch block to export as diffs."); + this._exporter.triggerOnDiffs(this._lastWatch, handler); + + return this; } input(changes:runtime.Change[]) { - let trans = new runtime.Transaction(changes[0].transaction, this.runtimeBlocks, changes); + let trans = new runtime.Transaction(changes[0].transaction, this.runtimeBlocks, changes, this._exporter && this._exporter.handle); trans.exec(this.index); return trans; } test(transaction:number, eavns:TestChange[]) { let changes:Change[] = []; - let trans = new runtime.Transaction(transaction, this.runtimeBlocks, changes); + let trans = new runtime.Transaction(transaction, this.runtimeBlocks, changes, this._exporter && this._exporter.handle); for(let [e, a, v, round = 0, count = 1] of eavns as EAVRCTuple[]) { let change = Change.fromValues(e, a, v, "my-awesome-node", transaction, round, count); if(round === 0) { @@ -1129,7 +1183,7 @@ export class Program { } } trans.exec(this.index); - console.log(trans.changes.map((change, ix) => ` <- ${change}`).join("\n")); + console.info(trans.changes.map((change, ix) => ` <- ${change}`).join("\n")); return this; } } diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 9346f0255..156bf492f 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -39,6 +39,12 @@ export function printConstraint(constraint:Constraint) { } } +export function maybeReverse(value?:ID):ID|RawValue|undefined { + if(value === undefined) return value; + let raw = GlobalInterner.reverse(value); + return (""+raw).indexOf("|") === -1 ? raw : value; +} + //------------------------------------------------------------------------ // Runtime //------------------------------------------------------------------------ @@ -266,7 +272,7 @@ export class Change { } toString() { - return `Change(${GlobalInterner.reverse(this.e)}, ${GlobalInterner.reverse(this.a)}, ${GlobalInterner.reverse(this.v)}, ${GlobalInterner.reverse(this.n)}, ${this.transaction}, ${this.round}, ${this.count})`; + return `Change(${this.e}, ${GlobalInterner.reverse(this.a)}, ${maybeReverse(this.v)}, ${GlobalInterner.reverse(this.n)}, ${this.transaction}, ${this.round}, ${this.count})`; } equal(other:Change, withoutNode?:boolean, withoutE?:boolean) { @@ -278,6 +284,24 @@ export class Change { this.round == other.round && this.count == other.count; } + + reverse(interner:Interner = GlobalInterner) { + let {e, a, v, n, transaction, round, count} = this; + return new RawChange(interner.reverse(e), interner.reverse(a), interner.reverse(v), interner.reverse(n), transaction, round, count); + } +} + +/** A change with all attributes un-interned. */ +export class RawChange { + constructor(public e: RawValue, public a: RawValue, public v: RawValue, public n: RawValue, + public transaction:number, public round:number, public count:Multiplicity) {} + + toString() { + let {e, a, v, n, transaction, round, count} = this; + let internedE = GlobalInterner.get(e); + let internedV = GlobalInterner.get(v); + return `RawChange(${internedE}, ${a}, ${maybeReverse(internedV) || v}, ${n}, ${transaction}, ${round}, ${count})`; + } } /** A changeset is a list of changes, intended to occur in a single transaction. */ @@ -1022,7 +1046,7 @@ export class JoinNode implements Node { } applyCombination(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator) { - debug(" GJ combo:", printPrefix(prefix), prefix); + debug(" Join combo:", prefix.slice()); let countOfSolved = 0; for(let ix = 0; ix < this.registerLookup.length; ix++) { if(!this.registerLookup[ix]) continue; @@ -1209,6 +1233,8 @@ export class JoinNode implements Node { this.unapplyConstraint(constraint, prefix); } + let shouldApply = true; + for(let constraintIx = 0; constraintIx < affectedConstraints.length; constraintIx++) { let mask = 1 << constraintIx; let isIncluded = (comboIx & mask) !== 0; @@ -1218,13 +1244,18 @@ export class JoinNode implements Node { if(isIncluded) { let valid = constraint.applyInput(input, prefix); // If any member of the input constraints fails, this whole combination is doomed. - if(valid === ApplyInputState.fail) break; + if(valid === ApplyInputState.fail) { + shouldApply = false; + break; + } //console.log(" " + printConstraint(constraint)); } } //console.log(" ", printPrefix(prefix)); - didSomething = this.applyCombination(index, input, prefix, transaction, round, results) || didSomething; + if(shouldApply) { + didSomething = this.applyCombination(index, input, prefix, transaction, round, results) || didSomething; + } } affectedConstraints.reset(); @@ -1257,6 +1288,10 @@ export class InsertNode implements Node { resolve = Scan.prototype.resolve; + key(e:ResolvedValue, a:ResolvedValue, v:ResolvedValue, round:number) { + return `${e}|${a}|${v}|${round}`; + } + exec(index:Index, input:Change, prefix:ID[], transactionId:number, round:number, results:Iterator, transaction:Transaction):boolean { let {e,a,v,n} = this.resolve(prefix); @@ -1270,7 +1305,7 @@ export class InsertNode implements Node { let prefixRound = prefix[prefix.length - 2]; let prefixCount = prefix[prefix.length - 1]; - let key = `${e}|${a}|${v}|${prefixRound + 1}`; + let key = this.key(e, a, v, prefixRound + 1); let prevCount = this.intermediates[key] || 0; let newCount = prevCount + prefixCount; this.intermediates[key] = newCount; @@ -1279,6 +1314,8 @@ export class InsertNode implements Node { if(prevCount > 0 && newCount <= 0) delta = -1; if(prevCount <= 0 && newCount > 0) delta = 1; + debug(" ?? <-", e, a, v, prefixRound + 1, {prevCount, newCount, delta}) + if(delta) { // @TODO: when we do removes, we could say that if the result is a remove, we want to // dereference these ids instead of referencing them. This would allow us to clean up @@ -1302,6 +1339,24 @@ export class InsertNode implements Node { } } +export class WatchNode extends InsertNode { + constructor(public e:ID|Register, + public a:ID|Register, + public v:ID|Register, + public n:ID|Register, + public blockId:number) { + super(e, a, v, n); + } + + key(e:ResolvedValue, a:ResolvedValue, v:ResolvedValue) { + return `${e}|${a}|${v}`; + } + + output(transaction:Transaction, change:Change) { + transaction.export(this.blockId, change); + } +} + //------------------------------------------------------------------------------ // BinaryFlow //------------------------------------------------------------------------------ @@ -1472,7 +1527,7 @@ export class AntiJoinPresovledRight extends AntiJoin { } } -export class UnionFlow { +export class UnionFlow implements Node { constructor(public branches:Node[], public registers:Register[]) { } exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { @@ -1483,7 +1538,7 @@ export class UnionFlow { } } -export class ChooseFlow { +export class ChooseFlow implements Node { branches:Node[] = []; branchResults:Iterator[] = []; @@ -1569,10 +1624,10 @@ export class Block { //------------------------------------------------------------------------------ export class Transaction { - round = 0; protected roundChanges:Change[][] = []; - constructor(public transaction:number, public blocks:Block[], public changes:Change[]) {} + protected exportedChanges:{[blockId:number]: Change[]} = {}; + constructor(public transaction:number, public blocks:Block[], public changes:Change[], protected exportHandler?:ExportHandler) {} output(change:Change) { debug(" <-", change.toString()) @@ -1581,6 +1636,38 @@ export class Transaction { this.roundChanges[change.round] = cur; } + export(blockId:number, change:Change) { + if(!this.exportedChanges[blockId]) this.exportedChanges[blockId] = [change]; + else this.exportedChanges[blockId].push(change); + } + + protected collapseMultiplicity(changes:Change[], results:Change[] /* output */) { + // We sort the changes to group all the same EAVs together. + // @FIXME: This sort comparator is flawed. It can't differentiate certain EAVs, e.g.: + // A: [1, 2, 3] + // B: [2, 1, 3] + changes.sort((a,b) => (a.e - b.e) + (a.a - b.a) + (a.v - b.v)); + let changeIx = 0; + for(let changeIx = 0; changeIx < changes.length; changeIx++) { + let current = changes[changeIx]; + + // Collapse each subsequent matching EAV's multiplicity into the current one's. + while(changeIx + 1 < changes.length) { + let next = changes[changeIx + 1]; + if(next.e == current.e && next.a == current.a && next.v == current.v) { + current.count += next.count; + changeIx++; + } else { + break; + } + } + // console.log("next round change:", current.toString()) + if(current.count !== 0) results.push(current); + } + + return results; + } + exec(index:Index) { let {changes, roundChanges} = this; let changeIx = 0; @@ -1607,31 +1694,73 @@ export class Transaction { for(let ix = this.round + 1; ix < maxRound; ix++) { let nextRoundChanges = roundChanges[ix]; if(nextRoundChanges) { - nextRoundChanges.sort((a,b) => (a.e - b.e) + (a.a - b.a) + (a.v - b.v)) - let changeIx = 0; - for(let changeIx = 0; changeIx < nextRoundChanges.length; changeIx++) { - let current = nextRoundChanges[changeIx]; - while(changeIx + 1 < nextRoundChanges.length) { - let next = nextRoundChanges[changeIx + 1]; - if(next.e == current.e && next.a == current.a && next.v == current.v) { - current.count += next.count; - changeIx++; - } else { - break; - } - } - // console.log("next round change:", current.toString()) - if(current.count !== 0) changes.push(current); - } - break; + let oldLength = changes.length; + this.collapseMultiplicity(nextRoundChanges, changes); + + // We only want to break to begin the next fixedpoint when we have something new to run. + if(oldLength < changes.length) break; } } } } + let exportingBlocks = Object.keys(this.exportedChanges); + if(exportingBlocks.length) { + if(!this.exportHandler) throw new Error("Unable to export changes without export handler."); + + for(let blockId of exportingBlocks) { + let exports = createArray("exportsArray"); + this.collapseMultiplicity(this.exportedChanges[+blockId], exports); + this.exportedChanges[+blockId] = exports; + } + this.exportHandler(this.exportedChanges); + } + // Once the transaction is effectively done, we need to clean up after ourselves. We // arena allocated a bunch of IDs related to function call outputs, which we can now // safely release. GlobalInterner.releaseArena("functionOutput"); } } + +//------------------------------------------------------------------------------ +// Exporter +//------------------------------------------------------------------------------ +interface Map {[key:number]: V}; + +type ExportHandler = (blockChanges:Map) => void; +export type DiffConsumer = (changes:Readonly) => void; + +export class Exporter { + protected _diffTriggers:Map = {}; + protected _blocks:ID[] = []; + + triggerOnDiffs(blockId:ID, handler:DiffConsumer):void { + if(!this._diffTriggers[blockId]) this._diffTriggers[blockId] = createArray(); + if(this._diffTriggers[blockId].indexOf(handler) === -1) { + this._diffTriggers[blockId].push(handler); + } + if(this._blocks.indexOf(blockId) === -1) { + this._blocks.push(blockId); + } + } + + handle = (blockChanges:Map) => { + for(let blockId of this._blocks) { + let changes = blockChanges[blockId]; + if(changes && changes.length) { + let diffTriggers = this._diffTriggers[blockId]; + if(diffTriggers) { + let output:RawChange[] = createArray("exporterOutput"); + for(let change of changes) { + output.push(change.reverse()); + } + + for(let trigger of diffTriggers) { + trigger(output); + } + } + } + } + } +} diff --git a/src/watchers/html.ts b/src/watchers/html.ts new file mode 100644 index 000000000..575f50b77 --- /dev/null +++ b/src/watchers/html.ts @@ -0,0 +1,303 @@ +import {RawValue, RawChange} from "../runtime/runtime"; +import {Watcher} from "./watcher"; + +interface Map{[key:string]: V} + +interface RawRecord extends Map {} + +function accumulateChangesAs(changes:RawChange[]) { + let adds:Map = {}; + let removes:Map = {}; + + for(let {e, a, v, count} of changes) { + if(count === 1) { + let record = adds[e] = adds[e] || Object.create(null); + if(record[a]) throw new Error("accumulateChanges supports only a single value per attribute."); + record[a] = v; + } else { + let record = removes[e] = removes[e] || Object.create(null); + if(record[a]) throw new Error("accumulateChanges supports only a single value per attribute."); + record[a] = v; + } + } + + return {adds, removes}; +} + +interface Style extends Map {__size: number} +interface Instance extends HTMLElement {__element?: string, __styles?: string[], __sort?: number} + +class HTMLWatcher extends Watcher { + styles:Map = Object.create(null); + roots:Map = Object.create(null); + instances:Map = Object.create(null); + styleToInstances:Map = Object.create(null); + + getStyle(id:string) { + return this.styles[id] = this.styles[id] || {__size: 0}; + } + + getInstance(id:string, tagname:RawValue = "div"):Instance { + if(this.roots[id]) return this.roots[id]!; + return this.instances[id] = this.instances[id] || document.createElement(tagname as string); + } + + clearInstance(id:string) { + let instance = this.instances[id]; + if(instance && instance.parentElement) { + instance.parentElement.removeChild(instance); + } + this.instances[id] = undefined; + } + + getRoot(id:string, tagname:RawValue = "div"):Instance { + return this.roots[id] = this.roots[id] || document.createElement(tagname as string); + } + + clearRoot(id:string) { + this.roots[id] = undefined; + } + + insertChild(parent:Instance, child:Instance) { + let current; + for(let curIx = 0; curIx < parent.childNodes.length; curIx++) { + let cur = parent.childNodes[curIx] as Instance; + if(cur === child) continue; + if(cur.__sort !== undefined && cur.__sort > child.__sort) { + current = cur; + break; + } + } + + if(current) { + parent.insertBefore(child, current); + } else { + parent.appendChild(child); + } + } + + setStyleAttribute(style:Style, attribute:string, value:RawValue, count:-1|1) { + if(count === -1) { + if(!style[attribute]) throw new Error(`Cannot remove non-existent attribute '${attribute}'`); + if(style[attribute] !== value) throw new Error(`Cannot remove mismatched AV ${attribute}: ${value} (current: ${style[attribute]})`); + style[attribute] = undefined; + } else { + if(style[attribute]) throw new Error(`Cannot add already present attribute '${attribute}'`); + style[attribute] = value; + } + style.__size += count; + } + + addStyleInstance(styleId:string, instanceId:string) { + let instance = this.getInstance(instanceId); + let style = this.getStyle(styleId); + for(let prop in style) { + if(prop === "__size") continue; + instance.style[prop as any] = style[prop] as string; + } + if(this.styleToInstances[styleId]) this.styleToInstances[styleId]!.push(instanceId); + else this.styleToInstances[styleId] = [instanceId]; + + if(!instance.__styles) instance.__styles = []; + if(instance.__styles.indexOf(styleId) === -1) instance.__styles.push(styleId); + } + + removeStyleInstance(styleId:string, instanceId:string) { + let instance = this.instances[instanceId]; + if(!instance) return; + instance.removeAttribute("style"); + let ix = instance.__styles!.indexOf(styleId); + instance.__styles!.splice(ix, 1); + + for(let otherStyleId of instance.__styles!) { + let style = this.getStyle(otherStyleId); + for(let prop in style) { + if(prop === "__size") continue; + instance.style[prop as any] = style[prop] as string; + } + } + } + + setup() { + this.program + .block("Elements with no parents are roots.", ({find, record, lib, not}) => { + let elem = find("html/element"); + not(({find}) => { + find("html/element", {children: elem}); + }); + return [ + record("html/root", "html/instance", {element: elem, tagname: elem.tagname}) + ]; + }) + .block("Create an instance for each child of a rooted parent.", ({find, record, lib, not}) => { + let elem = find("html/element"); + let parentElem = find("html/element", {children: elem}); + let parent = find("html/instance", {element: parentElem}); + + return [ + record("html/instance", {element: elem, tagname: elem.tagname, parent}) + ]; + }) + .watch("Export all instances.", ({find, record}) => { + let instance = find("html/instance"); + return [ + record({tagname: instance.tagname, element: instance.element, instance}) + ]; + }) + .asDiffs((changes) => { + // console.log("Diffs: (html/instance)"); + // console.log(" " + changes.join("\n ")); + + let diff = accumulateChangesAs<{tagname:string, element:string, instance:string}>(changes); + for(let e of Object.keys(diff.removes)) { + let {instance:instanceId} = diff.removes[e]; + this.clearInstance(instanceId); + } + for(let e of Object.keys(diff.adds)) { + let {instance:instanceId, tagname, element} = diff.adds[e]; + let instance = this.getInstance(instanceId, tagname); + instance.__element = element; + } + }) + + .watch("Export roots.", ({find, record}) => { + let root = find("html/root"); + return [ + record({instance: root}) + ]; + }) + .asDiffs((changes) => { + // console.log("Diffs: (html/root)"); + // console.log(" " + changes.join("\n ")); + + for(let {e, a, v:rootId, count} of changes) { + if(count === 1) { + let root = this.roots[rootId] = this.getInstance(rootId); + document.body.appendChild(root); + } else { + let root = this.roots[rootId]; + if(root && root.parentElement) { + root.parentElement.removeChild(root); + } + } + } + }) + + .watch("Export instance parents.", ({find, record}) => { + let instance = find("html/instance"); + return [ + record({instance, parent: instance.parent}) + ]; + }) + .asDiffs((changes) => { + // console.log("Diffs: (html/parent)"); + // console.log(" " + changes.join("\n ")); + + let diff = accumulateChangesAs<{instance:string, parent:string}>(changes); + for(let e of Object.keys(diff.removes)) { + let {instance:instanceId, parent:parentId} = diff.removes[e]; + if(this.instances[parentId]) { + let instance = this.instances[instanceId]; + if(instance && instance.parentElement) { + instance.parentElement.removeChild(instance); + } + } + } + for(let e of Object.keys(diff.adds)) { + let {instance:instanceId, parent:parentId} = diff.adds[e]; + let instance = this.getInstance(instanceId); + this.insertChild(this.getInstance(parentId), instance); + } + }) + + .watch("Export html styles.", ({find, record, lib, not, lookup}) => { + let elem = find("html/element"); + let style = elem.style; + let {attribute, value} = lookup(style); + return [ + style.add(attribute, value) + ]; + }) + .asDiffs((changes) => { + // console.log("Diffs: (html/style)"); + // console.log(" " + changes.join("\n ")); + + let changed = []; + for(let {e:styleId, a, v, count} of changes) { + changed.push(styleId); + let style = this.getStyle(styleId); + this.setStyleAttribute(style, a, v, count); + + let instances = this.styleToInstances[styleId]; + if(instances) { + for(let instanceId of instances) { + let instance = this.getInstance(instanceId); + instance.style[a] = style[a] as any; + } + } + } + + for(let styleId of changed) { + let style = this.getStyle(styleId); + if(style.__size === 0) { + this.styles[styleId] = undefined; + } + } + }) + + .watch("Export element attributes.", ({find, record, lookup}) => { + let instance = find("html/instance"); + let elem = instance.element; + let {attribute, value} = lookup(elem); + return [ + instance.add(attribute, value) + ]; + }) + .asDiffs((changes) => { + // console.log("Diffs: (html/attribute)"); + // console.log(" " + changes.join("\n ")); + + for(let {e, a, v, count} of changes) { + let instance = this.instances[e]; + if(!instance) continue; + if(a === "text") { + instance.textContent = count > 0 ? v : undefined; + + } else if(a === "style") { + if(count > 0) { + this.addStyleInstance(v, e); + } else { + this.removeStyleInstance(v, e); + } + + } else if(a === "tagname") { + if(count < 0) continue; + if((""+v).toUpperCase() !== instance.tagName) { + // handled by html/instance + html/root + throw new Error("Unable to change element tagname."); + } + + } else if(a === "children") { + // Handled by html/parent + + } else if(a === "sort") { + instance.__sort = v; + let parent = instance.parentElement; + if(parent) { + this.insertChild(parent, instance); + } + + } else { + if(count === 1) { + instance.setAttribute(a, v); + } else { + instance.removeAttribute(a); + } + } + } + }); + // console.log(this); + } +} + +Watcher.register("html", HTMLWatcher); diff --git a/src/watchers/watcher.ts b/src/watchers/watcher.ts new file mode 100644 index 000000000..c056ed830 --- /dev/null +++ b/src/watchers/watcher.ts @@ -0,0 +1,32 @@ +import {Program, BlockFunction} from "../runtime/dsl"; + +export class Watcher { + protected static _registry:{[id:string]: typeof Watcher} = {}; + + static register(id:string, watcher:typeof Watcher) { + if(this._registry[id]) { + if(this._registry[id] === watcher) return; + throw new Error(`Attempting to overwrite existing watcher with id '${id}'`); + } + this._registry[id] = watcher; + } + + static unregister(id:string) { + delete this._registry[id]; + } + + static attach(id:string, program:Program) { + if(!this._registry[id]) throw new Error("Unable to attach unknown watcher."); + let watcher = new this._registry[id](program); + return watcher; + } + + + get program() { return this._program; } + + constructor(protected _program:Program) { + this.setup(); + } + + setup() {} +}