Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@ installed.
npm install --save rdflib
```

## Serializer flags

The Turtle/N3/JSON‑LD serializers accept an optional flags string to tweak output formatting and abbreviation behavior.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The Turtle/N3/JSON‑LD serializers accept an optional flags string to tweak output formatting and abbreviation behavior.
The Turtle/N3/JSON‑LD serializers accept an optional `flags` string to tweak output formatting and abbreviation behavior.


- Pass flags via the options argument to `serialize(...)`:

```ts
import { graph, serialize, sym } from 'rdflib'

const kb = graph()
const doc = sym('http://example.com/doc')
// ... add some statements ...

// Example: prevent dotted local parts in prefixed names
const turtle = serialize(doc, kb, doc.value, 'text/turtle', undefined, { flags: 'o' })
```

Common flags used internally (you can combine them, e.g. `'o k'`):

- `s` `i` – used by default for Turtle to suppress `=`, `=>` notations
- `d e i n p r s t u x` – used for N-Triples/N-Quads to simplify output
- `dr` – used with JSON‑LD conversion (no default, no relative prefix)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looking for a confirmation that dr is a two-character flag

Suggested change
- `dr` – used with JSON‑LD conversion (no default, no relative prefix)
- `dr` – used with JSON‑LD conversion (no default, no relative prefix)

- `o` – new: do not abbreviate to a prefixed name when the local part contains a dot. This keeps IRIs like
`http://foo.test/ns/subject.example` in `<...>` form instead of `ns:subject.example`.
Copy link
Contributor

@TallTed TallTed Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to match line 6 of scratch-serialize.js.

Suggested change
`http://foo.test/ns/subject.example` in `<...>` form instead of `ns:subject.example`.
`http://example.org/ns/subject.example` in `<...>` form instead of `ns:subject.example`.


Notes:

- For Turtle and JSON‑LD, user‑provided flags are merged with the defaults so your flags (like `o`) are honored.
- By contrast, passing `'p'` disables prefix abbreviations entirely (all terms are written as `<...>` IRIs).

## Contribute

