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