Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions docs/advanced-filemaker-scripting-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ever reads our own extended output.

## Core invariant

> `ToDisplayLine()` and `FromDisplayParams()` together are lossless for
> `ToDisplayLine()` and `PopulateFromDisplay()` together are lossless for
> every piece of state the POCO carries. XML state that is dropped is
> dropped because it carries no information, not because the display
> can't express it.
Expand Down Expand Up @@ -177,6 +177,6 @@ per-step extension. Covered here for completeness.
- `ScriptLineParser.ParseLine`
(`src/SharpFM.Model/Scripting/ScriptLineParser.cs`) — disabled-step
prefix and bracket tokenization.
- `PerformScriptStep.FromDisplayParams`,
`GoToLayoutStep.FromDisplayParams` — Form 1 regex parsers for named
- `PerformScriptStep.PopulateFromDisplay`,
`GoToLayoutStep.PopulateFromDisplay` — Form 1 regex parsers for named
refs with `(#id)` suffixes.
27 changes: 5 additions & 22 deletions src/SharpFM.Model/Scripting/Registry/StepMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using SharpFM.Model.Scripting.Shapes;

namespace SharpFM.Model.Scripting.Registry;
Expand All @@ -12,12 +10,11 @@ namespace SharpFM.Model.Scripting.Registry;
/// discovers them via reflection at first access.
///
/// <para>
/// The factory delegates (<see cref="FromXml"/>,
/// <see cref="FromDisplay"/>) live on the metadata so the registry can
/// bridge them into the legacy <c>StepXmlFactory</c> and
/// <c>StepDisplayFactory</c> surfaces without touching the POCO's
/// declaration site. Once the legacy surfaces are retired the delegates
/// become the sole construction path.
/// Construction is not described here: <see cref="StepRegistry"/> always
/// constructs a blank instance and calls its (possibly overridden)
/// <see cref="ScriptStep.PopulateFromXml"/> / <see cref="ScriptStep.PopulateFromDisplay"/>.
/// A step opts into hand-written parsing by overriding those methods, not
/// by describing it on this record.
/// </para>
/// </summary>
public sealed record StepMetadata
Expand Down Expand Up @@ -57,18 +54,4 @@ public sealed record StepMetadata

/// <summary>Behavioural intelligence — tooltip / lint source.</summary>
public StepNotes? Notes { get; init; }

/// <summary>
/// Delegate that constructs a POCO instance from a source
/// <c>&lt;Step&gt;</c> element. Usually assigned via method-group
/// reference to the POCO's static <c>FromXml</c> method.
/// </summary>
public Func<XElement, ScriptStep>? FromXml { get; init; }

/// <summary>
/// Delegate that constructs a POCO instance from parsed display-text
/// tokens. Usually assigned via method-group reference to the POCO's
/// static <c>FromDisplayParams</c> method.
/// </summary>
public Func<bool, string[], ScriptStep>? FromDisplay { get; init; }
}
27 changes: 20 additions & 7 deletions src/SharpFM.Model/Scripting/Registry/StepRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,26 @@ private static void Scan()
if (metadata.Id != 0)
_byId[metadata.Id] = metadata;

// Bridge into legacy factories so callers that still use
// StepXmlFactory / StepDisplayFactory pick up POCO-backed
// construction without each POCO needing a ModuleInitializer.
if (metadata.FromXml is { } fromXml)
StepXmlFactory.Register(metadata.Name, fromXml);
if (metadata.FromDisplay is { } fromDisplay)
StepDisplayFactory.Register(metadata.Name, fromDisplay);
// Bridge into StepXmlFactory / StepDisplayFactory so dispatch finds
// the typed POCO without each POCO needing a ModuleInitializer.
// Every step is registered the same way — customization happens
// through virtual dispatch on the constructed instance
// (ScriptStep.PopulateFromXml / PopulateFromDisplay), not a
// per-step delegate lookup.
StepXmlFactory.Register(metadata.Name, el =>
{
var instance = (ScriptStep)Activator.CreateInstance(type, nonPublic: true)!;
instance.Enabled = el.Attribute("enable")?.Value != "False";
instance.PopulateFromXml(el);
return instance;
});
StepDisplayFactory.Register(metadata.Name, (enabled, hrParams) =>
{
var instance = (ScriptStep)Activator.CreateInstance(type, nonPublic: true)!;
instance.Enabled = enabled;
instance.PopulateFromDisplay(hrParams);
return instance;
});
}

// Sort for deterministic All iteration.
Expand Down
12 changes: 12 additions & 0 deletions src/SharpFM.Model/Scripting/ScriptStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ protected ScriptStep(bool enabled)
public abstract XElement ToXml();
public abstract string ToDisplayLine();

