1pub mod cell;
25mod normalize;
26pub mod template;
27
28use libxml::tree::{Node, NodeType};
29use once_cell::sync::Lazy;
30
31use self::{
32 cell::Cell,
33 normalize::*,
34 template::{Align, Axis, BorderSpec, ColumnSpec, Row, Template, TemplateConfig},
35};
36use crate::{
37 BoxOps,
38 common::{
39 arena::SymHashMap, dimension::Dimension, error::*, numeric_ops::NumericOps, object::Object,
40 },
41 digested::Digested,
42 document::{Document, get_node_qname, with_node_qname},
43 gullet::{self, ExpansionLevel},
44 mouth::Mouth,
45 state::*,
46 stomach::*,
47 token::Catcode,
48 tokens::Tokens,
49};
50
51#[cfg(feature = "token-locators")]
56fn cell_loc(cell: &Cell) -> Option<crate::common::locator::Locator> {
57 cell
58 .boxes
59 .as_ref()
60 .and_then(|b| b.get_locator())
61 .filter(|l| l.from_line != 0)
62}
63
64#[cfg(feature = "token-locators")]
66fn row_span(row: &Row) -> Option<crate::common::locator::Locator> {
67 row
68 .get_columns()
69 .iter()
70 .filter_map(cell_loc)
71 .reduce(|a, b| crate::common::locator::Locator::new_range(a, b).unwrap_or(a))
72}
73use std::{
74 borrow::Cow,
75 collections::VecDeque,
76 fmt::{self, Debug, Display},
77 rc::Rc,
78};
79
80use regex::Regex;
81use rustc_hash::FxHashMap as HashMap;
82
83pub type OpenContainerFn =
85 Rc<dyn Fn(&mut Document, HashMap<String, String>) -> Result<Option<Node>>>;
86pub type CloseContainerFn = Rc<dyn Fn(&mut Document) -> Result<Option<Node>>>;
87pub type OpenRowFn = Rc<dyn Fn(&mut Document, HashMap<String, Stored>) -> Result<()>>;
88pub type CloseRowFn = Rc<dyn Fn(&mut Document) -> Result<Option<Node>>>;
89pub type OpenColumnFn = Rc<dyn Fn(&mut Document, HashMap<String, String>) -> Result<Option<Node>>>;
90pub type CloseColumnFn = Rc<dyn Fn(&mut Document) -> Result<Option<Node>>>;
91
92static SINGLE_PUNCT: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*[\.,;]\s*$").unwrap());
93
94pub struct AlignmentConfig {
95 pub template: Option<Template>,
96 pub open_container: OpenContainerFn,
97 pub close_container: CloseContainerFn,
98 pub open_row: OpenRowFn,
99 pub close_row: CloseRowFn,
100 pub open_column: OpenColumnFn,
101 pub close_column: CloseColumnFn,
102 pub properties: SymHashMap<Stored>,
103 pub xml_attributes: HashMap<String, String>,
104 pub is_math: bool,
105}
106
107#[derive(Clone)]
108pub struct Alignment {
109 in_column: bool,
110 in_row: bool,
111 in_tabular_head: bool,
112 is_math: bool,
113 is_normalized: bool,
114 pub is_halign: bool,
116 current_column: usize,
117 current_row: Option<usize>,
118 reversion: Option<Tokens>,
119 content_reversion: Option<Tokens>,
120 rows: VecDeque<Row>,
121 properties: SymHashMap<Stored>,
122 xml_attributes: HashMap<String, String>,
123 template: Template,
124 open_container: OpenContainerFn,
125 close_container: CloseContainerFn,
126 open_row: OpenRowFn,
127 close_row: CloseRowFn,
128 open_column: OpenColumnFn,
129 close_column: CloseColumnFn,
130 cached_width: Option<Dimension>,
131 cached_height: Option<Dimension>,
132 cached_depth: Option<Dimension>,
133 column_widths: Vec<Dimension>,
134 row_heights: Vec<Dimension>,
135 row_depths: Vec<Dimension>,
136 pub head_rows: Vec<Row>,
138 pub foot_rows: Vec<Row>,
139}
140impl Alignment {
141 pub fn new(config: AlignmentConfig) -> Self {
153 let template = config.template.unwrap_or_default();
154 let mut xml_attributes = config.xml_attributes;
157 for key in ["width", "height", "depth"] {
158 xml_attributes.remove(key);
159 }
160 Alignment {
161 template,
162 current_row: None,
163 reversion: None,
164 content_reversion: None,
165 cached_width: None,
166 cached_height: None,
167 cached_depth: None,
168 open_container: config.open_container,
169 close_container: config.close_container,
170 open_row: config.open_row,
171 close_row: config.close_row,
172 open_column: config.open_column,
173 close_column: config.close_column,
174 current_column: 0,
175 is_math: config.is_math,
176 in_row: false,
177 in_column: false,
178 in_tabular_head: false,
179 is_normalized: false,
180 is_halign: false,
181 properties: config.properties,
182 xml_attributes,
183 rows: VecDeque::new(),
184 column_widths: Vec::new(),
185 row_heights: Vec::new(),
186 row_depths: Vec::new(),
187 head_rows: Vec::new(),
188 foot_rows: Vec::new(),
189 }
190 }
191
192 pub fn get_template(&self) -> &Template { &self.template }
193
194 pub fn current_row(&self) -> Option<&Row> {
195 match self.current_row {
196 Some(idx) => self.rows.get(idx),
197 None => None,
198 }
199 }
200 pub fn current_row_mut(&mut self) -> Option<&mut Row> {
201 match self.current_row {
202 Some(idx) => self.rows.get_mut(idx),
203 None => None,
204 }
205 }
206
207 pub fn new_row(&mut self) -> Option<&Row> {
208 let row = self.template.clone();
209 self.current_row = Some(self.rows.len());
210 self.rows.push_back(row);
211 self.current_column = 0;
212 self.rows.back()
213 }
214
215 pub fn remove_row(&mut self) -> Option<Row> { self.rows.pop_back() }
216
217 pub fn prepend_rows(&mut self, new_rows: Vec<Row>) {
218 for new_row in new_rows.into_iter().rev() {
219 self.rows.push_front(new_row)
220 }
221 }
222
223 pub fn append_rows(&mut self, new_rows: Vec<Row>) {
224 for new_row in new_rows.into_iter() {
225 self.rows.push_back(new_row)
226 }
227 }
228
229 pub fn rows(&self) -> &VecDeque<Row> { &self.rows }
230 pub fn get_cached_height(&self) -> Option<Dimension> { self.cached_height }
231 pub fn get_cached_depth(&self) -> Option<Dimension> { self.cached_depth }
232 pub fn get_row_heights(&self) -> &[Dimension] { &self.row_heights }
233 pub fn get_column_widths(&self) -> &[Dimension] { &self.column_widths }
234 pub fn normalize(&mut self) -> Result<()> { normalize_alignment(self) }
237
238 pub fn add_line(&mut self, border: &str, cols: Vec<usize>) {
239 if let Some(row_idx) = self.current_row {
240 let Some(row) = self.rows.get_mut(row_idx) else {
241 return;
242 };
243 self.current_column = 1;
244 if !cols.is_empty() {
245 for c in cols {
253 if let Some(colspec) = row.get_column_mut(c) {
254 colspec.border.push_str(border);
255 }
256 }
257 } else {
258 for colspec in row.get_columns_mut() {
259 colspec.border.push_str(border)
260 }
261 }
262 }
263 }
264
265 pub fn next_column(&mut self) -> Result<Option<&mut Cell>> {
266 if self.current_row.is_none() {
267 return Ok(None);
268 }
269 self.current_column += 1;
270 let Some(current_row) = self.rows.get_mut(self.current_row.unwrap()) else {
275 Error!(
276 "unexpected",
277 "alignment_row",
278 "Alignment row index points past the materialised rows; dropping the cell"
279 );
280 return Ok(None);
281 };
282 if current_row.get_column_mut(self.current_column).is_some() {
283 Ok(current_row.get_column_mut(self.current_column))
284 } else {
285 Error!("unexpected", "&", "Extra alignment tab '&'");
287 let fallback = Cell {
288 align: Some(Align::Center),
289 ..Cell::default()
290 };
291 current_row.add_column(fallback);
292 Ok(current_row.get_column_mut(self.current_column))
293 }
294 }
295
296 pub fn last_column(&mut self) -> Option<&mut Cell> {
297 if let Some(row_idx) = self.current_row {
298 if let Some(row) = self.rows.get_mut(row_idx) {
299 self.current_column = row.get_columns().len();
300 row.get_column_mut(self.current_column)
301 } else {
302 None
303 }
304 } else {
305 None
306 }
307 }
308
309 pub fn current_column_number(&self) -> usize { self.current_column }
310
311 pub fn set_row_property(&mut self, key: &str, value: String) {
313 if let Some(row_idx) = self.current_row
314 && let Some(row) = self.rows.get_mut(row_idx)
315 {
316 row.properties.insert(key.to_string(), Stored::from(value));
317 }
318 }
319
320 pub fn current_row_number(&self) -> usize {
321 self.rows.iter().filter(|row| !row.is_pseudo()).count()
322 }
323
324 pub fn get_name(&self) -> Option<&str> { self.xml_attributes.get("name").map(String::as_str) }
328
329 pub fn current_column(&mut self) -> Option<&mut Cell> {
330 self
331 .current_row
332 .and_then(|cw| self.rows.get_mut(cw)?.get_column_mut(self.current_column))
333 }
334
335 pub fn get_column(&mut self, n: usize) -> Option<&mut Cell> {
336 self.get_column_mut(n)
338 }
339
340 pub fn get_column_mut(&mut self, n: usize) -> Option<&mut Cell> {
341 self
342 .current_row
343 .and_then(|cw| self.rows.get_mut(cw)?.get_column_mut(n))
344 }
345
346 pub fn add_before_row(&mut self, boxes: Vec<Digested>) {
348 if let Some(cw) = self.current_row
349 && let Some(current_row) = self.rows.get_mut(cw)
350 {
351 current_row.before.extend(boxes);
352 }
353 }
354
355 pub fn add_after_row(&mut self, boxes: Vec<Digested>) {
356 if let Some(cw) = self.current_row
357 && let Some(current_row) = self.rows.get_mut(cw)
358 {
359 current_row.after.extend(boxes);
360 }
361 }
362
363 pub fn omit_column(&mut self) {
364 if let Some(column) = self.current_column() {
365 column.omitted = true;
366 }
367 }
368
369 pub fn omit_next_column(&mut self) {
370 if let Some(cw) = self.current_row
371 && let Some(row) = self.rows.get_mut(cw)
372 && let Some(column) = row.get_column_mut(self.current_column + 1)
373 {
374 column.omitted = true;
375 }
376 }
377
378 pub fn get_column_before(&mut self) -> Tokens {
379 if let Some(column) = self.current_column() {
380 if !column.omitted {
381 Tokens!(
382 T_CS!("\\lx@alignment@column@before"),
383 column.before.clone().unwrap_or_default().unlist()
384 )
385 } else {
386 Tokens!()
387 }
388 } else {
389 Tokens!()
390 }
391 }
392
393 pub fn get_column_after(&mut self) -> Tokens {
394 if let Some(column) = self.current_column() {
395 if !column.omitted {
396 Tokens!(
398 column.after.clone().unwrap_or_default().unlist(),
399 T_CS!("\\lx@alignment@column@after")
400 )
401 } else {
402 Tokens!()
403 }
404 } else {
405 Tokens!()
406 }
407 }
408
409 pub fn revert(&self) -> Result<Tokens> { Ok(self.reversion.clone().unwrap_or_default()) }
410
411 pub fn set_reversion(&mut self, rev: Tokens) { self.reversion = Some(rev); }
412 pub fn set_content_reversion(&mut self, rev: Tokens) { self.content_reversion = Some(rev); }
413
414 pub fn is_in_row(&self) -> bool { self.in_row }
417 pub fn is_in_column(&self) -> bool { self.in_column }
418 pub fn start_row(&mut self, pseudorow: bool) -> Result<()> {
419 self.new_row();
420 bgroup(); if pseudorow {
422 self.current_row_mut().unwrap().set_pseudo()
423 } else {
424 let row_num = self.current_row_number();
427 assign_value("alignmentRowNumber", row_num as i32, None);
428 let row_before = digest(T_CS!("\\lx@alignment@row@before"))?;
429 push_box_list(row_before);
430 }
431 self.in_row = true;
432 assign_value("alignmentStartColumn", 0, None); Ok(())
434 }
435
436 pub fn end_row(&mut self) -> Result<()> {
437 if self.in_row {
438 if self.in_column {
439 self.end_column()?;
440 }
441 egroup()?; self.in_row = false;
443 }
444 Ok(()) }
446
447 pub fn start_column(&mut self, pseudorow: bool) -> Result<()> {
448 if !self.in_row {
449 self.start_row(pseudorow)?;
450 } else if pseudorow {
451 self.current_row_mut().unwrap().set_pseudo();
452 }
453 bgroup(); assign_value("alignmentStartColumn", self.current_column_number(), None);
456 let _colspec = self.next_column()?;
460 set_align_group_count(1000000);
461 self.in_column = true;
462 Ok(())
463 }
464
465 pub fn end_column(&mut self) -> Result<()> {
466 if self.in_column {
467 egroup()?; self.in_column = false;
469 }
470 Ok(())
471 }
472
473 pub fn set_in_tabular_head(&mut self) { self.in_tabular_head = true; }
474 pub fn unset_in_tabular_head(&mut self) { self.in_tabular_head = false; }
475 pub fn is_in_tabular_head(&self) -> bool { self.in_tabular_head }
476
477 pub fn get_properties_mut(&mut self) -> &mut SymHashMap<Stored> { &mut self.properties }
478 pub fn get_xml_attributes_mut(&mut self) -> &mut HashMap<String, String> {
479 &mut self.xml_attributes
480 }
481}
482
483impl Object for Alignment {}
487impl BoxOps for Alignment {
488 fn get_properties(&self) -> &SymHashMap<Stored> { &self.properties }
489 fn with_properties<R, FnR>(&self, caller: FnR) -> R
490 where FnR: FnOnce(&SymHashMap<Stored>) -> R {
491 caller(&self.properties)
492 }
493 fn get_property(&self, key: &str) -> Option<Cow<'_, Stored>> {
494 self.properties.get(key).map(Cow::Borrowed)
495 }
496 fn get_properties_mut(&mut self) -> &mut SymHashMap<Stored> { &mut self.properties }
497
498 fn compute_size(
499 &self,
500 _options: SymHashMap<Stored>,
501 ) -> Result<(Dimension, Dimension, Dimension)> {
502 Ok((
503 Dimension::default(),
504 Dimension::default(),
505 Dimension::default(),
506 ))
507 }
508 fn get_font(&self) -> Result<Option<Rc<crate::common::font::Font>>> { Ok(None) }
509 fn get_string(&self) -> Result<Cow<'_, str>> { Ok(Cow::Borrowed("")) }
510
511 fn compute_size_and_cache(
512 &mut self,
513 _options: SymHashMap<Stored>,
514 ) -> Result<(Dimension, Dimension, Dimension)> {
515 normalize_alignment(self)?;
516 let w = self.cached_width.unwrap();
517 let h = self.cached_height.unwrap();
518 let d = self.cached_depth.unwrap();
519 self.properties.insert("cached_width", Stored::Dimension(w));
521 self
522 .properties
523 .insert("cached_height", Stored::Dimension(h));
524 self.properties.insert("cached_depth", Stored::Dimension(d));
525 Ok((w, h, d))
526 }
527
528 fn be_absorbed(&self, _document: &mut Document) -> Result<Vec<Node>> {
529 fatal!(
537 Internal,
538 Misdefined,
539 "Alignment::be_absorbed called — use be_absorbed_mut instead \
540 (alignment rearrangement requires &mut self)"
541 );
542 }
543 fn be_absorbed_mut(&mut self, document: &mut Document) -> Result<Vec<Node>> {
544 let ismath = self.is_math;
545 normalize_alignment(self)?;
546 #[cfg(feature = "token-locators")]
549 let table_span = self
550 .rows
551 .iter()
552 .filter_map(row_span)
553 .reduce(|a, b| crate::common::locator::Locator::new_range(a, b).unwrap_or(a));
554 let rows = &mut self.rows;
555 if rows.is_empty() {
556 return Ok(Vec::new());
557 }
558
559 let absorb_limit = lookup_int("absorb_limit");
561 if absorb_limit > 0 {
562 let mut absorb_count = lookup_int("absorb_count");
563 absorb_count += 1;
564 assign_value("absorb_count", absorb_count, Some(Scope::Global));
565 if absorb_count > absorb_limit {
566 fatal!(
567 Timeout,
568 Convert,
569 s!(
570 "Whatsit absorb limit of {} exceeded, infinite loop?",
571 absorb_limit
572 )
573 );
574 }
575 }
576
577 let mut attrs = HashMap::default();
580 std::mem::swap(&mut attrs, &mut self.xml_attributes);
581 if let Some(w) = self.cached_width {
584 attrs.insert("cwidth".to_string(), format!("{}", w.px_value(None)));
585 }
586 if let Some(h) = self.cached_height {
587 attrs.insert("cheight".to_string(), format!("{}", h.px_value(None)));
588 }
589 if let Some(d) = self.cached_depth {
590 attrs.insert("cdepth".to_string(), format!("{}", d.px_value(None)));
591 }
592 let open_container_fn = &self.open_container;
593 #[cfg(feature = "token-locators")]
595 document.set_current_box_locator(table_span);
596 open_container_fn(document, attrs)?;
597
598 for row in rows {
599 #[cfg(feature = "token-locators")]
601 document.set_current_box_locator(row_span(row));
602 let vpad_opt = row.get_padding().copied();
603 let mut open_row_attrs = HashMap::default();
605 for (k, v) in &row.properties {
606 open_row_attrs.insert(k.clone(), v.clone());
607 }
608 if let Some(x) = row.x {
609 open_row_attrs.insert("x".to_string(), Stored::Dimension(x));
610 }
611 if let Some(y) = row.y {
612 open_row_attrs.insert("y".to_string(), Stored::Dimension(y));
613 }
614 if let Some(w) = row.cached_width {
615 open_row_attrs.insert("cwidth".to_string(), Stored::Dimension(w));
616 }
617 if let Some(h) = row.cached_height {
618 open_row_attrs.insert("cheight".to_string(), Stored::Dimension(h));
619 }
620 if let Some(d) = row.cached_depth {
621 open_row_attrs.insert("cdepth".to_string(), Stored::Dimension(d));
622 }
623 let open_row_fn = &self.open_row;
624 open_row_fn(document, open_row_attrs)?;
625 for before in row.before.iter() {
626 document.absorb(before, None)?;
627 }
628 for cell in row.get_columns_mut().iter_mut() {
629 if cell.skipped {
630 continue;
631 }
632 let mut border_chars: Vec<char> =
636 cell.border.chars().filter(|c| !c.is_whitespace()).collect();
637 border_chars.sort_unstable();
638 let mut border = String::new();
639 for (idx, &c) in border_chars.iter().enumerate() {
640 border.push(c);
641 if idx + 1 < border_chars.len() && border_chars[idx + 1] != c {
643 border.push(' ');
644 }
645 }
646 let open_column_fn = &self.open_column;
647 let mut cell_attrs = HashMap::default();
648 if let Some(x) = cell.x {
650 cell_attrs.insert("x".to_string(), format!("{}", x.px_value(None)));
651 }
652 if let Some(y) = cell.y {
653 cell_attrs.insert("y".to_string(), format!("{}", y.px_value(None)));
654 }
655 if let Some(w) = cell.cached_width {
656 cell_attrs.insert("cwidth".to_string(), format!("{}", w.px_value(None)));
657 }
658 if let Some(h) = cell.cached_height {
659 cell_attrs.insert("cheight".to_string(), format!("{}", h.px_value(None)));
660 }
661 if let Some(d) = cell.cached_depth {
662 cell_attrs.insert("cdepth".to_string(), format!("{}", d.px_value(None)));
663 }
664 if let Some(ref align) = cell.align {
672 let td_align = if *align == Align::Justify {
673 "left".to_string()
674 } else {
675 align.name()
676 };
677 cell_attrs.insert(String::from("align"), td_align);
678 }
679 if let Some(ref vattach) = cell.vattach {
680 cell_attrs.insert(String::from("vattach"), vattach.clone());
681 }
682 if let Some(w) = cell.width {
683 cell_attrs.insert(String::from("width"), w.to_attribute());
684 }
685 if let Some(vpad) = vpad_opt {
686 cell_attrs.insert(String::from("cssstyle"), s!("padding-bottom: {vpad}"));
687 }
688 if let Some(ref bg) = cell.backgroundcolor {
690 cell_attrs.insert(String::from("backgroundcolor"), bg.clone());
691 }
692 if cell.colspan.unwrap_or(1) != 1 {
694 cell_attrs.insert(String::from("colspan"), cell.colspan.unwrap().to_string());
695 }
696 if cell.rowspan.unwrap_or(1) != 1 {
697 cell_attrs.insert(String::from("rowspan"), cell.rowspan.unwrap().to_string());
698 }
699 if !border.is_empty() {
700 cell_attrs.insert(String::from("border"), border);
701 }
702 if cell.thead_in_column || cell.thead_in_row {
703 let mut thead = String::new();
704 if cell.thead_in_column {
705 thead.push_str(Axis::Column.name());
706 if cell.thead_in_row {
707 thead.push(' ');
708 }
709 }
710 if cell.thead_in_row {
711 thead.push_str(Axis::Row.name());
712 }
713 if !thead.is_empty() {
714 cell_attrs.insert(String::from("thead"), thead);
715 }
716 }
717 let mut classes: Vec<String> = Vec::new();
720 let empty = cell.empty;
721 let has_boxes = cell.boxes.is_some();
723 let mut pre_absorb: Option<Digested> = None;
724 let mut post_absorb: Option<Digested> = None;
725 if !ismath {
726 let threshold_02em: i64 = 131072;
728 let threshold_15em: i64 = 983040;
729 let lpad = cell
758 .lspaces
759 .as_ref()
760 .and_then(|ls| ls.get_width(None).ok().flatten())
761 .map(|rv| rv.value_of())
762 .unwrap_or_else(|| {
763 if intercol_reachable_in_before(&cell.before) || template_has_fill(&cell.before) {
764 threshold_02em
765 } else {
766 0
767 }
768 });
769 let rpad = cell
770 .rspaces
771 .as_ref()
772 .and_then(|rs| rs.get_width(None).ok().flatten())
773 .map(|rv| rv.value_of())
774 .unwrap_or_else(|| {
775 if template_has_intercol(&cell.after) {
778 threshold_02em
779 } else {
780 0
781 }
782 });
783 if !ismath && (!empty || has_boxes) && lpad < threshold_02em {
785 classes.push("ltx_nopad_l".to_string());
786 } else if lpad < threshold_15em {
787 if ismath && cell.lspaces.is_some() {
791 pre_absorb = cell.lspaces.take();
792 }
793 } else {
794 pre_absorb = cell.lspaces.take();
795 }
796 if !ismath && (!empty || has_boxes) && rpad < threshold_02em {
798 classes.push("ltx_nopad_r".to_string());
799 } else if rpad < threshold_15em {
800 } else {
802 post_absorb = cell.rspaces.take();
803 }
804 }
805 if let Some(ref cell_class) = cell.class {
806 classes.insert(0, cell_class.clone());
807 }
808 let class_str: String = classes
809 .into_iter()
810 .filter(|s| !s.is_empty())
811 .collect::<Vec<_>>()
812 .join(" ");
813 if !class_str.is_empty() {
814 cell_attrs.insert(String::from("class"), class_str);
815 }
816 #[cfg(feature = "token-locators")]
823 document.set_current_box_locator(cell_loc(cell));
824 cell.cell = open_column_fn(document, cell_attrs)?;
825 if !cell.skippable {
827 let box_ref = cell.boxes.as_ref().unwrap();
828 document.set_box_to_absorb(Some(box_ref.clone()));
830 let cur_qname = get_node_qname(document.get_node());
833 let wrap_xmarg =
834 ismath && !crate::common::arena::with(cur_qname, |s| s.ends_with("_Capture_"));
835 if wrap_xmarg {
836 document.open_element("ltx:XMArg", Some(string_map!("rule" => "Anything")), None)?;
838 }
839 if let Some(ref pre) = pre_absorb {
841 document.absorb(pre, None)?;
842 }
843 if ismath
846 && pre_absorb.is_none()
847 && let Some(ref lsp) = cell.lspaces
848 {
849 document.absorb(lsp, None)?;
850 }
851 document.absorb(box_ref, None)?;
852 if let Some(ref post) = post_absorb {
854 document.absorb(post, None)?;
855 }
856 if wrap_xmarg {
857 document.close_element("ltx:XMArg")?;
859 }
860 document.expire_box_to_absorb();
862 } else if let Some(ref boxes) = cell.boxes {
863 for item in boxes.unlist() {
870 if item.get_property_bool("alignmentPreserve") {
871 document.absorb(&item, None)?;
872 }
873 }
874 }
875 let close_column_fn = &self.close_column;
876 close_column_fn(document)?;
877 }
878 for after in row.after.iter() {
879 document.absorb(after, None)?;
880 }
881 let close_row_fn = &self.close_row;
882 close_row_fn(document)?;
883 }
884 let close_container_fn = &self.close_container;
885 let node_opt = close_container_fn(document)?;
886
887 if let Some(mut node) = node_opt {
891 if document
892 .findnodes("ancestor::ltx:tabular", Some(&node))
893 .is_empty()
894 {
895 let hashead = !document
896 .findnodes("descendant::ltx:td[@thead]", Some(&node))
897 .is_empty();
898 let guess_headers = self
900 .properties
901 .get("guess_headers")
902 .map(|v| !matches!(v, Stored::Bool(false)))
903 .unwrap_or(false);
904 if guess_headers && !hashead {
905 guess_alignment_headers(document, &mut node, self)?;
906 }
907 else if hashead && !ismath {
910 alignment_regroup_rows(document, &node)?;
912 }
913 }
914 Ok(vec![node])
915 } else {
916 Ok(Vec::new())
917 }
918 }
919}
920
921impl Debug for Alignment {
922 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
923 write!(
924 f,
925 "Alignment{{template:{:?}, properties:{:?}, rows:{:?} }}",
926 self.template, self.properties, self.rows
927 )
928 }
929}
930
931impl Display for Alignment {
932 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{self:?}") }
933}
934impl PartialEq for Alignment {
935 fn eq(&self, other: &Alignment) -> bool {
936 self.template == other.template
938 }
939}
940
941pub fn read_alignment_template() -> Result<Template> {
951 gullet::skip_spaces()?;
952 local_build_template(Template::default());
953 let mut tokens = vec![T_BEGIN!()];
954 let mut nopens = 0;
955 while let Some(open) = gullet::read_token()? {
956 if open.get_catcode() == Catcode::BEGIN {
957 nopens += 1;
958 } else {
959 gullet::unread_one(open);
960 break;
961 }
962 }
963 while let Some(op) = gullet::read_token()? {
964 let cc = op.get_catcode();
965 if cc == Catcode::SPACE {
966 } else if cc == Catcode::END {
967 let mut last_op = op;
968 nopens -= 1;
969 while nopens > 0 {
970 if let Some(next_op) = gullet::read_token()? {
971 last_op = next_op;
972 if last_op.get_catcode() != Catcode::END {
973 break;
974 }
975 } else {
976 break;
977 }
978 nopens -= 1;
979 }
980 if nopens <= 0 {
981 break;
982 }
983 gullet::unread_one(last_op);
984 } else {
985 match lookup_expandable(&T_CS!(s!("\\NC@rewrite@{op}")), None)? {
986 Some(defn) => {
987 let invoked = defn.invoke(true)?;
988 gullet::unread(invoked);
989 },
990 _ => {
991 if cc == Catcode::BEGIN {
992 let balanced_arg = gullet::read_balanced(ExpansionLevel::Off, false, false)?;
993 if !balanced_arg.is_empty() {
994 gullet::unread(balanced_arg);
995 }
996 } else {
997 Warn!("unexpected", op, s!("Unrecognized tabular template {op:?}"));
998 }
999 },
1000 }
1001 }
1002 if nopens <= 0 {
1003 break;
1004 }
1005 }
1006 tokens.push(T_END!());
1007 with_current_build_template(|template_opt| {
1008 let t = template_opt.unwrap();
1009 t.set_reversion(Tokens::new(tokens));
1010 t.finish();
1012 });
1013 Ok(take_build_template().unwrap())
1014}
1015
1016pub fn parse_alignment_template(spec: &str) -> Result<Template> {
1017 let reader_mouth = Mouth::new(&s!("{{{spec}}}"), None)?;
1018 gullet::reading_from_mouth(reader_mouth, read_alignment_template)
1019}
1020
1021pub fn matrix_template() -> Template {
1022 Template::new(TemplateConfig {
1023 repeated: vec![Cell {
1024 before: Some(Tokens!(T_CS!("\\hfil"))),
1025 after: Some(Tokens!(T_CS!("\\hfil"))),
1026 ..Cell::default()
1027 }],
1028 ..TemplateConfig::default()
1029 })
1030}
1031
1032#[allow(dead_code)]
1045fn template_has_fill(tokens: &Option<Tokens>) -> bool {
1046 if let Some(toks) = tokens {
1047 for tok in toks.unlist_ref() {
1048 let s = tok.to_string();
1049 if s == "\\hfil" || s == "\\hfill" || s == "\\hskip" {
1050 return true;
1051 }
1052 }
1053 }
1054 false
1055}
1056
1057fn intercol_reachable_in_before(tokens: &Option<Tokens>) -> bool {
1064 if let Some(toks) = tokens {
1065 for tok in toks.unlist_ref() {
1066 let s = tok.to_string();
1067 if s == "\\lx@intercol" || s.contains("intercol") {
1068 return true;
1069 }
1070 if s == "\\vrule"
1072 || s == "\\relax"
1073 || s == "\\hfil"
1074 || s == "\\hfill"
1075 || s == "\\hskip"
1076 || s == "\\lx@column@trimright"
1077 {
1078 continue;
1079 }
1080 return false;
1082 }
1083 }
1084 false
1085}
1086
1087fn template_has_intercol(tokens: &Option<Tokens>) -> bool {
1092 if let Some(toks) = tokens {
1093 for tok in toks.unlist_ref() {
1094 let s = tok.to_string();
1095 if s == "\\lx@intercol" || s.contains("intercol") {
1096 return true;
1097 }
1098 }
1099 }
1100 false
1101}
1102
1103fn guess_alignment_headers(
1104 document: &mut Document,
1105 table: &mut Node,
1106 alignment: &mut Alignment,
1107) -> Result<()> {
1108 if !document
1111 .findnodes("ancestor::ltx:tabular", Some(table))
1112 .is_empty()
1113 {
1114 return Ok(());
1115 }
1116 let tag = get_node_qname(table);
1117 let ismath = tag == crate::pin!("ltx:XMArray");
1123 let reversed = false;
1124 classify_alignment_rows(alignment);
1127
1128 {
1129 let mut rows = collect_alignment_rows(alignment);
1130 if rows.is_empty() {
1131 return Ok(());
1132 }
1133 alignment_characterize_lines(document, Axis::Row, false, rows.as_mut_slice())?;
1134 }
1135 {
1137 let mut cols = collect_alignment_columns(alignment);
1138 if cols.is_empty() {
1139 return Ok(());
1140 }
1141 alignment_characterize_lines(document, Axis::Column, false, cols.as_mut_slice())?;
1143 }
1144
1145 let rows = collect_alignment_rows(alignment);
1147 let mut n_h = 0;
1148 let mut n_d = 0;
1149 for r in rows.iter() {
1150 for c in r {
1151 match c.cell_type {
1152 Some('h') => n_h += 1,
1153 Some('d') => n_d += 1,
1154 Some(other) => panic!("unexpected cell_type {}", other),
1155 None => {},
1156 }
1157 }
1158 }
1159 if n_d == 1 {
1162 n_h = 0;
1164 for r in rows {
1165 for c in r {
1166 c.cell_type = Some('d');
1167 if let Some(ref mut cell) = c.cell {
1168 cell.remove_attribute("thead")?;
1169 }
1170 }
1171 }
1172 }
1173 if !ismath && !reversed {
1176 alignment_regroup_rows(document, table)?;
1177 }
1178 if n_h > 0 {
1179 document.add_class(table, "ltx_guessed_headers")?;
1181 }
1182
1183 Ok(())
1186}
1187
1188fn alignment_regroup_rows(document: &mut Document, table: &Node) -> Result<()> {
1194 let mut rows = document.findnodes("ltx:tr", Some(table));
1195 let mut heads = Vec::with_capacity(rows.len());
1198 let mut maxreach = 0;
1199 while !rows.is_empty() {
1201 let cells = document.findnodes("ltx:td", Some(&rows[0]));
1202 if cells
1204 .iter()
1205 .any(|cell| cell.get_attribute("thead").is_none())
1206 {
1207 break;
1208 }
1209 let line = heads.len();
1210 heads.push(rows.remove(0));
1211 for cell in cells {
1212 let this_rowspan = cell
1215 .get_attribute("rowspan")
1216 .and_then(|v| v.parse::<usize>().ok())
1217 .unwrap_or(0)
1218 + line;
1219 if this_rowspan > maxreach {
1220 maxreach = this_rowspan;
1221 }
1222 }
1223 }
1224 if maxreach > heads.len() {
1225 heads.append(&mut rows);
1227 rows = heads;
1228 heads = Vec::new();
1229 }
1230 let mut foots = VecDeque::new();
1232 while !rows.is_empty() {
1233 let cells = document.findnodes("ltx:td", Some(rows.last().unwrap()));
1234 if cells
1236 .iter()
1237 .any(|cell| cell.get_attribute("thead").is_none())
1238 {
1239 break;
1240 }
1241 foots.push_front(rows.pop().unwrap())
1242 }
1243 if !heads.is_empty() {
1244 document.wrap_nodes("ltx:thead", heads)?;
1245 }
1246 if !rows.is_empty() {
1247 document.wrap_nodes("ltx:tbody", rows)?;
1248 }
1249 if !foots.is_empty() {
1250 document.wrap_nodes("ltx:tfoot", foots.into_iter().collect())?;
1251 }
1252 Ok(())
1253}
1254
1255fn classify_alignment_rows(alignment: &mut Alignment) {
1258 let mut ncols = 0;
1259 for arow in &mut alignment.rows {
1260 let n = arow.get_columns().len();
1261 if n > ncols {
1262 ncols = n;
1263 }
1264 }
1265 let (mut h, mut v) = (false, false);
1267 for arow in alignment.rows.iter_mut() {
1268 let cols = arow.get_columns_mut();
1269 let this_row_len = cols.len();
1270 for col in cols.iter_mut() {
1272 col.cell_type = Some('d');
1273 col.content_class = Some(
1274 if col.align == Some(Align::Justify) {
1276 ColumnSpec::MathAltText
1277 } else if col.cell.is_some() {
1278 classify_alignment_cell(col.cell.as_ref().unwrap())
1279 } else {
1280 ColumnSpec::Unknown
1281 },
1282 );
1283 col.content_length = Some(if col.content_class == Some(ColumnSpec::Graphics) {
1286 1000
1287 } else if col.cell.is_some() {
1288 col.cell.as_ref().unwrap().get_content().chars().count()
1289 } else {
1290 0
1291 });
1292 let (mut border_top, mut border_bottom, mut border_left, mut border_right) = (0, 0, 0, 0);
1293 for c in col.border.chars() {
1294 match c {
1295 'l' | 'L' => border_left += 1,
1296 'r' | 'R' => border_right += 1,
1297 't' | 'T' => border_top += 1,
1298 'b' | 'B' => border_bottom += 1,
1299 _ => {}, }
1301 }
1302 if (border_top > 0) || (border_bottom > 0) {
1304 h = true;
1305 }
1306 if (border_right > 0) || (border_left > 0) {
1307 v = true;
1308 }
1309 col.border_top = Some(border_top);
1310 col.border_bottom = Some(border_bottom);
1311 col.border_left = Some(border_left);
1312 col.border_right = Some(border_right);
1313 }
1314 let to_pad = ncols - this_row_len;
1316 if to_pad > 0 {
1317 for _ in 0..to_pad {
1318 let col = Cell {
1319 align: Some(Align::Center),
1320 cell_type: Some('d'),
1321 content_class: Some(ColumnSpec::Empty),
1322 content_length: Some(0),
1323 ..Cell::default()
1324 };
1325 cols.push(col);
1326 }
1327 }
1328 }
1329 let nrows_pre = alignment.rows.len();
1346 for r in 0..nrows_pre {
1347 let row_len = alignment.rows[r].get_columns().len();
1348 for c in 0..row_len {
1349 let (rs, cs, ca, cc, cl, rb, bb) = {
1350 let cell = &mut alignment.rows[r].get_columns_mut()[c];
1351 let rs = cell.rowspan.unwrap_or(1);
1352 let cs = cell.colspan.unwrap_or(1);
1353 let ca = cell.align.clone();
1354 let cc = cell.content_class;
1355 let cl = cell.content_length;
1356 let rb = cell.border_right;
1357 cell.border_right = Some(0);
1358 let bb = cell.border_bottom;
1359 cell.border_bottom = Some(0);
1360 (rs, cs, ca, cc, cl, rb, bb)
1361 };
1362 for sc in 1..cs {
1364 if let Some(cell) = alignment.rows[r].get_columns_mut().get_mut(c + sc) {
1365 cell.align = ca.clone();
1366 cell.content_class = cc;
1367 cell.content_length = cl;
1368 }
1369 }
1370 for sr in 1..rs {
1372 if r + sr >= nrows_pre {
1373 break;
1374 }
1375 for sc in 0..cs {
1376 if let Some(cell) = alignment.rows[r + sr].get_columns_mut().get_mut(c + sc) {
1377 cell.align = ca.clone();
1378 cell.content_class = cc;
1379 cell.content_length = cl;
1380 }
1381 }
1382 }
1383 for sr in 0..rs {
1385 if r + sr >= nrows_pre {
1386 break;
1387 }
1388 if let Some(cell) = alignment.rows[r + sr].get_columns_mut().get_mut(c + cs - 1) {
1389 cell.border_right = rb;
1390 }
1391 }
1392 if r + rs - 1 < nrows_pre {
1394 for sc in 0..cs {
1395 if let Some(cell) = alignment.rows[r + rs - 1].get_columns_mut().get_mut(c + sc) {
1396 cell.border_bottom = bb;
1397 }
1398 }
1399 }
1400 }
1401 }
1402 if ncols == 0 {
1407 return;
1408 }
1409 for row in alignment.rows.iter_mut() {
1410 let cols = row.get_columns_mut();
1411 cols[0].border_left = Some(if v { 1 } else { 0 });
1412 if ncols > 1 {
1413 if cols[1].border_left.unwrap_or(0) > 0 {
1414 cols[0].border_right = cols[1].border_left;
1415 }
1416 if cols[ncols - 2].border_right.unwrap_or(0) > 0 {
1417 cols[ncols - 1].border_left = cols[ncols - 2].border_right;
1418 }
1419 }
1420 cols[ncols - 1].border_right = Some(if v { 1 } else { 0 });
1421 }
1422 let nrows = alignment.rows.len();
1423 for c in 0..ncols {
1424 alignment.rows[0].get_columns_mut()[c].border_top = Some(if h { 1 } else { 0 });
1425 if nrows > 1 {
1426 if let Some(bt) = alignment.rows[1].get_columns_mut()[c].border_top
1427 && bt > 0
1428 {
1429 alignment.rows[0].get_columns_mut()[c].border_bottom = Some(bt);
1431 }
1432 if let Some(bb) = alignment.rows[nrows - 2].get_columns_mut()[c].border_bottom
1433 && bb > 0
1434 {
1435 alignment.rows[nrows - 1].get_columns_mut()[c].border_top = Some(bb);
1437 }
1438 }
1439 alignment.rows[nrows - 1].get_columns_mut()[c].border_bottom = Some(if h { 1 } else { 0 });
1440 }
1441 for r in 1..nrows - 1 {
1446 for c in 1..ncols - 1 {
1447 if let Some(bb) = alignment.rows[r - 1].get_columns_mut()[c].border_bottom
1448 && bb > 0
1449 {
1450 alignment.rows[r].get_columns_mut()[c].border_top = Some(bb);
1452 }
1453 if let Some(bt) = alignment.rows[r + 1].get_columns_mut()[c].border_top
1454 && bt > 0
1455 {
1456 alignment.rows[r].get_columns_mut()[c].border_bottom = Some(bt);
1458 }
1459 if let Some(br) = alignment.rows[r].get_columns_mut()[c - 1].border_right
1460 && br > 0
1461 {
1462 alignment.rows[r].get_columns_mut()[c].border_left = Some(br);
1464 }
1465 if let Some(bl) = alignment.rows[r].get_columns_mut()[c + 1].border_left
1466 && bl > 0
1467 {
1468 alignment.rows[r].get_columns_mut()[c].border_right = Some(bl);
1470 }
1471 }
1472 }
1473 }
1491
1492fn collect_alignment_rows(alignment: &mut Alignment) -> Vec<Vec<&mut Cell>> {
1493 alignment
1494 .rows
1495 .iter_mut()
1496 .map(|x| x.get_columns_mut().iter_mut().collect())
1497 .collect()
1498}
1499
1500fn collect_alignment_columns(alignment: &mut Alignment) -> Vec<Vec<&mut Cell>> {
1501 let mut row_cells: Vec<_> = alignment
1502 .rows
1503 .iter_mut()
1504 .map(|r| r.get_columns_mut().iter_mut())
1505 .collect();
1506 let n_cols = row_cells[0].len();
1507 let n_rows = row_cells.len();
1508 let mut columns = Vec::with_capacity(n_cols);
1509 for _ in 0..n_cols {
1510 let mut column = Vec::with_capacity(n_rows);
1511 for row_iter in row_cells.iter_mut() {
1512 column.push(row_iter.next().unwrap());
1513 }
1514 columns.push(column);
1515 }
1516 columns
1517}
1518
1519fn classify_alignment_cell(xcell: &Node) -> ColumnSpec {
1522 let content = xcell.get_content();
1523 let mut inferred_classes: Vec<ColumnSpec> = Vec::new();
1524 if !content.is_empty()
1530 && content
1531 .chars()
1532 .all(|c| c.is_whitespace() || c.is_ascii_digit() || ('\u{1D7D8}'..='\u{1D7E1}').contains(&c))
1533 {
1534 inferred_classes.push(ColumnSpec::Integer);
1535 } else {
1536 let mut nodes = xcell.get_child_nodes();
1537 while !nodes.is_empty() {
1538 let ch = nodes.remove(0);
1539 match ch.get_type() {
1540 Some(NodeType::TextNode) => {
1541 let text = ch.get_content();
1542 if !(text.chars().all(|c| c.is_whitespace())
1543 || (inferred_classes.first() == Some(&ColumnSpec::Math)
1544 && SINGLE_PUNCT.is_match(&text)))
1545 {
1546 inferred_classes.push(ColumnSpec::Text);
1547 }
1548 },
1549 Some(NodeType::ElementNode) => {
1550 with_node_qname(&ch, |chtag| match chtag {
1551 "ltx:text" => {
1552 if inferred_classes.last() != Some(&ColumnSpec::Text) {
1556 inferred_classes.push(ColumnSpec::Text);
1557 }
1558 },
1559 "ltx:graphics" => {
1560 if inferred_classes.first() != Some(&ColumnSpec::Graphics) {
1561 inferred_classes.push(ColumnSpec::Graphics);
1562 }
1563 },
1564 "ltx:Math" => {
1565 if inferred_classes.first() != Some(&ColumnSpec::Math) {
1566 inferred_classes.push(ColumnSpec::Math);
1567 }
1568 },
1569 "ltx:XMText" => {
1570 if inferred_classes.first() != Some(&ColumnSpec::Text) {
1571 inferred_classes.push(ColumnSpec::Text);
1572 }
1573 },
1574 "ltx:XMArg" | "ltx:inline-block" | "ltx:p" => {
1575 let mut children = ch.get_child_nodes();
1580 children.append(&mut nodes);
1581 nodes = children;
1582 },
1583 other if other.starts_with("ltx:XM") => {
1584 if inferred_classes.first() != Some(&ColumnSpec::Math) {
1585 inferred_classes.push(ColumnSpec::Math);
1586 }
1587 },
1588 _ => {
1589 if inferred_classes.is_empty() {
1590 inferred_classes.push(ColumnSpec::Unknown);
1591 }
1592 },
1593 })
1594 },
1595 _ => {},
1596 }
1597 }
1598 }
1599
1600 if inferred_classes.len() > 1 {
1602 let mut alt_peekable = inferred_classes.iter().peekable();
1603 let mut is_alternating = true;
1604 while let Some(c) = alt_peekable.next() {
1605 match c {
1606 ColumnSpec::Math | ColumnSpec::Integer => {
1607 if let Some(peek) = alt_peekable.peek()
1608 && !matches!(peek, ColumnSpec::Text)
1609 {
1610 is_alternating = false;
1611 break;
1612 }
1613 },
1614 ColumnSpec::Text => {
1615 if let Some(peek) = alt_peekable.peek()
1616 && !matches!(peek, ColumnSpec::Math | ColumnSpec::Integer)
1617 {
1618 is_alternating = false;
1619 break;
1620 }
1621 },
1622 _ => {
1623 is_alternating = false;
1624 break;
1625 },
1626 }
1627 }
1628 if is_alternating {
1629 inferred_classes = vec![ColumnSpec::MathAltText];
1630 }
1631 }
1632 if inferred_classes.is_empty() {
1634 ColumnSpec::Empty
1635 } else if inferred_classes.len() == 1 {
1636 inferred_classes[0]
1637 } else {
1638 let all_text = inferred_classes
1641 .iter()
1642 .all(|c| matches!(c, ColumnSpec::Text));
1643 if all_text {
1644 ColumnSpec::MultiText
1645 } else {
1646 ColumnSpec::Unknown
1647 }
1648 }
1649}
1650
1651const MIN_ALIGNMENT_DATA_LINES: usize = 1; const MAX_ALIGNMENT_HEADER_LINES: usize = 4; fn alignment_characterize_lines(
1667 document: &mut Document,
1668 axis: Axis,
1669 reversed: bool,
1670 lines: &mut [Vec<&mut Cell>],
1671) -> Result<()> {
1672 let n = lines.len();
1673 if n < 2 {
1674 return Ok(());
1675 }
1676 let (mut max_diff, mut min_diff, _avg_diff) = (0.0, 99999999.0, 0.0);
1680 for l in 0..n - 1 {
1681 let d = alignment_compare(axis, true, reversed, l, l + 1, lines);
1682 if d > max_diff {
1685 max_diff = d;
1686 }
1687 if d < min_diff {
1688 min_diff = d;
1689 }
1690 }
1691 if max_diff < 0.05 {
1693 return Ok(());
1695 }
1696 if (n > 2) && ((max_diff - min_diff) < max_diff * 0.5) {
1697 return Ok(());
1699 }
1700 let tab_threshold = min_diff + 0.3 * (max_diff - min_diff);
1701
1702 let (minh, mut maxh) = (1, 1);
1706 let mut diff;
1707 loop {
1708 diff = alignment_compare(axis, true, reversed, maxh - 1, maxh, lines);
1709 if diff >= tab_threshold {
1710 break;
1711 }
1712 maxh += 1;
1713 }
1714 if maxh > MAX_ALIGNMENT_HEADER_LINES {
1715 return Ok(());
1717 }
1718 while alignment_compare(axis, true, reversed, maxh, maxh + 1, lines) > tab_threshold {
1719 maxh += 1;
1720 }
1721 if maxh > MAX_ALIGNMENT_HEADER_LINES {
1722 maxh = MAX_ALIGNMENT_HEADER_LINES;
1723 }
1724 let nn = lines[0].len() - 1;
1727 for nh in (minh..=maxh).rev() {
1729 let heads = alignment_test_headers(nh, tab_threshold, axis, lines);
1731 if !heads.is_empty() {
1732 for h in heads {
1734 for (i, cell) in lines[h].iter_mut().enumerate() {
1735 cell.cell_type = Some('h');
1736 if let Some(ref mut xcell) = cell.cell {
1737 if (cell.content_class == Some(ColumnSpec::Empty))
1740 && ((i == 0
1741 && (if axis == Axis::Row {
1742 cell.border_left.unwrap_or(0) == 0
1743 } else {
1744 cell.border_top.unwrap_or(0) == 0
1745 }))
1746 || (i == nn
1747 && (if axis == Axis::Row {
1748 cell.border_right.unwrap_or(0) == 0
1749 } else {
1750 cell.border_bottom.unwrap_or(0) == 0
1751 })))
1752 {
1753 } else {
1754 document.add_ss_values(xcell, "thead", axis.marker_name())?;
1755 }
1756 }
1757 }
1758 }
1759 return Ok(());
1760 }
1761 }
1762 Ok(())
1763}
1764
1765fn alignment_test_headers(
1767 nhead: usize,
1768 tab_threshold: f64,
1769 axis: Axis,
1770 lines: &[Vec<&mut Cell>],
1771) -> Vec<usize> {
1772 let mut heads: Vec<usize> = (0..nhead).collect(); let mut head_length = alignment_max_content_length(0, 0, nhead - 1, lines);
1775 let mut next_line = nhead; let nrep = lines.len() / nhead;
1779 if nhead > 1 {
1780 let mut matched = true;
1782 for r in 1..nrep {
1783 matched =
1784 matched && alignment_match_head(0, r * nhead, nhead, tab_threshold, axis, lines) > 0;
1785 }
1786 if matched {
1790 return Vec::new();
1791 }
1792 }
1793
1794 let ndata = alignment_skip_data(next_line, tab_threshold, axis, lines);
1796 if ndata < nhead {
1798 return Vec::new();
1800 }
1801 if (ndata < nhead) && (ndata < 2) {
1802 return Vec::new();
1803 }
1804 let mut data_length = alignment_max_content_length(0, next_line, next_line + ndata - 1, lines);
1806 next_line += ndata;
1807
1808 let mut nd;
1809 while next_line < lines.len() {
1812 nd = if ndata > 1 {
1816 alignment_match_data(nhead, next_line, ndata, tab_threshold, axis, lines)
1817 } else {
1818 0
1819 };
1820 if nd > 0 {
1822 data_length = alignment_max_content_length(data_length, next_line, next_line + nd - 1, lines);
1823 next_line += nd;
1824 }
1825 else if alignment_match_head(0, next_line, nhead, tab_threshold, axis, lines) > 0 {
1827 for idx in next_line..next_line + nhead {
1829 heads.push(idx);
1830 }
1831 head_length =
1832 alignment_max_content_length(head_length, next_line, next_line + nhead - 1, lines);
1833 next_line += nhead;
1834 nd = alignment_match_data(nhead, next_line, ndata, tab_threshold, axis, lines);
1835 if nd == 0 {
1836 return Vec::new();
1837 }
1838 data_length = alignment_max_content_length(data_length, next_line, next_line + nd - 1, lines);
1839 next_line += nd;
1840 } else {
1841 return Vec::new();
1843 }
1844 }
1845 if (head_length > 10) && (head_length > 4 * data_length) {
1848 return Vec::new();
1851 }
1852 if head_length >= 1000 {
1854 return Vec::new();
1858 }
1859
1860 heads
1862}
1863
1864fn alignment_match_head(
1865 p1: usize,
1866 p2: usize,
1867 nhead: usize,
1868 tab_threshold: f64,
1869 axis: Axis,
1870 tablines: &[Vec<&mut Cell>],
1871) -> usize {
1872 let nh = alignment_match_lines(p1, p2, nhead, tab_threshold, axis, tablines);
1873 let ok = nhead == nh;
1874 if ok { nhead } else { 0 }
1877}
1878
1879fn alignment_match_data(
1880 p1: usize,
1881 p2: usize,
1882 n: usize,
1883 tab_threshold: f64,
1884 axis: Axis,
1885 tablines: &[Vec<&mut Cell>],
1886) -> usize {
1887 let nd = alignment_match_lines(p1, p2, n, tab_threshold, axis, tablines);
1888 let ok = (nd as f64 * 1.0) / n as f64 > 0.66;
1889 if ok { nd } else { 0 }
1892}
1893
1894fn alignment_match_lines(
1896 p1: usize,
1897 p2: usize,
1898 n: usize,
1899 tab_threshold: f64,
1900 axis: Axis,
1901 tablines: &[Vec<&mut Cell>],
1902) -> usize {
1903 let max_n = tablines.len();
1904 for i in 0..n {
1905 if (p1 + i >= max_n)
1906 || (p2 + i >= max_n)
1907 || alignment_compare(axis, false, false, p1 + i, p2 + i, tablines) >= tab_threshold
1908 {
1909 return i;
1910 }
1911 }
1912 n
1913}
1914
1915fn alignment_skip_data(
1925 i: usize,
1926 tab_threshold: f64,
1927 axis: Axis,
1928 tablines: &[Vec<&mut Cell>],
1929) -> usize {
1930 let tab_lines_length = tablines.len();
1931 if i >= tab_lines_length {
1932 return 0;
1933 }
1934 let _header_width = if !tablines.is_empty() {
1935 tablines[0].len()
1936 } else {
1937 1
1938 };
1939 let mut n = 1;
1940 while i + n < tab_lines_length {
1941 if alignment_compare(axis, true, false, i + n - 1, i + n, tablines) >= tab_threshold {
1942 break;
1946 }
1947 n += 1;
1948 }
1949 if n >= MIN_ALIGNMENT_DATA_LINES { n } else { 0 }
1950}
1951
1952fn alignment_max_content_length(
1954 mut length: usize,
1955 from: usize,
1956 to: usize,
1957 tablines: &[Vec<&mut Cell>],
1958) -> usize {
1959 for item in tablines.iter().take(to + 1).skip(from) {
1960 let mut l = 0;
1961 for cell in item.iter() {
1962 l += cell.content_length.unwrap_or(0);
1963 }
1964 if l > length {
1965 length = l;
1966 }
1967 }
1968 length
1969}
1970
1971fn alignment_compare(
1979 axis: Axis,
1980 for_adjacency: bool,
1981 reversed: bool,
1982 p1: usize,
1983 p2: usize,
1984 lines: &[Vec<&mut Cell>],
1985) -> f64 {
1986 let max_guard = lines.len();
1987 if p1 >= max_guard || p2 >= max_guard {
1988 return 0.0;
1989 }
1990 let line1 = &lines[p1];
1991 let line2 = &lines[p2];
1992 if line1.is_empty() && line2.is_empty() {
1993 return 0.0;
1994 } else if line1.is_empty() || line2.is_empty() {
1995 return 99999.0;
1996 }
1997 let ncells = line1.len();
1998 let mut diff = 0.0;
1999
2000 for (cell1, cell2) in line1.iter().zip(line2.iter()) {
2001 if cell1.content_class.is_none()
2003 || cell2.content_class.is_none()
2004 || cell1.border_left.is_none()
2005 || cell2.border_left.is_none()
2006 || cell1.border_right.is_none()
2007 || cell2.border_right.is_none()
2008 || cell1.border_bottom.is_none()
2009 || cell2.border_bottom.is_none()
2010 || cell1.border_top.is_none()
2011 || cell2.border_top.is_none()
2012 {
2013 continue;
2014 }
2015 if cell1.align != cell2.align
2016 && cell1.content_class != Some(ColumnSpec::Empty)
2017 && cell2.content_class != Some(ColumnSpec::Empty)
2018 {
2019 diff += 0.75;
2020 }
2021 let d = cell1
2022 .content_class
2023 .as_ref()
2024 .unwrap()
2025 .difference_heuristic(cell2.content_class.as_ref().unwrap());
2026 if d > 0.0 {
2027 diff += d;
2028 }
2029 if for_adjacency {
2031 let mut inner_diffs = 0.0;
2033 if axis == Axis::Row {
2034 if cell1.border_right != cell2.border_right {
2035 inner_diffs += 1.0;
2036 }
2037 if cell1.border_left != cell2.border_left {
2038 inner_diffs += 1.0;
2039 }
2040 } else {
2041 if cell1.border_top != cell2.border_top {
2042 inner_diffs += 1.0;
2043 }
2044 if cell1.border_bottom != cell2.border_bottom {
2045 inner_diffs += 1.0;
2046 }
2047 };
2048 diff += 0.3 * inner_diffs;
2049 let pedge = if axis == Axis::Row {
2051 if reversed {
2052 BorderSpec::Top
2053 } else {
2054 BorderSpec::Bottom
2055 }
2056 } else if reversed {
2057 BorderSpec::Left
2058 } else {
2059 BorderSpec::Right
2060 };
2061 let border1_pedge = cell1.border_at(pedge);
2062 let border2_pedge = cell2.border_at(pedge);
2063 if let Some(b1p) = border1_pedge
2064 && b1p > 0
2065 && (border1_pedge != border2_pedge)
2066 {
2067 diff += (b1p as i64 - border2_pedge.unwrap_or(0) as i64).abs() as f64;
2068 }
2069 } else {
2070 let mut inner_diffs = 0.0;
2072 if cell1.border_right != cell2.border_right {
2073 inner_diffs += 1.0;
2074 }
2075 if cell1.border_left != cell2.border_left {
2076 inner_diffs += 1.0;
2077 }
2078 if cell1.border_top != cell2.border_top {
2079 inner_diffs += 1.0;
2080 }
2081 if cell1.border_bottom != cell2.border_bottom {
2082 inner_diffs += 1.0;
2083 }
2084 diff += 0.3 * inner_diffs;
2085 }
2086 }
2087 diff /= ncells as f64;
2088 diff
2091}