Skip to main content

cdx_core/content/
block.rs

1//! Content block types.
2
3use serde::{Deserialize, Serialize};
4
5use super::Text;
6use crate::extensions::ExtensionBlock;
7
8/// Root content structure for a Codex document.
9///
10/// The content file contains a version identifier and an array of blocks
11/// that make up the document content.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct Content {
14    /// Content model version (e.g., "0.1").
15    pub version: String,
16
17    /// Array of content blocks.
18    pub blocks: Vec<Block>,
19}
20
21impl Content {
22    /// Create new content with the default version.
23    #[must_use]
24    pub fn new(blocks: Vec<Block>) -> Self {
25        Self {
26            version: crate::SPEC_VERSION.to_string(),
27            blocks,
28        }
29    }
30
31    /// Create empty content.
32    #[must_use]
33    pub fn empty() -> Self {
34        Self::new(Vec::new())
35    }
36
37    /// Check if the content has any blocks.
38    #[must_use]
39    pub fn is_empty(&self) -> bool {
40        self.blocks.is_empty()
41    }
42
43    /// Get the number of blocks.
44    #[must_use]
45    pub fn len(&self) -> usize {
46        self.blocks.len()
47    }
48}
49
50impl Default for Content {
51    fn default() -> Self {
52        Self::empty()
53    }
54}
55
56/// Common attributes that can appear on any block.
57#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct BlockAttributes {
60    /// Text direction.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub dir: Option<String>,
63
64    /// BCP 47 language tag.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub lang: Option<String>,
67
68    /// Writing mode for text direction.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub writing_mode: Option<WritingMode>,
71}
72
73impl BlockAttributes {
74    /// Check if attributes are empty (all None).
75    #[must_use]
76    pub fn is_empty(&self) -> bool {
77        self.dir.is_none() && self.lang.is_none() && self.writing_mode.is_none()
78    }
79}
80
81/// A content block in the document tree.
82///
83/// Blocks are the structural elements of a document, containing
84/// either other blocks (containers) or text content (leaves).
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86#[serde(tag = "type", rename_all = "camelCase")]
87pub enum Block {
88    /// Standard paragraph block.
89    Paragraph {
90        /// Optional unique identifier.
91        #[serde(default, skip_serializing_if = "Option::is_none")]
92        id: Option<String>,
93
94        /// Text content.
95        children: Vec<Text>,
96
97        /// Block attributes.
98        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
99        attributes: BlockAttributes,
100    },
101
102    /// Section heading (levels 1-6).
103    Heading {
104        /// Optional unique identifier.
105        #[serde(default, skip_serializing_if = "Option::is_none")]
106        id: Option<String>,
107
108        /// Heading level (1-6).
109        level: u8,
110
111        /// Text content.
112        children: Vec<Text>,
113
114        /// Block attributes.
115        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
116        attributes: BlockAttributes,
117    },
118
119    /// Ordered or unordered list.
120    List {
121        /// Optional unique identifier.
122        #[serde(default, skip_serializing_if = "Option::is_none")]
123        id: Option<String>,
124
125        /// Whether the list is ordered (numbered).
126        ordered: bool,
127
128        /// Starting number for ordered lists.
129        #[serde(default, skip_serializing_if = "Option::is_none")]
130        start: Option<u32>,
131
132        /// List items (must be `ListItem` blocks).
133        children: Vec<Block>,
134
135        /// Block attributes.
136        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
137        attributes: BlockAttributes,
138    },
139
140    /// Item within a list.
141    ListItem {
142        /// Optional unique identifier.
143        #[serde(default, skip_serializing_if = "Option::is_none")]
144        id: Option<String>,
145
146        /// Checkbox state (None = not a checkbox).
147        #[serde(default, skip_serializing_if = "Option::is_none")]
148        checked: Option<bool>,
149
150        /// Block content.
151        children: Vec<Block>,
152
153        /// Block attributes.
154        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
155        attributes: BlockAttributes,
156    },
157
158    /// Quoted content block.
159    Blockquote {
160        /// Optional unique identifier.
161        #[serde(default, skip_serializing_if = "Option::is_none")]
162        id: Option<String>,
163
164        /// Block content.
165        children: Vec<Block>,
166
167        /// Block attributes.
168        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
169        attributes: BlockAttributes,
170    },
171
172    /// Source code or preformatted text.
173    CodeBlock {
174        /// Optional unique identifier.
175        #[serde(default, skip_serializing_if = "Option::is_none")]
176        id: Option<String>,
177
178        /// Programming language identifier.
179        #[serde(default, skip_serializing_if = "Option::is_none")]
180        language: Option<String>,
181
182        /// Syntax highlighting theme.
183        #[serde(default, skip_serializing_if = "Option::is_none")]
184        highlighting: Option<String>,
185
186        /// Pre-tokenized syntax highlighting.
187        #[serde(default, skip_serializing_if = "Option::is_none")]
188        tokens: Option<Vec<CodeToken>>,
189
190        /// Code content (single text node, no marks).
191        children: Vec<Text>,
192
193        /// Block attributes.
194        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
195        attributes: BlockAttributes,
196    },
197
198    /// Thematic break between sections.
199    HorizontalRule {
200        /// Optional unique identifier.
201        #[serde(default, skip_serializing_if = "Option::is_none")]
202        id: Option<String>,
203    },
204
205    /// Embedded or referenced image.
206    Image(ImageBlock),
207
208    /// Tabular data.
209    Table {
210        /// Optional unique identifier.
211        #[serde(default, skip_serializing_if = "Option::is_none")]
212        id: Option<String>,
213
214        /// Table rows.
215        children: Vec<Block>,
216
217        /// Block attributes.
218        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
219        attributes: BlockAttributes,
220    },
221
222    /// Row within a table.
223    TableRow {
224        /// Optional unique identifier.
225        #[serde(default, skip_serializing_if = "Option::is_none")]
226        id: Option<String>,
227
228        /// Whether this is a header row.
229        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
230        header: bool,
231
232        /// Table cells.
233        children: Vec<Block>,
234
235        /// Block attributes.
236        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
237        attributes: BlockAttributes,
238    },
239
240    /// Cell within a table row.
241    TableCell(TableCellBlock),
242
243    /// Mathematical content.
244    Math(MathBlock),
245
246    /// Line break within a block.
247    Break {
248        /// Optional unique identifier.
249        #[serde(default, skip_serializing_if = "Option::is_none")]
250        id: Option<String>,
251    },
252
253    /// Definition list.
254    DefinitionList(DefinitionListBlock),
255
256    /// Definition item (term + description pair).
257    DefinitionItem {
258        /// Optional unique identifier.
259        #[serde(default, skip_serializing_if = "Option::is_none")]
260        id: Option<String>,
261
262        /// Children (typically `DefinitionTerm` and `DefinitionDescription`).
263        children: Vec<Block>,
264
265        /// Block attributes.
266        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
267        attributes: BlockAttributes,
268    },
269
270    /// Definition term.
271    DefinitionTerm {
272        /// Optional unique identifier.
273        #[serde(default, skip_serializing_if = "Option::is_none")]
274        id: Option<String>,
275
276        /// Term text content.
277        children: Vec<Text>,
278
279        /// Block attributes.
280        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
281        attributes: BlockAttributes,
282    },
283
284    /// Definition description.
285    DefinitionDescription {
286        /// Optional unique identifier.
287        #[serde(default, skip_serializing_if = "Option::is_none")]
288        id: Option<String>,
289
290        /// Description content (blocks).
291        children: Vec<Block>,
292
293        /// Block attributes.
294        #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
295        attributes: BlockAttributes,
296    },
297
298    /// Scientific/technical measurement.
299    Measurement(MeasurementBlock),
300
301    /// Block-level signature.
302    Signature(SignatureBlock),
303
304    /// SVG image.
305    Svg(SvgBlock),
306
307    /// Barcode (QR, Data Matrix, etc.).
308    Barcode(BarcodeBlock),
309
310    /// Figure container.
311    Figure(FigureBlock),
312
313    /// Figure caption.
314    FigCaption(FigCaptionBlock),
315
316    /// Admonition block (note, warning, tip, etc.).
317    Admonition(AdmonitionBlock),
318
319    /// Extension block for custom/unknown block types.
320    ///
321    /// Extension blocks use namespaced types like "forms:textInput" or
322    /// "semantic:citation". When parsing, unknown types are preserved
323    /// as extension blocks with their raw attributes intact.
324    Extension(ExtensionBlock),
325}
326
327/// Image block content.
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub struct ImageBlock {
330    /// Optional unique identifier.
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub id: Option<String>,
333
334    /// Image source (path or URL).
335    pub src: String,
336
337    /// Alternative text for accessibility.
338    pub alt: String,
339
340    /// Image title/caption.
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub title: Option<String>,
343
344    /// Intrinsic width in pixels.
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub width: Option<u32>,
347
348    /// Intrinsic height in pixels.
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub height: Option<u32>,
351}
352
353/// Table cell block content.
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
355pub struct TableCellBlock {
356    /// Optional unique identifier.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub id: Option<String>,
359
360    /// Number of columns to span.
361    #[serde(default = "default_span", skip_serializing_if = "is_default_span")]
362    pub colspan: u32,
363
364    /// Number of rows to span.
365    #[serde(default = "default_span", skip_serializing_if = "is_default_span")]
366    pub rowspan: u32,
367
368    /// Text alignment.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub align: Option<CellAlign>,
371
372    /// Cell content.
373    pub children: Vec<Text>,
374
375    /// Block attributes.
376    #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
377    pub attributes: BlockAttributes,
378}
379
380fn default_span() -> u32 {
381    1
382}
383
384#[allow(clippy::trivially_copy_pass_by_ref)] // Required by serde skip_serializing_if
385fn is_default_span(span: &u32) -> bool {
386    *span == 1
387}
388
389/// Cell text alignment.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "lowercase")]
392pub enum CellAlign {
393    /// Left alignment.
394    Left,
395    /// Center alignment.
396    Center,
397    /// Right alignment.
398    Right,
399}
400
401/// Mathematical content block.
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403pub struct MathBlock {
404    /// Optional unique identifier.
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub id: Option<String>,
407
408    /// Display mode (true) vs inline (false).
409    pub display: bool,
410
411    /// Math format.
412    pub format: MathFormat,
413
414    /// Math content in the specified format.
415    pub value: String,
416}
417
418/// Mathematical content format.
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
420#[serde(rename_all = "lowercase")]
421pub enum MathFormat {
422    /// LaTeX format.
423    Latex,
424    /// `MathML` format.
425    Mathml,
426}
427
428/// Writing mode for text direction.
429///
430/// Controls the direction in which text flows within a block.
431/// This is particularly important for CJK (Chinese, Japanese, Korean)
432/// languages which can be written vertically.
433#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(rename_all = "kebab-case")]
435pub enum WritingMode {
436    /// Horizontal text, top-to-bottom block flow (default).
437    /// Used for Latin, Cyrillic, Arabic, Hebrew scripts.
438    #[default]
439    HorizontalTb,
440
441    /// Vertical text, right-to-left block flow.
442    /// Traditional Chinese, Japanese, Korean.
443    VerticalRl,
444
445    /// Vertical text, left-to-right block flow.
446    /// Used for Mongolian script.
447    VerticalLr,
448
449    /// Sideways text, right-to-left (90° clockwise rotation).
450    SidewaysRl,
451
452    /// Sideways text, left-to-right (90° counter-clockwise rotation).
453    SidewaysLr,
454}
455
456/// Measurement block for scientific/technical values.
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[serde(rename_all = "camelCase")]
459pub struct MeasurementBlock {
460    /// Optional unique identifier.
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub id: Option<String>,
463
464    /// The numeric value.
465    pub value: f64,
466
467    /// Uncertainty value (optional).
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub uncertainty: Option<f64>,
470
471    /// How to display uncertainty.
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub uncertainty_notation: Option<UncertaintyNotation>,
474
475    /// Scientific notation exponent (optional).
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub exponent: Option<i32>,
478
479    /// Display format (required).
480    pub display: String,
481
482    /// Unit of measurement (optional).
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub unit: Option<String>,
485}
486
487impl MeasurementBlock {
488    /// Create a new measurement block.
489    #[must_use]
490    pub fn new(value: f64, display: impl Into<String>) -> Self {
491        Self {
492            id: None,
493            value,
494            uncertainty: None,
495            uncertainty_notation: None,
496            exponent: None,
497            display: display.into(),
498            unit: None,
499        }
500    }
501
502    /// Set the unit.
503    #[must_use]
504    pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
505        self.unit = Some(unit.into());
506        self
507    }
508
509    /// Set the uncertainty.
510    #[must_use]
511    pub fn with_uncertainty(mut self, uncertainty: f64, notation: UncertaintyNotation) -> Self {
512        self.uncertainty = Some(uncertainty);
513        self.uncertainty_notation = Some(notation);
514        self
515    }
516
517    /// Set the exponent.
518    #[must_use]
519    pub fn with_exponent(mut self, exponent: i32) -> Self {
520        self.exponent = Some(exponent);
521        self
522    }
523}
524
525/// Uncertainty notation format.
526#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
527#[serde(rename_all = "lowercase")]
528pub enum UncertaintyNotation {
529    /// Parenthetical notation, e.g., 1.234(5).
530    Parenthetical,
531    /// Plus-minus notation, e.g., 1.234 ± 0.005.
532    Plusminus,
533    /// Range notation, e.g., 1.229–1.239.
534    Range,
535    /// Percentage notation, e.g., 1.234 ± 0.4%.
536    Percent,
537}
538
539/// Block signature for attestation.
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub struct SignatureBlock {
543    /// Optional unique identifier.
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub id: Option<String>,
546
547    /// Type of signature.
548    pub signature_type: BlockSignatureType,
549
550    /// Signer information.
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub signer: Option<SignerDetails>,
553
554    /// Timestamp of signature.
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub timestamp: Option<String>,
557
558    /// Purpose of signature.
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub purpose: Option<SignaturePurpose>,
561
562    /// Reference to digital signature if applicable.
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub digital_signature_ref: Option<String>,
565}
566
567impl SignatureBlock {
568    /// Create a new signature block.
569    #[must_use]
570    pub fn new(signature_type: BlockSignatureType) -> Self {
571        Self {
572            id: None,
573            signature_type,
574            signer: None,
575            timestamp: None,
576            purpose: None,
577            digital_signature_ref: None,
578        }
579    }
580
581    /// Set the signer details.
582    #[must_use]
583    pub fn with_signer(mut self, signer: SignerDetails) -> Self {
584        self.signer = Some(signer);
585        self
586    }
587
588    /// Set the purpose.
589    #[must_use]
590    pub fn with_purpose(mut self, purpose: SignaturePurpose) -> Self {
591        self.purpose = Some(purpose);
592        self
593    }
594}
595
596/// Type of block-level signature.
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
598#[serde(rename_all = "lowercase")]
599pub enum BlockSignatureType {
600    /// Handwritten signature.
601    Handwritten,
602    /// Digital cryptographic signature.
603    Digital,
604    /// Electronic signature (e.g., typed name).
605    Electronic,
606    /// Stamp or seal.
607    Stamp,
608}
609
610/// Purpose of a signature.
611#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(rename_all = "lowercase")]
613pub enum SignaturePurpose {
614    /// Certification of accuracy.
615    Certification,
616    /// Approval of content.
617    Approval,
618    /// Witnessing.
619    Witness,
620    /// Acknowledgment of receipt/understanding.
621    Acknowledgment,
622    /// Authorship attribution.
623    Authorship,
624}
625
626/// Signer details for signatures.
627#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
628pub struct SignerDetails {
629    /// Signer's name.
630    pub name: String,
631
632    /// Signer's title.
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub title: Option<String>,
635
636    /// Signer's organization.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub organization: Option<String>,
639
640    /// Signer's email.
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub email: Option<String>,
643
644    /// Signer's unique identifier.
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub id: Option<String>,
647}
648
649impl SignerDetails {
650    /// Create new signer details.
651    #[must_use]
652    pub fn new(name: impl Into<String>) -> Self {
653        Self {
654            name: name.into(),
655            title: None,
656            organization: None,
657            email: None,
658            id: None,
659        }
660    }
661
662    /// Set the title.
663    #[must_use]
664    pub fn with_title(mut self, title: impl Into<String>) -> Self {
665        self.title = Some(title.into());
666        self
667    }
668
669    /// Set the organization.
670    #[must_use]
671    pub fn with_organization(mut self, organization: impl Into<String>) -> Self {
672        self.organization = Some(organization.into());
673        self
674    }
675}
676
677/// SVG image block.
678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
679pub struct SvgBlock {
680    /// Optional unique identifier.
681    #[serde(default, skip_serializing_if = "Option::is_none")]
682    pub id: Option<String>,
683
684    /// Source reference (mutually exclusive with content).
685    #[serde(default, skip_serializing_if = "Option::is_none")]
686    pub src: Option<String>,
687
688    /// Inline SVG content (mutually exclusive with src).
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub content: Option<String>,
691
692    /// Width.
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub width: Option<u32>,
695
696    /// Height.
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub height: Option<u32>,
699
700    /// Alternative text for accessibility.
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub alt: Option<String>,
703}
704
705impl SvgBlock {
706    /// Create an SVG block from a source reference.
707    #[must_use]
708    pub fn from_src(src: impl Into<String>) -> Self {
709        Self {
710            id: None,
711            src: Some(src.into()),
712            content: None,
713            width: None,
714            height: None,
715            alt: None,
716        }
717    }
718
719    /// Create an SVG block from inline content.
720    #[must_use]
721    pub fn from_content(content: impl Into<String>) -> Self {
722        Self {
723            id: None,
724            src: None,
725            content: Some(content.into()),
726            width: None,
727            height: None,
728            alt: None,
729        }
730    }
731
732    /// Set the alt text.
733    #[must_use]
734    pub fn with_alt(mut self, alt: impl Into<String>) -> Self {
735        self.alt = Some(alt.into());
736        self
737    }
738
739    /// Set dimensions.
740    #[must_use]
741    pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
742        self.width = Some(width);
743        self.height = Some(height);
744        self
745    }
746}
747
748/// Barcode block.
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750#[serde(rename_all = "camelCase")]
751pub struct BarcodeBlock {
752    /// Optional unique identifier.
753    #[serde(default, skip_serializing_if = "Option::is_none")]
754    pub id: Option<String>,
755
756    /// Barcode format.
757    pub format: BarcodeFormat,
758
759    /// Encoded data.
760    pub data: String,
761
762    /// Error correction level (for QR codes).
763    #[serde(default, skip_serializing_if = "Option::is_none")]
764    pub error_correction: Option<ErrorCorrectionLevel>,
765
766    /// Size specification.
767    #[serde(default, skip_serializing_if = "Option::is_none")]
768    pub size: Option<BarcodeSize>,
769
770    /// Quiet zone around barcode.
771    #[serde(default, skip_serializing_if = "Option::is_none")]
772    pub quiet_zone: Option<String>,
773
774    /// Alternative text (required for accessibility).
775    pub alt: String,
776}
777
778impl BarcodeBlock {
779    /// Create a new barcode block.
780    #[must_use]
781    pub fn new(format: BarcodeFormat, data: impl Into<String>, alt: impl Into<String>) -> Self {
782        Self {
783            id: None,
784            format,
785            data: data.into(),
786            error_correction: None,
787            size: None,
788            quiet_zone: None,
789            alt: alt.into(),
790        }
791    }
792
793    /// Set error correction level.
794    #[must_use]
795    pub fn with_error_correction(mut self, level: ErrorCorrectionLevel) -> Self {
796        self.error_correction = Some(level);
797        self
798    }
799
800    /// Set the size.
801    #[must_use]
802    pub fn with_size(mut self, width: String, height: String) -> Self {
803        self.size = Some(BarcodeSize { width, height });
804        self
805    }
806}
807
808/// Barcode format.
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
810#[serde(rename_all = "lowercase")]
811pub enum BarcodeFormat {
812    /// QR Code.
813    Qr,
814    /// Data Matrix.
815    DataMatrix,
816    /// Code 128.
817    Code128,
818    /// Code 39.
819    Code39,
820    /// EAN-13.
821    Ean13,
822    /// EAN-8.
823    Ean8,
824    /// UPC-A.
825    UpcA,
826    /// PDF417.
827    Pdf417,
828}
829
830/// Error correction level for barcodes.
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
832pub enum ErrorCorrectionLevel {
833    /// Low (~7% recovery).
834    L,
835    /// Medium (~15% recovery).
836    M,
837    /// Quartile (~25% recovery).
838    Q,
839    /// High (~30% recovery).
840    H,
841}
842
843/// Barcode size specification.
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
845pub struct BarcodeSize {
846    /// Width with units.
847    pub width: String,
848    /// Height with units.
849    pub height: String,
850}
851
852/// Figure block (container for images/diagrams with captions).
853#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
854pub struct FigureBlock {
855    /// Optional unique identifier.
856    #[serde(default, skip_serializing_if = "Option::is_none")]
857    pub id: Option<String>,
858
859    /// Figure numbering mode.
860    #[serde(default, skip_serializing_if = "Option::is_none")]
861    pub numbering: Option<FigureNumbering>,
862
863    /// Subfigures within this figure.
864    #[serde(default, skip_serializing_if = "Option::is_none")]
865    pub subfigures: Option<Vec<Subfigure>>,
866
867    /// Figure contents (usually image, svg, or other visual block).
868    pub children: Vec<Block>,
869
870    /// Block attributes.
871    #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
872    pub attributes: BlockAttributes,
873}
874
875impl FigureBlock {
876    /// Create a new figure block.
877    #[must_use]
878    pub fn new(children: Vec<Block>) -> Self {
879        Self {
880            id: None,
881            numbering: None,
882            subfigures: None,
883            children,
884            attributes: BlockAttributes::default(),
885        }
886    }
887}
888
889/// Figure numbering mode.
890#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891#[serde(rename_all = "camelCase")]
892pub enum FigureNumbering {
893    /// Automatic numbering.
894    Auto,
895    /// No numbering.
896    #[serde(rename = "none")]
897    Unnumbered,
898    /// Explicit number.
899    #[serde(untagged)]
900    Number(u32),
901}
902
903/// A subfigure within a figure.
904#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
905pub struct Subfigure {
906    /// Optional unique identifier.
907    #[serde(default, skip_serializing_if = "Option::is_none")]
908    pub id: Option<String>,
909
910    /// Optional label (e.g., "(a)", "(b)").
911    #[serde(default, skip_serializing_if = "Option::is_none")]
912    pub label: Option<String>,
913
914    /// Subfigure content blocks.
915    pub children: Vec<Block>,
916}
917
918/// A pre-tokenized syntax highlighting token.
919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
920#[serde(rename_all = "camelCase")]
921pub struct CodeToken {
922    /// Token type (e.g., "keyword", "string", "comment").
923    pub token_type: String,
924
925    /// Token value text.
926    pub value: String,
927
928    /// Optional scope name for the token.
929    #[serde(default, skip_serializing_if = "Option::is_none")]
930    pub scope: Option<String>,
931}
932
933/// Figure caption block.
934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
935pub struct FigCaptionBlock {
936    /// Optional unique identifier.
937    #[serde(default, skip_serializing_if = "Option::is_none")]
938    pub id: Option<String>,
939
940    /// Caption text content.
941    pub children: Vec<Text>,
942
943    /// Block attributes.
944    #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
945    pub attributes: BlockAttributes,
946}
947
948impl FigCaptionBlock {
949    /// Create a new figure caption.
950    #[must_use]
951    pub fn new(children: Vec<Text>) -> Self {
952        Self {
953            id: None,
954            children,
955            attributes: BlockAttributes::default(),
956        }
957    }
958}
959
960/// Admonition block for callout boxes (note, warning, tip, etc.).
961#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
962#[serde(rename_all = "camelCase")]
963pub struct AdmonitionBlock {
964    /// Optional unique identifier.
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    pub id: Option<String>,
967
968    /// Admonition variant (note, tip, warning, etc.).
969    pub variant: AdmonitionVariant,
970
971    /// Optional title (if not provided, variant name is used).
972    #[serde(default, skip_serializing_if = "Option::is_none")]
973    pub title: Option<String>,
974
975    /// Content blocks within the admonition.
976    pub children: Vec<Block>,
977
978    /// Block attributes.
979    #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
980    pub attributes: BlockAttributes,
981}
982
983impl AdmonitionBlock {
984    /// Create a new admonition block.
985    #[must_use]
986    pub fn new(variant: AdmonitionVariant, children: Vec<Block>) -> Self {
987        Self {
988            id: None,
989            variant,
990            title: None,
991            children,
992            attributes: BlockAttributes::default(),
993        }
994    }
995
996    /// Set the admonition title.
997    #[must_use]
998    pub fn with_title(mut self, title: impl Into<String>) -> Self {
999        self.title = Some(title.into());
1000        self
1001    }
1002
1003    /// Set the block ID.
1004    #[must_use]
1005    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1006        self.id = Some(id.into());
1007        self
1008    }
1009}
1010
1011/// Admonition variant/type.
1012#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
1013#[serde(rename_all = "lowercase")]
1014pub enum AdmonitionVariant {
1015    /// General note or information.
1016    Note,
1017    /// Helpful tip.
1018    Tip,
1019    /// Informational notice.
1020    Info,
1021    /// Warning about potential issues.
1022    Warning,
1023    /// Caution about important considerations.
1024    Caution,
1025    /// Danger alert for critical issues.
1026    Danger,
1027    /// Important notice requiring attention.
1028    Important,
1029    /// Example content.
1030    Example,
1031}
1032
1033/// Definition list block.
1034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1035pub struct DefinitionListBlock {
1036    /// Optional unique identifier.
1037    #[serde(default, skip_serializing_if = "Option::is_none")]
1038    pub id: Option<String>,
1039
1040    /// Definition items (must be `DefinitionItem` blocks).
1041    pub children: Vec<Block>,
1042
1043    /// Block attributes.
1044    #[serde(default, skip_serializing_if = "BlockAttributes::is_empty")]
1045    pub attributes: BlockAttributes,
1046}
1047
1048impl DefinitionListBlock {
1049    /// Create a new definition list.
1050    #[must_use]
1051    pub fn new(children: Vec<Block>) -> Self {
1052        Self {
1053            id: None,
1054            children,
1055            attributes: BlockAttributes::default(),
1056        }
1057    }
1058}
1059
1060// Convenience constructors
1061impl Block {
1062    /// Create a paragraph block.
1063    #[must_use]
1064    pub fn paragraph(children: Vec<Text>) -> Self {
1065        Self::Paragraph {
1066            id: None,
1067            children,
1068            attributes: BlockAttributes::default(),
1069        }
1070    }
1071
1072    /// Create a heading block.
1073    #[must_use]
1074    pub fn heading(level: u8, children: Vec<Text>) -> Self {
1075        Self::Heading {
1076            id: None,
1077            level: level.clamp(1, 6),
1078            children,
1079            attributes: BlockAttributes::default(),
1080        }
1081    }
1082
1083    /// Create an unordered list.
1084    #[must_use]
1085    pub fn unordered_list(items: Vec<Block>) -> Self {
1086        Self::List {
1087            id: None,
1088            ordered: false,
1089            start: None,
1090            children: items,
1091            attributes: BlockAttributes::default(),
1092        }
1093    }
1094
1095    /// Create an ordered list.
1096    #[must_use]
1097    pub fn ordered_list(items: Vec<Block>) -> Self {
1098        Self::List {
1099            id: None,
1100            ordered: true,
1101            start: None,
1102            children: items,
1103            attributes: BlockAttributes::default(),
1104        }
1105    }
1106
1107    /// Create a list item.
1108    #[must_use]
1109    pub fn list_item(children: Vec<Block>) -> Self {
1110        Self::ListItem {
1111            id: None,
1112            checked: None,
1113            children,
1114            attributes: BlockAttributes::default(),
1115        }
1116    }
1117
1118    /// Create a checkbox list item.
1119    #[must_use]
1120    pub fn checkbox(checked: bool, children: Vec<Block>) -> Self {
1121        Self::ListItem {
1122            id: None,
1123            checked: Some(checked),
1124            children,
1125            attributes: BlockAttributes::default(),
1126        }
1127    }
1128
1129    /// Create a blockquote.
1130    #[must_use]
1131    pub fn blockquote(children: Vec<Block>) -> Self {
1132        Self::Blockquote {
1133            id: None,
1134            children,
1135            attributes: BlockAttributes::default(),
1136        }
1137    }
1138
1139    /// Create a code block.
1140    #[must_use]
1141    pub fn code_block(code: impl Into<String>, language: Option<String>) -> Self {
1142        Self::CodeBlock {
1143            id: None,
1144            language,
1145            highlighting: None,
1146            tokens: None,
1147            children: vec![Text::plain(code)],
1148            attributes: BlockAttributes::default(),
1149        }
1150    }
1151
1152    /// Create a horizontal rule.
1153    #[must_use]
1154    pub fn horizontal_rule() -> Self {
1155        Self::HorizontalRule { id: None }
1156    }
1157
1158    /// Create an image block.
1159    #[must_use]
1160    pub fn image(src: impl Into<String>, alt: impl Into<String>) -> Self {
1161        Self::Image(ImageBlock {
1162            id: None,
1163            src: src.into(),
1164            alt: alt.into(),
1165            title: None,
1166            width: None,
1167            height: None,
1168        })
1169    }
1170
1171    /// Create a table.
1172    #[must_use]
1173    pub fn table(rows: Vec<Block>) -> Self {
1174        Self::Table {
1175            id: None,
1176            children: rows,
1177            attributes: BlockAttributes::default(),
1178        }
1179    }
1180
1181    /// Create a table row.
1182    #[must_use]
1183    pub fn table_row(cells: Vec<Block>, header: bool) -> Self {
1184        Self::TableRow {
1185            id: None,
1186            header,
1187            children: cells,
1188            attributes: BlockAttributes::default(),
1189        }
1190    }
1191
1192    /// Create a table cell.
1193    #[must_use]
1194    pub fn table_cell(children: Vec<Text>) -> Self {
1195        Self::TableCell(TableCellBlock {
1196            id: None,
1197            colspan: 1,
1198            rowspan: 1,
1199            align: None,
1200            children,
1201            attributes: BlockAttributes::default(),
1202        })
1203    }
1204
1205    /// Create a math block.
1206    #[must_use]
1207    pub fn math(value: impl Into<String>, format: MathFormat, display: bool) -> Self {
1208        Self::Math(MathBlock {
1209            id: None,
1210            display,
1211            format,
1212            value: value.into(),
1213        })
1214    }
1215
1216    /// Create a line break.
1217    #[must_use]
1218    pub fn line_break() -> Self {
1219        Self::Break { id: None }
1220    }
1221
1222    /// Create a definition list.
1223    #[must_use]
1224    pub fn definition_list(items: Vec<Block>) -> Self {
1225        Self::DefinitionList(DefinitionListBlock::new(items))
1226    }
1227
1228    /// Create a definition item.
1229    #[must_use]
1230    pub fn definition_item(children: Vec<Block>) -> Self {
1231        Self::DefinitionItem {
1232            id: None,
1233            children,
1234            attributes: BlockAttributes::default(),
1235        }
1236    }
1237
1238    /// Create a definition term.
1239    #[must_use]
1240    pub fn definition_term(children: Vec<Text>) -> Self {
1241        Self::DefinitionTerm {
1242            id: None,
1243            children,
1244            attributes: BlockAttributes::default(),
1245        }
1246    }
1247
1248    /// Create a definition description.
1249    #[must_use]
1250    pub fn definition_description(children: Vec<Block>) -> Self {
1251        Self::DefinitionDescription {
1252            id: None,
1253            children,
1254            attributes: BlockAttributes::default(),
1255        }
1256    }
1257
1258    /// Create a measurement block.
1259    #[must_use]
1260    pub fn measurement(value: f64, display: impl Into<String>) -> Self {
1261        Self::Measurement(MeasurementBlock::new(value, display))
1262    }
1263
1264    /// Create a signature block.
1265    #[must_use]
1266    pub fn signature(signature_type: BlockSignatureType) -> Self {
1267        Self::Signature(SignatureBlock::new(signature_type))
1268    }
1269
1270    /// Create an SVG block from a source reference.
1271    #[must_use]
1272    pub fn svg_from_src(src: impl Into<String>) -> Self {
1273        Self::Svg(SvgBlock::from_src(src))
1274    }
1275
1276    /// Create an SVG block from inline content.
1277    #[must_use]
1278    pub fn svg_from_content(content: impl Into<String>) -> Self {
1279        Self::Svg(SvgBlock::from_content(content))
1280    }
1281
1282    /// Create a barcode block.
1283    #[must_use]
1284    pub fn barcode(format: BarcodeFormat, data: impl Into<String>, alt: impl Into<String>) -> Self {
1285        Self::Barcode(BarcodeBlock::new(format, data, alt))
1286    }
1287
1288    /// Create a figure block.
1289    #[must_use]
1290    pub fn figure(children: Vec<Block>) -> Self {
1291        Self::Figure(FigureBlock::new(children))
1292    }
1293
1294    /// Create a figure caption.
1295    #[must_use]
1296    pub fn figcaption(children: Vec<Text>) -> Self {
1297        Self::FigCaption(FigCaptionBlock::new(children))
1298    }
1299
1300    /// Create an admonition block.
1301    #[must_use]
1302    pub fn admonition(variant: AdmonitionVariant, children: Vec<Block>) -> Self {
1303        Self::Admonition(AdmonitionBlock::new(variant, children))
1304    }
1305
1306    /// Get the block type as a string.
1307    ///
1308    /// For extension blocks, this returns "extension". Use [`ExtensionBlock::full_type()`]
1309    /// to get the namespaced type like "forms:textInput".
1310    #[must_use]
1311    pub fn block_type(&self) -> &'static str {
1312        match self {
1313            Self::Paragraph { .. } => "paragraph",
1314            Self::Heading { .. } => "heading",
1315            Self::List { .. } => "list",
1316            Self::ListItem { .. } => "listItem",
1317            Self::Blockquote { .. } => "blockquote",
1318            Self::CodeBlock { .. } => "codeBlock",
1319            Self::HorizontalRule { .. } => "horizontalRule",
1320            Self::Image(_) => "image",
1321            Self::Table { .. } => "table",
1322            Self::TableRow { .. } => "tableRow",
1323            Self::TableCell(_) => "tableCell",
1324            Self::Math(_) => "math",
1325            Self::Break { .. } => "break",
1326            Self::DefinitionList(_) => "definitionList",
1327            Self::DefinitionItem { .. } => "definitionItem",
1328            Self::DefinitionTerm { .. } => "definitionTerm",
1329            Self::DefinitionDescription { .. } => "definitionDescription",
1330            Self::Measurement(_) => "measurement",
1331            Self::Signature(_) => "signature",
1332            Self::Svg(_) => "svg",
1333            Self::Barcode(_) => "barcode",
1334            Self::Figure(_) => "figure",
1335            Self::FigCaption(_) => "figCaption",
1336            Self::Admonition(_) => "admonition",
1337            Self::Extension(_) => "extension",
1338        }
1339    }
1340
1341    /// Get the block's ID if it has one.
1342    #[must_use]
1343    pub fn id(&self) -> Option<&str> {
1344        match self {
1345            Self::Paragraph { id, .. }
1346            | Self::Heading { id, .. }
1347            | Self::List { id, .. }
1348            | Self::ListItem { id, .. }
1349            | Self::Blockquote { id, .. }
1350            | Self::CodeBlock { id, .. }
1351            | Self::HorizontalRule { id }
1352            | Self::Table { id, .. }
1353            | Self::TableRow { id, .. }
1354            | Self::Break { id }
1355            | Self::DefinitionItem { id, .. }
1356            | Self::DefinitionTerm { id, .. }
1357            | Self::DefinitionDescription { id, .. } => id.as_deref(),
1358            Self::Image(img) => img.id.as_deref(),
1359            Self::TableCell(cell) => cell.id.as_deref(),
1360            Self::Math(math) => math.id.as_deref(),
1361            Self::DefinitionList(dl) => dl.id.as_deref(),
1362            Self::Measurement(m) => m.id.as_deref(),
1363            Self::Signature(sig) => sig.id.as_deref(),
1364            Self::Svg(svg) => svg.id.as_deref(),
1365            Self::Barcode(bc) => bc.id.as_deref(),
1366            Self::Figure(fig) => fig.id.as_deref(),
1367            Self::FigCaption(fc) => fc.id.as_deref(),
1368            Self::Admonition(adm) => adm.id.as_deref(),
1369            Self::Extension(ext) => ext.id.as_deref(),
1370        }
1371    }
1372
1373    /// Create an extension block.
1374    #[must_use]
1375    pub fn extension(namespace: impl Into<String>, block_type: impl Into<String>) -> Self {
1376        Self::Extension(ExtensionBlock::new(namespace, block_type))
1377    }
1378
1379    /// Check if this is an extension block.
1380    #[must_use]
1381    pub fn is_extension(&self) -> bool {
1382        matches!(self, Self::Extension(_))
1383    }
1384
1385    /// Get the extension block if this is one.
1386    #[must_use]
1387    pub fn as_extension(&self) -> Option<&ExtensionBlock> {
1388        match self {
1389            Self::Extension(ext) => Some(ext),
1390            _ => None,
1391        }
1392    }
1393}
1394
1395#[cfg(test)]
1396mod tests {
1397    use super::*;
1398
1399    #[test]
1400    fn test_content_new() {
1401        let content = Content::new(vec![Block::paragraph(vec![Text::plain("Hello")])]);
1402        assert_eq!(content.version, "0.1");
1403        assert_eq!(content.len(), 1);
1404        assert!(!content.is_empty());
1405    }
1406
1407    #[test]
1408    fn test_content_empty() {
1409        let content = Content::empty();
1410        assert!(content.is_empty());
1411        assert_eq!(content.len(), 0);
1412    }
1413
1414    #[test]
1415    fn test_paragraph() {
1416        let block = Block::paragraph(vec![Text::plain("Hello")]);
1417        assert_eq!(block.block_type(), "paragraph");
1418        assert!(block.id().is_none());
1419    }
1420
1421    #[test]
1422    fn test_heading() {
1423        let block = Block::heading(1, vec![Text::plain("Title")]);
1424        if let Block::Heading { level, .. } = &block {
1425            assert_eq!(*level, 1);
1426        } else {
1427            panic!("Expected Heading");
1428        }
1429
1430        // Test clamping
1431        let block = Block::heading(10, vec![Text::plain("Title")]);
1432        if let Block::Heading { level, .. } = &block {
1433            assert_eq!(*level, 6);
1434        }
1435    }
1436
1437    #[test]
1438    fn test_list() {
1439        let items = vec![
1440            Block::list_item(vec![Block::paragraph(vec![Text::plain("Item 1")])]),
1441            Block::list_item(vec![Block::paragraph(vec![Text::plain("Item 2")])]),
1442        ];
1443        let list = Block::unordered_list(items);
1444        assert_eq!(list.block_type(), "list");
1445    }
1446
1447    #[test]
1448    fn test_code_block() {
1449        let block = Block::code_block("fn main() {}", Some("rust".to_string()));
1450        if let Block::CodeBlock {
1451            language, children, ..
1452        } = &block
1453        {
1454            assert_eq!(language.as_deref(), Some("rust"));
1455            assert_eq!(children[0].value, "fn main() {}");
1456        } else {
1457            panic!("Expected CodeBlock");
1458        }
1459    }
1460
1461    #[test]
1462    fn test_image() {
1463        let block = Block::image("assets/photo.png", "A photo");
1464        if let Block::Image(img) = &block {
1465            assert_eq!(img.src, "assets/photo.png");
1466            assert_eq!(img.alt, "A photo");
1467        } else {
1468            panic!("Expected Image");
1469        }
1470    }
1471
1472    #[test]
1473    fn test_math() {
1474        let block = Block::math("E = mc^2", MathFormat::Latex, true);
1475        if let Block::Math(math) = &block {
1476            assert_eq!(math.value, "E = mc^2");
1477            assert_eq!(math.format, MathFormat::Latex);
1478            assert!(math.display);
1479        } else {
1480            panic!("Expected Math");
1481        }
1482    }
1483
1484    #[test]
1485    fn test_block_serialization() {
1486        let block = Block::paragraph(vec![Text::plain("Test")]);
1487        let json = serde_json::to_string(&block).unwrap();
1488        assert!(json.contains("\"type\":\"paragraph\""));
1489    }
1490
1491    #[test]
1492    fn test_content_serialization() {
1493        let content = Content::new(vec![
1494            Block::heading(1, vec![Text::plain("Title")]),
1495            Block::paragraph(vec![Text::plain("Body")]),
1496        ]);
1497        let json = serde_json::to_string_pretty(&content).unwrap();
1498        assert!(json.contains("\"version\": \"0.1\""));
1499        assert!(json.contains("\"type\": \"heading\""));
1500        assert!(json.contains("\"type\": \"paragraph\""));
1501    }
1502
1503    #[test]
1504    fn test_block_deserialization() {
1505        let json = r#"{
1506            "type": "heading",
1507            "level": 2,
1508            "children": [{"value": "Section"}]
1509        }"#;
1510        let block: Block = serde_json::from_str(json).unwrap();
1511        if let Block::Heading {
1512            level, children, ..
1513        } = block
1514        {
1515            assert_eq!(level, 2);
1516            assert_eq!(children[0].value, "Section");
1517        } else {
1518            panic!("Expected Heading");
1519        }
1520    }
1521
1522    #[test]
1523    fn test_table_serialization() {
1524        let table = Block::table(vec![Block::table_row(
1525            vec![Block::table_cell(vec![Text::plain("Header")])],
1526            true,
1527        )]);
1528        let json = serde_json::to_string(&table).unwrap();
1529        assert!(json.contains("\"type\":\"table\""));
1530        assert!(json.contains("\"type\":\"tableRow\""));
1531        assert!(json.contains("\"header\":true"));
1532    }
1533
1534    #[test]
1535    fn test_extension_block() {
1536        let ext = Block::extension("forms", "textInput");
1537        assert!(ext.is_extension());
1538        assert_eq!(ext.block_type(), "extension");
1539
1540        if let Block::Extension(inner) = &ext {
1541            assert_eq!(inner.namespace, "forms");
1542            assert_eq!(inner.block_type, "textInput");
1543            assert_eq!(inner.full_type(), "forms:textInput");
1544        } else {
1545            panic!("Expected Extension");
1546        }
1547    }
1548
1549    #[test]
1550    fn test_extension_as_extension() {
1551        let ext = Block::extension("semantic", "citation");
1552        let inner = ext.as_extension().expect("should be extension");
1553        assert_eq!(inner.namespace, "semantic");
1554
1555        let para = Block::paragraph(vec![Text::plain("Not extension")]);
1556        assert!(para.as_extension().is_none());
1557    }
1558
1559    #[test]
1560    fn test_extension_with_fallback() {
1561        let fallback = Block::paragraph(vec![Text::plain("[Form field]")]);
1562        let ext = ExtensionBlock::new("forms", "textInput")
1563            .with_id("name-field")
1564            .with_fallback(fallback);
1565
1566        assert_eq!(ext.id, Some("name-field".to_string()));
1567        assert!(ext.fallback_content().is_some());
1568    }
1569
1570    // Tests for new block types
1571
1572    #[test]
1573    fn test_definition_list() {
1574        let dl = Block::definition_list(vec![Block::definition_item(vec![
1575            Block::definition_term(vec![Text::plain("Term")]),
1576            Block::definition_description(vec![Block::paragraph(vec![Text::plain("Description")])]),
1577        ])]);
1578        assert_eq!(dl.block_type(), "definitionList");
1579    }
1580
1581    #[test]
1582    fn test_measurement() {
1583        let m = Block::measurement(9.81, "9.81 m/s²");
1584        assert_eq!(m.block_type(), "measurement");
1585        if let Block::Measurement(meas) = &m {
1586            assert!((meas.value - 9.81).abs() < 0.001);
1587            assert_eq!(meas.display, "9.81 m/s²");
1588        } else {
1589            panic!("Expected Measurement");
1590        }
1591    }
1592
1593    #[test]
1594    fn test_measurement_with_uncertainty() {
1595        let m = MeasurementBlock::new(1.234, "1.234(5) m")
1596            .with_unit("m")
1597            .with_uncertainty(0.005, UncertaintyNotation::Parenthetical);
1598        assert_eq!(m.unit, Some("m".to_string()));
1599        assert!(m.uncertainty.is_some());
1600        assert_eq!(
1601            m.uncertainty_notation,
1602            Some(UncertaintyNotation::Parenthetical)
1603        );
1604    }
1605
1606    #[test]
1607    fn test_signature() {
1608        let sig = Block::signature(BlockSignatureType::Digital);
1609        assert_eq!(sig.block_type(), "signature");
1610        if let Block::Signature(s) = &sig {
1611            assert_eq!(s.signature_type, BlockSignatureType::Digital);
1612        } else {
1613            panic!("Expected Signature");
1614        }
1615    }
1616
1617    #[test]
1618    fn test_signature_with_signer() {
1619        let signer = SignerDetails::new("John Doe")
1620            .with_title("CEO")
1621            .with_organization("Acme Corp");
1622        let sig = SignatureBlock::new(BlockSignatureType::Handwritten)
1623            .with_signer(signer)
1624            .with_purpose(SignaturePurpose::Approval);
1625        assert!(sig.signer.is_some());
1626        assert_eq!(sig.purpose, Some(SignaturePurpose::Approval));
1627    }
1628
1629    #[test]
1630    fn test_svg_from_src() {
1631        let svg = Block::svg_from_src("diagram.svg");
1632        assert_eq!(svg.block_type(), "svg");
1633        if let Block::Svg(s) = &svg {
1634            assert_eq!(s.src, Some("diagram.svg".to_string()));
1635            assert!(s.content.is_none());
1636        } else {
1637            panic!("Expected Svg");
1638        }
1639    }
1640
1641    #[test]
1642    fn test_svg_from_content() {
1643        let svg = Block::svg_from_content("<svg>...</svg>");
1644        if let Block::Svg(s) = &svg {
1645            assert!(s.src.is_none());
1646            assert_eq!(s.content, Some("<svg>...</svg>".to_string()));
1647        } else {
1648            panic!("Expected Svg");
1649        }
1650    }
1651
1652    #[test]
1653    fn test_barcode() {
1654        let bc = Block::barcode(
1655            BarcodeFormat::Qr,
1656            "https://example.com",
1657            "Link to example.com",
1658        );
1659        assert_eq!(bc.block_type(), "barcode");
1660        if let Block::Barcode(b) = &bc {
1661            assert_eq!(b.format, BarcodeFormat::Qr);
1662            assert_eq!(b.data, "https://example.com");
1663            assert_eq!(b.alt, "Link to example.com");
1664        } else {
1665            panic!("Expected Barcode");
1666        }
1667    }
1668
1669    #[test]
1670    fn test_barcode_with_options() {
1671        let bc = BarcodeBlock::new(BarcodeFormat::Qr, "data", "Meaningful alt text")
1672            .with_error_correction(ErrorCorrectionLevel::H)
1673            .with_size("2in".to_string(), "2in".to_string());
1674        assert_eq!(bc.error_correction, Some(ErrorCorrectionLevel::H));
1675        assert!(bc.size.is_some());
1676    }
1677
1678    #[test]
1679    fn test_figure() {
1680        let fig = Block::figure(vec![
1681            Block::image("photo.png", "A photo"),
1682            Block::figcaption(vec![Text::plain("Figure 1: A photo")]),
1683        ]);
1684        assert_eq!(fig.block_type(), "figure");
1685    }
1686
1687    #[test]
1688    fn test_figcaption() {
1689        let fc = Block::figcaption(vec![Text::plain("Caption text")]);
1690        assert_eq!(fc.block_type(), "figCaption");
1691    }
1692
1693    #[test]
1694    fn test_writing_mode_serialization() {
1695        let attrs = BlockAttributes {
1696            dir: None,
1697            lang: None,
1698            writing_mode: Some(WritingMode::VerticalRl),
1699        };
1700        let json = serde_json::to_string(&attrs).unwrap();
1701        assert!(json.contains("\"writingMode\":\"vertical-rl\""));
1702    }
1703
1704    #[test]
1705    fn test_writing_mode_deserialization() {
1706        let json = r#"{"writingMode":"vertical-lr"}"#;
1707        let attrs: BlockAttributes = serde_json::from_str(json).unwrap();
1708        assert_eq!(attrs.writing_mode, Some(WritingMode::VerticalLr));
1709    }
1710
1711    #[test]
1712    fn test_measurement_serialization() {
1713        let m = MeasurementBlock::new(299_792_458.0, "299,792,458 m/s")
1714            .with_unit("m/s")
1715            .with_exponent(8);
1716        let json = serde_json::to_string(&m).unwrap();
1717        assert!(json.contains("\"value\":299792458")); // JSON doesn't use underscores
1718        assert!(json.contains("\"unit\":\"m/s\""));
1719        assert!(json.contains("\"exponent\":8"));
1720    }
1721
1722    #[test]
1723    fn test_barcode_format_serialization() {
1724        let bc = BarcodeBlock::new(BarcodeFormat::DataMatrix, "ABC123", "Product code ABC123");
1725        let json = serde_json::to_string(&bc).unwrap();
1726        // DataMatrix becomes datamatrix with lowercase rename_all
1727        assert!(json.contains("\"format\":\"datamatrix\""));
1728    }
1729
1730    #[test]
1731    fn test_signature_type_serialization() {
1732        let sig = SignatureBlock::new(BlockSignatureType::Electronic);
1733        let json = serde_json::to_string(&sig).unwrap();
1734        assert!(json.contains("\"signatureType\":\"electronic\""));
1735    }
1736
1737    #[test]
1738    fn test_new_block_types_deserialization() {
1739        // Definition list
1740        let json = r#"{"type":"definitionList","children":[]}"#;
1741        let block: Block = serde_json::from_str(json).unwrap();
1742        assert_eq!(block.block_type(), "definitionList");
1743
1744        // Measurement
1745        let json = r#"{"type":"measurement","value":3.14159,"display":"π"}"#;
1746        let block: Block = serde_json::from_str(json).unwrap();
1747        assert_eq!(block.block_type(), "measurement");
1748
1749        // SVG
1750        let json = r#"{"type":"svg","src":"diagram.svg"}"#;
1751        let block: Block = serde_json::from_str(json).unwrap();
1752        assert_eq!(block.block_type(), "svg");
1753
1754        // Barcode
1755        let json = r#"{"type":"barcode","format":"qr","data":"test","alt":"Test QR code"}"#;
1756        let block: Block = serde_json::from_str(json).unwrap();
1757        assert_eq!(block.block_type(), "barcode");
1758
1759        // Figure
1760        let json = r#"{"type":"figure","children":[]}"#;
1761        let block: Block = serde_json::from_str(json).unwrap();
1762        assert_eq!(block.block_type(), "figure");
1763    }
1764
1765    // Admonition tests
1766
1767    #[test]
1768    fn test_admonition() {
1769        let adm = Block::admonition(
1770            AdmonitionVariant::Warning,
1771            vec![Block::paragraph(vec![Text::plain("Be careful!")])],
1772        );
1773        assert_eq!(adm.block_type(), "admonition");
1774        if let Block::Admonition(a) = &adm {
1775            assert_eq!(a.variant, AdmonitionVariant::Warning);
1776            assert_eq!(a.children.len(), 1);
1777        } else {
1778            panic!("Expected Admonition");
1779        }
1780    }
1781
1782    #[test]
1783    fn test_admonition_with_title() {
1784        let adm = AdmonitionBlock::new(
1785            AdmonitionVariant::Note,
1786            vec![Block::paragraph(vec![Text::plain("Important info")])],
1787        )
1788        .with_title("Please Note")
1789        .with_id("note-1");
1790
1791        assert_eq!(adm.title, Some("Please Note".to_string()));
1792        assert_eq!(adm.id, Some("note-1".to_string()));
1793    }
1794
1795    #[test]
1796    fn test_admonition_serialization() {
1797        let adm = Block::admonition(
1798            AdmonitionVariant::Tip,
1799            vec![Block::paragraph(vec![Text::plain("Pro tip!")])],
1800        );
1801        let json = serde_json::to_string(&adm).unwrap();
1802        assert!(json.contains("\"type\":\"admonition\""));
1803        assert!(json.contains("\"variant\":\"tip\""));
1804    }
1805
1806    #[test]
1807    fn test_admonition_deserialization() {
1808        let json = r#"{
1809            "type": "admonition",
1810            "variant": "danger",
1811            "title": "Warning!",
1812            "children": [
1813                {"type": "paragraph", "children": [{"value": "Do not proceed!"}]}
1814            ]
1815        }"#;
1816        let block: Block = serde_json::from_str(json).unwrap();
1817        assert_eq!(block.block_type(), "admonition");
1818        if let Block::Admonition(adm) = block {
1819            assert_eq!(adm.variant, AdmonitionVariant::Danger);
1820            assert_eq!(adm.title, Some("Warning!".to_string()));
1821            assert_eq!(adm.children.len(), 1);
1822        } else {
1823            panic!("Expected Admonition");
1824        }
1825    }
1826
1827    #[test]
1828    fn test_admonition_variant_display() {
1829        assert_eq!(AdmonitionVariant::Note.to_string(), "Note");
1830        assert_eq!(AdmonitionVariant::Warning.to_string(), "Warning");
1831        assert_eq!(AdmonitionVariant::Important.to_string(), "Important");
1832    }
1833
1834    #[test]
1835    fn test_all_admonition_variants() {
1836        let variants = [
1837            (AdmonitionVariant::Note, "note"),
1838            (AdmonitionVariant::Tip, "tip"),
1839            (AdmonitionVariant::Info, "info"),
1840            (AdmonitionVariant::Warning, "warning"),
1841            (AdmonitionVariant::Caution, "caution"),
1842            (AdmonitionVariant::Danger, "danger"),
1843            (AdmonitionVariant::Important, "important"),
1844            (AdmonitionVariant::Example, "example"),
1845        ];
1846
1847        for (variant, expected_name) in variants {
1848            let adm = AdmonitionBlock::new(variant, vec![]);
1849            let json = serde_json::to_string(&adm).unwrap();
1850            assert!(
1851                json.contains(&format!("\"variant\":\"{expected_name}\"")),
1852                "Variant {variant:?} should serialize as {expected_name}",
1853            );
1854        }
1855    }
1856}
1857
1858#[cfg(test)]
1859mod proptests {
1860    use super::*;
1861    use proptest::prelude::*;
1862
1863    /// Generate arbitrary text content for testing.
1864    fn arb_text_content() -> impl Strategy<Value = String> {
1865        "[a-zA-Z0-9 .,!?]{0,100}".prop_map(|s| s)
1866    }
1867
1868    /// Generate arbitrary optional string.
1869    fn arb_optional_string() -> impl Strategy<Value = Option<String>> {
1870        prop_oneof![Just(None), "[a-zA-Z0-9_-]{1,20}".prop_map(Some),]
1871    }
1872
1873    /// Generate arbitrary heading level.
1874    fn arb_heading_level() -> impl Strategy<Value = u8> {
1875        1u8..=6u8
1876    }
1877
1878    /// Generate arbitrary paragraph block.
1879    fn arb_paragraph() -> impl Strategy<Value = Block> {
1880        (arb_optional_string(), arb_text_content()).prop_map(|(id, text)| {
1881            let mut block = Block::paragraph(vec![Text::plain(text)]);
1882            if let Block::Paragraph {
1883                id: ref mut block_id,
1884                ..
1885            } = block
1886            {
1887                *block_id = id;
1888            }
1889            block
1890        })
1891    }
1892
1893    /// Generate arbitrary heading block.
1894    fn arb_heading() -> impl Strategy<Value = Block> {
1895        (
1896            arb_optional_string(),
1897            arb_heading_level(),
1898            arb_text_content(),
1899        )
1900            .prop_map(|(id, level, text)| {
1901                let mut block = Block::heading(level, vec![Text::plain(text)]);
1902                if let Block::Heading {
1903                    id: ref mut block_id,
1904                    ..
1905                } = block
1906                {
1907                    *block_id = id;
1908                }
1909                block
1910            })
1911    }
1912
1913    /// Generate arbitrary code block.
1914    fn arb_code_block() -> impl Strategy<Value = Block> {
1915        (
1916            arb_optional_string(),
1917            arb_text_content(),
1918            arb_optional_string(),
1919        )
1920            .prop_map(|(id, code, language)| {
1921                let mut block = Block::code_block(code, language);
1922                if let Block::CodeBlock {
1923                    id: ref mut block_id,
1924                    ..
1925                } = block
1926                {
1927                    *block_id = id;
1928                }
1929                block
1930            })
1931    }
1932
1933    /// Generate arbitrary math block.
1934    fn arb_math_block() -> impl Strategy<Value = Block> {
1935        (
1936            arb_optional_string(),
1937            arb_text_content(),
1938            prop_oneof![Just(MathFormat::Latex), Just(MathFormat::Mathml)],
1939            any::<bool>(),
1940        )
1941            .prop_map(|(id, value, format, display)| {
1942                let mut block = Block::math(value, format, display);
1943                if let Block::Math(ref mut math) = block {
1944                    math.id = id;
1945                }
1946                block
1947            })
1948    }
1949
1950    proptest! {
1951        /// Paragraph JSON roundtrip - serialize and deserialize should preserve data.
1952        #[test]
1953        fn paragraph_json_roundtrip(para in arb_paragraph()) {
1954            let json = serde_json::to_string(&para).unwrap();
1955            let parsed: Block = serde_json::from_str(&json).unwrap();
1956            prop_assert_eq!(para, parsed);
1957        }
1958
1959        /// Heading JSON roundtrip - serialize and deserialize should preserve data.
1960        #[test]
1961        fn heading_json_roundtrip(heading in arb_heading()) {
1962            let json = serde_json::to_string(&heading).unwrap();
1963            let parsed: Block = serde_json::from_str(&json).unwrap();
1964            prop_assert_eq!(heading, parsed);
1965        }
1966
1967        /// Code block JSON roundtrip - serialize and deserialize should preserve data.
1968        #[test]
1969        fn code_block_json_roundtrip(code in arb_code_block()) {
1970            let json = serde_json::to_string(&code).unwrap();
1971            let parsed: Block = serde_json::from_str(&json).unwrap();
1972            prop_assert_eq!(code, parsed);
1973        }
1974
1975        /// Math block JSON roundtrip - serialize and deserialize should preserve data.
1976        #[test]
1977        fn math_block_json_roundtrip(math in arb_math_block()) {
1978            let json = serde_json::to_string(&math).unwrap();
1979            let parsed: Block = serde_json::from_str(&json).unwrap();
1980            prop_assert_eq!(math, parsed);
1981        }
1982
1983        /// Heading level is always clamped to valid range 1-6.
1984        #[test]
1985        fn heading_level_clamped(level in any::<u8>()) {
1986            let block = Block::heading(level, vec![Text::plain("Test")]);
1987            if let Block::Heading { level: actual, .. } = block {
1988                prop_assert!((1..=6).contains(&actual));
1989            } else {
1990                prop_assert!(false, "Expected Heading block");
1991            }
1992        }
1993
1994        /// Content version is always set correctly.
1995        #[test]
1996        fn content_version_is_spec_version(blocks in prop::collection::vec(arb_paragraph(), 0..5)) {
1997            let blocks_len = blocks.len();
1998            let content = Content::new(blocks);
1999            prop_assert_eq!(&content.version, crate::SPEC_VERSION);
2000            prop_assert_eq!(content.len(), blocks_len);
2001        }
2002
2003        /// Block type returns correct string for paragraphs.
2004        #[test]
2005        fn paragraph_block_type(para in arb_paragraph()) {
2006            prop_assert_eq!(para.block_type(), "paragraph");
2007        }
2008
2009        /// Block type returns correct string for headings.
2010        #[test]
2011        fn heading_block_type(heading in arb_heading()) {
2012            prop_assert_eq!(heading.block_type(), "heading");
2013        }
2014    }
2015
2016    #[test]
2017    fn code_block_highlighting_and_tokens_roundtrip() {
2018        let json = serde_json::json!({
2019            "type": "codeBlock",
2020            "value": "let x = 1;",
2021            "language": "rust",
2022            "children": [],
2023            "highlighting": "monokai",
2024            "tokens": [
2025                { "tokenType": "keyword", "value": "let" },
2026                { "tokenType": "identifier", "value": "x", "scope": "variable" }
2027            ]
2028        });
2029        let block: Block = serde_json::from_value(json).unwrap();
2030        if let Block::CodeBlock {
2031            highlighting,
2032            tokens,
2033            ..
2034        } = &block
2035        {
2036            assert_eq!(highlighting.as_deref(), Some("monokai"));
2037            let toks = tokens.as_ref().unwrap();
2038            assert_eq!(toks.len(), 2);
2039            assert_eq!(toks[0].token_type, "keyword");
2040            assert_eq!(toks[1].scope.as_deref(), Some("variable"));
2041        } else {
2042            panic!("Expected CodeBlock");
2043        }
2044        // Roundtrip
2045        let serialized = serde_json::to_value(&block).unwrap();
2046        let parsed: Block = serde_json::from_value(serialized).unwrap();
2047        assert_eq!(block, parsed);
2048    }
2049
2050    #[test]
2051    fn code_block_without_new_fields_defaults_to_none() {
2052        let json = serde_json::json!({
2053            "type": "codeBlock",
2054            "value": "print('hello')",
2055            "language": "python",
2056            "children": []
2057        });
2058        let block: Block = serde_json::from_value(json).unwrap();
2059        if let Block::CodeBlock {
2060            highlighting,
2061            tokens,
2062            ..
2063        } = &block
2064        {
2065            assert!(highlighting.is_none());
2066            assert!(tokens.is_none());
2067        } else {
2068            panic!("Expected CodeBlock");
2069        }
2070    }
2071
2072    #[test]
2073    fn figure_numbering_serialization() {
2074        // Auto
2075        let json = serde_json::to_value(FigureNumbering::Auto).unwrap();
2076        assert_eq!(json, serde_json::json!("auto"));
2077
2078        // Unnumbered
2079        let json = serde_json::to_value(FigureNumbering::Unnumbered).unwrap();
2080        assert_eq!(json, serde_json::json!("none"));
2081
2082        // Number
2083        let json = serde_json::to_value(FigureNumbering::Number(3)).unwrap();
2084        assert_eq!(json, serde_json::json!(3));
2085
2086        // Roundtrip
2087        let auto: FigureNumbering = serde_json::from_str("\"auto\"").unwrap();
2088        assert_eq!(auto, FigureNumbering::Auto);
2089        let unnumbered: FigureNumbering = serde_json::from_str("\"none\"").unwrap();
2090        assert_eq!(unnumbered, FigureNumbering::Unnumbered);
2091        let num: FigureNumbering = serde_json::from_str("3").unwrap();
2092        assert_eq!(num, FigureNumbering::Number(3));
2093    }
2094
2095    #[test]
2096    fn figure_with_numbering_and_subfigures_roundtrip() {
2097        let json = serde_json::json!({
2098            "type": "figure",
2099            "children": [
2100                { "type": "image", "src": "fig1.png", "alt": "Figure 1" }
2101            ],
2102            "numbering": "auto",
2103            "subfigures": [
2104                {
2105                    "id": "sub-a",
2106                    "label": "(a)",
2107                    "children": [
2108                        { "type": "paragraph", "children": [{ "type": "text", "value": "Sub A" }] }
2109                    ]
2110                }
2111            ]
2112        });
2113        let block: Block = serde_json::from_value(json).unwrap();
2114        if let Block::Figure(fig) = &block {
2115            assert_eq!(fig.numbering, Some(FigureNumbering::Auto));
2116            let subs = fig.subfigures.as_ref().unwrap();
2117            assert_eq!(subs.len(), 1);
2118            assert_eq!(subs[0].id.as_deref(), Some("sub-a"));
2119            assert_eq!(subs[0].label.as_deref(), Some("(a)"));
2120        } else {
2121            panic!("Expected Figure block");
2122        }
2123        let serialized = serde_json::to_value(&block).unwrap();
2124        let parsed: Block = serde_json::from_value(serialized).unwrap();
2125        assert_eq!(block, parsed);
2126    }
2127
2128    #[test]
2129    fn figure_without_new_fields_defaults_to_none() {
2130        let json = serde_json::json!({
2131            "type": "figure",
2132            "children": []
2133        });
2134        let block: Block = serde_json::from_value(json).unwrap();
2135        if let Block::Figure(fig) = &block {
2136            assert!(fig.numbering.is_none());
2137            assert!(fig.subfigures.is_none());
2138        } else {
2139            panic!("Expected Figure block");
2140        }
2141    }
2142}