Skip to content

Commit 39b8bda

Browse files
fix: N3 PATCH semicolon shorthand and 'a' keyword not parsed
The N3 parser split on semicolons (Turtle shorthand for same subject, different predicate) but dropped the subject for continuation statements. Statements with only predicate-object were silently discarded as they had fewer than 3 tokens. Also handle the 'a' keyword as rdf:type shorthand. Fixes JavaScriptSolidServer#212
1 parent e827e52 commit 39b8bda

2 files changed

Lines changed: 77 additions & 20 deletions

File tree

src/patch/n3-patch.js

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function parseN3Patch(patchText, baseUri) {
5757

5858
/**
5959
* Parse triples from N3 block content
60+
* Handles Turtle semicolon shorthand (same subject, different predicate-object)
6061
*/
6162
function parseTriples(content, prefixes, baseUri) {
6263
const triples = [];
@@ -68,11 +69,37 @@ function parseTriples(content, prefixes, baseUri) {
6869
// Split by '.' but be careful with strings containing '.'
6970
const statements = splitStatements(content);
7071

72+
let lastSubject = null;
7173
for (const stmt of statements) {
72-
const triple = parseStatement(stmt.trim(), prefixes, baseUri);
73-
if (triple) {
74-
triples.push(triple);
74+
const trimmed = stmt.trim();
75+
if (!trimmed) continue;
76+
77+
const tokens = tokenize(trimmed);
78+
if (tokens.length < 2) continue;
79+
80+
let subject, predicate, object;
81+
82+
if (tokens.length >= 3) {
83+
// Full triple: subject predicate object
84+
subject = resolveValue(tokens[0], prefixes, baseUri);
85+
predicate = resolveValue(tokens[1], prefixes, baseUri);
86+
object = resolveValue(tokens.slice(2).join(' '), prefixes, baseUri);
87+
lastSubject = subject;
88+
} else if (tokens.length === 2 && lastSubject) {
89+
// Semicolon continuation: predicate object (reuse last subject)
90+
subject = lastSubject;
91+
predicate = resolveValue(tokens[0], prefixes, baseUri);
92+
object = resolveValue(tokens[1], prefixes, baseUri);
93+
} else {
94+
continue;
95+
}
96+
97+
// Handle 'a' as rdf:type
98+
if (predicate === 'a') {
99+
predicate = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
75100
}
101+
102+
triples.push({ subject, predicate, object });
76103
}
77104

78105
return triples;
@@ -121,23 +148,6 @@ function splitStatements(content) {
121148
return statements;
122149
}
123150

124-
/**
125-
* Parse a single N3 statement into a triple
126-
*/
127-
function parseStatement(stmt, prefixes, baseUri) {
128-
if (!stmt) return null;
129-
130-
// Tokenize - split by whitespace but respect quotes
131-
const tokens = tokenize(stmt);
132-
if (tokens.length < 3) return null;
133-
134-
const subject = resolveValue(tokens[0], prefixes, baseUri);
135-
const predicate = resolveValue(tokens[1], prefixes, baseUri);
136-
const object = resolveValue(tokens.slice(2).join(' '), prefixes, baseUri);
137-
138-
return { subject, predicate, object };
139-
}
140-
141151
/**
142152
* Tokenize a statement respecting quoted strings
143153
*/
@@ -430,6 +440,7 @@ function convertToJsonLd(object) {
430440
* Expand a potentially prefixed predicate to full URI
431441
*/
432442
function expandPredicate(predicate) {
443+
if (predicate === 'a') return 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
433444
const commonPrefixes = {
434445
'solid': SOLID_NS,
435446
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',

test/patch.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,52 @@ describe('PATCH Operations', () => {
185185
assert.ok(data['@graph'], 'Should have @graph');
186186
assert.strictEqual(data['@graph'].length, 2, 'Should have 2 nodes');
187187
});
188+
189+
it('should handle semicolon shorthand and rdf:type "a" keyword', async () => {
190+
// Create initial resource with @graph
191+
const initial = {
192+
'@context': { 'solid': 'http://www.w3.org/ns/solid/terms#' },
193+
'@graph': []
194+
};
195+
196+
await request('/patchtest/public/patch-semicolon.json', {
197+
method: 'PUT',
198+
headers: { 'Content-Type': 'application/ld+json' },
199+
body: JSON.stringify(initial),
200+
auth: 'patchtest'
201+
});
202+
203+
// Use semicolons and 'a' keyword (Turtle shorthand)
204+
const patch = `
205+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
206+
@prefix wf: <http://www.w3.org/2005/01/wf/flow#>.
207+
_:patch a solid:InsertDeletePatch;
208+
solid:inserts {
209+
<#reg1> a solid:TypeRegistration;
210+
solid:forClass wf:Tracker;
211+
solid:instance <https://example.com/todo/data.jsonld#this>.
212+
}.
213+
`;
214+
215+
const res = await request('/patchtest/public/patch-semicolon.json', {
216+
method: 'PATCH',
217+
headers: { 'Content-Type': 'text/n3' },
218+
body: patch,
219+
auth: 'patchtest'
220+
});
221+
222+
assertStatus(res, 204);
223+
224+
// Verify all three triples were inserted
225+
const verify = await request('/patchtest/public/patch-semicolon.json');
226+
const data = await verify.json();
227+
const node = data['@graph'].find(n => n['@id'] && n['@id'].includes('#reg1'));
228+
assert.ok(node, 'Should have the reg1 node');
229+
assert.ok(node['rdf:type'] || node['http://www.w3.org/1999/02/22-rdf-syntax-ns#type'],
230+
'Should have rdf:type (from "a" keyword)');
231+
assert.ok(node['solid:forClass'], 'Should have solid:forClass');
232+
assert.ok(node['solid:instance'], 'Should have solid:instance');
233+
});
188234
});
189235

190236
describe('PATCH Error Handling', () => {

0 commit comments

Comments
 (0)