@getodk/xforms-engine
Advanced tools
Comparing version 0.1.1 to 0.2.0
import { XFormDefinition } from '../XFormDefinition.ts'; | ||
import { DependencyContext } from '../expression/DependencyContext.ts'; | ||
import { ParsedTokenList, TokenListParser } from '../lib/TokenListParser.ts'; | ||
import { RepeatElementDefinition } from './RepeatElementDefinition.ts'; | ||
import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts'; | ||
@@ -8,3 +10,2 @@ import { InputDefinition } from './control/InputDefinition.ts'; | ||
import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts'; | ||
import { RepeatGroupDefinition } from './group/RepeatGroupDefinition.ts'; | ||
import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts'; | ||
@@ -16,3 +17,4 @@ | ||
} | ||
type SupportedBodyElementDefinition = RepeatGroupDefinition | LogicalGroupDefinition | PresentationGroupDefinition | StructuralGroupDefinition | InputDefinition | AnySelectDefinition; | ||
export type ControlElementDefinition = AnySelectDefinition | InputDefinition; | ||
type SupportedBodyElementDefinition = RepeatElementDefinition | LogicalGroupDefinition | PresentationGroupDefinition | StructuralGroupDefinition | ControlElementDefinition; | ||
export type AnyBodyElementDefinition = SupportedBodyElementDefinition | UnsupportedBodyElementDefinition; | ||
@@ -24,5 +26,2 @@ export type BodyElementDefinitionArray = readonly AnyBodyElementDefinition[]; | ||
}>; | ||
export type NonRepeatGroupElementDefinition = Exclude<AnyGroupElementDefinition, { | ||
readonly type: 'repeat-group'; | ||
}>; | ||
export declare const groupElementDefinition: (element: AnyBodyElementDefinition) => AnyGroupElementDefinition | null; | ||
@@ -43,2 +42,4 @@ export type AnyControlElementDefinition = Extract<AnyBodyElementDefinition, { | ||
} | ||
declare const bodyClassParser: TokenListParser<"pages", "pages">; | ||
export type BodyClassList = ParsedTokenList<typeof bodyClassParser>; | ||
export declare class BodyDefinition extends DependencyContext { | ||
@@ -48,2 +49,19 @@ protected readonly form: XFormDefinition; | ||
readonly element: Element; | ||
/** | ||
* @todo this class is already an oddity in that it's **like** an element | ||
* definition, but it isn't one itself. Adding this property here emphasizes | ||
* that awkwardness. It also extends the applicable scope where instances of | ||
* this class are accessed. While it's still ephemeral, it's anticipated that | ||
* this extension might cause some disomfort. If so, the most plausible | ||
* alternative is an additional refactor to: | ||
* | ||
* 1. Introduce a `BodyElementDefinition` sublass for `<h:body>`. | ||
* 2. Disambiguate the respective names of those, in some reasonable way. | ||
* 3. Add a layer of indirection between this class and that new body element | ||
* definition's class. | ||
* 4. At that point, we may as well prioritize the little bit of grunt work to | ||
* pass the `BodyDefinition` instance by reference rather than assigning it | ||
* to anything. | ||
*/ | ||
readonly classes: BodyClassList; | ||
readonly elements: readonly AnyBodyElementDefinition[]; | ||
@@ -55,5 +73,4 @@ protected readonly elementsByReference: BodyElementMap; | ||
getBodyElement(reference: string): AnyBodyElementDefinition | null; | ||
getRepeatGroup(reference: string): RepeatGroupDefinition | null; | ||
toJSON(): Omit<this, "toJSON" | "form" | "isTranslated" | "registerDependentExpression" | "dependencyExpressions" | "getBodyElement" | "getRepeatGroup">; | ||
toJSON(): Omit<this, "toJSON" | "form" | "isTranslated" | "registerDependentExpression" | "dependencyExpressions" | "getBodyElement">; | ||
} | ||
export {}; |
import { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import { ParsedTokenList } from '../../lib/TokenListParser.ts'; | ||
import { BodyElementParentContext } from '../BodyDefinition.ts'; | ||
@@ -14,2 +15,3 @@ import { BodyElementDefinition } from '../BodyElementDefinition.ts'; | ||
readonly hint: HintDefinition | null; | ||
abstract readonly appearances: ParsedTokenList<any>; | ||
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element); | ||
@@ -16,0 +18,0 @@ } |
@@ -0,1 +1,4 @@ | ||
import { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import { BodyElementParentContext } from '../BodyDefinition.ts'; | ||
import { InputAppearanceDefinition } from '../appearance/inputAppearanceParser.ts'; | ||
import { ControlDefinition } from './ControlDefinition.ts'; | ||
@@ -6,2 +9,4 @@ | ||
readonly type = "input"; | ||
readonly appearances: InputAppearanceDefinition; | ||
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element); | ||
} |
@@ -5,2 +5,3 @@ import { CollectionValues } from '../../../../../common/types/collections/CollectionValues.ts'; | ||
import { AnyBodyElementDefinition, BodyElementParentContext } from '../../BodyDefinition.ts'; | ||
import { SelectAppearanceDefinition } from '../../appearance/selectAppearanceParser.ts'; | ||
import { ControlDefinition } from '../ControlDefinition.ts'; | ||
@@ -10,3 +11,11 @@ import { ItemDefinition } from './ItemDefinition.ts'; | ||
declare const selectLocalNames: Set<"select" | "rank" | "select1">; | ||
/** | ||
* @todo We were previously a bit overzealous about introducing `<rank>` support | ||
* here. It'll likely still fit, but we should approach it with more intention. | ||
* | ||
* @todo `<trigger>` is *almost* reasonable to support here too. The main | ||
* hesitation is that its single, implicit "item" does not have a distinct | ||
* <label>, and presumably has different UX **and translation** considerations. | ||
*/ | ||
declare const selectLocalNames: Set<"select" | "select1">; | ||
export type SelectType = CollectionValues<typeof selectLocalNames>; | ||
@@ -20,2 +29,3 @@ export interface SelectElement extends LocalNamedElement<SelectType> { | ||
readonly element: SelectElement; | ||
readonly appearances: SelectAppearanceDefinition; | ||
readonly itemset: ItemsetDefinition | null; | ||
@@ -22,0 +32,0 @@ readonly items: readonly ItemDefinition[]; |
import { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import { BodyElementDefinitionArray, BodyElementParentContext } from '../BodyDefinition.ts'; | ||
import { BodyElementDefinition } from '../BodyElementDefinition.ts'; | ||
import { StructureElementAppearanceDefinition } from '../appearance/structureElementAppearanceParser.ts'; | ||
import { LabelDefinition } from '../text/LabelDefinition.ts'; | ||
@@ -19,6 +20,2 @@ | ||
* have a `ref` | ||
* - `repeat-group` is not mentioned by the spec; it is an extension of | ||
* `logical-group`, wherein its `ref` is the same as its immediate `<repeat>` | ||
* child's `nodeset` (usage of each attribute is normalized during | ||
* initialization, in {@link XFormDOM}) | ||
* - `structural-group` is any `<group>` element which does not satisfy any of | ||
@@ -31,3 +28,2 @@ * the other usage scenarios; this isn't exactly the terminology used, but is | ||
* | ||
* - `<group ref="$ref"><repeat nodeset="$ref">` -> `repeat-group`, else | ||
* - `<group ref="$ref">` -> `logical-group`, else | ||
@@ -37,3 +33,3 @@ * - `<group><label>` -> `presentation-group`, else | ||
*/ | ||
export type GroupType = 'logical-group' | 'presentation-group' | 'repeat-group' | 'structural-group'; | ||
export type GroupType = 'logical-group' | 'presentation-group' | 'structural-group'; | ||
export declare abstract class BaseGroupDefinition<Type extends GroupType> extends BodyElementDefinition<Type> { | ||
@@ -45,2 +41,3 @@ private static groupTypes; | ||
readonly reference: string | null; | ||
readonly appearances: StructureElementAppearanceDefinition; | ||
readonly label: LabelDefinition | null; | ||
@@ -50,3 +47,1 @@ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element, children?: BodyElementDefinitionArray); | ||
} | ||
export declare const repeatGroup: <T extends BaseGroupDefinition<any>>(groupDefinition: T) => Extract<T, BaseGroupDefinition<'repeat-group'>> | null; | ||
export declare const nonRepeatGroup: <T extends BaseGroupDefinition<any>>(groupDefinition: T) => Exclude<T, BaseGroupDefinition<'repeat-group'>> | null; |
@@ -6,2 +6,3 @@ import { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import { BaseGroupDefinition } from '../group/BaseGroupDefinition.ts'; | ||
import { RepeatElementDefinition } from '../RepeatElementDefinition.ts'; | ||
import { TextElement, TextElementOwner, TextElementDefinition } from './TextElementDefinition.ts'; | ||
@@ -16,2 +17,3 @@ | ||
static forControl(form: XFormDefinition, control: AnyControlDefinition): LabelDefinition | null; | ||
static forRepeatGroup(form: XFormDefinition, repeat: RepeatElementDefinition): LabelDefinition | null; | ||
static forGroup(form: XFormDefinition, group: BaseGroupDefinition<any>): LabelDefinition | null; | ||
@@ -18,0 +20,0 @@ static forItem(form: XFormDefinition, item: ItemDefinition): LabelDefinition | null; |
@@ -5,6 +5,6 @@ import { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import { BodyElementDefinition } from '../BodyElementDefinition.ts'; | ||
import { InputDefinition } from '../control/InputDefinition.ts'; | ||
import { RepeatElementDefinition } from '../RepeatElementDefinition.ts'; | ||
import { AnyControlDefinition } from '../control/ControlDefinition.ts'; | ||
import { ItemDefinition } from '../control/select/ItemDefinition.ts'; | ||
import { ItemsetDefinition } from '../control/select/ItemsetDefinition.ts'; | ||
import { AnySelectDefinition } from '../control/select/SelectDefinition.ts'; | ||
import { TextElementOutputPart } from './TextElementOutputPart.ts'; | ||
@@ -18,3 +18,3 @@ import { TextElementReferencePart } from './TextElementReferencePart.ts'; | ||
} | ||
export type TextElementOwner = AnyGroupElementDefinition | AnySelectDefinition | InputDefinition | ItemDefinition | ItemsetDefinition; | ||
export type TextElementOwner = AnyControlDefinition | AnyGroupElementDefinition | ItemDefinition | ItemsetDefinition | RepeatElementDefinition; | ||
export type TextElementChild = TextElementOutputPart | TextElementStaticPart; | ||
@@ -21,0 +21,0 @@ export declare abstract class TextElementDefinition<Type extends TextElementType> extends BodyElementDefinition<Type> { |
import { AnyNodeDefinition } from '../model/NodeDefinition.ts'; | ||
import { InstanceNodeType } from './node-types.js'; | ||
import { NodeAppearances } from './NodeAppearances.ts'; | ||
import { TextRange } from './TextRange.ts'; | ||
import { InstanceNodeType } from './node-types.ts'; | ||
@@ -112,2 +113,6 @@ export interface BaseNodeState { | ||
/** | ||
* @see {@link TokenListParser} for details. | ||
*/ | ||
readonly appearances: NodeAppearances<any> | null; | ||
/** | ||
* Each node has a definition which specifies aspects of the node defined in | ||
@@ -114,0 +119,0 @@ * the form. These aspects include (but are not limited to) the node's data |
@@ -1,4 +0,5 @@ | ||
import { NonRepeatGroupElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { AnyGroupElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { SubtreeDefinition } from '../model/SubtreeDefinition.ts'; | ||
import { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import { NodeAppearances } from './NodeAppearances.ts'; | ||
import { RootNode } from './RootNode.ts'; | ||
@@ -14,4 +15,5 @@ import { GeneralChildNode, GeneralParentNode } from './hierarchy.ts'; | ||
export interface GroupDefinition extends SubtreeDefinition { | ||
readonly bodyElement: NonRepeatGroupElementDefinition; | ||
readonly bodyElement: AnyGroupElementDefinition; | ||
} | ||
export type GroupNodeAppearances = NodeAppearances<GroupDefinition>; | ||
/** | ||
@@ -22,2 +24,3 @@ * A node corresponding to an XForms `<group>`. | ||
readonly nodeType: 'group'; | ||
readonly appearances: GroupNodeAppearances; | ||
readonly definition: GroupDefinition; | ||
@@ -24,0 +27,0 @@ readonly root: RootNode; |
import { RepeatInstanceDefinition } from '../model/RepeatInstanceDefinition.ts'; | ||
import { RepeatTemplateDefinition } from '../model/RepeatTemplateDefinition.ts'; | ||
import { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import { NodeAppearances } from './NodeAppearances.ts'; | ||
import { RepeatRangeNode } from './RepeatRangeNode.ts'; | ||
@@ -15,4 +16,6 @@ import { RootNode } from './RootNode.ts'; | ||
export type RepeatDefinition = RepeatInstanceDefinition | RepeatTemplateDefinition; | ||
export type RepeatInstanceNodeAppearances = NodeAppearances<RepeatDefinition>; | ||
export interface RepeatInstanceNode extends BaseNode { | ||
readonly nodeType: 'repeat-instance'; | ||
readonly appearances: RepeatInstanceNodeAppearances; | ||
readonly definition: RepeatDefinition; | ||
@@ -19,0 +22,0 @@ readonly root: RootNode; |
@@ -1,3 +0,4 @@ | ||
import { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts'; | ||
import { RepeatRangeDefinition } from '../model/RepeatRangeDefinition.ts'; | ||
import { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import { NodeAppearances } from './NodeAppearances.ts'; | ||
import { RepeatInstanceNode } from './RepeatInstanceNode.ts'; | ||
@@ -23,2 +24,3 @@ import { RootNode } from './RootNode.ts'; | ||
} | ||
export type RepeatRangeNodeAppearances = NodeAppearances<RepeatRangeDefinition>; | ||
/** | ||
@@ -90,3 +92,4 @@ * Represents a contiguous set of zero or more {@link RepeatInstanceNode}s | ||
readonly nodeType: 'repeat-range'; | ||
readonly definition: RepeatSequenceDefinition; | ||
readonly appearances: RepeatRangeNodeAppearances; | ||
readonly definition: RepeatRangeDefinition; | ||
readonly root: RootNode; | ||
@@ -93,0 +96,0 @@ readonly parent: GeneralParentNode; |
@@ -0,1 +1,2 @@ | ||
import { BodyClassList } from '../body/BodyDefinition.ts'; | ||
import { RootDefinition } from '../model/RootDefinition.ts'; | ||
@@ -22,2 +23,20 @@ import { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
readonly nodeType: 'root'; | ||
/** | ||
* @todo this along with {@link classes} is... awkward. | ||
*/ | ||
readonly appearances: null; | ||
/** | ||
* @todo This is another odd deviation in {@link RootNode}. Unlike | ||
* {@link languages}, it doesn't feel particularly **essential**. While it | ||
* would deviate from XForms spec terminology, it seems like it _might be | ||
* reasonable_ to instead convey `<h:body class="...">` as | ||
* {@link RootNode.appearances} in the client interface. They do have slightly | ||
* different spec semantics (i.e. a body class can be anything, to trigger | ||
* styling in a form UI). But the **most likely anticipated** use case in Web | ||
* Forms would be the "pages" class, and perhaps "theme-grid". The former is | ||
* definitely conceptually similar to a XForms `appearance` (albeit | ||
* form-global, which is not a spec concept). The latter does as well, and we | ||
* already anticipate applying that concept in non-form-global ways. | ||
*/ | ||
readonly classes: BodyClassList; | ||
readonly definition: RootDefinition; | ||
@@ -24,0 +43,0 @@ readonly root: RootNode; |
import { AnySelectDefinition } from '../body/control/select/SelectDefinition.ts'; | ||
import { ValueNodeDefinition } from '../model/ValueNodeDefinition.ts'; | ||
import { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import { NodeAppearances } from './NodeAppearances.ts'; | ||
import { RootNode } from './RootNode.ts'; | ||
@@ -35,4 +36,6 @@ import { TextRange } from './TextRange.ts'; | ||
} | ||
export type SelectNodeAppearances = NodeAppearances<SelectDefinition>; | ||
export interface SelectNode extends BaseNode { | ||
readonly nodeType: 'select'; | ||
readonly appearances: SelectNodeAppearances; | ||
readonly definition: SelectDefinition; | ||
@@ -39,0 +42,0 @@ readonly root: RootNode; |
import { InputDefinition } from '../body/control/InputDefinition.ts'; | ||
import { ValueNodeDefinition } from '../model/ValueNodeDefinition.ts'; | ||
import { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import { NodeAppearances } from './NodeAppearances.ts'; | ||
import { RootNode } from './RootNode.ts'; | ||
@@ -21,2 +22,3 @@ import { GeneralParentNode } from './hierarchy.ts'; | ||
} | ||
export type StringNodeAppearances = NodeAppearances<StringDefinition>; | ||
/** | ||
@@ -31,2 +33,3 @@ * A node which can be assigned a string/text value. A string node **MAY** | ||
readonly nodeType: 'string'; | ||
readonly appearances: StringNodeAppearances; | ||
readonly definition: StringDefinition; | ||
@@ -33,0 +36,0 @@ readonly root: RootNode; |
@@ -49,2 +49,3 @@ import { SubtreeDefinition as BaseSubtreeDefinition } from '../model/SubtreeDefinition.ts'; | ||
readonly nodeType: 'subtree'; | ||
readonly appearances: null; | ||
readonly definition: SubtreeDefinition; | ||
@@ -51,0 +52,0 @@ readonly root: RootNode; |
@@ -25,12 +25,19 @@ import { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
export type AnyDescendantNode = DescendantNode<DescendantNodeDefinition, DescendantNodeStateSpec<any>, any>; | ||
interface DescendantNodeOptions { | ||
readonly computeReference?: Accessor<string>; | ||
} | ||
export declare abstract class DescendantNode<Definition extends DescendantNodeDefinition, Spec extends DescendantNodeStateSpec<any>, Child extends AnyChildNode | null = null> extends InstanceNode<Definition, Spec, Child> implements BaseNode, EvaluationContext, SubscribableDependency { | ||
readonly parent: DescendantNodeParent<Definition>; | ||
readonly definition: Definition; | ||
readonly hasReadonlyAncestor: Accessor<boolean>; | ||
readonly isSelfReadonly: Accessor<boolean>; | ||
readonly isReadonly: Accessor<boolean>; | ||
readonly hasNonRelevantAncestor: Accessor<boolean>; | ||
readonly isSelfRelevant: Accessor<boolean>; | ||
readonly isRelevant: Accessor<boolean>; | ||
protected readonly isRequired: Accessor<boolean>; | ||
readonly root: Root; | ||
readonly evaluator: XFormsXPathEvaluator; | ||
readonly contextNode: Element; | ||
constructor(parent: DescendantNodeParent<Definition>, definition: Definition); | ||
protected computeChildStepReference(parent: DescendantNodeParent<Definition>): string; | ||
protected abstract computeReference(parent: DescendantNodeParent<Definition>, definition: Definition): string; | ||
protected buildSharedStateSpec(parent: DescendantNodeParent<Definition>, definition: Definition): DescendantNodeSharedStateSpec; | ||
constructor(parent: DescendantNodeParent<Definition>, definition: Definition, options?: DescendantNodeOptions); | ||
protected createContextNode(parentContextNode: Element, nodeName: string): Element; | ||
@@ -78,1 +85,2 @@ /** | ||
} | ||
export {}; |
import { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import { Accessor, Signal } from 'solid-js'; | ||
import { BaseNode } from '../../client/BaseNode.ts'; | ||
import { NodeAppearances } from '../../client/NodeAppearances.ts'; | ||
import { InstanceNodeType } from '../../client/node-types.ts'; | ||
@@ -31,5 +32,2 @@ import { TextRange } from '../../index.ts'; | ||
type AnyInstanceNode = InstanceNode<AnyNodeDefinition, InstanceNodeStateSpec<any>, any>; | ||
interface InitializedStateOptions<T, K extends keyof T> { | ||
readonly uninitializedFallback: T[K]; | ||
} | ||
/** | ||
@@ -42,2 +40,10 @@ * This type has the same effect as {@link MaterializedChildren}, but abstractly | ||
}; | ||
interface ComputableReferenceNode { | ||
readonly parent: AnyParentNode | null; | ||
readonly definition: AnyNodeDefinition; | ||
} | ||
type ComputeInstanceNodeReference = <This extends ComputableReferenceNode>(this: This, parent: This['parent'], definition: This['definition']) => string; | ||
export interface InstanceNodeOptions { | ||
readonly computeReference?: () => string; | ||
} | ||
export declare abstract class InstanceNode<Definition extends AnyNodeDefinition, Spec extends InstanceNodeStateSpec<any>, Child extends AnyChildNode | null = null> implements BaseNode, EvaluationContext, SubscribableDependency { | ||
@@ -47,25 +53,9 @@ readonly engineConfig: InstanceConfig; | ||
readonly definition: Definition; | ||
protected readonly isStateInitialized: Accessor<boolean>; | ||
protected abstract readonly state: SharedNodeState<Spec>; | ||
protected abstract readonly engineState: EngineState<Spec>; | ||
/** | ||
* Provides a generalized mechanism for accessing a reactive state value | ||
* during a node's construction, while {@link engineState} is still being | ||
* defined and thus isn't assigned. | ||
* | ||
* The fallback value specified in {@link options} will be returned on access | ||
* until {@link isStateInitialized} returns true. This ensures: | ||
* | ||
* - a value of the expected type will be available | ||
* - any read access will become reactive to the actual state, once it has | ||
* been initialized and {@link engineState} is assigned | ||
* | ||
* @todo This is one among several chicken/egg problems encountered trying to | ||
* support state initialization in which some aspects of the state derive from | ||
* other aspects of it. It would be nice to dispense with this entirely. But | ||
* if it must persist, we should also consider replacing the method with a | ||
* direct accessor once state initialization completes, so the initialized | ||
* check is only called until it becomes impertinent. | ||
* @package Exposed on every node type to facilitate inheritance, as well as | ||
* conditional behavior for value nodes. | ||
*/ | ||
protected getInitializedState<K extends keyof EngineState<Spec>>(key: K, options: InitializedStateOptions<EngineState<Spec>, K>): EngineState<Spec>[K]; | ||
abstract readonly hasReadonlyAncestor: Accessor<boolean>; | ||
/** | ||
@@ -75,3 +65,3 @@ * @package Exposed on every node type to facilitate inheritance, as well as | ||
*/ | ||
get isReadonly(): boolean; | ||
abstract readonly isReadonly: Accessor<boolean>; | ||
/** | ||
@@ -81,5 +71,11 @@ * @package Exposed on every node type to facilitate inheritance, as well as | ||
*/ | ||
get isRelevant(): boolean; | ||
abstract readonly hasNonRelevantAncestor: Accessor<boolean>; | ||
/** | ||
* @package Exposed on every node type to facilitate inheritance, as well as | ||
* conditional behavior for value nodes. | ||
*/ | ||
abstract readonly isRelevant: Accessor<boolean>; | ||
readonly nodeId: NodeID; | ||
abstract readonly nodeType: InstanceNodeType; | ||
abstract readonly appearances: NodeAppearances<Definition>; | ||
abstract readonly currentState: InstanceNodeCurrentState<Spec, Child>; | ||
@@ -89,5 +85,7 @@ abstract readonly root: Root; | ||
readonly scope: ReactiveScope; | ||
get contextReference(): string; | ||
readonly computeReference: ComputeInstanceNodeReference; | ||
protected readonly computeChildStepReference: ComputeInstanceNodeReference; | ||
readonly contextReference: () => string; | ||
abstract readonly contextNode: Element; | ||
constructor(engineConfig: InstanceConfig, parent: AnyParentNode | null, definition: Definition); | ||
constructor(engineConfig: InstanceConfig, parent: AnyParentNode | null, definition: Definition, options?: InstanceNodeOptions); | ||
/** | ||
@@ -104,5 +102,4 @@ * @package This presently serves a few internal use cases, where certain | ||
abstract getChildren(this: AnyInstanceNode): readonly AnyChildNode[]; | ||
protected abstract computeReference(parent: AnyInstanceNode | null, definition: Definition): string; | ||
getNodeByReference(this: AnyNode, visited: WeakSet<AnyNode>, dependencyReference: string): SubscribableDependency | null; | ||
getSubscribableDependencyByReference(this: AnyNode, reference: string): SubscribableDependency | null; | ||
getNodesByReference(this: AnyNode, visited: WeakSet<AnyNode>, dependencyReference: string): readonly SubscribableDependency[]; | ||
getSubscribableDependenciesByReference(this: AnyNode, reference: string): readonly SubscribableDependency[]; | ||
/** | ||
@@ -109,0 +106,0 @@ * This is a default implementation suitable for most node types. The rest |
import { Accessor } from 'solid-js'; | ||
import { GroupDefinition, GroupNode } from '../client/GroupNode.ts'; | ||
import { GroupDefinition, GroupNode, GroupNodeAppearances } from '../client/GroupNode.ts'; | ||
import { TextRange } from '../index.ts'; | ||
@@ -25,8 +25,8 @@ import { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts'; | ||
protected engineState: EngineState<GroupStateSpec>; | ||
readonly nodeType = "group"; | ||
readonly appearances: GroupNodeAppearances; | ||
readonly currentState: MaterializedChildren<CurrentState<GroupStateSpec>, GeneralChildNode>; | ||
readonly nodeType = "group"; | ||
constructor(parent: GeneralParentNode, definition: GroupDefinition); | ||
protected computeReference(parent: GeneralParentNode): string; | ||
getChildren(): readonly GeneralChildNode[]; | ||
} | ||
export {}; |
import { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import { Accessor } from 'solid-js'; | ||
import { ReactiveScope } from '../../lib/reactivity/scope.ts'; | ||
@@ -28,9 +29,9 @@ import { SubscribableDependency } from './SubscribableDependency.ts'; | ||
*/ | ||
get contextReference(): string; | ||
readonly contextReference: Accessor<string>; | ||
readonly contextNode: Node; | ||
/** | ||
* Resolves a nodeset reference, possibly relative to the | ||
* {@link EvaluationContext.contextNode}. | ||
* Resolves nodes corresponding to the provided node-set reference, possibly | ||
* relative to the {@link EvaluationContext.contextNode}. | ||
*/ | ||
readonly getSubscribableDependencyByReference: (reference: string) => SubscribableDependency | null; | ||
getSubscribableDependenciesByReference(reference: string): readonly SubscribableDependency[]; | ||
} |
@@ -18,4 +18,4 @@ import { ReactiveScope } from '../../lib/reactivity/scope.ts'; | ||
readonly contextNode: Element; | ||
get isReadonly(): boolean; | ||
get isRelevant(): boolean; | ||
isReadonly(): boolean; | ||
isRelevant(): boolean; | ||
readonly encodeValue: (this: unknown, runtimeValue: RuntimeValue) => InstanceValue; | ||
@@ -22,0 +22,0 @@ readonly decodeValue: (this: unknown, instanceValue: InstanceValue) => RuntimeValue; |
import { Accessor } from 'solid-js'; | ||
import { RepeatDefinition, RepeatInstanceNode } from '../client/RepeatInstanceNode.ts'; | ||
import { RepeatDefinition, RepeatInstanceNode, RepeatInstanceNodeAppearances } from '../client/RepeatInstanceNode.ts'; | ||
import { TextRange } from '../index.ts'; | ||
@@ -33,6 +33,30 @@ import { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts'; | ||
protected engineState: EngineState<RepeatInstanceStateSpec>; | ||
/** | ||
* @todo Should we special case repeat `readonly` inheritance the same way | ||
* we do for `relevant`? | ||
* | ||
* @see {@link hasNonRelevantAncestor} | ||
*/ | ||
readonly hasReadonlyAncestor: Accessor<boolean>; | ||
/** | ||
* A repeat instance can inherit non-relevance, just like any other node. That | ||
* inheritance is derived from the repeat instance's parent node in the | ||
* primary instance XML/DOM tree (and would be semantically expected to do so | ||
* even if we move away from that implementation detail). | ||
* | ||
* Since {@link RepeatInstance.parent} is a {@link RepeatRange}, which is a | ||
* runtime data model fiction that does not exist in that hierarchy, we pass | ||
* this call through, allowing the {@link RepeatRange} to check the actual | ||
* primary instance parent node's relevance state. | ||
* | ||
* @todo Should we apply similar reasoning in {@link hasReadonlyAncestor}? | ||
*/ | ||
readonly hasNonRelevantAncestor: Accessor<boolean>; | ||
readonly nodeType = "repeat-instance"; | ||
/** | ||
* @see {@link RepeatRange.appearances} | ||
*/ | ||
readonly appearances: RepeatInstanceNodeAppearances; | ||
readonly currentState: MaterializedChildren<CurrentState<RepeatInstanceStateSpec>, GeneralChildNode>; | ||
constructor(parent: RepeatRange, definition: RepeatDefinition, options: RepeatInstanceOptions); | ||
protected computeReference(parent: RepeatRange): string; | ||
protected initializeContextNode(parentContextNode: Element, nodeName: string): Element; | ||
@@ -39,0 +63,0 @@ subscribe(): void; |
import { Accessor } from 'solid-js'; | ||
import { RepeatRangeNode } from '../client/RepeatRangeNode.ts'; | ||
import { RepeatRangeNode, RepeatRangeNodeAppearances } from '../client/RepeatRangeNode.ts'; | ||
import { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts'; | ||
@@ -7,3 +7,3 @@ import { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts'; | ||
import { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts'; | ||
import { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts'; | ||
import { RepeatRangeDefinition } from '../model/RepeatRangeDefinition.ts'; | ||
import { RepeatDefinition, RepeatInstance } from './RepeatInstance.ts'; | ||
@@ -25,3 +25,3 @@ import { Root } from './Root.ts'; | ||
} | ||
export declare class RepeatRange extends DescendantNode<RepeatSequenceDefinition, RepeatRangeStateSpec, RepeatInstance> implements RepeatRangeNode, EvaluationContext, SubscribableDependency { | ||
export declare class RepeatRange extends DescendantNode<RepeatRangeDefinition, RepeatRangeStateSpec, RepeatInstance> implements RepeatRangeNode, EvaluationContext, SubscribableDependency { | ||
/** | ||
@@ -53,8 +53,87 @@ * A repeat range doesn't have a corresponding primary instance element of its | ||
protected engineState: EngineState<RepeatRangeStateSpec>; | ||
/** | ||
* @todo Should we special case repeat `readonly` state the same way | ||
* we do for `relevant`? | ||
* | ||
* @see {@link isSelfRelevant} | ||
*/ | ||
isSelfReadonly: Accessor<boolean>; | ||
private readonly emptyRangeEvaluationContext; | ||
/** | ||
* @see {@link isSelfRelevant} | ||
*/ | ||
private readonly isEmptyRangeSelfRelevant; | ||
/** | ||
* A repeat range does not exist in the primary instance tree. A `relevant` | ||
* expression applies to each {@link RepeatInstance} child of the repeat | ||
* range. Determining whether a repeat range itself "is relevant" isn't a | ||
* concept the spec addresses, but it may be used by clients to determine | ||
* whether to allow interaction with the range (e.g. by adding a repeat | ||
* instance, or presenting the range's label when empty). | ||
* | ||
* As a naive first pass, it seems like the heuristic for this should be: | ||
* | ||
* 1. Does the repeat range have any repeat instance children? | ||
* | ||
* - If yes, go to 2. | ||
* - If no, go to 3. | ||
* | ||
* 2. Does one or more of those children return `true` for the node's | ||
* `relevant` expression (i.e. is the repeat instance "self relevant")? | ||
* | ||
* 3. Does the relevant expression return `true` for the repeat range itself | ||
* (where, at least for now, the context of that evaluation would be the | ||
* repeat range's {@link anchorNode} to ensure correct relative expressions | ||
* resolve correctly)? | ||
* | ||
* @todo While (3) is proactively implemented, there isn't presently a test | ||
* exercising it. It felt best for now to surface this for discussion in | ||
* review to validate that it's going in the right direction. | ||
* | ||
* @todo While (2) **is actually tested**, the tests currently in place behave | ||
* the same way with only the logic for (3), regardless of whether the repeat | ||
* range actually has any repeat instance children. It's unclear (a) if that's | ||
* a preferable simplification and (b) how that might affect performance (in | ||
* theory it could vary depending on form structure and runtime state). | ||
*/ | ||
readonly isSelfRelevant: Accessor<boolean>; | ||
readonly nodeType = "repeat-range"; | ||
/** | ||
* @todo RepeatRange*, RepeatInstance* (and RepeatTemplate*) all share the | ||
* same body element, and thus all share the same definition `bodyElement`. As | ||
* such, they also all share the same `appearances`. At time of writing, | ||
* `web-forms` (Vue UI package) treats a `RepeatRangeNode`... | ||
* | ||
* - ... as a group, if the node has a label (i.e. | ||
* `<group><label/><repeat/></group>`) | ||
* - ... effectively as a fragment containing only its instances, otherwise | ||
* | ||
* We now collapse `<group><repeat>` into `<repeat>`, and no longer treat | ||
* "repeat group" as a concept (after parsing). According to the spec, these | ||
* appearances **are supposed to** come from that "repeat group" in the form | ||
* definition. In practice, many forms do define appearances directly on a | ||
* repeat element. The engine currently produces an error if both are defined | ||
* simultaneously, but otherwise makes no distinction between appearances in | ||
* these form definition shapes: | ||
* | ||
* ```xml | ||
* <group ref="/data/rep1" appearance="..."> | ||
* <repeat nodeset="/data/rep1"/> | ||
* </group> | ||
* | ||
* <group ref="/data/rep1"> | ||
* <repeat nodeset="/data/rep1"/ appearance="..."> | ||
* </group> | ||
* | ||
* <repeat nodeset="/data/rep1"/ appearance="..."> | ||
* ``` | ||
* | ||
* All of the above creates considerable ambiguity about where "repeat | ||
* appearances" should apply, under which circumstances. | ||
*/ | ||
readonly appearances: RepeatRangeNodeAppearances; | ||
readonly currentState: MaterializedChildren<CurrentState<RepeatRangeStateSpec>, RepeatInstance>; | ||
constructor(parent: GeneralParentNode, definition: RepeatSequenceDefinition); | ||
constructor(parent: GeneralParentNode, definition: RepeatRangeDefinition); | ||
private getLastIndex; | ||
protected initializeContextNode(parentContextNode: Element): Element; | ||
protected computeReference(parent: GeneralParentNode): string; | ||
getInstanceIndex(instance: RepeatInstance): number; | ||
@@ -61,0 +140,0 @@ addInstances(afterIndex?: number, count?: number, definition?: RepeatDefinition): Root; |
import { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import { Accessor, Signal } from 'solid-js'; | ||
import { XFormDOM } from '../XFormDOM.ts'; | ||
import { BodyClassList } from '../body/BodyDefinition.ts'; | ||
import { ActiveLanguage, FormLanguage, FormLanguages } from '../client/FormLanguage.ts'; | ||
@@ -32,7 +33,12 @@ import { RootNode } from '../client/RootNode.ts'; | ||
export declare class Root extends InstanceNode<RootDefinition, RootStateSpec, GeneralChildNode> implements RootNode, EvaluationContext, EvaluationContextRoot, SubscribableDependency, TranslationContext { | ||
static initialize(xformDOM: XFormDOM, definition: RootDefinition, engineConfig: InstanceConfig): Promise<Root>; | ||
private readonly childrenState; | ||
readonly hasReadonlyAncestor: () => boolean; | ||
readonly isReadonly: () => boolean; | ||
readonly hasNonRelevantAncestor: () => boolean; | ||
readonly isRelevant: () => boolean; | ||
protected readonly state: SharedNodeState<RootStateSpec>; | ||
protected readonly engineState: EngineState<RootStateSpec>; | ||
readonly nodeType = "root"; | ||
readonly appearances: null; | ||
readonly classes: BodyClassList; | ||
readonly currentState: MaterializedChildren<CurrentState<RootStateSpec>, GeneralChildNode>; | ||
@@ -42,4 +48,2 @@ protected readonly instanceDOM: XFormDOM; | ||
readonly evaluator: XFormsXPathEvaluator; | ||
private readonly rootReference; | ||
get contextReference(): string; | ||
readonly contextNode: Element; | ||
@@ -49,22 +53,3 @@ readonly parent: null; | ||
get activeLanguage(): ActiveLanguage; | ||
protected constructor(xformDOM: XFormDOM, definition: RootDefinition, engineConfig: InstanceConfig); | ||
/** | ||
* Waits until form state is fully initialized. | ||
* | ||
* As much as possible, all instance state computations are implemented so | ||
* that they complete synchronously. | ||
* | ||
* There is currently one exception: because instance nodes may form | ||
* computation dependencies into their descendants as well as their ancestors, | ||
* there is an allowance **during form initialization only** to account for | ||
* this chicken/egg scenario. Note that this allowance is intentionally, | ||
* strictly limited: if form state initialization is not resolved within a | ||
* single microtask tick we throw/reject. | ||
* | ||
* All subsequent computations are always performed synchronously (and we will | ||
* use tests to validate this, by utilizing the synchronously returned `Root` | ||
* state from client-facing write interfaces). | ||
*/ | ||
formStateInitialized(): Promise<void>; | ||
protected computeReference(_parent: null, definition: RootDefinition): string; | ||
constructor(xformDOM: XFormDOM, definition: RootDefinition, engineConfig: InstanceConfig); | ||
getChildren(): readonly GeneralChildNode[]; | ||
@@ -71,0 +56,0 @@ setLanguage(language: FormLanguage): Root; |
import { Accessor } from 'solid-js'; | ||
import { AnySelectDefinition } from '../body/control/select/SelectDefinition.ts'; | ||
import { SelectItem, SelectNode } from '../client/SelectNode.ts'; | ||
import { SelectItem, SelectNode, SelectNodeAppearances } from '../client/SelectNode.ts'; | ||
import { TextRange } from '../index.ts'; | ||
@@ -32,2 +32,3 @@ import { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts'; | ||
readonly nodeType = "select"; | ||
readonly appearances: SelectNodeAppearances; | ||
readonly currentState: CurrentState<SelectFieldStateSpec>; | ||
@@ -39,3 +40,2 @@ readonly encodeValue: (runtimeValue: readonly SelectItem[]) => string; | ||
protected getSelectItemsByValue(valueOptions?: readonly SelectItem[]): ReadonlyMap<string, SelectItem>; | ||
protected computeReference(parent: GeneralParentNode): string; | ||
protected updateSelectedItemValues(values: readonly string[]): void; | ||
@@ -42,0 +42,0 @@ protected setSelectedItemValue(value: string | null): void; |
import { Accessor } from 'solid-js'; | ||
import { InputDefinition } from '../body/control/InputDefinition.ts'; | ||
import { StringNode } from '../client/StringNode.ts'; | ||
import { StringNode, StringNodeAppearances } from '../client/StringNode.ts'; | ||
import { TextRange } from '../index.ts'; | ||
@@ -31,2 +31,3 @@ import { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts'; | ||
readonly nodeType = "string"; | ||
readonly appearances: StringNodeAppearances; | ||
readonly currentState: CurrentState<StringFieldStateSpec>; | ||
@@ -36,3 +37,2 @@ readonly encodeValue: (value: string) => string; | ||
constructor(parent: GeneralParentNode, definition: StringFieldDefinition); | ||
protected computeReference(parent: GeneralParentNode): string; | ||
getChildren(): readonly []; | ||
@@ -39,0 +39,0 @@ setValue(value: string): Root; |
@@ -25,7 +25,7 @@ import { Accessor } from 'solid-js'; | ||
readonly nodeType = "subtree"; | ||
readonly appearances: null; | ||
readonly currentState: MaterializedChildren<CurrentState<SubtreeStateSpec>, GeneralChildNode>; | ||
constructor(parent: GeneralParentNode, definition: SubtreeDefinition); | ||
protected computeReference(parent: GeneralParentNode): string; | ||
getChildren(): readonly GeneralChildNode[]; | ||
} | ||
export {}; |
@@ -12,2 +12,6 @@ import { KnownAttributeLocalNamedElement, LocalNamedElement } from '../../../../common/types/dom.ts'; | ||
} | ||
export interface RepeatGroupLabelElement extends LabelElement { | ||
getAttribute(name: 'form-definition-source'): 'repeat-group'; | ||
getAttribute(name: string): string; | ||
} | ||
export interface RepeatElement extends KnownAttributeLocalNamedElement<'repeat', 'nodeset'> { | ||
@@ -21,3 +25,4 @@ } | ||
export declare const getLabelElement: (parent: Element) => LabelElement | null; | ||
export declare const getRepeatGroupLabelElement: (parent: Element) => RepeatGroupLabelElement | null; | ||
export declare const getRepeatElement: (parent: Element) => RepeatElement | null; | ||
export declare const getValueElement: (parent: ItemElement | ItemsetElement) => ValueElement | null; |
import { AnyChildNode } from '../../instance/hierarchy.ts'; | ||
import { NodeID } from '../../instance/identity.ts'; | ||
import { ChildrenState } from './createChildrenState.ts'; | ||
import { ReactiveScope } from './scope.ts'; | ||
@@ -19,2 +20,2 @@ export interface EncodedParentState { | ||
*/ | ||
export declare const materializeCurrentStateChildren: <Child extends AnyChildNode, ParentState extends EncodedParentState>(currentState: ParentState, childrenState: ChildrenState<Child>) => MaterializedChildren<ParentState, Child>; | ||
export declare const materializeCurrentStateChildren: <Child extends AnyChildNode, ParentState extends EncodedParentState>(scope: ReactiveScope, currentState: ParentState, childrenState: ChildrenState<Child>) => MaterializedChildren<ParentState, Child>; |
import { AnyBodyElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import { BindDefinition } from './BindDefinition.ts'; | ||
@@ -8,3 +7,3 @@ import { ModelNode, NodeChildren, NodeDefaultValue, NodeDefinition, NodeDefinitionType, NodeInstances, NodeParent } from './NodeDefinition.ts'; | ||
export type DescendentNodeType = Exclude<NodeDefinitionType, 'root'>; | ||
type DescendentNodeBodyElement = AnyBodyElementDefinition | RepeatDefinition; | ||
type DescendentNodeBodyElement = AnyBodyElementDefinition; | ||
export declare abstract class DescendentNodeDefinition<Type extends DescendentNodeType, BodyElement extends DescendentNodeBodyElement | null = DescendentNodeBodyElement | null> implements NodeDefinition<Type> { | ||
@@ -11,0 +10,0 @@ readonly parent: NodeParent<Type>; |
import { AnyBodyElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts'; | ||
import { BindDefinition } from './BindDefinition.ts'; | ||
import { RepeatInstanceDefinition } from './RepeatInstanceDefinition.ts'; | ||
import { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
import { RepeatTemplateDefinition } from './RepeatTemplateDefinition.ts'; | ||
@@ -19,8 +19,8 @@ import { RootDefinition } from './RootDefinition.ts'; | ||
/** | ||
* Corresponds to a sequence of model/entry DOM subtrees which in turn | ||
* Corresponds to a range/sequence of model/entry DOM subtrees which in turn | ||
* corresponds to a <repeat> in the form body definition. | ||
*/ | ||
export type RepeatSequenceType = 'repeat-sequence'; | ||
export type RepeatRangeType = 'repeat-range'; | ||
/** | ||
* Corresponds to a template definition for a repeat sequence, which either has | ||
* Corresponds to a template definition for a repeat range, which either has | ||
* an explicit `jr:template=""` attribute in the form definition or is inferred | ||
@@ -33,3 +33,3 @@ * as a template from the form's first element matched by a <repeat nodeset>. | ||
* in turn corresponds to a <repeat> in the form body definition, and a | ||
* 'repeat-sequence' definition. | ||
* 'repeat-range' definition. | ||
*/ | ||
@@ -50,10 +50,10 @@ export type RepeatInstanceType = 'repeat-instance'; | ||
export type ValueNodeType = 'value-node'; | ||
export type NodeDefinitionType = RootNodeType | RepeatSequenceType | RepeatTemplateType | RepeatInstanceType | SubtreeNodeType | ValueNodeType; | ||
export type NodeDefinitionType = RootNodeType | RepeatRangeType | RepeatTemplateType | RepeatInstanceType | SubtreeNodeType | ValueNodeType; | ||
export type ParentNodeDefinition = RootDefinition | RepeatTemplateDefinition | RepeatInstanceDefinition | SubtreeDefinition; | ||
export type ChildNodeDefinition = RepeatSequenceDefinition | SubtreeDefinition | ValueNodeDefinition; | ||
export type ChildNodeDefinition = RepeatRangeDefinition | SubtreeDefinition | ValueNodeDefinition; | ||
export type ChildNodeInstanceDefinition = RepeatTemplateDefinition | RepeatInstanceDefinition | SubtreeDefinition | ValueNodeDefinition; | ||
export type NodeChildren<Type extends NodeDefinitionType> = Type extends ParentNodeDefinition['type'] ? readonly ChildNodeDefinition[] : null; | ||
export type NodeInstances<Type extends NodeDefinitionType> = Type extends 'repeat-sequence' ? readonly RepeatInstanceDefinition[] : null; | ||
export type NodeInstances<Type extends NodeDefinitionType> = Type extends 'repeat-range' ? readonly RepeatInstanceDefinition[] : null; | ||
export type NodeParent<Type extends NodeDefinitionType> = Type extends ChildNodeDefinition['type'] | ChildNodeInstanceDefinition['type'] ? ParentNodeDefinition : null; | ||
export type ModelNode<Type extends NodeDefinitionType> = Type extends 'repeat-sequence' ? null : Element; | ||
export type ModelNode<Type extends NodeDefinitionType> = Type extends 'repeat-range' ? null : Element; | ||
export type NodeDefaultValue<Type extends NodeDefinitionType> = Type extends 'value-node' ? string : null; | ||
@@ -65,3 +65,3 @@ export interface NodeDefinition<Type extends NodeDefinitionType> { | ||
readonly nodeName: string; | ||
readonly bodyElement: AnyBodyElementDefinition | RepeatDefinition | null; | ||
readonly bodyElement: AnyBodyElementDefinition | RepeatElementDefinition | null; | ||
readonly isTranslated: boolean; | ||
@@ -76,5 +76,5 @@ readonly dependencyExpressions: ReadonlySet<string>; | ||
} | ||
export type AnyNodeDefinition = RootDefinition | RepeatSequenceDefinition | RepeatTemplateDefinition | RepeatInstanceDefinition | SubtreeDefinition | ValueNodeDefinition; | ||
export type AnyNodeDefinition = RootDefinition | RepeatRangeDefinition | RepeatTemplateDefinition | RepeatInstanceDefinition | SubtreeDefinition | ValueNodeDefinition; | ||
export type TypedNodeDefinition<Type> = Extract<AnyNodeDefinition, { | ||
readonly type: Type; | ||
}>; |
@@ -1,8 +0,7 @@ | ||
import { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts'; | ||
import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
import { ChildNodeDefinition, NodeDefinition } from './NodeDefinition.ts'; | ||
import { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
export declare class RepeatInstanceDefinition extends DescendentNodeDefinition<'repeat-instance', RepeatDefinition> implements NodeDefinition<'repeat-instance'> { | ||
protected readonly sequence: RepeatSequenceDefinition; | ||
export declare class RepeatInstanceDefinition extends DescendentNodeDefinition<'repeat-instance', RepeatElementDefinition> implements NodeDefinition<'repeat-instance'> { | ||
readonly node: Element; | ||
@@ -14,4 +13,4 @@ readonly type = "repeat-instance"; | ||
readonly defaultValue: null; | ||
constructor(sequence: RepeatSequenceDefinition, node: Element); | ||
toJSON(): Omit<this, "toJSON" | "parent" | "bind" | "root" | "bodyElement" | "sequence">; | ||
constructor(range: RepeatRangeDefinition, node: Element); | ||
toJSON(): Omit<this, "toJSON" | "parent" | "bind" | "root" | "bodyElement">; | ||
} |
import { JAVAROSA_NAMESPACE_URI } from '../../../common/src/constants/xmlns.ts'; | ||
import { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts'; | ||
import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
import { ChildNodeDefinition, NodeDefinition } from './NodeDefinition.ts'; | ||
import { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
@@ -17,6 +17,5 @@ interface ExplicitRepeatTemplateElement extends Element { | ||
} | ||
export declare class RepeatTemplateDefinition extends DescendentNodeDefinition<'repeat-template', RepeatDefinition> implements NodeDefinition<'repeat-template'> { | ||
protected readonly sequence: RepeatSequenceDefinition; | ||
export declare class RepeatTemplateDefinition extends DescendentNodeDefinition<'repeat-template', RepeatElementDefinition> implements NodeDefinition<'repeat-template'> { | ||
protected readonly templateNode: ExplicitRepeatTemplateElement; | ||
static parseModelNodes(sequence: RepeatSequenceDefinition, modelNodes: readonly [Element, ...Element[]]): ParsedRepeatNodes; | ||
static parseModelNodes(range: RepeatRangeDefinition, modelNodes: readonly [Element, ...Element[]]): ParsedRepeatNodes; | ||
readonly type = "repeat-template"; | ||
@@ -28,5 +27,5 @@ readonly node: Element; | ||
readonly defaultValue: null; | ||
protected constructor(sequence: RepeatSequenceDefinition, templateNode: ExplicitRepeatTemplateElement); | ||
toJSON(): Omit<this, "toJSON" | "parent" | "bind" | "root" | "bodyElement" | "sequence">; | ||
protected constructor(range: RepeatRangeDefinition, templateNode: ExplicitRepeatTemplateElement); | ||
toJSON(): Omit<this, "toJSON" | "parent" | "bind" | "root" | "bodyElement">; | ||
} | ||
export {}; |
import { XFormDefinition } from '../XFormDefinition.ts'; | ||
import { BodyClassList } from '../body/BodyDefinition.ts'; | ||
import { BindDefinition } from './BindDefinition.ts'; | ||
@@ -9,2 +10,3 @@ import { ModelDefinition } from './ModelDefinition.ts'; | ||
protected readonly model: ModelDefinition; | ||
readonly classes: BodyClassList; | ||
readonly type = "root"; | ||
@@ -23,5 +25,5 @@ readonly bind: BindDefinition; | ||
readonly dependencyExpressions: ReadonlySet<string>; | ||
constructor(form: XFormDefinition, model: ModelDefinition); | ||
constructor(form: XFormDefinition, model: ModelDefinition, classes: BodyClassList); | ||
buildSubtree(parent: ParentNodeDefinition): readonly ChildNodeDefinition[]; | ||
toJSON(): Omit<this, "model" | "toJSON" | "form" | "bind" | "root" | "bodyElement" | "buildSubtree">; | ||
} |
@@ -1,2 +0,2 @@ | ||
import { AnyBodyElementDefinition, NonRepeatGroupElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { AnyBodyElementDefinition, AnyGroupElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { BindDefinition } from './BindDefinition.ts'; | ||
@@ -6,3 +6,3 @@ import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
export declare class SubtreeDefinition extends DescendentNodeDefinition<'subtree', NonRepeatGroupElementDefinition | null> implements NodeDefinition<'subtree'> { | ||
export declare class SubtreeDefinition extends DescendentNodeDefinition<'subtree', AnyGroupElementDefinition | null> implements NodeDefinition<'subtree'> { | ||
readonly node: Element; | ||
@@ -9,0 +9,0 @@ readonly type = "subtree"; |
@@ -1,3 +0,2 @@ | ||
import { AnyBodyElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { AnyControlDefinition } from '../body/control/ControlDefinition.ts'; | ||
import { AnyBodyElementDefinition, ControlElementDefinition } from '../body/BodyDefinition.ts'; | ||
import { BindDefinition } from './BindDefinition.ts'; | ||
@@ -7,3 +6,3 @@ import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
export declare class ValueNodeDefinition extends DescendentNodeDefinition<'value-node', AnyControlDefinition | null> implements NodeDefinition<'value-node'> { | ||
export declare class ValueNodeDefinition extends DescendentNodeDefinition<'value-node', ControlElementDefinition | null> implements NodeDefinition<'value-node'> { | ||
readonly node: Element; | ||
@@ -10,0 +9,0 @@ readonly type = "value-node"; |
{ | ||
"name": "@getodk/xforms-engine", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"license": "Apache-2.0", | ||
@@ -62,3 +62,3 @@ "description": "XForms engine for ODK Web Forms", | ||
"@getodk/tree-sitter-xpath": "0.1.1", | ||
"@getodk/xpath": "0.1.1", | ||
"@getodk/xpath": "0.1.2", | ||
"@playwright/test": "^1.44.1", | ||
@@ -65,0 +65,0 @@ "@vitest/browser": "^1.6.0", |
import type { XFormDefinition } from '../XFormDefinition.ts'; | ||
import { DependencyContext } from '../expression/DependencyContext.ts'; | ||
import type { ParsedTokenList } from '../lib/TokenListParser.ts'; | ||
import { TokenListParser } from '../lib/TokenListParser.ts'; | ||
import { RepeatElementDefinition } from './RepeatElementDefinition.ts'; | ||
import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts'; | ||
@@ -10,3 +13,2 @@ import { ControlDefinition } from './control/ControlDefinition.ts'; | ||
import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts'; | ||
import { RepeatGroupDefinition } from './group/RepeatGroupDefinition.ts'; | ||
import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts'; | ||
@@ -19,10 +21,14 @@ | ||
// prettier-ignore | ||
export type ControlElementDefinition = | ||
| AnySelectDefinition | ||
| InputDefinition; | ||
type SupportedBodyElementDefinition = | ||
// eslint-disable-next-line @typescript-eslint/sort-type-constituents | ||
| RepeatGroupDefinition | ||
| RepeatElementDefinition | ||
| LogicalGroupDefinition | ||
| PresentationGroupDefinition | ||
| StructuralGroupDefinition | ||
| InputDefinition | ||
| AnySelectDefinition; | ||
| ControlElementDefinition; | ||
@@ -33,3 +39,3 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const BodyElementDefinitionConstructors = [ | ||
RepeatGroupDefinition, | ||
RepeatElementDefinition, | ||
LogicalGroupDefinition, | ||
@@ -55,7 +61,2 @@ PresentationGroupDefinition, | ||
export type NonRepeatGroupElementDefinition = Exclude< | ||
AnyGroupElementDefinition, | ||
{ readonly type: 'repeat-group' } | ||
>; | ||
const isGroupElementDefinition = ( | ||
@@ -103,9 +104,9 @@ element: AnyBodyElementDefinition | ||
if (element instanceof RepeatGroupDefinition) { | ||
if (element instanceof RepeatElementDefinition) { | ||
if (reference == null) { | ||
throw new Error('Missing reference for repeat/repeat group'); | ||
throw new Error('Missing reference for repeat'); | ||
} | ||
this.set(reference, element); | ||
this.mapElementsByReference(element.repeatChildren); | ||
this.mapElementsByReference(element.children); | ||
} | ||
@@ -148,2 +149,6 @@ | ||
const bodyClassParser = new TokenListParser(['pages' /*, 'theme-grid' */]); | ||
export type BodyClassList = ParsedTokenList<typeof bodyClassParser>; | ||
export class BodyDefinition extends DependencyContext { | ||
@@ -170,2 +175,21 @@ static getChildElementDefinitions( | ||
readonly element: Element; | ||
/** | ||
* @todo this class is already an oddity in that it's **like** an element | ||
* definition, but it isn't one itself. Adding this property here emphasizes | ||
* that awkwardness. It also extends the applicable scope where instances of | ||
* this class are accessed. While it's still ephemeral, it's anticipated that | ||
* this extension might cause some disomfort. If so, the most plausible | ||
* alternative is an additional refactor to: | ||
* | ||
* 1. Introduce a `BodyElementDefinition` sublass for `<h:body>`. | ||
* 2. Disambiguate the respective names of those, in some reasonable way. | ||
* 3. Add a layer of indirection between this class and that new body element | ||
* definition's class. | ||
* 4. At that point, we may as well prioritize the little bit of grunt work to | ||
* pass the `BodyDefinition` instance by reference rather than assigning it | ||
* to anything. | ||
*/ | ||
readonly classes: BodyClassList; | ||
readonly elements: readonly AnyBodyElementDefinition[]; | ||
@@ -186,2 +210,3 @@ | ||
this.element = element; | ||
this.classes = bodyClassParser.parseFrom(element, 'class'); | ||
this.elements = BodyDefinition.getChildElementDefinitions(form, this, element); | ||
@@ -195,12 +220,2 @@ this.elementsByReference = new BodyElementMap(this.elements); | ||
getRepeatGroup(reference: string): RepeatGroupDefinition | null { | ||
const element = this.getBodyElement(reference); | ||
if (element?.type === 'repeat-group') { | ||
return element; | ||
} | ||
return null; | ||
} | ||
toJSON() { | ||
@@ -207,0 +222,0 @@ const { form, ...rest } = this; |
import type { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import type { ParsedTokenList } from '../../lib/TokenListParser.ts'; | ||
import type { BodyElementParentContext } from '../BodyDefinition.ts'; | ||
@@ -26,2 +27,5 @@ import { BodyElementDefinition } from '../BodyElementDefinition.ts'; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
abstract readonly appearances: ParsedTokenList<any>; | ||
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) { | ||
@@ -28,0 +32,0 @@ super(form, parent, element); |
@@ -0,1 +1,7 @@ | ||
import type { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import type { BodyElementParentContext } from '../BodyDefinition.ts'; | ||
import { | ||
inputAppearanceParser, | ||
type InputAppearanceDefinition, | ||
} from '../appearance/inputAppearanceParser.ts'; | ||
import { ControlDefinition } from './ControlDefinition.ts'; | ||
@@ -9,2 +15,9 @@ | ||
readonly type = 'input'; | ||
readonly appearances: InputAppearanceDefinition; | ||
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) { | ||
super(form, parent, element); | ||
this.appearances = inputAppearanceParser.parseFrom(element, 'appearance'); | ||
} | ||
} |
@@ -6,2 +6,4 @@ import type { CollectionValues } from '@getodk/common/types/collections/CollectionValues.ts'; | ||
import type { AnyBodyElementDefinition, BodyElementParentContext } from '../../BodyDefinition.ts'; | ||
import type { SelectAppearanceDefinition } from '../../appearance/selectAppearanceParser.ts'; | ||
import { selectAppearanceParser } from '../../appearance/selectAppearanceParser.ts'; | ||
import { ControlDefinition } from '../ControlDefinition.ts'; | ||
@@ -11,6 +13,11 @@ import { ItemDefinition } from './ItemDefinition.ts'; | ||
// TODO: `<trigger>` is *almost* reasonable to support here too. The main | ||
// hesitation is that its single, implicit "item" does not have a distinct | ||
// <label>, and presumably has different UX **and translation** considerations. | ||
const selectLocalNames = new Set(['rank', 'select', 'select1'] as const); | ||
/** | ||
* @todo We were previously a bit overzealous about introducing `<rank>` support | ||
* here. It'll likely still fit, but we should approach it with more intention. | ||
* | ||
* @todo `<trigger>` is *almost* reasonable to support here too. The main | ||
* hesitation is that its single, implicit "item" does not have a distinct | ||
* <label>, and presumably has different UX **and translation** considerations. | ||
*/ | ||
const selectLocalNames = new Set([/* 'rank', */ 'select', 'select1'] as const); | ||
@@ -39,2 +46,3 @@ export type SelectType = CollectionValues<typeof selectLocalNames>; | ||
override readonly element: SelectElement; | ||
readonly appearances: SelectAppearanceDefinition; | ||
@@ -51,4 +59,5 @@ readonly itemset: ItemsetDefinition | null; | ||
this.type = element.localName as Type; | ||
this.element = element; | ||
this.type = element.localName as Type; | ||
this.appearances = selectAppearanceParser.parseFrom(element, 'appearance'); | ||
@@ -55,0 +64,0 @@ const itemsetElement = getItemsetElement(element); |
import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts'; | ||
import type { XFormDOM } from '../../XFormDOM.ts'; | ||
import type { XFormDefinition } from '../../XFormDefinition.ts'; | ||
import { getLabelElement, getRepeatElement } from '../../lib/dom/query.ts'; | ||
import { getLabelElement } from '../../lib/dom/query.ts'; | ||
import { | ||
@@ -11,2 +10,4 @@ BodyDefinition, | ||
import { BodyElementDefinition } from '../BodyElementDefinition.ts'; | ||
import type { StructureElementAppearanceDefinition } from '../appearance/structureElementAppearanceParser.ts'; | ||
import { structureElementAppearanceParser } from '../appearance/structureElementAppearanceParser.ts'; | ||
import { LabelDefinition } from '../text/LabelDefinition.ts'; | ||
@@ -27,6 +28,2 @@ | ||
* have a `ref` | ||
* - `repeat-group` is not mentioned by the spec; it is an extension of | ||
* `logical-group`, wherein its `ref` is the same as its immediate `<repeat>` | ||
* child's `nodeset` (usage of each attribute is normalized during | ||
* initialization, in {@link XFormDOM}) | ||
* - `structural-group` is any `<group>` element which does not satisfy any of | ||
@@ -39,3 +36,2 @@ * the other usage scenarios; this isn't exactly the terminology used, but is | ||
* | ||
* - `<group ref="$ref"><repeat nodeset="$ref">` -> `repeat-group`, else | ||
* - `<group ref="$ref">` -> `logical-group`, else | ||
@@ -45,7 +41,3 @@ * - `<group><label>` -> `presentation-group`, else | ||
*/ | ||
export type GroupType = | ||
| 'logical-group' | ||
| 'presentation-group' | ||
| 'repeat-group' | ||
| 'structural-group'; | ||
export type GroupType = 'logical-group' | 'presentation-group' | 'structural-group'; | ||
@@ -55,2 +47,4 @@ export abstract class BaseGroupDefinition< | ||
> extends BodyElementDefinition<Type> { | ||
// TODO: does this really accomplish anything? It seems highly unlikely it | ||
// has enough performance benefit to outweigh its memory and lookup costs. | ||
private static groupTypes = new UpsertableMap<Element, GroupType | null>(); | ||
@@ -64,16 +58,4 @@ | ||
const ref = element.getAttribute('ref'); | ||
if (ref != null) { | ||
const repeat = getRepeatElement(element); | ||
if (repeat == null) { | ||
return 'logical-group'; | ||
} | ||
if (repeat.getAttribute('nodeset') === ref) { | ||
return 'repeat-group'; | ||
} | ||
throw new Error('Unexpected <repeat> child of unrelated <group>'); | ||
if (element.hasAttribute('ref')) { | ||
return 'logical-group'; | ||
} | ||
@@ -96,2 +78,3 @@ | ||
override readonly reference: string | null; | ||
readonly appearances: StructureElementAppearanceDefinition; | ||
override readonly label: LabelDefinition | null; | ||
@@ -109,2 +92,3 @@ | ||
this.reference = element.getAttribute('ref'); | ||
this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance'); | ||
this.label = LabelDefinition.forGroup(form, this); | ||
@@ -118,3 +102,3 @@ } | ||
return childName !== 'label' && childName !== 'repeat'; | ||
return childName !== 'label'; | ||
}); | ||
@@ -125,23 +109,1 @@ | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export const repeatGroup = <T extends BaseGroupDefinition<any>>( | ||
groupDefinition: T | ||
): Extract<T, BaseGroupDefinition<'repeat-group'>> | null => { | ||
if (groupDefinition.type === 'repeat-group') { | ||
return groupDefinition as Extract<T, BaseGroupDefinition<'repeat-group'>>; | ||
} | ||
return null; | ||
}; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export const nonRepeatGroup = <T extends BaseGroupDefinition<any>>( | ||
groupDefinition: T | ||
): Exclude<T, BaseGroupDefinition<'repeat-group'>> | null => { | ||
if (groupDefinition.type === 'repeat-group') { | ||
return null; | ||
} | ||
return groupDefinition as Exclude<T, BaseGroupDefinition<'repeat-group'>>; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { getLabelElement } from '../../lib/dom/query.ts'; | ||
import { getLabelElement, getRepeatGroupLabelElement } from '../../lib/dom/query.ts'; | ||
import type { XFormDefinition } from '../../XFormDefinition.ts'; | ||
@@ -7,2 +7,3 @@ import type { AnyControlDefinition } from '../control/ControlDefinition.ts'; | ||
import type { BaseGroupDefinition } from '../group/BaseGroupDefinition.ts'; | ||
import type { RepeatElementDefinition } from '../RepeatElementDefinition.ts'; | ||
import type { TextElement, TextElementOwner } from './TextElementDefinition.ts'; | ||
@@ -35,2 +36,15 @@ import { TextElementDefinition } from './TextElementDefinition.ts'; | ||
static forRepeatGroup( | ||
form: XFormDefinition, | ||
repeat: RepeatElementDefinition | ||
): LabelDefinition | null { | ||
const repeatGroupLabel = getRepeatGroupLabelElement(repeat.element); | ||
if (repeatGroupLabel == null) { | ||
return null; | ||
} | ||
return new this(form, repeat, repeatGroupLabel); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
@@ -37,0 +51,0 @@ static forGroup(form: XFormDefinition, group: BaseGroupDefinition<any>): LabelDefinition | null { |
@@ -6,6 +6,6 @@ import { isCommentNode, isElementNode, isTextNode } from '@getodk/common/lib/dom/predicates.ts'; | ||
import { BodyElementDefinition } from '../BodyElementDefinition.ts'; | ||
import type { InputDefinition } from '../control/InputDefinition.ts'; | ||
import type { RepeatElementDefinition } from '../RepeatElementDefinition.ts'; | ||
import type { AnyControlDefinition } from '../control/ControlDefinition.ts'; | ||
import type { ItemDefinition } from '../control/select/ItemDefinition.ts'; | ||
import type { ItemsetDefinition } from '../control/select/ItemsetDefinition.ts'; | ||
import type { AnySelectDefinition } from '../control/select/SelectDefinition.ts'; | ||
import { TextElementOutputPart } from './TextElementOutputPart.ts'; | ||
@@ -22,7 +22,7 @@ import { TextElementReferencePart } from './TextElementReferencePart.ts'; | ||
export type TextElementOwner = | ||
| AnyControlDefinition | ||
| AnyGroupElementDefinition | ||
| AnySelectDefinition | ||
| InputDefinition | ||
| ItemDefinition | ||
| ItemsetDefinition; | ||
| ItemsetDefinition | ||
| RepeatElementDefinition; | ||
@@ -29,0 +29,0 @@ export type TextElementChild = TextElementOutputPart | TextElementStaticPart; |
@@ -0,5 +1,7 @@ | ||
import type { TokenListParser } from '../lib/TokenListParser.ts'; | ||
import type { AnyNodeDefinition } from '../model/NodeDefinition.ts'; | ||
import type { InstanceNodeType } from './node-types.js'; | ||
import type { NodeAppearances } from './NodeAppearances.ts'; | ||
import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts'; | ||
import type { TextRange } from './TextRange.ts'; | ||
import type { InstanceNodeType } from './node-types.ts'; | ||
@@ -130,2 +132,8 @@ export interface BaseNodeState { | ||
/** | ||
* @see {@link TokenListParser} for details. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
readonly appearances: NodeAppearances<any> | null; | ||
/** | ||
* Each node has a definition which specifies aspects of the node defined in | ||
@@ -132,0 +140,0 @@ * the form. These aspects include (but are not limited to) the node's data |
@@ -1,4 +0,5 @@ | ||
import type { NonRepeatGroupElementDefinition } from '../body/BodyDefinition.ts'; | ||
import type { AnyGroupElementDefinition } from '../body/BodyDefinition.ts'; | ||
import type { SubtreeDefinition } from '../model/SubtreeDefinition.ts'; | ||
import type { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import type { NodeAppearances } from './NodeAppearances.ts'; | ||
import type { RootNode } from './RootNode.ts'; | ||
@@ -17,5 +18,7 @@ import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts'; | ||
export interface GroupDefinition extends SubtreeDefinition { | ||
readonly bodyElement: NonRepeatGroupElementDefinition; | ||
readonly bodyElement: AnyGroupElementDefinition; | ||
} | ||
export type GroupNodeAppearances = NodeAppearances<GroupDefinition>; | ||
/** | ||
@@ -30,2 +33,3 @@ * A node corresponding to an XForms `<group>`. | ||
readonly nodeType: 'group'; | ||
readonly appearances: GroupNodeAppearances; | ||
readonly definition: GroupDefinition; | ||
@@ -32,0 +36,0 @@ readonly root: RootNode; |
import type { RepeatInstanceDefinition } from '../model/RepeatInstanceDefinition.ts'; | ||
import type { RepeatTemplateDefinition } from '../model/RepeatTemplateDefinition.ts'; | ||
import type { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import type { NodeAppearances } from './NodeAppearances.ts'; | ||
import type { RepeatRangeNode } from './RepeatRangeNode.ts'; | ||
@@ -25,4 +26,7 @@ import type { RootNode } from './RootNode.ts'; | ||
export type RepeatInstanceNodeAppearances = NodeAppearances<RepeatDefinition>; | ||
export interface RepeatInstanceNode extends BaseNode { | ||
readonly nodeType: 'repeat-instance'; | ||
readonly appearances: RepeatInstanceNodeAppearances; | ||
readonly definition: RepeatDefinition; | ||
@@ -29,0 +33,0 @@ readonly root: RootNode; |
@@ -1,3 +0,4 @@ | ||
import type { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts'; | ||
import type { RepeatRangeDefinition } from '../model/RepeatRangeDefinition.ts'; | ||
import type { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import type { NodeAppearances } from './NodeAppearances.ts'; | ||
import type { RepeatInstanceNode } from './RepeatInstanceNode.ts'; | ||
@@ -26,2 +27,4 @@ import type { RootNode } from './RootNode.ts'; | ||
export type RepeatRangeNodeAppearances = NodeAppearances<RepeatRangeDefinition>; | ||
/** | ||
@@ -93,3 +96,4 @@ * Represents a contiguous set of zero or more {@link RepeatInstanceNode}s | ||
readonly nodeType: 'repeat-range'; | ||
readonly definition: RepeatSequenceDefinition; | ||
readonly appearances: RepeatRangeNodeAppearances; | ||
readonly definition: RepeatRangeDefinition; | ||
readonly root: RootNode; | ||
@@ -96,0 +100,0 @@ readonly parent: GeneralParentNode; |
@@ -0,1 +1,2 @@ | ||
import type { BodyClassList } from '../body/BodyDefinition.ts'; | ||
import type { RootDefinition } from '../model/RootDefinition.ts'; | ||
@@ -24,2 +25,23 @@ import type { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
readonly nodeType: 'root'; | ||
/** | ||
* @todo this along with {@link classes} is... awkward. | ||
*/ | ||
readonly appearances: null; | ||
/** | ||
* @todo This is another odd deviation in {@link RootNode}. Unlike | ||
* {@link languages}, it doesn't feel particularly **essential**. While it | ||
* would deviate from XForms spec terminology, it seems like it _might be | ||
* reasonable_ to instead convey `<h:body class="...">` as | ||
* {@link RootNode.appearances} in the client interface. They do have slightly | ||
* different spec semantics (i.e. a body class can be anything, to trigger | ||
* styling in a form UI). But the **most likely anticipated** use case in Web | ||
* Forms would be the "pages" class, and perhaps "theme-grid". The former is | ||
* definitely conceptually similar to a XForms `appearance` (albeit | ||
* form-global, which is not a spec concept). The latter does as well, and we | ||
* already anticipate applying that concept in non-form-global ways. | ||
*/ | ||
readonly classes: BodyClassList; | ||
readonly definition: RootDefinition; | ||
@@ -26,0 +48,0 @@ readonly root: RootNode; |
import type { AnySelectDefinition } from '../body/control/select/SelectDefinition.ts'; | ||
import type { ValueNodeDefinition } from '../model/ValueNodeDefinition.ts'; | ||
import type { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import type { NodeAppearances } from './NodeAppearances.ts'; | ||
import type { RootNode } from './RootNode.ts'; | ||
@@ -41,4 +42,7 @@ import type { StringNode } from './StringNode.ts'; | ||
export type SelectNodeAppearances = NodeAppearances<SelectDefinition>; | ||
export interface SelectNode extends BaseNode { | ||
readonly nodeType: 'select'; | ||
readonly appearances: SelectNodeAppearances; | ||
readonly definition: SelectDefinition; | ||
@@ -45,0 +49,0 @@ readonly root: RootNode; |
import type { InputDefinition } from '../body/control/InputDefinition.ts'; | ||
import type { ValueNodeDefinition } from '../model/ValueNodeDefinition.ts'; | ||
import type { BaseNode, BaseNodeState } from './BaseNode.ts'; | ||
import type { NodeAppearances } from './NodeAppearances.ts'; | ||
import type { RootNode } from './RootNode.ts'; | ||
@@ -24,2 +25,4 @@ import type { GeneralParentNode } from './hierarchy.ts'; | ||
export type StringNodeAppearances = NodeAppearances<StringDefinition>; | ||
/** | ||
@@ -34,2 +37,3 @@ * A node which can be assigned a string/text value. A string node **MAY** | ||
readonly nodeType: 'string'; | ||
readonly appearances: StringNodeAppearances; | ||
readonly definition: StringDefinition; | ||
@@ -36,0 +40,0 @@ readonly root: RootNode; |
@@ -53,2 +53,3 @@ import type { SubtreeDefinition as BaseSubtreeDefinition } from '../model/SubtreeDefinition.ts'; | ||
readonly nodeType: 'subtree'; | ||
readonly appearances: null; | ||
readonly definition: SubtreeDefinition; | ||
@@ -55,0 +56,0 @@ readonly root: RootNode; |
import type { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import type { Accessor } from 'solid-js'; | ||
import { createMemo } from 'solid-js'; | ||
import type { BaseNode } from '../../client/BaseNode.ts'; | ||
@@ -55,2 +54,6 @@ import { createComputedExpression } from '../../lib/reactivity/createComputedExpression.ts'; | ||
interface DescendantNodeOptions { | ||
readonly computeReference?: Accessor<string>; | ||
} | ||
export abstract class DescendantNode< | ||
@@ -65,2 +68,36 @@ Definition extends DescendantNodeDefinition, | ||
{ | ||
readonly hasReadonlyAncestor: Accessor<boolean> = () => { | ||
const { parent } = this; | ||
return parent.hasReadonlyAncestor() || parent.isReadonly(); | ||
}; | ||
readonly isSelfReadonly: Accessor<boolean>; | ||
readonly isReadonly: Accessor<boolean> = () => { | ||
if (this.hasReadonlyAncestor()) { | ||
return true; | ||
} | ||
return this.isSelfReadonly(); | ||
}; | ||
readonly hasNonRelevantAncestor: Accessor<boolean> = () => { | ||
const { parent } = this; | ||
return parent.hasNonRelevantAncestor() || !parent.isRelevant(); | ||
}; | ||
readonly isSelfRelevant: Accessor<boolean>; | ||
readonly isRelevant: Accessor<boolean> = () => { | ||
if (this.hasNonRelevantAncestor()) { | ||
return false; | ||
} | ||
return this.isSelfRelevant(); | ||
}; | ||
protected readonly isRequired: Accessor<boolean>; | ||
readonly root: Root; | ||
@@ -72,5 +109,6 @@ readonly evaluator: XFormsXPathEvaluator; | ||
override readonly parent: DescendantNodeParent<Definition>, | ||
override readonly definition: Definition | ||
override readonly definition: Definition, | ||
options?: DescendantNodeOptions | ||
) { | ||
super(parent.engineConfig, parent, definition); | ||
super(parent.engineConfig, parent, definition, options); | ||
@@ -82,44 +120,8 @@ const { evaluator, root } = parent; | ||
this.contextNode = this.initializeContextNode(parent.contextNode, definition.nodeName); | ||
} | ||
protected computeChildStepReference(parent: DescendantNodeParent<Definition>): string { | ||
return `${parent.contextReference}/${this.definition.nodeName}`; | ||
} | ||
const { readonly, relevant, required } = definition.bind; | ||
protected abstract override computeReference( | ||
parent: DescendantNodeParent<Definition>, | ||
definition: Definition | ||
): string; | ||
protected buildSharedStateSpec( | ||
parent: DescendantNodeParent<Definition>, | ||
definition: Definition | ||
): DescendantNodeSharedStateSpec { | ||
return this.scope.runTask(() => { | ||
const reference = createMemo(() => this.contextReference); | ||
const { bind } = definition; | ||
// TODO: we can likely short-circuit `readonly` computation when a node | ||
// is non-relevant. | ||
const selfReadonly = createComputedExpression(this, bind.readonly); | ||
const readonly = createMemo(() => { | ||
return parent.isReadonly || selfReadonly(); | ||
}); | ||
const selfRelevant = createComputedExpression(this, bind.relevant); | ||
const relevant = createMemo(() => { | ||
return parent.isRelevant && selfRelevant(); | ||
}); | ||
// TODO: we can likely short-circuit `required` computation when a node | ||
// is non-relevant. | ||
const required = createComputedExpression(this, bind.required); | ||
return { | ||
reference, | ||
readonly, | ||
relevant, | ||
required, | ||
}; | ||
}); | ||
this.isSelfReadonly = createComputedExpression(this, readonly); | ||
this.isSelfRelevant = createComputedExpression(this, relevant); | ||
this.isRequired = createComputedExpression(this, required); | ||
} | ||
@@ -126,0 +128,0 @@ |
import type { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import type { Accessor, Signal } from 'solid-js'; | ||
import { createSignal } from 'solid-js'; | ||
import type { BaseNode } from '../../client/BaseNode.ts'; | ||
import type { NodeAppearances } from '../../client/NodeAppearances.ts'; | ||
import type { InstanceNodeType } from '../../client/node-types.ts'; | ||
@@ -43,6 +43,2 @@ import type { TextRange } from '../../index.ts'; | ||
interface InitializedStateOptions<T, K extends keyof T> { | ||
readonly uninitializedFallback: T[K]; | ||
} | ||
/** | ||
@@ -65,2 +61,17 @@ * This type has the same effect as {@link MaterializedChildren}, but abstractly | ||
interface ComputableReferenceNode { | ||
readonly parent: AnyParentNode | null; | ||
readonly definition: AnyNodeDefinition; | ||
} | ||
type ComputeInstanceNodeReference = <This extends ComputableReferenceNode>( | ||
this: This, | ||
parent: This['parent'], | ||
definition: This['definition'] | ||
) => string; | ||
export interface InstanceNodeOptions { | ||
readonly computeReference?: () => string; | ||
} | ||
export abstract class InstanceNode< | ||
@@ -74,4 +85,2 @@ Definition extends AnyNodeDefinition, | ||
{ | ||
protected readonly isStateInitialized: Accessor<boolean>; | ||
protected abstract readonly state: SharedNodeState<Spec>; | ||
@@ -81,30 +90,12 @@ protected abstract readonly engineState: EngineState<Spec>; | ||
/** | ||
* Provides a generalized mechanism for accessing a reactive state value | ||
* during a node's construction, while {@link engineState} is still being | ||
* defined and thus isn't assigned. | ||
* | ||
* The fallback value specified in {@link options} will be returned on access | ||
* until {@link isStateInitialized} returns true. This ensures: | ||
* | ||
* - a value of the expected type will be available | ||
* - any read access will become reactive to the actual state, once it has | ||
* been initialized and {@link engineState} is assigned | ||
* | ||
* @todo This is one among several chicken/egg problems encountered trying to | ||
* support state initialization in which some aspects of the state derive from | ||
* other aspects of it. It would be nice to dispense with this entirely. But | ||
* if it must persist, we should also consider replacing the method with a | ||
* direct accessor once state initialization completes, so the initialized | ||
* check is only called until it becomes impertinent. | ||
* @package Exposed on every node type to facilitate inheritance, as well as | ||
* conditional behavior for value nodes. | ||
*/ | ||
protected getInitializedState<K extends keyof EngineState<Spec>>( | ||
key: K, | ||
options: InitializedStateOptions<EngineState<Spec>, K> | ||
): EngineState<Spec>[K] { | ||
if (this.isStateInitialized()) { | ||
return this.engineState[key]; | ||
} | ||
abstract readonly hasReadonlyAncestor: Accessor<boolean>; | ||
return options.uninitializedFallback; | ||
} | ||
/** | ||
* @package Exposed on every node type to facilitate inheritance, as well as | ||
* conditional behavior for value nodes. | ||
*/ | ||
abstract readonly isReadonly: Accessor<boolean>; | ||
@@ -115,7 +106,3 @@ /** | ||
*/ | ||
get isReadonly(): boolean { | ||
return (this as AnyInstanceNode).getInitializedState('readonly', { | ||
uninitializedFallback: false, | ||
}); | ||
} | ||
abstract readonly hasNonRelevantAncestor: Accessor<boolean>; | ||
@@ -126,7 +113,3 @@ /** | ||
*/ | ||
get isRelevant(): boolean { | ||
return (this as AnyInstanceNode).getInitializedState('relevant', { | ||
uninitializedFallback: true, | ||
}); | ||
} | ||
abstract readonly isRelevant: Accessor<boolean>; | ||
@@ -139,2 +122,4 @@ // BaseNode: identity | ||
abstract readonly appearances: NodeAppearances<Definition>; | ||
abstract readonly currentState: InstanceNodeCurrentState<Spec, Child>; | ||
@@ -151,6 +136,21 @@ | ||
readonly computeReference: ComputeInstanceNodeReference; | ||
protected readonly computeChildStepReference: ComputeInstanceNodeReference = ( | ||
parent, | ||
definition | ||
): string => { | ||
if (parent == null) { | ||
throw new Error( | ||
'Cannot compute child step reference of node without parent (was this called from `Root`?)' | ||
); | ||
} | ||
return `${parent.contextReference()}/${definition.nodeName}`; | ||
}; | ||
// EvaluationContext: node-specific | ||
get contextReference(): string { | ||
readonly contextReference = (): string => { | ||
return this.computeReference(this.parent, this.definition); | ||
} | ||
}; | ||
@@ -162,4 +162,7 @@ abstract readonly contextNode: Element; | ||
readonly parent: AnyParentNode | null, | ||
readonly definition: Definition | ||
readonly definition: Definition, | ||
options?: InstanceNodeOptions | ||
) { | ||
this.computeReference = options?.computeReference ?? this.computeChildStepReference; | ||
this.scope = createReactiveScope(); | ||
@@ -169,15 +172,2 @@ this.engineConfig = engineConfig; | ||
this.definition = definition; | ||
const checkStateInitialized = () => this.engineState != null; | ||
const [isStateInitialized, setStateInitialized] = createSignal(checkStateInitialized()); | ||
this.isStateInitialized = isStateInitialized; | ||
queueMicrotask(() => { | ||
if (checkStateInitialized()) { | ||
setStateInitialized(true); | ||
} else { | ||
throw new Error('Node state was never initialized'); | ||
} | ||
}); | ||
} | ||
@@ -197,14 +187,9 @@ | ||
protected abstract computeReference( | ||
parent: AnyInstanceNode | null, | ||
definition: Definition | ||
): string; | ||
getNodeByReference( | ||
getNodesByReference( | ||
this: AnyNode, | ||
visited: WeakSet<AnyNode>, | ||
dependencyReference: string | ||
): SubscribableDependency | null { | ||
): readonly SubscribableDependency[] { | ||
if (visited.has(this)) { | ||
return null; | ||
return []; | ||
} | ||
@@ -217,3 +202,7 @@ | ||
if (dependencyReference === nodeset) { | ||
return this; | ||
if (this.nodeType === 'repeat-instance') { | ||
return [this.parent]; | ||
} | ||
return [this]; | ||
} | ||
@@ -225,28 +214,22 @@ | ||
) { | ||
const children = this.getChildren(); | ||
if (children == null) { | ||
return null; | ||
} | ||
for (const child of children) { | ||
const dependency = child.getNodeByReference(visited, dependencyReference); | ||
if (dependency != null) { | ||
return dependency; | ||
} | ||
} | ||
return this.getChildren().flatMap((child) => { | ||
return child.getNodesByReference(visited, dependencyReference); | ||
}); | ||
} | ||
return this.parent?.getNodeByReference(visited, dependencyReference) ?? null; | ||
return this.parent?.getNodesByReference(visited, dependencyReference) ?? []; | ||
} | ||
// EvaluationContext: node-relative | ||
getSubscribableDependencyByReference( | ||
getSubscribableDependenciesByReference( | ||
this: AnyNode, | ||
reference: string | ||
): SubscribableDependency | null { | ||
const visited = new WeakSet<SubscribableDependency>(); | ||
): readonly SubscribableDependency[] { | ||
if (this.nodeType === 'root') { | ||
const visited = new WeakSet<AnyNode>(); | ||
return this.getNodeByReference(visited, reference); | ||
return this.getNodesByReference(visited, reference); | ||
} | ||
return this.root.getSubscribableDependenciesByReference(reference); | ||
} | ||
@@ -253,0 +236,0 @@ |
import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts'; | ||
import { SelectDefinition } from '../body/control/select/SelectDefinition.ts'; | ||
import type { GroupDefinition } from '../client/GroupNode.ts'; | ||
@@ -8,3 +7,5 @@ import type { SubtreeDefinition } from '../client/SubtreeNode.ts'; | ||
import { RepeatRange } from './RepeatRange.ts'; | ||
import { SelectField, type SelectFieldDefinition } from './SelectField.ts'; | ||
import type { SelectFieldDefinition } from './SelectField.ts'; | ||
import { SelectField } from './SelectField.ts'; | ||
import type { StringFieldDefinition } from './StringField.ts'; | ||
import { StringField } from './StringField.ts'; | ||
@@ -36,3 +37,3 @@ import { Subtree } from './Subtree.ts'; | ||
case 'repeat-sequence': { | ||
case 'repeat-range': { | ||
return new RepeatRange(parent, child); | ||
@@ -42,7 +43,16 @@ } | ||
case 'value-node': { | ||
if (child.bodyElement instanceof SelectDefinition) { | ||
return new SelectField(parent, child as SelectFieldDefinition); | ||
// TODO: this sort of awkwardness might go away if we embrace a | ||
// proliferation of node types throughout. | ||
switch (child.bodyElement?.type) { | ||
case 'select': | ||
case 'select1': | ||
return new SelectField(parent, child as SelectFieldDefinition); | ||
case 'input': | ||
case undefined: | ||
return new StringField(parent, child as StringFieldDefinition); | ||
default: | ||
throw new UnreachableError(child.bodyElement); | ||
} | ||
return new StringField(parent, child); | ||
} | ||
@@ -49,0 +59,0 @@ |
import type { Accessor } from 'solid-js'; | ||
import type { GroupDefinition, GroupNode } from '../client/GroupNode.ts'; | ||
import type { GroupDefinition, GroupNode, GroupNodeAppearances } from '../client/GroupNode.ts'; | ||
import type { TextRange } from '../index.ts'; | ||
@@ -41,9 +41,11 @@ import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; | ||
// GroupNode | ||
readonly nodeType = 'group'; | ||
readonly appearances: GroupNodeAppearances; | ||
readonly currentState: MaterializedChildren<CurrentState<GroupStateSpec>, GeneralChildNode>; | ||
readonly nodeType = 'group'; | ||
constructor(parent: GeneralParentNode, definition: GroupDefinition) { | ||
super(parent, definition); | ||
this.appearances = definition.bodyElement.appearances; | ||
const childrenState = createChildrenState<Group, GeneralChildNode>(this); | ||
@@ -56,3 +58,6 @@ | ||
{ | ||
...this.buildSharedStateSpec(parent, definition), | ||
reference: this.contextReference, | ||
readonly: this.isReadonly, | ||
relevant: this.isRelevant, | ||
required: this.isRequired, | ||
@@ -72,3 +77,7 @@ label: createNodeLabel(this, definition), | ||
this.engineState = state.engineState; | ||
this.currentState = materializeCurrentStateChildren(state.currentState, childrenState); | ||
this.currentState = materializeCurrentStateChildren( | ||
this.scope, | ||
state.currentState, | ||
childrenState | ||
); | ||
@@ -78,6 +87,2 @@ childrenState.setChildren(buildChildren(this)); | ||
protected computeReference(parent: GeneralParentNode): string { | ||
return this.computeChildStepReference(parent); | ||
} | ||
getChildren(): readonly GeneralChildNode[] { | ||
@@ -84,0 +89,0 @@ return this.childrenState.getChildren(); |
@@ -34,5 +34,5 @@ import { identity } from '@getodk/common/lib/identity.ts'; | ||
return Root.initialize(form.xformDOM, form.model.root, engineConfig); | ||
return new Root(form.xformDOM, form.model.root, engineConfig); | ||
}; | ||
initializeForm satisfies InitializeForm; |
import type { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import type { Accessor } from 'solid-js'; | ||
import type { DependentExpression } from '../../expression/DependentExpression.ts'; | ||
@@ -30,3 +31,3 @@ import type { ReactiveScope } from '../../lib/reactivity/scope.ts'; | ||
*/ | ||
get contextReference(): string; | ||
readonly contextReference: Accessor<string>; | ||
@@ -36,8 +37,6 @@ readonly contextNode: Node; | ||
/** | ||
* Resolves a nodeset reference, possibly relative to the | ||
* {@link EvaluationContext.contextNode}. | ||
* Resolves nodes corresponding to the provided node-set reference, possibly | ||
* relative to the {@link EvaluationContext.contextNode}. | ||
*/ | ||
readonly getSubscribableDependencyByReference: ( | ||
reference: string | ||
) => SubscribableDependency | null; | ||
getSubscribableDependenciesByReference(reference: string): readonly SubscribableDependency[]; | ||
} |
@@ -22,4 +22,4 @@ import type { ReactiveScope } from '../../lib/reactivity/scope.ts'; | ||
get isReadonly(): boolean; | ||
get isRelevant(): boolean; | ||
isReadonly(): boolean; | ||
isRelevant(): boolean; | ||
@@ -26,0 +26,0 @@ readonly encodeValue: (this: unknown, runtimeValue: RuntimeValue) => InstanceValue; |
import type { Accessor } from 'solid-js'; | ||
import { createComputed, createSignal, on } from 'solid-js'; | ||
import type { RepeatDefinition, RepeatInstanceNode } from '../client/RepeatInstanceNode.ts'; | ||
import type { | ||
RepeatDefinition, | ||
RepeatInstanceNode, | ||
RepeatInstanceNodeAppearances, | ||
} from '../client/RepeatInstanceNode.ts'; | ||
import type { TextRange } from '../index.ts'; | ||
@@ -49,5 +53,35 @@ import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; | ||
/** | ||
* @todo Should we special case repeat `readonly` inheritance the same way | ||
* we do for `relevant`? | ||
* | ||
* @see {@link hasNonRelevantAncestor} | ||
*/ | ||
declare readonly hasReadonlyAncestor: Accessor<boolean>; | ||
/** | ||
* A repeat instance can inherit non-relevance, just like any other node. That | ||
* inheritance is derived from the repeat instance's parent node in the | ||
* primary instance XML/DOM tree (and would be semantically expected to do so | ||
* even if we move away from that implementation detail). | ||
* | ||
* Since {@link RepeatInstance.parent} is a {@link RepeatRange}, which is a | ||
* runtime data model fiction that does not exist in that hierarchy, we pass | ||
* this call through, allowing the {@link RepeatRange} to check the actual | ||
* primary instance parent node's relevance state. | ||
* | ||
* @todo Should we apply similar reasoning in {@link hasReadonlyAncestor}? | ||
*/ | ||
override readonly hasNonRelevantAncestor: Accessor<boolean> = () => { | ||
return this.parent.hasNonRelevantAncestor(); | ||
}; | ||
// RepeatInstanceNode | ||
readonly nodeType = 'repeat-instance'; | ||
/** | ||
* @see {@link RepeatRange.appearances} | ||
*/ | ||
readonly appearances: RepeatInstanceNodeAppearances; | ||
readonly currentState: MaterializedChildren< | ||
@@ -63,4 +97,17 @@ CurrentState<RepeatInstanceStateSpec>, | ||
) { | ||
super(parent, definition); | ||
const { precedingInstance } = options; | ||
const precedingIndex = precedingInstance?.currentIndex ?? (() => -1); | ||
const initialIndex = precedingIndex() + 1; | ||
const [currentIndex, setCurrentIndex] = createSignal(initialIndex); | ||
super(parent, definition, { | ||
computeReference: (): string => { | ||
const currentPosition = currentIndex() + 1; | ||
return `${parent.contextReference()}[${currentPosition}]`; | ||
}, | ||
}); | ||
this.appearances = definition.bodyElement.appearances; | ||
const childrenState = createChildrenState<RepeatInstance, GeneralChildNode>(this); | ||
@@ -72,7 +119,2 @@ | ||
const { precedingInstance } = options; | ||
const precedingIndex = precedingInstance?.currentIndex ?? (() => -1); | ||
const initialIndex = precedingIndex() + 1; | ||
const [currentIndex, setCurrentIndex] = createSignal(initialIndex); | ||
this.currentIndex = currentIndex; | ||
@@ -83,4 +125,8 @@ | ||
{ | ||
...this.buildSharedStateSpec(parent, definition), | ||
reference: this.contextReference, | ||
readonly: this.isReadonly, | ||
relevant: this.isRelevant, | ||
required: this.isRequired, | ||
// TODO: only-child <group><label> | ||
label: createNodeLabel(this, definition), | ||
@@ -99,3 +145,7 @@ hint: null, | ||
this.engineState = state.engineState; | ||
this.currentState = materializeCurrentStateChildren(state.currentState, childrenState); | ||
this.currentState = materializeCurrentStateChildren( | ||
this.scope, | ||
state.currentState, | ||
childrenState | ||
); | ||
@@ -126,8 +176,2 @@ // Maintain current index state, updating as the parent range's children | ||
protected computeReference(parent: RepeatRange): string { | ||
const currentPosition = this.currentIndex() + 1; | ||
return `${parent.contextReference}[${currentPosition}]`; | ||
} | ||
protected override initializeContextNode(parentContextNode: Element, nodeName: string): Element { | ||
@@ -134,0 +178,0 @@ return this.createContextNode(parentContextNode, nodeName); |
import { insertAtIndex } from '@getodk/common/lib/array/insert.ts'; | ||
import type { Accessor } from 'solid-js'; | ||
import type { RepeatRangeNode } from '../client/RepeatRangeNode.ts'; | ||
import type { RepeatRangeNode, RepeatRangeNodeAppearances } from '../client/RepeatRangeNode.ts'; | ||
import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; | ||
import { createChildrenState } from '../lib/reactivity/createChildrenState.ts'; | ||
import { createComputedExpression } from '../lib/reactivity/createComputedExpression.ts'; | ||
import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts'; | ||
@@ -13,3 +14,3 @@ import { materializeCurrentStateChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts'; | ||
import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts'; | ||
import type { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts'; | ||
import type { RepeatRangeDefinition } from '../model/RepeatRangeDefinition.ts'; | ||
import type { RepeatDefinition } from './RepeatInstance.ts'; | ||
@@ -35,3 +36,3 @@ import { RepeatInstance } from './RepeatInstance.ts'; | ||
export class RepeatRange | ||
extends DescendantNode<RepeatSequenceDefinition, RepeatRangeStateSpec, RepeatInstance> | ||
extends DescendantNode<RepeatRangeDefinition, RepeatRangeStateSpec, RepeatInstance> | ||
implements RepeatRangeNode, EvaluationContext, SubscribableDependency | ||
@@ -69,10 +70,107 @@ { | ||
/** | ||
* @todo Should we special case repeat `readonly` state the same way | ||
* we do for `relevant`? | ||
* | ||
* @see {@link isSelfRelevant} | ||
*/ | ||
declare isSelfReadonly: Accessor<boolean>; | ||
private readonly emptyRangeEvaluationContext: EvaluationContext & { | ||
readonly contextNode: Comment; | ||
}; | ||
/** | ||
* @see {@link isSelfRelevant} | ||
*/ | ||
private readonly isEmptyRangeSelfRelevant: Accessor<boolean>; | ||
/** | ||
* A repeat range does not exist in the primary instance tree. A `relevant` | ||
* expression applies to each {@link RepeatInstance} child of the repeat | ||
* range. Determining whether a repeat range itself "is relevant" isn't a | ||
* concept the spec addresses, but it may be used by clients to determine | ||
* whether to allow interaction with the range (e.g. by adding a repeat | ||
* instance, or presenting the range's label when empty). | ||
* | ||
* As a naive first pass, it seems like the heuristic for this should be: | ||
* | ||
* 1. Does the repeat range have any repeat instance children? | ||
* | ||
* - If yes, go to 2. | ||
* - If no, go to 3. | ||
* | ||
* 2. Does one or more of those children return `true` for the node's | ||
* `relevant` expression (i.e. is the repeat instance "self relevant")? | ||
* | ||
* 3. Does the relevant expression return `true` for the repeat range itself | ||
* (where, at least for now, the context of that evaluation would be the | ||
* repeat range's {@link anchorNode} to ensure correct relative expressions | ||
* resolve correctly)? | ||
* | ||
* @todo While (3) is proactively implemented, there isn't presently a test | ||
* exercising it. It felt best for now to surface this for discussion in | ||
* review to validate that it's going in the right direction. | ||
* | ||
* @todo While (2) **is actually tested**, the tests currently in place behave | ||
* the same way with only the logic for (3), regardless of whether the repeat | ||
* range actually has any repeat instance children. It's unclear (a) if that's | ||
* a preferable simplification and (b) how that might affect performance (in | ||
* theory it could vary depending on form structure and runtime state). | ||
*/ | ||
override readonly isSelfRelevant: Accessor<boolean> = () => { | ||
const instances = this.childrenState.getChildren(); | ||
if (instances.length > 0) { | ||
return instances.some((instance) => instance.isSelfRelevant()); | ||
} | ||
return this.isEmptyRangeSelfRelevant(); | ||
}; | ||
// RepeatRangeNode | ||
readonly nodeType = 'repeat-range'; | ||
/** | ||
* @todo RepeatRange*, RepeatInstance* (and RepeatTemplate*) all share the | ||
* same body element, and thus all share the same definition `bodyElement`. As | ||
* such, they also all share the same `appearances`. At time of writing, | ||
* `web-forms` (Vue UI package) treats a `RepeatRangeNode`... | ||
* | ||
* - ... as a group, if the node has a label (i.e. | ||
* `<group><label/><repeat/></group>`) | ||
* - ... effectively as a fragment containing only its instances, otherwise | ||
* | ||
* We now collapse `<group><repeat>` into `<repeat>`, and no longer treat | ||
* "repeat group" as a concept (after parsing). According to the spec, these | ||
* appearances **are supposed to** come from that "repeat group" in the form | ||
* definition. In practice, many forms do define appearances directly on a | ||
* repeat element. The engine currently produces an error if both are defined | ||
* simultaneously, but otherwise makes no distinction between appearances in | ||
* these form definition shapes: | ||
* | ||
* ```xml | ||
* <group ref="/data/rep1" appearance="..."> | ||
* <repeat nodeset="/data/rep1"/> | ||
* </group> | ||
* | ||
* <group ref="/data/rep1"> | ||
* <repeat nodeset="/data/rep1"/ appearance="..."> | ||
* </group> | ||
* | ||
* <repeat nodeset="/data/rep1"/ appearance="..."> | ||
* ``` | ||
* | ||
* All of the above creates considerable ambiguity about where "repeat | ||
* appearances" should apply, under which circumstances. | ||
*/ | ||
readonly appearances: RepeatRangeNodeAppearances; | ||
readonly currentState: MaterializedChildren<CurrentState<RepeatRangeStateSpec>, RepeatInstance>; | ||
constructor(parent: GeneralParentNode, definition: RepeatSequenceDefinition) { | ||
constructor(parent: GeneralParentNode, definition: RepeatRangeDefinition) { | ||
super(parent, definition); | ||
this.appearances = definition.bodyElement.appearances; | ||
const childrenState = createChildrenState<RepeatRange, RepeatInstance>(this); | ||
@@ -82,6 +180,31 @@ | ||
this.anchorNode = this.contextNode.ownerDocument.createComment( | ||
`Begin repeat range: ${definition.nodeset}` | ||
); | ||
this.contextNode.append(this.anchorNode); | ||
this.emptyRangeEvaluationContext = { | ||
scope: this.scope, | ||
evaluator: this.evaluator, | ||
root: this.root, | ||
contextReference: this.contextReference, | ||
contextNode: this.anchorNode, | ||
getSubscribableDependenciesByReference: (reference) => { | ||
return this.getSubscribableDependenciesByReference(reference); | ||
}, | ||
}; | ||
this.isEmptyRangeSelfRelevant = createComputedExpression( | ||
this.emptyRangeEvaluationContext, | ||
definition.bind.relevant | ||
); | ||
const state = createSharedNodeState( | ||
this.scope, | ||
{ | ||
...this.buildSharedStateSpec(parent, definition), | ||
reference: this.contextReference, | ||
readonly: this.isReadonly, | ||
relevant: this.isRelevant, | ||
required: this.isRequired, | ||
@@ -99,10 +222,9 @@ label: createNodeLabel(this, definition), | ||
this.anchorNode = this.contextNode.ownerDocument.createComment( | ||
`Begin repeat range: ${definition.nodeset}` | ||
); | ||
this.contextNode.append(this.anchorNode); | ||
this.state = state; | ||
this.engineState = state.engineState; | ||
this.currentState = materializeCurrentStateChildren(state.currentState, childrenState); | ||
this.currentState = materializeCurrentStateChildren( | ||
this.scope, | ||
state.currentState, | ||
childrenState | ||
); | ||
@@ -124,6 +246,2 @@ definition.instances.forEach((instanceDefinition, index) => { | ||
protected computeReference(parent: GeneralParentNode): string { | ||
return this.computeChildStepReference(parent); | ||
} | ||
getInstanceIndex(instance: RepeatInstance): number { | ||
@@ -130,0 +248,0 @@ return this.engineState.children.indexOf(instance.nodeId); |
@@ -5,2 +5,3 @@ import type { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
import type { XFormDOM } from '../XFormDOM.ts'; | ||
import type { BodyClassList } from '../body/BodyDefinition.ts'; | ||
import type { ActiveLanguage, FormLanguage, FormLanguages } from '../client/FormLanguage.ts'; | ||
@@ -102,17 +103,9 @@ import type { RootNode } from '../client/RootNode.ts'; | ||
{ | ||
static async initialize( | ||
xformDOM: XFormDOM, | ||
definition: RootDefinition, | ||
engineConfig: InstanceConfig | ||
): Promise<Root> { | ||
const instance = new Root(xformDOM, definition, engineConfig); | ||
await instance.formStateInitialized(); | ||
return instance; | ||
} | ||
private readonly childrenState: ChildrenState<GeneralChildNode>; | ||
// InstanceNode | ||
readonly hasReadonlyAncestor = () => false; | ||
readonly isReadonly = () => false; | ||
readonly hasNonRelevantAncestor = () => false; | ||
readonly isRelevant = () => true; | ||
protected readonly state: SharedNodeState<RootStateSpec>; | ||
@@ -123,3 +116,4 @@ protected readonly engineState: EngineState<RootStateSpec>; | ||
readonly nodeType = 'root'; | ||
readonly appearances = null; | ||
readonly classes: BodyClassList; | ||
readonly currentState: MaterializedChildren<CurrentState<RootStateSpec>, GeneralChildNode>; | ||
@@ -135,8 +129,2 @@ | ||
private readonly rootReference: string; | ||
override get contextReference(): string { | ||
return this.rootReference; | ||
} | ||
readonly contextNode: Element; | ||
@@ -154,9 +142,11 @@ | ||
protected constructor( | ||
xformDOM: XFormDOM, | ||
definition: RootDefinition, | ||
engineConfig: InstanceConfig | ||
) { | ||
super(engineConfig, null, definition); | ||
constructor(xformDOM: XFormDOM, definition: RootDefinition, engineConfig: InstanceConfig) { | ||
const reference = definition.nodeset; | ||
super(engineConfig, null, definition, { | ||
computeReference: () => reference, | ||
}); | ||
this.classes = definition.classes; | ||
const childrenState = createChildrenState<Root, GeneralChildNode>(this); | ||
@@ -166,6 +156,2 @@ | ||
const reference = definition.nodeset; | ||
this.rootReference = reference; | ||
const instanceDOM = xformDOM.createInstance(); | ||
@@ -196,3 +182,7 @@ const evaluator = instanceDOM.primaryInstanceEvaluator; | ||
this.engineState = state.engineState; | ||
this.currentState = materializeCurrentStateChildren(state.currentState, childrenState); | ||
this.currentState = materializeCurrentStateChildren( | ||
this.scope, | ||
state.currentState, | ||
childrenState | ||
); | ||
@@ -211,36 +201,2 @@ const contextNode = instanceDOM.xformDocument.createElement(definition.nodeName); | ||
/** | ||
* Waits until form state is fully initialized. | ||
* | ||
* As much as possible, all instance state computations are implemented so | ||
* that they complete synchronously. | ||
* | ||
* There is currently one exception: because instance nodes may form | ||
* computation dependencies into their descendants as well as their ancestors, | ||
* there is an allowance **during form initialization only** to account for | ||
* this chicken/egg scenario. Note that this allowance is intentionally, | ||
* strictly limited: if form state initialization is not resolved within a | ||
* single microtask tick we throw/reject. | ||
* | ||
* All subsequent computations are always performed synchronously (and we will | ||
* use tests to validate this, by utilizing the synchronously returned `Root` | ||
* state from client-facing write interfaces). | ||
*/ | ||
async formStateInitialized(): Promise<void> { | ||
await new Promise<void>((resolve) => { | ||
queueMicrotask(resolve); | ||
}); | ||
if (!this.isStateInitialized()) { | ||
throw new Error( | ||
'Form state initialization failed to complete in a single frame. Has some aspect of reactive computation been made asynchronous by mistake?' | ||
); | ||
} | ||
} | ||
// InstanceNode | ||
protected computeReference(_parent: null, definition: RootDefinition): string { | ||
return definition.nodeset; | ||
} | ||
getChildren(): readonly GeneralChildNode[] { | ||
@@ -247,0 +203,0 @@ return this.childrenState.getChildren(); |
@@ -5,3 +5,3 @@ import { xmlXPathWhitespaceSeparatedList } from '@getodk/common/lib/string/whitespace.ts'; | ||
import type { AnySelectDefinition } from '../body/control/select/SelectDefinition.ts'; | ||
import type { SelectItem, SelectNode } from '../client/SelectNode.ts'; | ||
import type { SelectItem, SelectNode, SelectNodeAppearances } from '../client/SelectNode.ts'; | ||
import type { TextRange } from '../index.ts'; | ||
@@ -54,3 +54,3 @@ import { createSelectItems } from '../lib/reactivity/createSelectItems.ts'; | ||
readonly nodeType = 'select'; | ||
readonly appearances: SelectNodeAppearances; | ||
readonly currentState: CurrentState<SelectFieldStateSpec>; | ||
@@ -88,2 +88,3 @@ | ||
this.appearances = definition.bodyElement.appearances; | ||
this.selectExclusive = definition.bodyElement.type === 'select1'; | ||
@@ -98,3 +99,6 @@ | ||
{ | ||
...this.buildSharedStateSpec(parent, definition), | ||
reference: this.contextReference, | ||
readonly: this.isReadonly, | ||
relevant: this.isRelevant, | ||
required: this.isRequired, | ||
@@ -127,6 +131,2 @@ label: createNodeLabel(this, definition), | ||
protected computeReference(parent: GeneralParentNode): string { | ||
return this.computeChildStepReference(parent); | ||
} | ||
protected updateSelectedItemValues(values: readonly string[]) { | ||
@@ -133,0 +133,0 @@ const itemsByValue = untrack(() => this.getSelectItemsByValue()); |
import { identity } from '@getodk/common/lib/identity.ts'; | ||
import type { Accessor } from 'solid-js'; | ||
import type { InputDefinition } from '../body/control/InputDefinition.ts'; | ||
import type { StringNode } from '../client/StringNode.ts'; | ||
import type { StringNode, StringNodeAppearances } from '../client/StringNode.ts'; | ||
import type { TextRange } from '../index.ts'; | ||
@@ -46,3 +46,3 @@ import { createValueState } from '../lib/reactivity/createValueState.ts'; | ||
readonly nodeType = 'string'; | ||
readonly appearances: StringNodeAppearances; | ||
readonly currentState: CurrentState<StringFieldStateSpec>; | ||
@@ -58,6 +58,11 @@ | ||
this.appearances = (definition.bodyElement?.appearances ?? null) as StringNodeAppearances; | ||
const state = createSharedNodeState( | ||
this.scope, | ||
{ | ||
...this.buildSharedStateSpec(parent, definition), | ||
reference: this.contextReference, | ||
readonly: this.isReadonly, | ||
relevant: this.isRelevant, | ||
required: this.isRequired, | ||
@@ -80,6 +85,2 @@ label: createNodeLabel(this, definition), | ||
protected computeReference(parent: GeneralParentNode): string { | ||
return this.computeChildStepReference(parent); | ||
} | ||
// InstanceNode | ||
@@ -86,0 +87,0 @@ getChildren(): readonly [] { |
@@ -39,3 +39,3 @@ import { type Accessor } from 'solid-js'; | ||
readonly nodeType = 'subtree'; | ||
readonly appearances = null; | ||
readonly currentState: MaterializedChildren<CurrentState<SubtreeStateSpec>, GeneralChildNode>; | ||
@@ -53,3 +53,6 @@ | ||
{ | ||
...this.buildSharedStateSpec(parent, definition), | ||
reference: this.contextReference, | ||
readonly: this.isReadonly, | ||
relevant: this.isRelevant, | ||
required: this.isRequired, | ||
@@ -69,3 +72,7 @@ label: null, | ||
this.engineState = state.engineState; | ||
this.currentState = materializeCurrentStateChildren(state.currentState, childrenState); | ||
this.currentState = materializeCurrentStateChildren( | ||
this.scope, | ||
state.currentState, | ||
childrenState | ||
); | ||
@@ -75,6 +82,2 @@ childrenState.setChildren(buildChildren(this)); | ||
protected computeReference(parent: GeneralParentNode): string { | ||
return this.computeChildStepReference(parent); | ||
} | ||
getChildren(): readonly GeneralChildNode[] { | ||
@@ -81,0 +84,0 @@ return this.childrenState.getChildren(); |
@@ -12,2 +12,6 @@ import { ScopedElementLookup } from '@getodk/common/lib/dom/compatibility.ts'; | ||
const labelLookup = new ScopedElementLookup(':scope > label', 'label'); | ||
const repeatGroupLabelLookup = new ScopedElementLookup( | ||
':scope > label[form-definition-source="repeat-group"]', | ||
'label[form-definition-source="repeat-group"]' | ||
); | ||
const repeatLookup = new ScopedElementLookup(':scope > repeat[nodeset]', 'repeat[nodeset]'); | ||
@@ -24,2 +28,7 @@ const valueLookup = new ScopedElementLookup(':scope > value', 'value'); | ||
export interface RepeatGroupLabelElement extends LabelElement { | ||
getAttribute(name: 'form-definition-source'): 'repeat-group'; | ||
getAttribute(name: string): string; | ||
} | ||
export interface RepeatElement extends KnownAttributeLocalNamedElement<'repeat', 'nodeset'> {} | ||
@@ -45,2 +54,6 @@ | ||
export const getRepeatGroupLabelElement = (parent: Element): RepeatGroupLabelElement | null => { | ||
return repeatGroupLabelLookup.getElement<RepeatGroupLabelElement>(parent); | ||
}; | ||
export const getRepeatElement = (parent: Element): RepeatElement | null => { | ||
@@ -47,0 +60,0 @@ return repeatLookup.getElement<RepeatElement>(parent); |
@@ -1,2 +0,3 @@ | ||
import { createMemo, createSignal, type Accessor, type Setter, type Signal } from 'solid-js'; | ||
import type { Accessor, Setter, Signal } from 'solid-js'; | ||
import { createSignal } from 'solid-js'; | ||
import type { OpaqueReactiveObjectFactory } from '../../index.ts'; | ||
@@ -47,8 +48,52 @@ import type { AnyChildNode, AnyParentNode } from '../../instance/hierarchy.ts'; | ||
return parent.scope.runTask(() => { | ||
const children = createSignal<readonly Child[]>([]); | ||
const [getChildren, setChildren] = children; | ||
const childIds = createMemo((): readonly NodeID[] => { | ||
return getChildren().map((child) => child.nodeId); | ||
}); | ||
const baseState = createSignal<readonly Child[]>([]); | ||
const [getChildren, baseSetChildren] = baseState; | ||
/** | ||
* Note: this is clearly derived state. It would be obvious to use | ||
* `createMemo`, which is exactly what a previous iteration did. This caused | ||
* mysterious issues when clients: | ||
* | ||
* - Also used Solid-based reactivity | ||
* - Accessed node children state within their own `createMemo` calls | ||
* | ||
* It's quite likely that there's a more robust and general solution, which | ||
* may be applicable if we also generalize this approach to | ||
* encode/materialize shared structured state (e.g. it may be applicable for | ||
* select values, form language, probably much more in coming features). | ||
* | ||
* In the meantime, while this approach is marginally more complex, it is | ||
* likely also slightly more efficient. We can revisit the tradeoff if/when | ||
* those hypothetical generalizations become a priority. | ||
*/ | ||
const ids = createSignal<readonly NodeID[]>([]); | ||
const [childIds, setChildIds] = ids; | ||
type ChildrenSetterCallback = (prev: readonly Child[]) => readonly Child[]; | ||
type ChildrenOrSetterCallback = ChildrenSetterCallback | readonly Child[]; | ||
const setChildren: Setter<readonly Child[]> = ( | ||
valueOrSetterCallback: ChildrenOrSetterCallback | ||
) => { | ||
let setterCallback: ChildrenSetterCallback; | ||
if (typeof valueOrSetterCallback === 'function') { | ||
setterCallback = valueOrSetterCallback; | ||
} else { | ||
setterCallback = (_) => valueOrSetterCallback; | ||
} | ||
return baseSetChildren((prev) => { | ||
const result = setterCallback(prev); | ||
setChildIds(() => { | ||
return result.map((child) => child.nodeId); | ||
}); | ||
return result; | ||
}); | ||
}; | ||
const children: Signal<readonly Child[]> = [getChildren, setChildren]; | ||
return { | ||
@@ -55,0 +100,0 @@ children, |
@@ -90,3 +90,3 @@ import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts'; | ||
return dependencyReferences.flatMap((reference) => { | ||
return context.getSubscribableDependencyByReference(reference) ?? []; | ||
return context.getSubscribableDependenciesByReference(reference) ?? []; | ||
}); | ||
@@ -93,0 +93,0 @@ }); |
@@ -55,7 +55,4 @@ import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts'; | ||
readonly root: EvaluationContextRoot; | ||
readonly contextReference: Accessor<string>; | ||
get contextReference(): string { | ||
return this.selectField.contextReference; | ||
} | ||
constructor( | ||
@@ -68,6 +65,7 @@ private readonly selectField: SelectField, | ||
this.root = selectField.root; | ||
this.contextReference = selectField.contextReference; | ||
} | ||
getSubscribableDependencyByReference(reference: string): SubscribableDependency | null { | ||
return this.selectField.getSubscribableDependencyByReference(reference); | ||
getSubscribableDependenciesByReference(reference: string): readonly SubscribableDependency[] { | ||
return this.selectField.getSubscribableDependenciesByReference(reference); | ||
} | ||
@@ -74,0 +72,0 @@ } |
@@ -75,3 +75,3 @@ import { createComputed, createMemo, createSignal, untrack } from 'solid-js'; | ||
{ | ||
isRelevant: context.isRelevant, | ||
isRelevant: context.isRelevant(), | ||
value: initialValue, | ||
@@ -95,3 +95,3 @@ }, | ||
createComputed(() => { | ||
const isRelevant = context.isRelevant; | ||
const isRelevant = context.isRelevant(); | ||
@@ -127,3 +127,3 @@ setValueForPersistence((persisted) => { | ||
const persisted = setValueForPersistence({ | ||
isRelevant: context.isRelevant, | ||
isRelevant: context.isRelevant(), | ||
value, | ||
@@ -195,4 +195,4 @@ }); | ||
const setValue: SimpleAtomicStateSetter<RuntimeValue> = (value) => { | ||
if (context.isReadonly) { | ||
const reference = untrack(() => context.contextReference); | ||
if (context.isReadonly()) { | ||
const reference = untrack(() => context.contextReference()); | ||
@@ -222,3 +222,3 @@ throw new Error(`Cannot write to readonly field: ${reference}`); | ||
createComputed(() => { | ||
if (context.isRelevant) { | ||
if (context.isRelevant()) { | ||
const calculated = calculate(); | ||
@@ -225,0 +225,0 @@ const value = context.decodeValue(calculated); |
@@ -6,2 +6,3 @@ import type { AnyChildNode } from '../../instance/hierarchy.ts'; | ||
import type { CurrentState } from './node-state/createCurrentState.ts'; | ||
import type { ReactiveScope } from './scope.ts'; | ||
@@ -100,2 +101,3 @@ interface InconsistentChildrenStateDetails { | ||
>( | ||
scope: ReactiveScope, | ||
currentState: ParentState, | ||
@@ -110,3 +112,3 @@ childrenState: ChildrenState<Child> | ||
if (key === 'children') { | ||
const expectedChildIDs = currentState.children; | ||
const expectedChildIDs = scope.runTask(() => currentState.children); | ||
const children = childrenState.getChildren(); | ||
@@ -113,0 +115,0 @@ |
import type { AnyBodyElementDefinition } from '../body/BodyDefinition.ts'; | ||
import type { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import type { BindDefinition } from './BindDefinition.ts'; | ||
@@ -17,3 +16,3 @@ import type { | ||
type DescendentNodeBodyElement = AnyBodyElementDefinition | RepeatDefinition; | ||
type DescendentNodeBodyElement = AnyBodyElementDefinition; | ||
@@ -20,0 +19,0 @@ export abstract class DescendentNodeDefinition< |
@@ -11,3 +11,3 @@ import type { XFormDefinition } from '../XFormDefinition.ts'; | ||
this.binds = ModelBindMap.fromModel(this); | ||
this.root = new RootDefinition(form, this); | ||
this.root = new RootDefinition(form, this, form.body.classes); | ||
} | ||
@@ -14,0 +14,0 @@ |
import type { AnyBodyElementDefinition } from '../body/BodyDefinition.ts'; | ||
import type { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import type { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts'; | ||
import type { BindDefinition } from './BindDefinition.ts'; | ||
import type { RepeatInstanceDefinition } from './RepeatInstanceDefinition.ts'; | ||
import type { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import type { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
import type { RepeatTemplateDefinition } from './RepeatTemplateDefinition.ts'; | ||
@@ -20,9 +20,9 @@ import type { RootDefinition } from './RootDefinition.ts'; | ||
/** | ||
* Corresponds to a sequence of model/entry DOM subtrees which in turn | ||
* Corresponds to a range/sequence of model/entry DOM subtrees which in turn | ||
* corresponds to a <repeat> in the form body definition. | ||
*/ | ||
export type RepeatSequenceType = 'repeat-sequence'; | ||
export type RepeatRangeType = 'repeat-range'; | ||
/** | ||
* Corresponds to a template definition for a repeat sequence, which either has | ||
* Corresponds to a template definition for a repeat range, which either has | ||
* an explicit `jr:template=""` attribute in the form definition or is inferred | ||
@@ -36,3 +36,3 @@ * as a template from the form's first element matched by a <repeat nodeset>. | ||
* in turn corresponds to a <repeat> in the form body definition, and a | ||
* 'repeat-sequence' definition. | ||
* 'repeat-range' definition. | ||
*/ | ||
@@ -60,3 +60,3 @@ export type RepeatInstanceType = 'repeat-instance'; | ||
| RootNodeType | ||
| RepeatSequenceType | ||
| RepeatRangeType | ||
| RepeatTemplateType | ||
@@ -77,3 +77,3 @@ | RepeatInstanceType | ||
export type ChildNodeDefinition = | ||
| RepeatSequenceDefinition | ||
| RepeatRangeDefinition | ||
| SubtreeDefinition | ||
@@ -98,3 +98,3 @@ | ValueNodeDefinition; | ||
export type NodeInstances<Type extends NodeDefinitionType> = | ||
Type extends 'repeat-sequence' | ||
Type extends 'repeat-range' | ||
? readonly RepeatInstanceDefinition[] | ||
@@ -112,3 +112,3 @@ : null; | ||
export type ModelNode<Type extends NodeDefinitionType> = | ||
Type extends 'repeat-sequence' | ||
Type extends 'repeat-range' | ||
? null | ||
@@ -129,3 +129,3 @@ : Element; | ||
readonly nodeName: string; | ||
readonly bodyElement: AnyBodyElementDefinition | RepeatDefinition | null; | ||
readonly bodyElement: AnyBodyElementDefinition | RepeatElementDefinition | null; | ||
@@ -147,3 +147,3 @@ readonly isTranslated: boolean; | ||
| RootDefinition | ||
| RepeatSequenceDefinition | ||
| RepeatRangeDefinition | ||
| RepeatTemplateDefinition | ||
@@ -150,0 +150,0 @@ | RepeatInstanceDefinition |
@@ -1,8 +0,8 @@ | ||
import { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts'; | ||
import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
import type { ChildNodeDefinition, NodeDefinition } from './NodeDefinition.ts'; | ||
import type { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import type { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
export class RepeatInstanceDefinition | ||
extends DescendentNodeDefinition<'repeat-instance', RepeatDefinition> | ||
extends DescendentNodeDefinition<'repeat-instance', RepeatElementDefinition> | ||
implements NodeDefinition<'repeat-instance'> | ||
@@ -18,15 +18,10 @@ { | ||
constructor( | ||
protected readonly sequence: RepeatSequenceDefinition, | ||
range: RepeatRangeDefinition, | ||
readonly node: Element | ||
) { | ||
const { | ||
bind, | ||
bodyElement: repeatGroupBodyElement, | ||
parent: repeatSequenceParent, | ||
root, | ||
} = sequence; | ||
const { bind, bodyElement, parent, root } = range; | ||
super(repeatSequenceParent, bind, repeatGroupBodyElement.repeat); | ||
super(parent, bind, bodyElement); | ||
this.nodeName = sequence.nodeName; | ||
this.nodeName = range.nodeName; | ||
this.children = root.buildSubtree(this); | ||
@@ -36,3 +31,3 @@ } | ||
toJSON() { | ||
const { bind, bodyElement, parent, root, sequence, ...rest } = this; | ||
const { bind, bodyElement, parent, root, ...rest } = this; | ||
@@ -39,0 +34,0 @@ return rest; |
import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts'; | ||
import type { RepeatDefinition } from '../body/RepeatDefinition.ts'; | ||
import type { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts'; | ||
import { BindDefinition } from './BindDefinition.ts'; | ||
import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
import type { ChildNodeDefinition, NodeDefinition } from './NodeDefinition.ts'; | ||
import type { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import type { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
@@ -76,10 +76,10 @@ const repeatTemplates = new WeakMap<BindDefinition, RepeatTemplateDefinition>(); | ||
export class RepeatTemplateDefinition | ||
extends DescendentNodeDefinition<'repeat-template', RepeatDefinition> | ||
extends DescendentNodeDefinition<'repeat-template', RepeatElementDefinition> | ||
implements NodeDefinition<'repeat-template'> | ||
{ | ||
static parseModelNodes( | ||
sequence: RepeatSequenceDefinition, | ||
range: RepeatRangeDefinition, | ||
modelNodes: readonly [Element, ...Element[]] | ||
): ParsedRepeatNodes { | ||
const { bind } = sequence; | ||
const { bind } = range; | ||
@@ -93,3 +93,3 @@ let template = repeatTemplates.get(bind); | ||
instanceNodes = rest; | ||
template = new this(sequence, templateNode); | ||
template = new this(range, templateNode); | ||
} else { | ||
@@ -126,13 +126,8 @@ // TODO: this is under the assumption that for any depth > 1, if a | ||
protected constructor( | ||
protected readonly sequence: RepeatSequenceDefinition, | ||
range: RepeatRangeDefinition, | ||
protected readonly templateNode: ExplicitRepeatTemplateElement | ||
) { | ||
const { | ||
bind, | ||
bodyElement: repeatGroupBodyElement, | ||
parent: repeatSequenceParent, | ||
root, | ||
} = sequence; | ||
const { bind, bodyElement, parent, root } = range; | ||
super(repeatSequenceParent, bind, repeatGroupBodyElement.repeat); | ||
super(parent, bind, bodyElement); | ||
@@ -149,3 +144,3 @@ const node = templateNode.cloneNode(true) as Element; | ||
toJSON() { | ||
const { bind, bodyElement, parent, root, sequence, ...rest } = this; | ||
const { bind, bodyElement, parent, root, ...rest } = this; | ||
@@ -152,0 +147,0 @@ return rest; |
import type { XFormDefinition } from '../XFormDefinition.ts'; | ||
import type { RepeatGroupDefinition } from '../body/group/RepeatGroupDefinition.ts'; | ||
import type { BodyClassList } from '../body/BodyDefinition.ts'; | ||
import type { BindDefinition } from './BindDefinition.ts'; | ||
@@ -10,3 +10,3 @@ import type { ModelDefinition } from './ModelDefinition.ts'; | ||
} from './NodeDefinition.ts'; | ||
import { RepeatSequenceDefinition } from './RepeatSequenceDefinition.ts'; | ||
import { RepeatRangeDefinition } from './RepeatRangeDefinition.ts'; | ||
import { SubtreeDefinition } from './SubtreeDefinition.ts'; | ||
@@ -33,3 +33,4 @@ import { ValueNodeDefinition } from './ValueNodeDefinition.ts'; | ||
protected readonly form: XFormDefinition, | ||
protected readonly model: ModelDefinition | ||
protected readonly model: ModelDefinition, | ||
readonly classes: BodyClassList | ||
) { | ||
@@ -91,12 +92,5 @@ // TODO: theoretically the pertinent step in the bind's `nodeset` *could* be | ||
const [firstChild, ...restChildren] = children; | ||
const repeatGroup = body.getRepeatGroup(nodeset); | ||
if (repeatGroup != null) { | ||
const repeatDefinition = (bodyElement as RepeatGroupDefinition).repeat; | ||
if (repeatDefinition == null) { | ||
throw 'TODO: this is why I have hesitated to pick an "is repeat" predicate direction'; | ||
} | ||
return new RepeatSequenceDefinition(parent, bind, repeatGroup, children); | ||
if (bodyElement?.type === 'repeat') { | ||
return new RepeatRangeDefinition(parent, bind, bodyElement, children); | ||
} | ||
@@ -103,0 +97,0 @@ |
import type { | ||
AnyBodyElementDefinition, | ||
NonRepeatGroupElementDefinition, | ||
AnyGroupElementDefinition, | ||
} from '../body/BodyDefinition.ts'; | ||
@@ -14,3 +14,3 @@ import type { BindDefinition } from './BindDefinition.ts'; | ||
export class SubtreeDefinition | ||
extends DescendentNodeDefinition<'subtree', NonRepeatGroupElementDefinition | null> | ||
extends DescendentNodeDefinition<'subtree', AnyGroupElementDefinition | null> | ||
implements NodeDefinition<'subtree'> | ||
@@ -33,3 +33,3 @@ { | ||
bodyElement != null && | ||
(bodyElement.category !== 'structure' || bodyElement.type === 'repeat-group') | ||
(bodyElement.category !== 'structure' || bodyElement.type === 'repeat') | ||
) { | ||
@@ -36,0 +36,0 @@ throw new Error(`Unexpected body element for nodeset ${bind.nodeset}`); |
@@ -1,3 +0,2 @@ | ||
import type { AnyBodyElementDefinition } from '../body/BodyDefinition.ts'; | ||
import type { AnyControlDefinition } from '../body/control/ControlDefinition.ts'; | ||
import type { AnyBodyElementDefinition, ControlElementDefinition } from '../body/BodyDefinition.ts'; | ||
import type { BindDefinition } from './BindDefinition.ts'; | ||
@@ -8,3 +7,3 @@ import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts'; | ||
export class ValueNodeDefinition | ||
extends DescendentNodeDefinition<'value-node', AnyControlDefinition | null> | ||
extends DescendentNodeDefinition<'value-node', ControlElementDefinition | null> | ||
implements NodeDefinition<'value-node'> | ||
@@ -11,0 +10,0 @@ { |
@@ -1,2 +0,2 @@ | ||
import { XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts'; | ||
import { XFORMS_NAMESPACE_URI, XMLNS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts'; | ||
import { XFormsXPathEvaluator } from '@getodk/xpath'; | ||
@@ -44,3 +44,79 @@ | ||
const normalizeRepeatGroups = (xformDocument: XMLDocument, body: Element): void => { | ||
const normalizeRepeatGroupAttributes = (group: Element, repeat: Element): void => { | ||
for (const groupAttribute of group.attributes) { | ||
const { localName, namespaceURI, nodeName, value } = groupAttribute; | ||
if ( | ||
// Don't propagate namespace declarations (which appear as attributes in | ||
// the browser/XML DOM, either named `xmlns` or with an `xmlns` prefix, | ||
// always in the XMLNS namespace). | ||
namespaceURI === XMLNS_NAMESPACE_URI || | ||
// Don't propagate `ref`, it has been normalized as `nodeset` on the | ||
// repeat element. | ||
localName === 'ref' || | ||
// TODO: this accommodates tests of this normalization process, where | ||
// certain nodes of interest are given an `id` attribute, and looked up | ||
// for the purpose of asserting what was normalized about them. It's | ||
// unclear if there's a generally expected behavior around the attribute. | ||
localName === 'id' | ||
) { | ||
continue; | ||
} | ||
// TODO: The `appearance` attribute is propagated from | ||
// `<group appearance><repeat>` to `<repeat appearance>`. But we presently | ||
// bail if both elements define the attribute. | ||
// | ||
// The spec is clear that the attribute is only supported on `<group>` and | ||
// control elements, which would suggest it should not be present on a | ||
// `<repeat>` element directly. But many form fixtures (in e.g. Enketo) | ||
// do have `<repeat apperance>`. | ||
// | ||
// It may be reasonable to relax this by: | ||
// | ||
// - Detecting if they share the same appearances, treated as a no-op. | ||
// | ||
// - Assume they're both meant to apply, and concatenate. | ||
if ( | ||
localName === 'appearance' && | ||
namespaceURI === XFORMS_NAMESPACE_URI && | ||
repeat.hasAttribute(localName) | ||
) { | ||
const ref = group.getAttribute('ref'); | ||
throw new Error( | ||
`Failed to normalize conflicting "appearances" attribute of group/repeat "${ref}"` | ||
); | ||
} | ||
repeat.setAttributeNS(namespaceURI, nodeName, value); | ||
} | ||
}; | ||
const normalizeRepeatGroupLabel = (group: Element, repeat: Element): void => { | ||
const groupLabel = Array.from(group.children).find((child) => { | ||
return child.localName === 'label'; | ||
}); | ||
if (groupLabel == null) { | ||
return; | ||
} | ||
const repeatLabel = groupLabel.cloneNode(true) as Element; | ||
repeatLabel.setAttribute('form-definition-source', 'repeat-group'); | ||
repeat.prepend(repeatLabel); | ||
groupLabel.remove(); | ||
}; | ||
const unwrapRepeatGroup = (group: Element, repeat: Element): void => { | ||
normalizeRepeatGroupAttributes(group, repeat); | ||
normalizeRepeatGroupLabel(group, repeat); | ||
group.replaceWith(repeat); | ||
}; | ||
const normalizeRepeatGroups = (body: Element): void => { | ||
const repeats = body.querySelectorAll('repeat'); | ||
@@ -67,7 +143,4 @@ | ||
if (group == null) { | ||
group = xformDocument.createElementNS(XFORMS_NAMESPACE_URI, 'group'); | ||
group.setAttribute('ref', repeatNodeset); | ||
repeat.before(group); | ||
group.append(repeat); | ||
if (group != null) { | ||
unwrapRepeatGroup(group, repeat); | ||
} | ||
@@ -118,3 +191,3 @@ } | ||
normalizeBodyRefNodesetAttributes(body); | ||
normalizeRepeatGroups(xformDocument, body); | ||
normalizeRepeatGroups(body); | ||
@@ -121,0 +194,0 @@ normalizedXML = html.outerHTML; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
9842217
216
85855
0