Skip to main content

appcore_filemaker/
ir.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: ir.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Defines bounded ir contracts and behavior for this crate.
12
13use std::collections::{BTreeMap, BTreeSet};
14
15use serde::{Deserialize, Serialize};
16
17use crate::source::ModelKind;
18use crate::{
19    CollisionPolicy, DataSchema, ErrorCode, FileMakerError, ImageOptions, Length, Orientation,
20    PageTemplate, Result, Size, Style,
21};
22
23/// Validated stable element identifier.
24#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash, Serialize)]
25#[serde(transparent)]
26pub struct ElementId(String);
27
28impl ElementId {
29    /// Validates and creates an identifier.
30    pub fn new(value: impl Into<String>) -> Result<Self> {
31        let value = value.into();
32        if value.is_empty()
33            || value.len() > 128
34            || !value.bytes().all(|byte| {
35                byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_' | b'/')
36            })
37        {
38            return Err(FileMakerError::new(
39                ErrorCode::SchemaField,
40                "element ID must be 1..128 safe ASCII characters",
41            ));
42        }
43        Ok(Self(value))
44    }
45
46    /// Returns the validated ID.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51}
52
53impl<'de> Deserialize<'de> for ElementId {
54    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
55    where
56        D: serde::Deserializer<'de>,
57    {
58        let value = String::deserialize(deserializer)?;
59        Self::new(value).map_err(serde::de::Error::custom)
60    }
61}
62
63/// Format-neutral element kind.
64#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum ElementKind {
67    /// Text.
68    Text,
69    /// Image.
70    Image,
71    /// Line.
72    Line,
73    /// Rectangle.
74    Rect,
75    /// Circle.
76    Circle,
77    /// Ellipse.
78    Ellipse,
79    /// Polygon.
80    Polygon,
81    /// Vector path.
82    Path,
83    /// Group.
84    Group,
85    /// First-class table.
86    Table,
87    /// Reserved chart node.
88    Chart,
89    /// UTF-8 QR Model 2 node, lowered to square-module vector paths by layout.
90    Qr,
91    /// EAN-8/EAN-13 node; exact digit count selects the checked encoding.
92    Barcode,
93}
94
95impl ElementKind {
96    /// Parses the exact schema spelling.
97    pub fn parse(value: &str) -> Result<Self> {
98        match value {
99            "text" => Ok(Self::Text),
100            "image" => Ok(Self::Image),
101            "line" => Ok(Self::Line),
102            "rect" => Ok(Self::Rect),
103            "circle" => Ok(Self::Circle),
104            "ellipse" => Ok(Self::Ellipse),
105            "polygon" => Ok(Self::Polygon),
106            "path" => Ok(Self::Path),
107            "group" => Ok(Self::Group),
108            "table" => Ok(Self::Table),
109            "chart" => Ok(Self::Chart),
110            "qr" => Ok(Self::Qr),
111            "barcode" => Ok(Self::Barcode),
112            _ => Err(FileMakerError::new(
113                ErrorCode::SchemaField,
114                format!("unsupported element type `{value}`"),
115            )),
116        }
117    }
118}
119
120/// Source-independent geometry intent retained before measurement.
121#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
122pub struct GeometryIr {
123    /// Optional horizontal position.
124    pub x: Option<Length>,
125    /// Optional vertical position.
126    pub y: Option<Length>,
127    /// Optional/automatic width.
128    pub width: Option<Length>,
129    /// Optional/automatic height.
130    pub height: Option<Length>,
131    /// Minimum, preferred, maximum, and aspect-ratio size intent.
132    pub constraints: crate::LayoutConstraints,
133    /// Optional horizontal alignment inside the resolved container.
134    pub align_x: Option<crate::Alignment>,
135    /// Optional vertical alignment inside the resolved container.
136    pub align_y: Option<crate::Alignment>,
137    /// Named containing region.
138    pub region: Option<String>,
139    /// Named anchor expressions.
140    pub anchors: BTreeMap<String, String>,
141}
142
143/// Source-independent transform intent retained until page geometry is known.
144#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
145pub struct TransformIr {
146    /// Horizontal translation.
147    pub translate_x: Length,
148    /// Vertical translation.
149    pub translate_y: Length,
150    /// Clockwise integer-degree rotation.
151    pub rotate: i32,
152    /// Horizontal fixed-point scale.
153    pub scale_x: i64,
154    /// Vertical fixed-point scale.
155    pub scale_y: i64,
156    /// Horizontal transform origin.
157    pub origin_x: Length,
158    /// Vertical transform origin.
159    pub origin_y: Length,
160}
161
162/// Format-neutral text layout intent retained until font measurement.
163#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
164pub struct TextIr {
165    /// Overflow behavior.
166    pub overflow: crate::TextOverflow,
167    /// Optional maximum line count.
168    pub max_lines: Option<usize>,
169    /// Optional explicit minimum font size.
170    pub min_font_size: Option<Length>,
171    /// Line-height multiplier in millionths.
172    pub line_height: u32,
173    /// Horizontal lines or top-to-bottom right-to-left vertical columns.
174    pub writing_mode: crate::WritingMode,
175    /// Inline alignment applied after shaping and wrapping.
176    pub align_x: crate::Alignment,
177    /// Symmetric inline padding applied to every line.
178    #[serde(default = "default_inline_padding")]
179    pub padding_inline: crate::Length,
180    /// Per-side block padding resolved by layout and shared by exporters.
181    #[serde(default)]
182    pub padding: crate::TextBlockPadding,
183}
184
185fn default_inline_padding() -> crate::Length {
186    crate::Length::Absolute(crate::Unit::ZERO)
187}
188
189/// Bound table intent retained until row/column measurement and pagination.
190#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
191pub struct TableIr {
192    /// Validated planning contract.
193    pub spec: crate::TableSpec,
194    /// Header height in source-relative units.
195    pub header_height: Length,
196    /// Fixed row height or auto measurement.
197    pub row_height: Option<Length>,
198    /// Optional first/continuation page body rectangles within the element bounds.
199    #[serde(default)]
200    pub page_bodies: Option<TablePageBodies>,
201    /// Bound bounded rows; empty before data binding.
202    pub rows: Vec<crate::DataRow>,
203}
204
205/// Vertical body region used by one physical table-page role.
206#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct TablePageBody {
209    /// Vertical offset from the table element's top edge.
210    pub offset_y: Length,
211    /// Usable table-body height beginning at `offset_y`.
212    pub height: Length,
213}
214
215/// Distinct first-page and continuation-page body regions for a table.
216#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
217#[serde(deny_unknown_fields)]
218pub struct TablePageBodies {
219    /// Region used for the first table fragment.
220    pub first: TablePageBody,
221    /// Region used for every continuation fragment.
222    pub continuation: TablePageBody,
223}
224
225/// Source-relative vector path command retained until layout resolves its units.
226#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
227#[serde(tag = "command", rename_all = "snake_case")]
228pub enum PathCommandIr {
229    /// Starts a contour.
230    Move { x: Length, y: Length },
231    /// Adds a straight segment.
232    Line { x: Length, y: Length },
233    /// Adds a cubic Bézier segment.
234    Curve {
235        x1: Length,
236        y1: Length,
237        x2: Length,
238        y2: Length,
239        x: Length,
240        y: Length,
241    },
242    /// Closes the current contour.
243    Close,
244}
245
246/// Named region retained in source-relative units until page layout.
247#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
248pub struct RegionIr {
249    /// Horizontal position.
250    pub x: Length,
251    /// Vertical position.
252    pub y: Length,
253    /// Width.
254    pub width: Length,
255    /// Height.
256    pub height: Length,
257    /// Region collision policy inherited after document and page policies.
258    pub collision: Option<crate::CollisionPolicy>,
259}
260
261/// Page-relative non-painted geometry retained until layout.
262#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
263pub struct ExclusionIr {
264    /// Horizontal coordinate.
265    pub x: Length,
266    /// Vertical coordinate.
267    pub y: Length,
268    /// Width.
269    pub width: Length,
270    /// Height.
271    pub height: Length,
272    /// Collision group exposed by the exclusion.
273    pub group: String,
274    /// Candidate groups blocked by this exclusion; empty means every group.
275    pub collides_with: BTreeSet<String>,
276}
277
278/// Layout strategy for a node and its children.
279#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
280#[serde(rename_all = "snake_case")]
281pub enum LayoutMode {
282    /// Coordinates are resolved independently.
283    #[default]
284    Absolute,
285    /// Children flow from top to bottom.
286    FlowVertical,
287    /// Children flow from left to right.
288    FlowHorizontal,
289}
290
291/// Provenance retained through binding and layout.
292#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
293pub struct Provenance {
294    /// Logical source path.
295    pub source: String,
296    /// Component expansion chain.
297    pub components: Vec<String>,
298    /// Applied style names in cascade order.
299    pub styles: Vec<String>,
300    /// Runtime patch sequence numbers.
301    pub patches: Vec<u64>,
302}
303
304/// Author-supplied edit policy carried for optional external tool bridges.
305#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
306pub struct AiPolicy {
307    /// Compact document purpose; the deterministic core does not interpret it.
308    pub purpose: String,
309    /// Bounded textual rules; the deterministic core does not interpret them.
310    pub rules: Vec<String>,
311    /// IDs an external bridge may edit; empty delegates to bridge defaults.
312    pub editable: BTreeSet<String>,
313    /// IDs an external bridge must never edit.
314    pub locked: BTreeSet<String>,
315}
316
317/// Expanded format-neutral element.
318#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
319pub struct ElementIr {
320    /// Stable ID.
321    pub id: ElementId,
322    /// Element kind.
323    pub kind: ElementKind,
324    /// Geometry intent.
325    pub geometry: GeometryIr,
326    /// Transform intent resolved only after layout proposes a box.
327    pub transform: TransformIr,
328    /// Literal or bound text.
329    pub text: Option<String>,
330    /// Ordered same-style inline text segments with explicit trailing gaps.
331    #[serde(default)]
332    pub text_segments: Vec<TextSegmentIr>,
333    /// Text measurement and overflow intent.
334    pub text_options: TextIr,
335    /// First-class table planning intent and bound rows.
336    pub table: Option<TableIr>,
337    /// Explicit asset reference.
338    pub asset: Option<String>,
339    /// Vector commands in element-local source coordinates.
340    pub path: Vec<PathCommandIr>,
341    /// Image crop and fit intent.
342    pub image: ImageOptions,
343    /// Typed inline/cascaded style.
344    pub style: Style,
345    /// Ordered conditional style layers evaluated during data binding.
346    #[serde(default)]
347    pub style_rules: Vec<crate::ElementStyleRule>,
348    /// Layout strategy.
349    pub layout: LayoutMode,
350    /// Distribution of children on a flow's primary axis.
351    pub distribute: crate::Distribution,
352    /// Gap between flow children.
353    pub gap: Length,
354    /// Optional policy overriding the inherited collision policy.
355    pub collision: Option<CollisionPolicy>,
356    /// Child elements in deterministic source order.
357    pub children: Vec<ElementIr>,
358    /// Immutable after compile unless a privileged caller builds a new IR.
359    pub locked: bool,
360    /// Visibility after binding rules.
361    pub hidden: bool,
362    /// Keep this element with following siblings in a vertical flow when the
363    /// complete contiguous block fits on one page.
364    #[serde(default)]
365    pub keep_with_next: bool,
366    /// Visual layer independent of collision.
367    pub layer: String,
368    /// Visual order within layer.
369    pub z_index: i32,
370    /// Data binding expression.
371    pub binding: Option<String>,
372    /// Conditional expression.
373    pub when: Option<String>,
374    /// Repeat expression.
375    pub repeat: Option<String>,
376    /// Provenance.
377    pub provenance: Provenance,
378    /// Root-only master/role page placement; children inherit their root.
379    pub page_placement: Option<crate::PagePlacement>,
380}
381
382/// One resolved inline text segment.
383#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
384pub struct TextSegmentIr {
385    /// Literal or resolved segment content.
386    pub text: Option<String>,
387    /// Binding expression evaluated against the typed data object.
388    pub binding: Option<String>,
389    /// Gap following this segment.
390    pub gap_after: Length,
391}
392
393impl ElementIr {
394    pub(crate) fn clone_with_text(&self, text: String) -> Self {
395        Self {
396            id: self.id.clone(),
397            kind: self.kind,
398            geometry: self.geometry.clone(),
399            transform: self.transform,
400            text: Some(text),
401            text_segments: Vec::new(),
402            text_options: self.text_options,
403            table: self.table.clone(),
404            asset: self.asset.clone(),
405            path: self.path.clone(),
406            image: self.image,
407            style: self.style.clone(),
408            style_rules: self.style_rules.clone(),
409            layout: self.layout,
410            distribute: self.distribute,
411            gap: self.gap,
412            collision: self.collision.clone(),
413            children: self.children.clone(),
414            locked: self.locked,
415            hidden: self.hidden,
416            keep_with_next: self.keep_with_next,
417            layer: self.layer.clone(),
418            z_index: self.z_index,
419            binding: self.binding.clone(),
420            when: self.when.clone(),
421            repeat: self.repeat.clone(),
422            provenance: self.provenance.clone(),
423            page_placement: self.page_placement,
424        }
425    }
426}
427
428/// Expanded reusable template IR.
429#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
430pub struct TemplateIr {
431    /// Template identity.
432    pub id: String,
433    /// Model.
434    pub model: ModelKind,
435    /// Optional page size.
436    pub page_size: Option<Size>,
437    /// Source orientation.
438    pub orientation: Orientation,
439    /// Resolved trim, margin, bleed, safe-area, and crop metadata.
440    pub page_template: Option<PageTemplate>,
441    /// Document-wide collision policy.
442    pub collision: Option<crate::CollisionPolicy>,
443    /// Page collision policy inherited after the document policy.
444    pub page_collision: Option<crate::CollisionPolicy>,
445    /// Named guides.
446    pub guides: BTreeMap<String, Length>,
447    /// Named regions.
448    pub regions: BTreeMap<String, RegionIr>,
449    /// Named page-relative exclusions.
450    pub exclusions: BTreeMap<String, ExclusionIr>,
451    /// Optional typed data contract.
452    pub data_schema: DataSchema,
453    /// Optional external-tool edit policy, retained but never executed by core.
454    pub ai_policy: AiPolicy,
455    /// Expanded root nodes.
456    pub elements: Vec<ElementIr>,
457}
458
459/// Bound instance ready for measurement and layout.
460#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
461pub struct DocumentIr {
462    /// Template identity.
463    pub template_id: String,
464    /// Model.
465    pub model: ModelKind,
466    /// Optional page size.
467    pub page_size: Option<Size>,
468    /// Resolved page metadata.
469    pub page_template: Option<PageTemplate>,
470    /// Bound document-wide collision policy.
471    pub collision: Option<crate::CollisionPolicy>,
472    /// Bound page collision policy inherited after the document policy.
473    pub page_collision: Option<crate::CollisionPolicy>,
474    /// Named guides.
475    pub guides: BTreeMap<String, Length>,
476    /// Named regions.
477    pub regions: BTreeMap<String, RegionIr>,
478    /// Named page-relative exclusions.
479    pub exclusions: BTreeMap<String, ExclusionIr>,
480    /// Optional external-tool edit policy, retained but never executed by core.
481    pub ai_policy: AiPolicy,
482    /// Bound root nodes.
483    pub elements: Vec<ElementIr>,
484}