rapiddoc_render/
structure.rs1use rapiddoc_core::{Error, ErrorCode, Rect};
3use rapiddoc_native::grid::{Cell, Table};
4
5fn invalid(message: &str) -> Error {
6 Error {
7 code: ErrorCode::InvalidInput,
8 message: message.into(),
9 }
10}
11
12pub fn html(tokens: &[String], texts: &[String]) -> Result<String, Error> {
15 if tokens.len() > rapiddoc_core::resources::limit(10_010)
16 || texts.len() > rapiddoc_core::resources::limit(10_000)
17 || texts.iter().map(String::len).sum::<usize>() > rapiddoc_core::resources::limit(1_000_000)
18 || tokens.iter().map(String::len).sum::<usize>()
19 > rapiddoc_core::resources::limit(1_000_000)
20 {
21 return Err(Error {
22 code: ErrorCode::ResourceLimit,
23 message: "表格结构或文字超过预算".into(),
24 });
25 }
26 if tokens.len() < 6
27 || tokens[..3] != ["<html>", "<body>", "<table>"]
28 || tokens[tokens.len() - 3..] != ["</table>", "</body>", "</html>"]
29 {
30 return Err(invalid("表格结构外层不完整"));
31 }
32 let inner = &tokens[3..tokens.len() - 3];
33 let mut index = 0;
34 let mut row = 0;
35 let mut in_row = false;
36 let mut column = 0;
37 let mut columns = 0;
38 let mut section: Option<&str> = None;
39 let mut occupied = std::collections::HashSet::new();
40 let mut cells = Vec::new();
41 while index < inner.len() {
42 match inner[index].as_str() {
43 "<thead>" | "<tbody>" if !in_row && section.is_none() => {
44 section = Some(&inner[index]);
45 }
46 "</thead>" if !in_row && section == Some("<thead>") => {
47 section = None;
48 }
49 "</tbody>" if !in_row && section == Some("<tbody>") => {
50 section = None;
51 }
52 "<tr>" if !in_row => {
53 in_row = true;
54 column = 0;
55 }
56 "</tr>" if in_row => {
57 in_row = false;
58 row += 1;
59 }
60 "<td></td>" | "<td>" | "<td" if in_row => {
61 let tag = inner[index].as_str();
62 let mut row_span = None;
63 let mut column_span = None;
64 if tag == "<td" {
65 index += 1;
66 while index < inner.len() && inner[index] != ">" {
67 let attribute = inner[index].trim();
68 let (name, value) = attribute
69 .split_once('=')
70 .ok_or_else(|| invalid("未知单元格属性"))?;
71 let number = value
72 .strip_prefix('"')
73 .and_then(|s| s.strip_suffix('"'))
74 .or_else(|| value.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
75 .ok_or_else(|| invalid("单元格跨度需要引号"))?;
76 if number.is_empty() || !number.bytes().all(|b| b.is_ascii_digit()) {
77 return Err(invalid("单元格跨度无效"));
78 }
79 let value: usize = number.parse().map_err(|_| invalid("单元格跨度溢出"))?;
80 if !(1..=1000).contains(&value) {
81 return Err(invalid("单元格跨度超限"));
82 }
83 let slot = match name {
84 "rowspan" => &mut row_span,
85 "colspan" => &mut column_span,
86 _ => return Err(invalid("未知单元格属性")),
87 };
88 if slot.replace(value).is_some() {
89 return Err(invalid("单元格属性重复"));
90 }
91 index += 1;
92 }
93 if index == inner.len() {
94 return Err(invalid("单元格开始标签未闭合"));
95 }
96 }
97 if tag != "<td></td>" {
98 index += 1;
99 if inner.get(index).map(String::as_str) != Some("</td>") {
100 return Err(invalid("单元格未闭合或包含未知结构"));
101 }
102 }
103 while occupied.contains(&(row, column)) {
104 column += 1;
105 }
106 let row_span = row_span.unwrap_or(1);
107 let column_span = column_span.unwrap_or(1);
108 if row + row_span > rapiddoc_core::resources::limit(1000)
109 || column + column_span > rapiddoc_core::resources::limit(1000)
110 || row_span * column_span + occupied.len()
111 > rapiddoc_core::resources::limit(100_000)
112 {
113 return Err(invalid("表格逻辑网格超过限制"));
114 }
115 for r in row..row + row_span {
116 for c in column..column + column_span {
117 if !occupied.insert((r, c)) {
118 return Err(invalid("表格单元格重复覆盖"));
119 }
120 }
121 }
122 let text = texts
123 .get(cells.len())
124 .ok_or_else(|| invalid("结构格数和单元格框数不匹配"))?
125 .clone();
126 cells.push(Cell {
127 row,
128 column,
129 row_span,
130 column_span,
131 text,
132 character_indices: vec![],
133 });
134 column += column_span;
135 columns = columns.max(column);
136 }
137 _ => return Err(invalid("未知或错位的表格 token")),
138 }
139 index += 1;
140 }
141 if in_row || section.is_some() || cells.len() != texts.len() {
142 return Err(invalid("结构未闭合或格数不匹配"));
143 }
144 let table = Table {
146 bounds: Rect::try_from([0., 0., columns as f64, row as f64])?,
147 rows: row,
148 columns,
149 cells,
150 };
151 crate::table_html(&table, 100_000)
152}