rwml/model.rs
1//! The rich document model — a typed intermediate representation built lazily on
2//! top of the piece table, character/paragraph properties, the style sheet, and
3//! the table structure.
4//!
5//! The flat [`crate::Document::text`] path is untouched and stays fast; the model
6//! is only assembled when a caller asks for [`crate::Document::model`],
7//! [`crate::Document::to_markdown`], or [`crate::Document::to_html`].
8//!
9//! The shape mirrors the de-facto Rust/Pandoc document IRs (a document is a list
10//! of block-level nodes; paragraphs hold inline runs) so the Markdown/HTML
11//! exporters are simple folds.
12
13use std::collections::BTreeMap;
14
15use crate::annotation::{NoteKind, RevisionKind};
16
17/// A whole `.doc` document as an ordered list of block-level nodes plus
18/// document-level metadata.
19#[derive(Debug, Clone, Default, PartialEq)]
20pub struct DocModel {
21 /// Block-level content, in reading order.
22 pub blocks: Vec<Block>,
23 /// Source-region spans for content that came from distinct Word
24 /// subdocuments. Empty for authored models and sources that do not expose a
25 /// region map yet.
26 pub regions: Vec<SourceRegion>,
27 /// Document-level metadata (codepage, language, counts).
28 pub meta: DocMeta,
29 /// Authored string custom document properties.
30 pub custom_properties: BTreeMap<String, String>,
31 /// Authored custom XML data-store items.
32 pub custom_xml_items: Vec<CustomXmlItem>,
33 /// Document-level layout for authoring/rendering (page size, header/footer,
34 /// metadata). Defaults to A4 portrait with no running header/footer.
35 pub setup: DocSetup,
36}
37
38/// A generated custom XML data-store item.
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
40pub struct CustomXmlItem {
41 /// Custom XML store item ID.
42 pub store_item_id: String,
43 /// Raw XML payload for `customXml/itemN.xml`.
44 pub xml: String,
45}
46
47/// A generated Office web-extension task pane package entry.
48#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct WebExtensionTaskPane {
50 /// Web extension instance id.
51 pub extension_id: String,
52 /// Add-in reference id from the deployment catalog or manifest.
53 pub reference_id: String,
54 /// Add-in version string.
55 pub version: String,
56 /// Store or catalog pointer.
57 pub store: String,
58 /// Store or catalog kind, such as `EXCatalog`, `FileSystem`, or `OMEX`.
59 pub store_type: String,
60 /// Web-extension custom property bag.
61 pub properties: BTreeMap<String, String>,
62 /// Last docked task-pane location.
63 pub dock_state: String,
64 /// Whether the task pane is visible by default.
65 pub visible: bool,
66 /// Default task-pane width.
67 pub width: u32,
68 /// Task-pane row index among panes docked in the same location.
69 pub row: u32,
70 /// Whether the task pane is locked in the UI.
71 pub locked: bool,
72}
73
74/// A coarse source subdocument region from the original Word file.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum SourceRegionKind {
77 /// Main body text.
78 Main,
79 /// Legacy footnote subdocument.
80 Footnote,
81 /// Header/footer subdocument.
82 HeaderFooter,
83 /// Legacy annotation/comment subdocument.
84 Annotation,
85 /// Legacy endnote subdocument.
86 Endnote,
87 /// Text-box subdocument.
88 TextBox,
89}
90
91/// A span of [`DocModel::blocks`] that came from one source subdocument.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct SourceRegion {
94 /// Region kind.
95 pub kind: SourceRegionKind,
96 /// Source-specific story index within the subdocument, when a format table
97 /// exposes one. For legacy `.doc` header/footer regions this is the
98 /// `PlcfHdd` story index.
99 pub source_story_index: Option<usize>,
100 /// Inclusive start block index in [`DocModel::blocks`].
101 pub block_start: usize,
102 /// Exclusive end block index in [`DocModel::blocks`].
103 pub block_end: usize,
104 /// UTF-16 CP start in the original Word source stream.
105 pub source_start_cp: usize,
106 /// UTF-16 CP length reported by the source metadata.
107 pub source_len_cp: usize,
108 /// Visible character start within the flattened model text.
109 pub text_start: usize,
110 /// Visible character length contributed by this region.
111 pub text_len: usize,
112}
113
114impl DocModel {
115 /// Iterate source-region spans of the requested kind.
116 pub fn source_regions(&self, kind: SourceRegionKind) -> impl Iterator<Item = &SourceRegion> {
117 self.regions
118 .iter()
119 .filter(move |region| region.kind == kind)
120 }
121
122 /// Return the block slice covered by a source-region span.
123 ///
124 /// The range is clamped to this model, so passing a stale region from another
125 /// model returns an empty or shortened slice instead of panicking.
126 pub fn source_region_blocks(&self, region: &SourceRegion) -> &[Block] {
127 let start = region.block_start.min(self.blocks.len());
128 let end = region.block_end.min(self.blocks.len());
129 if start <= end {
130 &self.blocks[start..end]
131 } else {
132 &self.blocks[0..0]
133 }
134 }
135
136 /// Concatenate visible text from the blocks covered by a source-region span.
137 pub fn source_region_text(&self, region: &SourceRegion) -> String {
138 let mut out = String::new();
139 append_blocks_text(self.source_region_blocks(region), &mut out);
140 out
141 }
142
143 /// Concatenate visible text from all source regions of one kind, in model
144 /// order. Returns an empty string when the model has no matching regions.
145 pub fn source_region_kind_text(&self, kind: SourceRegionKind) -> String {
146 let mut out = String::new();
147 for region in self.source_regions(kind) {
148 append_blocks_text(self.source_region_blocks(region), &mut out);
149 }
150 out
151 }
152}
153
154fn append_blocks_text(blocks: &[Block], out: &mut String) {
155 for block in blocks {
156 match block {
157 Block::Paragraph(paragraph) => {
158 for run in ¶graph.runs {
159 out.push_str(&run.text);
160 }
161 }
162 Block::Table(table) => {
163 for row in &table.rows {
164 for cell in &row.cells {
165 append_blocks_text(&cell.blocks, out);
166 }
167 }
168 }
169 Block::Image(_) | Block::Chart(_) | Block::SectionBreak(_) => {}
170 Block::PageBreak => out.push('\n'),
171 }
172 }
173}
174
175/// Document-level metadata.
176#[derive(Debug, Clone, Default, PartialEq)]
177pub struct DocMeta {
178 /// ANSI codepage of 8-bit pieces (e.g. 949 for Korean).
179 pub codepage: u16,
180 /// FIB language id (`lid`).
181 pub lid: u16,
182 /// Aggregate counts (paragraphs, tables, figures, characters).
183 pub stats: Stats,
184}
185
186/// Aggregate document statistics, mirroring the project-wide `DocStats` contract.
187#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
188pub struct Stats {
189 /// Number of paragraphs (including headings, list items, and cell paragraphs).
190 pub paragraphs: u32,
191 /// Number of tables.
192 pub tables: u16,
193 /// Number of images / figures.
194 pub figures: u16,
195 /// Total visible character count.
196 pub text_chars: usize,
197}
198
199/// A block-level node.
200#[derive(Debug, Clone, PartialEq)]
201pub enum Block {
202 /// A paragraph — also carries headings and list items via [`ParaProps`].
203 Paragraph(Paragraph),
204 /// A table (rows → cells → blocks).
205 Table(Table),
206 /// A block-level image (an image-only paragraph).
207 Image(Image),
208 /// A block-level chart with literal category/value data.
209 Chart(Chart),
210 /// An explicit page break between block-level items.
211 PageBreak,
212 /// A section boundary carrying layout for the section that just ended.
213 SectionBreak(SectionSetup),
214}
215
216/// Section-break start behavior for a WordprocessingML section boundary.
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum SectionBreakKind {
219 /// Start the following section on the next page (`nextPage`).
220 NextPage,
221 /// Start the following section on the next even page (`evenPage`).
222 EvenPage,
223 /// Start the following section on the next odd page (`oddPage`).
224 OddPage,
225}
226
227#[cfg(feature = "docx")]
228impl SectionBreakKind {
229 pub(crate) fn wml_value(self) -> &'static str {
230 match self {
231 SectionBreakKind::NextPage => "nextPage",
232 SectionBreakKind::EvenPage => "evenPage",
233 SectionBreakKind::OddPage => "oddPage",
234 }
235 }
236
237 pub(crate) fn from_wml_value(value: &str) -> Option<Self> {
238 let value = value.trim();
239 match value {
240 "nextPage" => Some(SectionBreakKind::NextPage),
241 "evenPage" => Some(SectionBreakKind::EvenPage),
242 "oddPage" => Some(SectionBreakKind::OddPage),
243 _ => None,
244 }
245 }
246}
247
248/// A paragraph: inline runs plus paragraph-level properties.
249#[derive(Debug, Clone, Default, PartialEq)]
250pub struct Paragraph {
251 /// Paragraph-level properties (style, heading, alignment, list membership).
252 pub props: ParaProps,
253 /// Inline runs, in reading order.
254 pub runs: Vec<Run>,
255}
256
257impl Paragraph {
258 /// The concatenated visible text of all runs (hidden runs included — they are
259 /// part of the indexed text; the HTML exporter may choose to omit them).
260 pub fn text(&self) -> String {
261 self.runs.iter().map(|r| r.text.as_str()).collect()
262 }
263
264 /// `true` if the paragraph has no visible text and no image in any run.
265 pub fn is_blank(&self) -> bool {
266 self.runs
267 .iter()
268 .all(|r| r.text.trim().is_empty() && r.image.is_none())
269 }
270}
271
272/// Paragraph spacing in points; `None` = unset (renderer/writer uses defaults).
273#[derive(Debug, Clone, Copy, Default, PartialEq)]
274pub struct Spacing {
275 /// Space above the paragraph.
276 pub before_pt: Option<f32>,
277 /// Space below the paragraph.
278 pub after_pt: Option<f32>,
279 /// Line height as a multiple of the font size (e.g. `1.5`).
280 pub line_pct: Option<f32>,
281}
282
283/// Paragraph indentation in points; `None` = unset.
284#[derive(Debug, Clone, Copy, Default, PartialEq)]
285pub struct Indent {
286 /// Left indent.
287 pub left_pt: Option<f32>,
288 /// Right indent.
289 pub right_pt: Option<f32>,
290 /// First-line additional indent.
291 pub first_line_pt: Option<f32>,
292 /// Hanging indent (first line outdented by this much).
293 pub hanging_pt: Option<f32>,
294}
295
296/// Paragraph-level properties.
297#[derive(Debug, Clone, Default, PartialEq)]
298pub struct ParaProps {
299 /// Raw Word paragraph style id (`w:pStyle/@w:val`), if known or authored.
300 pub style_id: Option<String>,
301 /// Resolved style name (e.g. `Heading 1`, `제목 1`), if the style sheet was read.
302 pub style_name: Option<String>,
303 /// Heading level `1..=6`, from the outline level or a recognized style name.
304 pub heading_level: Option<u8>,
305 /// Paragraph alignment.
306 pub align: Align,
307 /// Raw outline level `0..=8` (9/None = body text).
308 pub outline_level: Option<u8>,
309 /// List membership — `Some` makes this paragraph a list item.
310 pub list: Option<ListInfo>,
311 /// Spacing (before/after/line).
312 pub spacing: Spacing,
313 /// Indentation.
314 pub indent: Indent,
315 /// Paragraph background shading, if any.
316 pub shading: Option<Color>,
317 /// Force this paragraph to begin on a new page (`w:pageBreakBefore`).
318 pub page_break_before: bool,
319 /// Right-to-left paragraph layout (`w:bidi`).
320 pub bidi: bool,
321}
322
323/// A paragraph style definition for generated `.docx` output.
324#[derive(Debug, Clone, Default, PartialEq)]
325pub struct ParagraphStyle {
326 /// Stable Word style id (`w:styleId` / paragraph `w:pStyle` value).
327 pub id: String,
328 /// Human-readable style name.
329 pub name: String,
330 /// Base style id, if any.
331 pub based_on: Option<String>,
332 /// Next paragraph style id, if any.
333 pub next: Option<String>,
334 /// Whether the style should appear in Word's quick style gallery.
335 pub q_format: bool,
336 /// Optional heading level `1..=9`, emitted as style `outlineLvl`.
337 pub heading_level: Option<u8>,
338 /// Default paragraph alignment.
339 pub align: Align,
340 /// Default paragraph spacing.
341 pub spacing: Spacing,
342 /// Default paragraph indentation.
343 pub indent: Indent,
344 /// Default paragraph background shading.
345 pub shading: Option<Color>,
346 /// Default character properties for runs using this style.
347 pub run: CharProps,
348}
349
350/// A comment to author on a generated run.
351#[derive(Debug, Clone, Default, PartialEq, Eq)]
352pub struct AuthoredComment {
353 /// Comment body text.
354 pub text: String,
355 /// Comment author, if any.
356 pub author: Option<String>,
357 /// Author initials, if any.
358 pub initials: Option<String>,
359 /// Comment timestamp, if any.
360 pub date: Option<String>,
361 /// Parent comment id for authored replies, if any.
362 pub parent_comment_id: Option<String>,
363}
364
365/// Tracked revision metadata to author on a generated run.
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct AuthoredRevision {
368 /// Revision kind. Generated `.docx` currently supports insertion/deletion.
369 pub kind: RevisionKind,
370 /// Revision author, if any.
371 pub author: Option<String>,
372 /// Revision timestamp, if any.
373 pub date: Option<String>,
374}
375
376/// Plain text content-control metadata to author on a generated run.
377#[derive(Debug, Clone, Default, PartialEq, Eq)]
378pub struct AuthoredContentControl {
379 /// Human-readable content-control title/alias, if any.
380 pub alias: Option<String>,
381 /// Machine-readable content-control tag, if any.
382 pub tag: Option<String>,
383 /// XPath for a data-bound content control, if any.
384 pub data_binding_xpath: Option<String>,
385 /// Custom XML store item ID for a data-bound content control, if any.
386 pub data_binding_store_item_id: Option<String>,
387}
388
389/// Footnote or endnote metadata to author after a generated run.
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct AuthoredNote {
392 /// Footnote or endnote.
393 pub kind: NoteKind,
394 /// Note body text.
395 pub text: String,
396}
397
398impl Default for AuthoredRevision {
399 fn default() -> Self {
400 Self {
401 kind: RevisionKind::Insertion,
402 author: None,
403 date: None,
404 }
405 }
406}
407
408/// List membership of a paragraph.
409#[derive(Debug, Clone, Default, PartialEq, Eq)]
410pub struct ListInfo {
411 /// 0-based nesting level.
412 pub level: u8,
413 /// `true` = numbered list, `false` = bullet list.
414 pub ordered: bool,
415 /// The rendered autonumber label (e.g. `1.`, `가.`) — used by the flat text
416 /// path; the Markdown/HTML exporters use native list syntax instead.
417 pub label: String,
418}
419
420/// An inline run of text with uniform character properties.
421#[derive(Debug, Clone, Default, PartialEq)]
422pub struct Run {
423 /// The run's text.
424 pub text: String,
425 /// Character formatting.
426 pub props: CharProps,
427 /// Field role (a hyperlink result carries its URL).
428 pub field: FieldRole,
429 /// Mark generated simple fields dirty so Word can refresh them.
430 pub field_dirty: bool,
431 /// Best-effort reason an imported field result remains cached, when known.
432 pub field_unsupported_reason: Option<FieldUnsupportedReason>,
433 /// An inline picture (the run's text is empty when this is set).
434 pub image: Option<Image>,
435 /// Authored comment anchored to this run.
436 pub comment: Option<AuthoredComment>,
437 /// Authored tracked insertion/deletion metadata for this run.
438 pub revision: Option<AuthoredRevision>,
439 /// Authored plain text content-control metadata for this run.
440 pub content_control: Option<AuthoredContentControl>,
441 /// Authored bookmark name wrapping this run.
442 pub bookmark: Option<String>,
443 /// Authored footnote/endnote anchored after this run.
444 pub note: Option<AuthoredNote>,
445}
446
447/// Best-effort reason an imported field remains cached in the model.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum FieldUnsupportedReason {
450 /// The field points at a bookmark/scope rwml could not resolve.
451 UnresolvedBookmark,
452 /// The instruction contains a switch whose value can change the result.
453 UnsupportedSwitch,
454 /// The instruction is supported, but the document contains no computable value.
455 NoComputedResult,
456}
457
458pub(crate) fn referenceable_bookmark_name(name: &str) -> bool {
459 !name.trim().is_empty()
460 && !name.starts_with('\\')
461 && !name.contains('"')
462 && !name.chars().any(char::is_whitespace)
463}
464
465pub(crate) fn normalize_field_instruction(instruction: &str) -> String {
466 instruction.split_whitespace().collect::<Vec<_>>().join(" ")
467}
468
469/// An sRGB color.
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
471pub struct Color {
472 /// Red channel.
473 pub r: u8,
474 /// Green channel.
475 pub g: u8,
476 /// Blue channel.
477 pub b: u8,
478}
479
480impl Color {
481 /// Construct from components.
482 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
483 Color { r, g, b }
484 }
485}
486
487/// A physical side of a table border.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum TableBorderSide {
490 /// Top border.
491 Top,
492 /// Left border.
493 Left,
494 /// Bottom border.
495 Bottom,
496 /// Right border.
497 Right,
498 /// Horizontal inside borders.
499 InsideHorizontal,
500 /// Vertical inside borders.
501 InsideVertical,
502}
503
504/// Uniform table border line style.
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
506pub enum TableBorderStyle {
507 /// Single solid line (`single`).
508 Single,
509 /// Dotted line (`dotted`).
510 Dotted,
511 /// Dashed line (`dashed`).
512 Dashed,
513 /// Double solid line (`double`).
514 Double,
515}
516
517#[cfg(feature = "docx")]
518impl TableBorderStyle {
519 pub(crate) fn wml_value(self) -> &'static str {
520 match self {
521 TableBorderStyle::Single => "single",
522 TableBorderStyle::Dotted => "dotted",
523 TableBorderStyle::Dashed => "dashed",
524 TableBorderStyle::Double => "double",
525 }
526 }
527
528 pub(crate) fn from_wml_value(value: &str) -> Option<Self> {
529 let value = value.trim();
530 match value {
531 "single" => Some(TableBorderStyle::Single),
532 "dotted" => Some(TableBorderStyle::Dotted),
533 "dashed" => Some(TableBorderStyle::Dashed),
534 "double" => Some(TableBorderStyle::Double),
535 _ => None,
536 }
537 }
538}
539
540/// Optional table border colors by physical side.
541#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
542pub struct TableBorderColors {
543 /// Top border color.
544 pub top: Option<Color>,
545 /// Left border color.
546 pub left: Option<Color>,
547 /// Bottom border color.
548 pub bottom: Option<Color>,
549 /// Right border color.
550 pub right: Option<Color>,
551 /// Horizontal inside border color.
552 pub inside_h: Option<Color>,
553 /// Vertical inside border color.
554 pub inside_v: Option<Color>,
555}
556
557impl TableBorderColors {
558 /// Return the color for a side, if one was set.
559 pub fn get(&self, side: TableBorderSide) -> Option<Color> {
560 match side {
561 TableBorderSide::Top => self.top,
562 TableBorderSide::Left => self.left,
563 TableBorderSide::Bottom => self.bottom,
564 TableBorderSide::Right => self.right,
565 TableBorderSide::InsideHorizontal => self.inside_h,
566 TableBorderSide::InsideVertical => self.inside_v,
567 }
568 }
569
570 /// Set the color for a side.
571 pub fn set(&mut self, side: TableBorderSide, color: Color) {
572 match side {
573 TableBorderSide::Top => self.top = Some(color),
574 TableBorderSide::Left => self.left = Some(color),
575 TableBorderSide::Bottom => self.bottom = Some(color),
576 TableBorderSide::Right => self.right = Some(color),
577 TableBorderSide::InsideHorizontal => self.inside_h = Some(color),
578 TableBorderSide::InsideVertical => self.inside_v = Some(color),
579 }
580 }
581}
582
583/// Optional table border widths by physical side, in eighths of a point.
584#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
585pub struct TableBorderSizes {
586 /// Top border width.
587 pub top: Option<u16>,
588 /// Left border width.
589 pub left: Option<u16>,
590 /// Bottom border width.
591 pub bottom: Option<u16>,
592 /// Right border width.
593 pub right: Option<u16>,
594 /// Horizontal inside border width.
595 pub inside_h: Option<u16>,
596 /// Vertical inside border width.
597 pub inside_v: Option<u16>,
598}
599
600impl TableBorderSizes {
601 /// Return the width for a side, if one was set.
602 pub fn get(&self, side: TableBorderSide) -> Option<u16> {
603 match side {
604 TableBorderSide::Top => self.top,
605 TableBorderSide::Left => self.left,
606 TableBorderSide::Bottom => self.bottom,
607 TableBorderSide::Right => self.right,
608 TableBorderSide::InsideHorizontal => self.inside_h,
609 TableBorderSide::InsideVertical => self.inside_v,
610 }
611 }
612
613 /// Set the width for a side.
614 pub fn set(&mut self, side: TableBorderSide, size: u16) {
615 let size = size.max(1);
616 match side {
617 TableBorderSide::Top => self.top = Some(size),
618 TableBorderSide::Left => self.left = Some(size),
619 TableBorderSide::Bottom => self.bottom = Some(size),
620 TableBorderSide::Right => self.right = Some(size),
621 TableBorderSide::InsideHorizontal => self.inside_h = Some(size),
622 TableBorderSide::InsideVertical => self.inside_v = Some(size),
623 }
624 }
625}
626
627/// Optional table border line styles by physical side.
628#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
629pub struct TableBorderStyles {
630 /// Top border style.
631 pub top: Option<TableBorderStyle>,
632 /// Left border style.
633 pub left: Option<TableBorderStyle>,
634 /// Bottom border style.
635 pub bottom: Option<TableBorderStyle>,
636 /// Right border style.
637 pub right: Option<TableBorderStyle>,
638 /// Horizontal inside border style.
639 pub inside_h: Option<TableBorderStyle>,
640 /// Vertical inside border style.
641 pub inside_v: Option<TableBorderStyle>,
642}
643
644impl TableBorderStyles {
645 /// Return the style for a side, if one was set.
646 pub fn get(&self, side: TableBorderSide) -> Option<TableBorderStyle> {
647 match side {
648 TableBorderSide::Top => self.top,
649 TableBorderSide::Left => self.left,
650 TableBorderSide::Bottom => self.bottom,
651 TableBorderSide::Right => self.right,
652 TableBorderSide::InsideHorizontal => self.inside_h,
653 TableBorderSide::InsideVertical => self.inside_v,
654 }
655 }
656
657 /// Set the style for a side.
658 pub fn set(&mut self, side: TableBorderSide, style: TableBorderStyle) {
659 match side {
660 TableBorderSide::Top => self.top = Some(style),
661 TableBorderSide::Left => self.left = Some(style),
662 TableBorderSide::Bottom => self.bottom = Some(style),
663 TableBorderSide::Right => self.right = Some(style),
664 TableBorderSide::InsideHorizontal => self.inside_h = Some(style),
665 TableBorderSide::InsideVertical => self.inside_v = Some(style),
666 }
667 }
668}
669
670/// Vertical alignment of a run (super/subscript).
671#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
672pub enum VertAlign {
673 /// On the baseline (normal).
674 #[default]
675 Baseline,
676 /// Superscript.
677 Super,
678 /// Subscript.
679 Sub,
680}
681
682/// Character-level formatting that affects rendering.
683///
684/// `font`/`color`/`highlight` make this non-`Copy`; clone where a `CharProps` is
685/// needed by value.
686#[derive(Debug, Clone, Default, PartialEq, Eq)]
687pub struct CharProps {
688 /// Bold.
689 pub bold: bool,
690 /// Italic.
691 pub italic: bool,
692 /// Underlined.
693 pub underline: bool,
694 /// Struck through.
695 pub strike: bool,
696 /// Hidden text (`fVanish`) — kept in the flat/Markdown text for index recall,
697 /// but the HTML exporter omits it.
698 pub hidden: bool,
699 /// Font family name (emitted to `w:rFonts` ascii+eastAsia), if known.
700 pub font: Option<String>,
701 /// Font size in half-points (Word unit; `24` = 12pt), if known.
702 pub size_half_pt: Option<u16>,
703 /// Text color, if known.
704 pub color: Option<Color>,
705 /// Highlight color name (`w:highlight`, e.g. `"yellow"`), if any.
706 pub highlight: Option<String>,
707 /// Super/subscript.
708 pub vert_align: VertAlign,
709 /// Small caps (`w:smallCaps`): lowercase letters render as small capitals.
710 pub small_caps: bool,
711 /// All caps (`w:caps`): text renders uppercased regardless of stored case.
712 pub caps: bool,
713 /// Right-to-left run text (`w:rtl`).
714 pub rtl: bool,
715}
716
717/// Whether a run is plain text or the cached result of a field.
718#[derive(Debug, Clone, Default, PartialEq, Eq)]
719pub enum FieldRole {
720 /// Plain text (not part of a field).
721 #[default]
722 None,
723 /// A hyperlink — `url` from the `HYPERLINK` field instruction.
724 Hyperlink {
725 /// The link target.
726 url: String,
727 },
728 /// A simple non-hyperlink field whose instruction is preserved.
729 Simple {
730 /// Normalized field instruction such as `PAGE`, `FILENAME \p`, or `REF Figure1`.
731 instruction: String,
732 },
733 /// Any other field (PAGE, REF, …) whose result text is kept verbatim.
734 Other,
735}
736
737/// Paragraph alignment.
738#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
739pub enum Align {
740 /// Left-aligned (the default).
741 #[default]
742 Left,
743 /// Centered.
744 Center,
745 /// Right-aligned.
746 Right,
747 /// Justified.
748 Justify,
749}
750
751/// A table: rows of cells.
752#[derive(Debug, Clone, Default, PartialEq)]
753pub struct Table {
754 /// The table's rows, top to bottom.
755 pub rows: Vec<Row>,
756 /// Number of leading header rows (repeated `sprmTTableHeader` rows).
757 pub header_rows: usize,
758 /// Column widths as fractions of the table width; empty = even split.
759 pub col_widths_pct: Vec<f32>,
760 /// Table width as a fraction of the available width, if set (`None` = auto).
761 pub width_pct: Option<f32>,
762 /// Use Word's fixed table layout algorithm instead of autofit.
763 pub fixed_layout: bool,
764 /// Table indentation in twips, if explicitly set.
765 pub indent_twips: Option<i32>,
766 /// Table alignment, if explicitly set.
767 pub align: Option<Align>,
768 /// Visual right-to-left table ordering (`w:bidiVisual`).
769 pub bidi_visual: bool,
770 /// Uniform table border color, if explicitly set.
771 pub border_color: Option<Color>,
772 /// Side-specific table border colors, overriding `border_color` per side.
773 pub border_colors: TableBorderColors,
774 /// Uniform table border width in eighths of a point, if explicitly set.
775 pub border_size_eighths: Option<u16>,
776 /// Side-specific table border widths, overriding `border_size_eighths` per side.
777 pub border_sizes: TableBorderSizes,
778 /// Uniform table border style, if explicitly set.
779 pub border_style: Option<TableBorderStyle>,
780 /// Side-specific table border styles, overriding `border_style` per side.
781 pub border_styles: TableBorderStyles,
782}
783
784impl Table {
785 /// `true` if every cell holds exactly one single-line paragraph and no cell
786 /// spans more than one row or column — i.e. a clean GFM pipe table is lossless.
787 pub fn is_simple_grid(&self) -> bool {
788 self.rows.iter().all(|row| {
789 row.cells.iter().all(|c| {
790 c.row_span <= 1
791 && c.col_span <= 1
792 && c.blocks.len() <= 1
793 && c.blocks.iter().all(|b| match b {
794 Block::Paragraph(p) => !p.text().contains('\n'),
795 _ => false,
796 })
797 })
798 })
799 }
800}
801
802/// A table row.
803#[derive(Debug, Clone, Default, PartialEq)]
804pub struct Row {
805 /// The row's cells, left to right.
806 pub cells: Vec<Cell>,
807}
808
809/// Vertical alignment of cell content.
810#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
811pub enum VCell {
812 /// Top (the default).
813 #[default]
814 Top,
815 /// Vertically centered.
816 Center,
817 /// Bottom.
818 Bottom,
819}
820
821/// Table-cell margins in twips (1/20 point), ordered by physical side.
822#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
823pub struct CellMargins {
824 /// Top margin in twips.
825 pub top: u32,
826 /// Right margin in twips.
827 pub right: u32,
828 /// Bottom margin in twips.
829 pub bottom: u32,
830 /// Left margin in twips.
831 pub left: u32,
832}
833
834/// A table cell — may hold block content and span rows/columns.
835#[derive(Debug, Clone, PartialEq)]
836pub struct Cell {
837 /// Block-level content of the cell.
838 pub blocks: Vec<Block>,
839 /// Number of rows this cell spans (1 = no vertical merge).
840 pub row_span: u16,
841 /// Number of columns this cell spans (1 = no horizontal merge).
842 pub col_span: u16,
843 /// Whether this is a header cell (`<th>`).
844 pub is_header: bool,
845 /// Cell background shading, if any.
846 pub shading: Option<Color>,
847 /// Vertical alignment of the cell content.
848 pub valign: VCell,
849 /// Cell width as a fraction of the table width, if set (`None` = auto).
850 pub width_pct: Option<f32>,
851 /// Per-cell margins in twips, if explicitly set.
852 pub margins: Option<CellMargins>,
853}
854
855impl Default for Cell {
856 fn default() -> Self {
857 Cell {
858 blocks: Vec::new(),
859 row_span: 1,
860 col_span: 1,
861 is_header: false,
862 shading: None,
863 valign: VCell::Top,
864 width_pct: None,
865 margins: None,
866 }
867 }
868}
869
870impl Cell {
871 /// The cell's text, paragraphs joined by newlines.
872 pub fn text(&self) -> String {
873 self.blocks
874 .iter()
875 .filter_map(|b| match b {
876 Block::Paragraph(p) => Some(p.text()),
877 _ => None,
878 })
879 .collect::<Vec<_>>()
880 .join("\n")
881 }
882}
883
884/// An embedded image. `bytes`/`mime` are present only when the picture was
885/// extracted; otherwise the node is a placeholder.
886#[derive(Debug, Clone, Default, PartialEq)]
887pub struct Image {
888 /// Alt text, if any.
889 pub alt: Option<String>,
890 /// Raw image bytes, when extracted.
891 pub bytes: Option<Vec<u8>>,
892 /// MIME type of `bytes` (e.g. `image/png`), when known.
893 pub mime: Option<String>,
894 /// Intrinsic width in pixels, parsed from the image header, when known.
895 pub width_px: Option<u32>,
896 /// Intrinsic height in pixels, parsed from the image header, when known.
897 pub height_px: Option<u32>,
898 /// Clockwise rotation in whole degrees.
899 pub rotation_degrees: Option<i32>,
900 /// Page-relative floating anchor offset in EMUs, when authoring `wp:anchor`.
901 pub floating_offset_emu: Option<(i64, i64)>,
902}
903
904/// Supported chart layouts for authored `.docx` output.
905#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
906pub enum ChartKind {
907 /// A clustered horizontal bar chart.
908 #[default]
909 Bar,
910 /// A stacked horizontal bar chart.
911 StackedBar,
912 /// A 100% stacked horizontal bar chart.
913 PercentStackedBar,
914 /// A clustered 3-D horizontal bar chart.
915 Bar3D,
916 /// A stacked 3-D horizontal bar chart.
917 StackedBar3D,
918 /// A 100% stacked 3-D horizontal bar chart.
919 PercentStackedBar3D,
920 /// A clustered vertical column chart.
921 Column,
922 /// A stacked vertical column chart.
923 StackedColumn,
924 /// A 100% stacked vertical column chart.
925 PercentStackedColumn,
926 /// A clustered 3-D vertical column chart.
927 Column3D,
928 /// A stacked 3-D vertical column chart.
929 StackedColumn3D,
930 /// A 100% stacked 3-D vertical column chart.
931 PercentStackedColumn3D,
932 /// A line chart with category labels on the horizontal axis.
933 Line,
934 /// A line chart without point markers.
935 LineNoMarkers,
936 /// A smoothed line chart with category labels on the horizontal axis.
937 SmoothLine,
938 /// A stacked line chart with category labels on the horizontal axis.
939 StackedLine,
940 /// A 100% stacked line chart with category labels on the horizontal axis.
941 PercentStackedLine,
942 /// A 3-D line chart with category labels on the horizontal axis.
943 Line3D,
944 /// An area chart with category labels on the horizontal axis.
945 Area,
946 /// A stacked area chart with category labels on the horizontal axis.
947 StackedArea,
948 /// A 100% stacked area chart with category labels on the horizontal axis.
949 PercentStackedArea,
950 /// A 3-D area chart with category labels on the horizontal axis.
951 Area3D,
952 /// A stacked 3-D area chart with category labels on the horizontal axis.
953 StackedArea3D,
954 /// A 100% stacked 3-D area chart with category labels on the horizontal axis.
955 PercentStackedArea3D,
956 /// A radar chart with category labels around a radial axis.
957 Radar,
958 /// A radar chart with explicit point markers.
959 RadarWithMarkers,
960 /// A filled radar chart with category labels around a radial axis.
961 FilledRadar,
962 /// A scatter chart with numeric horizontal and vertical values.
963 Scatter,
964 /// A marker-only scatter chart with numeric horizontal and vertical values.
965 ScatterMarkers,
966 /// A straight-line scatter chart without point markers.
967 ScatterLines,
968 /// A smoothed scatter chart with point markers.
969 ScatterSmooth,
970 /// A smoothed scatter chart without point markers.
971 ScatterSmoothNoMarkers,
972 /// A bubble chart with numeric horizontal values, vertical values, and sizes.
973 Bubble,
974 /// A 3-D bubble chart with numeric horizontal values, vertical values, and sizes.
975 Bubble3D,
976 /// A pie chart using the first series as slice values.
977 Pie,
978 /// An exploded pie chart using the first series as slice values.
979 ExplodedPie,
980 /// A 3-D pie chart using the first series as slice values.
981 Pie3D,
982 /// An exploded 3-D pie chart using the first series as slice values.
983 ExplodedPie3D,
984 /// A pie-of-pie chart using the first series as slice values.
985 PieOfPie,
986 /// A bar-of-pie chart using the first series as slice values.
987 BarOfPie,
988 /// A doughnut chart using the first series as slice values.
989 Doughnut,
990 /// An exploded doughnut chart using the first series as slice values.
991 ExplodedDoughnut,
992 /// A surface chart using category columns and series rows as a value grid.
993 Surface,
994 /// A 3-D surface chart using category columns and series rows as a value grid.
995 Surface3D,
996 /// A high-low-close stock chart using date/category labels.
997 StockHighLowClose,
998 /// A stock chart using date/category labels and open/high/low/close-style series.
999 Stock,
1000 /// A waterfall chart using the first series as category deltas.
1001 Waterfall,
1002 /// A treemap chart using the first series as rectangle sizes.
1003 Treemap,
1004 /// A sunburst chart using the first series as radial slice sizes.
1005 Sunburst,
1006 /// A histogram chart using category bins and the first series as bin counts.
1007 Histogram,
1008 /// A box-and-whisker chart using the first series as distribution values.
1009 BoxWhisker,
1010 /// A funnel chart using the first series as staged values.
1011 Funnel,
1012}
1013
1014/// Supported shape styles for authored 3-D bar/column charts.
1015#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1016pub enum ChartShape {
1017 /// Rectangular 3-D boxes.
1018 #[default]
1019 Box,
1020 /// Cylindrical 3-D bars or columns.
1021 Cylinder,
1022 /// Cone-shaped 3-D bars or columns.
1023 Cone,
1024 /// Cone-shaped 3-D bars or columns scaled to the maximum value.
1025 ConeToMax,
1026 /// Pyramid-shaped 3-D bars or columns.
1027 Pyramid,
1028 /// Pyramid-shaped 3-D bars or columns scaled to the maximum value.
1029 PyramidToMax,
1030}
1031
1032/// One named chart series with literal numeric values.
1033#[derive(Debug, Clone, Default, PartialEq)]
1034pub struct ChartSeries {
1035 /// Series display name.
1036 pub name: String,
1037 /// Values aligned with [`Chart::categories`].
1038 pub values: Vec<f64>,
1039 /// Optional bubble sizes aligned with [`ChartSeries::values`].
1040 pub bubble_sizes: Vec<f64>,
1041}
1042
1043/// A block-level chart with literal category and numeric caches.
1044#[derive(Debug, Clone, Default, PartialEq)]
1045pub struct Chart {
1046 /// Chart layout.
1047 pub kind: ChartKind,
1048 /// Optional display title.
1049 pub title: Option<String>,
1050 /// Category labels shared by all series.
1051 pub categories: Vec<String>,
1052 /// Named numeric series.
1053 pub series: Vec<ChartSeries>,
1054 /// Drawing width in pixels, interpreted at 96 dpi.
1055 pub width_px: Option<u32>,
1056 /// Drawing height in pixels, interpreted at 96 dpi.
1057 pub height_px: Option<u32>,
1058 /// Alternate text for the chart drawing.
1059 pub alt: Option<String>,
1060 /// Render surface-family charts as wireframes instead of filled surfaces.
1061 pub wireframe: bool,
1062 /// Shape style for 3-D bar and 3-D column charts.
1063 pub shape: ChartShape,
1064}
1065
1066/// Page geometry, in points. Default is A4 portrait with 1-inch margins.
1067#[derive(Debug, Clone, Copy, PartialEq)]
1068pub struct PageSetup {
1069 /// Page width.
1070 pub width_pt: f32,
1071 /// Page height.
1072 pub height_pt: f32,
1073 /// Uniform margin fallback (used for any side without an explicit override).
1074 pub margin_pt: f32,
1075 /// Left margin override (points); falls back to `margin_pt`. Per-side overrides
1076 /// let asymmetric layouts (a wide left sidebar, a binding gutter) render with
1077 /// the correct content box instead of a forced-uniform margin.
1078 pub margin_left_pt: Option<f32>,
1079 /// Right margin override (points); falls back to `margin_pt`.
1080 pub margin_right_pt: Option<f32>,
1081 /// Top margin override (points); falls back to `margin_pt`.
1082 pub margin_top_pt: Option<f32>,
1083 /// Bottom margin override (points); falls back to `margin_pt`.
1084 pub margin_bottom_pt: Option<f32>,
1085 /// Landscape orientation (swaps width/height semantics on emit).
1086 pub landscape: bool,
1087}
1088
1089impl PageSetup {
1090 /// Left margin (override or uniform fallback).
1091 pub fn left(&self) -> f32 {
1092 self.margin_left_pt.unwrap_or(self.margin_pt)
1093 }
1094 /// Right margin (override or uniform fallback).
1095 pub fn right(&self) -> f32 {
1096 self.margin_right_pt.unwrap_or(self.margin_pt)
1097 }
1098 /// Top margin (override or uniform fallback).
1099 pub fn top(&self) -> f32 {
1100 self.margin_top_pt.unwrap_or(self.margin_pt)
1101 }
1102 /// Bottom margin (override or uniform fallback).
1103 pub fn bottom(&self) -> f32 {
1104 self.margin_bottom_pt.unwrap_or(self.margin_pt)
1105 }
1106}
1107
1108impl Default for PageSetup {
1109 fn default() -> Self {
1110 // A4 = 210×297mm = 595.3×841.9pt; 1in = 72pt margins.
1111 PageSetup {
1112 width_pt: 595.3,
1113 height_pt: 841.9,
1114 margin_pt: 72.0,
1115 margin_left_pt: None,
1116 margin_right_pt: None,
1117 margin_top_pt: None,
1118 margin_bottom_pt: None,
1119 landscape: false,
1120 }
1121 }
1122}
1123
1124/// Display format for generated section page numbers.
1125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1126pub enum PageNumberFormat {
1127 /// Decimal numbers (`1`, `2`, `3`).
1128 Decimal,
1129 /// Zero-padded decimal numbers (`01`, `02`, `03`).
1130 DecimalZero,
1131 /// Decimal numbers surrounded by dashes (`- 1 -`, `- 2 -`, `- 3 -`).
1132 NumberInDash,
1133 /// Full-width decimal digits (`1`, `2`, `3`).
1134 DecimalFullWidth,
1135 /// Half-width decimal digits (`1`, `2`, `3`).
1136 DecimalHalfWidth,
1137 /// Alternate full-width decimal digits.
1138 DecimalFullWidth2,
1139 /// Circled decimal digits for one through twenty, then decimal fallback.
1140 DecimalEnclosedCircle,
1141 /// Decimal digits followed by full-stop glyphs for one through twenty.
1142 DecimalEnclosedFullstop,
1143 /// Parenthesized decimal digits for one through twenty.
1144 DecimalEnclosedParen,
1145 /// Korean Ganada sequence.
1146 Ganada,
1147 /// Korean Chosung sequence.
1148 Chosung,
1149 /// Korean digital numerals.
1150 KoreanDigital,
1151 /// Native Korean counting words.
1152 KoreanCounting,
1153 /// Korean legal numerals.
1154 KoreanLegal,
1155 /// Alternate Korean digital numerals.
1156 KoreanDigital2,
1157 /// Lowercase letters (`a`, `b`, `c`).
1158 LowerLetter,
1159 /// Uppercase letters (`A`, `B`, `C`).
1160 UpperLetter,
1161 /// Lowercase Roman numerals (`i`, `ii`, `iii`).
1162 LowerRoman,
1163 /// Uppercase Roman numerals (`I`, `II`, `III`).
1164 UpperRoman,
1165 /// Ordinal decimal numbers (`1st`, `2nd`, `3rd`).
1166 Ordinal,
1167 /// Cardinal text (`one`, `two`, `three`).
1168 CardinalText,
1169 /// Ordinal text (`first`, `second`, `third`).
1170 OrdinalText,
1171}
1172
1173#[cfg(feature = "docx")]
1174impl PageNumberFormat {
1175 pub(crate) fn wml_value(self) -> &'static str {
1176 match self {
1177 PageNumberFormat::Decimal => "decimal",
1178 PageNumberFormat::DecimalZero => "decimalZero",
1179 PageNumberFormat::NumberInDash => "numberInDash",
1180 PageNumberFormat::DecimalFullWidth => "decimalFullWidth",
1181 PageNumberFormat::DecimalHalfWidth => "decimalHalfWidth",
1182 PageNumberFormat::DecimalFullWidth2 => "decimalFullWidth2",
1183 PageNumberFormat::DecimalEnclosedCircle => "decimalEnclosedCircle",
1184 PageNumberFormat::DecimalEnclosedFullstop => "decimalEnclosedFullstop",
1185 PageNumberFormat::DecimalEnclosedParen => "decimalEnclosedParen",
1186 PageNumberFormat::Ganada => "ganada",
1187 PageNumberFormat::Chosung => "chosung",
1188 PageNumberFormat::KoreanDigital => "koreanDigital",
1189 PageNumberFormat::KoreanCounting => "koreanCounting",
1190 PageNumberFormat::KoreanLegal => "koreanLegal",
1191 PageNumberFormat::KoreanDigital2 => "koreanDigital2",
1192 PageNumberFormat::LowerLetter => "lowerLetter",
1193 PageNumberFormat::UpperLetter => "upperLetter",
1194 PageNumberFormat::LowerRoman => "lowerRoman",
1195 PageNumberFormat::UpperRoman => "upperRoman",
1196 PageNumberFormat::Ordinal => "ordinal",
1197 PageNumberFormat::CardinalText => "cardinalText",
1198 PageNumberFormat::OrdinalText => "ordinalText",
1199 }
1200 }
1201
1202 pub(crate) fn from_wml_value(value: &str) -> Option<Self> {
1203 let value = value.trim();
1204 match value {
1205 "decimal" => Some(PageNumberFormat::Decimal),
1206 "decimalZero" => Some(PageNumberFormat::DecimalZero),
1207 "numberInDash" => Some(PageNumberFormat::NumberInDash),
1208 "decimalFullWidth" => Some(PageNumberFormat::DecimalFullWidth),
1209 "decimalHalfWidth" => Some(PageNumberFormat::DecimalHalfWidth),
1210 "decimalFullWidth2" => Some(PageNumberFormat::DecimalFullWidth2),
1211 "decimalEnclosedCircle" => Some(PageNumberFormat::DecimalEnclosedCircle),
1212 "decimalEnclosedFullstop" => Some(PageNumberFormat::DecimalEnclosedFullstop),
1213 "decimalEnclosedParen" => Some(PageNumberFormat::DecimalEnclosedParen),
1214 "ganada" => Some(PageNumberFormat::Ganada),
1215 "chosung" => Some(PageNumberFormat::Chosung),
1216 "koreanDigital" => Some(PageNumberFormat::KoreanDigital),
1217 "koreanCounting" => Some(PageNumberFormat::KoreanCounting),
1218 "koreanLegal" => Some(PageNumberFormat::KoreanLegal),
1219 "koreanDigital2" => Some(PageNumberFormat::KoreanDigital2),
1220 "lowerLetter" => Some(PageNumberFormat::LowerLetter),
1221 "upperLetter" => Some(PageNumberFormat::UpperLetter),
1222 "lowerRoman" => Some(PageNumberFormat::LowerRoman),
1223 "upperRoman" => Some(PageNumberFormat::UpperRoman),
1224 "ordinal" => Some(PageNumberFormat::Ordinal),
1225 "cardinalText" => Some(PageNumberFormat::CardinalText),
1226 "ordinalText" => Some(PageNumberFormat::OrdinalText),
1227 _ => None,
1228 }
1229 }
1230}
1231
1232/// Text flow direction for generated `.docx` section properties.
1233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1234pub enum TextDirection {
1235 /// Left-to-right lines flowing from top to bottom (`lrTb`).
1236 LeftToRightTopToBottom,
1237 /// Top-to-bottom columns flowing from right to left (`tbRl`).
1238 TopToBottomRightToLeft,
1239 /// Bottom-to-top columns flowing from left to right (`btLr`).
1240 BottomToTopLeftToRight,
1241 /// Vertically oriented left-to-right text flowing from top to bottom (`lrTbV`).
1242 LeftToRightTopToBottomVertical,
1243 /// Vertically oriented top-to-bottom text flowing from right to left (`tbRlV`).
1244 TopToBottomRightToLeftVertical,
1245 /// Vertically oriented top-to-bottom text flowing from left to right (`tbLrV`).
1246 TopToBottomLeftToRightVertical,
1247}
1248
1249#[cfg(feature = "docx")]
1250impl TextDirection {
1251 pub(crate) fn wml_value(self) -> &'static str {
1252 match self {
1253 TextDirection::LeftToRightTopToBottom => "lrTb",
1254 TextDirection::TopToBottomRightToLeft => "tbRl",
1255 TextDirection::BottomToTopLeftToRight => "btLr",
1256 TextDirection::LeftToRightTopToBottomVertical => "lrTbV",
1257 TextDirection::TopToBottomRightToLeftVertical => "tbRlV",
1258 TextDirection::TopToBottomLeftToRightVertical => "tbLrV",
1259 }
1260 }
1261
1262 pub(crate) fn from_wml_value(value: &str) -> Option<Self> {
1263 let value = value.trim();
1264 match value {
1265 "lrTb" => Some(TextDirection::LeftToRightTopToBottom),
1266 "tbRl" => Some(TextDirection::TopToBottomRightToLeft),
1267 "btLr" => Some(TextDirection::BottomToTopLeftToRight),
1268 "lrTbV" => Some(TextDirection::LeftToRightTopToBottomVertical),
1269 "tbRlV" => Some(TextDirection::TopToBottomRightToLeftVertical),
1270 "tbLrV" => Some(TextDirection::TopToBottomLeftToRightVertical),
1271 _ => None,
1272 }
1273 }
1274}
1275
1276/// Document grid behavior for a WordprocessingML section.
1277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1278pub enum DocGridType {
1279 /// No document grid (`default`).
1280 Default,
1281 /// Line grid only (`lines`).
1282 Lines,
1283 /// Line and character grid (`linesAndChars`).
1284 LinesAndChars,
1285 /// Character grid only (`snapToChars`).
1286 SnapToChars,
1287}
1288
1289#[cfg(feature = "docx")]
1290impl DocGridType {
1291 pub(crate) fn wml_value(self) -> &'static str {
1292 match self {
1293 DocGridType::Default => "default",
1294 DocGridType::Lines => "lines",
1295 DocGridType::LinesAndChars => "linesAndChars",
1296 DocGridType::SnapToChars => "snapToChars",
1297 }
1298 }
1299
1300 pub(crate) fn from_wml_value(value: &str) -> Option<Self> {
1301 let value = value.trim();
1302 match value {
1303 "default" => Some(DocGridType::Default),
1304 "lines" => Some(DocGridType::Lines),
1305 "linesAndChars" => Some(DocGridType::LinesAndChars),
1306 "snapToChars" => Some(DocGridType::SnapToChars),
1307 _ => None,
1308 }
1309 }
1310}
1311
1312/// Document grid settings for a WordprocessingML section.
1313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1314pub struct DocGrid {
1315 /// Grid behavior.
1316 pub grid_type: DocGridType,
1317 /// Grid line pitch (`w:linePitch`) in twentieths of a point, if set.
1318 pub line_pitch: Option<u32>,
1319 /// Grid character pitch adjustment (`w:charSpace`), if set.
1320 pub character_space: Option<u32>,
1321}
1322
1323/// Section-level layout recovered from or generated into `.docx` section
1324/// properties.
1325///
1326/// In WordprocessingML a section property block closes the section that came
1327/// before it. `Block::SectionBreak(setup)` follows that convention: the stored
1328/// setup describes the section ending at that block, while `DocModel::setup`
1329/// describes the final section.
1330#[derive(Debug, Clone, Default, PartialEq)]
1331pub struct SectionSetup {
1332 /// Section-break start behavior, if this setup belongs to a section boundary.
1333 pub section_break: Option<SectionBreakKind>,
1334 /// Page geometry.
1335 pub page: PageSetup,
1336 /// Running header content (empty = none).
1337 pub header: Vec<Block>,
1338 /// First-page running header content (empty = none or default applies).
1339 pub first_header: Vec<Block>,
1340 /// Even-page running header content (empty = none or default applies).
1341 pub even_header: Vec<Block>,
1342 /// Running footer content (empty = none).
1343 pub footer: Vec<Block>,
1344 /// First-page running footer content (empty = none or default applies).
1345 pub first_footer: Vec<Block>,
1346 /// Even-page running footer content (empty = none or default applies).
1347 pub even_footer: Vec<Block>,
1348 /// Use distinct first-page section behavior (`w:titlePg`).
1349 pub title_page: bool,
1350 /// Emit a centered page number (`PAGE` field) in the footer.
1351 pub page_numbers: bool,
1352 /// Display page number to start this section at, if explicitly set.
1353 pub page_number_start: Option<u32>,
1354 /// Display page-number format for this section, if explicitly set.
1355 pub page_number_format: Option<PageNumberFormat>,
1356 /// Number of text columns in this section, if explicitly set.
1357 pub columns: Option<u16>,
1358 /// Text flow direction for this section, if explicitly set.
1359 pub text_direction: Option<TextDirection>,
1360 /// Document grid settings for this section, if explicitly set.
1361 pub doc_grid: Option<DocGrid>,
1362}
1363
1364/// Document-level layout + metadata, for authoring and rendering. All fields are
1365/// optional/default so existing read paths are unaffected.
1366#[derive(Debug, Clone, Default, PartialEq)]
1367pub struct DocSetup {
1368 /// Page geometry.
1369 pub page: PageSetup,
1370 /// Paragraph style definitions for generated `.docx` output.
1371 pub styles: Vec<ParagraphStyle>,
1372 /// Running header content (empty = none).
1373 pub header: Vec<Block>,
1374 /// First-page running header content (empty = none or default applies).
1375 pub first_header: Vec<Block>,
1376 /// Even-page running header content (empty = none or default applies).
1377 pub even_header: Vec<Block>,
1378 /// Running footer content (empty = none).
1379 pub footer: Vec<Block>,
1380 /// First-page running footer content (empty = none or default applies).
1381 pub first_footer: Vec<Block>,
1382 /// Even-page running footer content (empty = none or default applies).
1383 pub even_footer: Vec<Block>,
1384 /// Use distinct first-page section behavior (`w:titlePg`).
1385 pub title_page: bool,
1386 /// Emit a centered page number (`PAGE` field) in the footer.
1387 pub page_numbers: bool,
1388 /// Display page number to start the final/current section at, if explicitly set.
1389 pub page_number_start: Option<u32>,
1390 /// Display page-number format for the final/current section, if explicitly set.
1391 pub page_number_format: Option<PageNumberFormat>,
1392 /// Number of text columns in the final/current section, if explicitly set.
1393 pub columns: Option<u16>,
1394 /// Text flow direction for the final/current section, if explicitly set.
1395 pub text_direction: Option<TextDirection>,
1396 /// Document grid settings for the final/current section, if explicitly set.
1397 pub doc_grid: Option<DocGrid>,
1398 /// Optional document identifier emitted to `word/settings.xml` as `w14:docId`.
1399 pub document_id: Option<String>,
1400 /// Authored Office web-extension task panes.
1401 pub web_extension_task_panes: Vec<WebExtensionTaskPane>,
1402 /// Document title metadata.
1403 pub title: Option<String>,
1404 /// Document subject metadata.
1405 pub subject: Option<String>,
1406 /// Document author metadata.
1407 pub creator: Option<String>,
1408 /// Document description metadata.
1409 pub description: Option<String>,
1410 /// Document keywords metadata.
1411 pub keywords: Option<String>,
1412 /// Document category metadata.
1413 pub category: Option<String>,
1414 /// Document content-status metadata.
1415 pub content_status: Option<String>,
1416 /// Document last-modified-by metadata.
1417 pub last_modified_by: Option<String>,
1418 /// Document creation timestamp metadata.
1419 pub created: Option<String>,
1420 /// Document last-modified timestamp metadata.
1421 pub modified: Option<String>,
1422 /// Document last-printed timestamp metadata.
1423 pub last_printed: Option<String>,
1424 /// Document revision-count metadata.
1425 pub revision: Option<String>,
1426 /// Document version metadata.
1427 pub version: Option<String>,
1428}
1429
1430impl From<&DocSetup> for SectionSetup {
1431 fn from(setup: &DocSetup) -> Self {
1432 SectionSetup {
1433 section_break: None,
1434 page: setup.page,
1435 header: setup.header.clone(),
1436 first_header: setup.first_header.clone(),
1437 even_header: setup.even_header.clone(),
1438 footer: setup.footer.clone(),
1439 first_footer: setup.first_footer.clone(),
1440 even_footer: setup.even_footer.clone(),
1441 title_page: setup.title_page,
1442 page_numbers: setup.page_numbers,
1443 page_number_start: setup.page_number_start,
1444 page_number_format: setup.page_number_format,
1445 columns: setup.columns,
1446 text_direction: setup.text_direction,
1447 doc_grid: setup.doc_grid,
1448 }
1449 }
1450}
1451
1452#[cfg(all(test, feature = "docx"))]
1453mod tests {
1454 use super::{DocGridType, PageNumberFormat, SectionBreakKind, TableBorderStyle, TextDirection};
1455
1456 #[test]
1457 fn wml_enum_parsers_trim_ooxml_values() {
1458 assert_eq!(
1459 SectionBreakKind::from_wml_value(" evenPage "),
1460 Some(SectionBreakKind::EvenPage)
1461 );
1462 assert_eq!(
1463 TableBorderStyle::from_wml_value(" dotted "),
1464 Some(TableBorderStyle::Dotted)
1465 );
1466 assert_eq!(
1467 PageNumberFormat::from_wml_value(" lowerRoman "),
1468 Some(PageNumberFormat::LowerRoman)
1469 );
1470 assert_eq!(
1471 TextDirection::from_wml_value(" tbRl "),
1472 Some(TextDirection::TopToBottomRightToLeft)
1473 );
1474 assert_eq!(
1475 DocGridType::from_wml_value(" linesAndChars "),
1476 Some(DocGridType::LinesAndChars)
1477 );
1478 }
1479}