/// <summary>
/// Populates this instance from a source <c>&lt;Step&gt;</c> element.
/// <see cref="Enabled"/> is already set by the caller before this runs.
/// </summary>
protected internal abstract void PopulateFromXml(XElement step);

/// <summary>
/// Populates this instance from parsed display-text tokens.
/// <see cref="Enabled"/> is already set by the caller before this runs.
/// </summary>
protected internal abstract void PopulateFromDisplay(string[] hrParams);

public virtual List<ScriptDiagnostic> Validate(int lineIndex) => new();

/// <summary>
Expand Down
42 changes: 42 additions & 0 deletions src/SharpFM.Model/Scripting/ScriptStepOfT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Xml.Linq;
using SharpFM.Model.Scripting.Registry;
using SharpFM.Model.Scripting.Serialization;

namespace SharpFM.Model.Scripting;

/// <summary>
/// Self-typed base for shape-driven step POCOs (the curiously recurring
/// pattern, as in <see cref="System.IParsable{TSelf}"/>). Supplies the
/// shape-engine serializers once — <c>TSelf.Metadata</c> resolves through
/// the <see cref="IStepFactory"/> static-abstract constraint. A step
/// overrides <see cref="ScriptStep.PopulateFromXml"/>,
/// <see cref="ScriptStep.PopulateFromDisplay"/>, <see cref="ToDisplayLine"/>,
/// or <see cref="ToXml"/> only where shape-driven behavior isn't sufficient;
/// unrecognized state falls through to these generic defaults.
/// </summary>
public abstract class ScriptStep<TSelf> : ScriptStep
where TSelf : ScriptStep<TSelf>, IStepFactory
{
protected ScriptStep(bool enabled) : base(enabled) { }

public override XElement ToXml() => StepXmlRenderer.Render(this, TSelf.Metadata);

public override string ToDisplayLine() => StepDisplayRenderer.Render(this, TSelf.Metadata);

protected internal override void PopulateFromXml(XElement step) =>
StepXmlParser.Populate(this, step, TSelf.Metadata);

protected internal override void PopulateFromDisplay(string[] hrParams) =>
StepDisplayParser.Populate(this, hrParams, TSelf.Metadata);

/// <summary>Typed counterpart of the <see cref="ScriptStep.FromXml"/> dispatch
/// for callers that already know the step kind.</summary>
public static TSelf Parse(XElement step)
{
var instance = (TSelf)Activator.CreateInstance(typeof(TSelf), nonPublic: true)!;
instance.Enabled = step.Attribute("enable")?.Value != "False";
instance.PopulateFromXml(step);
return instance;
}
}
12 changes: 10 additions & 2 deletions src/SharpFM.Model/Scripting/Serialization/StepDisplayParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ public static ScriptStep Parse(Type pocoType, bool enabled, string[] hrParams, S
throw new InvalidOperationException(
$"{pocoType.Name} needs a parameterless constructor for shape-driven display parsing.");
instance.Enabled = enabled;
Populate(instance, hrParams, meta);
return instance;
}

/// <summary>
/// Populates an already-constructed instance's shape-bound properties
/// from parsed display-text tokens. <see cref="ScriptStep.Enabled"/> is
/// not touched here — callers set it before invoking this.
/// </summary>
public static void Populate(ScriptStep instance, string[] hrParams, StepMetadata meta)
{
var slots = ShapeHrView.HrNodes(meta.Shape).Where(n => n is not HrOnly).ToList();
var used = new bool[slots.Count];

Expand Down Expand Up @@ -75,8 +85,6 @@ public static ScriptStep Parse(Type pocoType, bool enabled, string[] hrParams, S
bound = true;
}
}

return instance;
}

private static void Assign(object target, ShapeNode node, string value)
Expand Down
20 changes: 15 additions & 5 deletions src/SharpFM.Model/Scripting/Serialization/StepXmlParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,21 @@ public static ScriptStep Parse(Type pocoType, XElement step, StepMetadata meta)
$"{pocoType.Name} needs a parameterless constructor for shape-driven parsing.");

instance.Enabled = step.Attribute("enable")?.Value != "False";
Populate(instance, step, meta);
return instance;
}

/// <summary>
/// Populates an already-constructed instance's shape-bound properties
/// from a source <c>&lt;Step&gt;</c> element. <see cref="ScriptStep.Enabled"/>
/// is not touched here — callers set it before invoking this.
/// </summary>
public static void Populate(ScriptStep instance, XElement step, StepMetadata meta)
{
foreach (var node in meta.Shape)
Populate(instance, step, node);
PopulateNode(instance, step, node);

CapturePassthrough(instance, step, meta.Shape);
return instance;
}

