Compare commits

...

9 Commits

8 changed files with 151 additions and 51 deletions

View File

@ -296,14 +296,19 @@ ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')}
})`;
if (s.isDynamic) {
return wrap(t`__SYNDICATE__.Turn.active.assertDataflow(() => ({
target: currentSyndicateTarget,
assertion: ${assertion},
}));`);
if (s.test === void 0) {
return wrap(t`__SYNDICATE__.Turn.active.assertDataflow(() => ({ target: currentSyndicateTarget, assertion: ${assertion} }));`);
} else {
return wrap(t`__SYNDICATE__.Turn.active.assertDataflow(() => (${walk(s.test)})
? ({ target: currentSyndicateTarget, assertion: ${assertion} })
: ({ target: void 0, assertion: void 0 }));`);
}
} else {
return wrap(
t`__SYNDICATE__.Turn.active.replace(currentSyndicateTarget, void 0, ${assertion});`
);
if (s.test === void 0) {
return wrap(t`__SYNDICATE__.Turn.active.replace(currentSyndicateTarget, void 0, ${assertion});`);
} else {
return wrap(t`__SYNDICATE__.Turn.active.replace(currentSyndicateTarget, void 0, (${walk(s.test)}) ? ${assertion} : void 0);`);
}
}
});

View File

@ -75,6 +75,7 @@ export interface PseudoEventEndpointStatement extends GenericEventEndpointStatem
export interface AssertionEventEndpointStatement extends GenericEventEndpointStatement {
triggerType: 'asserted' | 'retracted' | 'message';
pattern: ValuePattern;
test?: Expr,
}
export type EventHandlerEndpointStatement =
@ -325,7 +326,11 @@ export class SyndicateParser {
atomString('message'))),
option(map(kw('snapshot'), _ => o.isDynamic = false)),
bind(o as AssertionEventEndpointStatement, 'pattern',
this.valuePattern(1, atom('=>'))),
this.valuePattern(1, atom('=>'),
seq(atom('when'), group('(', discard)))),
option(seq(atom('when'), group(
'(', bind(o as AssertionEventEndpointStatement, 'test',
this.expr())))),
this.mandatoryIfNotTerminal(
o, seq(atom('=>'), this.statement(o.body))))));
});

View File

@ -6,6 +6,7 @@ const currentSyndicateTarget = Syndicate.Dataspace.global;
(() => {
async function translateScripts() {
if (SchemaReady) await SchemaReady;
const syndicateScripts =
Array.from(document.getElementsByTagName('script'))

View File

@ -187,7 +187,7 @@ __SYNDICATE__.Turn.active.facet(() => {
}
}
}),
}),
})
}));
});`));
@ -206,8 +206,57 @@ __SYNDICATE__.Turn.active.facet(() => {
}
}
}),
}),
})
}));
});`));
});
describe('on', () => {
it('message with guard', () => expectCodeEqual(`
on message S.Focus(entity) when (isLast.value) => {
text.node.focus();
}`, `__SYNDICATE__.Turn.active.assertDataflow(() => (isLast.value) ?
({
target: currentSyndicateTarget,
assertion: __SYNDICATE__.Observe({
pattern: __SYNDICATE__.QuasiValue.finish((__SYNDICATE__.QuasiValue.ctor(S.Focus, (__SYNDICATE__.QuasiValue.lit(__SYNDICATE__.fromJS(entity)))))),
observer: __SYNDICATE__.Turn.ref({
message: (__vs) => {
if (Array.isArray(__vs)) {
text.node.focus();
}
}
}),
})
}) :
({
target: void 0,
assertion: void 0
}));`));
it('asserted with guard', () => expectCodeEqual(`on asserted P when (someTest) => x;`, `
__SYNDICATE__.Turn.active.assertDataflow(() => (someTest) ?
({
target: currentSyndicateTarget,
assertion: __SYNDICATE__.Observe({
pattern: __SYNDICATE__.QuasiValue.finish((__SYNDICATE__.QuasiValue.lit(__SYNDICATE__.fromJS(P)))),
observer: __SYNDICATE__.Turn.ref({
assert: (__vs, __handle) => {
if (Array.isArray(__vs)) {
x;
}
}
}),
})
}) :
({
target: void 0,
assertion: void 0
}));`));
});

View File

@ -1,7 +1,7 @@
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { IdentityMap, KeyedDictionary, stringify } from '@preserves/core';
import { IdentityMap, KeyedDictionary, stringify, strip } from '@preserves/core';
import { Index, IndexObserver } from './skeleton.js';
import { Actor, AnyValue, Assertion, DetailedAction, Entity, Facet, Handle, LocalAction, Ref, Turn } from './actor.js';
import { Observe, toObserve } from '../gen/dataspace.js';
@ -79,7 +79,7 @@ export class Dataspace implements Partial<Entity> {
readonly options: DataspaceOptions;
readonly index = new Index();
readonly handleMap = new IdentityMap<Handle, Assertion>();
readonly observerMap = new IdentityMap<Ref, DataspaceObserver>();
readonly observerMap = new KeyedDictionary<Ref, Observe, DataspaceObserver>();
readonly data = this;
constructor(options?: DataspaceOptions) {
@ -90,12 +90,11 @@ export class Dataspace implements Partial<Entity> {
const is_new = this.index.addAssertion(v, Turn.active);
this.options.tracer?.('+', v, this, is_new);
if (is_new) {
const o = toObserve(v);
const o = toObserve(strip(v));
if (o !== void 0) {
const target = o.observer;
const observer = new DataspaceObserver(target);
this.observerMap.set(target, observer);
this.index.addObserver(o.pattern, observer, Turn.active);
const io = new DataspaceObserver(o.observer);
this.observerMap.set(o, io);
this.index.addObserver(o.pattern, io, Turn.active);
}
if (this.options.dumpIndex ?? false) this.index.dump();
}
@ -109,12 +108,12 @@ export class Dataspace implements Partial<Entity> {
const is_last = this.index.removeAssertion(v, Turn.active);
this.options.tracer?.('-', v, this, is_last);
if (is_last) {
const o = toObserve(v);
const o = toObserve(strip(v));
if (o !== void 0) {
const io = this.observerMap.get(o.observer);
const io = this.observerMap.get(o);
if (io !== void 0) {
this.index.removeObserver(o.pattern, io, Turn.active);
this.observerMap.delete(o.observer);
this.observerMap.delete(o);
}
}
if (this.options.dumpIndex ?? false) this.index.dump();

View File

@ -1,7 +1,7 @@
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { canonicalString, KeyedDictionary, is, Record, RecordConstructorInfo, Value, _iterMap, DictionaryMap, Dictionary, EncodableDictionary } from '@preserves/core';
import { canonicalString, KeyedDictionary, is, Record, RecordConstructorInfo, Value, _iterMap, DictionaryMap, Dictionary, EncodableDictionary, unannotate } from '@preserves/core';
import { AnyValue, Ref } from './actor.js';
import * as P from '../gen/dataspacePatterns.js';
@ -10,6 +10,7 @@ export type Path = Array<AnyValue>;
export type Shape = string;
export function classOfValue(v: any): Shape | null {
v = unannotate(v);
if (Record.isRecord(v)) {
return constructorInfoSignature(Record.constructorInfo(v));
} else if (Array.isArray(v)) {
@ -38,6 +39,7 @@ export function constructorInfoSignature(ci: RecordConstructorInfo<Value>): stri
}
export function step(v: AnyValue, index: AnyValue): AnyValue | undefined {
v = unannotate(v);
const vMap = Dictionary.asMap<Ref>(v);
if (vMap) {
return vMap.get(index);
@ -227,7 +229,7 @@ export function drop_lit(p: P.Pattern): AnyValue | null {
target.push(walk(p));
}
}
function walk(p: P.Pattern): AnyValue {
switch (p._variant) {
case 'group':

View File

@ -70,7 +70,22 @@ function nodeInserter(n: number): PlaceholderAction {
switch (typeof f) {
case 'string': newNode = document.createTextNode(f); break;
case 'number': newNode = document.createTextNode('' + f); break;
default: newNode = f; break;
case 'object':
if (f !== null && 'nodeType' in f) {
newNode = f;
break;
}
/* fall through */
default: {
let info;
try {
info = '' + f;
} catch (_e) {
info = (f as any).toString();
}
newNode = document.createTextNode(`<ERROR: invalid HtmlFragment: ${info}>`);
break;
}
}
node.parentNode?.insertBefore(newNode, node);
}

View File

@ -20,23 +20,28 @@ type Wrapped = {
export type NodeGenerator = (t: HtmlTemplater) => ReturnType<HtmlTemplater>;
export class Widget implements EventTarget {
readonly nodeGenerator: NodeGenerator;
readonly facet: Facet;
private _node: ChildNode | null = null;
parentField: Dataflow.Field<ParentNode | null>;
callbacks = new Map<string, Map<EventListenerOrEventListenerObject, Wrapped>>();
get node(): ChildNode {
return this._node!;
}
constructor (node: ChildNode);
constructor (nodeGenerator: NodeGenerator);
constructor (template: string | HTMLTemplateElement, data: object);
constructor (arg0: NodeGenerator | string | HTMLTemplateElement, data?: object) {
constructor (arg0: ChildNode | NodeGenerator | string | HTMLTemplateElement, data?: object) {
let nodeGenerator: NodeGenerator;
if (data === void 0) {
this.nodeGenerator = arg0 as NodeGenerator;
if (typeof arg0 === 'function') {
nodeGenerator = arg0 as NodeGenerator;
} else {
nodeGenerator = () => [arg0 as ChildNode];
}
} else {
this.nodeGenerator = templateGenerator(arg0 as (string | HTMLTemplateElement), data);
nodeGenerator = templateGenerator(arg0 as (string | HTMLTemplateElement), data);
}
this.facet = Turn.activeFacet;
@ -49,7 +54,7 @@ export class Widget implements EventTarget {
const thisTemplate = template();
dataflow {
const nodes = this.nodeGenerator(thisTemplate);
const nodes = nodeGenerator(thisTemplate);
if (nodes.length !== 1) {
throw new Error(`@syndicate-lang/html2: Expected exactly one node from template`);
}
@ -59,24 +64,18 @@ export class Widget implements EventTarget {
throw new Error(`@syndicate-lang/html2: Node generator is not stable`);
}
}
}
field parentField: ParentNode | null = this._node?.parentNode ?? null;
this.parentField = parentField;
dataflow {
const p = this.parentField.value;
if (this.node.parentNode !== p) {
if (p === null) {
this.node.remove();
} else {
p.appendChild(this.node);
}
}
get _nodeAsParent(): ParentNode | null {
if (this._node && 'querySelector' in this._node) {
return this._node as unknown as ParentNode;
} else {
return null;
}
}
get parent(): ParentNode | null {
return this.parentField.value;
return this.node.parentNode;
}
set parent(p: string | ParentNode | null) {
@ -87,10 +86,34 @@ export class Widget implements EventTarget {
if (typeof p === 'string') {
p = wrt.querySelector(p);
}
this.parentField.value = p;
if (this.node.parentNode !== p) {
if (p === null) {
this.node.remove();
} else {
p.appendChild(this.node);
}
}
return this;
}
querySelector(selector: string): Widget | null;
querySelector<T extends Widget>(selector: string, ctor: { new(e: Element): T }): T | null;
querySelector<T extends Widget>(selector: string, ctor?: { new(e: Element): T }): Widget | null {
const e = this._nodeAsParent?.querySelector(selector);
return e ? new (ctor ?? Widget)(e) : null;
}
querySelectorAll(selector: string): Widget[];
querySelectorAll<T extends Widget>(selector: string, ctor: { new(e: Element): T }): T[];
querySelectorAll<T extends Widget>(selector: string, ctor?: { new(e: Element): T }): Widget[] {
const es = this._nodeAsParent?.querySelectorAll(selector);
const ws: Widget[] = [];
if (es) es.forEach(e => ws.push(new (ctor ?? Widget)(e)));
return ws;
}
on(type: string, callback: EventListenerOrEventListenerObject): this {
this.addEventListener(type, callback);
return this;
@ -173,20 +196,21 @@ export class ValueWidget extends Widget {
this._valueAsNumber = valueAsNumber;
if ('value' in this.node) {
const readValues = (n: any) => {
this.suppressCycleWarning();
this._value.value = n?.value ?? '';
this._valueAsNumber.value = n?.valueAsNumber ?? NaN;
};
this.on(triggerEvent, e => readValues(e.target));
readValues(this.node);
this.on(triggerEvent, () => this.readValues());
this.readValues();
dataflow { this.valueAsNumber = this._valueAsNumber.value; }
dataflow { this.value = this._value.value; }
}
}
readValues() {
const n = this.node as any;
this.suppressCycleWarning();
this._value.value = n.value ?? '';
this._valueAsNumber.value = n.valueAsNumber ?? NaN;
}
get value(): string {
return this._value.value;
}