oxttl/trig.rs
1//! A [TriG](https://www.w3.org/TR/trig/) streaming parser implemented by [`TriGParser`]
2//! and a serializer implemented by [`TriGSerializer`].
3
4use crate::lexer::N3Lexer;
5use crate::terse::TriGRecognizer;
6#[cfg(feature = "async-tokio")]
7use crate::toolkit::TokioAsyncReaderIterator;
8use crate::toolkit::{Parser, ReaderIterator, SliceIterator, TurtleParseError, TurtleSyntaxError};
9use oxiri::{Iri, IriParseError};
10use oxrdf::vocab::{rdf, xsd};
11use oxrdf::{
12 GraphName, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, Quad, QuadRef, Subject, TermRef,
13};
14use std::borrow::Cow;
15use std::collections::hash_map::Iter;
16use std::collections::{BTreeMap, HashMap};
17use std::fmt;
18use std::io::{self, Read, Write};
19#[cfg(feature = "async-tokio")]
20use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
21
22/// A [TriG](https://www.w3.org/TR/trig/) streaming parser.
23///
24/// Support for [TriG-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#trig-star) is available behind the `rdf-star` feature and the [`TriGParser::with_quoted_triples`] option.
25///
26/// Count the number of people:
27/// ```
28/// use oxrdf::vocab::rdf;
29/// use oxrdf::NamedNodeRef;
30/// use oxttl::TriGParser;
31///
32/// let file = br#"@base <http://example.com/> .
33/// @prefix schema: <http://schema.org/> .
34/// <foo> a schema:Person ;
35/// schema:name "Foo" .
36/// <bar> a schema:Person ;
37/// schema:name "Bar" ."#;
38///
39/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
40/// let mut count = 0;
41/// for quad in TriGParser::new().for_reader(file.as_ref()) {
42/// let quad = quad?;
43/// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
44/// count += 1;
45/// }
46/// }
47/// assert_eq!(2, count);
48/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
49/// ```
50#[derive(Default, Clone)]
51#[must_use]
52pub struct TriGParser {
53 unchecked: bool,
54 base: Option<Iri<String>>,
55 prefixes: HashMap<String, Iri<String>>,
56 #[cfg(feature = "rdf-star")]
57 with_quoted_triples: bool,
58}
59
60impl TriGParser {
61 /// Builds a new [`TriGParser`].
62 #[inline]
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 /// Assumes the file is valid to make parsing faster.
68 ///
69 /// It will skip some validations.
70 ///
71 /// Note that if the file is actually not valid, broken RDF might be emitted by the parser.
72 #[inline]
73 pub fn unchecked(mut self) -> Self {
74 self.unchecked = true;
75 self
76 }
77
78 #[inline]
79 pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Result<Self, IriParseError> {
80 self.base = Some(Iri::parse(base_iri.into())?);
81 Ok(self)
82 }
83
84 #[inline]
85 pub fn with_prefix(
86 mut self,
87 prefix_name: impl Into<String>,
88 prefix_iri: impl Into<String>,
89 ) -> Result<Self, IriParseError> {
90 self.prefixes
91 .insert(prefix_name.into(), Iri::parse(prefix_iri.into())?);
92 Ok(self)
93 }
94
95 /// Enables [TriG-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#trig-star).
96 #[cfg(feature = "rdf-star")]
97 #[inline]
98 pub fn with_quoted_triples(mut self) -> Self {
99 self.with_quoted_triples = true;
100 self
101 }
102
103 /// Parses a TriG file from a [`Read`] implementation.
104 ///
105 /// Count the number of people:
106 /// ```
107 /// use oxrdf::vocab::rdf;
108 /// use oxrdf::NamedNodeRef;
109 /// use oxttl::TriGParser;
110 ///
111 /// let file = br#"@base <http://example.com/> .
112 /// @prefix schema: <http://schema.org/> .
113 /// <foo> a schema:Person ;
114 /// schema:name "Foo" .
115 /// <bar> a schema:Person ;
116 /// schema:name "Bar" ."#;
117 ///
118 /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
119 /// let mut count = 0;
120 /// for quad in TriGParser::new().for_reader(file.as_ref()) {
121 /// let quad = quad?;
122 /// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
123 /// count += 1;
124 /// }
125 /// }
126 /// assert_eq!(2, count);
127 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
128 /// ```
129 pub fn for_reader<R: Read>(self, reader: R) -> ReaderTriGParser<R> {
130 ReaderTriGParser {
131 inner: self.low_level().parser.for_reader(reader),
132 }
133 }
134
135 /// Parses a TriG file from a [`AsyncRead`] implementation.
136 ///
137 /// Count the number of people:
138 /// ```
139 /// # #[tokio::main(flavor = "current_thread")]
140 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
141 /// use oxrdf::vocab::rdf;
142 /// use oxrdf::NamedNodeRef;
143 /// use oxttl::TriGParser;
144 ///
145 /// let file = br#"@base <http://example.com/> .
146 /// @prefix schema: <http://schema.org/> .
147 /// <foo> a schema:Person ;
148 /// schema:name "Foo" .
149 /// <bar> a schema:Person ;
150 /// schema:name "Bar" ."#;
151 ///
152 /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
153 /// let mut count = 0;
154 /// let mut parser = TriGParser::new().for_tokio_async_reader(file.as_ref());
155 /// while let Some(triple) = parser.next().await {
156 /// let triple = triple?;
157 /// if triple.predicate == rdf::TYPE && triple.object == schema_person.into() {
158 /// count += 1;
159 /// }
160 /// }
161 /// assert_eq!(2, count);
162 /// # Ok(())
163 /// # }
164 /// ```
165 #[cfg(feature = "async-tokio")]
166 pub fn for_tokio_async_reader<R: AsyncRead + Unpin>(
167 self,
168 reader: R,
169 ) -> TokioAsyncReaderTriGParser<R> {
170 TokioAsyncReaderTriGParser {
171 inner: self.low_level().parser.for_tokio_async_reader(reader),
172 }
173 }
174
175 /// Parses a TriG file from a byte slice.
176 ///
177 /// Count the number of people:
178 /// ```
179 /// use oxrdf::vocab::rdf;
180 /// use oxrdf::NamedNodeRef;
181 /// use oxttl::TriGParser;
182 ///
183 /// let file = br#"@base <http://example.com/> .
184 /// @prefix schema: <http://schema.org/> .
185 /// <foo> a schema:Person ;
186 /// schema:name "Foo" .
187 /// <bar> a schema:Person ;
188 /// schema:name "Bar" ."#;
189 ///
190 /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
191 /// let mut count = 0;
192 /// for quad in TriGParser::new().for_slice(file) {
193 /// let quad = quad?;
194 /// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
195 /// count += 1;
196 /// }
197 /// }
198 /// assert_eq!(2, count);
199 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
200 /// ```
201 pub fn for_slice(self, slice: &[u8]) -> SliceTriGParser<'_> {
202 SliceTriGParser {
203 inner: TriGRecognizer::new_parser(
204 slice,
205 true,
206 true,
207 #[cfg(feature = "rdf-star")]
208 self.with_quoted_triples,
209 self.unchecked,
210 self.base,
211 self.prefixes,
212 )
213 .into_iter(),
214 }
215 }
216
217 /// Allows to parse a TriG file by using a low-level API.
218 ///
219 /// Count the number of people:
220 /// ```
221 /// use oxrdf::vocab::rdf;
222 /// use oxrdf::NamedNodeRef;
223 /// use oxttl::TriGParser;
224 ///
225 /// let file: [&[u8]; 5] = [
226 /// b"@base <http://example.com/>",
227 /// b". @prefix schema: <http://schema.org/> .",
228 /// b"<foo> a schema:Person",
229 /// b" ; schema:name \"Foo\" . <bar>",
230 /// b" a schema:Person ; schema:name \"Bar\" .",
231 /// ];
232 ///
233 /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
234 /// let mut count = 0;
235 /// let mut parser = TriGParser::new().low_level();
236 /// let mut file_chunks = file.iter();
237 /// while !parser.is_end() {
238 /// // We feed more data to the parser
239 /// if let Some(chunk) = file_chunks.next() {
240 /// parser.extend_from_slice(chunk);
241 /// } else {
242 /// parser.end(); // It's finished
243 /// }
244 /// // We read as many quads from the parser as possible
245 /// while let Some(quad) = parser.parse_next() {
246 /// let quad = quad?;
247 /// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
248 /// count += 1;
249 /// }
250 /// }
251 /// }
252 /// assert_eq!(2, count);
253 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
254 /// ```
255 pub fn low_level(self) -> LowLevelTriGParser {
256 LowLevelTriGParser {
257 parser: TriGRecognizer::new_parser(
258 Vec::new(),
259 false,
260 true,
261 #[cfg(feature = "rdf-star")]
262 self.with_quoted_triples,
263 self.unchecked,
264 self.base,
265 self.prefixes,
266 ),
267 }
268 }
269}
270
271/// Parses a TriG file from a [`Read`] implementation.
272///
273/// Can be built using [`TriGParser::for_reader`].
274///
275/// Count the number of people:
276/// ```
277/// use oxrdf::vocab::rdf;
278/// use oxrdf::NamedNodeRef;
279/// use oxttl::TriGParser;
280///
281/// let file = br#"@base <http://example.com/> .
282/// @prefix schema: <http://schema.org/> .
283/// <foo> a schema:Person ;
284/// schema:name "Foo" .
285/// <bar> a schema:Person ;
286/// schema:name "Bar" ."#;
287///
288/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
289/// let mut count = 0;
290/// for quad in TriGParser::new().for_reader(file.as_ref()) {
291/// let quad = quad?;
292/// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
293/// count += 1;
294/// }
295/// }
296/// assert_eq!(2, count);
297/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
298/// ```
299#[must_use]
300pub struct ReaderTriGParser<R: Read> {
301 inner: ReaderIterator<R, TriGRecognizer>,
302}
303
304impl<R: Read> ReaderTriGParser<R> {
305 /// The list of IRI prefixes considered at the current step of the parsing.
306 ///
307 /// This method returns (prefix name, prefix value) tuples.
308 /// It is empty at the beginning of the parsing and gets updated when prefixes are encountered.
309 /// It should be full at the end of the parsing (but if a prefix is overridden, only the latest version will be returned).
310 ///
311 /// ```
312 /// use oxttl::TriGParser;
313 ///
314 /// let file = br#"@base <http://example.com/> .
315 /// @prefix schema: <http://schema.org/> .
316 /// <foo> a schema:Person ;
317 /// schema:name "Foo" ."#;
318 ///
319 /// let mut parser = TriGParser::new().for_reader(file.as_ref());
320 /// assert_eq!(parser.prefixes().collect::<Vec<_>>(), []); // No prefix at the beginning
321 ///
322 /// parser.next().unwrap()?; // We read the first triple
323 /// assert_eq!(
324 /// parser.prefixes().collect::<Vec<_>>(),
325 /// [("schema", "http://schema.org/")]
326 /// ); // There are now prefixes
327 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
328 /// ```
329 pub fn prefixes(&self) -> TriGPrefixesIter<'_> {
330 TriGPrefixesIter {
331 inner: self.inner.parser.context.prefixes(),
332 }
333 }
334
335 /// The base IRI considered at the current step of the parsing.
336 ///
337 /// ```
338 /// use oxttl::TriGParser;
339 ///
340 /// let file = br#"@base <http://example.com/> .
341 /// @prefix schema: <http://schema.org/> .
342 /// <foo> a schema:Person ;
343 /// schema:name "Foo" ."#;
344 ///
345 /// let mut parser = TriGParser::new().for_reader(file.as_ref());
346 /// assert!(parser.base_iri().is_none()); // No base at the beginning because none has been given to the parser.
347 ///
348 /// parser.next().unwrap()?; // We read the first triple
349 /// assert_eq!(parser.base_iri(), Some("http://example.com/")); // There is now a base IRI.
350 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
351 /// ```
352 pub fn base_iri(&self) -> Option<&str> {
353 self.inner
354 .parser
355 .context
356 .lexer_options
357 .base_iri
358 .as_ref()
359 .map(Iri::as_str)
360 }
361}
362
363impl<R: Read> Iterator for ReaderTriGParser<R> {
364 type Item = Result<Quad, TurtleParseError>;
365
366 fn next(&mut self) -> Option<Self::Item> {
367 self.inner.next()
368 }
369}
370
371/// Parses a TriG file from a [`AsyncRead`] implementation.
372///
373/// Can be built using [`TriGParser::for_tokio_async_reader`].
374///
375/// Count the number of people:
376/// ```
377/// # #[tokio::main(flavor = "current_thread")]
378/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
379/// use oxrdf::vocab::rdf;
380/// use oxrdf::NamedNodeRef;
381/// use oxttl::TriGParser;
382///
383/// let file = br#"@base <http://example.com/> .
384/// @prefix schema: <http://schema.org/> .
385/// <foo> a schema:Person ;
386/// schema:name "Foo" .
387/// <bar> a schema:Person ;
388/// schema:name "Bar" ."#;
389///
390/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
391/// let mut count = 0;
392/// let mut parser = TriGParser::new().for_tokio_async_reader(file.as_ref());
393/// while let Some(triple) = parser.next().await {
394/// let triple = triple?;
395/// if triple.predicate == rdf::TYPE && triple.object == schema_person.into() {
396/// count += 1;
397/// }
398/// }
399/// assert_eq!(2, count);
400/// # Ok(())
401/// # }
402/// ```
403#[cfg(feature = "async-tokio")]
404#[must_use]
405pub struct TokioAsyncReaderTriGParser<R: AsyncRead + Unpin> {
406 inner: TokioAsyncReaderIterator<R, TriGRecognizer>,
407}
408
409#[cfg(feature = "async-tokio")]
410impl<R: AsyncRead + Unpin> TokioAsyncReaderTriGParser<R> {
411 /// Reads the next triple or returns `None` if the file is finished.
412 pub async fn next(&mut self) -> Option<Result<Quad, TurtleParseError>> {
413 self.inner.next().await
414 }
415
416 /// The list of IRI prefixes considered at the current step of the parsing.
417 ///
418 /// This method returns (prefix name, prefix value) tuples.
419 /// It is empty at the beginning of the parsing and gets updated when prefixes are encountered.
420 /// It should be full at the end of the parsing (but if a prefix is overridden, only the latest version will be returned).
421 ///
422 /// ```
423 /// # #[tokio::main(flavor = "current_thread")]
424 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
425 /// use oxttl::TriGParser;
426 ///
427 /// let file = br#"@base <http://example.com/> .
428 /// @prefix schema: <http://schema.org/> .
429 /// <foo> a schema:Person ;
430 /// schema:name "Foo" ."#;
431 ///
432 /// let mut parser = TriGParser::new().for_tokio_async_reader(file.as_ref());
433 /// assert_eq!(parser.prefixes().collect::<Vec<_>>(), []); // No prefix at the beginning
434 ///
435 /// parser.next().await.unwrap()?; // We read the first triple
436 /// assert_eq!(
437 /// parser.prefixes().collect::<Vec<_>>(),
438 /// [("schema", "http://schema.org/")]
439 /// ); // There are now prefixes
440 /// # Ok(())
441 /// # }
442 /// ```
443 pub fn prefixes(&self) -> TriGPrefixesIter<'_> {
444 TriGPrefixesIter {
445 inner: self.inner.parser.context.prefixes(),
446 }
447 }
448
449 /// The base IRI considered at the current step of the parsing.
450 ///
451 /// ```
452 /// # #[tokio::main(flavor = "current_thread")]
453 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
454 /// use oxttl::TriGParser;
455 ///
456 /// let file = br#"@base <http://example.com/> .
457 /// @prefix schema: <http://schema.org/> .
458 /// <foo> a schema:Person ;
459 /// schema:name "Foo" ."#;
460 ///
461 /// let mut parser = TriGParser::new().for_tokio_async_reader(file.as_ref());
462 /// assert!(parser.base_iri().is_none()); // No base IRI at the beginning
463 ///
464 /// parser.next().await.unwrap()?; // We read the first triple
465 /// assert_eq!(parser.base_iri(), Some("http://example.com/")); // There is now a base IRI
466 /// # Ok(())
467 /// # }
468 /// ```
469 pub fn base_iri(&self) -> Option<&str> {
470 self.inner
471 .parser
472 .context
473 .lexer_options
474 .base_iri
475 .as_ref()
476 .map(Iri::as_str)
477 }
478}
479
480/// Parses a TriG file from a byte slice.
481///
482/// Can be built using [`TriGParser::for_slice`].
483///
484/// Count the number of people:
485/// ```
486/// use oxrdf::vocab::rdf;
487/// use oxrdf::NamedNodeRef;
488/// use oxttl::TriGParser;
489///
490/// let file = br#"@base <http://example.com/> .
491/// @prefix schema: <http://schema.org/> .
492/// <foo> a schema:Person ;
493/// schema:name "Foo" .
494/// <bar> a schema:Person ;
495/// schema:name "Bar" ."#;
496///
497/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
498/// let mut count = 0;
499/// for quad in TriGParser::new().for_slice(file) {
500/// let quad = quad?;
501/// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
502/// count += 1;
503/// }
504/// }
505/// assert_eq!(2, count);
506/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
507/// ```
508#[must_use]
509pub struct SliceTriGParser<'a> {
510 inner: SliceIterator<'a, TriGRecognizer>,
511}
512
513impl SliceTriGParser<'_> {
514 /// The list of IRI prefixes considered at the current step of the parsing.
515 ///
516 /// This method returns (prefix name, prefix value) tuples.
517 /// It is empty at the beginning of the parsing and gets updated when prefixes are encountered.
518 /// It should be full at the end of the parsing (but if a prefix is overridden, only the latest version will be returned).
519 ///
520 /// ```
521 /// use oxttl::TriGParser;
522 ///
523 /// let file = br#"@base <http://example.com/> .
524 /// @prefix schema: <http://schema.org/> .
525 /// <foo> a schema:Person ;
526 /// schema:name "Foo" ."#;
527 ///
528 /// let mut parser = TriGParser::new().for_slice(file);
529 /// assert_eq!(parser.prefixes().collect::<Vec<_>>(), []); // No prefix at the beginning
530 ///
531 /// parser.next().unwrap()?; // We read the first triple
532 /// assert_eq!(
533 /// parser.prefixes().collect::<Vec<_>>(),
534 /// [("schema", "http://schema.org/")]
535 /// ); // There are now prefixes
536 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
537 /// ```
538 pub fn prefixes(&self) -> TriGPrefixesIter<'_> {
539 TriGPrefixesIter {
540 inner: self.inner.parser.context.prefixes(),
541 }
542 }
543
544 /// The base IRI considered at the current step of the parsing.
545 ///
546 /// ```
547 /// use oxttl::TriGParser;
548 ///
549 /// let file = br#"@base <http://example.com/> .
550 /// @prefix schema: <http://schema.org/> .
551 /// <foo> a schema:Person ;
552 /// schema:name "Foo" ."#;
553 ///
554 /// let mut parser = TriGParser::new().for_slice(file);
555 /// assert!(parser.base_iri().is_none()); // No base at the beginning because none has been given to the parser.
556 ///
557 /// parser.next().unwrap()?; // We read the first triple
558 /// assert_eq!(parser.base_iri(), Some("http://example.com/")); // There is now a base IRI.
559 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
560 /// ```
561 pub fn base_iri(&self) -> Option<&str> {
562 self.inner
563 .parser
564 .context
565 .lexer_options
566 .base_iri
567 .as_ref()
568 .map(Iri::as_str)
569 }
570}
571
572impl Iterator for SliceTriGParser<'_> {
573 type Item = Result<Quad, TurtleSyntaxError>;
574
575 fn next(&mut self) -> Option<Self::Item> {
576 self.inner.next()
577 }
578}
579
580/// Parses a TriG file by using a low-level API.
581///
582/// Can be built using [`TriGParser::low_level`].
583///
584/// Count the number of people:
585/// ```
586/// use oxrdf::vocab::rdf;
587/// use oxrdf::NamedNodeRef;
588/// use oxttl::TriGParser;
589///
590/// let file: [&[u8]; 5] = [
591/// b"@base <http://example.com/>",
592/// b". @prefix schema: <http://schema.org/> .",
593/// b"<foo> a schema:Person",
594/// b" ; schema:name \"Foo\" . <bar>",
595/// b" a schema:Person ; schema:name \"Bar\" .",
596/// ];
597///
598/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
599/// let mut count = 0;
600/// let mut parser = TriGParser::new().low_level();
601/// let mut file_chunks = file.iter();
602/// while !parser.is_end() {
603/// // We feed more data to the parser
604/// if let Some(chunk) = file_chunks.next() {
605/// parser.extend_from_slice(chunk);
606/// } else {
607/// parser.end(); // It's finished
608/// }
609/// // We read as many quads from the parser as possible
610/// while let Some(quad) = parser.parse_next() {
611/// let quad = quad?;
612/// if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
613/// count += 1;
614/// }
615/// }
616/// }
617/// assert_eq!(2, count);
618/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
619/// ```
620pub struct LowLevelTriGParser {
621 parser: Parser<Vec<u8>, TriGRecognizer>,
622}
623
624impl LowLevelTriGParser {
625 /// Adds some extra bytes to the parser. Should be called when [`parse_next`](Self::parse_next) returns [`None`] and there is still unread data.
626 pub fn extend_from_slice(&mut self, other: &[u8]) {
627 self.parser.extend_from_slice(other)
628 }
629
630 /// Tell the parser that the file is finished.
631 ///
632 /// This triggers the parsing of the final bytes and might lead [`parse_next`](Self::parse_next) to return some extra values.
633 pub fn end(&mut self) {
634 self.parser.end()
635 }
636
637 /// Returns if the parsing is finished i.e. [`end`](Self::end) has been called and [`parse_next`](Self::parse_next) is always going to return `None`.
638 pub fn is_end(&self) -> bool {
639 self.parser.is_end()
640 }
641
642 /// Attempt to parse a new quad from the already provided data.
643 ///
644 /// Returns [`None`] if the parsing is finished or more data is required.
645 /// If it is the case more data should be fed using [`extend_from_slice`](Self::extend_from_slice).
646 pub fn parse_next(&mut self) -> Option<Result<Quad, TurtleSyntaxError>> {
647 self.parser.parse_next()
648 }
649
650 /// The list of IRI prefixes considered at the current step of the parsing.
651 ///
652 /// This method returns (prefix name, prefix value) tuples.
653 /// It is empty at the beginning of the parsing and gets updated when prefixes are encountered.
654 /// It should be full at the end of the parsing (but if a prefix is overridden, only the latest version will be returned).
655 ///
656 /// ```
657 /// use oxttl::TriGParser;
658 ///
659 /// let file = br#"@base <http://example.com/> .
660 /// @prefix schema: <http://schema.org/> .
661 /// <foo> a schema:Person ;
662 /// schema:name "Foo" ."#;
663 ///
664 /// let mut parser = TriGParser::new().low_level();
665 /// parser.extend_from_slice(file);
666 /// assert_eq!(parser.prefixes().collect::<Vec<_>>(), []); // No prefix at the beginning
667 ///
668 /// parser.parse_next().unwrap()?; // We read the first triple
669 /// assert_eq!(
670 /// parser.prefixes().collect::<Vec<_>>(),
671 /// [("schema", "http://schema.org/")]
672 /// ); // There are now prefixes
673 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
674 /// ```
675 pub fn prefixes(&self) -> TriGPrefixesIter<'_> {
676 TriGPrefixesIter {
677 inner: self.parser.context.prefixes(),
678 }
679 }
680
681 /// The base IRI considered at the current step of the parsing.
682 ///
683 /// ```
684 /// use oxttl::TriGParser;
685 ///
686 /// let file = br#"@base <http://example.com/> .
687 /// @prefix schema: <http://schema.org/> .
688 /// <foo> a schema:Person ;
689 /// schema:name "Foo" ."#;
690 ///
691 /// let mut parser = TriGParser::new().low_level();
692 /// parser.extend_from_slice(file);
693 /// assert!(parser.base_iri().is_none()); // No base IRI at the beginning
694 ///
695 /// parser.parse_next().unwrap()?; // We read the first triple
696 /// assert_eq!(parser.base_iri(), Some("http://example.com/")); // There is now a base IRI
697 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
698 /// ```
699 pub fn base_iri(&self) -> Option<&str> {
700 self.parser
701 .context
702 .lexer_options
703 .base_iri
704 .as_ref()
705 .map(Iri::as_str)
706 }
707}
708
709/// Iterator on the file prefixes.
710///
711/// See [`LowLevelTriGParser::prefixes`].
712pub struct TriGPrefixesIter<'a> {
713 inner: Iter<'a, String, Iri<String>>,
714}
715
716impl<'a> Iterator for TriGPrefixesIter<'a> {
717 type Item = (&'a str, &'a str);
718
719 #[inline]
720 fn next(&mut self) -> Option<Self::Item> {
721 let (key, value) = self.inner.next()?;
722 Some((key.as_str(), value.as_str()))
723 }
724
725 #[inline]
726 fn size_hint(&self) -> (usize, Option<usize>) {
727 self.inner.size_hint()
728 }
729}
730
731/// A [TriG](https://www.w3.org/TR/trig/) serializer.
732///
733/// Support for [TriG-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#trig-star) is available behind the `rdf-star` feature.
734///
735/// ```
736/// use oxrdf::{NamedNodeRef, QuadRef};
737/// use oxrdf::vocab::rdf;
738/// use oxttl::TriGSerializer;
739///
740/// let mut serializer = TriGSerializer::new()
741/// .with_prefix("schema", "http://schema.org/")?
742/// .for_writer(Vec::new());
743/// serializer.serialize_quad(QuadRef::new(
744/// NamedNodeRef::new("http://example.com#me")?,
745/// rdf::TYPE,
746/// NamedNodeRef::new("http://schema.org/Person")?,
747/// NamedNodeRef::new("http://example.com")?,
748/// ))?;
749/// assert_eq!(
750/// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
751/// serializer.finish()?.as_slice()
752/// );
753/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
754/// ```
755#[derive(Default, Clone)]
756#[must_use]
757pub struct TriGSerializer {
758 base_iri: Option<Iri<String>>,
759 prefixes: BTreeMap<String, String>,
760}
761
762impl TriGSerializer {
763 /// Builds a new [`TriGSerializer`].
764 #[inline]
765 pub fn new() -> Self {
766 Self {
767 base_iri: None,
768 prefixes: BTreeMap::new(),
769 }
770 }
771
772 #[inline]
773 pub fn with_prefix(
774 mut self,
775 prefix_name: impl Into<String>,
776 prefix_iri: impl Into<String>,
777 ) -> Result<Self, IriParseError> {
778 self.prefixes.insert(
779 Iri::parse(prefix_iri.into())?.into_inner(),
780 prefix_name.into(),
781 );
782 Ok(self)
783 }
784
785 /// Adds a base IRI to the serialization.
786 ///
787 /// ```
788 /// use oxrdf::vocab::rdf;
789 /// use oxrdf::{NamedNodeRef, QuadRef};
790 /// use oxttl::TriGSerializer;
791 ///
792 /// let mut serializer = TriGSerializer::new()
793 /// .with_base_iri("http://example.com")?
794 /// .with_prefix("ex", "http://example.com/ns#")?
795 /// .for_writer(Vec::new());
796 /// serializer.serialize_quad(QuadRef::new(
797 /// NamedNodeRef::new("http://example.com/me")?,
798 /// rdf::TYPE,
799 /// NamedNodeRef::new("http://example.com/ns#Person")?,
800 /// NamedNodeRef::new("http://example.com")?,
801 /// ))?;
802 /// assert_eq!(
803 /// b"@base <http://example.com> .\n@prefix ex: </ns#> .\n<> {\n\t</me> a ex:Person .\n}\n",
804 /// serializer.finish()?.as_slice()
805 /// );
806 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
807 /// ```
808 #[inline]
809 pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Result<Self, IriParseError> {
810 self.base_iri = Some(Iri::parse(base_iri.into())?);
811 Ok(self)
812 }
813
814 /// Writes a TriG file to a [`Write`] implementation.
815 ///
816 /// ```
817 /// use oxrdf::{NamedNodeRef, QuadRef};
818 /// use oxrdf::vocab::rdf;
819 /// use oxttl::TriGSerializer;
820 ///
821 /// let mut serializer = TriGSerializer::new()
822 /// .with_prefix("schema", "http://schema.org/")?
823 /// .for_writer(Vec::new());
824 /// serializer.serialize_quad(QuadRef::new(
825 /// NamedNodeRef::new("http://example.com#me")?,
826 /// rdf::TYPE,
827 /// NamedNodeRef::new("http://schema.org/Person")?,
828 /// NamedNodeRef::new("http://example.com")?,
829 /// ))?;
830 /// assert_eq!(
831 /// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
832 /// serializer.finish()?.as_slice()
833 /// );
834 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
835 /// ```
836 pub fn for_writer<W: Write>(self, writer: W) -> WriterTriGSerializer<W> {
837 WriterTriGSerializer {
838 writer,
839 low_level_writer: self.low_level(),
840 }
841 }
842
843 /// Writes a TriG file to a [`AsyncWrite`] implementation.
844 ///
845 /// ```
846 /// # #[tokio::main(flavor = "current_thread")]
847 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
848 /// use oxrdf::{NamedNodeRef, QuadRef};
849 /// use oxrdf::vocab::rdf;
850 /// use oxttl::TriGSerializer;
851 ///
852 /// let mut serializer = TriGSerializer::new()
853 /// .with_prefix("schema", "http://schema.org/")?
854 /// .for_tokio_async_writer(Vec::new());
855 /// serializer
856 /// .serialize_quad(QuadRef::new(
857 /// NamedNodeRef::new("http://example.com#me")?,
858 /// rdf::TYPE,
859 /// NamedNodeRef::new("http://schema.org/Person")?,
860 /// NamedNodeRef::new("http://example.com")?,
861 /// ))
862 /// .await?;
863 /// assert_eq!(
864 /// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
865 /// serializer.finish().await?.as_slice()
866 /// );
867 /// # Ok(())
868 /// # }
869 /// ```
870 #[cfg(feature = "async-tokio")]
871 pub fn for_tokio_async_writer<W: AsyncWrite + Unpin>(
872 self,
873 writer: W,
874 ) -> TokioAsyncWriterTriGSerializer<W> {
875 TokioAsyncWriterTriGSerializer {
876 writer,
877 low_level_writer: self.low_level(),
878 buffer: Vec::new(),
879 }
880 }
881
882 /// Builds a low-level TriG writer.
883 ///
884 /// ```
885 /// use oxrdf::{NamedNodeRef, QuadRef};
886 /// use oxrdf::vocab::rdf;
887 /// use oxttl::TriGSerializer;
888 ///
889 /// let mut buf = Vec::new();
890 /// let mut serializer = TriGSerializer::new()
891 /// .with_prefix("schema", "http://schema.org/")?
892 /// .low_level();
893 /// serializer.serialize_quad(
894 /// QuadRef::new(
895 /// NamedNodeRef::new("http://example.com#me")?,
896 /// rdf::TYPE,
897 /// NamedNodeRef::new("http://schema.org/Person")?,
898 /// NamedNodeRef::new("http://example.com")?,
899 /// ),
900 /// &mut buf,
901 /// )?;
902 /// serializer.finish(&mut buf)?;
903 /// assert_eq!(
904 /// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
905 /// buf.as_slice()
906 /// );
907 /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
908 /// ```
909 pub fn low_level(self) -> LowLevelTriGSerializer {
910 LowLevelTriGSerializer {
911 prefixes: self.prefixes,
912 base_iri: self.base_iri,
913 prelude_written: false,
914 current_graph_name: GraphName::DefaultGraph,
915 current_subject_predicate: None,
916 }
917 }
918}
919
920/// Writes a TriG file to a [`Write`] implementation.
921///
922/// Can be built using [`TriGSerializer::for_writer`].
923///
924/// ```
925/// use oxrdf::{NamedNodeRef, QuadRef};
926/// use oxrdf::vocab::rdf;
927/// use oxttl::TriGSerializer;
928///
929/// let mut serializer = TriGSerializer::new()
930/// .with_prefix("schema", "http://schema.org/")?
931/// .for_writer(Vec::new());
932/// serializer.serialize_quad(QuadRef::new(
933/// NamedNodeRef::new("http://example.com#me")?,
934/// rdf::TYPE,
935/// NamedNodeRef::new("http://schema.org/Person")?,
936/// NamedNodeRef::new("http://example.com")?,
937/// ))?;
938/// assert_eq!(
939/// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
940/// serializer.finish()?.as_slice()
941/// );
942/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
943/// ```
944#[must_use]
945pub struct WriterTriGSerializer<W: Write> {
946 writer: W,
947 low_level_writer: LowLevelTriGSerializer,
948}
949
950impl<W: Write> WriterTriGSerializer<W> {
951 /// Writes an extra quad.
952 pub fn serialize_quad<'a>(&mut self, q: impl Into<QuadRef<'a>>) -> io::Result<()> {
953 self.low_level_writer.serialize_quad(q, &mut self.writer)
954 }
955
956 /// Ends the write process and returns the underlying [`Write`].
957 pub fn finish(mut self) -> io::Result<W> {
958 self.low_level_writer.finish(&mut self.writer)?;
959 Ok(self.writer)
960 }
961}
962
963/// Writes a TriG file to a [`AsyncWrite`] implementation.
964///
965/// Can be built using [`TriGSerializer::for_tokio_async_writer`].
966///
967/// ```
968/// # #[tokio::main(flavor = "current_thread")]
969/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
970/// use oxrdf::{NamedNodeRef, QuadRef};
971/// use oxrdf::vocab::rdf;
972/// use oxttl::TriGSerializer;
973///
974/// let mut serializer = TriGSerializer::new()
975/// .with_prefix("schema", "http://schema.org/")?
976/// .for_tokio_async_writer(Vec::new());
977/// serializer
978/// .serialize_quad(QuadRef::new(
979/// NamedNodeRef::new("http://example.com#me")?,
980/// rdf::TYPE,
981/// NamedNodeRef::new("http://schema.org/Person")?,
982/// NamedNodeRef::new("http://example.com")?,
983/// ))
984/// .await?;
985/// assert_eq!(
986/// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
987/// serializer.finish().await?.as_slice()
988/// );
989/// # Ok(())
990/// # }
991/// ```
992#[cfg(feature = "async-tokio")]
993#[must_use]
994pub struct TokioAsyncWriterTriGSerializer<W: AsyncWrite + Unpin> {
995 writer: W,
996 low_level_writer: LowLevelTriGSerializer,
997 buffer: Vec<u8>,
998}
999
1000#[cfg(feature = "async-tokio")]
1001impl<W: AsyncWrite + Unpin> TokioAsyncWriterTriGSerializer<W> {
1002 /// Writes an extra quad.
1003 pub async fn serialize_quad<'a>(&mut self, q: impl Into<QuadRef<'a>>) -> io::Result<()> {
1004 self.low_level_writer.serialize_quad(q, &mut self.buffer)?;
1005 self.writer.write_all(&self.buffer).await?;
1006 self.buffer.clear();
1007 Ok(())
1008 }
1009
1010 /// Ends the write process and returns the underlying [`Write`].
1011 pub async fn finish(mut self) -> io::Result<W> {
1012 self.low_level_writer.finish(&mut self.buffer)?;
1013 self.writer.write_all(&self.buffer).await?;
1014 self.buffer.clear();
1015 Ok(self.writer)
1016 }
1017}
1018
1019/// Writes a TriG file by using a low-level API.
1020///
1021/// Can be built using [`TriGSerializer::low_level`].
1022///
1023/// ```
1024/// use oxrdf::{NamedNodeRef, QuadRef};
1025/// use oxrdf::vocab::rdf;
1026/// use oxttl::TriGSerializer;
1027///
1028/// let mut buf = Vec::new();
1029/// let mut serializer = TriGSerializer::new()
1030/// .with_prefix("schema", "http://schema.org/")?
1031/// .low_level();
1032/// serializer.serialize_quad(
1033/// QuadRef::new(
1034/// NamedNodeRef::new("http://example.com#me")?,
1035/// rdf::TYPE,
1036/// NamedNodeRef::new("http://schema.org/Person")?,
1037/// NamedNodeRef::new("http://example.com")?,
1038/// ),
1039/// &mut buf,
1040/// )?;
1041/// serializer.finish(&mut buf)?;
1042/// assert_eq!(
1043/// b"@prefix schema: <http://schema.org/> .\n<http://example.com> {\n\t<http://example.com#me> a schema:Person .\n}\n",
1044/// buf.as_slice()
1045/// );
1046/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
1047/// ```
1048pub struct LowLevelTriGSerializer {
1049 prefixes: BTreeMap<String, String>,
1050 base_iri: Option<Iri<String>>,
1051 prelude_written: bool,
1052 current_graph_name: GraphName,
1053 current_subject_predicate: Option<(Subject, NamedNode)>,
1054}
1055
1056impl LowLevelTriGSerializer {
1057 /// Writes an extra quad.
1058 pub fn serialize_quad<'a>(
1059 &mut self,
1060 q: impl Into<QuadRef<'a>>,
1061 mut writer: impl Write,
1062 ) -> io::Result<()> {
1063 if !self.prelude_written {
1064 self.prelude_written = true;
1065 if let Some(base_iri) = &self.base_iri {
1066 writeln!(writer, "@base <{base_iri}> .")?;
1067 }
1068 for (prefix_iri, prefix_name) in &self.prefixes {
1069 writeln!(
1070 writer,
1071 "@prefix {prefix_name}: <{}> .",
1072 relative_iri(prefix_iri, &self.base_iri)
1073 )?;
1074 }
1075 }
1076 let q = q.into();
1077 if q.graph_name == self.current_graph_name.as_ref() {
1078 if let Some((current_subject, current_predicate)) =
1079 self.current_subject_predicate.take()
1080 {
1081 if q.subject == current_subject.as_ref() {
1082 if q.predicate == current_predicate {
1083 self.current_subject_predicate = Some((current_subject, current_predicate));
1084 write!(writer, " , {}", self.term(q.object))
1085 } else {
1086 self.current_subject_predicate =
1087 Some((current_subject, q.predicate.into_owned()));
1088 writeln!(writer, " ;")?;
1089 if !self.current_graph_name.is_default_graph() {
1090 write!(writer, "\t")?;
1091 }
1092 write!(
1093 writer,
1094 "\t{} {}",
1095 self.predicate(q.predicate),
1096 self.term(q.object)
1097 )
1098 }
1099 } else {
1100 self.current_subject_predicate =
1101 Some((q.subject.into_owned(), q.predicate.into_owned()));
1102 writeln!(writer, " .")?;
1103 if !self.current_graph_name.is_default_graph() {
1104 write!(writer, "\t")?;
1105 }
1106 write!(
1107 writer,
1108 "{} {} {}",
1109 self.term(q.subject),
1110 self.predicate(q.predicate),
1111 self.term(q.object)
1112 )
1113 }
1114 } else {
1115 self.current_subject_predicate =
1116 Some((q.subject.into_owned(), q.predicate.into_owned()));
1117 if !self.current_graph_name.is_default_graph() {
1118 write!(writer, "\t")?;
1119 }
1120 write!(
1121 writer,
1122 "{} {} {}",
1123 self.term(q.subject),
1124 self.predicate(q.predicate),
1125 self.term(q.object)
1126 )
1127 }
1128 } else {
1129 if self.current_subject_predicate.is_some() {
1130 writeln!(writer, " .")?;
1131 }
1132 if !self.current_graph_name.is_default_graph() {
1133 writeln!(writer, "}}")?;
1134 }
1135 self.current_graph_name = q.graph_name.into_owned();
1136 self.current_subject_predicate =
1137 Some((q.subject.into_owned(), q.predicate.into_owned()));
1138 match self.current_graph_name.as_ref() {
1139 GraphNameRef::NamedNode(g) => {
1140 writeln!(writer, "{} {{", self.term(g))?;
1141 write!(writer, "\t")?;
1142 }
1143 GraphNameRef::BlankNode(g) => {
1144 writeln!(writer, "{} {{", self.term(g))?;
1145 write!(writer, "\t")?;
1146 }
1147 GraphNameRef::DefaultGraph => (),
1148 }
1149
1150 write!(
1151 writer,
1152 "{} {} {}",
1153 self.term(q.subject),
1154 self.predicate(q.predicate),
1155 self.term(q.object)
1156 )
1157 }
1158 }
1159
1160 fn predicate<'a>(&'a self, named_node: impl Into<NamedNodeRef<'a>>) -> TurtlePredicate<'a> {
1161 TurtlePredicate {
1162 named_node: named_node.into(),
1163 prefixes: &self.prefixes,
1164 base_iri: &self.base_iri,
1165 }
1166 }
1167
1168 fn term<'a>(&'a self, term: impl Into<TermRef<'a>>) -> TurtleTerm<'a> {
1169 TurtleTerm {
1170 term: term.into(),
1171 prefixes: &self.prefixes,
1172 base_iri: &self.base_iri,
1173 }
1174 }
1175
1176 /// Finishes to write the file.
1177 pub fn finish(&mut self, mut writer: impl Write) -> io::Result<()> {
1178 if self.current_subject_predicate.is_some() {
1179 writeln!(writer, " .")?;
1180 }
1181 if !self.current_graph_name.is_default_graph() {
1182 writeln!(writer, "}}")?;
1183 }
1184 Ok(())
1185 }
1186}
1187
1188struct TurtlePredicate<'a> {
1189 named_node: NamedNodeRef<'a>,
1190 prefixes: &'a BTreeMap<String, String>,
1191 base_iri: &'a Option<Iri<String>>,
1192}
1193
1194impl fmt::Display for TurtlePredicate<'_> {
1195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1196 if self.named_node == rdf::TYPE {
1197 f.write_str("a")
1198 } else {
1199 TurtleTerm {
1200 term: self.named_node.into(),
1201 prefixes: self.prefixes,
1202 base_iri: self.base_iri,
1203 }
1204 .fmt(f)
1205 }
1206 }
1207}
1208
1209struct TurtleTerm<'a> {
1210 term: TermRef<'a>,
1211 prefixes: &'a BTreeMap<String, String>,
1212 base_iri: &'a Option<Iri<String>>,
1213}
1214
1215impl fmt::Display for TurtleTerm<'_> {
1216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1217 match self.term {
1218 TermRef::NamedNode(v) => {
1219 for (prefix_iri, prefix_name) in self.prefixes {
1220 if let Some(local_name) = v.as_str().strip_prefix(prefix_iri) {
1221 if local_name.is_empty() {
1222 return write!(f, "{prefix_name}:");
1223 } else if let Some(escaped_local_name) = escape_local_name(local_name) {
1224 return write!(f, "{prefix_name}:{escaped_local_name}");
1225 }
1226 }
1227 }
1228 write!(f, "<{}>", relative_iri(v.as_str(), self.base_iri))
1229 }
1230 TermRef::BlankNode(v) => write!(f, "{v}"),
1231 TermRef::Literal(v) => {
1232 let value = v.value();
1233 let inline = match v.datatype() {
1234 xsd::BOOLEAN => is_turtle_boolean(value),
1235 xsd::INTEGER => is_turtle_integer(value),
1236 xsd::DECIMAL => is_turtle_decimal(value),
1237 xsd::DOUBLE => is_turtle_double(value),
1238 _ => false,
1239 };
1240 if inline {
1241 f.write_str(value)
1242 } else if v.is_plain() {
1243 write!(f, "{v}")
1244 } else {
1245 write!(
1246 f,
1247 "{}^^{}",
1248 LiteralRef::new_simple_literal(v.value()),
1249 TurtleTerm {
1250 term: v.datatype().into(),
1251 prefixes: self.prefixes,
1252 base_iri: self.base_iri,
1253 }
1254 )
1255 }
1256 }
1257 #[cfg(feature = "rdf-star")]
1258 TermRef::Triple(t) => {
1259 write!(
1260 f,
1261 "<< {} {} {} >>",
1262 TurtleTerm {
1263 term: t.subject.as_ref().into(),
1264 prefixes: self.prefixes,
1265 base_iri: self.base_iri,
1266 },
1267 TurtleTerm {
1268 term: t.predicate.as_ref().into(),
1269 prefixes: self.prefixes,
1270 base_iri: self.base_iri,
1271 },
1272 TurtleTerm {
1273 term: t.object.as_ref(),
1274 prefixes: self.prefixes,
1275 base_iri: self.base_iri,
1276 }
1277 )
1278 }
1279 }
1280 }
1281}
1282
1283fn relative_iri<'a>(iri: &'a str, base_iri: &Option<Iri<String>>) -> Cow<'a, str> {
1284 if let Some(base_iri) = base_iri {
1285 if let Ok(relative) = base_iri.relativize(&Iri::parse_unchecked(iri)) {
1286 return relative.into_inner().into();
1287 }
1288 }
1289 iri.into()
1290}
1291
1292fn is_turtle_boolean(value: &str) -> bool {
1293 matches!(value, "true" | "false")
1294}
1295
1296fn is_turtle_integer(value: &str) -> bool {
1297 // [19] INTEGER ::= [+-]? [0-9]+
1298 let mut value = value.as_bytes();
1299 if let Some(v) = value.strip_prefix(b"+") {
1300 value = v;
1301 } else if let Some(v) = value.strip_prefix(b"-") {
1302 value = v;
1303 }
1304 !value.is_empty() && value.iter().all(u8::is_ascii_digit)
1305}
1306
1307fn is_turtle_decimal(value: &str) -> bool {
1308 // [20] DECIMAL ::= [+-]? [0-9]* '.' [0-9]+
1309 let mut value = value.as_bytes();
1310 if let Some(v) = value.strip_prefix(b"+") {
1311 value = v;
1312 } else if let Some(v) = value.strip_prefix(b"-") {
1313 value = v;
1314 }
1315 while value.first().is_some_and(u8::is_ascii_digit) {
1316 value = &value[1..];
1317 }
1318 let Some(value) = value.strip_prefix(b".") else {
1319 return false;
1320 };
1321 !value.is_empty() && value.iter().all(u8::is_ascii_digit)
1322}
1323
1324fn is_turtle_double(value: &str) -> bool {
1325 // [21] DOUBLE ::= [+-]? ([0-9]+ '.' [0-9]* EXPONENT | '.' [0-9]+ EXPONENT | [0-9]+ EXPONENT)
1326 // [154s] EXPONENT ::= [eE] [+-]? [0-9]+
1327 let mut value = value.as_bytes();
1328 if let Some(v) = value.strip_prefix(b"+") {
1329 value = v;
1330 } else if let Some(v) = value.strip_prefix(b"-") {
1331 value = v;
1332 }
1333 let mut with_before = false;
1334 while value.first().is_some_and(u8::is_ascii_digit) {
1335 value = &value[1..];
1336 with_before = true;
1337 }
1338 let mut with_after = false;
1339 if let Some(v) = value.strip_prefix(b".") {
1340 value = v;
1341 while value.first().is_some_and(u8::is_ascii_digit) {
1342 value = &value[1..];
1343 with_after = true;
1344 }
1345 }
1346 if let Some(v) = value.strip_prefix(b"e") {
1347 value = v;
1348 } else if let Some(v) = value.strip_prefix(b"E") {
1349 value = v;
1350 } else {
1351 return false;
1352 }
1353 if let Some(v) = value.strip_prefix(b"+") {
1354 value = v;
1355 } else if let Some(v) = value.strip_prefix(b"-") {
1356 value = v;
1357 }
1358 (with_before || with_after) && !value.is_empty() && value.iter().all(u8::is_ascii_digit)
1359}
1360
1361fn escape_local_name(value: &str) -> Option<String> {
1362 // TODO: PLX
1363 // [168s] PN_LOCAL ::= (PN_CHARS_U | ':' | [0-9] | PLX) ((PN_CHARS | '.' | ':' | PLX)* (PN_CHARS | ':' | PLX))?
1364 let mut output = String::with_capacity(value.len());
1365 let mut chars = value.chars();
1366 let first = chars.next()?;
1367 if N3Lexer::is_possible_pn_chars_u(first) || first == ':' || first.is_ascii_digit() {
1368 output.push(first);
1369 } else if can_be_escaped_in_local_name(first) {
1370 output.push('\\');
1371 output.push(first);
1372 } else {
1373 return None;
1374 }
1375
1376 while let Some(c) = chars.next() {
1377 if N3Lexer::is_possible_pn_chars(c) || c == ':' || (c == '.' && !chars.as_str().is_empty())
1378 {
1379 output.push(c);
1380 } else if can_be_escaped_in_local_name(c) {
1381 output.push('\\');
1382 output.push(c);
1383 } else {
1384 return None;
1385 }
1386 }
1387
1388 Some(output)
1389}
1390
1391fn can_be_escaped_in_local_name(c: char) -> bool {
1392 matches!(
1393 c,
1394 '_' | '~'
1395 | '.'
1396 | '-'
1397 | '!'
1398 | '$'
1399 | '&'
1400 | '\''
1401 | '('
1402 | ')'
1403 | '*'
1404 | '+'
1405 | ','
1406 | ';'
1407 | '='
1408 | '/'
1409 | '?'
1410 | '#'
1411 | '@'
1412 | '%'
1413 )
1414}
1415
1416#[cfg(test)]
1417#[allow(clippy::panic_in_result_fn)]
1418mod tests {
1419 use super::*;
1420 use oxrdf::BlankNodeRef;
1421
1422 #[test]
1423 fn test_write() -> io::Result<()> {
1424 let mut serializer = TriGSerializer::new()
1425 .with_prefix("ex", "http://example.com/")
1426 .unwrap()
1427 .for_writer(Vec::new());
1428 serializer.serialize_quad(QuadRef::new(
1429 NamedNodeRef::new_unchecked("http://example.com/s"),
1430 NamedNodeRef::new_unchecked("http://example.com/p"),
1431 NamedNodeRef::new_unchecked("http://example.com/o."),
1432 NamedNodeRef::new_unchecked("http://example.com/g"),
1433 ))?;
1434 serializer.serialize_quad(QuadRef::new(
1435 NamedNodeRef::new_unchecked("http://example.com/s"),
1436 NamedNodeRef::new_unchecked("http://example.com/p"),
1437 NamedNodeRef::new_unchecked("http://example.com/o{o}"),
1438 NamedNodeRef::new_unchecked("http://example.com/g"),
1439 ))?;
1440 serializer.serialize_quad(QuadRef::new(
1441 NamedNodeRef::new_unchecked("http://example.com/s"),
1442 NamedNodeRef::new_unchecked("http://example.com/p"),
1443 NamedNodeRef::new_unchecked("http://example.com/"),
1444 NamedNodeRef::new_unchecked("http://example.com/g"),
1445 ))?;
1446 serializer.serialize_quad(QuadRef::new(
1447 NamedNodeRef::new_unchecked("http://example.com/s"),
1448 NamedNodeRef::new_unchecked("http://example.com/p"),
1449 LiteralRef::new_simple_literal("foo"),
1450 NamedNodeRef::new_unchecked("http://example.com/g"),
1451 ))?;
1452 serializer.serialize_quad(QuadRef::new(
1453 NamedNodeRef::new_unchecked("http://example.com/s"),
1454 NamedNodeRef::new_unchecked("http://example.com/p2"),
1455 LiteralRef::new_language_tagged_literal_unchecked("foo", "en"),
1456 NamedNodeRef::new_unchecked("http://example.com/g"),
1457 ))?;
1458 serializer.serialize_quad(QuadRef::new(
1459 BlankNodeRef::new_unchecked("b"),
1460 NamedNodeRef::new_unchecked("http://example.com/p2"),
1461 BlankNodeRef::new_unchecked("b2"),
1462 NamedNodeRef::new_unchecked("http://example.com/g"),
1463 ))?;
1464 serializer.serialize_quad(QuadRef::new(
1465 BlankNodeRef::new_unchecked("b"),
1466 NamedNodeRef::new_unchecked("http://example.com/p2"),
1467 LiteralRef::new_typed_literal("true", xsd::BOOLEAN),
1468 GraphNameRef::DefaultGraph,
1469 ))?;
1470 serializer.serialize_quad(QuadRef::new(
1471 BlankNodeRef::new_unchecked("b"),
1472 NamedNodeRef::new_unchecked("http://example.org/p2"),
1473 LiteralRef::new_typed_literal("false", xsd::BOOLEAN),
1474 NamedNodeRef::new_unchecked("http://example.com/g2"),
1475 ))?;
1476 assert_eq!(
1477 String::from_utf8(serializer.finish()?).unwrap(),
1478 "@prefix ex: <http://example.com/> .\nex:g {\n\tex:s ex:p ex:o\\. , <http://example.com/o{o}> , ex: , \"foo\" ;\n\t\tex:p2 \"foo\"@en .\n\t_:b ex:p2 _:b2 .\n}\n_:b ex:p2 true .\nex:g2 {\n\t_:b <http://example.org/p2> false .\n}\n"
1479 );
1480 Ok(())
1481 }
1482}