use std::collections::HashMap;
use std::rc::Rc;
use super::props::{Border, FramePr, Jc, Leader, LineRule, Look, Tab, TabKind, VertAlign, Width};
#[derive(Clone, Debug, PartialEq)]
pub struct RunStyle {
pub ascii: String,
pub h_ansi: String,
pub east_asia: String,
pub cs: String,
pub east_asia_unset: bool,
pub hint_east_asia: bool,
pub size: f64,
pub bold: bool,
pub italic: bool,
pub caps: bool,
pub small_caps: bool,
pub strike: bool,
pub dstrike: bool,
pub hidden: bool,
pub underline: Option<String>,
pub color: Option<u32>,
pub highlight: Option<u32>,
pub shading: Option<u32>,
pub vert_align: VertAlign,
pub position: f64,
pub spacing: f64,
pub scale: f64,
pub kern: f64,
pub ligatures: u8,
pub em: Option<String>,
pub toc: bool,
pub lang: String,
pub style_id: Option<String>,
pub direct: Raw,
}
pub type Run = Rc<RunStyle>;
#[derive(Clone, Debug, Default)]
pub struct Raw(pub Option<Rc<str>>);
impl PartialEq for Raw {
fn eq(&self, _: &Raw) -> bool {
true
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ParaStyle {
pub style_id: Option<String>,
pub jc: Jc,
pub ind_left: f64,
pub ind_right: f64,
pub ind_first: f64,
pub space_before: f64,
pub space_after: f64,
pub before_auto: bool,
pub after_auto: bool,
pub line: f64,
pub line_rule: LineRule,
pub contextual: bool,
pub keep_next: bool,
pub keep_lines: bool,
pub page_break_before: bool,
pub widow_control: bool,
pub suppress_auto_hyphens: bool,
pub suppress_line_numbers: bool,
pub tabs: Vec<Tab>,
pub border_top: Option<Border>,
pub border_bottom: Option<Border>,
pub border_left: Option<Border>,
pub border_right: Option<Border>,
pub border_between: Option<Border>,
pub shading: Option<u32>,
pub frame: Option<FramePr>,
pub text_alignment: String,
pub outline_lvl: Option<i64>,
pub list_jc: String,
pub num: Option<(i64, i64)>,
pub list_fmt: Option<String>,
pub direct: Raw,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BreakKind {
Line,
Page,
Column,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NoteKind {
Foot,
End,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FieldId(pub u64);
impl FieldId {
pub fn next() -> FieldId {
FieldId(next_para_id())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ElementId {
Field(FieldId),
Control(ControlId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FieldView {
#[default]
Result,
Code,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FieldKind {
Page,
NumPages,
SectionPages,
Section,
PageRef(String),
Ref(String),
Seq(String),
StyleRef(String),
Date,
If,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Picture {
pub w: f64,
pub h: f64,
pub image: Option<usize>,
pub alt: Rc<str>,
pub lock_aspect: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Float {
pub id: u64,
pub w: f64,
pub auto_w: bool,
pub h: f64,
pub image: Option<usize>,
pub alt: Rc<str>,
pub lock_aspect: bool,
pub text_box: Option<TextBox>,
pub h_relative: String,
pub h_align: Option<String>,
pub h_offset: f64,
pub v_relative: String,
pub v_align: Option<String>,
pub v_offset: f64,
pub wrap: String,
pub wrap_text: String,
pub dist: [f64; 4],
pub behind: bool,
pub allow_overlap: bool,
pub layout_in_cell: bool,
pub order: usize,
pub src: Option<std::ops::Range<usize>>,
pub locked: bool,
pub frame_src: Option<std::ops::Range<usize>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextBox {
pub blocks: Vec<Block>,
pub insets: [f64; 4],
pub outline: f64,
pub auto_fit: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Inline {
Text(String, Run),
Tab(Run),
PTab(PTab, Run),
Break(BreakKind, Run),
Sym {
ch: char,
font: String,
run: Run,
control: Option<ControlId>,
},
Picture(Picture, Run),
Float(Box<Float>, Run),
NoteRef {
kind: NoteKind,
id: i64,
custom: bool,
run: Run,
},
NoteMark(NoteKind, i64, Run),
Separator {
continuation: bool,
run: Run,
},
FieldBegin {
id: FieldId,
kind: Option<FieldKind>,
lock: bool,
dirty: bool,
view: FieldView,
label: Option<Rc<str>>,
value: Option<String>,
run: Run,
},
FieldSep {
id: FieldId,
run: Run,
},
FieldEnd {
id: FieldId,
run: Run,
},
Bookmark(String),
ControlStart(ControlId),
ControlEnd(ControlId),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ListLabel {
pub text: String,
pub run: Run,
pub jc: String,
pub suff: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Paragraph {
pub style: Rc<ParaStyle>,
pub mark: Run,
pub list: Option<ListLabel>,
pub inlines: Vec<Inline>,
pub src: Option<Rc<ParaSrc>>,
pub id: u64,
pub controls: Vec<ControlId>,
}
pub fn next_para_id() -> u64 {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
#[derive(Debug, PartialEq)]
pub struct ParaSrc {
pub ranges: Vec<std::ops::Range<usize>>,
pub segs: Vec<(std::ops::Range<usize>, usize)>,
pub lossy: bool,
}
pub struct Source {
pub package: Vec<u8>,
pub main: String,
pub xml: String,
pub blocks: Vec<Vec<Block>>,
pub stories: HashMap<String, (String, Vec<Block>)>,
pub image_parts: Vec<String>,
pub notes: Vec<(String, String, bool)>,
pub footnotes: HashMap<i64, Vec<Block>>,
pub endnotes: HashMap<i64, Vec<Block>>,
pub controls: HashMap<ControlId, Control>,
}
impl std::fmt::Debug for Source {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Source({}, {} bytes)", self.main, self.xml.len())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DataPart {
pub namespace: Option<String>,
pub item_id: String,
pub xml: String,
pub src: Option<String>,
pub changed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ControlId(pub u64);
impl ControlId {
pub fn next() -> ControlId {
ControlId(next_para_id())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Control {
pub props: ControlProps,
pub open: String,
pub close: String,
pub read: Option<ControlProps>,
pub at: Option<(usize, usize)>,
}
impl Control {
pub fn new(props: ControlProps) -> Control {
Control { props, open: String::new(), close: String::new(), read: None, at: None }
}
pub fn as_read(&self) -> bool {
!self.open.is_empty() && self.read.as_ref() == Some(&self.props)
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct ControlProps {
pub kind: ControlKind,
pub tag: Option<String>,
pub alias: Option<String>,
pub lock: Lock,
pub showing_placeholder: bool,
pub placeholder: Option<String>,
pub binding: Option<String>,
pub kind_xml: Option<String>,
pub rest: Vec<(String, String)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Lock {
#[default]
None,
ControlLocked,
ContentLocked,
Both,
}
impl Lock {
pub fn parse(v: &str) -> Lock {
match v {
"sdtLocked" => Lock::ControlLocked,
"contentLocked" => Lock::ContentLocked,
"sdtContentLocked" => Lock::Both,
_ => Lock::None,
}
}
pub fn val(self) -> Option<&'static str> {
match self {
Lock::None => None,
Lock::ControlLocked => Some("sdtLocked"),
Lock::ContentLocked => Some("contentLocked"),
Lock::Both => Some("sdtContentLocked"),
}
}
pub fn deletable(self) -> bool {
matches!(self, Lock::None | Lock::ContentLocked)
}
pub fn editable(self) -> bool {
matches!(self, Lock::None | Lock::ControlLocked)
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub enum ControlKind {
PlainText { multi_line: bool },
RichText,
Picture,
Checkbox { checked: char, unchecked: char },
Date { format: String, lid: Option<String> },
DropDown,
Group,
RepeatingSection,
RepeatingItem,
#[default]
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VMerge {
None,
Restart,
Continue,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Cell {
pub width: Option<Width>,
pub span: usize,
pub v_merge: VMerge,
pub top: Option<Border>,
pub left: Option<Border>,
pub bottom: Option<Border>,
pub right: Option<Border>,
pub shading: Option<u32>,
pub margins: [f64; 4],
pub v_align: String,
pub text_direction: String,
pub no_wrap: bool,
pub direct: Raw,
pub blocks: Vec<Block>,
pub controls: Vec<ControlId>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Row {
pub height: Option<(f64, String)>,
pub cant_split: bool,
pub header: bool,
pub grid_before: usize,
pub grid_after: usize,
pub w_before: f64,
pub w_after: f64,
pub cells: Vec<Cell>,
pub controls: Vec<ControlId>,
pub src: Option<Rc<std::ops::Range<usize>>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Table {
pub style_id: Option<String>,
pub width: Width,
pub jc: Jc,
pub ind: f64,
pub borders: [Option<Border>; 6],
pub fixed: bool,
pub cell_spacing: f64,
pub look: Option<Look>,
pub bands: (i64, i64),
pub floating: bool,
pub grid: Vec<f64>,
pub rows: Vec<Row>,
pub src: Option<Rc<std::ops::Range<usize>>>,
pub id: u64,
pub direct: Raw,
pub controls: Vec<ControlId>,
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant, reason = "paragraphs dominate; boxing them costs an allocation each")]
pub enum Block {
Para(Paragraph),
Table(Table),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SectionBreak {
NextPage,
Continuous,
EvenPage,
OddPage,
NextColumn,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SectionPage {
pub page_w: f64,
pub page_h: f64,
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
pub header: f64,
pub footer: f64,
pub gutter: f64,
pub columns: Columns,
pub title_pg: bool,
}
impl Section {
pub fn page(&self) -> SectionPage {
SectionPage {
page_w: self.page_w,
page_h: self.page_h,
top: self.top,
bottom: self.bottom,
left: self.left,
right: self.right,
header: self.header,
footer: self.footer,
gutter: self.gutter,
columns: self.columns.clone(),
title_pg: self.title_pg,
}
}
pub fn set_page(&mut self, p: SectionPage) {
(self.page_w, self.page_h, self.top, self.bottom, self.left, self.right) = (p.page_w, p.page_h, p.top, p.bottom, p.left, p.right);
(self.header, self.footer, self.gutter, self.columns, self.title_pg) = (p.header, p.footer, p.gutter, p.columns, p.title_pg);
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Columns {
pub cols: Vec<(f64, f64)>,
pub equal: bool,
pub num: usize,
pub space: f64,
pub separator: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LineNumbering {
pub count_by: i64,
pub start: i64,
pub distance: f64,
pub restart: String,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PageBorders {
pub top: Option<Border>,
pub left: Option<Border>,
pub bottom: Option<Border>,
pub right: Option<Border>,
pub offset_from_page: bool,
pub display: String,
pub surround_header: bool,
pub surround_footer: bool,
}
pub type HfRefs = [Option<String>; 3];
#[derive(Clone, Debug, PartialEq)]
pub struct Section {
pub break_kind: SectionBreak,
pub page_w: f64,
pub page_h: f64,
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
pub header: f64,
pub footer: f64,
pub gutter: f64,
pub columns: Columns,
pub title_pg: bool,
pub headers: HfRefs,
pub footers: HfRefs,
pub pg_start: Option<i64>,
pub pg_fmt: Option<String>,
pub line_numbers: Option<LineNumbering>,
pub v_align: String,
pub page_borders: Option<PageBorders>,
pub blocks: Vec<Block>,
}
#[derive(Clone, Debug, Default)]
pub struct Settings {
pub compat_mode: i64,
pub compat: std::collections::HashSet<String>,
pub default_tab: f64,
pub compress_punctuation: bool,
pub even_and_odd: bool,
pub mirror_margins: bool,
pub gutter_at_top: bool,
pub auto_hyphenation: bool,
pub hyphenation_zone: f64,
pub consecutive_hyphen_limit: i64,
pub do_not_hyphenate_caps: bool,
pub borders_do_not_surround_header: bool,
pub borders_do_not_surround_footer: bool,
}
impl Settings {
pub fn has(&self, flag: &str) -> bool {
self.compat.contains(flag)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AddedList {
pub abstract_id: i64,
pub num_id: i64,
pub abstract_xml: String,
}
#[derive(Clone, Debug, Default)]
pub struct Document {
pub sections: Vec<Section>,
pub stories: std::collections::BTreeMap<String, Vec<Block>>,
pub footnotes: HashMap<i64, Vec<Block>>,
pub endnotes: HashMap<i64, Vec<Block>>,
pub separators: HashMap<String, Vec<Block>>,
pub images: Vec<Vec<u8>>,
pub controls: HashMap<ControlId, Control>,
pub data_parts: Vec<DataPart>,
pub removed_data: Vec<String>,
pub placeholders: HashMap<usize, (bool, String)>,
pub placeholder_keys: HashMap<usize, String>,
pub filled: std::collections::HashSet<usize>,
pub fixed: std::collections::HashSet<usize>,
pub edited_sections: std::collections::HashSet<usize>,
pub undeletable: std::collections::HashSet<usize>,
pub settings: Settings,
pub line_number_run: Option<Run>,
pub source: Option<Rc<Source>>,
pub resolver: Option<Rc<super::resolve::Resolver>>,
pub added_lists: Vec<AddedList>,
}
impl Document {
pub fn derive_image_sets(&mut self) {
self.fixed.clear();
self.undeletable.clear();
self.placeholders.clear();
self.placeholder_keys.clear();
if !self.controls.values().any(|c| c.props.kind == ControlKind::Picture) {
return;
}
type Found = Vec<(usize, bool, ControlId)>;
fn pick(open: &[ControlId], block: Option<ControlId>, controls: &HashMap<ControlId, Control>) -> Option<ControlId> {
open.iter().rev().copied().find(|c| controls.get(c).is_some_and(|k| k.props.kind == ControlKind::Picture)).or(block)
}
fn walk(blocks: &[Block], controls: &HashMap<ControlId, Control>, out: &mut Found) {
for b in blocks {
match b {
Block::Para(p) => {
let block = p.controls.iter().rev().copied().find(|c| controls.get(c).is_some_and(|k| k.props.kind == ControlKind::Picture));
let mut open: Vec<ControlId> = Vec::new();
for i in &p.inlines {
match i {
Inline::ControlStart(c) => open.push(*c),
Inline::ControlEnd(c) => {
if let Some(k) = open.iter().rposition(|x| x == c) {
open.truncate(k);
}
}
Inline::Picture(pic, _) => {
if let (Some(img), Some(c)) = (pic.image, pick(&open, block, controls)) {
out.push((img, true, c));
}
}
Inline::Float(f, _) => {
if let (Some(img), Some(c)) = (f.image, pick(&open, block, controls)) {
out.push((img, false, c));
}
if let Some(tb) = &f.text_box {
walk(&tb.blocks, controls, out);
}
}
_ => {}
}
}
}
Block::Table(t) => t.rows.iter().flat_map(|r| r.cells.iter()).for_each(|c| walk(&c.blocks, controls, out)),
}
}
}
let mut found = Found::new();
let stories =
self.sections.iter().map(|s| &s.blocks).chain(self.stories.values()).chain(self.footnotes.values()).chain(self.endnotes.values());
stories.for_each(|b| walk(b, &self.controls, &mut found));
for (img, inline, c) in found {
let props = &self.controls[&c].props;
let (kind, key) = props
.tag
.as_deref()
.and_then(|t| t.split_once(':'))
.map_or(("", ""), |(k, rest)| (k.trim(), rest.split(';').next().unwrap_or("").trim()));
let merge = kind.eq_ignore_ascii_case("MergeImage");
let placeholder = merge || kind.eq_ignore_ascii_case("Placeholder");
if placeholder || matches!(props.lock, Lock::ContentLocked | Lock::Both) {
self.fixed.insert(img);
}
if matches!(props.lock, Lock::ControlLocked | Lock::Both) {
self.undeletable.insert(img);
}
if inline && placeholder && props.showing_placeholder {
self.placeholders.insert(img, (merge, props.alias.clone().unwrap_or_else(|| key.to_string())));
self.placeholder_keys.insert(img, key.to_string());
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PTab {
pub align: TabKind,
pub margin: bool,
pub leader: Leader,
}
impl Inline {
pub fn run(&self) -> Option<&Run> {
match self {
Inline::Text(_, r)
| Inline::Tab(r)
| Inline::PTab(_, r)
| Inline::Break(_, r)
| Inline::Picture(_, r)
| Inline::Float(_, r)
| Inline::NoteMark(_, _, r) => Some(r),
Inline::Sym { run, .. }
| Inline::NoteRef { run, .. }
| Inline::Separator { run, .. }
| Inline::FieldBegin { run, .. }
| Inline::FieldSep { run, .. }
| Inline::FieldEnd { run, .. } => Some(run),
Inline::Bookmark(_) | Inline::ControlStart(_) | Inline::ControlEnd(_) => None,
}
}
pub fn units(&self) -> usize {
match self {
Inline::Text(s, _) => s.chars().count(),
Inline::Bookmark(_) | Inline::Float(..) | Inline::ControlStart(_) | Inline::ControlEnd(_) => 0,
_ => 1,
}
}
}
impl Tab {
pub fn is_clear(&self) -> bool {
self.kind == TabKind::Clear
}
}
impl Leader {
pub fn glyph(self) -> Option<char> {
match self {
Leader::None => None,
Leader::Dot => Some('.'),
Leader::Hyphen => Some('-'),
Leader::Underscore => Some('_'),
Leader::MiddleDot => Some('·'),
}
}
}