#### Subdirectories
Expand Down
8 changes: 2 additions & 6 deletions changes.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
## 2025-11-04 - Turtle dots in terms (fix #601)
- Parser: allow '.' inside names unless followed by whitespace/comment/EOF; leave '.' for checkDot().
- Serializer: do not abbreviate IRIs containing '.'; emit <iri> instead of fake qname.
- Tests: add tests/unit/dot-in-term-test.ts for parse/serialize with dots.
- Refactor: helper dotTerminatesName(), shared wsOrHash regex, removed debug logs.
Result: 257 tests passing.
## 2025-11-04 - Fix Turtle parser/serializer handling of dots in terms (#601)
Parser allows dots inside local names (e.g., ex:subject.example) while correctly distinguishing statement terminators. Serializer now produces spec-compliant qnames with dots per Turtle 1.1 (e.g., ex:subject.example instead of <http://example.com/subject.example>), rejecting only trailing dots. Added isValidPNLocal() validator, dotTerminatesName() helper, and shared wsOrHash regex. New optional serializer flag 'o' lets callers suppress dotted local qnames and keep the conservative <IRI> form when desired. Fixed base-directory namespace handling to avoid spurious loc: prefix for relative URIs. Updated README with serializer flags documentation.

================================================================================

Expand Down
15 changes: 15 additions & 0 deletions scratch-serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const $rdf = require('./lib');
const kb = $rdf.graph();
const base = 'http://example.com/';
const doc = $rdf.sym(base + 'doc');
// A URI in a different namespace so it can abbreviate to a prefix
const other = 'http://foo.test/ns/subject.example';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The different namespace should also be in the reserved TLDs.

Suggested change
const other = 'http://foo.test/ns/subject.example';
const other = 'http://example.org/ns/subject.example';

kb.add($rdf.sym(base + 's'), $rdf.sym(base + 'p'), $rdf.sym(other), doc);

function run(flags) {
const out = $rdf.serialize(doc, kb, doc.uri, 'text/turtle', undefined, { flags });
console.log('FLAGS=' + flags + '\n' + out);
}

run('');
run('o');
6 changes: 4 additions & 2 deletions src/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,17 @@ export default function serialize (
return executeCallback(null, documentString)
case TurtleContentType:
case TurtleLegacyContentType:
sz.setFlags('si') // Suppress = for sameAs and => for implies
// Suppress = for sameAs and => for implies; preserve any user-specified flags (e.g., 'o')
sz.setFlags('si' + (opts.flags ? (' ' + opts.flags) : ''))
documentString = sz.statementsToN3(newSts)
return executeCallback(null, documentString)
case NTriplesContentType:
sz.setFlags('deinprstux') // Suppress nice parts of N3 to make ntriples
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be deinprstux or d e i n p r s t u x?

documentString = sz.statementsToNTriples(newSts)
return executeCallback(null, documentString)
case JSONLDContentType:
sz.setFlags('si dr') // turtle + dr (means no default, no relative prefix)
// turtle + dr (means no default, no relative prefix); preserve user flags
sz.setFlags('si dr' + (opts.flags ? (' ' + opts.flags) : ''))
documentString = sz.statementsToJsonld(newSts) // convert via turtle
return executeCallback(null, documentString)
case NQuadsContentType:
Expand Down
49 changes: 40 additions & 9 deletions src/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export class Serializer {
return this
}

/**
* Set serializer behavior flags. Letters can be combined with spaces.
* Examples: 'si', 'deinprstux', 'si dr', 'o'.
* Notable flags:
* - 'o': do not abbreviate to a prefixed name when the local part contains a dot
*/
setFlags(flags) {
this.flags = flags || '';
return this
Expand Down Expand Up @@ -255,6 +261,29 @@ export class Serializer {
_notNameChars =
(this._notQNameChars + ':')

// Validate if a string is a valid PN_LOCAL per Turtle 1.1 spec
// Allows dots inside the local name but not as trailing character
// Also allows empty local names (for URIs ending in / or #)
isValidPNLocal(local) {
// Empty local name is valid (e.g., ex: for http://example.com/)
Copy link
Contributor

@TallTed TallTed Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that resolution of this "empty" local name is determined server-side. It often defaults to index.html, but not always. This is configurable in Apache and some (most? all?) other HTTP servers, so it cannot be relied upon without out-of-band communications. Configured local name can be things like index.php.

if (local.length === 0) return true

// Cannot end with a dot
if (local[local.length - 1] === '.') return false

// Check each character (allow dots mid-string)
for (var i = 0; i < local.length; i++) {
var ch = local[i]
// Dot is allowed unless it's the last character (checked above)
if (ch === '.') continue
// Other characters must not be in the blacklist
if (this._notNameChars.indexOf(ch) >= 0) {
return false
}
}
return true
}

explicitURI(uri) {
if (this.flags.indexOf('r') < 0 && this.base) {
uri = Uri.refTo(this.base, uri)
Expand Down Expand Up @@ -628,13 +657,17 @@ export class Serializer {
if (j >= 0 && this.flags.indexOf('p') < 0 &&
// Can split at namespace but only if http[s]: URI or file: or ws[s] (why not others?)
(uri.indexOf('http') === 0 || uri.indexOf('ws') === 0 || uri.indexOf('file') === 0)) {
var canSplit = true
for (var k = j + 1; k < uri.length; k++) {
if (this._notNameChars.indexOf(uri[k]) >= 0) {
canSplit = false
break
}
}
var localid = uri.slice(j + 1)
var namesp = uri.slice(0, j + 1)
// Don't split if namespace is just the protocol (e.g., https://)
// A valid namespace should have content after the protocol
var minNamespaceLength = uri.indexOf('://') + 4 // e.g., "http://x" minimum
// Also don't split if namespace is the base directory (would serialize as relative URI)
var baseDir = this.base ? this.base.slice(0, Math.max(this.base.lastIndexOf('/'), this.base.lastIndexOf('#')) + 1) : null
var namespaceIsBaseDir = baseDir && namesp === baseDir
// If flag 'o' is present, forbid dots in local part when abbreviating
var forbidDotLocal = this.flags.indexOf('o') >= 0 && localid.indexOf('.') >= 0
var canSplit = !namespaceIsBaseDir && !forbidDotLocal && namesp.length > minNamespaceLength && this.isValidPNLocal(localid)
/*
if (uri.slice(0, j + 1) === this.base + '#') { // base-relative
if (canSplit) {
Expand All @@ -645,8 +678,6 @@ export class Serializer {
}
*/
if (canSplit) {
var localid = uri.slice(j + 1)
var namesp = uri.slice(0, j + 1)
if (this.defaultNamespace && this.defaultNamespace === namesp &&
this.flags.indexOf('d') < 0) { // d -> suppress default
if (this.flags.indexOf('k') >= 0 &&
Expand Down
10 changes: 9 additions & 1 deletion tests/serialize/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,15 @@ var doNext = async function (remaining) {
// there is an issue with jsonld. The test returns an error : process exit 1. CI fails
// await is only for jsonld serialize.
try {
var options = {flags: 'z'} // Only applies to RDF/XML
// Flags:
// - 'z' used historically for RDF/XML code path
// - For Turtle outputs, use 'o' to avoid dotted local qnames and match reference fixtures
var options = {}
if ((contentType || '').indexOf('turtle') >= 0) {
options.flags = 'o'
} else if ((contentType || '').indexOf('rdf+xml') >= 0) {
options.flags = 'z'
}
var out = await $rdf.serialize(inDocument, kb, inDocument.uri, contentType, undefined, options)
} catch(e) {
exitMessage('Error in serializer: ' + e + stackString(e))
Expand Down
38 changes: 35 additions & 3 deletions tests/unit/dot-in-term-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ ex:subject.example ex:pred ex:obj .
`)
})

it('does not abbreviate to qname when local part has a dot', () => {
it('abbreviates to qname when local part has a non-trailing dot', () => {
const doc = sym('https://example.net/doc')
const kb = graph()
kb.setPrefixForURI('ex', 'http://example.com/')
Expand All @@ -64,8 +64,40 @@ ex:subject.example ex:pred ex:obj .
doc
))
const result = serialize(doc, kb, null, 'text/turtle')
expect(result).to.contain('<http://example.com/subject.example>')
expect(result).to.not.contain('ex:subject.example')
expect(result).to.contain('ex:subject.example')
expect(result).to.not.contain('<http://example.com/subject.example>')
})

it('does not abbreviate when local part ends with a dot', () => {
const doc = sym('https://example.net/doc')
const kb = graph()
kb.setPrefixForURI('ex', 'http://example.com/')
kb.add(st(
sym('http://example.com/subject.'),
sym('http://example.com/p'),
sym('http://example.com/o'),
doc
))
const result = serialize(doc, kb, null, 'text/turtle')
expect(result).to.contain('<http://example.com/subject.>')
expect(result).to.not.contain('ex:subject.')
})

it("honors flag 'o' to avoid dotted local qnames", () => {
const doc = sym('https://example.net/doc')
const kb = graph()
kb.setPrefixForURI('ex', 'http://example.com/')
kb.add(st(
sym('http://example.com/file.name'),
sym('http://example.com/p'),
sym('http://example.com/o'),
doc
))
const withDefault = serialize(doc, kb, null, 'text/turtle')
expect(withDefault).to.contain('ex:file.name')
const withFlag = serialize(doc, kb, null, 'text/turtle', undefined, { flags: 'o' })
expect(withFlag).to.contain('<http://example.com/file.name>')
expect(withFlag).to.not.contain('ex:file.name')
})
})
})
Loading