Construct the field, resolving any declared expression scope eagerly.
StringField options, plus scope: the id of the
expression scope this field's value is evaluated against.
Optionalscope?: stringId of the sohl.entity.expr.ExpressionScope declaring which
identifiers this field's expression may use (e.g. "skill.base") —
the scope the value will be evaluated against at its call site.
Drives the editor's autocomplete and its live out-of-scope check, and
reaches the sheet as data-expr-scope via the expressionField
partial. Resolved eagerly at construction, so an unknown or renamed id
throws while the schema is being built rather than yielding a field
that silently validates nothing.
Optional: omit it only when the field has no single fixed call site — an expression stored without a scope accepts any identifier.
Standard Foundry DataField context.
ReadonlyscopeId of the ExpressionScope this field's expression is evaluated
against, or undefined when the field declares none.
The resolved ExpressionScope this field declares, if any. The sheet hands it to the expression editor.
A foundry.data.fields.StringField whose value is a SafeExpression source string — a formula/predicate a GM authors (a Skill's
skillBaseFormula, an affliction's duration formula, and so on).It is a plain
StringFieldon the wire — the value is still just the source text. What it adds is a semantic marker: declaring a field as aSafeExpressionFieldis how a sheet knows the field holds an expression and warrants the SafeExpression code editor (an edit button that opensopenExpressionEditorDialog) rather than a bare text input.It deliberately does not reject an invalid expression at the schema boundary. By design a malformed formula is stored and surfaced as a warning by the consuming logic (e.g.
SkillLogic.computeSkillBaserenders a ✕ and an "Invalid expression" hint), so authors can save work-in-progress and fix it later — hard schema rejection would discard it and break that flow. The authoritative validity check (SafeExpression.validateSource) runs live in the editor, where Save stays disabled until the expression is valid.Using it
Declare it in
defineSchema()like any other field, naming the sohl.entity.expr.ExpressionScope its value will be evaluated against:Render it with the shared
expressionFieldHandlebars partial, which draws theformGroupplus the editor button and forwards the declared scope asdata-expr-scope— the sheet needs no per-field wiring:Then evaluate the stored source against the same scope, so what the editor accepted is exactly what the runtime allows:
Options
Every foundry.data.fields.StringField option is accepted (see SafeExpressionField.Options);
scopeis the only one this class adds.scopenullabletrueStringField'sfalse: "unset" isnull, per the null-at-the-edges convention.blankfalseStringField'strue, so a cleared editor or form input cleans tonullrather than leaving a second "empty" spelling ("").initialnulltrimtrueStringField— restated because surrounding whitespace is never meaningful in an expression.Only
nullable,blank, andinitialactually differ fromStringField; everything else (required,label,hint,choices,validate, …) behaves exactly as it does there. Override any of them per field — but think twice before revertingnullable/blank, since that pair is what keeps "no formula" a single value instead of two. Both shipped fields (SkillDataModel.skillBaseFormula,AfflictionDataModel.outcomeTrauma) pass nothing butscope.The scope is what lets the schema tell the sheet which identifiers a formula may use, so the editor's autocomplete and live validation match the call site that actually evaluates it. Before scopes, the sheet template carried a hand-typed
data-context="attr"string with nothing tying it to the evaluating code (issue #1142).