rdf/turtle: emit a space before ';' and '.' terminators (#419)#420
Merged
Conversation
Match the de-facto Turtle style used in W3C 1.1 spec examples,
Apache Jena's RIOT writer, and most hand-authored Turtle in the
Solid / linked-data ecosystem: a space between the previous
token and the statement terminator.
Before:
<s> foaf:name "Alice";
foaf:age 30.
After:
<s> foaf:name "Alice" ;
foaf:age 30 .
n3.js's writer hardcodes the no-space form (`;\n next`) and
exposes no config knob. Approach: a literal-aware post-pass on
the writer's output.
The naïve `\S;\n` → `\S ;\n` regex is unsafe — string literals
(especially triple-quoted) can contain `;\n` or `.\n` internally,
and inserting a space inside a literal would silently CHANGE the
literal's value. So:
1. Stash every string literal AND every <IRI> into placeholders
using a multi-character non-token-boundary sentinel
bracketed by NULs (n3.js escapes raw NUL inside literals
and never emits one in real Turtle output, so the sentinel
can't collide with real content).
2. Apply the spacing regex to the redacted output. Now `;`/`.`
only appear as actual statement terminators because all
literal/IRI internals are hidden.
3. Restore placeholders.
Stash order matters — triple-quoted before single-quoted, else
`"""` looks like an empty `""` followed by `"` to the
single-quoted regex. Same for triple-vs-single apostrophe.
Tests:
- "emits a space before ; and . terminators": positive
- "does NOT add a space inside literals containing ; or .":
safety regression — the post-pass MUST NOT corrupt literal
values like `"foo;bar"`, `"has.dot"`, `"a;b.c"`.
- "does NOT add a space inside an IRI containing ;": safety
regression for IRIs like `<https://example.test/path;with;semis>`.
Test count: 770 → 777 in full suite (+7: 3 #419 tests + 4 #416/#417/#415 follow-ups since main).
There was a problem hiding this comment.
Pull request overview
This PR targets Turtle serialization readability by enforcing a space before ; and . statement terminators (per common conventions in Turtle 1.1 examples and popular writers), and adds regression tests to ensure the behavior is applied without corrupting literals or IRIs.
Changes:
- Adds a unit test asserting
;and.terminators are preceded by whitespace. - Adds safety tests ensuring the spacing pass does not modify string literals containing
;/.. - Adds a safety test ensuring IRIs containing
;are preserved verbatim.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+188
to
+194
| // The literals must round-trip verbatim — no inserted space. | ||
| assert.ok(content.includes('"foo;bar"'), | ||
| `literal "foo;bar" must survive verbatim, got:\n${content}`); | ||
| assert.ok(content.includes('"has.dot"'), | ||
| `literal "has.dot" must survive verbatim, got:\n${content}`); | ||
| assert.ok(content.includes('"a;b.c"'), | ||
| `literal "a;b.c" must survive verbatim, got:\n${content}`); |
The two safety regression tests for #419 hardcoded that N3 serializes string literals with double quotes (`"foo;bar"`) and IRIs as `<...>`. That made them sensitive to N3 writer formatting choices — single quotes, long-string `"""..."""`, IRI escaping, etc. — even when the underlying literal value would still be preserved correctly. Fix: parse the emitted Turtle back to quads and assert the literal/IRI VALUES, not their lexical form. The properties the tests actually want to enforce are: - "foo;bar" round-trips as a literal whose value is foo;bar (no inserted space) - <https://example.test/path;with;semis> round-trips as a NamedNode with that exact IRI Both are now assertion-by-value. Resilient against any future N3 upgrade that changes quote style, IRI escaping, or the long- string threshold. Test count: same 777.
Comment on lines
+167
to
+171
| const { content } = await fromJsonLd(doc, 'text/turtle', 'https://example.test/', true); | ||
| // Every `;` must be preceded by whitespace (space or newline). | ||
| // Same for `.` at end-of-statement. | ||
| const offendingSemi = /[^\s];/.test(content); | ||
| const offendingDot = /[^\s]\.\s*$/m.test(content) || /[^\s]\.\n/.test(content); |
The "emits a space before ; and ." test asserted no offending terminators (without preceding whitespace) but didn't assert the terminators were actually present. If a future N3 upgrade ever switched to the "one triple per statement, no ; continuations" output style, the negative assertions would pass vacuously and the test would stop exercising the spacing behavior at all. Fix: pin the presence of at least one ` ;` and one ` .` (with preceding whitespace) before checking for offending forms. Now the test fails loudly if either disappears. Test count: same 777.
Comment on lines
+175
to
+184
| // Pin "at least one ; and one . exists" — otherwise an absent | ||
| // terminator would let the negative assertions pass vacuously. | ||
| assert.match(content, /\s;/, `output must contain at least one ; terminator, got:\n${content}`); | ||
| assert.match(content, /\s\.(?:\s|$)/, `output must contain at least one . terminator, got:\n${content}`); | ||
| // Every `;` must be preceded by whitespace (space or newline). | ||
| // Same for `.` at end-of-statement. | ||
| const offendingSemi = /[^\s];/.test(content); | ||
| const offendingDot = /[^\s]\.\s*$/m.test(content) || /[^\s]\.\n/.test(content); | ||
| assert.ok(!offendingSemi, `every ; must be preceded by whitespace, got:\n${content}`); | ||
| assert.ok(!offendingDot, `every . at line/doc end must be preceded by whitespace, got:\n${content}`); |
…space The pass-2 assertions used `\s` / `[\s]` patterns, which would treat `\n;`, `\t;`, etc. as acceptable too. The #419 intent is specifically a single SPACE before the terminator (matching W3C Turtle 1.1 examples and Apache Jena's RIOT writer): `value ;` / `value .` If a future N3 upgrade ever emitted `\n;` or `\t;`, the pass-2 test would have passed despite the visually-different output. Fix: tighten the assertions to require a literal ` ;` / ` .` (a single space). Both presence-checks and the offending-form negative checks use ` ` (space) explicitly instead of `\s`. Test count: same 777.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #419.
Before / after
Before:
```turtle
foaf:name "Alice";foaf:age 30.
```
After:
```turtle
foaf:name "Alice" ;foaf:age 30 .
```
Matches the de-facto convention in W3C Turtle 1.1 spec examples, Apache Jena's RIOT writer, and most hand-authored Turtle in the Solid / linked-data ecosystem. n3.js's writer hardcodes the no-space form and exposes no config knob, so this is a literal-aware post-pass on the writer's output.
Safety
The naïve
\S;\n → \S ;\nregex would silently corrupt string literals —"foo;bar"would become"foo ;bar". The post-pass:"...",'...',"""...""",'''...''') and every<IRI>into placeholders using a NUL-bracketed sentinel (n3.js escapes raw NUL inside literals, so the sentinel can't collide with real content);and.only appear as actual statement terminatorsTriple-quoted strings are stashed before single-quoted (otherwise
"""would be parsed as"" + ").Test plan
;and.in normal output are preceded by whitespace"foo;bar","has.dot","a;b.c"survive verbatim<https://example.test/path;with;semis>survives verbatim