use std::borrow::Cow;
pub type NodeId = u32;
pub type SubjectId = u32;
pub type PredicateId = u32;
pub type ObjectId = u32;
pub type TermToken = str;
const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
const RDF_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
const RDF_DIR_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString";
#[inline]
pub fn is_iri(t: &TermToken) -> bool {
t.starts_with('<') && !t.starts_with("<<") && t.ends_with('>')
}
#[inline]
pub fn is_quoted_triple(t: &TermToken) -> bool {
t.starts_with("<<") && t.ends_with(">>")
}
#[inline]
pub fn iri_content(t: &TermToken) -> Option<&str> {
if is_quoted_triple(t) {
return None;
}
t.strip_prefix('<').and_then(|s| s.strip_suffix('>'))
}
#[inline]
pub fn is_blank(t: &TermToken) -> bool {
t.starts_with("_:")
}
#[inline]
pub fn is_literal(t: &TermToken) -> bool {
t.starts_with('"')
}
fn closing_quote(t: &TermToken) -> usize {
let bytes = t.as_bytes();
let mut i = 1;
while i < bytes.len() {
match bytes[i] {
b'\\' => i += 2,
b'"' => break,
_ => i += 1,
}
}
i.min(t.len())
}
pub fn literal_lexical(token: &TermToken) -> Option<String> {
if !is_literal(token) {
return None;
}
Some(unescape_literal(&token[1..closing_quote(token)]))
}
pub fn lexical(token: &TermToken) -> Cow<'_, str> {
if is_literal(token) {
Cow::Owned(unescape_literal(&token[1..closing_quote(token)]))
} else if let Some(iri) = iri_content(token) {
Cow::Borrowed(iri)
} else {
Cow::Borrowed(token)
}
}
fn literal_suffix(token: &TermToken) -> Option<&str> {
if !is_literal(token) {
return None;
}
token.get(closing_quote(token) + 1..)
}
pub fn literal_datatype(token: &TermToken) -> Option<String> {
let suffix = literal_suffix(token)?;
if let Some(dt) = suffix.strip_prefix("^^<").and_then(|s| s.strip_suffix('>')) {
Some(dt.to_string())
} else if suffix.starts_with('@') {
if suffix.contains("--") {
Some(RDF_DIR_LANG_STRING.to_string())
} else {
Some(RDF_LANG_STRING.to_string())
}
} else if suffix.is_empty() {
Some(XSD_STRING.to_string())
} else {
None
}
}
pub fn lang_tag(token: &TermToken) -> Option<String> {
literal_suffix(token).map(|s| {
s.strip_prefix('@')
.unwrap_or("")
.split("--")
.next()
.unwrap_or("")
.to_string()
})
}
pub fn lang_dir(token: &TermToken) -> Option<String> {
let tag = literal_suffix(token)?.strip_prefix('@')?;
tag.split("--").nth(1).map(str::to_string)
}
pub fn as_number(token: &TermToken) -> Option<f64> {
let lex = if let Some(rest) = token.strip_prefix('"') {
&rest[..rest.find('"')?]
} else {
token
};
lex.parse::<f64>().ok()
}
pub fn escape_literal(s: &str) -> String {
if !s.contains(['\\', '"', '\n', '\r', '\t']) {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + 2);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out
}
pub fn make_literal(lexical: &str, lang: Option<&str>, datatype: Option<&str>) -> String {
let body = escape_literal(lexical);
match (lang.filter(|l| !l.is_empty()), datatype) {
(Some(l), _) => format!("\"{body}\"@{l}"),
(None, Some(dt)) => format!("\"{body}\"^^<{dt}>"),
(None, None) => format!("\"{body}\""),
}
}
pub fn unescape_literal(s: &str) -> String {
if !s.contains('\\') {
return s.to_string();
}
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
let unicode = |chars: &mut std::str::Chars, n: usize, out: &mut String| {
let hex: String = chars.take(n).collect();
match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
Some(ch) => out.push(ch),
None => out.push('\u{FFFD}'),
}
};
match chars.next() {
Some('t') => out.push('\t'),
Some('b') => out.push('\u{08}'),
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('f') => out.push('\u{0C}'),
Some('"') => out.push('"'),
Some('\'') => out.push('\''),
Some('\\') => out.push('\\'),
Some('u') => unicode(&mut chars, 4, &mut out),
Some('U') => unicode(&mut chars, 8, &mut out),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn term_kinds() {
assert!(is_iri("<http://example.org/x>"));
assert!(!is_iri("\"x\""));
assert!(!is_iri("_:b0"));
assert!(is_blank("_:b0"));
assert!(is_literal("\"x\"@en"));
assert_eq!(iri_content("<http://x>"), Some("http://x"));
assert_eq!(iri_content("\"x\""), None);
}
#[test]
fn lexical_values() {
assert_eq!(literal_lexical("\"hello\""), Some("hello".to_string()));
assert_eq!(literal_lexical("\"42\"^^<int>"), Some("42".to_string()));
assert_eq!(literal_lexical("\"hi\"@en"), Some("hi".to_string()));
assert_eq!(literal_lexical("<http://x>"), None);
assert_eq!(lexical("\"hi\"@en"), "hi");
assert_eq!(lexical("<http://x>"), "http://x");
assert_eq!(lexical("_:b0"), "_:b0");
}
#[test]
fn datatype_and_lang() {
assert_eq!(literal_datatype("\"42\"^^<int>").as_deref(), Some("int"));
assert_eq!(
literal_datatype("\"hi\"@en").as_deref(),
Some(RDF_LANG_STRING)
);
assert_eq!(literal_datatype("\"plain\"").as_deref(), Some(XSD_STRING));
assert_eq!(literal_datatype("<http://x>"), None);
assert_eq!(lang_tag("\"hi\"@en").as_deref(), Some("en"));
assert_eq!(lang_tag("\"plain\"").as_deref(), Some(""));
assert_eq!(lang_tag("<http://x>"), None);
}
#[test]
fn numbers() {
assert_eq!(as_number("\"30\"^^<int>"), Some(30.0));
assert_eq!(as_number("3.5"), Some(3.5));
assert_eq!(as_number("\"nope\""), None);
assert_eq!(as_number("<http://x>"), None);
}
#[test]
fn escapes() {
assert_eq!(unescape_literal("plain"), "plain");
assert_eq!(unescape_literal("a\\nb"), "a\nb");
assert_eq!(unescape_literal("a\\\"b"), "a\"b");
assert_eq!(unescape_literal("\\u0041"), "A");
assert_eq!(literal_lexical("\"a\\\"b\"@en"), Some("a\"b".to_string()));
}
}