/// <summary>
Expand All @@ -58,7 +68,7 @@ private static void CapturePassthrough(object target, XElement parent, IReadOnly
: extras);
}

private static void Populate(object target, XElement step, ShapeNode node)
private static void PopulateNode(object target, XElement step, ShapeNode node)
{
switch (node)
{
Expand Down Expand Up @@ -177,7 +187,7 @@ private static void Populate(object target, XElement step, ShapeNode node)
if (wrapper is not null)
{
foreach (var child in w.Children)
Populate(target, wrapper, child);
PopulateNode(target, wrapper, child);
CapturePassthrough(target, wrapper, w.Children);
}
return;
Expand All @@ -202,7 +212,7 @@ private static void Populate(object target, XElement step, ShapeNode node)
// its init-only positional properties.
var value = CreateBlank(match.WhenType);
foreach (var child in match.Children)
Populate(value, step, child);
PopulateNode(value, step, child);
Set(target, prop, value);
return;
}
Expand Down
22 changes: 10 additions & 12 deletions src/SharpFM.Model/Scripting/Steps/AVPlayerPlayStep.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using SharpFM.Model.Scripting.Registry;
using SharpFM.Model.Scripting.Serialization;
using SharpFM.Model.Scripting.Shapes;
using SharpFM.Model.Scripting.Values;

namespace SharpFM.Model.Scripting.Steps;

public sealed class AVPlayerPlayStep : ScriptStep, IStepFactory
public sealed class AVPlayerPlayStep : ScriptStep<AVPlayerPlayStep>, IStepFactory
{
public const int XmlId = 177;
public const string XmlName = "AVPlayer Play";
Expand Down Expand Up @@ -84,17 +82,12 @@ public AVPlayerPlayStep(
private static string PresentationHr(string x) => _PresentationToHr.TryGetValue(x, out var h) ? h : x;
private static string PresentationXml(string h) => _PresentationFromHr.TryGetValue(h, out var x) ? x : h;

public override XElement ToXml() => StepXmlRenderer.Render(this, Metadata);

// Hand-written: Hide Controls / Disable Interaction map the wire's
// "True"/absent values to On/Off, a translation display metadata cannot
// express without widening the nodes' wire ValidValues.
public override string ToDisplayLine() =>
"AVPlayer Play [ " + SourceHr(Source) + " ; " + "Repetition: " + (Repetition?.Text ?? "") + " ; " + "Presentation: " + PresentationHr(Presentation) + " ; " + "Position: " + (Position?.Text ?? "") + " ; " + "Start Offset: " + (StartOffset?.Text ?? "") + " ; " + "End Offset: " + (EndOffset?.Text ?? "") + " ; " + "Hide Controls: " + (HideControls == "True" ? "On" : "Off") + " ; " + "Disable Interaction: " + (DisableInteraction == "True" ? "On" : "Off") + " ]";

public static new ScriptStep FromXml(XElement step) =>
StepXmlParser.Parse<AVPlayerPlayStep>(step, Metadata);

/// <summary>
/// Display edits are anchor-preserved when a toggle is explicitly stored
/// as False: the display renders both the absent and the explicit-False
Expand All @@ -103,7 +96,7 @@ public override string ToDisplayLine() =>
public override bool IsFullyEditable =>
HideControls != "False" && DisableInteraction != "False";

public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
protected internal override void PopulateFromDisplay(string[] hrParams)
{
var tokens = hrParams.Select(h => h.Trim()).ToArray();
// The leading unlabeled token is the source enum.
Expand All @@ -126,7 +119,14 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
foreach (var tok in tokens) { if (tok.StartsWith("Hide Controls:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(14).Trim(); hideControls_v = v.Equals("On", StringComparison.OrdinalIgnoreCase) ? "True" : ""; break; } }
string disableInteraction_v = "";
foreach (var tok in tokens) { if (tok.StartsWith("Disable Interaction:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(20).Trim(); disableInteraction_v = v.Equals("On", StringComparison.OrdinalIgnoreCase) ? "True" : ""; break; } }
return new AVPlayerPlayStep(source_v, repetition_v, presentation_v, position_v, startOffset_v, endOffset_v, hideControls_v, disableInteraction_v, enabled);
Source = source_v;
Repetition = repetition_v;
Presentation = presentation_v;
Position = position_v;
StartOffset = startOffset_v;
EndOffset = endOffset_v;
HideControls = hideControls_v;
DisableInteraction = disableInteraction_v;
}

public static StepMetadata Metadata { get; } = new()
Expand All @@ -150,7 +150,5 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
new EnumValueChild("HideControls") { PocoProperty = "HideControls", HrLabel = "Hide Controls", Optional = true, DisplayValues = ["On", "Off"] },
new EnumValueChild("DisableInteraction") { PocoProperty = "DisableInteraction", HrLabel = "Disable Interaction", Optional = true, DisplayValues = ["On", "Off"] },
],
FromXml = FromXml,
FromDisplay = FromDisplayParams,
};
}
25 changes: 13 additions & 12 deletions src/SharpFM.Model/Scripting/Steps/AVPlayerSetOptionsStep.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using SharpFM.Model.Scripting.Registry;
using SharpFM.Model.Scripting.Serialization;
using SharpFM.Model.Scripting.Shapes;
using SharpFM.Model.Scripting.Values;

namespace SharpFM.Model.Scripting.Steps;

public sealed class AVPlayerSetOptionsStep : ScriptStep, IStepFactory
public sealed class AVPlayerSetOptionsStep : ScriptStep<AVPlayerSetOptionsStep>, IStepFactory
{
public const int XmlId = 179;
public const string XmlName = "AVPlayer Set Options";
Expand Down Expand Up @@ -114,16 +112,11 @@ public AVPlayerSetOptionsStep(
private static string SequenceHr(string x) => _SequenceToHr.TryGetValue(x, out var h) ? h : x;
private static string SequenceXml(string h) => _SequenceFromHr.TryGetValue(h, out var x) ? x : h;

public override XElement ToXml() => StepXmlRenderer.Render(this, Metadata);

// Hand-written: option toggles map the wire's "True"/absent values to
// On/Off, a translation display metadata cannot express.
public override string ToDisplayLine() =>
"AVPlayer Set Options [ " + "Presentation: " + PresentationHr(Presentation) + " ; " + "Disable Interaction: " + (DisableInteraction == "True" ? "On" : "Off") + " ; " + "Hide Controls: " + (HideControls == "True" ? "On" : "Off") + " ; " + "Disable External Controls: " + (DisableExternalControls == "True" ? "On" : "Off") + " ; " + "Pause in Background: " + (PauseInBackground == "True" ? "On" : "Off") + " ; " + "Position: " + (Position?.Text ?? "") + " ; " + "Start Offset: " + (StartOffset?.Text ?? "") + " ; " + "End Offset: " + (EndOffset?.Text ?? "") + " ; " + "Volume: " + (Volume?.Text ?? "") + " ; " + "Zoom: " + ZoomHr(Zoom) + " ; " + "Sequence: " + SequenceHr(Sequence) + " ]";

public static new ScriptStep FromXml(XElement step) =>
StepXmlParser.Parse<AVPlayerSetOptionsStep>(step, Metadata);

/// <summary>
/// Display edits are anchor-preserved when a toggle is explicitly stored
/// as False: the display renders both the absent and the explicit-False
Expand All @@ -133,7 +126,7 @@ public override string ToDisplayLine() =>
DisableInteraction != "False" && HideControls != "False"
&& DisableExternalControls != "False" && PauseInBackground != "False";

public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
protected internal override void PopulateFromDisplay(string[] hrParams)
{
var tokens = hrParams.Select(h => h.Trim()).ToArray();
string presentation_v = "Start Full Screen";
Expand All @@ -160,7 +153,17 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
foreach (var tok in tokens) { if (tok.StartsWith("Zoom:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(5).Trim(); zoom_v = ZoomXml(v); break; } }
string sequence_v = "None";
foreach (var tok in tokens) { if (tok.StartsWith("Sequence:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(9).Trim(); sequence_v = SequenceXml(v); break; } }
return new AVPlayerSetOptionsStep(presentation_v, disableInteraction_v, hideControls_v, disableExternalControls_v, pauseInBackground_v, position_v, startOffset_v, endOffset_v, volume_v, zoom_v, sequence_v, enabled);
Presentation = presentation_v;
DisableInteraction = disableInteraction_v;
HideControls = hideControls_v;
DisableExternalControls = disableExternalControls_v;
PauseInBackground = pauseInBackground_v;
Position = position_v;
StartOffset = startOffset_v;
EndOffset = endOffset_v;
Volume = volume_v;
Zoom = zoom_v;
Sequence = sequence_v;
}

public static StepMetadata Metadata { get; } = new()
Expand All @@ -186,7 +189,5 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
new EnumValueChild("Zoom") { PocoProperty = "Zoom", HrLabel = "Zoom", Optional = true, DisplayValues = ["Fit", "Fill", "Stretch", "Fit Only", "Fill Only", "Stretch Only"] },
new EnumValueChild("Sequence") { PocoProperty = "Sequence", HrLabel = "Sequence", Optional = true, DisplayValues = ["None", "Next", "Previous"] },
],
FromXml = FromXml,
FromDisplay = FromDisplayParams,
};
}
Loading
Loading