mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
* fix(export): escape Turtle/N-Triples string literals (fixes #1098) Add RDFSerializer._escape_turtle_literal and apply it to the semantica:text literal in serialize_to_turtle and the N-Triples text triple. Backslash, double quote, newline, CR, and tab are escaped per the RDF 1.1 Turtle STRING_LITERAL_QUOTE grammar, so entity text containing quotes or control characters no longer emits invalid Turtle/N-Triples. N-Triples previously escaped only quotes and newlines; now it also handles backslashes and tabs via the shared escaper. * fix(export): escape OWL-Time timestamp literals in Turtle output Addresses Qodo finding on #1148: the OWL-Time branch of serialize_to_turtle interpolated from_val/until_val directly into quoted literals. Apply _escape_turtle_literal there too so timestamps containing quotes, backslashes, or control characters cannot produce invalid Turtle. * chore: remove stray local files (AGENTS.md, evals superpowers docs) from PR branch ---------
This commit is contained in:
@@ -738,6 +738,22 @@ class RDFSerializer:
|
||||
# node to signal that valid_until is OPEN/unbounded. This keeps the
|
||||
# interval well-formed while remaining human- and machine-readable.
|
||||
|
||||
@staticmethod
|
||||
def _escape_turtle_literal(value: str) -> str:
|
||||
"""Escape a string value for safe embedding in a Turtle string literal.
|
||||
|
||||
Backslash must be escaped first, then the double quote and the
|
||||
recognized control characters (newline, carriage return, tab), per the
|
||||
RDF 1.1 Turtle grammar for STRING_LITERAL_QUOTE.
|
||||
"""
|
||||
return (
|
||||
value.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
)
|
||||
|
||||
def serialize_to_turtle(self, rdf_data: Dict[str, Any], **options) -> str:
|
||||
"""
|
||||
Serialize RDF to Turtle format.
|
||||
@@ -807,7 +823,7 @@ class RDFSerializer:
|
||||
|
||||
clauses = [
|
||||
f"a <{self._as_turtle_iri(entity_type, merged_namespaces)}>",
|
||||
f'semantica:text "{text}"',
|
||||
f'semantica:text "{self._escape_turtle_literal(text)}"',
|
||||
]
|
||||
if confidence is None:
|
||||
self.logger.warning(
|
||||
@@ -999,7 +1015,7 @@ class RDFSerializer:
|
||||
lines.append(f" time:hasEnd <{end_id}> .")
|
||||
lines.append(f"<{end_id}> a time:Instant ;")
|
||||
lines.append(
|
||||
f' time:inXSDDateTimeStamp "{until_val}"^^xsd:dateTimeStamp .'
|
||||
f' time:inXSDDateTimeStamp "{self._escape_turtle_literal(until_val)}"^^xsd:dateTimeStamp .'
|
||||
)
|
||||
else:
|
||||
lines[-1] = (
|
||||
@@ -1008,7 +1024,7 @@ class RDFSerializer:
|
||||
|
||||
lines.append(f"<{begin_id}> a time:Instant ;")
|
||||
lines.append(
|
||||
f' time:inXSDDateTimeStamp "{from_val}"^^xsd:dateTimeStamp .'
|
||||
f' time:inXSDDateTimeStamp "{self._escape_turtle_literal(from_val)}"^^xsd:dateTimeStamp .'
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
@@ -1305,7 +1321,7 @@ class RDFSerializer:
|
||||
# Text property
|
||||
text = entity.get("text") or entity.get("label", "")
|
||||
if text:
|
||||
safe_text = text.replace('"', '\\"').replace("\n", "\\n")
|
||||
safe_text = self._escape_turtle_literal(text)
|
||||
lines.append(
|
||||
f'{subject} {expand_uri("semantica:text")} "{safe_text}" .'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Regression tests for #1098: Turtle/N-Triples literal escaping."""
|
||||
import pytest
|
||||
|
||||
from semantica.export.rdf_exporter import RDFExporter, RDFSerializer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def serializer():
|
||||
return RDFSerializer()
|
||||
|
||||
|
||||
class TestTurtleLiteralEscaping:
|
||||
def test_quote_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": 'He said "hello"', "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
turtle = serializer.serialize_to_turtle(kg)
|
||||
assert '"He said \\"hello\\""' in turtle
|
||||
|
||||
def test_backslash_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": r"path\to\file", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
turtle = serializer.serialize_to_turtle(kg)
|
||||
assert r"path\\to\\file" in turtle
|
||||
|
||||
def test_newline_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": "line1\nline2", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
turtle = serializer.serialize_to_turtle(kg)
|
||||
assert "line1\\nline2" in turtle
|
||||
|
||||
def test_tab_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": "a\tb", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
turtle = serializer.serialize_to_turtle(kg)
|
||||
assert "a\\tb" in turtle
|
||||
|
||||
def test_plain_text_unchanged(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": "Apple Inc.", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
turtle = serializer.serialize_to_turtle(kg)
|
||||
assert 'semantica:text "Apple Inc."' in turtle
|
||||
|
||||
|
||||
class TestNTriplesLiteralEscaping:
|
||||
def test_quote_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": 'He said "hello"', "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
ntriples = serializer.serialize_to_ntriples(kg)
|
||||
assert '\\"hello\\"' in ntriples
|
||||
|
||||
def test_backslash_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": r"path\to\file", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
ntriples = serializer.serialize_to_ntriples(kg)
|
||||
assert r"path\\to\\file" in ntriples
|
||||
|
||||
def test_newline_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": "line1\nline2", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
ntriples = serializer.serialize_to_ntriples(kg)
|
||||
assert "line1\\nline2" in ntriples
|
||||
|
||||
def test_tab_in_text_is_escaped(self, serializer):
|
||||
kg = {
|
||||
"entities": [{"id": "e1", "text": "a\tb", "type": "ORG"}],
|
||||
"relationships": [],
|
||||
}
|
||||
ntriples = serializer.serialize_to_ntriples(kg)
|
||||
assert "a\\tb" in ntriples
|
||||
|
||||
|
||||
class TestOWLTimeLiteralEscaping:
|
||||
"""Timestamp literals in OWL-Time turtle output must also be escaped."""
|
||||
|
||||
def test_owl_time_timestamps_are_escaped(self):
|
||||
exporter = RDFExporter()
|
||||
kg = {
|
||||
"entities": [],
|
||||
"relationships": [
|
||||
{
|
||||
"id": "r1",
|
||||
"source_id": "a",
|
||||
"target_id": "b",
|
||||
"type": "works_for",
|
||||
"valid_from": "2020-01-01T00:00:00Z",
|
||||
"valid_until": None,
|
||||
}
|
||||
],
|
||||
}
|
||||
turtle = exporter.export_to_rdf(kg, format="turtle", include_temporal=True)
|
||||
assert 'time:inXSDDateTimeStamp "2020-01-01T00:00:00Z"' in turtle
|
||||
Reference in New Issue
Block a user