|
- namespace EnvelopeRenderer.Cli.Render;
-
- /// <summary>
- /// One `<text>` element from the template. Sprint 5, "Mix static text and CSV fields within
- /// a single text element": content is now an ordered sequence of <see cref="TemplateTextRun"/>s
- /// (literal-text runs and field-token runs), concatenated per record at render time — the
- /// pre-Sprint-5 "either pure static text or a single bound column" rule is simply the one-run
- /// special case of this same model (<see cref="StaticText"/>/<see cref="ColumnName"/> below are
- /// back-compat read-only projections for that one-run case, not separate storage), not a
- /// parallel code path that could drift from it. <see cref="TemplateXmlParser"/> enforces "every
- /// run is exactly one of literal/field" at parse time.
- /// </summary>
- /// <param name="Collapsible">Sprint 4, "Collapse blank optional address lines consistently":
- /// when <c>true</c> and this element's resolved text is blank for a given record, the element
- /// is not drawn and every other element sharing its X position (an "address line stack" — see
- /// <see cref="AddressLineCollapser"/>) shifts upward to close the gap. Defaults to <c>false</c>
- /// so every template written before this story renders identically (a blank line simply prints
- /// nothing at its fixed position, exactly as before).</param>
- /// <param name="Angle">Sprint 4, "Set a rotation angle for text and dynamic field elements":
- /// degrees, positive or negative, applied around the element's own bounding-box center (not its
- /// X/Y anchor). Defaults to <c>0</c> so every template written before this story renders
- /// unchanged.</param>
- /// <param name="Width">Sprint 9, "Add an adjustable width and height with text wrapping...":
- /// when set (together with <paramref name="Height"/>), the element's content wraps within this
- /// width instead of drawing as one unbounded line. <c>null</c> (the default) means every
- /// template written before this story renders exactly as before — no wrap, no width concept at
- /// all.</param>
- /// <param name="Height">Paired with <paramref name="Width"/>: wrapped content that would exceed
- /// this height is clipped rather than growing the box or shrinking the text. Only meaningful when
- /// <paramref name="Width"/> is also set; <c>null</c> is the pre-Sprint-9 default.</param>
- public sealed record TemplateElement(
- double X,
- double Y,
- string FontName,
- double Size,
- IReadOnlyList<TemplateTextRun> Runs,
- bool Collapsible = false,
- double Angle = 0,
- int RenderOrder = 0,
- double? Width = null,
- double? Height = null)
- {
- /// <summary>True only when both <see cref="Width"/> and <see cref="Height"/> are set — a
- /// one-sided value (e.g. a width with no height) is treated as "no box" rather than guessing
- /// a missing dimension, since wrap-without-a-clip-boundary or clip-without-a-wrap-width both
- /// have no well-defined meaning for this story.</summary>
- public bool HasBox => Width is > 0 && Height is > 0;
-
- /// <summary>Back-compat convenience factory matching the pre-Sprint-5 "pure static text"
- /// shape — builds the equivalent one-literal-run <see cref="Runs"/> list. (A second
- /// constructor overload was deliberately rejected here in favor of named factory methods: a
- /// positional/named-argument overload alongside the primary constructor's own
- /// <c>Collapsible</c>/<c>Angle</c> parameters is ambiguous to the compiler — CS1744 — for any
- /// caller using named arguments for either, which every <c>Collapsible: true</c>/
- /// <c>Angle: 45</c> call site in this codebase does.)</summary>
- public static TemplateElement Static(
- double x, double y, string fontName, double size, string text, bool collapsible = false, double angle = 0) =>
- new(x, y, fontName, size, new[] { new TemplateTextRun(text, null) }, collapsible, angle);
-
- /// <summary>Back-compat convenience factory matching the pre-Sprint-5 "single bound column"
- /// shape — builds the equivalent one-field-run <see cref="Runs"/> list.</summary>
- public static TemplateElement Dynamic(
- double x, double y, string fontName, double size, string columnName, bool collapsible = false, double angle = 0) =>
- new(x, y, fontName, size, new[] { new TemplateTextRun(null, columnName) }, collapsible, angle);
-
- public bool IsDynamic => Runs.Any(r => r.IsField);
-
- /// <summary>True only for the legacy "exactly one field run, nothing else" shape — the only
- /// case that still has a single, unambiguous bound column (used by the render-time pre-flight
- /// column check's error-message grouping and by any code that only makes sense for a
- /// single-column binding).</summary>
- public bool HasSingleColumnRun => Runs.Count == 1 && Runs[0].IsField;
-
- /// <summary>Back-compat projection: non-null only for the legacy single-literal-run case.</summary>
- public string? StaticText => Runs.Count == 1 && !Runs[0].IsField ? Runs[0].Literal : null;
-
- /// <summary>Back-compat projection: non-null only for the legacy single-field-run case.</summary>
- public string? ColumnName => HasSingleColumnRun ? Runs[0].ColumnName : null;
- }
|