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