Skip to main content

common/parser_tools/
djot_options.rs

1//! Per-feature selection for djot import/export.
2//!
3//! The djot importer and exporter round-trip a set of *optional* block-level
4//! attributes — paragraph alignment, line height, text direction, non-breakable
5//! lines and background color — through djot's native `{key=value}` block
6//! attribute syntax (e.g. `{alignment=center}` on the line before a paragraph).
7//! These two option structs let a caller choose which of those attributes are
8//! carried. Everything else (headings, lists, tables, blockquotes, code blocks
9//! and all inline formatting) is always imported/exported and is not gated.
10//!
11//! Both structs default to **all enabled** — the fully lossless round-trip. Use
12//! [`DjotImportOptions::none`] / [`DjotExportOptions::none`] to restrict the
13//! round-trip to the core structural and inline feature set only.
14//!
15//! The attribute keys used on the wire are the model field names:
16//! `alignment`, `line_height`, `direction`, `non_breakable_lines`,
17//! `page_break_before`, `background_color`, `top_margin`, `text_indent` and
18//! `semantic_role`. Block attributes are only emitted/read for standalone
19//! paragraphs and headings; list items, code blocks and table cells normalise
20//! their block styling away (the same boundary the other targets observe).
21
22use serde::{Deserialize, Serialize};
23
24/// Selects which optional block attributes the djot **importer** applies to the
25/// document model. An attribute present in the source but disabled here is
26/// parsed and discarded, exactly like an unrepresentable construct.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub struct DjotImportOptions {
29    /// Apply paragraph alignment from `{alignment=left|right|center|justify}`.
30    pub alignment: bool,
31    /// Apply line height from `{line_height=<int>}`.
32    pub line_height: bool,
33    /// Apply text direction from `{direction=ltr|rtl}`.
34    pub direction: bool,
35    /// Apply non-breakable lines from `{non_breakable_lines=true|false}`.
36    pub non_breakable_lines: bool,
37    /// Apply "start on a new page" from `{page_break_before=true|false}`.
38    pub page_break_before: bool,
39    /// Apply block background color from `{background_color="<value>"}`.
40    pub background_color: bool,
41    /// Apply the block's own space-above from `{top_margin=<int>}`, overriding
42    /// the document-wide paragraph spacing for that one block.
43    pub top_margin: bool,
44    /// Apply the block's own first-line indent from `{text_indent=<int>}`,
45    /// overriding the document-wide first-line indent for that one block.
46    pub text_indent: bool,
47    /// Carry the frame's semantic role from `{semantic_role=epigraph}`. Unlike its
48    /// neighbours this one describes the enclosing **blockquote**, not the paragraph it
49    /// is written on: it rides the quote's first block because that is the only channel
50    /// djot block attributes have, and the importer moves it onto the frame.
51    pub semantic_role: bool,
52}
53
54impl DjotImportOptions {
55    /// Every optional block attribute enabled — the lossless default.
56    pub const fn all() -> Self {
57        Self {
58            alignment: true,
59            line_height: true,
60            direction: true,
61            non_breakable_lines: true,
62            page_break_before: true,
63            background_color: true,
64            top_margin: true,
65            text_indent: true,
66            semantic_role: true,
67        }
68    }
69
70    /// No optional block attributes — import only the core structural and
71    /// inline feature set, discarding any block-attribute styling.
72    pub const fn none() -> Self {
73        Self {
74            alignment: false,
75            line_height: false,
76            direction: false,
77            non_breakable_lines: false,
78            page_break_before: false,
79            background_color: false,
80            top_margin: false,
81            text_indent: false,
82            semantic_role: false,
83        }
84    }
85}
86
87impl Default for DjotImportOptions {
88    fn default() -> Self {
89        Self::all()
90    }
91}
92
93/// Selects which optional block attributes the djot **exporter** emits as
94/// `{key=value}` block attributes. A disabled attribute is omitted from the
95/// output even when the model carries a value for it.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97pub struct DjotExportOptions {
98    /// Emit paragraph alignment as `{alignment=…}`.
99    pub alignment: bool,
100    /// Emit line height as `{line_height=…}`.
101    pub line_height: bool,
102    /// Emit text direction as `{direction=…}`.
103    pub direction: bool,
104    /// Emit non-breakable lines as `{non_breakable_lines=…}`.
105    pub non_breakable_lines: bool,
106    /// Emit "start on a new page" as `{page_break_before=…}`.
107    pub page_break_before: bool,
108    /// Emit block background color as `{background_color=…}`.
109    pub background_color: bool,
110    /// Emit the block's own space-above as `{top_margin=…}`.
111    pub top_margin: bool,
112    /// Emit the block's own first-line indent as `{text_indent=…}`.
113    pub text_indent: bool,
114    /// Emit the enclosing blockquote's semantic role as `{semantic_role=…}` on the
115    /// quote's first block, so the model→djot→model round trip stays a fixpoint.
116    pub semantic_role: bool,
117    /// Drop inline images instead of emitting `![alt](src){width=… height=…}`.
118    ///
119    /// Unlike every other field here this one is not a block attribute and is
120    /// **not** governed by [`Self::all`]/[`Self::none`]: an image is content, not
121    /// styling, so "emit no optional attributes" must not quietly delete it.
122    /// Only a caller that has decided not to place the image files beside the
123    /// output turns it on.
124    #[serde(default)]
125    pub omit_images: bool,
126}
127
128impl DjotExportOptions {
129    /// Every optional block attribute emitted — the lossless default.
130    pub const fn all() -> Self {
131        Self {
132            alignment: true,
133            line_height: true,
134            direction: true,
135            non_breakable_lines: true,
136            page_break_before: true,
137            background_color: true,
138            top_margin: true,
139            text_indent: true,
140            semantic_role: true,
141            omit_images: false,
142        }
143    }
144
145    /// No optional block attributes — emit only the core structural and inline
146    /// feature set, dropping any block-attribute styling.
147    pub const fn none() -> Self {
148        Self {
149            alignment: false,
150            line_height: false,
151            direction: false,
152            non_breakable_lines: false,
153            page_break_before: false,
154            background_color: false,
155            top_margin: false,
156            text_indent: false,
157            semantic_role: false,
158            // Content, not styling — see the field's own doc.
159            omit_images: false,
160        }
161    }
162}
163
164impl Default for DjotExportOptions {
165    fn default() -> Self {
166        Self::all()
167    }
168}