Skip to main content

document_svg/document/
turtle.rs

1//! Bounded RDF Turtle/N-Triples/N-Quads table preview.
2//!
3//! This adapter displays RDF statements as inert subject/predicate/object
4//! rows. Prefix declarations are shown as metadata, while inference, imports,
5//! remote resolution, blank-node expansion, and SPARQL execution are omitted.
6
7use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::document::html::{HtmlBlock, render_blocks_to_pages_with_warnings};
11use crate::error::{Error, Result};
12use crate::table::{TableAlign, TableData};
13
14const MAX_TURTLE_INPUT_BYTES: u64 = 64 * 1024 * 1024;
15const MAX_TURTLE_LINES: usize = 1_000_000;
16const MAX_TURTLE_LINE_BYTES: usize = 1024 * 1024;
17const MAX_TURTLE_TRIPLES: usize = 200_000;
18const MAX_TURTLE_CELL_CHARS: usize = 512;
19const MAX_TURTLE_TEXT_BYTES: usize = 64 * 1024 * 1024;
20type ParsedRdf = (Vec<String>, Vec<Vec<String>>, bool, Vec<String>);
21
22pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
23    let text = String::from_utf8_lossy(bytes);
24    text.lines().take(64).any(|line| {
25        let trimmed = line.trim_start();
26        trimmed.starts_with("@prefix")
27            || trimmed.starts_with("@base")
28            || trimmed.starts_with("PREFIX ")
29            || trimmed.starts_with("BASE ")
30    })
31}
32
33pub(crate) fn convert(
34    path: &Path,
35    options: &ConvertOptions,
36    sink: &mut dyn PageConsumer,
37) -> Result<Vec<String>> {
38    let bytes = read_limited_file(
39        path,
40        options.max_input_bytes.min(MAX_TURTLE_INPUT_BYTES),
41        "RDF input",
42    )?;
43    let text = std::str::from_utf8(&bytes)
44        .map_err(|error| Error::InvalidInput(format!("RDF input is not UTF-8: {error}")))?;
45    let (prefixes, mut rows, has_graph, mut warnings) = parse_statements(text)?;
46    if rows.is_empty() {
47        return Err(Error::InvalidInput(
48            "RDF input contains no renderable statements".into(),
49        ));
50    }
51    if !prefixes.is_empty() {
52        warnings.push(format!(
53            "RDF prefix declarations are displayed without namespace expansion ({})",
54            prefixes.len()
55        ));
56    }
57    let mut headers = vec!["Subject".into(), "Predicate".into(), "Object".into()];
58    if has_graph {
59        headers.push("Graph".into());
60        for row in &mut rows {
61            row.resize(4, String::new());
62        }
63    }
64    let table = TableData {
65        headers,
66        rows,
67        alignments: vec![TableAlign::Left; if has_graph { 4 } else { 3 }],
68        raw_source: String::new(),
69    };
70    let blocks = vec![
71        HtmlBlock::Heading {
72            level: 1,
73            text: "RDF statements".into(),
74        },
75        HtmlBlock::Table(table),
76    ];
77    render_blocks_to_pages_with_warnings(&blocks, sink, options, &warnings)?;
78    Ok(dedup_warnings(warnings))
79}
80
81fn parse_statements(text: &str) -> Result<ParsedRdf> {
82    let mut prefixes = Vec::new();
83    let mut rows = Vec::new();
84    let mut warnings = Vec::new();
85    let mut has_graph = false;
86    let mut subject = None::<String>;
87    let mut predicate = None::<String>;
88    let mut text_bytes = 0usize;
89    for (line_number, line) in text.lines().enumerate() {
90        if line_number >= MAX_TURTLE_LINES {
91            return Err(Error::LimitExceeded(format!(
92                "RDF input exceeds {MAX_TURTLE_LINES} lines"
93            )));
94        }
95        if line.len() > MAX_TURTLE_LINE_BYTES {
96            return Err(Error::LimitExceeded(format!(
97                "RDF line exceeds {MAX_TURTLE_LINE_BYTES} bytes"
98            )));
99        }
100        text_bytes = text_bytes.saturating_add(line.len());
101        if text_bytes > MAX_TURTLE_TEXT_BYTES {
102            return Err(Error::LimitExceeded(format!(
103                "RDF text exceeds {MAX_TURTLE_TEXT_BYTES} bytes"
104            )));
105        }
106        let trimmed = line.trim();
107        if trimmed.is_empty() || trimmed.starts_with('#') {
108            continue;
109        }
110        if trimmed.starts_with("@prefix")
111            || trimmed.starts_with("@base")
112            || trimmed.starts_with("PREFIX ")
113            || trimmed.starts_with("BASE ")
114        {
115            prefixes.push(truncate(trimmed));
116            continue;
117        }
118        let mut tokens = tokenize(trimmed);
119        if subject.is_none()
120            && !trimmed.contains(';')
121            && !trimmed.contains(',')
122            && tokens.len() >= 4
123        {
124            if tokens.last().is_some_and(|token| token == ".") {
125                tokens.pop();
126            }
127            if tokens.len() >= 4 {
128                if rows.len() >= MAX_TURTLE_TRIPLES {
129                    return Err(Error::LimitExceeded(format!(
130                        "RDF exceeds {MAX_TURTLE_TRIPLES} statements"
131                    )));
132                }
133                has_graph = true;
134                rows.push(
135                    tokens
136                        .into_iter()
137                        .take(4)
138                        .map(|value| truncate(&value))
139                        .collect(),
140                );
141                continue;
142            }
143        }
144        let mut delimiter = None::<char>;
145        for token in tokens {
146            let (term, token_delimiter) = split_delimiter(&token);
147            if term.is_empty() && token_delimiter.is_some() {
148                if token_delimiter == Some('.') {
149                    subject = None;
150                    predicate = None;
151                }
152                delimiter = token_delimiter;
153                continue;
154            }
155            if delimiter.is_none() && subject.is_none() {
156                subject = Some(term);
157                delimiter = token_delimiter;
158                continue;
159            }
160            if delimiter == Some('.') {
161                subject = Some(term);
162                delimiter = token_delimiter;
163                continue;
164            }
165            if predicate.is_none() {
166                predicate = Some(term);
167                delimiter = token_delimiter;
168                continue;
169            }
170            let Some(subject_value) = subject.clone() else {
171                continue;
172            };
173            let Some(predicate_value) = predicate.clone() else {
174                continue;
175            };
176            if rows.len() >= MAX_TURTLE_TRIPLES {
177                return Err(Error::LimitExceeded(format!(
178                    "RDF exceeds {MAX_TURTLE_TRIPLES} statements"
179                )));
180            }
181            rows.push(vec![
182                truncate(&subject_value),
183                truncate(&predicate_value),
184                truncate(&term),
185            ]);
186            match token_delimiter.or(delimiter) {
187                Some('.') => {
188                    subject = None;
189                    predicate = None;
190                    delimiter = None;
191                }
192                Some(';') => {
193                    predicate = None;
194                    delimiter = None;
195                }
196                Some(',') => {
197                    delimiter = None;
198                }
199                _ => {
200                    delimiter = None;
201                }
202            }
203        }
204        if let Some('.') = delimiter {
205            subject = None;
206            predicate = None;
207        }
208        if predicate.is_some() && subject.is_some() && !trimmed.ends_with('.') {
209            warnings.push(format!(
210                "RDF line {} continues a statement without a terminator",
211                line_number + 1
212            ));
213        }
214    }
215    if subject.is_some() || predicate.is_some() {
216        warnings.push("RDF input ended with an incomplete statement".into());
217    }
218    Ok((prefixes, rows, has_graph, warnings))
219}
220
221fn split_delimiter(token: &str) -> (String, Option<char>) {
222    let Some(last) = token.chars().last() else {
223        return (String::new(), None);
224    };
225    if matches!(last, '.' | ';' | ',') && !token.ends_with("...") {
226        let mut term = token.to_owned();
227        term.pop();
228        return (term, Some(last));
229    }
230    (token.to_owned(), None)
231}
232
233fn tokenize(line: &str) -> Vec<String> {
234    let mut tokens = Vec::new();
235    let mut current = String::new();
236    let mut quote = None::<char>;
237    let mut angle = false;
238    for character in line.chars() {
239        if let Some(delimiter) = quote {
240            current.push(character);
241            if character == delimiter {
242                quote = None;
243            }
244        } else if angle {
245            current.push(character);
246            if character == '>' {
247                angle = false;
248            }
249        } else if character == '"' || character == '\'' {
250            quote = Some(character);
251            current.push(character);
252        } else if character == '<' {
253            angle = true;
254            current.push(character);
255        } else if character.is_whitespace() {
256            if !current.is_empty() {
257                tokens.push(std::mem::take(&mut current));
258            }
259        } else {
260            current.push(character);
261        }
262    }
263    if !current.is_empty() {
264        tokens.push(current);
265    }
266    tokens
267}
268
269fn truncate(value: &str) -> String {
270    value.chars().take(MAX_TURTLE_CELL_CHARS).collect()
271}
272
273fn dedup_warnings(mut warnings: Vec<String>) -> Vec<String> {
274    warnings.sort();
275    warnings.dedup();
276    warnings
277}