Skip to main content

rapiddoc_render/
structure.rs

1//! 只接受 SLANet 表格语法,结构和 OCR 文字均不能注入任意 HTML。
2use 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
12/// 解析模型 token 成完整逻辑网格,再由通用安全表格输出器转义文字。
13/// 不纠正缺格、重叠、未闭合结构,也不接受模型输出的任意标签/属性。
14pub 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    // 仅供输出器验证拓扑的逻辑坐标,不作为模型像素坐标返回。
145    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}