From 66ae5dc781ee1c094edd033acfe18b2b88f3998e Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 29 Mar 2017 10:48:43 +0200 Subject: [PATCH 01/72] Added stub tests and writer for TRiG --- src/TRiGWriter.php | 307 ++++++++++++++++++++ test/TRiGWriterTest.php | 616 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 923 insertions(+) create mode 100644 src/TRiGWriter.php create mode 100644 test/TRiGWriterTest.php diff --git a/src/TRiGWriter.php b/src/TRiGWriter.php new file mode 100644 index 0000000..3783c19 --- /dev/null +++ b/src/TRiGWriter.php @@ -0,0 +1,307 @@ + '\\\\', '"': '\\"', '\t': '\\t', + '\n' => '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f' + ]; + + public function __construct($options) + { + /* Initialize writer, depending on the format + this._subject = null; + if (!(/triple|quad/i).test(options.format)) { + this._graph = ''; + this._prefixIRIs = Object.create(null); + options.prefixes && this.addPrefixes(options.prefixes); + }*/ + } + + /* +N3Writer.prototype = { + // ## Private methods + + // ### `_write` writes the argument to the output stream + _write: function (string, callback) { + this._outputStream.write(string, 'utf8', callback); + }, + + // ### `_writeTriple` writes the triple to the output stream + _writeTriple: function (subject, predicate, object, graph, done) { + try { + // Write the graph's label if it has changed + if (this._graph !== graph) { + // Close the previous graph and start the new one + this._write((this._subject === null ? '' : (this._graph ? '\n}\n' : '.\n')) + + (graph ? this._encodeIriOrBlankNode(graph) + ' {\n' : '')); + this._subject = null; + // Don't treat identical blank nodes as repeating graphs + this._graph = graph[0] !== '[' ? graph : ']'; + } + // Don't repeat the subject if it's the same + if (this._subject === subject) { + // Don't repeat the predicate if it's the same + if (this._predicate === predicate) + this._write(', ' + this._encodeObject(object), done); + // Same subject, different predicate + else + this._write(';\n ' + + this._encodePredicate(this._predicate = predicate) + ' ' + + this._encodeObject(object), done); + } + // Different subject; write the whole triple + else + this._write((this._subject === null ? '' : '.\n') + + this._encodeSubject(this._subject = subject) + ' ' + + this._encodePredicate(this._predicate = predicate) + ' ' + + this._encodeObject(object), done); + } + catch (error) { done && done(error); } + }, + + // ### `_writeTripleLine` writes the triple or quad to the output stream as a single line + _writeTripleLine: function (subject, predicate, object, graph, done) { + // Don't use prefixes + delete this._prefixMatch; + // Write the triple + try { + this._write(this._encodeIriOrBlankNode(subject) + ' ' + + this._encodeIriOrBlankNode(predicate) + ' ' + + this._encodeObject(object) + + (graph ? ' ' + this._encodeIriOrBlankNode(graph) + '.\n' : '.\n'), done); + } + catch (error) { done && done(error); } + }, + + // ### `_encodeIriOrBlankNode` represents an IRI or blank node + _encodeIriOrBlankNode: function (entity) { + // A blank node or list is represented as-is + var firstChar = entity[0]; + if (firstChar === '[' || firstChar === '(' || firstChar === '_' && entity[1] === ':') + return entity; + // Escape special characters + if (escape.test(entity)) + entity = entity.replace(escapeAll, characterReplacer); + // Try to represent the IRI as prefixed name + var prefixMatch = this._prefixRegex.exec(entity); + return !prefixMatch ? '<' + entity + '>' : + (!prefixMatch[1] ? entity : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]); + }, + + // ### `_encodeLiteral` represents a literal + _encodeLiteral: function (value, type, language) { + // Escape special characters + if (escape.test(value)) + value = value.replace(escapeAll, characterReplacer); + // Write the literal, possibly with type or language + if (language) + return '"' + value + '"@' + language; + else if (type) + return '"' + value + '"^^' + this._encodeIriOrBlankNode(type); + else + return '"' + value + '"'; + }, + + // ### `_encodeSubject` represents a subject + _encodeSubject: function (subject) { + if (subject[0] === '"') + throw new Error('A literal as subject is not allowed: ' + subject); + // Don't treat identical blank nodes as repeating subjects + if (subject[0] === '[') + this._subject = ']'; + return this._encodeIriOrBlankNode(subject); + }, + + // ### `_encodePredicate` represents a predicate + _encodePredicate: function (predicate) { + if (predicate[0] === '"') + throw new Error('A literal as predicate is not allowed: ' + predicate); + return predicate === RDF_TYPE ? 'a' : this._encodeIriOrBlankNode(predicate); + }, + + // ### `_encodeObject` represents an object + _encodeObject: function (object) { + // Represent an IRI or blank node + if (object[0] !== '"') + return this._encodeIriOrBlankNode(object); + // Represent a literal + var match = N3LiteralMatcher.exec(object); + if (!match) throw new Error('Invalid literal: ' + object); + return this._encodeLiteral(match[1], match[2], match[3]); + }, + + // ### `_blockedWrite` replaces `_write` after the writer has been closed + _blockedWrite: function () { + throw new Error('Cannot write because the writer has been closed.'); + }, + + // ### `addTriple` adds the triple to the output stream + addTriple: function (subject, predicate, object, graph, done) { + // The triple was given as a triple object, so shift parameters + if (object === undefined) + this._writeTriple(subject.subject, subject.predicate, subject.object, + subject.graph || '', predicate); + // The optional `graph` parameter was not provided + else if (typeof graph !== 'string') + this._writeTriple(subject, predicate, object, '', graph); + // The `graph` parameter was provided + else + this._writeTriple(subject, predicate, object, graph, done); + }, + + // ### `addTriples` adds the triples to the output stream + addTriples: function (triples) { + for (var i = 0; i < triples.length; i++) + this.addTriple(triples[i]); + }, + + // ### `addPrefix` adds the prefix to the output stream + addPrefix: function (prefix, iri, done) { + var prefixes = {}; + prefixes[prefix] = iri; + this.addPrefixes(prefixes, done); + }, + + // ### `addPrefixes` adds the prefixes to the output stream + addPrefixes: function (prefixes, done) { + // Add all useful prefixes + var prefixIRIs = this._prefixIRIs, hasPrefixes = false; + for (var prefix in prefixes) { + // Verify whether the prefix can be used and does not exist yet + var iri = prefixes[prefix]; + if (/[#\/]$/.test(iri) && prefixIRIs[iri] !== (prefix += ':')) { + hasPrefixes = true; + prefixIRIs[iri] = prefix; + // Finish a possible pending triple + if (this._subject !== null) { + this._write(this._graph ? '\n}\n' : '.\n'); + this._subject = null, this._graph = ''; + } + // Write prefix + this._write('@prefix ' + prefix + ' <' + iri + '>.\n'); + } + } + // Recreate the prefix matcher + if (hasPrefixes) { + var IRIlist = '', prefixList = ''; + for (var prefixIRI in prefixIRIs) { + IRIlist += IRIlist ? '|' + prefixIRI : prefixIRI; + prefixList += (prefixList ? '|' : '') + prefixIRIs[prefixIRI]; + } + IRIlist = IRIlist.replace(/[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&'); + this._prefixRegex = new RegExp('^(?:' + prefixList + ')[^\/]*$|' + + '^(' + IRIlist + ')([a-zA-Z][\\-_a-zA-Z0-9]*)$'); + } + // End a prefix block with a newline + this._write(hasPrefixes ? '\n' : '', done); + }, + + // ### `blank` creates a blank node with the given content + blank: function (predicate, object) { + var children = predicate, child, length; + // Empty blank node + if (predicate === undefined) + children = []; + // Blank node passed as blank("predicate", "object") + else if (typeof predicate === 'string') + children = [{ predicate: predicate, object: object }]; + // Blank node passed as blank({ predicate: predicate, object: object }) + else if (!('length' in predicate)) + children = [predicate]; + + switch (length = children.length) { + // Generate an empty blank node + case 0: + return '[]'; + // Generate a non-nested one-triple blank node + case 1: + child = children[0]; + if (child.object[0] !== '[') + return '[ ' + this._encodePredicate(child.predicate) + ' ' + + this._encodeObject(child.object) + ' ]'; + // Generate a multi-triple or nested blank node + default: + var contents = '['; + // Write all triples in order + for (var i = 0; i < length; i++) { + child = children[i]; + // Write only the object is the predicate is the same as the previous + if (child.predicate === predicate) + contents += ', ' + this._encodeObject(child.object); + // Otherwise, write the predicate and the object + else { + contents += (i ? ';\n ' : '\n ') + + this._encodePredicate(child.predicate) + ' ' + + this._encodeObject(child.object); + predicate = child.predicate; + } + } + return contents + '\n]'; + } + }, + + // ### `list` creates a list node with the given content + list: function (elements) { + var length = elements && elements.length || 0, contents = new Array(length); + for (var i = 0; i < length; i++) + contents[i] = this._encodeObject(elements[i]); + return '(' + contents.join(' ') + ')'; + }, + + // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes + _prefixRegex: /$0^/, + + // ### `end` signals the end of the output stream + end: function (done) { + // Finish a possible pending triple + if (this._subject !== null) { + this._write(this._graph ? '\n}\n' : '.\n'); + this._subject = null; + } + // Disallow further writing + this._write = this._blockedWrite; + + // Try to end the underlying stream, ensuring done is called exactly one time + var singleDone = done && function (error, result) { singleDone = null, done(error, result); }; + if (this._endStream) { + try { return this._outputStream.end(singleDone); } + catch (error) { } + } + singleDone && singleDone(); + }, +}; + +// Replaces a character by its escaped version +function characterReplacer(character) { + // Replace a single character by its escaped version + var result = escapeReplacements[character]; + if (result === undefined) { + // Replace a single character with its 4-bit unicode escape sequence + if (character.length === 1) { + result = character.charCodeAt(0).toString(16); + result = '\\u0000'.substr(0, 6 - result.length) + result; + } + // Replace a surrogate pair with its 8-bit unicode escape sequence + else { + result = ((character.charCodeAt(0) - 0xD800) * 0x400 + + character.charCodeAt(1) + 0x2400).toString(16); + result = '\\U00000000'.substr(0, 10 - result.length) + result; + } + } + return result; +}*/ + +} diff --git a/test/TRiGWriterTest.php b/test/TRiGWriterTest.php new file mode 100644 index 0000000..fa7bf4c --- /dev/null +++ b/test/TRiGWriterTest.php @@ -0,0 +1,616 @@ + .\n')); + + //should serialize 2 triples', + shouldSerialize(['abc', 'def', 'ghi'], + ['jkl', 'mno', 'pqr'], + ' .\n' + + ' .\n')); + + //should serialize 3 triples', + shouldSerialize(['abc', 'def', 'ghi'], + ['jkl', 'mno', 'pqr'], + ['stu', 'vwx', 'yz'], + ' .\n' + + ' .\n' + + ' .\n')); + + //should serialize a literal', + shouldSerialize(['a', 'b', '"cde"'], + ' "cde".\n')); + + //should serialize a literal with a type', + shouldSerialize(['a', 'b', '"cde"^^fgh'], + ' "cde"^^.\n')); + + //should serialize a literal with a language', + shouldSerialize(['a', 'b', '"cde"@en-us'], + ' "cde"@en-us.\n')); + + //should serialize a literal containing a single quote', + shouldSerialize(['a', 'b', '"c\'de"'], + ' "c\'de".\n')); + + //should serialize a literal containing a double quote', + shouldSerialize(['a', 'b', '"c"de"'], + ' "c\\"de".\n')); + + //should serialize a literal containing a backspace', + shouldSerialize(['a', 'b', '"c\\de"'], + ' "c\\\\de".\n')); + + //should serialize a literal containing a tab character', + shouldSerialize(['a', 'b', '"c\tde"'], + ' "c\\tde".\n')); + + //should serialize a literal containing a newline character', + shouldSerialize(['a', 'b', '"c\nde"'], + ' "c\\nde".\n')); + + //should serialize a literal containing a cariage return character', + shouldSerialize(['a', 'b', '"c\rde"'], + ' "c\\rde".\n')); + + //should serialize a literal containing a backspace character', + shouldSerialize(['a', 'b', '"c\bde"'], + ' "c\\bde".\n')); + + //should serialize a literal containing a form feed character', + shouldSerialize(['a', 'b', '"c\fde"'], + ' "c\\fde".\n')); + + //should serialize a literal containing a line separator', + shouldSerialize(['a', 'b', '"c\u2028de"'], + ' "c\u2028de".\n')); + + //should serialize a literal containing a paragraph separator', + shouldSerialize(['a', 'b', '"c\u2029de"'], + ' "c\u2029de".\n')); + + //should serialize a literal containing special unicode characters', + shouldSerialize(['a', 'b', '"c\u0000\u0001"'], + ' "c\\u0000\\u0001".\n')); + + //should serialize blank nodes', + shouldSerialize(['_:a', 'b', '_:c'], + '_:a _:c.\n')); + + //should not serialize a literal in the subject', + shouldNotSerialize(['"a"', 'b', '"c"'], + 'A literal as subject is not allowed: "a"')); + + //should not serialize a literal in the predicate', + shouldNotSerialize(['a', '"b"', '"c"'], + 'A literal as predicate is not allowed: "b"')); + + //should not serialize an invalid object literal', + shouldNotSerialize(['a', 'b', '"c'], + 'Invalid literal: "c')); + + //should not leave leading whitespace if the prefix set is empty', + shouldSerialize({}, + ['a', 'b', 'c'], + ' .\n')); + + //should serialize valid prefixes', + shouldSerialize({ prefixes: { a: 'http://a.org/', b: 'http://a.org/b#', c: 'http://a.org/b' } }, + '@prefix a: .\n' + + '@prefix b: .\n\n')); + + //should use prefixes when possible', + shouldSerialize({ prefixes: { a: 'http://a.org/', b: 'http://a.org/b#', c: 'http://a.org/b' } }, + ['http://a.org/bc', 'http://a.org/b#ef', 'http://a.org/bhi'], + ['http://a.org/bc/de', 'http://a.org/b#e#f', 'http://a.org/b#x/t'], + ['http://a.org/3a', 'http://a.org/b#3a', 'http://a.org/b#a3'], + '@prefix a: .\n' + + '@prefix b: .\n\n' + + 'a:bc b:ef a:bhi.\n' + + ' .\n' + + ' b:a3.\n')); + + //should expand prefixes when possible', + shouldSerialize({ prefixes: { a: 'http://a.org/', b: 'http://a.org/b#' } }, + ['a:bc', 'b:ef', 'c:bhi'], + '@prefix a: .\n' + + '@prefix b: .\n\n' + + 'a:bc b:ef .\n')); + + //should not repeat the same subjects', + shouldSerialize(['abc', 'def', 'ghi'], + ['abc', 'mno', 'pqr'], + ['stu', 'vwx', 'yz'], + ' ;\n' + + ' .\n' + + ' .\n')); + + //should not repeat the same predicates', + shouldSerialize(['abc', 'def', 'ghi'], + ['abc', 'def', 'pqr'], + ['abc', 'bef', 'ghi'], + ['abc', 'bef', 'pqr'], + ['stu', 'bef', 'yz'], + ' , ;\n' + + ' , .\n' + + ' .\n')); + + //should write rdf:type as "a"', + shouldSerialize(['abc', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'def'], + ' a .\n')); + + //should serialize a graph with 1 triple', + shouldSerialize(['abc', 'def', 'ghi', 'xyz'], + ' {\n' + + ' \n' + + '}\n')); + + //should serialize a graph with 3 triples', + shouldSerialize(['abc', 'def', 'ghi', 'xyz'], + ['jkl', 'mno', 'pqr', 'xyz'], + ['stu', 'vwx', 'yz', 'xyz'], + ' {\n' + + ' .\n' + + ' .\n' + + ' \n' + + '}\n')); + + //should serialize three graphs', + shouldSerialize(['abc', 'def', 'ghi', 'xyz'], + ['jkl', 'mno', 'pqr', ''], + ['stu', 'vwx', 'yz', 'abc'], + ' {\n \n}\n' + + ' .\n' + + ' {\n \n}\n')); + + //should output 8-bit unicode characters as escape sequences', + shouldSerialize(['\ud835\udc00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'], + '<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}\n')); + + //should not use escape sequences in blank nodes', + shouldSerialize(['_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00'], + '_:\ud835\udc00 {\n_:\ud835\udc00 _:\ud835\udc00 _:\ud835\udc00\n}\n')); + + //calls the done callback when ending the outputstream errors', function (done) { + var writer = new N3Writer({ + write: function () {}, + end: function () { throw new Error('error'); }, + }); + writer.end(done); + }); + + //sends output through end when no stream argument is given', function (done) { + var writer = new N3Writer(), notCalled = true; + writer.addTriple({ subject: 'a', predicate: 'b', object: 'c' }, function () { notCalled = false; }); + writer.end(function (error, output) { + output.should.equal(' .\n'); + done(notCalled || error); + }); + }); + + //respects the prefixes argument when no stream argument is given', function (done) { + var writer = new N3Writer({ prefixes: { a: 'b#' } }); + writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#c' }); + writer.end(function (error, output) { + output.should.equal('@prefix a: .\n\na:a a:b a:c.\n'); + done(error); + }); + }); + + //does not repeat identical prefixes', function (done) { + var writer = new N3Writer(); + writer.addPrefix('a', 'b#'); + writer.addPrefix('a', 'b#'); + writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#c' }); + writer.addPrefix('a', 'b#'); + writer.addPrefix('a', 'b#'); + writer.addPrefix('b', 'b#'); + writer.addPrefix('a', 'c#'); + writer.end(function (error, output) { + output.should.equal('@prefix a: .\n\na:a a:b a:c.\n' + + '@prefix b: .\n\n@prefix a: .\n\n'); + done(error); + }); + }); + + //serializes triples of a graph with a prefix declaration in between', function (done) { + var writer = new N3Writer(); + writer.addPrefix('a', 'b#'); + writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#c', graph: 'b#g' }); + writer.addPrefix('d', 'e#'); + writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#d', graph: 'b#g' }); + writer.end(function (error, output) { + output.should.equal('@prefix a: .\n\na:g {\na:a a:b a:c\n}\n' + + '@prefix d: .\n\na:g {\na:a a:b a:d\n}\n'); + done(error); + }); + }); + + //should accept triples with separated components', function (done) { + var writer = N3Writer(); + writer.addTriple('a', 'b', 'c'); + writer.addTriple('a', 'b', 'd'); + writer.end(function (error, output) { + output.should.equal(' , .\n'); + done(error); + }); + }); + + //should accept quads with separated components', function (done) { + var writer = N3Writer(); + writer.addTriple('a', 'b', 'c', 'g'); + writer.addTriple('a', 'b', 'd', 'g'); + writer.end(function (error, output) { + output.should.equal(' {\n , \n}\n'); + done(error); + }); + }); + + //should serialize triples with an empty blank node as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a1', 'b', writer.blank()); + writer.addTriple('a2', 'b', writer.blank([])); + writer.end(function (error, output) { + output.should.equal(' [].\n' + + ' [].\n'); + done(error); + }); + }); + + //should serialize triples with a one-triple blank node as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a1', 'b', writer.blank('d', 'e')); + writer.addTriple('a2', 'b', writer.blank({ predicate: 'd', object: 'e' })); + writer.addTriple('a3', 'b', writer.blank([{ predicate: 'd', object: 'e' }])); + writer.end(function (error, output) { + output.should.equal(' [ ].\n' + + ' [ ].\n' + + ' [ ].\n'); + done(error); + }); + }); + + //should serialize triples with a two-triple blank node as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a', 'b', writer.blank([ + { predicate: 'd', object: 'e' }, + { predicate: 'f', object: '"g"' }, + ])); + writer.end(function (error, output) { + output.should.equal(' [\n' + + ' ;\n' + + ' "g"\n' + + '].\n'); + done(error); + }); + }); + + //should serialize triples with a three-triple blank node as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a', 'b', writer.blank([ + { predicate: 'd', object: 'e' }, + { predicate: 'f', object: '"g"' }, + { predicate: 'h', object: 'i' }, + ])); + writer.end(function (error, output) { + output.should.equal(' [\n' + + ' ;\n' + + ' "g";\n' + + ' \n' + + '].\n'); + done(error); + }); + }); + + //should serialize triples with predicate-sharing blank node triples as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a', 'b', writer.blank([ + { predicate: 'd', object: 'e' }, + { predicate: 'd', object: 'f' }, + { predicate: 'g', object: 'h' }, + { predicate: 'g', object: 'i' }, + ])); + writer.end(function (error, output) { + output.should.equal(' [\n' + + ' , ;\n' + + ' , \n' + + '].\n'); + done(error); + }); + }); + + //should serialize triples with nested blank nodes as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a1', 'b', writer.blank([ + { predicate: 'd', object: writer.blank() }, + ])); + writer.addTriple('a2', 'b', writer.blank([ + { predicate: 'd', object: writer.blank('e', 'f') }, + { predicate: 'g', object: writer.blank('h', '"i"') }, + ])); + writer.addTriple('a3', 'b', writer.blank([ + { predicate: 'd', object: writer.blank([ + { predicate: 'g', object: writer.blank('h', 'i') }, + { predicate: 'j', object: writer.blank('k', '"l"') }, + ]) }, + ])); + writer.end(function (error, output) { + output.should.equal(' [\n' + + ' []\n' + + '].\n' + + ' [\n' + + ' [ ];\n' + + ' [ "i" ]\n' + + '].\n' + + ' [\n' + + ' [\n' + + ' [ ];\n' + + ' [ "l" ]\n' + + ']\n' + + '].\n'); + done(error); + }); + }); + + //should serialize triples with an empty blank node as subject', function (done) { + var writer = N3Writer(); + writer.addTriple(writer.blank(), 'b', 'c'); + writer.addTriple(writer.blank([]), 'b', 'c'); + writer.end(function (error, output) { + output.should.equal('[] .\n' + + '[] .\n'); + done(error); + }); + }); + + //should serialize triples with a one-triple blank node as subject', function (done) { + var writer = N3Writer(); + writer.addTriple(writer.blank('a', 'b'), 'c', 'd'); + writer.addTriple(writer.blank({ predicate: 'a', object: 'b' }), 'c', 'd'); + writer.addTriple(writer.blank([{ predicate: 'a', object: 'b' }]), 'c', 'd'); + writer.end(function (error, output) { + output.should.equal('[ ] .\n' + + '[ ] .\n' + + '[ ] .\n'); + done(error); + }); + }); + + //should serialize triples with an empty blank node as graph', function (done) { + var writer = N3Writer(); + writer.addTriple('a', 'b', 'c', writer.blank()); + writer.addTriple('a', 'b', 'c', writer.blank([])); + writer.end(function (error, output) { + output.should.equal('[] {\n \n}\n' + + '[] {\n \n}\n'); + done(error); + }); + }); + + //should serialize triples with an empty list as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a1', 'b', writer.list()); + writer.addTriple('a2', 'b', writer.list([])); + writer.end(function (error, output) { + output.should.equal(' ().\n' + + ' ().\n'); + done(error); + }); + }); + + //should serialize triples with a one-element list as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a1', 'b', writer.list(['c'])); + writer.addTriple('a2', 'b', writer.list(['"c"'])); + writer.end(function (error, output) { + output.should.equal(' ().\n' + + ' ("c").\n'); + done(error); + }); + }); + + //should serialize triples with a three-element list as object', function (done) { + var writer = N3Writer(); + writer.addTriple('a1', 'b', writer.list(['c', 'd', 'e'])); + writer.addTriple('a2', 'b', writer.list(['"c"', '"d"', '"e"'])); + writer.end(function (error, output) { + output.should.equal(' ( ).\n' + + ' ("c" "d" "e").\n'); + done(error); + }); + }); + + //should serialize triples with an empty list as subject', function (done) { + var writer = N3Writer(); + writer.addTriple(writer.list(), 'b1', 'c'); + writer.addTriple(writer.list([]), 'b2', 'c'); + writer.end(function (error, output) { + output.should.equal('() ;\n' + + ' .\n'); + done(error); + }); + }); + + //should serialize triples with a one-element list as subject', function (done) { + var writer = N3Writer(); + writer.addTriple(writer.list(['a']), 'b1', 'c'); + writer.addTriple(writer.list(['a']), 'b2', 'c'); + writer.end(function (error, output) { + output.should.equal('() ;\n' + + ' .\n'); + done(error); + }); + }); + + //should serialize triples with a three-element list as subject', function (done) { + var writer = N3Writer(); + writer.addTriple(writer.list(['a', '"b"', '"c"']), 'd', 'e'); + writer.end(function (error, output) { + output.should.equal('( "b" "c") .\n'); + done(error); + }); + }); + + //should accept triples in bulk', function (done) { + var writer = N3Writer(); + writer.addTriples([{ subject: 'a', predicate: 'b', object: 'c' }, + { subject: 'a', predicate: 'b', object: 'd' }]); + writer.end(function (error, output) { + output.should.equal(' , .\n'); + done(error); + }); + }); + + //should not allow writing after end', function (done) { + var writer = N3Writer(); + writer.addTriple({ subject: 'a', predicate: 'b', object: 'c' }); + writer.end(); + writer.addTriple({ subject: 'd', predicate: 'e', object: 'f' }, function (error) { + error.should.be.an.instanceof(Error); + error.should.have.property('message', 'Cannot write because the writer has been closed.'); + done(); + }); + }); + + //should write simple triples in N-Triples mode', function (done) { + var writer = N3Writer({ format: 'N-Triples' }); + writer.addTriple('a', 'b', 'c'); + writer.addTriple('a', 'b', 'd'); + writer.end(function (error, output) { + output.should.equal(' .\n .\n'); + done(error); + }); + }); + + //should not write an invalid literal in N-Triples mode', function (done) { + var writer = N3Writer({ format: 'N-Triples' }); + writer.addTriple('a', 'b', '"c', function (error) { + error.should.be.an.instanceof(Error); + error.should.have.property('message', 'Invalid literal: "c'); + done(); + }); + }); + + //should write simple quads in N-Quads mode', function (done) { + var writer = N3Writer({ format: 'N-Quads' }); + writer.addTriple('a', 'b', 'c'); + writer.addTriple('a', 'b', 'd', 'g'); + writer.end(function (error, output) { + output.should.equal(' .\n .\n'); + done(error); + }); + }); + + //should not write an invalid literal in N-Quads mode', function (done) { + var writer = N3Writer({ format: 'N-Triples' }); + writer.addTriple('a', 'b', '"c', function (error) { + error.should.be.an.instanceof(Error); + error.should.have.property('message', 'Invalid literal: "c'); + done(); + }); + }); + + //should end when the end option is not set', function (done) { + var outputStream = new QuickStream(), writer = N3Writer(outputStream, {}); + outputStream.should.have.property('ended', false); + writer.end(function () { + outputStream.should.have.property('ended', true); + done(); + }); + }); + + //should end when the end option is set to true', function (done) { + var outputStream = new QuickStream(), writer = N3Writer(outputStream, { end: true }); + outputStream.should.have.property('ended', false); + writer.end(function () { + outputStream.should.have.property('ended', true); + done(); + }); + }); + + //should not end when the end option is set to false', function (done) { + var outputStream = new QuickStream(), writer = N3Writer(outputStream, { end: false }); + outputStream.should.have.property('ended', false); + writer.end(function () { + outputStream.should.have.property('ended', false); + done(); + }); + }); + }); + +function shouldSerialize() { + var tripleArrays = Array.prototype.slice.call(arguments), + expectedResult = tripleArrays.pop(), + prefixes = tripleArrays[0] instanceof Array ? null : tripleArrays.shift(); + + return function (done) { + var outputStream = new QuickStream(), + writer = N3Writer(outputStream, prefixes); + (function next() { + var item = tripleArrays.shift(); + if (item) + writer.addTriple({ subject: item[0], predicate: item[1], object: item[2], graph: item[3] }, next); + else + writer.end(function (error) { + try { + outputStream.result.should.equal(expectedResult); + outputStream.should.have.property('ended', true); + done(error); + } + catch (e) { + done(e); + } + }); + })(); + }; +} + +function shouldNotSerialize() { + var tripleArrays = Array.prototype.slice.call(arguments), + expectedMessage = tripleArrays.pop(); + + return function (done) { + var outputStream = new QuickStream(), + writer = N3Writer(outputStream), + item = tripleArrays.shift(); + writer.addTriple({ subject: item[0], predicate: item[1], object: item[2], graph: item[3] }, + function (error) { + if (error) { + error.message.should.equal(expectedMessage); + done(); + } + }); + }; +} +*/ + + +} From 9208af5a795d254670b071dbe19ea33137bdad88 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 30 Mar 2017 16:47:20 +0200 Subject: [PATCH 02/72] Converted some tests and code from nodejs to PHP. Discarting streaming functionality --- src/TRiGWriter.php | 307 -------------------- src/TriGWriter.php | 329 +++++++++++++++++++++ test/TRiGWriterTest.php | 616 ---------------------------------------- test/TriGWriterTest.php | 590 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 919 insertions(+), 923 deletions(-) delete mode 100644 src/TRiGWriter.php create mode 100644 src/TriGWriter.php delete mode 100644 test/TRiGWriterTest.php create mode 100644 test/TriGWriterTest.php diff --git a/src/TRiGWriter.php b/src/TRiGWriter.php deleted file mode 100644 index 3783c19..0000000 --- a/src/TRiGWriter.php +++ /dev/null @@ -1,307 +0,0 @@ - '\\\\', '"': '\\"', '\t': '\\t', - '\n' => '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f' - ]; - - public function __construct($options) - { - /* Initialize writer, depending on the format - this._subject = null; - if (!(/triple|quad/i).test(options.format)) { - this._graph = ''; - this._prefixIRIs = Object.create(null); - options.prefixes && this.addPrefixes(options.prefixes); - }*/ - } - - /* -N3Writer.prototype = { - // ## Private methods - - // ### `_write` writes the argument to the output stream - _write: function (string, callback) { - this._outputStream.write(string, 'utf8', callback); - }, - - // ### `_writeTriple` writes the triple to the output stream - _writeTriple: function (subject, predicate, object, graph, done) { - try { - // Write the graph's label if it has changed - if (this._graph !== graph) { - // Close the previous graph and start the new one - this._write((this._subject === null ? '' : (this._graph ? '\n}\n' : '.\n')) + - (graph ? this._encodeIriOrBlankNode(graph) + ' {\n' : '')); - this._subject = null; - // Don't treat identical blank nodes as repeating graphs - this._graph = graph[0] !== '[' ? graph : ']'; - } - // Don't repeat the subject if it's the same - if (this._subject === subject) { - // Don't repeat the predicate if it's the same - if (this._predicate === predicate) - this._write(', ' + this._encodeObject(object), done); - // Same subject, different predicate - else - this._write(';\n ' + - this._encodePredicate(this._predicate = predicate) + ' ' + - this._encodeObject(object), done); - } - // Different subject; write the whole triple - else - this._write((this._subject === null ? '' : '.\n') + - this._encodeSubject(this._subject = subject) + ' ' + - this._encodePredicate(this._predicate = predicate) + ' ' + - this._encodeObject(object), done); - } - catch (error) { done && done(error); } - }, - - // ### `_writeTripleLine` writes the triple or quad to the output stream as a single line - _writeTripleLine: function (subject, predicate, object, graph, done) { - // Don't use prefixes - delete this._prefixMatch; - // Write the triple - try { - this._write(this._encodeIriOrBlankNode(subject) + ' ' + - this._encodeIriOrBlankNode(predicate) + ' ' + - this._encodeObject(object) + - (graph ? ' ' + this._encodeIriOrBlankNode(graph) + '.\n' : '.\n'), done); - } - catch (error) { done && done(error); } - }, - - // ### `_encodeIriOrBlankNode` represents an IRI or blank node - _encodeIriOrBlankNode: function (entity) { - // A blank node or list is represented as-is - var firstChar = entity[0]; - if (firstChar === '[' || firstChar === '(' || firstChar === '_' && entity[1] === ':') - return entity; - // Escape special characters - if (escape.test(entity)) - entity = entity.replace(escapeAll, characterReplacer); - // Try to represent the IRI as prefixed name - var prefixMatch = this._prefixRegex.exec(entity); - return !prefixMatch ? '<' + entity + '>' : - (!prefixMatch[1] ? entity : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]); - }, - - // ### `_encodeLiteral` represents a literal - _encodeLiteral: function (value, type, language) { - // Escape special characters - if (escape.test(value)) - value = value.replace(escapeAll, characterReplacer); - // Write the literal, possibly with type or language - if (language) - return '"' + value + '"@' + language; - else if (type) - return '"' + value + '"^^' + this._encodeIriOrBlankNode(type); - else - return '"' + value + '"'; - }, - - // ### `_encodeSubject` represents a subject - _encodeSubject: function (subject) { - if (subject[0] === '"') - throw new Error('A literal as subject is not allowed: ' + subject); - // Don't treat identical blank nodes as repeating subjects - if (subject[0] === '[') - this._subject = ']'; - return this._encodeIriOrBlankNode(subject); - }, - - // ### `_encodePredicate` represents a predicate - _encodePredicate: function (predicate) { - if (predicate[0] === '"') - throw new Error('A literal as predicate is not allowed: ' + predicate); - return predicate === RDF_TYPE ? 'a' : this._encodeIriOrBlankNode(predicate); - }, - - // ### `_encodeObject` represents an object - _encodeObject: function (object) { - // Represent an IRI or blank node - if (object[0] !== '"') - return this._encodeIriOrBlankNode(object); - // Represent a literal - var match = N3LiteralMatcher.exec(object); - if (!match) throw new Error('Invalid literal: ' + object); - return this._encodeLiteral(match[1], match[2], match[3]); - }, - - // ### `_blockedWrite` replaces `_write` after the writer has been closed - _blockedWrite: function () { - throw new Error('Cannot write because the writer has been closed.'); - }, - - // ### `addTriple` adds the triple to the output stream - addTriple: function (subject, predicate, object, graph, done) { - // The triple was given as a triple object, so shift parameters - if (object === undefined) - this._writeTriple(subject.subject, subject.predicate, subject.object, - subject.graph || '', predicate); - // The optional `graph` parameter was not provided - else if (typeof graph !== 'string') - this._writeTriple(subject, predicate, object, '', graph); - // The `graph` parameter was provided - else - this._writeTriple(subject, predicate, object, graph, done); - }, - - // ### `addTriples` adds the triples to the output stream - addTriples: function (triples) { - for (var i = 0; i < triples.length; i++) - this.addTriple(triples[i]); - }, - - // ### `addPrefix` adds the prefix to the output stream - addPrefix: function (prefix, iri, done) { - var prefixes = {}; - prefixes[prefix] = iri; - this.addPrefixes(prefixes, done); - }, - - // ### `addPrefixes` adds the prefixes to the output stream - addPrefixes: function (prefixes, done) { - // Add all useful prefixes - var prefixIRIs = this._prefixIRIs, hasPrefixes = false; - for (var prefix in prefixes) { - // Verify whether the prefix can be used and does not exist yet - var iri = prefixes[prefix]; - if (/[#\/]$/.test(iri) && prefixIRIs[iri] !== (prefix += ':')) { - hasPrefixes = true; - prefixIRIs[iri] = prefix; - // Finish a possible pending triple - if (this._subject !== null) { - this._write(this._graph ? '\n}\n' : '.\n'); - this._subject = null, this._graph = ''; - } - // Write prefix - this._write('@prefix ' + prefix + ' <' + iri + '>.\n'); - } - } - // Recreate the prefix matcher - if (hasPrefixes) { - var IRIlist = '', prefixList = ''; - for (var prefixIRI in prefixIRIs) { - IRIlist += IRIlist ? '|' + prefixIRI : prefixIRI; - prefixList += (prefixList ? '|' : '') + prefixIRIs[prefixIRI]; - } - IRIlist = IRIlist.replace(/[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&'); - this._prefixRegex = new RegExp('^(?:' + prefixList + ')[^\/]*$|' + - '^(' + IRIlist + ')([a-zA-Z][\\-_a-zA-Z0-9]*)$'); - } - // End a prefix block with a newline - this._write(hasPrefixes ? '\n' : '', done); - }, - - // ### `blank` creates a blank node with the given content - blank: function (predicate, object) { - var children = predicate, child, length; - // Empty blank node - if (predicate === undefined) - children = []; - // Blank node passed as blank("predicate", "object") - else if (typeof predicate === 'string') - children = [{ predicate: predicate, object: object }]; - // Blank node passed as blank({ predicate: predicate, object: object }) - else if (!('length' in predicate)) - children = [predicate]; - - switch (length = children.length) { - // Generate an empty blank node - case 0: - return '[]'; - // Generate a non-nested one-triple blank node - case 1: - child = children[0]; - if (child.object[0] !== '[') - return '[ ' + this._encodePredicate(child.predicate) + ' ' + - this._encodeObject(child.object) + ' ]'; - // Generate a multi-triple or nested blank node - default: - var contents = '['; - // Write all triples in order - for (var i = 0; i < length; i++) { - child = children[i]; - // Write only the object is the predicate is the same as the previous - if (child.predicate === predicate) - contents += ', ' + this._encodeObject(child.object); - // Otherwise, write the predicate and the object - else { - contents += (i ? ';\n ' : '\n ') + - this._encodePredicate(child.predicate) + ' ' + - this._encodeObject(child.object); - predicate = child.predicate; - } - } - return contents + '\n]'; - } - }, - - // ### `list` creates a list node with the given content - list: function (elements) { - var length = elements && elements.length || 0, contents = new Array(length); - for (var i = 0; i < length; i++) - contents[i] = this._encodeObject(elements[i]); - return '(' + contents.join(' ') + ')'; - }, - - // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes - _prefixRegex: /$0^/, - - // ### `end` signals the end of the output stream - end: function (done) { - // Finish a possible pending triple - if (this._subject !== null) { - this._write(this._graph ? '\n}\n' : '.\n'); - this._subject = null; - } - // Disallow further writing - this._write = this._blockedWrite; - - // Try to end the underlying stream, ensuring done is called exactly one time - var singleDone = done && function (error, result) { singleDone = null, done(error, result); }; - if (this._endStream) { - try { return this._outputStream.end(singleDone); } - catch (error) { } - } - singleDone && singleDone(); - }, -}; - -// Replaces a character by its escaped version -function characterReplacer(character) { - // Replace a single character by its escaped version - var result = escapeReplacements[character]; - if (result === undefined) { - // Replace a single character with its 4-bit unicode escape sequence - if (character.length === 1) { - result = character.charCodeAt(0).toString(16); - result = '\\u0000'.substr(0, 6 - result.length) + result; - } - // Replace a surrogate pair with its 8-bit unicode escape sequence - else { - result = ((character.charCodeAt(0) - 0xD800) * 0x400 + - character.charCodeAt(1) + 0x2400).toString(16); - result = '\\U00000000'.substr(0, 10 - result.length) + result; - } - } - return result; -}*/ - -} diff --git a/src/TriGWriter.php b/src/TriGWriter.php new file mode 100644 index 0000000..525e05d --- /dev/null +++ b/src/TriGWriter.php @@ -0,0 +1,329 @@ + "\\\\", '"'=> '\\"', "\t"=> "\\t", + "\n" => "\\n", "\r"=> "\\r", "\b"=> "\\b", "\f"=> "\\f" + ]; + + // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes + private $prefixRegex = "/$0^/"; + + private $subject, $graph, $prefixIRIs, $blocked = false, $string; + + // Replaces a character by its escaped version + private $characterReplacer; + + public function __construct($options = []) + { + /* Initialize writer, depending on the format*/ + $this->subject = null; + if (!isset($options["format"]) || !(preg_match("/triple|quad/i", $options["format"]))) { + $this->graph = ''; + $this->prefixIRIs = []; + if (isset($options["prefixes"])) { + $this->addPrefixes($options["prefixes"]); + } + } + + $this->characterReplacer = function ($character) { + // Replace a single character by its escaped version + $character = $character[0]; + + if (isset($character) && isset(self::ESCAPEREPLACEMENTS[$character])) { + return self::ESCAPEREPLACEMENTS[$character]; + } else { + // Replace a single character with its 4-bit unicode escape sequence + $result = ""; + if (strlen($character) === 1) { + //TODO + //$result = $character.charCodeAt(0).toString(16); + //$result = \'\\u0000\'.substr(0, 6 - strlen($result)) + $result; + } + // Replace a surrogate pair with its 8-bit unicode escape sequence + else { + //$result = (($character.charCodeAt(0) - 0xD800) * 0x400 + + //$character.charCodeAt(1) + 0x2400).toString(16); + //$result = \'\\U00000000\'.substr(0, 10 - strlen($result)) + $result; + } + return $result; + } + }; + } + + // ### `_write` writes the argument to the output stream + private function write ($string, $callback = null) { + //this._outputStream.write(string, 'utf8', callback); + if ($this->blocked) { + throw new Exception('Cannot write because the writer has been closed.'); + } else { + if (isset($callback)) { + $callback($string); + } else { + //buffer all + $this->string .= $string; + } + } + } + + // ### `_writeTriple` writes the triple to the output stream + private function writeTriple ($subject, $predicate, $object, $graph, $done = null) { + try { + // Write the graph's label if it has changed + if ($this->graph !== $graph) { + // Close the previous graph and start the new one + $this->write(($this->subject === null ? '' : ($this->graph ? "\n}\n" : ".\n")) . ($graph ? $this->encodeIriOrBlankNode($graph) . " {\n" : '')); + $this->subject = null; + // Don't treat identical blank nodes as repeating graphs + $this->graph = $graph[0] !== '[' ? $graph : ']'; + } + // Don't repeat the subject if it's the same + if ($this->subject === $subject) { + // Don't repeat the predicate if it's the same + if ($this->predicate === $predicate) + $this->write(', ' + $this->encodeObject($object), $done); + // Same subject, different predicate + else { + $this->write(";\n " . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); + } + + } + // Different subject; write the whole triple + else { + $this->write(($this->subject === null ? '' : ".\n") . $this->encodeSubject($this->subject = $subject) . ' ' . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); + } + } catch (Exception $error) { + if (isset($done)) { + $done($error); + } + } + } + + // ### `_writeTripleLine` writes the triple or quad to the output stream as a single line + private function writeTripleLine ($subject, $predicate, $object, $graph, $done = null) { + // Don't use prefixes + delete($this->prefixMatch); + // Write the triple + try { + $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . ($graph ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); + } catch (Exception $error) { + if (isset($done)) { + $done($error); + } + } + } + + + // ### `_encodeIriOrBlankNode` represents an IRI or blank node + private function encodeIriOrBlankNode ($entity) { + // A blank node or list is represented as-is + $firstChar = substr($entity, 0, 1); + if ($firstChar === '[' || $firstChar === '(' || $firstChar === '_' && substr($entity, 1, 1) === ':') + return $entity; + // Escape special characters + if (preg_match(self::ESCAPE, $entity)) + $entity = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer,$entity); + // Try to represent the IRI as prefixed name + if (preg_match($this->prefixRegex, $entity, $prefixMatch)) { + return '<' . $entity . '>'; + } else { + return !isset($prefixMatch[1]) ? $entity : $this->prefixIRIs[$prefixMatch[1]] . $prefixMatch[2]; + } + } + + // ### `_encodeLiteral` represents a literal + private function encodeLiteral ($value, $type = null, $language = null) { + // Escape special characters + if (preg_match(self::ESCAPE,$value)) + $value = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer, $value); + // Write the literal, possibly with type or language + if (isset($language)) + return '"' . $value . '"@' . $language; + else if (isset($type)) + return '"' . $value . '"^^' . $this->encodeIriOrBlankNode($type); + else + return '"' . $value . '"'; + } + + // ### `_encodeSubject` represents a subject + private function encodeSubject ($subject) { + if ($subject[0] === '"') + throw new Exception('A literal as subject is not allowed: ' . $subject); + // Don't treat identical blank nodes as repeating subjects + if ($subject[0] === '[') + $this->subject = ']'; + return $this->encodeIriOrBlankNode($subject); + } + + + // ### `_encodePredicate` represents a predicate + private function encodePredicate ($predicate) { + if ($predicate[0] === '"') + throw new Exception('A literal as predicate is not allowed: ' . $predicate); + return $predicate === self::RDF_TYPE ? 'a' : $this->encodeIriOrBlankNode($predicate); + } + + // ### `_encodeObject` represents an object + private function encodeObject ($object) { + // Represent an IRI or blank node + if ($object[0] !== '"') + return $this->encodeIriOrBlankNode($object); + // Represent a literal + if (preg_match(self::LITERALMATCHER, $object, $matches)) { + return $this->encodeLiteral($matches[1], isset($matches[2])?$matches[2]:null, isset($matches[3])?$matches[3]:null); + } + else { + throw new Exception('Invalid literal: ' . $object); + } + } + + + // ### `addTriple` adds the triple to the output stream + public function addTriple ($subject, $predicate = null, $object = null, $graph = null, $done = null) { + // The triple was given as a triple object, so shift parameters + if (is_array($subject)) { + $g = isset($subject["graph"])?$subject["graph"]:null; + $this->writeTriple($subject["subject"], $subject["predicate"], $subject["object"], $g, $predicate); + } + // The optional `graph` parameter was not provided + else if (!is_string($graph)) + $this->writeTriple($subject, $predicate, $object, '', $graph); + // The `graph` parameter was provided + else + $this->writeTriple($subject, $predicate, $object, $graph, $done); + } + + // ### `addTriples` adds the triples to the output stream + public function addTriples ($triples) { + for ($i = 0; $i < strlen($triples); $i++) + $this->addTriple(triples[$i]); + } + + // ### `addPrefix` adds the prefix to the output stream + public function addPrefix($prefix, $iri, $done) + { + $prefixes = []; + $prefixes[$prefix] = $iri; + $this->addPrefixes($prefixes, $done); + } + + // ### `addPrefixes` adds the prefixes to the output stream + public function addPrefixes ($prefixes, $done = null) { + // Add all useful prefixes + $prefixIRIs = $this->prefixIRIs; + $hasPrefixes = false; + foreach ($prefixes as $i => $prefix) { + // Verify whether the prefix can be used and does not exist yet + if (isset($prefixes[$prefix])) { + $iri = $prefixes[$prefix]; + if (preg_match('/[#\/]$/',$iri) && $prefixIRIs[$iri] !== ($prefix .= ':')) { + $hasPrefixes = true; + $prefixIRIs[$iri] = $prefix; + // Finish a possible pending triple + if ($this->subject !== null) { + $this->write($this->graph ? "\n}\n" : ".\n"); + $this->subject = null; + $this->graph = ''; + } + // Write prefix + $this->write('@prefix ' . $prefix . ' <' . $iri . ">.\n"); + } + } + + } + // Recreate the prefix matcher + if (isset($hasPrefixes)) { + $IRIlist = ''; + $prefixList = ''; + foreach ($prefixIRIs as $i => $prefixIRI) { + $IRIlist .= $IRIlist ? '|' . $prefixIRI : $prefixIRI; + $prefixList .= ($prefixList ? '|' : '') . $prefixIRIs[$prefixIRI]; + } + $IRIlist = preg_replace("/[\]\/\(\)\*\+\?\.\\\$]/", '\\$&', $IRIlist); + $this->prefixRegex = '/^(?:' . $prefixList . ')[^\/]*$|' . '^(' . $IRIlist . ')([a-zA-Z][\\-_a-zA-Z0-9]*)$/'; + } + // End a prefix block with a newline + $this->write($hasPrefixes ? "\n" : '', $done); + } +/* + // ### `blank` creates a blank node with the given content + public function blank (predicate, object) { + $children = predicate, child, length; + // Empty blank node + if (predicate === undefined) + children = []; + // Blank node passed as blank("predicate", "object") + else if (typeof predicate === 'string') + children = [{ predicate: predicate, object: object }]; + // Blank node passed as blank({ predicate: predicate, object: object }) + else if (!('length' in predicate)) + children = [predicate]; + + switch (length = children.length) { + // Generate an empty blank node + case 0: + return '[]'; + // Generate a non-nested one-triple blank node + case 1: + child = children[0]; + if (child.object[0] !== '[') + return '[ ' + $this->encodePredicate(child.predicate) + ' ' + + $this->encodeObject(child.object) + ' ]'; + // Generate a multi-triple or nested blank node + default: + $contents = '['; + // Write all triples in order + for ($i = 0; i < length; i++) { + child = children[i]; + // Write only the object is the predicate is the same as the previous + if (child.predicate === predicate) + contents += ', ' + $this->encodeObject(child.object); + // Otherwise, write the predicate and the object + else { + contents += (i ? ";\n " : "\n ") + + $this->encodePredicate(child.predicate) + ' ' + + $this->encodeObject(child.object); + predicate = child.predicate; + } + } + return contents + "\n]"; + } + } + + // ### `list` creates a list node with the given content + public function list (elements) { + $length = elements && elements.length || 0, contents = new Array(length); + for ($i = 0; i < length; i++) + contents[i] = $this->encodeObject(elements[i]); + return '(' + contents.join(' ') + ')'; + }*/ + + // ### `end` signals the end of the output stream + public function end($callback) + { + // Finish a possible pending triple + if ($this->subject !== null) { + $this->write($this->graph ? "\n}\n" : ".\n"); + $this->subject = null; + } + // Disallow further writing + $this->blocked = true; + $callback(null,$this->string); + } +} \ No newline at end of file diff --git a/test/TRiGWriterTest.php b/test/TRiGWriterTest.php deleted file mode 100644 index fa7bf4c..0000000 --- a/test/TRiGWriterTest.php +++ /dev/null @@ -1,616 +0,0 @@ - .\n')); - - //should serialize 2 triples', - shouldSerialize(['abc', 'def', 'ghi'], - ['jkl', 'mno', 'pqr'], - ' .\n' + - ' .\n')); - - //should serialize 3 triples', - shouldSerialize(['abc', 'def', 'ghi'], - ['jkl', 'mno', 'pqr'], - ['stu', 'vwx', 'yz'], - ' .\n' + - ' .\n' + - ' .\n')); - - //should serialize a literal', - shouldSerialize(['a', 'b', '"cde"'], - ' "cde".\n')); - - //should serialize a literal with a type', - shouldSerialize(['a', 'b', '"cde"^^fgh'], - ' "cde"^^.\n')); - - //should serialize a literal with a language', - shouldSerialize(['a', 'b', '"cde"@en-us'], - ' "cde"@en-us.\n')); - - //should serialize a literal containing a single quote', - shouldSerialize(['a', 'b', '"c\'de"'], - ' "c\'de".\n')); - - //should serialize a literal containing a double quote', - shouldSerialize(['a', 'b', '"c"de"'], - ' "c\\"de".\n')); - - //should serialize a literal containing a backspace', - shouldSerialize(['a', 'b', '"c\\de"'], - ' "c\\\\de".\n')); - - //should serialize a literal containing a tab character', - shouldSerialize(['a', 'b', '"c\tde"'], - ' "c\\tde".\n')); - - //should serialize a literal containing a newline character', - shouldSerialize(['a', 'b', '"c\nde"'], - ' "c\\nde".\n')); - - //should serialize a literal containing a cariage return character', - shouldSerialize(['a', 'b', '"c\rde"'], - ' "c\\rde".\n')); - - //should serialize a literal containing a backspace character', - shouldSerialize(['a', 'b', '"c\bde"'], - ' "c\\bde".\n')); - - //should serialize a literal containing a form feed character', - shouldSerialize(['a', 'b', '"c\fde"'], - ' "c\\fde".\n')); - - //should serialize a literal containing a line separator', - shouldSerialize(['a', 'b', '"c\u2028de"'], - ' "c\u2028de".\n')); - - //should serialize a literal containing a paragraph separator', - shouldSerialize(['a', 'b', '"c\u2029de"'], - ' "c\u2029de".\n')); - - //should serialize a literal containing special unicode characters', - shouldSerialize(['a', 'b', '"c\u0000\u0001"'], - ' "c\\u0000\\u0001".\n')); - - //should serialize blank nodes', - shouldSerialize(['_:a', 'b', '_:c'], - '_:a _:c.\n')); - - //should not serialize a literal in the subject', - shouldNotSerialize(['"a"', 'b', '"c"'], - 'A literal as subject is not allowed: "a"')); - - //should not serialize a literal in the predicate', - shouldNotSerialize(['a', '"b"', '"c"'], - 'A literal as predicate is not allowed: "b"')); - - //should not serialize an invalid object literal', - shouldNotSerialize(['a', 'b', '"c'], - 'Invalid literal: "c')); - - //should not leave leading whitespace if the prefix set is empty', - shouldSerialize({}, - ['a', 'b', 'c'], - ' .\n')); - - //should serialize valid prefixes', - shouldSerialize({ prefixes: { a: 'http://a.org/', b: 'http://a.org/b#', c: 'http://a.org/b' } }, - '@prefix a: .\n' + - '@prefix b: .\n\n')); - - //should use prefixes when possible', - shouldSerialize({ prefixes: { a: 'http://a.org/', b: 'http://a.org/b#', c: 'http://a.org/b' } }, - ['http://a.org/bc', 'http://a.org/b#ef', 'http://a.org/bhi'], - ['http://a.org/bc/de', 'http://a.org/b#e#f', 'http://a.org/b#x/t'], - ['http://a.org/3a', 'http://a.org/b#3a', 'http://a.org/b#a3'], - '@prefix a: .\n' + - '@prefix b: .\n\n' + - 'a:bc b:ef a:bhi.\n' + - ' .\n' + - ' b:a3.\n')); - - //should expand prefixes when possible', - shouldSerialize({ prefixes: { a: 'http://a.org/', b: 'http://a.org/b#' } }, - ['a:bc', 'b:ef', 'c:bhi'], - '@prefix a: .\n' + - '@prefix b: .\n\n' + - 'a:bc b:ef .\n')); - - //should not repeat the same subjects', - shouldSerialize(['abc', 'def', 'ghi'], - ['abc', 'mno', 'pqr'], - ['stu', 'vwx', 'yz'], - ' ;\n' + - ' .\n' + - ' .\n')); - - //should not repeat the same predicates', - shouldSerialize(['abc', 'def', 'ghi'], - ['abc', 'def', 'pqr'], - ['abc', 'bef', 'ghi'], - ['abc', 'bef', 'pqr'], - ['stu', 'bef', 'yz'], - ' , ;\n' + - ' , .\n' + - ' .\n')); - - //should write rdf:type as "a"', - shouldSerialize(['abc', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'def'], - ' a .\n')); - - //should serialize a graph with 1 triple', - shouldSerialize(['abc', 'def', 'ghi', 'xyz'], - ' {\n' + - ' \n' + - '}\n')); - - //should serialize a graph with 3 triples', - shouldSerialize(['abc', 'def', 'ghi', 'xyz'], - ['jkl', 'mno', 'pqr', 'xyz'], - ['stu', 'vwx', 'yz', 'xyz'], - ' {\n' + - ' .\n' + - ' .\n' + - ' \n' + - '}\n')); - - //should serialize three graphs', - shouldSerialize(['abc', 'def', 'ghi', 'xyz'], - ['jkl', 'mno', 'pqr', ''], - ['stu', 'vwx', 'yz', 'abc'], - ' {\n \n}\n' + - ' .\n' + - ' {\n \n}\n')); - - //should output 8-bit unicode characters as escape sequences', - shouldSerialize(['\ud835\udc00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'], - '<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}\n')); - - //should not use escape sequences in blank nodes', - shouldSerialize(['_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00'], - '_:\ud835\udc00 {\n_:\ud835\udc00 _:\ud835\udc00 _:\ud835\udc00\n}\n')); - - //calls the done callback when ending the outputstream errors', function (done) { - var writer = new N3Writer({ - write: function () {}, - end: function () { throw new Error('error'); }, - }); - writer.end(done); - }); - - //sends output through end when no stream argument is given', function (done) { - var writer = new N3Writer(), notCalled = true; - writer.addTriple({ subject: 'a', predicate: 'b', object: 'c' }, function () { notCalled = false; }); - writer.end(function (error, output) { - output.should.equal(' .\n'); - done(notCalled || error); - }); - }); - - //respects the prefixes argument when no stream argument is given', function (done) { - var writer = new N3Writer({ prefixes: { a: 'b#' } }); - writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#c' }); - writer.end(function (error, output) { - output.should.equal('@prefix a: .\n\na:a a:b a:c.\n'); - done(error); - }); - }); - - //does not repeat identical prefixes', function (done) { - var writer = new N3Writer(); - writer.addPrefix('a', 'b#'); - writer.addPrefix('a', 'b#'); - writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#c' }); - writer.addPrefix('a', 'b#'); - writer.addPrefix('a', 'b#'); - writer.addPrefix('b', 'b#'); - writer.addPrefix('a', 'c#'); - writer.end(function (error, output) { - output.should.equal('@prefix a: .\n\na:a a:b a:c.\n' + - '@prefix b: .\n\n@prefix a: .\n\n'); - done(error); - }); - }); - - //serializes triples of a graph with a prefix declaration in between', function (done) { - var writer = new N3Writer(); - writer.addPrefix('a', 'b#'); - writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#c', graph: 'b#g' }); - writer.addPrefix('d', 'e#'); - writer.addTriple({ subject: 'b#a', predicate: 'b#b', object: 'b#d', graph: 'b#g' }); - writer.end(function (error, output) { - output.should.equal('@prefix a: .\n\na:g {\na:a a:b a:c\n}\n' + - '@prefix d: .\n\na:g {\na:a a:b a:d\n}\n'); - done(error); - }); - }); - - //should accept triples with separated components', function (done) { - var writer = N3Writer(); - writer.addTriple('a', 'b', 'c'); - writer.addTriple('a', 'b', 'd'); - writer.end(function (error, output) { - output.should.equal(' , .\n'); - done(error); - }); - }); - - //should accept quads with separated components', function (done) { - var writer = N3Writer(); - writer.addTriple('a', 'b', 'c', 'g'); - writer.addTriple('a', 'b', 'd', 'g'); - writer.end(function (error, output) { - output.should.equal(' {\n , \n}\n'); - done(error); - }); - }); - - //should serialize triples with an empty blank node as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a1', 'b', writer.blank()); - writer.addTriple('a2', 'b', writer.blank([])); - writer.end(function (error, output) { - output.should.equal(' [].\n' + - ' [].\n'); - done(error); - }); - }); - - //should serialize triples with a one-triple blank node as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a1', 'b', writer.blank('d', 'e')); - writer.addTriple('a2', 'b', writer.blank({ predicate: 'd', object: 'e' })); - writer.addTriple('a3', 'b', writer.blank([{ predicate: 'd', object: 'e' }])); - writer.end(function (error, output) { - output.should.equal(' [ ].\n' + - ' [ ].\n' + - ' [ ].\n'); - done(error); - }); - }); - - //should serialize triples with a two-triple blank node as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a', 'b', writer.blank([ - { predicate: 'd', object: 'e' }, - { predicate: 'f', object: '"g"' }, - ])); - writer.end(function (error, output) { - output.should.equal(' [\n' + - ' ;\n' + - ' "g"\n' + - '].\n'); - done(error); - }); - }); - - //should serialize triples with a three-triple blank node as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a', 'b', writer.blank([ - { predicate: 'd', object: 'e' }, - { predicate: 'f', object: '"g"' }, - { predicate: 'h', object: 'i' }, - ])); - writer.end(function (error, output) { - output.should.equal(' [\n' + - ' ;\n' + - ' "g";\n' + - ' \n' + - '].\n'); - done(error); - }); - }); - - //should serialize triples with predicate-sharing blank node triples as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a', 'b', writer.blank([ - { predicate: 'd', object: 'e' }, - { predicate: 'd', object: 'f' }, - { predicate: 'g', object: 'h' }, - { predicate: 'g', object: 'i' }, - ])); - writer.end(function (error, output) { - output.should.equal(' [\n' + - ' , ;\n' + - ' , \n' + - '].\n'); - done(error); - }); - }); - - //should serialize triples with nested blank nodes as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a1', 'b', writer.blank([ - { predicate: 'd', object: writer.blank() }, - ])); - writer.addTriple('a2', 'b', writer.blank([ - { predicate: 'd', object: writer.blank('e', 'f') }, - { predicate: 'g', object: writer.blank('h', '"i"') }, - ])); - writer.addTriple('a3', 'b', writer.blank([ - { predicate: 'd', object: writer.blank([ - { predicate: 'g', object: writer.blank('h', 'i') }, - { predicate: 'j', object: writer.blank('k', '"l"') }, - ]) }, - ])); - writer.end(function (error, output) { - output.should.equal(' [\n' + - ' []\n' + - '].\n' + - ' [\n' + - ' [ ];\n' + - ' [ "i" ]\n' + - '].\n' + - ' [\n' + - ' [\n' + - ' [ ];\n' + - ' [ "l" ]\n' + - ']\n' + - '].\n'); - done(error); - }); - }); - - //should serialize triples with an empty blank node as subject', function (done) { - var writer = N3Writer(); - writer.addTriple(writer.blank(), 'b', 'c'); - writer.addTriple(writer.blank([]), 'b', 'c'); - writer.end(function (error, output) { - output.should.equal('[] .\n' + - '[] .\n'); - done(error); - }); - }); - - //should serialize triples with a one-triple blank node as subject', function (done) { - var writer = N3Writer(); - writer.addTriple(writer.blank('a', 'b'), 'c', 'd'); - writer.addTriple(writer.blank({ predicate: 'a', object: 'b' }), 'c', 'd'); - writer.addTriple(writer.blank([{ predicate: 'a', object: 'b' }]), 'c', 'd'); - writer.end(function (error, output) { - output.should.equal('[ ] .\n' + - '[ ] .\n' + - '[ ] .\n'); - done(error); - }); - }); - - //should serialize triples with an empty blank node as graph', function (done) { - var writer = N3Writer(); - writer.addTriple('a', 'b', 'c', writer.blank()); - writer.addTriple('a', 'b', 'c', writer.blank([])); - writer.end(function (error, output) { - output.should.equal('[] {\n \n}\n' + - '[] {\n \n}\n'); - done(error); - }); - }); - - //should serialize triples with an empty list as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a1', 'b', writer.list()); - writer.addTriple('a2', 'b', writer.list([])); - writer.end(function (error, output) { - output.should.equal(' ().\n' + - ' ().\n'); - done(error); - }); - }); - - //should serialize triples with a one-element list as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a1', 'b', writer.list(['c'])); - writer.addTriple('a2', 'b', writer.list(['"c"'])); - writer.end(function (error, output) { - output.should.equal(' ().\n' + - ' ("c").\n'); - done(error); - }); - }); - - //should serialize triples with a three-element list as object', function (done) { - var writer = N3Writer(); - writer.addTriple('a1', 'b', writer.list(['c', 'd', 'e'])); - writer.addTriple('a2', 'b', writer.list(['"c"', '"d"', '"e"'])); - writer.end(function (error, output) { - output.should.equal(' ( ).\n' + - ' ("c" "d" "e").\n'); - done(error); - }); - }); - - //should serialize triples with an empty list as subject', function (done) { - var writer = N3Writer(); - writer.addTriple(writer.list(), 'b1', 'c'); - writer.addTriple(writer.list([]), 'b2', 'c'); - writer.end(function (error, output) { - output.should.equal('() ;\n' + - ' .\n'); - done(error); - }); - }); - - //should serialize triples with a one-element list as subject', function (done) { - var writer = N3Writer(); - writer.addTriple(writer.list(['a']), 'b1', 'c'); - writer.addTriple(writer.list(['a']), 'b2', 'c'); - writer.end(function (error, output) { - output.should.equal('() ;\n' + - ' .\n'); - done(error); - }); - }); - - //should serialize triples with a three-element list as subject', function (done) { - var writer = N3Writer(); - writer.addTriple(writer.list(['a', '"b"', '"c"']), 'd', 'e'); - writer.end(function (error, output) { - output.should.equal('( "b" "c") .\n'); - done(error); - }); - }); - - //should accept triples in bulk', function (done) { - var writer = N3Writer(); - writer.addTriples([{ subject: 'a', predicate: 'b', object: 'c' }, - { subject: 'a', predicate: 'b', object: 'd' }]); - writer.end(function (error, output) { - output.should.equal(' , .\n'); - done(error); - }); - }); - - //should not allow writing after end', function (done) { - var writer = N3Writer(); - writer.addTriple({ subject: 'a', predicate: 'b', object: 'c' }); - writer.end(); - writer.addTriple({ subject: 'd', predicate: 'e', object: 'f' }, function (error) { - error.should.be.an.instanceof(Error); - error.should.have.property('message', 'Cannot write because the writer has been closed.'); - done(); - }); - }); - - //should write simple triples in N-Triples mode', function (done) { - var writer = N3Writer({ format: 'N-Triples' }); - writer.addTriple('a', 'b', 'c'); - writer.addTriple('a', 'b', 'd'); - writer.end(function (error, output) { - output.should.equal(' .\n .\n'); - done(error); - }); - }); - - //should not write an invalid literal in N-Triples mode', function (done) { - var writer = N3Writer({ format: 'N-Triples' }); - writer.addTriple('a', 'b', '"c', function (error) { - error.should.be.an.instanceof(Error); - error.should.have.property('message', 'Invalid literal: "c'); - done(); - }); - }); - - //should write simple quads in N-Quads mode', function (done) { - var writer = N3Writer({ format: 'N-Quads' }); - writer.addTriple('a', 'b', 'c'); - writer.addTriple('a', 'b', 'd', 'g'); - writer.end(function (error, output) { - output.should.equal(' .\n .\n'); - done(error); - }); - }); - - //should not write an invalid literal in N-Quads mode', function (done) { - var writer = N3Writer({ format: 'N-Triples' }); - writer.addTriple('a', 'b', '"c', function (error) { - error.should.be.an.instanceof(Error); - error.should.have.property('message', 'Invalid literal: "c'); - done(); - }); - }); - - //should end when the end option is not set', function (done) { - var outputStream = new QuickStream(), writer = N3Writer(outputStream, {}); - outputStream.should.have.property('ended', false); - writer.end(function () { - outputStream.should.have.property('ended', true); - done(); - }); - }); - - //should end when the end option is set to true', function (done) { - var outputStream = new QuickStream(), writer = N3Writer(outputStream, { end: true }); - outputStream.should.have.property('ended', false); - writer.end(function () { - outputStream.should.have.property('ended', true); - done(); - }); - }); - - //should not end when the end option is set to false', function (done) { - var outputStream = new QuickStream(), writer = N3Writer(outputStream, { end: false }); - outputStream.should.have.property('ended', false); - writer.end(function () { - outputStream.should.have.property('ended', false); - done(); - }); - }); - }); - -function shouldSerialize() { - var tripleArrays = Array.prototype.slice.call(arguments), - expectedResult = tripleArrays.pop(), - prefixes = tripleArrays[0] instanceof Array ? null : tripleArrays.shift(); - - return function (done) { - var outputStream = new QuickStream(), - writer = N3Writer(outputStream, prefixes); - (function next() { - var item = tripleArrays.shift(); - if (item) - writer.addTriple({ subject: item[0], predicate: item[1], object: item[2], graph: item[3] }, next); - else - writer.end(function (error) { - try { - outputStream.result.should.equal(expectedResult); - outputStream.should.have.property('ended', true); - done(error); - } - catch (e) { - done(e); - } - }); - })(); - }; -} - -function shouldNotSerialize() { - var tripleArrays = Array.prototype.slice.call(arguments), - expectedMessage = tripleArrays.pop(); - - return function (done) { - var outputStream = new QuickStream(), - writer = N3Writer(outputStream), - item = tripleArrays.shift(); - writer.addTriple({ subject: item[0], predicate: item[1], object: item[2], graph: item[3] }, - function (error) { - if (error) { - error.message.should.equal(expectedMessage); - done(); - } - }); - }; -} -*/ - - -} diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php new file mode 100644 index 0000000..242fac6 --- /dev/null +++ b/test/TriGWriterTest.php @@ -0,0 +1,590 @@ +shouldSerialize(''); + } + + public function testOneTriple () + { + + //should serialize 1 triple', + $this->shouldSerialize(['abc', 'def', 'ghi'], + ' .' . "\n"); + } + + public function testWriter() + { + + //should serialize 2 triples', + $this->shouldSerialize(['abc', 'def', 'ghi'], + ['jkl', 'mno', 'pqr'], + ' .' . "\n" . + ' .' . "\n"); + + //should serialize 3 triples', + $this->shouldSerialize(['abc', 'def', 'ghi'], + ['jkl', 'mno', 'pqr'], + ['stu', 'vwx', 'yz'], + ' .' . "\n" . + ' .' . "\n" . + ' .' . "\n"); + + //should serialize a literal', + $this->shouldSerialize(['a', 'b', '"cde"'], + ' "cde".' . "\n"); + + //should serialize a literal with a type', + $this->shouldSerialize(['a', 'b', '"cde"^^fgh'], + ' "cde"^^.' . "\n"); + + //should serialize a literal with a language', + $this->shouldSerialize(['a', 'b', '"cde"@en-us'], + ' "cde"@en-us.' . "\n"); + + //should serialize a literal containing a single quote', + $this->shouldSerialize(['a', 'b', '"c\'de"'], + ' "c\'de".' . "\n"); + + //should serialize a literal containing a double quote', + $this->shouldSerialize(['a', 'b', '"c"de"'], + ' "c\"de".' . "\n"); + + //should serialize a literal containing a backspace', + $this->shouldSerialize(['a', 'b', '"c\\de"'], + ' "c\\de".' . "\n"); + + //should serialize a literal containing a tab character', + $this->shouldSerialize(['a', 'b', '"c\tde"'], + " \"c\tde\".\n"); + + //should serialize a literal containing a newline character', + $this->shouldSerialize(['a', 'b', '"c' . "\n" . 'de"'], + ' "c\\' . "\n" . 'de".' . "\n"); + + //should serialize a literal containing a cariage return character', + $this->shouldSerialize(['a', 'b', '"c\rde"'], + ' "c\\rde".' . "\n"); + + //should serialize a literal containing a backspace character', + $this->shouldSerialize(['a', 'b', '"c\bde"'], + ' "c\\bde".' . "\n"); + + //should serialize a literal containing a form feed character', + $this->shouldSerialize(['a', 'b', '"c\fde"'], + ' "c\\fde".' . "\n"); + + //should serialize a literal containing a line separator', + $this->shouldSerialize(['a', 'b', '"c\u2028de"'], + ' "c\u2028de".' . "\n"); + + //should serialize a literal containing a paragraph separator', + $this->shouldSerialize(['a', 'b', '"c\u2029de"'], + ' "c\u2029de".' . "\n"); + + //should serialize a literal containing special unicode characters', + $this->shouldSerialize(['a', 'b', '"c\u0000\u0001"'], + ' "c\\u0000\\u0001".' . "\n"); + + //should serialize blank nodes', + $this->shouldSerialize(['_:a', 'b', '_:c'], + '_:a _:c.' . "\n"); +/* + //should not serialize a literal in the subject', + shouldNotSerialize(['"a"', 'b', '"c"'], + 'A literal as subject is not allowed: "a"'); + + //should not serialize a literal in the predicate', + shouldNotSerialize(['a', '"b"', '"c"'], + 'A literal as predicate is not allowed: "b"'); + + //should not serialize an invalid object literal', + shouldNotSerialize(['a', 'b', '"c'], + 'Invalid literal: "c'); +*/ + //should not leave leading whitespace if the prefix set is empty', + $this->shouldSerialize([], + ['a', 'b', 'c'], + ' .' . "\n"); + + //should serialize valid prefixes', + $this->shouldSerialize([ "prefixes" => [ "a" => 'http://a.org/', "b" => 'http://a.org/b#', "c" => 'http://a.org/b' ] ], + '@prefix a: .' . "\n" . + '@prefix b: .' . "\n" . "\n"); + + //should use prefixes when possible', + $this->shouldSerialize([ "prefixes" => ['a' => 'http://a.org/','b' => 'http://a.org/b#','c' => 'http://a.org/b' ] ], + ['http://a.org/bc', 'http://a.org/b#ef', 'http://a.org/bhi'], + ['http://a.org/bc/de', 'http://a.org/b#e#f', 'http://a.org/b#x/t'], + ['http://a.org/3a', 'http://a.org/b#3a', 'http://a.org/b#a3'], + '@prefix a: .' . "\n" . + '@prefix b: .' . "\n" . "\n" . + 'a:bc b:ef a:bhi.' . "\n" . + ' .' . "\n" . + ' b:a3.' . "\n"); + + //should expand prefixes when possible', + $this->shouldSerialize([ "prefixes" => ['a' => 'http://a.org/','b' => 'http://a.org/b#' ] ], + ['a:bc', 'b:ef', 'c:bhi'], + '@prefix a: .' . "\n" . + '@prefix b: .' . "\n" . "\n" . + 'a:bc b:ef .' . "\n"); + + //should not repeat the same subjects', + $this->shouldSerialize(['abc', 'def', 'ghi'], + ['abc', 'mno', 'pqr'], + ['stu', 'vwx', 'yz'], + ' ;' . "\n" . + ' .' . "\n" . + ' .' . "\n"); + + //should not repeat the same predicates', + $this->shouldSerialize(['abc', 'def', 'ghi'], + ['abc', 'def', 'pqr'], + ['abc', 'bef', 'ghi'], + ['abc', 'bef', 'pqr'], + ['stu', 'bef', 'yz'], + ' , ;' . "\n" . + ' , .' . "\n" . + ' .' . "\n"); + + //should write rdf:type as "a"', + $this->shouldSerialize(['abc', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'def'], + ' a .' . "\n"); + + //should serialize a graph with 1 triple', + $this->shouldSerialize(['abc', 'def', 'ghi', 'xyz'], + ' {' . "\n" . + ' ' . "\n" . + '}' . "\n"); + + //should serialize a graph with 3 triples', + $this->shouldSerialize(['abc', 'def', 'ghi', 'xyz'], + ['jkl', 'mno', 'pqr', 'xyz'], + ['stu', 'vwx', 'yz', 'xyz'], + ' {' . "\n" . + ' .' . "\n" . + ' .' . "\n" . + ' ' . "\n" . + '}' . "\n"); + + //should serialize three graphs', + $this->shouldSerialize(['abc', 'def', 'ghi', 'xyz'], + ['jkl', 'mno', 'pqr', ''], + ['stu', 'vwx', 'yz', 'abc'], + ' {' . "\n" . ' ' . "\n" . '}' . "\n" . + ' .' . "\n" . + ' {' . "\n" . ' ' . "\n" . '}' . "\n"); + + //should output 8-bit unicode characters as escape sequences', + $this->shouldSerialize(['\ud835\udc00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'], + '<\\U0001d400> {' . "\n" . '<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>' . "\n" . '}' . "\n"); + + //should not use escape sequences in blank nodes', + $this->shouldSerialize(['_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00'], + '_:\ud835\udc00 {' . "\n" . '_:\ud835\udc00 _:\ud835\udc00 _:\ud835\udc00' . "\n" . '}' . "\n"); + } + /* + public function testCallbackOnEnd () { + //sends output through end + $writer = new TriGWriter(); + $writer->addTriple(['subject' => 'a','predicate' => 'b','object' => 'c' ]); + $writer->end(function ($error, $output) { + $this->assertEquals(" .' . "\n" . '",$output); + }); + } + + public function testRespectingPrefixes () + { + //respects the prefixes argument when no stream argument is given', function (done) { + $writer = new TriGWriter([ "prefixes" => ['a' => 'b#' ]]); + $writer->addTriple(['subject' => 'b#a','predicate' => 'b#b','object' => 'b#c' ]); + $writer->end(function ($error, $output) { + $this->assertEquals("@prefix a: .' . "\n" . "\n" . 'a:a a:b a:c.' . "\n" . '",$output); + }); + }*/ + +/* + +//does not repeat identical prefixes', function (done) { +$writer = new TriGWriter(); +$writer->addPrefix('a', 'b#'); +$writer->addPrefix('a', 'b#'); +$writer->addTriple({'subject' => 'b#a','predicate' => 'b#b','object' => 'b#c' }); +$writer->addPrefix('a', 'b#'); +$writer->addPrefix('a', 'b#'); +$writer->addPrefix('b', 'b#'); +$writer->addPrefix('a', 'c#'); +$writer->end(function (error, output) { + output.should.equal('@prefix a: .' . "\n" . "\n" . 'a:a a:b a:c.' . "\n" . + '@prefix b: .' . "\n" . "\n" . '@prefix a: .' . "\n" . "\n"); + done(error); +}); +}); + +//serializes triples of a graph with a prefix declaration in between', function (done) { +$writer = new TriGWriter(); +$writer->addPrefix('a', 'b#'); +$writer->addTriple({'subject' => 'b#a','predicate' => 'b#b','object' => 'b#c','graph' => 'b#g' }); +$writer->addPrefix('d', 'e#'); +$writer->addTriple({'subject' => 'b#a','predicate' => 'b#b','object' => 'b#d','graph' => 'b#g' }); +$writer->end(function (error, output) { + output.should.equal('@prefix a: .' . "\n" . "\n" . 'a:g {' . "\n" . 'a:a a:b a:c' . "\n" . '}' . "\n" . + '@prefix d: .' . "\n" . "\n" . 'a:g {' . "\n" . 'a:a a:b a:d' . "\n" . '}' . "\n"); + done(error); +}); +}); + + //should accept triples with separated components', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a', 'b', 'c'); +$writer->addTriple('a', 'b', 'd'); +$writer->end(function (error, output) { + output.should.equal(' , .' . "\n"); + done(error); +}); +}); + + //should accept quads with separated components', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a', 'b', 'c', 'g'); +$writer->addTriple('a', 'b', 'd', 'g'); +$writer->end(function (error, output) { + output.should.equal(' {' . "\n" . ' , ' . "\n" . '}' . "\n"); + done(error); +}); +}); + + //should serialize triples with an empty blank node as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a1', 'b', $writer->blank()); +$writer->addTriple('a2', 'b', $writer->blank([])); +$writer->end(function (error, output) { + output.should.equal(' [].' . "\n" . + ' [].' . "\n"); + done(error); +}); +}); + + //should serialize triples with a one-triple blank node as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a1', 'b', $writer->blank('d', 'e')); +$writer->addTriple('a2', 'b', $writer->blank({'predicate' => 'd','object' => 'e' })); +$writer->addTriple('a3', 'b', $writer->blank([{'predicate' => 'd','object' => 'e' }])); +$writer->end(function (error, output) { + output.should.equal(' [ ].' . "\n" . + ' [ ].' . "\n" . + ' [ ].' . "\n"); + done(error); +}); +}); + + //should serialize triples with a two-triple blank node as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a', 'b', $writer->blank([ + {'predicate' => 'd','object' => 'e' }, + {'predicate' => 'f','object' => '"g"' }, +])); +$writer->end(function (error, output) { + output.should.equal(' [' . "\n" . + ' ;' . "\n" . + ' "g"' . "\n" . + '].' . "\n"); + done(error); +}); +}); + + //should serialize triples with a three-triple blank node as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a', 'b', $writer->blank([ + {'predicate' => 'd','object' => 'e' }, + {'predicate' => 'f','object' => '"g"' }, + {'predicate' => 'h','object' => 'i' }, +])); +$writer->end(function (error, output) { + output.should.equal(' [' . "\n" . + ' ;' . "\n" . + ' "g";' . "\n" . + ' ' . "\n" . + '].' . "\n"); + done(error); +}); +}); + + //should serialize triples with predicate-sharing blank node triples as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a', 'b', $writer->blank([ + {'predicate' => 'd','object' => 'e' }, + {'predicate' => 'd','object' => 'f' }, + {'predicate' => 'g','object' => 'h' }, + {'predicate' => 'g','object' => 'i' }, +])); +$writer->end(function (error, output) { + output.should.equal(' [' . "\n" . + ' , ;' . "\n" . + ' , ' . "\n" . + '].' . "\n"); + done(error); +}); +}); + + //should serialize triples with nested blank nodes as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a1', 'b', $writer->blank([ + {'predicate' => 'd', object: $writer->blank() }, +])); +$writer->addTriple('a2', 'b', $writer->blank([ + {'predicate' => 'd', object: $writer->blank('e', 'f') }, + {'predicate' => 'g', object: $writer->blank('h', '"i"') }, +])); +$writer->addTriple('a3', 'b', $writer->blank([ + {'predicate' => 'd', object: $writer->blank([ + {'predicate' => 'g', object: $writer->blank('h', 'i') }, + {'predicate' => 'j', object: $writer->blank('k', '"l"') }, + ]) }, +])); +$writer->end(function (error, output) { + output.should.equal(' [' . "\n" . + ' []' . "\n" . + '].' . "\n" . + ' [' . "\n" . + ' [ ];' . "\n" . + ' [ "i" ]' . "\n" . + '].' . "\n" . + ' [' . "\n" . + ' [' . "\n" . + ' [ ];' . "\n" . + ' [ "l" ]' . "\n" . + ']' . "\n" . + '].' . "\n"); + done(error); +}); +}); + + //should serialize triples with an empty blank node as subject', function (done) { +$writer = TriGWriter(); +$writer->addTriple($writer->blank(), 'b', 'c'); +$writer->addTriple($writer->blank([]), 'b', 'c'); +$writer->end(function (error, output) { + output.should.equal('[] .' . "\n" . + '[] .' . "\n"); + done(error); +}); +}); + + //should serialize triples with a one-triple blank node as subject', function (done) { +$writer = TriGWriter(); +$writer->addTriple($writer->blank('a', 'b'), 'c', 'd'); +$writer->addTriple($writer->blank({'predicate' => 'a','object' => 'b' }), 'c', 'd'); +$writer->addTriple($writer->blank([{'predicate' => 'a','object' => 'b' }]), 'c', 'd'); +$writer->end(function (error, output) { + output.should.equal('[ ] .' . "\n" . + '[ ] .' . "\n" . + '[ ] .' . "\n"); + done(error); +}); +}); + + //should serialize triples with an empty blank node as graph', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a', 'b', 'c', $writer->blank()); +$writer->addTriple('a', 'b', 'c', $writer->blank([])); +$writer->end(function (error, output) { + output.should.equal('[] {' . "\n" . ' ' . "\n" . '}' . "\n" . + '[] {' . "\n" . ' ' . "\n" . '}' . "\n"); + done(error); +}); +}); + + //should serialize triples with an empty list as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a1', 'b', $writer->list()); +$writer->addTriple('a2', 'b', $writer->list([])); +$writer->end(function (error, output) { + output.should.equal(' ().' . "\n" . + ' ().' . "\n"); + done(error); +}); +}); + + //should serialize triples with a one-element list as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a1', 'b', $writer->list(['c'])); +$writer->addTriple('a2', 'b', $writer->list(['"c"'])); +$writer->end(function (error, output) { + output.should.equal(' ().' . "\n" . + ' ("c").' . "\n"); + done(error); +}); +}); + + //should serialize triples with a three-element list as object', function (done) { +$writer = TriGWriter(); +$writer->addTriple('a1', 'b', $writer->list(['c', 'd', 'e'])); +$writer->addTriple('a2', 'b', $writer->list(['"c"', '"d"', '"e"'])); +$writer->end(function (error, output) { + output.should.equal(' ( ).' . "\n" . + ' ("c" "d" "e").' . "\n"); + done(error); +}); +}); + + //should serialize triples with an empty list as subject', function (done) { +$writer = TriGWriter(); +$writer->addTriple($writer->list(), 'b1', 'c'); +$writer->addTriple($writer->list([]), 'b2', 'c'); +$writer->end(function (error, output) { + output.should.equal('() ;' . "\n" . + ' .' . "\n"); + done(error); +}); +}); + + //should serialize triples with a one-element list as subject', function (done) { +$writer = TriGWriter(); +$writer->addTriple($writer->list(['a']), 'b1', 'c'); +$writer->addTriple($writer->list(['a']), 'b2', 'c'); +$writer->end(function (error, output) { + output.should.equal('() ;' . "\n" . + ' .' . "\n"); + done(error); +}); +}); + + //should serialize triples with a three-element list as subject', function (done) { +$writer = TriGWriter(); +$writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); +$writer->end(function (error, output) { + output.should.equal('( "b" "c") .' . "\n"); + done(error); +}); +}); + + //should accept triples in bulk', function (done) { +$writer = TriGWriter(); +$writer->addTriples([{'subject' => 'a','predicate' => 'b','object' => 'c' }, +{'subject' => 'a','predicate' => 'b','object' => 'd' }]); +$writer->end(function (error, output) { + output.should.equal(' , .' . "\n"); + done(error); +}); +}); + + //should not allow writing after end', function (done) { +$writer = TriGWriter(); +$writer->addTriple({'subject' => 'a','predicate' => 'b','object' => 'c' }); +$writer->end(); +$writer->addTriple({'subject' => 'd','predicate' => 'e','object' => 'f' }, function (error) { + error.should.be.an.instanceof(Exception); + error.should.have.property('message', 'Cannot write because the writer has been closed.'); + done(); +}); +}); + + //should write simple triples in N-Triples mode', function (done) { +$writer = TriGWriter({'format' => 'N-Triples' }); +$writer->addTriple('a', 'b', 'c'); +$writer->addTriple('a', 'b', 'd'); +$writer->end(function (error, output) { + output.should.equal(' .' . "\n" . ' .' . "\n"); + done(error); +}); +}); + + //should not write an invalid literal in N-Triples mode', function (done) { +$writer = TriGWriter({'format' => 'N-Triples' }); +$writer->addTriple('a', 'b', '"c', function (error) { + error.should.be.an.instanceof(Exception); + error.should.have.property('message', 'Invalid literal: "c'); + done(); +}); +}); + + //should write simple quads in N-Quads mode', function (done) { +$writer = TriGWriter({'format' => 'N-Quads' }); +$writer->addTriple('a', 'b', 'c'); +$writer->addTriple('a', 'b', 'd', 'g'); +$writer->end(function (error, output) { + output.should.equal(' .' . "\n" . ' .' . "\n"); + done(error); +}); +}); + + //should not write an invalid literal in N-Quads mode', function (done) { +$writer = TriGWriter({'format' => 'N-Triples' }); +$writer->addTriple('a', 'b', '"c', function (error) { + error.should.be.an.instanceof(Exception); + error.should.have.property('message', 'Invalid literal: "c'); + done(); +}); +}); + + //should end when the end option is not set', function (done) { +$outputStream = new QuickStream(), writer = TriGWriter(outputStream, {}); +outputStream.should.have.property('ended', false); +$writer->end(function () { + outputStream.should.have.property('ended', true); + done(); +}); +}); + + //should end when the end option is set to true', function (done) { +$outputStream = new QuickStream(), writer = TriGWriter(outputStream, { end: true }); +outputStream.should.have.property('ended', false); +$writer->end(function () { + outputStream.should.have.property('ended', true); + done(); +}); +}); + + //should not end when the end option is set to false', function (done) { +$outputStream = new QuickStream(), writer = TriGWriter(outputStream, { end: false }); +outputStream.should.have.property('ended', false); +$writer->end(function () { + outputStream.should.have.property('ended', false); + done(); +}); +}); +}); +*/ + + + /** + **/ + private function shouldSerialize() { + $numargs = func_num_args(); + $expectedResult = func_get_arg($numargs-1); + $i = 0; + $prefixes = []; + if (func_get_arg($i) !== 0 && isset(func_get_arg($i)["prefixes"] )) { + $prefixes = func_get_arg($i)["prefixes"]; + $i++; + } + $writer = new TrigWriter(["prefixes"=>$prefixes]); + for ($i; $i < $numargs-1; $i++) { + $item = func_get_arg($i); + $g = isset($item[3])?$item[3]:null; + $writer->addTriple(["subject"=> $item[0], "predicate"=> $item[1], "object"=> $item[2], "graph" => $g ]); + } + $writer->end(function ($error, $output) use ($expectedResult) { + $this->assertEquals($expectedResult,$output); + }); + } + + private function shouldNotSerialize() { + + } + + +} + From f32da52f029834eea4e795442513a0b83d024e16 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 30 Mar 2017 17:11:43 +0200 Subject: [PATCH 03/72] Ported list --- src/TriGWriter.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 525e05d..2f744fd 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -30,7 +30,7 @@ class TriGWriter // Replaces a character by its escaped version private $characterReplacer; - public function __construct($options = []) + public function __construct($options = []) { /* Initialize writer, depending on the format*/ $this->subject = null; @@ -305,14 +305,19 @@ public function blank (predicate, object) { return contents + "\n]"; } } - - // ### `list` creates a list node with the given content - public function list (elements) { - $length = elements && elements.length || 0, contents = new Array(length); - for ($i = 0; i < length; i++) - contents[i] = $this->encodeObject(elements[i]); - return '(' + contents.join(' ') + ')'; - }*/ +*/ + // ### `list` creates a list node with the given content + public function list ($elements) { + $length = 0; + if (isset($elements)) { + $length = strlen($elements); + } + $contents = []; + for ($i = 0; $i < $length; $i++) { + $contents[$i] = $this->encodeObject($elements[$i]); + } + return '(' . join($contents, ' ') . ')'; + } // ### `end` signals the end of the output stream public function end($callback) From e77ebc843b1af3f74b10e4a8bf296ce562be0b72 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 30 Mar 2017 18:38:31 +0200 Subject: [PATCH 04/72] Implemented three double quotes to serialize data --- src/TriGWriter.php | 48 ++++++++++++++++++++++++++--------------- test/TriGWriterTest.php | 14 ++++++------ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 2f744fd..63bd94a 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -13,13 +13,13 @@ class TriGWriter CONST RDF_TYPE = self::RDF_PREFIX . 'type'; // Characters in literals that require escaping -# CONST ESCAPE = '/["\\\t\n\r\b\f\x{0000}-\x{0019}\x{d800}-\x{dbff}]/u'; - CONST ESCAPE = '//'; + CONST ESCAPE = "/[\"\\\t\n\r\b\f]/"; #\x{0000}-\x{0019}\x{d800}-\x{dbff}]/u'; +# CONST ESCAPE = '//'; # CONST ESCAPEALL = '//'; CONST ESCAPEALL = "/[\"\\\t\n\r\b\f]/u";#\x{0000}-\x{0019} # [\x{d800}-\x{dbff}][\x{dc00}-\x{dfff}] CONST ESCAPEREPLACEMENTS = [ - "\\" => "\\\\", '"'=> '\\"', "\t"=> "\\t", - "\n" => "\\n", "\r"=> "\\r", "\b"=> "\\b", "\f"=> "\\f" + '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", + "\n" => '\\n', "\r" => "\\r", "\b"=> "\\b", "\f"=> "\\f" ]; // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes @@ -45,7 +45,7 @@ public function __construct($options = []) $this->characterReplacer = function ($character) { // Replace a single character by its escaped version $character = $character[0]; - + var_dump($character); if (isset($character) && isset(self::ESCAPEREPLACEMENTS[$character])) { return self::ESCAPEREPLACEMENTS[$character]; } else { @@ -71,7 +71,7 @@ public function __construct($options = []) private function write ($string, $callback = null) { //this._outputStream.write(string, 'utf8', callback); if ($this->blocked) { - throw new Exception('Cannot write because the writer has been closed.'); + throw new \Exception('Cannot write because the writer has been closed.'); } else { if (isset($callback)) { $callback($string); @@ -108,7 +108,7 @@ private function writeTriple ($subject, $predicate, $object, $graph, $done = nul else { $this->write(($this->subject === null ? '' : ".\n") . $this->encodeSubject($this->subject = $subject) . ' ' . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); } - } catch (Exception $error) { + } catch (\Exception $error) { if (isset($done)) { $done($error); } @@ -122,7 +122,7 @@ private function writeTripleLine ($subject, $predicate, $object, $graph, $done = // Write the triple try { $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . ($graph ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); - } catch (Exception $error) { + } catch (\Exception $error) { if (isset($done)) { $done($error); } @@ -149,22 +149,36 @@ private function encodeIriOrBlankNode ($entity) { // ### `_encodeLiteral` represents a literal private function encodeLiteral ($value, $type = null, $language = null) { - // Escape special characters - if (preg_match(self::ESCAPE,$value)) - $value = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer, $value); + // Escape special characters - TODO: unicode characters? + if (preg_match('/[\t\n\r\f\b]/',$value)) { + $value = str_replace(array('\\', '"""'), array('\\\\', '\\"""'), $value); + + // Check if the last character is a trailing double quote, if so, escape it. + $pos = strrpos($value, '"'); + if ($pos !== false && $pos + 1 == strlen($value)) { + $value = substr($value, 0, -1); + $value .= '\"'; + } + // enclose between 3 double quotes + $value = '"""' . $value . '"""'; + } else { + // enclose in double quotes, while escaping back slashes + $value = '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $value) . '"'; + } + // Write the literal, possibly with type or language if (isset($language)) - return '"' . $value . '"@' . $language; + return $value . '@' . $language; else if (isset($type)) - return '"' . $value . '"^^' . $this->encodeIriOrBlankNode($type); + return $value . '^^' . $this->encodeIriOrBlankNode($type); else - return '"' . $value . '"'; + return $value; } // ### `_encodeSubject` represents a subject private function encodeSubject ($subject) { if ($subject[0] === '"') - throw new Exception('A literal as subject is not allowed: ' . $subject); + throw new \Exception('A literal as subject is not allowed: ' . $subject); // Don't treat identical blank nodes as repeating subjects if ($subject[0] === '[') $this->subject = ']'; @@ -175,7 +189,7 @@ private function encodeSubject ($subject) { // ### `_encodePredicate` represents a predicate private function encodePredicate ($predicate) { if ($predicate[0] === '"') - throw new Exception('A literal as predicate is not allowed: ' . $predicate); + throw new \Exception('A literal as predicate is not allowed: ' . $predicate); return $predicate === self::RDF_TYPE ? 'a' : $this->encodeIriOrBlankNode($predicate); } @@ -189,7 +203,7 @@ private function encodeObject ($object) { return $this->encodeLiteral($matches[1], isset($matches[2])?$matches[2]:null, isset($matches[3])?$matches[3]:null); } else { - throw new Exception('Invalid literal: ' . $object); + throw new \Exception('Invalid literal: ' . $object); } } diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 242fac6..4d08ff7 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -58,19 +58,21 @@ public function testWriter() //should serialize a literal containing a double quote', $this->shouldSerialize(['a', 'b', '"c"de"'], - ' "c\"de".' . "\n"); + ' "c\\"de".' . "\n"); - //should serialize a literal containing a backspace', + //should serialize a literal containing a backslash' $this->shouldSerialize(['a', 'b', '"c\\de"'], - ' "c\\de".' . "\n"); + ' "c\\\\de".' . "\n"); //should serialize a literal containing a tab character', - $this->shouldSerialize(['a', 'b', '"c\tde"'], - " \"c\tde\".\n"); + $this->shouldSerialize(['a', 'b', "\"c\tde\""], + " \"\"\"c\\tde\"\"\".\n"); //should serialize a literal containing a newline character', + /* shouldSerialize(['a', 'b', '"c\nde"'], + ' "c\\nde".\n'));*/ $this->shouldSerialize(['a', 'b', '"c' . "\n" . 'de"'], - ' "c\\' . "\n" . 'de".' . "\n"); + ' """c' . "\n" . 'de""".' . "\n"); //should serialize a literal containing a cariage return character', $this->shouldSerialize(['a', 'b', '"c\rde"'], From b930a1717b9d438f1a3101c36da60131cee4afbb Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 30 Mar 2017 19:44:38 +0200 Subject: [PATCH 05/72] Prefixes work --- src/TriGWriter.php | 38 +++++++++------------ test/TriGWriterTest.php | 76 ++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 63bd94a..ab2da6f 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -7,7 +7,7 @@ class TriGWriter { // Matches a literal as represented in memory by the N3 library - CONST LITERALMATCHER = '/^"(.*)"(?:\^\^(.+)|@([\-a-z]+))?$/i'; + CONST LITERALMATCHER = '/^"(.*)"(?:\^\^(.+)|@([\-a-z]+))?$/is'; // rdf:type predicate (for 'a' abbreviation) CONST RDF_PREFIX = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; CONST RDF_TYPE = self::RDF_PREFIX . 'type'; @@ -102,7 +102,6 @@ private function writeTriple ($subject, $predicate, $object, $graph, $done = nul else { $this->write(";\n " . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); } - } // Different subject; write the whole triple else { @@ -150,7 +149,8 @@ private function encodeIriOrBlankNode ($entity) { // ### `_encodeLiteral` represents a literal private function encodeLiteral ($value, $type = null, $language = null) { // Escape special characters - TODO: unicode characters? - if (preg_match('/[\t\n\r\f\b]/',$value)) { + if (preg_match('/[\t\n\r\f]/',$value)) { + $value = str_replace(array('\\', '"""'), array('\\\\', '\\"""'), $value); // Check if the last character is a trailing double quote, if so, escape it. @@ -240,34 +240,30 @@ public function addPrefix($prefix, $iri, $done) // ### `addPrefixes` adds the prefixes to the output stream public function addPrefixes ($prefixes, $done = null) { // Add all useful prefixes - $prefixIRIs = $this->prefixIRIs; $hasPrefixes = false; - foreach ($prefixes as $i => $prefix) { + foreach ($prefixes as $prefix => $iri) { + // Verify whether the prefix can be used and does not exist yet - if (isset($prefixes[$prefix])) { - $iri = $prefixes[$prefix]; - if (preg_match('/[#\/]$/',$iri) && $prefixIRIs[$iri] !== ($prefix .= ':')) { - $hasPrefixes = true; - $prefixIRIs[$iri] = $prefix; - // Finish a possible pending triple - if ($this->subject !== null) { - $this->write($this->graph ? "\n}\n" : ".\n"); - $this->subject = null; - $this->graph = ''; - } - // Write prefix - $this->write('@prefix ' . $prefix . ' <' . $iri . ">.\n"); + if (preg_match('/[#\/]$/',$iri) && (!isset($this->prefixIRIs[$iri]) || $this->prefixIRIs[$iri] !== ($prefix . ':'))) { + $hasPrefixes = true; + $this->prefixIRIs[$iri] = $prefix; + // Finish a possible pending triple + if ($this->subject !== null) { + $this->write($this->graph ? "\n}\n" : ".\n"); + $this->subject = null; + $this->graph = ''; } + // Write prefix + $this->write('@prefix ' . $prefix . ': <' . $iri . ">.\n"); } - } // Recreate the prefix matcher if (isset($hasPrefixes)) { $IRIlist = ''; $prefixList = ''; - foreach ($prefixIRIs as $i => $prefixIRI) { + foreach ($this->prefixIRIs as $prefixIRI => $iri) { $IRIlist .= $IRIlist ? '|' . $prefixIRI : $prefixIRI; - $prefixList .= ($prefixList ? '|' : '') . $prefixIRIs[$prefixIRI]; + $prefixList .= ($prefixList ? '|' : '') . $iri; } $IRIlist = preg_replace("/[\]\/\(\)\*\+\?\.\\\$]/", '\\$&', $IRIlist); $this->prefixRegex = '/^(?:' . $prefixList . ')[^\/]*$|' . '^(' . $IRIlist . ')([a-zA-Z][\\-_a-zA-Z0-9]*)$/'; diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 4d08ff7..1666032 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -8,25 +8,16 @@ */ class TriGWriterTest extends PHPUnit_Framework_TestCase { - - public function testTriGWriter () - { - //should serialize 0 triples', - $this->shouldSerialize(''); - } - public function testOneTriple () + public function testZeroOrMoreTriples () { - + //should serialize 0 triples', + $this->shouldSerialize(''); //should serialize 1 triple', $this->shouldSerialize(['abc', 'def', 'ghi'], ' .' . "\n"); - } - public function testWriter() - { - - //should serialize 2 triples', + //should serialize 2 triples', $this->shouldSerialize(['abc', 'def', 'ghi'], ['jkl', 'mno', 'pqr'], ' .' . "\n" . @@ -39,7 +30,11 @@ public function testWriter() ' .' . "\n" . ' .' . "\n" . ' .' . "\n"); + } + public function testLiterals() + { + //should serialize a literal', $this->shouldSerialize(['a', 'b', '"cde"'], ' "cde".' . "\n"); @@ -66,7 +61,7 @@ public function testWriter() //should serialize a literal containing a tab character', $this->shouldSerialize(['a', 'b', "\"c\tde\""], - " \"\"\"c\\tde\"\"\".\n"); + " \"\"\"c\tde\"\"\".\n"); //should serialize a literal containing a newline character', /* shouldSerialize(['a', 'b', '"c\nde"'], @@ -75,32 +70,40 @@ public function testWriter() ' """c' . "\n" . 'de""".' . "\n"); //should serialize a literal containing a cariage return character', - $this->shouldSerialize(['a', 'b', '"c\rde"'], - ' "c\\rde".' . "\n"); + $this->shouldSerialize(['a', 'b', '"c' . "\r" . 'de"'], + ' """' . "c\rde" . '""".' . "\n"); //should serialize a literal containing a backspace character', - $this->shouldSerialize(['a', 'b', '"c\bde"'], - ' "c\\bde".' . "\n"); + /*$this->shouldSerialize(['a', 'b', '"c' . "\b" . 'de"'], + ' """' . "c\bde". '""".' . "\n");*/ //→ TODO: Doesn’t work properly //should serialize a literal containing a form feed character', - $this->shouldSerialize(['a', 'b', '"c\fde"'], - ' "c\\fde".' . "\n"); + $this->shouldSerialize(['a', 'b', '"c' . "\f" . 'de"'], + ' """c' . "\f" . 'de""".' . "\n"); //should serialize a literal containing a line separator', - $this->shouldSerialize(['a', 'b', '"c\u2028de"'], - ' "c\u2028de".' . "\n"); + $this->shouldSerialize(['a', 'b', "\"c\u{2028}de\""], + ' "c' . "\u{2028}" . 'de".' . "\n"); //should serialize a literal containing a paragraph separator', - $this->shouldSerialize(['a', 'b', '"c\u2029de"'], - ' "c\u2029de".' . "\n"); + $this->shouldSerialize(['a', 'b', "\"c\u{2029}de\""], + ' "c' . "\u{2029}" .'de".' . "\n"); //should serialize a literal containing special unicode characters', - $this->shouldSerialize(['a', 'b', '"c\u0000\u0001"'], - ' "c\\u0000\\u0001".' . "\n"); + $this->shouldSerialize(['a', 'b', "\"c\u{0000}\u{0001}\""], + ' "c'."\u{0000}\u{0001}" . '".' . "\n"); + } + public function testBlankNodes() + { //should serialize blank nodes', $this->shouldSerialize(['_:a', 'b', '_:c'], '_:a _:c.' . "\n"); + } + + public function testWrongLiterals() + { + /* //should not serialize a literal in the subject', shouldNotSerialize(['"a"', 'b', '"c"'], @@ -114,8 +117,13 @@ public function testWriter() shouldNotSerialize(['a', 'b', '"c'], 'Invalid literal: "c'); */ + } + + public function testPrefixes () + { + //should not leave leading whitespace if the prefix set is empty', - $this->shouldSerialize([], + $this->shouldSerialize(["prefixes" => []], ['a', 'b', 'c'], ' .' . "\n"); @@ -141,7 +149,10 @@ public function testWriter() '@prefix a: .' . "\n" . '@prefix b: .' . "\n" . "\n" . 'a:bc b:ef .' . "\n"); + } + public function testRepitition () + { //should not repeat the same subjects', $this->shouldSerialize(['abc', 'def', 'ghi'], ['abc', 'mno', 'pqr'], @@ -159,10 +170,19 @@ public function testWriter() ' , ;' . "\n" . ' , .' . "\n" . ' .' . "\n"); + } + public function testRdfType () + { + //should write rdf:type as "a"', $this->shouldSerialize(['abc', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'def'], ' a .' . "\n"); + } + + public function testQuads () + { + //should serialize a graph with 1 triple', $this->shouldSerialize(['abc', 'def', 'ghi', 'xyz'], @@ -571,7 +591,7 @@ private function shouldSerialize() { if (func_get_arg($i) !== 0 && isset(func_get_arg($i)["prefixes"] )) { $prefixes = func_get_arg($i)["prefixes"]; $i++; - } + }; $writer = new TrigWriter(["prefixes"=>$prefixes]); for ($i; $i < $numargs-1; $i++) { $item = func_get_arg($i); From 0d1720554faa6b5c3376cab28f72068cb1ebec67 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 30 Mar 2017 21:02:40 +0200 Subject: [PATCH 06/72] prefixes pass --- src/TriGWriter.php | 28 +++++++++++++++++----------- test/TriGWriterTest.php | 5 ++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index ab2da6f..93b208e 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -13,10 +13,8 @@ class TriGWriter CONST RDF_TYPE = self::RDF_PREFIX . 'type'; // Characters in literals that require escaping - CONST ESCAPE = "/[\"\\\t\n\r\b\f]/"; #\x{0000}-\x{0019}\x{d800}-\x{dbff}]/u'; -# CONST ESCAPE = '//'; -# CONST ESCAPEALL = '//'; - CONST ESCAPEALL = "/[\"\\\t\n\r\b\f]/u";#\x{0000}-\x{0019} # [\x{d800}-\x{dbff}][\x{dc00}-\x{dfff}] + CONST ESCAPE = "/[\"\\\t\n\r\f]/u"; #/u'; + CONST ESCAPEALL = "/[\"\\\t\n\r\b\f]/u"; CONST ESCAPEREPLACEMENTS = [ '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", "\n" => '\\n', "\r" => "\\r", "\b"=> "\\b", "\f"=> "\\f" @@ -133,14 +131,20 @@ private function writeTripleLine ($subject, $predicate, $object, $graph, $done = private function encodeIriOrBlankNode ($entity) { // A blank node or list is represented as-is $firstChar = substr($entity, 0, 1); - if ($firstChar === '[' || $firstChar === '(' || $firstChar === '_' && substr($entity, 1, 1) === ':') + if ($firstChar === '[' || $firstChar === '(' || $firstChar === '_' && substr($entity, 1, 1) === ':') { return $entity; + } // Escape special characters if (preg_match(self::ESCAPE, $entity)) $entity = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer,$entity); // Try to represent the IRI as prefixed name - if (preg_match($this->prefixRegex, $entity, $prefixMatch)) { - return '<' . $entity . '>'; + preg_match($this->prefixRegex, $entity, $prefixMatch); + if (!isset($prefixMatch[1]) && !isset($prefixMatch[2])) { + if (preg_match("/(.*?:)/",$entity,$match) && in_array($match[1], $this->prefixIRIs)) { + return $entity; + } else { + return '<' . $entity . '>'; + } } else { return !isset($prefixMatch[1]) ? $entity : $this->prefixIRIs[$prefixMatch[1]] . $prefixMatch[2]; } @@ -246,7 +250,7 @@ public function addPrefixes ($prefixes, $done = null) { // Verify whether the prefix can be used and does not exist yet if (preg_match('/[#\/]$/',$iri) && (!isset($this->prefixIRIs[$iri]) || $this->prefixIRIs[$iri] !== ($prefix . ':'))) { $hasPrefixes = true; - $this->prefixIRIs[$iri] = $prefix; + $this->prefixIRIs[$iri] = $prefix . ":"; // Finish a possible pending triple if ($this->subject !== null) { $this->write($this->graph ? "\n}\n" : ".\n"); @@ -264,9 +268,10 @@ public function addPrefixes ($prefixes, $done = null) { foreach ($this->prefixIRIs as $prefixIRI => $iri) { $IRIlist .= $IRIlist ? '|' . $prefixIRI : $prefixIRI; $prefixList .= ($prefixList ? '|' : '') . $iri; - } - $IRIlist = preg_replace("/[\]\/\(\)\*\+\?\.\\\$]/", '\\$&', $IRIlist); - $this->prefixRegex = '/^(?:' . $prefixList . ')[^\/]*$|' . '^(' . $IRIlist . ')([a-zA-Z][\\-_a-zA-Z0-9]*)$/'; + } + $IRIlist = preg_replace("/([\]\/\(\)\*\+\?\.\\\$])/", '${1}', $IRIlist); + $this->prefixRegex = '%^(?:' . $prefixList . ')[^/]*$|' . '^(' . $IRIlist . ')([a-zA-Z][\\-_a-zA-Z0-9]*)$%'; + } // End a prefix block with a newline $this->write($hasPrefixes ? "\n" : '', $done); @@ -340,5 +345,6 @@ public function end($callback) // Disallow further writing $this->blocked = true; $callback(null,$this->string); + return $this->string; } } \ No newline at end of file diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 1666032..6f2a6c5 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -103,7 +103,6 @@ public function testBlankNodes() public function testWrongLiterals() { - /* //should not serialize a literal in the subject', shouldNotSerialize(['"a"', 'b', '"c"'], @@ -150,7 +149,7 @@ public function testPrefixes () '@prefix b: .' . "\n" . "\n" . 'a:bc b:ef .' . "\n"); } - +/* public function testRepitition () { //should not repeat the same subjects', @@ -215,7 +214,7 @@ public function testQuads () //should not use escape sequences in blank nodes', $this->shouldSerialize(['_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00'], '_:\ud835\udc00 {' . "\n" . '_:\ud835\udc00 _:\ud835\udc00 _:\ud835\udc00' . "\n" . '}' . "\n"); - } + }*/ /* public function testCallbackOnEnd () { //sends output through end From 7d74fe313261dfe391a223effb5b84b4789a86c2 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 30 Mar 2017 21:32:47 +0200 Subject: [PATCH 07/72] Quads tested --- src/TriGWriter.php | 16 ++++++++++------ test/TriGWriterTest.php | 13 +++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 93b208e..0b61724 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -83,10 +83,13 @@ private function write ($string, $callback = null) { // ### `_writeTriple` writes the triple to the output stream private function writeTriple ($subject, $predicate, $object, $graph, $done = null) { try { + if (isset($graph) && $graph === ""){ + $graph = null; + } // Write the graph's label if it has changed if ($this->graph !== $graph) { // Close the previous graph and start the new one - $this->write(($this->subject === null ? '' : ($this->graph ? "\n}\n" : ".\n")) . ($graph ? $this->encodeIriOrBlankNode($graph) . " {\n" : '')); + $this->write(($this->subject === null ? '' : ($this->graph ? "\n}\n" : ".\n")) . (isset($graph) ? $this->encodeIriOrBlankNode($graph) . " {\n" : '')); $this->subject = null; // Don't treat identical blank nodes as repeating graphs $this->graph = $graph[0] !== '[' ? $graph : ']'; @@ -95,10 +98,11 @@ private function writeTriple ($subject, $predicate, $object, $graph, $done = nul if ($this->subject === $subject) { // Don't repeat the predicate if it's the same if ($this->predicate === $predicate) - $this->write(', ' + $this->encodeObject($object), $done); + $this->write(', ' . $this->encodeObject($object), $done); // Same subject, different predicate - else { - $this->write(";\n " . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); + else { + $this->predicate = $predicate; + $this->write(";\n " . $this->encodePredicate($predicate) . ' ' . $this->encodeObject($object), $done); } } // Different subject; write the whole triple @@ -118,7 +122,7 @@ private function writeTripleLine ($subject, $predicate, $object, $graph, $done = delete($this->prefixMatch); // Write the triple try { - $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . ($graph ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); + $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . (isset($graph) ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); } catch (\Exception $error) { if (isset($done)) { $done($error); @@ -146,7 +150,7 @@ private function encodeIriOrBlankNode ($entity) { return '<' . $entity . '>'; } } else { - return !isset($prefixMatch[1]) ? $entity : $this->prefixIRIs[$prefixMatch[1]] . $prefixMatch[2]; + return !isset($prefixMatch[1]) ? $entity : $this->prefixIRIs[$prefixMatch[1]] . $prefixMatch[2]; } } diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 6f2a6c5..6ac1e00 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -149,7 +149,7 @@ public function testPrefixes () '@prefix b: .' . "\n" . "\n" . 'a:bc b:ef .' . "\n"); } -/* + public function testRepitition () { //should not repeat the same subjects', @@ -208,13 +208,14 @@ public function testQuads () ' {' . "\n" . ' ' . "\n" . '}' . "\n"); //should output 8-bit unicode characters as escape sequences', - $this->shouldSerialize(['\ud835\udc00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'], +/* SHOULD IT? + $this->shouldSerialize(["\u{d835}\u{dc00}", "\u{d835}\u{dc00}", "\"\u{d835}\u{dc00}\"^^\u{d835}\u{dc00}", "\u{d835}\u{dc00}"], '<\\U0001d400> {' . "\n" . '<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>' . "\n" . '}' . "\n"); - +*/ //should not use escape sequences in blank nodes', - $this->shouldSerialize(['_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00', '_:\ud835\udc00'], - '_:\ud835\udc00 {' . "\n" . '_:\ud835\udc00 _:\ud835\udc00 _:\ud835\udc00' . "\n" . '}' . "\n"); - }*/ + $this->shouldSerialize(["_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}"], + "_:\u{d835}\u{dc00} {" . "\n" . "_:\u{d835}\u{dc00} _:\u{d835}\u{dc00} _:\u{d835}\u{dc00}" . "\n" . '}' . "\n"); + } /* public function testCallbackOnEnd () { //sends output through end From 085cb50a845840ea5b3f1337bc2547e0cb41081e Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 31 Mar 2017 17:19:26 +0200 Subject: [PATCH 08/72] Cleaning up more prefix test cases --- src/TriGWriter.php | 2 +- test/TriGWriterTest.php | 334 ++++++++++++++++------------------------ 2 files changed, 134 insertions(+), 202 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 0b61724..47bf77f 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -238,7 +238,7 @@ public function addTriples ($triples) { } // ### `addPrefix` adds the prefix to the output stream - public function addPrefix($prefix, $iri, $done) + public function addPrefix($prefix, $iri, $done = null) { $prefixes = []; $prefixes[$prefix] = $iri; diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 6ac1e00..6709d2a 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -216,13 +216,13 @@ public function testQuads () $this->shouldSerialize(["_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}"], "_:\u{d835}\u{dc00} {" . "\n" . "_:\u{d835}\u{dc00} _:\u{d835}\u{dc00} _:\u{d835}\u{dc00}" . "\n" . '}' . "\n"); } - /* + public function testCallbackOnEnd () { //sends output through end $writer = new TriGWriter(); $writer->addTriple(['subject' => 'a','predicate' => 'b','object' => 'c' ]); $writer->end(function ($error, $output) { - $this->assertEquals(" .' . "\n" . '",$output); + $this->assertEquals(" .\n",$output); }); } @@ -232,351 +232,283 @@ public function testRespectingPrefixes () $writer = new TriGWriter([ "prefixes" => ['a' => 'b#' ]]); $writer->addTriple(['subject' => 'b#a','predicate' => 'b#b','object' => 'b#c' ]); $writer->end(function ($error, $output) { - $this->assertEquals("@prefix a: .' . "\n" . "\n" . 'a:a a:b a:c.' . "\n" . '",$output); + $this->assertEquals("@prefix a: .\n\na:a a:b a:c.\n",$output); }); - }*/ - -/* + } -//does not repeat identical prefixes', function (done) { -$writer = new TriGWriter(); -$writer->addPrefix('a', 'b#'); -$writer->addPrefix('a', 'b#'); -$writer->addTriple({'subject' => 'b#a','predicate' => 'b#b','object' => 'b#c' }); -$writer->addPrefix('a', 'b#'); -$writer->addPrefix('a', 'b#'); -$writer->addPrefix('b', 'b#'); -$writer->addPrefix('a', 'c#'); -$writer->end(function (error, output) { - output.should.equal('@prefix a: .' . "\n" . "\n" . 'a:a a:b a:c.' . "\n" . - '@prefix b: .' . "\n" . "\n" . '@prefix a: .' . "\n" . "\n"); - done(error); -}); -}); + public function testOtherPrefixes () + { + + //does not repeat identical prefixes', function (done) { + $writer = new TriGWriter(); + $writer->addPrefix('a', 'b#'); + $writer->addPrefix('a', 'b#'); + $writer->addTriple(['subject' => 'b#a','predicate' => 'b#b','object' => 'b#c' ]); + $writer->addPrefix('a', 'b#'); + $writer->addPrefix('a', 'b#'); + $writer->addPrefix('b', 'b#'); + $writer->addPrefix('a', 'c#'); + $writer->end(function ($error, $output) { + $this->assertEquals('@prefix a: .' . "\n" . "\n" . 'a:a a:b a:c.' . "\n" . '@prefix b: .' . "\n" . "\n" . '@prefix a: .' . "\n" . "\n",$output); + }); -//serializes triples of a graph with a prefix declaration in between', function (done) { -$writer = new TriGWriter(); -$writer->addPrefix('a', 'b#'); -$writer->addTriple({'subject' => 'b#a','predicate' => 'b#b','object' => 'b#c','graph' => 'b#g' }); -$writer->addPrefix('d', 'e#'); -$writer->addTriple({'subject' => 'b#a','predicate' => 'b#b','object' => 'b#d','graph' => 'b#g' }); -$writer->end(function (error, output) { - output.should.equal('@prefix a: .' . "\n" . "\n" . 'a:g {' . "\n" . 'a:a a:b a:c' . "\n" . '}' . "\n" . - '@prefix d: .' . "\n" . "\n" . 'a:g {' . "\n" . 'a:a a:b a:d' . "\n" . '}' . "\n"); - done(error); -}); -}); + //serializes triples of a graph with a prefix declaration in between', function (done) { + $writer = new TriGWriter(); + $writer->addPrefix('a', 'b#'); + $writer->addTriple(['subject' => 'b#a','predicate' => 'b#b','object' => 'b#c','graph' => 'b#g' ]); + $writer->addPrefix('d', 'e#'); + $writer->addTriple(['subject' => 'b#a','predicate' => 'b#b','object' => 'b#d','graph' => 'b#g' ]); + $writer->end(function ($error, $output) { + $this->assertEquals('@prefix a: .' . "\n" . "\n" . 'a:g {' . "\n" . 'a:a a:b a:c' . "\n" . '}' . "\n" . '@prefix d: .' . "\n" . "\n" . 'a:g {' . "\n" . 'a:a a:b a:d' . "\n" . '}' . "\n",$output); + }); //should accept triples with separated components', function (done) { -$writer = TriGWriter(); -$writer->addTriple('a', 'b', 'c'); -$writer->addTriple('a', 'b', 'd'); -$writer->end(function (error, output) { - output.should.equal(' , .' . "\n"); - done(error); -}); -}); + $writer = new TriGWriter(); + $writer->addTriple('a', 'b', 'c'); + $writer->addTriple('a', 'b', 'd'); + $writer->end(function ($error, $output) { + $this->assertEquals(' , .' . "\n",$output); + }); //should accept quads with separated components', function (done) { -$writer = TriGWriter(); -$writer->addTriple('a', 'b', 'c', 'g'); -$writer->addTriple('a', 'b', 'd', 'g'); -$writer->end(function (error, output) { - output.should.equal(' {' . "\n" . ' , ' . "\n" . '}' . "\n"); - done(error); -}); -}); + $writer = new TriGWriter(); + $writer->addTriple('a', 'b', 'c', 'g'); + $writer->addTriple('a', 'b', 'd', 'g'); + $writer->end(function ($error, $output) { + $this->assertEquals(' {' . "\n" . ' , ' . "\n" . '}' . "\n",$output); + }); + } + + public function testBlankNodes2 () + { + } + +/* //should serialize triples with an empty blank node as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->blank()); $writer->addTriple('a2', 'b', $writer->blank([])); $writer->end(function (error, output) { - output.should.equal(' [].' . "\n" . - ' [].' . "\n"); - done(error); -}); + $this->assertEquals(' [].' . "\n" . ' [].' . "\n",$output); + }); //should serialize triples with a one-triple blank node as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->blank('d', 'e')); -$writer->addTriple('a2', 'b', $writer->blank({'predicate' => 'd','object' => 'e' })); -$writer->addTriple('a3', 'b', $writer->blank([{'predicate' => 'd','object' => 'e' }])); +$writer->addTriple('a2', 'b', $writer->blank(['predicate' => 'd','object' => 'e' ])); +$writer->addTriple('a3', 'b', $writer->blank([['predicate' => 'd','object' => 'e' ]])); $writer->end(function (error, output) { - output.should.equal(' [ ].' . "\n" . - ' [ ].' . "\n" . - ' [ ].' . "\n"); - done(error); -}); + $this->assertEquals(' [ ].' . "\n" . ' [ ].' . "\n" . ' [ ].' . "\n",$output); + }); //should serialize triples with a two-triple blank node as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a', 'b', $writer->blank([ - {'predicate' => 'd','object' => 'e' }, - {'predicate' => 'f','object' => '"g"' }, + ['predicate' => 'd','object' => 'e' ], + ['predicate' => 'f','object' => '"g"' ], ])); $writer->end(function (error, output) { - output.should.equal(' [' . "\n" . - ' ;' . "\n" . - ' "g"' . "\n" . - '].' . "\n"); - done(error); -}); + $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g"' . "\n" . '].' . "\n",$output); + }); //should serialize triples with a three-triple blank node as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a', 'b', $writer->blank([ - {'predicate' => 'd','object' => 'e' }, - {'predicate' => 'f','object' => '"g"' }, - {'predicate' => 'h','object' => 'i' }, + ['predicate' => 'd','object' => 'e' ], + ['predicate' => 'f','object' => '"g"' ], + ['predicate' => 'h','object' => 'i' ], ])); $writer->end(function (error, output) { - output.should.equal(' [' . "\n" . - ' ;' . "\n" . - ' "g";' . "\n" . - ' ' . "\n" . - '].' . "\n"); - done(error); -}); + $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g";' . "\n" . ' ' . "\n" . '].' . "\n",$output); + }); //should serialize triples with predicate-sharing blank node triples as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a', 'b', $writer->blank([ - {'predicate' => 'd','object' => 'e' }, - {'predicate' => 'd','object' => 'f' }, - {'predicate' => 'g','object' => 'h' }, - {'predicate' => 'g','object' => 'i' }, + ['predicate' => 'd','object' => 'e' ], + ['predicate' => 'd','object' => 'f' ], + ['predicate' => 'g','object' => 'h' ], + ['predicate' => 'g','object' => 'i' ], ])); $writer->end(function (error, output) { - output.should.equal(' [' . "\n" . - ' , ;' . "\n" . - ' , ' . "\n" . - '].' . "\n"); - done(error); -}); + $this->assertEquals(' [' . "\n" . ' , ;' . "\n" . ' , ' . "\n" . '].' . "\n",$output); + }); //should serialize triples with nested blank nodes as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->blank([ - {'predicate' => 'd', object: $writer->blank() }, + ['predicate' => 'd', object: $writer->blank() ], ])); $writer->addTriple('a2', 'b', $writer->blank([ - {'predicate' => 'd', object: $writer->blank('e', 'f') }, - {'predicate' => 'g', object: $writer->blank('h', '"i"') }, + ['predicate' => 'd', object: $writer->blank('e', 'f') ], + ['predicate' => 'g', object: $writer->blank('h', '"i"') ], ])); $writer->addTriple('a3', 'b', $writer->blank([ - {'predicate' => 'd', object: $writer->blank([ - {'predicate' => 'g', object: $writer->blank('h', 'i') }, - {'predicate' => 'j', object: $writer->blank('k', '"l"') }, - ]) }, + ['predicate' => 'd', object: $writer->blank([ + ['predicate' => 'g', object: $writer->blank('h', 'i') ], + ['predicate' => 'j', object: $writer->blank('k', '"l"') ], + ]) ], ])); $writer->end(function (error, output) { - output.should.equal(' [' . "\n" . - ' []' . "\n" . - '].' . "\n" . - ' [' . "\n" . - ' [ ];' . "\n" . - ' [ "i" ]' . "\n" . - '].' . "\n" . - ' [' . "\n" . - ' [' . "\n" . - ' [ ];' . "\n" . - ' [ "l" ]' . "\n" . - ']' . "\n" . - '].' . "\n"); - done(error); -}); + $this->assertEquals(' [' . "\n" . ' []' . "\n" . '].' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "i" ]' . "\n" . '].' . "\n" . ' [' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "l" ]' . "\n" . ']' . "\n" . '].' . "\n",$output); + }); //should serialize triples with an empty blank node as subject', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple($writer->blank(), 'b', 'c'); $writer->addTriple($writer->blank([]), 'b', 'c'); $writer->end(function (error, output) { - output.should.equal('[] .' . "\n" . - '[] .' . "\n"); - done(error); -}); + $this->assertEquals('[] .' . "\n" . '[] .' . "\n",$output); + }); //should serialize triples with a one-triple blank node as subject', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple($writer->blank('a', 'b'), 'c', 'd'); -$writer->addTriple($writer->blank({'predicate' => 'a','object' => 'b' }), 'c', 'd'); -$writer->addTriple($writer->blank([{'predicate' => 'a','object' => 'b' }]), 'c', 'd'); +$writer->addTriple($writer->blank(['predicate' => 'a','object' => 'b' ]), 'c', 'd'); +$writer->addTriple($writer->blank([['predicate' => 'a','object' => 'b' ]]), 'c', 'd'); $writer->end(function (error, output) { - output.should.equal('[ ] .' . "\n" . - '[ ] .' . "\n" . - '[ ] .' . "\n"); - done(error); -}); + $this->assertEquals('[ ] .' . "\n" . '[ ] .' . "\n" . '[ ] .' . "\n",$output); + }); //should serialize triples with an empty blank node as graph', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a', 'b', 'c', $writer->blank()); $writer->addTriple('a', 'b', 'c', $writer->blank([])); $writer->end(function (error, output) { - output.should.equal('[] {' . "\n" . ' ' . "\n" . '}' . "\n" . - '[] {' . "\n" . ' ' . "\n" . '}' . "\n"); - done(error); -}); + $this->assertEquals('[] {' . "\n" . ' ' . "\n" . '}' . "\n" . '[] {' . "\n" . ' ' . "\n" . '}' . "\n",$output); + }); //should serialize triples with an empty list as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->list()); $writer->addTriple('a2', 'b', $writer->list([])); $writer->end(function (error, output) { - output.should.equal(' ().' . "\n" . - ' ().' . "\n"); - done(error); -}); + $this->assertEquals(' ().' . "\n" . ' ().' . "\n",$output); + }); //should serialize triples with a one-element list as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->list(['c'])); $writer->addTriple('a2', 'b', $writer->list(['"c"'])); $writer->end(function (error, output) { - output.should.equal(' ().' . "\n" . - ' ("c").' . "\n"); - done(error); -}); + $this->assertEquals(' ().' . "\n" . ' ("c").' . "\n",$output); + }); //should serialize triples with a three-element list as object', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->list(['c', 'd', 'e'])); $writer->addTriple('a2', 'b', $writer->list(['"c"', '"d"', '"e"'])); $writer->end(function (error, output) { - output.should.equal(' ( ).' . "\n" . - ' ("c" "d" "e").' . "\n"); - done(error); -}); + $this->assertEquals(' ( ).' . "\n" . ' ("c" "d" "e").' . "\n",$output); + }); //should serialize triples with an empty list as subject', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple($writer->list(), 'b1', 'c'); $writer->addTriple($writer->list([]), 'b2', 'c'); $writer->end(function (error, output) { - output.should.equal('() ;' . "\n" . - ' .' . "\n"); - done(error); -}); + $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); + }); //should serialize triples with a one-element list as subject', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple($writer->list(['a']), 'b1', 'c'); $writer->addTriple($writer->list(['a']), 'b2', 'c'); $writer->end(function (error, output) { - output.should.equal('() ;' . "\n" . - ' .' . "\n"); - done(error); -}); + $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); + }); //should serialize triples with a three-element list as subject', function (done) { -$writer = TriGWriter(); +$writer = new TriGWriter(); $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); $writer->end(function (error, output) { - output.should.equal('( "b" "c") .' . "\n"); - done(error); -}); + $this->assertEquals('( "b" "c") .' . "\n",$output); + }); //should accept triples in bulk', function (done) { -$writer = TriGWriter(); -$writer->addTriples([{'subject' => 'a','predicate' => 'b','object' => 'c' }, -{'subject' => 'a','predicate' => 'b','object' => 'd' }]); +$writer = new TriGWriter(); +$writer->addTriples([['subject' => 'a','predicate' => 'b','object' => 'c' ], +['subject' => 'a','predicate' => 'b','object' => 'd' }]); $writer->end(function (error, output) { - output.should.equal(' , .' . "\n"); - done(error); -}); + $this->assertEquals(' , .' . "\n",$output); + }); //should not allow writing after end', function (done) { -$writer = TriGWriter(); -$writer->addTriple({'subject' => 'a','predicate' => 'b','object' => 'c' }); +$writer = new TriGWriter(); +$writer->addTriple(['subject' => 'a','predicate' => 'b','object' => 'c' ]); $writer->end(); -$writer->addTriple({'subject' => 'd','predicate' => 'e','object' => 'f' }, function (error) { +$writer->addTriple(['subject' => 'd','predicate' => 'e','object' => 'f' ], function (error) { error.should.be.an.instanceof(Exception); error.should.have.property('message', 'Cannot write because the writer has been closed.'); - done(); -}); }); //should write simple triples in N-Triples mode', function (done) { -$writer = TriGWriter({'format' => 'N-Triples' }); +$writer = new TriGWriter({'format' => 'N-Triples' }); $writer->addTriple('a', 'b', 'c'); $writer->addTriple('a', 'b', 'd'); $writer->end(function (error, output) { - output.should.equal(' .' . "\n" . ' .' . "\n"); - done(error); -}); + $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); }); //should not write an invalid literal in N-Triples mode', function (done) { -$writer = TriGWriter({'format' => 'N-Triples' }); +$writer = new TriGWriter({'format' => 'N-Triples' }); $writer->addTriple('a', 'b', '"c', function (error) { error.should.be.an.instanceof(Exception); error.should.have.property('message', 'Invalid literal: "c'); - done(); -}); }); //should write simple quads in N-Quads mode', function (done) { -$writer = TriGWriter({'format' => 'N-Quads' }); +$writer = new TriGWriter({'format' => 'N-Quads' }); $writer->addTriple('a', 'b', 'c'); $writer->addTriple('a', 'b', 'd', 'g'); $writer->end(function (error, output) { - output.should.equal(' .' . "\n" . ' .' . "\n"); - done(error); -}); + $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); + }); //should not write an invalid literal in N-Quads mode', function (done) { -$writer = TriGWriter({'format' => 'N-Triples' }); +$writer = new TriGWriter({'format' => 'N-Triples' }); $writer->addTriple('a', 'b', '"c', function (error) { error.should.be.an.instanceof(Exception); error.should.have.property('message', 'Invalid literal: "c'); - done(); -}); }); //should end when the end option is not set', function (done) { -$outputStream = new QuickStream(), writer = TriGWriter(outputStream, {}); +$outputStream = new QuickStream(), writer = new TriGWriter(outputStream, {}); outputStream.should.have.property('ended', false); $writer->end(function () { outputStream.should.have.property('ended', true); - done(); -}); }); //should end when the end option is set to true', function (done) { -$outputStream = new QuickStream(), writer = TriGWriter(outputStream, { end: true }); +$outputStream = new QuickStream(), writer = new TriGWriter(outputStream, { end: true }); outputStream.should.have.property('ended', false); $writer->end(function () { outputStream.should.have.property('ended', true); - done(); -}); }); //should not end when the end option is set to false', function (done) { -$outputStream = new QuickStream(), writer = TriGWriter(outputStream, { end: false }); -outputStream.should.have.property('ended', false); -$writer->end(function () { - outputStream.should.have.property('ended', false); - done(); -}); -}); + $outputStream = new QuickStream(), writer = new TriGWriter(outputStream, { end: false }); + outputStream.should.have.property('ended', false); + $writer->end(function () { + outputStream.should.have.property('ended', false); }); */ From 718952e4e95cf5c7a1a6cb9115179ca1681b55d3 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 31 Mar 2017 17:27:58 +0200 Subject: [PATCH 09/72] Blank nodes serialize --- src/TriGWriter.php | 84 +++++++++++++++++++++-------------------- test/TriGWriterTest.php | 51 ++++++++++++------------- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 47bf77f..f9f341d 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -280,51 +280,53 @@ public function addPrefixes ($prefixes, $done = null) { // End a prefix block with a newline $this->write($hasPrefixes ? "\n" : '', $done); } -/* + // ### `blank` creates a blank node with the given content - public function blank (predicate, object) { - $children = predicate, child, length; - // Empty blank node - if (predicate === undefined) - children = []; - // Blank node passed as blank("predicate", "object") - else if (typeof predicate === 'string') - children = [{ predicate: predicate, object: object }]; - // Blank node passed as blank({ predicate: predicate, object: object }) - else if (!('length' in predicate)) - children = [predicate]; + public function blank ($predicate = null, $object = null) { + $children = $predicate; + $child = ""; + $length=""; + // Empty blank node + if (!isset($predicate)) + $children = []; + // Blank node passed as blank("$predicate", "object") + else if (is_string($predicate)) + $children = [[ "predicate" => $predicate, "object" => $object ]]; + // Blank node passed as blank({ $predicate: $predicate, object: object }) + else if (!is_array($predicate)) + $children = [$predicate]; - switch (length = children.length) { - // Generate an empty blank node - case 0: - return '[]'; - // Generate a non-nested one-triple blank node - case 1: - child = children[0]; - if (child.object[0] !== '[') - return '[ ' + $this->encodePredicate(child.predicate) + ' ' + - $this->encodeObject(child.object) + ' ]'; - // Generate a multi-triple or nested blank node - default: - $contents = '['; - // Write all triples in order - for ($i = 0; i < length; i++) { - child = children[i]; - // Write only the object is the predicate is the same as the previous - if (child.predicate === predicate) - contents += ', ' + $this->encodeObject(child.object); - // Otherwise, write the predicate and the object - else { - contents += (i ? ";\n " : "\n ") + - $this->encodePredicate(child.predicate) + ' ' + - $this->encodeObject(child.object); - predicate = child.predicate; + switch ($length = sizeof($children)) { + // Generate an empty blank node + case 0: + return '[]'; + // Generate a non-nested one-triple blank node + case 1: + $child = $children[0]; + if ($child["object"][0] !== '[') + return '[ ' . $this->encodePredicate($child["predicate"]) . ' ' . + $this->encodeObject($child["object"]) . ' ]'; + // Generate a multi-triple or nested blank node + default: + $contents = '['; + // Write all triples in order + for ($i = 0; $i < $length; $i++) { + $child = $children[$i]; + // Write only the object is the $predicate is the same as the previous + if ($child["predicate"] === $predicate) + $contents .= ', ' . $this->encodeObject($child["object"]); + // Otherwise, write the $predicate and the object + else { + $contents .= ($i ? ";\n " : "\n ") . + $this->encodePredicate($child["predicate"]) . ' ' . + $this->encodeObject($child["object"]); + $predicate = $child["predicate"]; + } + } + return $contents . "\n]"; } - } - return contents + "\n]"; } - } -*/ + // ### `list` creates a list node with the given content public function list ($elements) { $length = 0; diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 6709d2a..974d9b4 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -281,25 +281,24 @@ public function testOtherPrefixes () public function testBlankNodes2 () { - + //should serialize triples with an empty blank node as object', function (done) { + $writer = new TriGWriter(); + $writer->addTriple('a1', 'b', $writer->blank()); + $writer->addTriple('a2', 'b', $writer->blank([])); + $writer->end(function ($error, $output) { + $this->assertEquals(' [].' . "\n" . ' [].' . "\n",$output); + }); } /* - //should serialize triples with an empty blank node as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a1', 'b', $writer->blank()); -$writer->addTriple('a2', 'b', $writer->blank([])); -$writer->end(function (error, output) { - $this->assertEquals(' [].' . "\n" . ' [].' . "\n",$output); - -}); + //should serialize triples with a one-triple blank node as object', function (done) { $writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->blank('d', 'e')); $writer->addTriple('a2', 'b', $writer->blank(['predicate' => 'd','object' => 'e' ])); $writer->addTriple('a3', 'b', $writer->blank([['predicate' => 'd','object' => 'e' ]])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' [ ].' . "\n" . ' [ ].' . "\n" . ' [ ].' . "\n",$output); }); @@ -310,7 +309,7 @@ public function testBlankNodes2 () ['predicate' => 'd','object' => 'e' ], ['predicate' => 'f','object' => '"g"' ], ])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g"' . "\n" . '].' . "\n",$output); }); @@ -322,7 +321,7 @@ public function testBlankNodes2 () ['predicate' => 'f','object' => '"g"' ], ['predicate' => 'h','object' => 'i' ], ])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g";' . "\n" . ' ' . "\n" . '].' . "\n",$output); }); @@ -335,7 +334,7 @@ public function testBlankNodes2 () ['predicate' => 'g','object' => 'h' ], ['predicate' => 'g','object' => 'i' ], ])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' [' . "\n" . ' , ;' . "\n" . ' , ' . "\n" . '].' . "\n",$output); }); @@ -355,7 +354,7 @@ public function testBlankNodes2 () ['predicate' => 'j', object: $writer->blank('k', '"l"') ], ]) ], ])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' [' . "\n" . ' []' . "\n" . '].' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "i" ]' . "\n" . '].' . "\n" . ' [' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "l" ]' . "\n" . ']' . "\n" . '].' . "\n",$output); }); @@ -364,7 +363,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple($writer->blank(), 'b', 'c'); $writer->addTriple($writer->blank([]), 'b', 'c'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals('[] .' . "\n" . '[] .' . "\n",$output); }); @@ -374,7 +373,7 @@ public function testBlankNodes2 () $writer->addTriple($writer->blank('a', 'b'), 'c', 'd'); $writer->addTriple($writer->blank(['predicate' => 'a','object' => 'b' ]), 'c', 'd'); $writer->addTriple($writer->blank([['predicate' => 'a','object' => 'b' ]]), 'c', 'd'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals('[ ] .' . "\n" . '[ ] .' . "\n" . '[ ] .' . "\n",$output); }); @@ -383,7 +382,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple('a', 'b', 'c', $writer->blank()); $writer->addTriple('a', 'b', 'c', $writer->blank([])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals('[] {' . "\n" . ' ' . "\n" . '}' . "\n" . '[] {' . "\n" . ' ' . "\n" . '}' . "\n",$output); }); @@ -392,7 +391,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->list()); $writer->addTriple('a2', 'b', $writer->list([])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' ().' . "\n" . ' ().' . "\n",$output); }); @@ -401,7 +400,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->list(['c'])); $writer->addTriple('a2', 'b', $writer->list(['"c"'])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' ().' . "\n" . ' ("c").' . "\n",$output); }); @@ -410,7 +409,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple('a1', 'b', $writer->list(['c', 'd', 'e'])); $writer->addTriple('a2', 'b', $writer->list(['"c"', '"d"', '"e"'])); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' ( ).' . "\n" . ' ("c" "d" "e").' . "\n",$output); }); @@ -419,7 +418,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple($writer->list(), 'b1', 'c'); $writer->addTriple($writer->list([]), 'b2', 'c'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); }); @@ -428,7 +427,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriple($writer->list(['a']), 'b1', 'c'); $writer->addTriple($writer->list(['a']), 'b2', 'c'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); }); @@ -436,7 +435,7 @@ public function testBlankNodes2 () //should serialize triples with a three-element list as subject', function (done) { $writer = new TriGWriter(); $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals('( "b" "c") .' . "\n",$output); }); @@ -445,7 +444,7 @@ public function testBlankNodes2 () $writer = new TriGWriter(); $writer->addTriples([['subject' => 'a','predicate' => 'b','object' => 'c' ], ['subject' => 'a','predicate' => 'b','object' => 'd' }]); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' , .' . "\n",$output); }); @@ -463,7 +462,7 @@ public function testBlankNodes2 () $writer = new TriGWriter({'format' => 'N-Triples' }); $writer->addTriple('a', 'b', 'c'); $writer->addTriple('a', 'b', 'd'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); }); @@ -478,7 +477,7 @@ public function testBlankNodes2 () $writer = new TriGWriter({'format' => 'N-Quads' }); $writer->addTriple('a', 'b', 'c'); $writer->addTriple('a', 'b', 'd', 'g'); -$writer->end(function (error, output) { +$writer->end(function ($error, $output) { $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); }); From 84b45e3f39f749d5d6513dd1a7a3e1b2f9585412 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 31 Mar 2017 17:49:40 +0200 Subject: [PATCH 10/72] Almost fully there --- src/TriGWriter.php | 17 ++- test/TriGWriterTest.php | 248 ++++++++++++++++++++-------------------- 2 files changed, 131 insertions(+), 134 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index f9f341d..71e4b83 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -233,8 +233,8 @@ public function addTriple ($subject, $predicate = null, $object = null, $graph = // ### `addTriples` adds the triples to the output stream public function addTriples ($triples) { - for ($i = 0; $i < strlen($triples); $i++) - $this->addTriple(triples[$i]); + for ($i = 0; $i < sizeof($triples); $i++) + $this->addTriple($triples[$i]); } // ### `addPrefix` adds the prefix to the output stream @@ -281,7 +281,7 @@ public function addPrefixes ($prefixes, $done = null) { $this->write($hasPrefixes ? "\n" : '', $done); } - // ### `blank` creates a blank node with the given content + // ### `blank` creates a blank node with the given content public function blank ($predicate = null, $object = null) { $children = $predicate; $child = ""; @@ -292,10 +292,9 @@ public function blank ($predicate = null, $object = null) { // Blank node passed as blank("$predicate", "object") else if (is_string($predicate)) $children = [[ "predicate" => $predicate, "object" => $object ]]; - // Blank node passed as blank({ $predicate: $predicate, object: object }) - else if (!is_array($predicate)) - $children = [$predicate]; - + // Blank node passed as blank({ predicate: $predicate, object: $object }) + else if (is_array($predicate) && isset($predicate["predicate"])) + $children = [$predicate]; switch ($length = sizeof($children)) { // Generate an empty blank node case 0: @@ -328,10 +327,10 @@ public function blank ($predicate = null, $object = null) { } // ### `list` creates a list node with the given content - public function list ($elements) { + public function list ($elements = null) { $length = 0; if (isset($elements)) { - $length = strlen($elements); + $length = sizeof($elements); } $contents = []; for ($i = 0; $i < $length; $i++) { diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 974d9b4..58e5c8a 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -288,167 +288,165 @@ public function testBlankNodes2 () $writer->end(function ($error, $output) { $this->assertEquals(' [].' . "\n" . ' [].' . "\n",$output); }); - } - -/* - - //should serialize triples with a one-triple blank node as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a1', 'b', $writer->blank('d', 'e')); -$writer->addTriple('a2', 'b', $writer->blank(['predicate' => 'd','object' => 'e' ])); -$writer->addTriple('a3', 'b', $writer->blank([['predicate' => 'd','object' => 'e' ]])); -$writer->end(function ($error, $output) { - $this->assertEquals(' [ ].' . "\n" . ' [ ].' . "\n" . ' [ ].' . "\n",$output); + //should serialize triples with a one-triple blank node as object', function (done) { + $writer = new TriGWriter(); + $writer->addTriple('a1', 'b', $writer->blank('d', 'e')); + $writer->addTriple('a2', 'b', $writer->blank(['predicate' => 'd','object' => 'e' ])); + $writer->addTriple('a3', 'b', $writer->blank([['predicate' => 'd','object' => 'e' ]])); + $writer->end(function ($error, $output) { + $this->assertEquals(' [ ].' . "\n" . ' [ ].' . "\n" . ' [ ].' . "\n",$output); + }); + -}); + //should serialize triples with a two-triple blank node as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a', 'b', $writer->blank([ - ['predicate' => 'd','object' => 'e' ], - ['predicate' => 'f','object' => '"g"' ], -])); -$writer->end(function ($error, $output) { - $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g"' . "\n" . '].' . "\n",$output); + $writer = new TriGWriter(); + $writer->addTriple('a', 'b', $writer->blank([ + ['predicate' => 'd','object' => 'e' ], + ['predicate' => 'f','object' => '"g"' ], + ])); + $writer->end(function ($error, $output) { + $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g"' . "\n" . '].' . "\n",$output); -}); + }); //should serialize triples with a three-triple blank node as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a', 'b', $writer->blank([ - ['predicate' => 'd','object' => 'e' ], - ['predicate' => 'f','object' => '"g"' ], - ['predicate' => 'h','object' => 'i' ], -])); -$writer->end(function ($error, $output) { - $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g";' . "\n" . ' ' . "\n" . '].' . "\n",$output); + $writer = new TriGWriter(); + $writer->addTriple('a', 'b', $writer->blank([ + ['predicate' => 'd','object' => 'e' ], + ['predicate' => 'f','object' => '"g"' ], + ['predicate' => 'h','object' => 'i' ], + ])); + $writer->end(function ($error, $output) { + $this->assertEquals(' [' . "\n" . ' ;' . "\n" . ' "g";' . "\n" . ' ' . "\n" . '].' . "\n",$output); -}); + }); //should serialize triples with predicate-sharing blank node triples as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a', 'b', $writer->blank([ - ['predicate' => 'd','object' => 'e' ], - ['predicate' => 'd','object' => 'f' ], - ['predicate' => 'g','object' => 'h' ], - ['predicate' => 'g','object' => 'i' ], -])); -$writer->end(function ($error, $output) { - $this->assertEquals(' [' . "\n" . ' , ;' . "\n" . ' , ' . "\n" . '].' . "\n",$output); + $writer = new TriGWriter(); + $writer->addTriple('a', 'b', $writer->blank([ + ['predicate' => 'd','object' => 'e' ], + ['predicate' => 'd','object' => 'f' ], + ['predicate' => 'g','object' => 'h' ], + ['predicate' => 'g','object' => 'i' ], + ])); + $writer->end(function ($error, $output) { + $this->assertEquals(' [' . "\n" . ' , ;' . "\n" . ' , ' . "\n" . '].' . "\n",$output); -}); + }); //should serialize triples with nested blank nodes as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a1', 'b', $writer->blank([ - ['predicate' => 'd', object: $writer->blank() ], -])); -$writer->addTriple('a2', 'b', $writer->blank([ - ['predicate' => 'd', object: $writer->blank('e', 'f') ], - ['predicate' => 'g', object: $writer->blank('h', '"i"') ], -])); -$writer->addTriple('a3', 'b', $writer->blank([ - ['predicate' => 'd', object: $writer->blank([ - ['predicate' => 'g', object: $writer->blank('h', 'i') ], - ['predicate' => 'j', object: $writer->blank('k', '"l"') ], - ]) ], -])); -$writer->end(function ($error, $output) { - $this->assertEquals(' [' . "\n" . ' []' . "\n" . '].' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "i" ]' . "\n" . '].' . "\n" . ' [' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "l" ]' . "\n" . ']' . "\n" . '].' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple('a1', 'b', $writer->blank([ + ['predicate' => 'd', "object" => $writer->blank() ], + ])); + $writer->addTriple('a2', 'b', $writer->blank([ + ['predicate' => 'd', "object" => $writer->blank('e', 'f') ], + ['predicate' => 'g', "object" => $writer->blank('h', '"i"') ], + ])); + $writer->addTriple('a3', 'b', $writer->blank([ + ['predicate' => 'd', "object" => $writer->blank([ + ['predicate' => 'g', "object" => $writer->blank('h', 'i') ], + ['predicate' => 'j', "object" => $writer->blank('k', '"l"') ], + ]) ], + ])); + $writer->end(function ($error, $output) { + $this->assertEquals(' [' . "\n" . ' []' . "\n" . '].' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "i" ]' . "\n" . '].' . "\n" . ' [' . "\n" . ' [' . "\n" . ' [ ];' . "\n" . ' [ "l" ]' . "\n" . ']' . "\n" . '].' . "\n",$output); + }); //should serialize triples with an empty blank node as subject', function (done) { -$writer = new TriGWriter(); -$writer->addTriple($writer->blank(), 'b', 'c'); -$writer->addTriple($writer->blank([]), 'b', 'c'); -$writer->end(function ($error, $output) { - $this->assertEquals('[] .' . "\n" . '[] .' . "\n",$output); + $writer = new TriGWriter(); + $writer->addTriple($writer->blank(), 'b', 'c'); + $writer->addTriple($writer->blank([]), 'b', 'c'); + $writer->end(function ($error, $output) { + $this->assertEquals('[] .' . "\n" . '[] .' . "\n",$output); -}); + }); //should serialize triples with a one-triple blank node as subject', function (done) { -$writer = new TriGWriter(); -$writer->addTriple($writer->blank('a', 'b'), 'c', 'd'); -$writer->addTriple($writer->blank(['predicate' => 'a','object' => 'b' ]), 'c', 'd'); -$writer->addTriple($writer->blank([['predicate' => 'a','object' => 'b' ]]), 'c', 'd'); -$writer->end(function ($error, $output) { - $this->assertEquals('[ ] .' . "\n" . '[ ] .' . "\n" . '[ ] .' . "\n",$output); + $writer = new TriGWriter(); + $writer->addTriple($writer->blank('a', 'b'), 'c', 'd'); + $writer->addTriple($writer->blank(['predicate' => 'a','object' => 'b' ]), 'c', 'd'); + $writer->addTriple($writer->blank([['predicate' => 'a','object' => 'b' ]]), 'c', 'd'); + $writer->end(function ($error, $output) { + $this->assertEquals('[ ] .' . "\n" . '[ ] .' . "\n" . '[ ] .' . "\n",$output); -}); + }); //should serialize triples with an empty blank node as graph', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a', 'b', 'c', $writer->blank()); -$writer->addTriple('a', 'b', 'c', $writer->blank([])); -$writer->end(function ($error, $output) { - $this->assertEquals('[] {' . "\n" . ' ' . "\n" . '}' . "\n" . '[] {' . "\n" . ' ' . "\n" . '}' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple('a', 'b', 'c', $writer->blank()); + $writer->addTriple('a', 'b', 'c', $writer->blank([])); + $writer->end(function ($error, $output) { + $this->assertEquals('[] {' . "\n" . ' ' . "\n" . '}' . "\n" . '[] {' . "\n" . ' ' . "\n" . '}' . "\n",$output); + }); + } + public function testLists () + { //should serialize triples with an empty list as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a1', 'b', $writer->list()); -$writer->addTriple('a2', 'b', $writer->list([])); -$writer->end(function ($error, $output) { - $this->assertEquals(' ().' . "\n" . ' ().' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple('a1', 'b', $writer->list()); + $writer->addTriple('a2', 'b', $writer->list([])); + $writer->end(function ($error, $output) { + $this->assertEquals(' ().' . "\n" . ' ().' . "\n",$output); + }); //should serialize triples with a one-element list as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a1', 'b', $writer->list(['c'])); -$writer->addTriple('a2', 'b', $writer->list(['"c"'])); -$writer->end(function ($error, $output) { - $this->assertEquals(' ().' . "\n" . ' ("c").' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple('a1', 'b', $writer->list(['c'])); + $writer->addTriple('a2', 'b', $writer->list(['"c"'])); + $writer->end(function ($error, $output) { + $this->assertEquals(' ().' . "\n" . ' ("c").' . "\n",$output); + }); //should serialize triples with a three-element list as object', function (done) { -$writer = new TriGWriter(); -$writer->addTriple('a1', 'b', $writer->list(['c', 'd', 'e'])); -$writer->addTriple('a2', 'b', $writer->list(['"c"', '"d"', '"e"'])); -$writer->end(function ($error, $output) { - $this->assertEquals(' ( ).' . "\n" . ' ("c" "d" "e").' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple('a1', 'b', $writer->list(['c', 'd', 'e'])); + $writer->addTriple('a2', 'b', $writer->list(['"c"', '"d"', '"e"'])); + $writer->end(function ($error, $output) { + $this->assertEquals(' ( ).' . "\n" . ' ("c" "d" "e").' . "\n",$output); + }); //should serialize triples with an empty list as subject', function (done) { -$writer = new TriGWriter(); -$writer->addTriple($writer->list(), 'b1', 'c'); -$writer->addTriple($writer->list([]), 'b2', 'c'); -$writer->end(function ($error, $output) { - $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple($writer->list(), 'b1', 'c'); + $writer->addTriple($writer->list([]), 'b2', 'c'); + $writer->end(function ($error, $output) { + $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); + }); //should serialize triples with a one-element list as subject', function (done) { -$writer = new TriGWriter(); -$writer->addTriple($writer->list(['a']), 'b1', 'c'); -$writer->addTriple($writer->list(['a']), 'b2', 'c'); -$writer->end(function ($error, $output) { - $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple($writer->list(['a']), 'b1', 'c'); + $writer->addTriple($writer->list(['a']), 'b2', 'c'); + $writer->end(function ($error, $output) { + $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); + }); //should serialize triples with a three-element list as subject', function (done) { -$writer = new TriGWriter(); -$writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); -$writer->end(function ($error, $output) { - $this->assertEquals('( "b" "c") .' . "\n",$output); - -}); + $writer = new TriGWriter(); + $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); + $writer->end(function ($error, $output) { + $this->assertEquals('( "b" "c") .' . "\n",$output); + }); + } + public function testTriplesBulk () + { //should accept triples in bulk', function (done) { -$writer = new TriGWriter(); -$writer->addTriples([['subject' => 'a','predicate' => 'b','object' => 'c' ], -['subject' => 'a','predicate' => 'b','object' => 'd' }]); -$writer->end(function ($error, $output) { - $this->assertEquals(' , .' . "\n",$output); + $writer = new TriGWriter(); + $writer->addTriples([['subject' => 'a','predicate' => 'b','object' => 'c' ], + ['subject' => 'a','predicate' => 'b','object' => 'd' ]]); + $writer->end(function ($error, $output) { + $this->assertEquals(' , .' . "\n",$output); -}); + }); + } +/* //should not allow writing after end', function (done) { $writer = new TriGWriter(); $writer->addTriple(['subject' => 'a','predicate' => 'b','object' => 'c' ]); @@ -535,7 +533,7 @@ private function shouldSerialize() { } private function shouldNotSerialize() { - + //TODO } From 4c58ba3602fe513c5d429c5cb8a5d14970a09cec Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 31 Mar 2017 18:16:28 +0200 Subject: [PATCH 11/72] Make end callback optional --- src/TriGWriter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 71e4b83..2dd83c9 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -43,7 +43,6 @@ public function __construct($options = []) $this->characterReplacer = function ($character) { // Replace a single character by its escaped version $character = $character[0]; - var_dump($character); if (isset($character) && isset(self::ESCAPEREPLACEMENTS[$character])) { return self::ESCAPEREPLACEMENTS[$character]; } else { @@ -340,7 +339,7 @@ public function list ($elements = null) { } // ### `end` signals the end of the output stream - public function end($callback) + public function end($callback = null) { // Finish a possible pending triple if ($this->subject !== null) { @@ -349,7 +348,8 @@ public function end($callback) } // Disallow further writing $this->blocked = true; - $callback(null,$this->string); + if ($callback) + $callback(null,$this->string); return $this->string; } } \ No newline at end of file From d29f4442b1ea63a93fcf89794431c800d9344578 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 31 Mar 2017 19:26:32 +0200 Subject: [PATCH 12/72] Updated documentation --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++--- examples/write.php | 24 +++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 examples/write.php diff --git a/README.md b/README.md index 8ff63f2..b5ca855 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ $triple = [ Encode literals as follows (similar to N3.js) -``` +```php '"Tom"@en-gb' // lowercase language '"1"^^http://www.w3.org/2001/XMLSchema#integer' // no angular brackets <> ``` @@ -43,7 +43,66 @@ Install this library using [composer](http://getcomposer.org): composer install pietercolpaert/hardf ``` -Currently, we only have the `pietercolpaert\hardf\Util` class available, that will help you to create and evaluate literals, IRIs, and expand prefixes. +### Util class +```php +use pietercolpaert\hardf\Util; +``` + +A static class with a couple of helpful functions for handling our specific triple representation. It will help you to create and evaluate literals, IRIs, and expand prefixes. + +```php +$bool = isIRI($term); +$bool = isLiteral($term); +$bool = isBlank($term); +$bool = isDefaultGraph($term); +$bool = inDefaultGraph($triple); +$value = getLiteralValue($literal); +$literalType = getLiteralType($literal); +$lang = getLiteralLanguage($literal); +$bool = isPrefixedName($term); +$expanded = expandPrefixedName($prefixedName, $prefixes); +$iri = createIRI($iri); +$literalObject = createLiteral($value, $modifier = null); +``` + +See the documentation at https://github.com/RubenVerborgh/N3.js#utility for more information about what the functions exactly do. -See the documentation at https://github.com/RubenVerborgh/N3.js#utility. Instead of N3Util, you will have to use `pietercolpaert\hardf::Util`. +### TriGWriter class +```php +use pietercolpaert\hardf\TriGWriter +``` + +A class that should be instantiated and can write TriG or Turtle + +Example use: +```php +$writer = new TriGWriter([ + "prefixes" => [ + "schema" =>"http://schema.org/", + "dct" =>"http://purl.org/dc/terms/", + "geo" =>"http://www.w3.org/2003/01/geo/wgs84_pos#", + "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs"=> "http://www.w3.org/2000/01/rdf-schema#" + ] +]); +$writer->addPrefix("ex","http://example.org/"); +$writer->addTriple("schema:Person","dct:title","\"Person\"@en","http://example.org/#test"); +$writer->addTriple("schema:Person","schema:label","\"Person\"@en","http://example.org/#test"); +$writer->addTriple("ex:1","dct:title","\"Person1\"@en","http://example.org/#test"); +$writer->addTriple("ex:1","http://www.w3.org/1999/02/22-rdf-syntax-ns#type","schema:Person","http://example.org/#test"); +$writer->addTriple("ex:2","dct:title","\"Person2\"@en","http://example.org/#test"); +$writer->addTriple("schema:Person","dct:title","\"Person\"@en","http://example.org/#test2"); +echo $writer->end(); +``` + +All methods: +```php +addTriple ($subject, $predicate, $object, $graphl); +addTriples ($triples); +addPrefix($prefix, $iri); +addPrefixes ($prefixes, $done); +blank ($predicate, $object); +list ($elements); +end(); +``` diff --git a/examples/write.php b/examples/write.php new file mode 100644 index 0000000..af67a22 --- /dev/null +++ b/examples/write.php @@ -0,0 +1,24 @@ + [ + "schema" =>"http://schema.org/", + "dct" =>"http://purl.org/dc/terms/", + "geo" =>"http://www.w3.org/2003/01/geo/wgs84_pos#", + "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs"=> "http://www.w3.org/2000/01/rdf-schema#" + ] +]); + +$writer->addPrefix("ex","http://example.org/"); +$writer->addTriple("schema:Person","dct:title","\"Person\"@en","http://example.org/#test"); +$writer->addTriple("schema:Person","schema:label","\"Person\"@en","http://example.org/#test"); +$writer->addTriple("ex:1","dct:title","\"Person1\"@en","http://example.org/#test"); +$writer->addTriple("ex:1","http://www.w3.org/1999/02/22-rdf-syntax-ns#type","schema:Person","http://example.org/#test"); +$writer->addTriple("ex:2","dct:title","\"Person2\"@en","http://example.org/#test"); +$writer->addTriple("schema:Person","dct:title","\"Person\"@en","http://example.org/#test2"); +echo $writer->end(); From efa847bd555ce6833778badbf2c0b62501fab979 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 3 Apr 2017 08:33:43 +0200 Subject: [PATCH 13/72] Added documentation and streaming functionality --- README.md | 23 +++++++++++++++-------- src/TriGWriter.php | 15 ++++++++++++++- test/TriGWriterTest.php | 18 +++++++++++++++--- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b5ca855..0b396cc 100644 --- a/README.md +++ b/README.md @@ -96,13 +96,20 @@ $writer->addTriple("schema:Person","dct:title","\"Person\"@en","http://example.o echo $writer->end(); ``` -All methods: +All methods (some may throw an exception): ```php -addTriple ($subject, $predicate, $object, $graphl); -addTriples ($triples); -addPrefix($prefix, $iri); -addPrefixes ($prefixes, $done); -blank ($predicate, $object); -list ($elements); -end(); +//The method names should speak for themselves: +$writer = new TriGWriter(["prefixes": [ /* ... */]]); +$writer->addTriple($subject, $predicate, $object, $graphl); +$writer->addTriples($triples); +$writer->addPrefix($prefix, $iri); +$writer->addPrefixes($prefixes); +//Creates blank node($predicate and/or $object are optional) +$writer->blank($predicate, $object); +//Creates rdf:list with $elements +$list = $writer->list($elements); +//Returns the current output it is already able to create and clear the internal memory use (useful for streaming) +$out .= $writer->read(); +//Call this at the end. The return value will be the full triple output, or the rest of the output such as closing dots and brackets +$out .= $writer->end(); ``` diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 2dd83c9..039db06 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -39,7 +39,8 @@ public function __construct($options = []) $this->addPrefixes($options["prefixes"]); } } - + + // TODO: I think we could do without this... $this->characterReplacer = function ($character) { // Replace a single character by its escaped version $character = $character[0]; @@ -78,6 +79,18 @@ private function write ($string, $callback = null) { } } } + + // ### Reads a bit of the string + public function read () + { + if ($this->blocked) { + throw new \Exception('Cannot write because the writer has been closed.'); + } else { + $string = $this->string; + $this->string = ""; + return $string; + } + } // ### `_writeTriple` writes the triple to the output stream private function writeTriple ($subject, $predicate, $object, $graph, $done = null) { diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 58e5c8a..4295cf4 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -429,11 +429,23 @@ public function testLists () //should serialize triples with a three-element list as subject', function (done) { $writer = new TriGWriter(); $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); - $writer->end(function ($error, $output) { - $this->assertEquals('( "b" "c") .' . "\n",$output); - }); + $output = $writer->end(); + $this->assertEquals('( "b" "c") .' . "\n",$output); } + + public function testPartialRead () + { + //should only partially output the already given data and then continue writing until end + $writer = new TriGWriter(); + $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); + $output = $writer->read(); + $this->assertEquals('( "b" "c") ', $output); + $writer->addTriple('a', 'b', 'c'); + $output = $writer->end(); + $this->assertEquals(".\n .\n",$output); + } + public function testTriplesBulk () { //should accept triples in bulk', function (done) { From 807a464f1d60f09c5cdaafb0d828a742db1381e7 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 5 Apr 2017 16:04:00 +0200 Subject: [PATCH 14/72] Work in Progress --- README.md | 32 +- examples/parseAndWrite.php | 25 + src/N3Lexer.php | 435 ++++++++ src/TriGParser.php | 964 ++++++++++++++++++ src/TriGWriter.php | 5 +- test/TriGParserTest.php | 1943 ++++++++++++++++++++++++++++++++++++ 6 files changed, 3400 insertions(+), 4 deletions(-) create mode 100644 examples/parseAndWrite.php create mode 100644 src/N3Lexer.php create mode 100644 src/TriGParser.php create mode 100644 test/TriGParserTest.php diff --git a/README.md b/README.md index 0b396cc..5d14b9c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ See the documentation at https://github.com/RubenVerborgh/N3.js#utility for more ### TriGWriter class ```php -use pietercolpaert\hardf\TriGWriter +use pietercolpaert\hardf\TriGWriter; ``` A class that should be instantiated and can write TriG or Turtle @@ -113,3 +113,33 @@ $out .= $writer->read(); //Call this at the end. The return value will be the full triple output, or the rest of the output such as closing dots and brackets $out .= $writer->end(); ``` + +### TriGParser class + +Next to TriG, the TriGParser class also parses Turtle, N-Triples, N-Quads and the W3C “Team Submission” N3 + +Basic example: +```php +use pietercolpaert\hardf\TriGParser; +use pietercolpaert\hardf\TriGWriter; + +echo "--- First, simple implementation ---\n"; +$parser = new TriGParser(); +$writer = new TriGWriter(["format"=>"trig"]); +$triples = $parser->parse(" ."); +$writer->addTriples($triples); +echo $writer->end(); + +echo "--- Second streaming implementation with callbacks ---\n"; +$parser = new TriGParser(); +$writer = new TriGWriter(["format"=>"trig"]); +$parser->parse(" . .", function ($e, $triple) use ($writer) { + if (!$e && $triple) + $writer->addTriple($triple); + else if (!$triple) + echo $writer->end(); + else + echo "Error occured: " . $e; +}); +``` + diff --git a/examples/parseAndWrite.php b/examples/parseAndWrite.php new file mode 100644 index 0000000..4a8afc3 --- /dev/null +++ b/examples/parseAndWrite.php @@ -0,0 +1,25 @@ +"trig"]); +$triples = $parser->parse(" ."); +$writer->addTriples($triples); +echo $writer->end(); + +//Or, option 2, the streaming version +echo "--- Second streaming implementation with callbacks ---\n"; +$parser = new TriGParser(); +$writer = new TriGWriter(["format"=>"trig"]); +$error = null; +$parser->parse(" . .", function ($e, $triple) use ($writer) { + if (!$e && $triple) + $writer->addTriple($triple); + else if (!$triple) + echo $writer->end(); + else + echo "Error occured: " . $e; +}); diff --git a/src/N3Lexer.php b/src/N3Lexer.php new file mode 100644 index 0000000..5e052d9 --- /dev/null +++ b/src/N3Lexer.php @@ -0,0 +1,435 @@ + '\\', "'"=> "'", '"' => '"', + 'n' => '\n', 'r' => '\r', 't' => '\t', 'f' => '\f', 'b' => '\b', + '_' => '_', '~' => '~', '.' => '.', '-' => '-', '!' => '!', '$' => '$', '&' => '&', + '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', ';' => ';', '=' => '=', + '/' => '/', '?' => '?', '#' => '#', '@' => '@', '%' => '%' + ]; + private $illegalIriChars = '/[\x00-\x20<>\\"\{\}\|\^\`]/'; + + private $input; + private $line = 1; + + private $prevTokenType; + + public function __construct($options = []) { + + // In line mode (N-Triples or N-Quads), only simple features may be parsed + if (isset($options["lineMode"])) { + // Don't tokenize special literals + $this->tripleQuotedString = '/$0^/'; + $this->number = '/$0^/'; + $this->boolean = '/$0^/'; + // Swap the tokenize method for a restricted version + /*$this->tokenize = $this->tokenize; //TODO: what was this originally? + $this->tokenize = function ($input, $callback) { + $this->tokenize($input, function ($error, $token) { + if (!$error && preg_match('/^(?:IRI|prefixed|literal|langcode|type|\.|eof)$/',$token["type"])) + $callback && callback($error, $token); + else + $callback && callback($error || self::_syntaxError($token['type'], $callback = null)); + }); + };*/ + } + // Enable N3 functionality by default + $this->n3Mode = $options["n3"] !== false; + // Disable comment tokens by default + $this->comments = isset($options["comments"])?$options["comments"]:null; + } + + // ## Regular expressions + private $iri ='/^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/'; // IRI with escape sequences; needs sanity check after unescaping + private $unescapedIri = '/^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/'; // IRI without escape sequences; no unescaping + private $unescapedString= '/^"[^"\\]+"(?=[^"\\])/'; // non-empty string without escape sequences + private $singleQuotedString= '/^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^\'[^\'\\]*(?:\\.[^\'\\]*)*\'(?=[^\'\\])/'; + private $tripleQuotedString = '/^""("[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*")""|^\'\'(\'[^\'\\]*(?:(?:\\.|\'(?!\'\'))[^\'\\]*)*\')\'\'/'; + private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i'; + private $prefix = '/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:(?=[#\s<])/'; + + private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; + + //private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; + private $variable = '/^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?=[.,;!\^\s#()\[\]\{\}"\'<])/'; + + private $blank = '/^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?:[ \t]+|(?=\.?[,;:\s#()\[\]\{\}"\'<]))/'; + private $number = '/^[\-+]?(?:\d+\.?\d*([eE](?:[\-\+])?\d+)|\d*\.?\d+)(?=[.,;:\s#()\[\]\{\}"\'<])/'; + private $boolean = '/^(?:true|false)(?=[.,;\s#()\[\]\{\}"\'<])/'; + private $keyword = '/^@[a-z]+(?=[\s#<])/i'; + private $sparqlKeyword= '/^(?:PREFIX|BASE|GRAPH)(?=[\s#<])/i'; + private $shortPredicates= '/^a(?=\s+|<)/'; + private $newline= '/^[ \t]*(?:#[^\n\r]*)?(?:\r\n|\n|\r)[ \t]*/'; + private $comment= '/#([^\n\r]*)/'; + private $whitespace= '/^[ \t]+/'; + private $endOfFile= '/^(?:#[^\n\r]*)?$/'; + + // ## Private methods + // ### `_tokenizeToEnd` tokenizes as for as possible, emitting tokens through the callback + private function tokenizeToEnd($callback, $inputFinished) { + + // Continue parsing as far as possible; the loop will return eventually + $input = $this->input; + // Signals the syntax error through the callback + $reportSyntaxError = function ($self) use ($callback, $input) { $callback($self->syntaxError(preg_match("/^\S*/", $input)[0]), null); }; + + $outputComments = $this->comments; + while (true) { //TODO + // Count and skip whitespace lines + $whiteSpaceMatch; + $comment; + while (preg_match($this->newline, $input, $whiteSpaceMatch)) { + // Try to find a comment + if ($outputComments && preg_match($this->comment, $whiteSpaceMatch[0], $comment)) + callback(null, [ "line"=> $this->line, "type" => 'comment', "value"=> $comment[1], "prefix"=> '' ]); + // Advance the input + $input = substr($input,strlen($whiteSpaceMatch[0]), strlen($input)); + $this->line++; + } + // Skip whitespace on current line + if (preg_match($this->whitespace, $input, $whiteSpaceMatch)) + $input = substr($input,strlen($whiteSpaceMatch[0]), strlen($input)); + + // Stop for now if we're at the end + if (preg_match($this->endOfFile, $input)) { + // If the $input is finished, emit EOF + if ($inputFinished) { + // Try to find a final comment + if ($outputComments && preg_match($this->comment, $input, $comment)) + $callback(null, [ "line"=> $this->line, "type"=> 'comment', "value"=> $comment[1], "prefix"=> '' ]); + $callback($input = null, [ "line"=> $this->line, "type"=> 'eof', "value"=> '', "prefix"=> '' ]); + } + $this->input = $input; + return $input; + } + + // Look for specific token types based on the first character + $line = $this->line; + $type = ''; + $value = ''; + $prefix = ''; + $firstChar = $input[0]; + $match = null; + $matchLength = 0; + $unescaped = null; + $inconclusive = false; + switch ($firstChar) { + case '^': + // We need at least 3 tokens lookahead to distinguish ^^ and ^^pre:fixed + if (strlen($input) < 3) + break; + // Try to match a type + else if ($input[1] === '^') { + $this->prevTokenType = '^^'; + // Move to type IRI or prefixed name + $input = substr($input,2); + if ($input[0] !== '<') { + $inconclusive = true; + break; + } + } + // If no type, it must be a path expression + else { + if ($this->n3Mode) { + $matchLength = 1; + $type = '^'; + } + break; + } + // Fall through in case the type is an IRI + case '<': + // Try to find a full IRI without escape sequences + if (preg_match($this->unescapedIri, $input, $match)){ + $type = 'IRI'; + $value = $match[1]; + } + + // Try to find a full IRI with escape sequences + else if (preg_match($this->iri, $input, $match)) { + $unescaped = $this->unescape(match[1]); + if ($unescaped === null || preg_match($illegalIriChars,$unescaped)) + return $reportSyntaxError($this); + $type = 'IRI'; + $value = $unescaped; + } + // Try to find a backwards implication arrow + else if ($this->n3Mode && strlen($input) > 1 && $input[1] === '=') { + $type = 'inverse'; + $matchLength = 2; + $value = 'http://www.w3.org/2000/10/swap/log#implies'; + } + + break; + case '_': + // Try to find a blank node. Since it can contain (but not end with) a dot, + // we always need a non-dot character before deciding it is a prefixed name. + // Therefore, try inserting a space if we're at the end of the $input. + if ((preg_match($this->blank, $input, $match)) || + $inputFinished && (preg_match($this->blank, $input . ' ', $match))) { + $type = 'blank'; + $prefix = '_'; + $value = $match[1]; + } + + break; + + case '"': + case "'": + // Try to find a non-empty double-quoted literal without escape sequences + if (preg_match($this->unescapedString, $input, $match)){ + $type = 'literal'; + $value = $match[0]; + } + // Try to find any other literal wrapped in a pair of single or double quotes + else if (preg_match($this->singleQuotedString, $input, $match)) { + $unescaped = $this->unescape($match[0]); + if ($unescaped === null) + return $reportSyntaxError($this); + $type = 'literal'; + $value = preg_replace('/^'|'$/g', '"',$unescaped); + } + // Try to find a literal wrapped in three pairs of single or double quotes + else if (preg_match($this->tripleQuotedString, $input, $match)) { + $unescaped = isset($match[1])?$match[1]:$match[2]; + // Count the newlines and advance line counter + $this->line .= strlen(preg_split('/\r\n|\r|\n/',$unescaped)) - 1; + $unescaped = $this->unescape($unescaped); + if ($unescaped === null) + return $reportSyntaxError($this); + $type = 'literal'; + $value = preg_replace("/^'|'$/g", '"',$unescaped); + } + break; + + case '?': + // Try to find a variable + if ($this->n3Mode && (preg_match($this->variable, $input, $match))) { + $type = 'var'; + $value = $match[0]; + } + break; + + case '@': + // Try to find a language code + if ($this->prevTokenType === 'literal' && preg_match($this->langcode, $input, $match)){ + $type = 'langcode'; + $value = $match[1]; + } + + // Try to find a keyword + else if (preg_match($this->keyword, $input, $match)) + $type = $match[0]; + break; + + case '.': + // Try to find a dot as punctuation + if (strlen($input) === 1 ? $inputFinished : ($input[1] < '0' || $input[1] > '9')) { + $type = '.'; + $matchLength = 1; + break; + } + // Fall through to numerical case (could be a decimal dot) + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + // Try to find a number + if (preg_match($this->number, $input, $match)) { + $type = 'literal'; + $value = '"' . $match[0] . '"^^http://www.w3.org/2001/XMLSchema#' . ($match[1] ? 'double' : (preg_match("/^[+\-]?\d+$/",$match[0]) ? 'integer' : 'decimal')); + } + break; + case 'B': + case 'b': + case 'p': + case 'P': + case 'G': + case 'g': + // Try to find a SPARQL-style keyword + if (preg_match($this->sparqlKeyword, $input, $match)) + $type = strtoupper($match[0]); + else + $inconclusive = true; + break; + + case 'f': + case 't': + // Try to match a boolean + if (preg_match($this->boolean, $input, $match)){ + $type = 'literal'; + $value = '"' . $match[0] . '"^^http://www.w3.org/2001/XMLSchema#boolean'; + } else + $inconclusive = true; + break; + + case 'a': + // Try to find an abbreviated predicate + if (preg_match($this->shortPredicates, $input, $match)) { + $type = 'abbreviation'; + $value = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; + } + else + $inconclusive = true; + break; + case '=': + // Try to find an implication arrow or equals sign + if ($this->n3Mode && strlen($input) > 1) { + $type = 'abbreviation'; + if ($input[1] !== '>') { + $matchLength = 1; + $value = 'http://www.w3.org/2002/07/owl#sameAs'; + } + else{ + $matchLength = 2; + $value = 'http://www.w3.org/2000/10/swap/log#implies'; + } + } + break; + + case '!': + if (!$this->n3Mode) + break; + case ',': + case ';': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + // The next token is punctuation + $matchLength = 1; + $type = $firstChar; + break; + default: + $inconclusive = true; + } + + // Some first characters do not allow an immediate decision, so inspect more + if ($inconclusive) { + // Try to find a prefix + if (($this->prevTokenType === '@prefix' || $this->prevTokenType === 'PREFIX') && preg_match($this->prefix, $input, $match)){ + $type = 'prefix'; + $value = isset($match[1])?$match[1]:''; + } + // Try to find a prefixed name. Since it can contain (but not end with) a dot, + // we always need a non-dot character before deciding it is a prefixed name. + // Therefore, try inserting a space if we're at the end of the input. + else if (preg_match($this->prefixed, $input, $match) || $inputFinished && (preg_match($this->prefixed, $input . ' ', $match))) { + $type = 'prefixed'; + $prefix = isset($match[1])?$match[1]:''; + $value = $this->unescape($match[2]); + } + } + + // A type token is special: it can only be emitted after an IRI or prefixed name is read + if ($this->prevTokenType === '^^') { + switch ($type) { + case 'prefixed': $type = 'type'; break; + case 'IRI': $type = 'typeIRI'; break; + default: $type = ''; + } + } + + // What if nothing of the above was found? + if (!$type) { + // We could be in streaming mode, and then we just wait for more input to arrive. + // Otherwise, a syntax error has occurred in the input. + // One exception: error on an unaccounted linebreak (= not inside a triple-quoted literal). + if ($inputFinished || (!preg_match("/^'''|^\"\"\"/",$input) && preg_match("/\n|\r/",$input))) + return $reportSyntaxError($this); + else { + $this->input = $input; + return $input; + } + } + // Emit the parsed token + $callback(null, [ "line"=> $line, "type"=> $type, "value"=>$value, "prefix"=> $prefix ]); + $this->prevTokenType = $type; + + // Advance to next part to tokenize + $input = substr($input,$matchLength>0?$matchLength:strlen($match[0]), strlen($input)); + } + + } + + // ### `_unescape` replaces N3 escape codes by their corresponding characters + private function unescape($item) { + return preg_replace_callback($this->escapeSequence, function ($sequence, $unicode4, $unicode8, $escapedChar) { + $charCode; + if ($unicode4) { + $charCode = parseInt($unicode4, 16); + return fromCharCode($charCode); + } + else if ($unicode8) { + $charCode = parseInt($unicode8, 16); + if ($charCode <= 0xFFFF) return fromCharCode($charCode); + return fromCharCode(0xD800 . (($charCode -= 0x10000) / 0x400), 0xDC00 . ($charCode & 0x3FF)); + } + else { + $replacement = escapeReplacements[$escapedChar]; + if (!$replacement) + throw new Error(); + return $replacement; + } + },$item); + } + + // ### `_syntaxError` creates a syntax error for the given issue + private function syntaxError($issue, $line = 0) { + $this->input = null; + return new \Exception('Unexpected "' . $issue . '" on line ' . $line . '.'); + } + + + // ## Public methods + + // ### `tokenize` starts the transformation of an N3 document into an array of tokens. + // The input can be a string or a stream. + public function tokenize($input, $finalize = true) { + // If the input is a string, continuously emit tokens through the callback until the end + $this->input = $input; + $tokens = []; + $error = ""; + $this->tokenizeToEnd(function ($e, $t) use (&$tokens,&$error) { + if (isset($e)) { + $error = $e; + } + array_push($tokens, $t); + }, $finalize); + if ($error) throw $error; + return $tokens; + } + + // Adds the data chunk to the buffer and parses as far as possible + public function tokenizeChunk($input) + { + return $this->tokenize($input, false); + } + + public function end() + { + // Parses the rest + return $this->tokenizeToEnd(true); + } +} + diff --git a/src/TriGParser.php b/src/TriGParser.php new file mode 100644 index 0000000..09d4d36 --- /dev/null +++ b/src/TriGParser.php @@ -0,0 +1,964 @@ +contextStack = []; + $this->graph = null; + + // Set the document IRI + $this->setBase(isset($options["documentIRI"]) ? $options["documentIRI"]:null); + + // Set supported features depending on the format + if (!isset($options["format"])) { + $options["format"] = ""; + } + $format = strtolower($options["format"]); + $isTurtle = $format === 'turtle'; + $isTriG = $format === 'trig'; + + $isNTriples = strpos($format,"triple")?true:false; + $isNQuads = strpos($format, "quad")?true:false; + $isN3 = strpos($format, "n3")?true:false; + $isLineMode = $isNTriples || $isNQuads; + if (!($this->supportsNamedGraphs = !($isTurtle || $isN3))) + $this->readPredicateOrNamedGraph = $this->readPredicate; + $this->supportsQuads = !($isTurtle || $isTriG || $isNTriples || $isN3); + // Disable relative IRIs in N-Triples or N-Quads mode + if ($isLineMode) { + $this->base = ''; + $this->resolveIRI = function ($token) { + $this->error('Disallowed relative IRI', $token); + return $this->callback = function () {}; + $this->subject = null; + }; + } + $this->blankNodePrefix = !isset($options["blankNodePrefix"]) ? '' : + '_:' . preg_replace('/^_:/', '', $options["blankNodePrefix"]); + + $this->lexer = isset($options["lexer"])? $options["lexer"] : new N3Lexer([ "lineMode"=> $isLineMode, "n3"=> $isN3 ]); + // Disable explicit quantifiers by default - TODO: is !! same in PHP? + $this->explicitQuantifiers = isset($options["explicitQuantifiers"])?$options["explicitQuantifiers"]:null; + + //This will initiate the callback methods + $this->initReaders(); + } + + // ## Private class methods + // ### `_resetBlankNodeIds` restarts blank node identification + public function _resetBlankNodeIds () { + $this->blankNodePrefix = 0; + $this->blankNodeCount = 0; + } + + // ### `_setBase` sets the base IRI to resolve relative IRIs + private function setBase ($baseIRI = null) { + if (!$baseIRI) + $this->base = null; + else { + // Remove fragment if present + $fragmentPos = strpos($baseIRI,'#'); + if ($fragmentPos) + $baseIRI = substr($baseIRI,0, $fragmentPos); + // Set base IRI and its components + $this->base = $baseIRI; + $this->basePath = strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); + preg_match($schemeAuthority, $baseIRI, $matches); + $this->baseRoot = $matches[0]; + $this->baseScheme = $matches[1]; + } + } + + // ### `_saveContext` stores the current parsing context + // when entering a new scope (list, blank node, formula) + private function saveContext($type, $graph, $subject, $predicate, $object) { + $n3Mode = $this->n3Mode; + $this->contextStack.push([ + "subject"=> $subject, "predicate"=> $predicate,"object"=> $object, + "graph" => $graph, "type"=> $type, + "inverse" => $n3Mode ? $this->inversePredicate : false, + "blankPrefix"=> $n3Mode ? $this->prefixes["_"] : '', + "quantified"=> $n3Mode ? $this->quantified : null + ]); + // The settings below only apply to N3 streams + if ($n3Mode) { + // Every new scope resets the predicate direction + $this->inversePredicate = false; + // In N3, blank nodes are scoped to a formula + // (using a dot as separator, as a blank node label cannot start with it) + $this->prefixes["_"] = $this->graph . '.'; + // Quantifiers are scoped to a formula TODO: is this correct? + //$this->quantified = Object.create($this->quantified); + } + } + + // ### `_restoreContext` restores the parent context + // when leaving a scope (list, blank node, formula) + private function restoreContext() { + $context = $this->contextStack.pop(); + $n3Mode = $this->n3Mode; + $this->subject = $context["subject"]; + $this->predicate = $context["predicate"]; + $this->object = $context["object"]; + $this->graph = $context["graph"]; + // The settings below only apply to N3 streams + if ($n3Mode) { + $this->inversePredicate = $context["inverse"]; + $this->prefixes["_"] = $context["blankPrefix"]; + $this->quantified = $context["quantified"]; + } + } + + + private function initReaders () + { + // ### `_readInTopContext` reads a token when in the top context + $this->readInTopContext = function ($token) { + if (!isset($token["type"])) { + $token["type"] = ""; + } + switch ($token["type"]) { + // If an EOF token arrives in the top context, signal that we're done + case 'eof': + if ($this->graph !== null) + return $this->error('Unclosed graph', $token); + unset($this->prefixes["_"]); + return call_user_func($this->callback,null, null, $this->prefixes); + // It could be a prefix declaration + case 'PREFIX': + $this->sparqlStyle = true; + case '@prefix': + return $this->readPrefix; + // It could be a base declaration + case 'BASE': + $this->sparqlStyle = true; + case '@base': + return $this->readBaseIRI; + // It could be a graph + case '{': + if ($this->supportsNamedGraphs) { + $this->graph = ''; + $this->subject = null; + return $this->readSubject; + } + case 'GRAPH': + if ($this->supportsNamedGraphs) + return $this->readNamedGraphLabel; + // Otherwise, the next token must be a subject + default: + return call_user_func($this->readSubject,$token); + } + }; + + // ### `_readEntity` reads an IRI, prefixed name, blank node, or variable + $this->readEntity = function ($token, $quantifier = null) { + $value; + switch ($token["type"]) { + // Read a relative or absolute IRI + case 'IRI': + case 'typeIRI': + $value = ($this->base === null || preg_match($this->absoluteIRI,$token["value"])) ? $token["value"] : $this->resolveIRI($token); + break; + // Read a blank node or prefixed name + case 'type': + case 'blank': + case 'prefixed': + $prefix = $this->prefixes[$token["prefix"]]; + if (!isset($prefix)) + return call_user_func($this->error,'Undefined prefix "' . $token["prefix"] . ':"', $token); + $value = $prefix . $token["value"]; + break; + // Read a variable + case 'var': + return $token["value"]; + // Everything else is not an entity + default: + return $this->error('Expected entity but got ' . $token["type"], $token); + } + // In N3 mode, replace the entity if it is quantified + if (!isset($quantifier) && isset($this->n3Mode) && isset($this->quantified[$value])) + $value = $this->quantified[$value]; + return $value; + }; + + // ### `_readSubject` reads a triple's subject + $this->readSubject = function ($token) { + $this->predicate = null; + switch ($token["type"]) { + case '[': + // Start a new triple with a new blank node as subject + $this->saveContext('blank', $this->graph, + $this->subject = '_:b' . $this->blankNodeCount++, null, null); + return $this->readBlankNodeHead; + case '(': + // Start a new list + $this->saveContext('list', $this->graph, self::RDF_NIL, null, null); + $this->subject = null; + return $this->readListItem; + case '{': + // Start a new formula + if (!$this->n3Mode) + return $this->error('Unexpected graph', $token); + $this->saveContext('formula', $this->graph, + $this->graph = '_:b' . $this->blankNodeCount++, null, null); + return $this->readSubject; + case '}': + // No subject; the graph in which we are reading is closed instead + return $this->readPunctuation($token); + case '@forSome': + $this->subject = null; + $this->predicate = 'http://www.w3.org/2000/10/swap/reify#forSome'; + $this->quantifiedPrefix = '_:b'; + return $this->readQuantifierList; + case '@forAll': + $this->subject = null; + $this->predicate = 'http://www.w3.org/2000/10/swap/reify#forAll'; + $this->quantifiedPrefix = '?b-'; + return $this->readQuantifierList; + default: + // Read the subject entity + $this->subject = call_user_func($this->readEntity,$token); + if ($this->subject == null) + return; + // In N3 mode, the subject might be a path + if (isset($this->n3Mode)) + return $this->getPathReader($this->readPredicateOrNamedGraph); + } + + // The next token must be a predicate, + // or, if the subject was actually a graph IRI, a named graph + return $this->readPredicateOrNamedGraph; + }; + + // ### `_readPredicate` reads a triple's predicate + $this->readPredicate = function ($token) { + $type = $token["type"]; + switch ($type) { + case 'inverse': + $this->inversePredicate = true; + case 'abbreviation': + $this->predicate = $token["value"]; + break; + case '.': + case ']': + case '}': + // Expected predicate didn't come, must have been trailing semicolon + if ($this->predicate === null) + return $this->error('Unexpected ' . $type, $token); + $this->subject = null; + return $type === ']' ? $this->readBlankNodeTail($token) : $this->readPunctuation($token); + case ';': + // Extra semicolons can be safely ignored + return $this->readPredicate; + case 'blank': + if (!$this->n3Mode) + return $this->error('Disallowed blank node as predicate', $token); + default: + $this->predicate = call_user_func($this->readEntity,$token); + if ($this->predicate == null) + return; + } + // The next token must be an object + return $this->readObject; + }; + + // ### `_readObject` reads a triple's object + $this->readObject = function ($token) { + switch ($token["type"]) { + case 'literal': + $this->object = $token["value"]; + return $this->readDataTypeOrLang; + case '[': + // Start a new triple with a new blank node as subject + $this->saveContext('blank', $this->graph, $this->subject, $this->predicate, + $this->subject = '_:b' . $this->blankNodeCount++); + return $this->readBlankNodeHead; + case '(': + // Start a new list + $this->saveContext('list', $this->graph, $this->subject, $this->predicate, self::RDF_NIL); + $this->subject = null; + return $this->readListItem; + case '{': + // Start a new formula + if (!$this->n3Mode) + return $this->error('Unexpected graph', $token); + $this->saveContext('formula', $this->graph, $this->subject, $this->predicate, + $this->graph = '_:b' . $this->blankNodeCount++); + return $this->readSubject; + default: + // Read the object entity + $this->object = call_user_func($this->readEntity, $token); + if ($this->object == null) + return; + // In N3 mode, the object might be a path + if (isset($this->n3Mode)) + return $this->getPathReader($this->getContextEndReader()); + } + return call_user_func($this->getContextEndReader); + }; + + // ### `_readPredicateOrNamedGraph` reads a triple's predicate, or a named graph + $this->readPredicateOrNamedGraph = function ($token) { + return $token["type"] === '{' ? call_user_func($this->readGraph,$token) : call_user_func($this->readPredicate,$token); + }; + + // ### `_readGraph` reads a graph + $this->readGraph = function ($token) { + if ($token["type"] !== '{') + return $this->error('Expected graph but got ' . $token["type"], $token); + // The "subject" we read is actually the GRAPH's label + $this->graph = $this->subject; + $this->subject = null; + return $this->readSubject; + }; + + // ### `_readBlankNodeHead` reads the head of a blank node + $this->readBlankNodeHead = function ($token) { + if ($token["type"] === ']') { + $this->subject = null; + return $this->readBlankNodeTail($token); + } + else { + $this->predicate = null; + return $this->readPredicate($token); + } + }; + + // ### `_readBlankNodeTail` reads the end of a blank node + $this->readBlankNodeTail = function ($token) { + if ($token["type"] !== ']') + return $this->readBlankNodePunctuation($token); + + // Store blank node triple + if ($this->subject !== null) + $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + + // Restore the parent context containing this blank node + $empty = $this->predicate === null; + $this->restoreContext(); + // If the blank node was the subject, continue reading the predicate + if ($this->object === null) + // If the blank node was empty, it could be a named graph label + return $empty ? $this->readPredicateOrNamedGraph : $this->readPredicateAfterBlank; + // If the blank node was the object, restore previous context and read punctuation + else + return $this->getContextEndReader(); + }; + + // ### `_readPredicateAfterBlank` reads a predicate after an anonymous blank node + $this->readPredicateAfterBlank = function ($token) { + // If a dot follows a blank node in top context, there is no predicate + if ($token["type"] === '.' && sizeof($this->contextStack) === 0) { + $this->subject = null; // cancel the current triple + return $this->readPunctuation($token); + } + return $this->readPredicate($token); + }; + + // ### `_readListItem` reads items from a list + $this->readListItem = function ($token) { + $item = null; // The item of the list + $list = null; // The list itself + $prevList = $this->subject; // The previous list that contains this list + $stack = $this->contextStack; // The stack of parent contexts + $parent = $stack[sizeof($stack) - 1];// The parent containing the current list + $next = $this->readListItem; // The next function to execute + $itemComplete = true; // Whether the item has been read fully + + switch ($token["type"]) { + case '[': + // Stack the current list triple and start a new triple with a blank node as subject + $item = '_:b' . $this->blankNodeCount++; + $this->subject = $item; + $this->saveContext('blank', $this->graph, $list = '_:b' . $this->blankNodeCount++, self::RDF_FIRST, $this->subject); + $next = $this->readBlankNodeHead; + break; + case '(': + // Stack the current list triple and start a new list + $this->saveContext('list', $this->graph, $list = '_:b' . $this->blankNodeCount++, + self::RDF_FIRST, self::RDF_NIL); + $this->subject = null; + break; + case ')': + // Closing the list; restore the parent context + $this->restoreContext(); + // If this list is contained within a parent list, return the membership triple here. + // This will be ` rdf:first .`. + if (sizeof($stack) !== 0 && $stack[sizeof($stack) - 1] === 'list') + $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + // Was this list the parent's subject? + if ($this->predicate === null) { + // The next token is the predicate + $next = $this->readPredicate; + // No list tail if this was an empty list + if ($this->subject === self::RDF_NIL) + return $next; + } + // The list was in the parent context's object + else { + $next = $this->getContextEndReader(); + // No list tail if this was an empty list + if ($this->object === self::RDF_NIL) + return $next; + } + // Close the list by making the head nil + $list = self::RDF_NIL; + break; + case 'literal': + $item = $token["value"]; + $itemComplete = false; // Can still have a datatype or language + $next = $this->readListItemDataTypeOrLang; + break; + default: + $item = $this->readEntity($token); + if ($item == null) + return; + } + + // Create a new blank node if no item head was assigned yet + if ($list === null) + $list = '_:b' . $this->blankNodeCount++; + $this->subject = $list; + + // Is this the first element of the list? + if ($prevList === null) { + // This list is either the subject or the object of its parent + if ($parent['predicate'] === null) + $parent['subject'] = $list; + else + $parent['object'] = $list; + } + else { + // Continue the previous list with the current list + $this->triple($prevList, self::RDF_REST, $list, $this->graph); + } + // Add the item's value + if ($item !== null) { + // In N3 mode, the item might be a path + if ($this->n3Mode && ($token["type"] === 'IRI' || $token["type"] === 'prefixed')) { + // Create a new context to add the item's path + $this->saveContext('item', $this->graph, $list, self::RDF_FIRST, $item); + $this->subject = $item; + $this->predicate = null; + // _readPath will restore the context and output the item + return $this->getPathReader($this->readListItem); + } + // Output the item if it is complete + if ($itemComplete) + $this->triple($list, self::RDF_FIRST, $item, $this->graph); + // Otherwise, save it for completion + else + $this->object = $item; + } + return $next; + }; + + // ### `_readDataTypeOrLang` reads an _optional_ data type or language + $this->readDataTypeOrLang = function ($token) { + return $this->completeLiteral($token, false); + }; + + // ### `_readListItemDataTypeOrLang` reads an _optional_ data type or language in a list + $this->readListItemDataTypeOrLang = function ($token) { + return $this->completeLiteral($token, true); + }; + + // ### `_completeLiteral` completes the object with a data type or language + $this->completeLiteral = function ($token, $listItem) { + $suffix = false; + switch ($token["type"]) { + // Add a "^^type" suffix for types (IRIs and blank nodes) + case 'type': + case 'typeIRI': + $suffix = true; + $this->object .= '^^' . $this->readEntity($token); + break; + // Add an "@lang" suffix for language tags + case 'langcode': + $suffix = true; + $this->object .= '@' . strtolower($token["value"]); + break; + } + // If this literal was part of a list, write the item + // (we could also check the context stack, but passing in a flag is faster) + if ($listItem) + $this->triple($this->subject, self::RDF_FIRST, $this->object, $this->graph); + // Continue with the rest of the input + if ($suffix) + return $this->getContextEndReader(); + else { + $this->readCallback = $this->getContextEndReader(); + return $this->readCallback($token); + } + }; + + // ### `_readFormulaTail` reads the end of a formula + $this->readFormulaTail = function ($token) { + if ($token["type"] !== '}') + return $this->readPunctuation($token); + + // Store the last triple of the formula + if ($this->subject !== null) + $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + + // Restore the parent context containing this formula + $this->restoreContext(); + // If the formula was the subject, continue reading the predicate. + // If the formula was the object, read punctuation. + return $this->object === null ? $this->readPredicate : $this->getContextEndReader(); + }; + + // ### `_readPunctuation` reads punctuation between triples or triple parts + $this->readPunctuation = function ($token) { + $next; + $subject = $this->subject; + $graph = $this->graph; + $inversePredicate = $this->inversePredicate; + switch ($token["type"]) { + // A closing brace ends a graph + case '}': + if ($this->graph === null) + return $this->error('Unexpected graph closing', $token); + if ($this->n3Mode) + return $this->readFormulaTail($token); + $this->graph = null; + // A dot just ends the statement, without sharing anything with the next + case '.': + $this->subject = null; + $next = sizeof($this->contextStack) ? $this->readSubject : $this->readInTopContext; + if ($inversePredicate) $this->inversePredicate = false;//TODO: What’s this? + break; + // Semicolon means the subject is shared; predicate and object are different + case ';': + $next = $this->readPredicate; + break; + // Comma means both the subject and predicate are shared; the object is different + case ',': + $next = $this->readObject; + break; + default: + // An entity means this is a quad (only allowed if not already inside a graph) + $graph = call_user_func($this->readEntity,$token); + if ($this->supportsQuads && $this->graph === null && $graph) { + $next = $this->readQuadPunctuation; + break; + } + return $this->error('Expected punctuation to follow "' . $this->object . '"', $token); + } + // A triple has been completed now, so return it + if ($subject !== null) { + $predicate = $this->predicate; + $object = $this->object; + if (!$inversePredicate) + call_user_func($this->triple,$subject, $predicate, $object, $graph); + else + call_user_func($this->triple,$object, $predicate, $subject, $graph); + } + return $next; + }; + + // ### `_readBlankNodePunctuation` reads punctuation in a blank node + $this->readBlankNodePunctuation = function ($token) { + $next; + switch ($token["type"]) { + // Semicolon means the subject is shared; predicate and object are different + case ';': + $next = $this->readPredicate; + break; + // Comma means both the subject and predicate are shared; the object is different + case ',': + $next = $this->readObject; + break; + default: + return $this->error('Expected punctuation to follow "' . $this->object . '"', $token); + } + // A triple has been completed now, so return it + $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + return $next; + }; + + // ### `_readQuadPunctuation` reads punctuation after a quad + $this->readQuadPunctuation = function ($token) { + if ($token["type"] !== '.') + return call_user_func($this->error,'Expected dot to follow quad', $token); + return $this->readInTopContext; + }; + + // ### `_readPrefix` reads the prefix of a prefix declaration + $this->readPrefix = function ($token) { + if ($token["type"] !== 'prefix') + return $this->error('Expected prefix to follow @prefix', $token); + $this->prefix = $token["value"]; + return $this->readPrefixIRI; + }; + + // ### `_readPrefixIRI` reads the IRI of a prefix declaration + $this->readPrefixIRI = function ($token) { + if ($token["type"] !== 'IRI') + return $this->error('Expected IRI to follow prefix "' . $this->prefix . ':"', $token); + $prefixIRI = $this->readEntity($token); + $this->prefixes[$this->prefix] = $prefixIRI; + $this->prefixCallback($this->prefix, $prefixIRI); + return $this->readDeclarationPunctuation; + }; + + // ### `_readBaseIRI` reads the IRI of a base declaration + $this->readBaseIRI = function ($token) { + if ($token["type"] !== 'IRI') + return $this->error('Expected IRI to follow base declaration', $token); + $this->setBase($this->base === null || preg_match($this->absoluteIRI,$token["value"]) ? + $token["value"] : $this->resolveIRI($token)); + return $this->readDeclarationPunctuation; + }; + + // ### `_readNamedGraphLabel` reads the label of a named graph + $this->readNamedGraphLabel = function ($token) { + switch ($token["type"]) { + case 'IRI': + case 'blank': + case 'prefixed': + $this->readGraph; //TODO: what’s this? + return $this->readSubject($token); + case '[': + return $this->readNamedGraphBlankLabel; + default: + return $this->error('Invalid graph label', $token); + } + }; + + // ### `_readNamedGraphLabel` reads a blank node label of a named graph + $this->readNamedGraphBlankLabel = function ($token) { + if ($token["type"] !== ']') + return $this->error('Invalid graph label', $token); + $this->subject = '_:b' . $this->blankNodeCount++; + return $this->readGraph; + }; + + // ### `_readDeclarationPunctuation` reads the punctuation of a declaration + $this->readDeclarationPunctuation = function ($token) { + // SPARQL-style declarations don't have punctuation + if ($this->sparqlStyle) { + $this->sparqlStyle = false; + return $this->readInTopContext($token); + } + + if ($token["type"] !== '.') + return $this->error('Expected declaration to end with a dot', $token); + return $this->readInTopContext; + }; + + // Reads a list of quantified symbols from a @forSome or @forAll statement + $this->readQuantifierList = function ($token) { + $entity; + switch ($token["type"]) { + case 'IRI': + case 'prefixed': + $entity = $this->readEntity($token, true); + if (!$entity) + break; + default: + return $this->error('Unexpected ' . $token["type"], $token); + } + // Without explicit quantifiers, map entities to a quantified entity + if (!$this->explicitQuantifiers) + $this->quantified[$entity] = $this->quantifiedPrefix . $this->blankNodeCount++; + // With explicit quantifiers, output the reified quantifier + else { + // If this is the first item, start a new quantifier list + if ($this->subject === null) { + $this->subject = '_:b' . $this->blankNodeCount++; + $this->triple(isset($this->graph)?$this->graph:'', $this->predicate, $this->subject, self::QUANTIFIERS_GRAPH); + } + // Otherwise, continue the previous list + else + $this->triple($this->subject, self::RDF_REST, + $this->subject = '_:b' . $this->blankNodeCount++, self::QUANTIFIERS_GRAPH); + // Output the list item + $this->triple($this->subject, self::RDF_FIRST, $entity, self::QUANTIFIERS_GRAPH); + } + return $this->readQuantifierPunctuation; + }; + + // Reads punctuation from a @forSome or @forAll statement + $this->readQuantifierPunctuation = function ($token) { + // Read more quantifiers + if ($token["type"] === ',') + return $this->readQuantifierList; + // End of the quantifier list + else { + // With explicit quantifiers, close the quantifier list + if ($this->explicitQuantifiers) { + $this->triple($this->subject, self::RDF_REST, self::RDF_NIL, self::QUANTIFIERS_GRAPH); + $this->subject = null; + } + // Read a dot + $this->readCallback = $this->getContextEndReader(); + return $this->readCallback($token); + } + }; + + // ### `_getPathReader` reads a potential path and then resumes with the given function + $this->getPathReader = function ($afterPath) { + $this->afterPath = $afterPath; + return $this->readPath; + }; + + // ### `_readPath` reads a potential path + $this->readPath = function ($token) { + switch ($token["type"]) { + // Forward path + case '!': return $this->readForwardPath; + // Backward path + case '^': return $this->readBackwardPath; + // Not a path; resume reading where we left off + default: + $stack = $this->contextStack; + $parent = is_array($stack) && $stack[sizeof($stack) - 1]; + // If we were reading a list item, we still need to output it + if ($parent && $parent["type"] === 'item') { + // The list item is the remaining subejct after reading the path + $item = $this->subject; + // Switch back to the context of the list + $this->restoreContext(); + // Output the list item + $this->triple($this->subject, self::RDF_FIRST, $item, $this->graph); + } + return $this->afterPath($token); + } + }; + + // ### `_readForwardPath` reads a '!' path + $this->readForwardPath = function ($token) { + $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; + // The next token is the predicate + $predicate = $this->readEntity($token); + if (!$predicate) + return; + // If we were reading a subject, replace the subject by the path's object + if ($this->predicate === null) { + $subject = $this->subject; + $this->subject = $object; + } + // If we were reading an object, replace the subject by the path's object + else { + $subject = $this->object; + $this->object = $object; + } + // Emit the path's current triple and read its next section + $this->triple($subject, $predicate, $object, $this->graph); + return $this->readPath; + }; + + // ### `_readBackwardPath` reads a '^' path + $this->readBackwardPath = function ($token) { + $subject = '_:b' . $this->blankNodeCount++; + $predicate; $object; + // The next token is the predicate + $predicate = $this->readEntity($token); + if ($predicate) + return; + // If we were reading a subject, replace the subject by the path's subject + if ($this->predicate === null) { + $object = $this->subject; + $this->subject = $subject; + } + // If we were reading an object, replace the subject by the path's subject + else { + $object = $this->object; + $this->object = $subject; + } + // Emit the path's current triple and read its next section + $this->triple($subject, $predicate, $object, $this->graph); + return $this->readPath; + }; + + // ### `_getContextEndReader` gets the next reader function at the end of a context + $this->getContextEndReader = function () { + $contextStack = $this->contextStack; + if (!sizeof($contextStack)) + return $this->readPunctuation; + + switch ($contextStack[sizeof($contextStack) - 1]["type"]) { + case 'blank': + return $this->readBlankNodeTail; + case 'list': + return $this->readListItem; + case 'formula': + return $this->readFormulaTail; + } + }; + + // ### `_triple` emits a triple through the callback + $this->triple = function ($subject, $predicate, $object, $graph) { + call_user_func($this->callback,null, [ 'subject'=> $subject, 'predicate'=> $predicate, 'object'=> $object, 'graph'=> isset($graph)?$graph:'' ]); + }; + + // ### `_error` emits an error message through the callback + $this->error = function ($message, $token) { + call_user_func($this->callback, new \Exception($message . ' on line ' . $token['line'] . '.')); + }; + + // ### `_resolveIRI` resolves a relative IRI token against the base path, + // assuming that a base path has been set and that the IRI is indeed relative + $this->resolveIRI = function ($token) { + $iri = $token["value"]; + switch ($iri[0]) { + // An empty relative IRI indicates the base IRI + case undefined: return $this->base; + // Resolve relative fragment IRIs against the base IRI + case '#': return $this->base . $iri; + // Resolve relative query string IRIs by replacing the query string + case '?': return preg_replace('/(?:\?.*)?$/', $iri, $this->base); + // Resolve root-relative IRIs at the root of the base IRI + case '/': + // Resolve scheme-relative IRIs to the scheme + return ($iri[1] === '/' ? $this->baseScheme : $this->baseRoot) . $this->removeDotSegments($iri); + // Resolve all other IRIs at the base IRI's path + default: + return $this->removeDotSegments($this->basePath . $iri); + } + }; + + // ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986 + $this->removeDotSegments = function ($iri) { + // Don't modify the IRI if it does not contain any dot segments + if (!preg_match($this->dotSegments,$iri)) + return $iri; + + // Start with an imaginary slash before the IRI in order to resolve trailing './' and '../' + $result = ''; + $length = strlen($iri); + $i = -1; + $pathStart = -1; + $segmentStart = 0; + $next = '/'; + + while ($i < $length) { + switch ($next) { + // The path starts with the first slash after the authority + case ':': + if ($pathStart < 0) { + // Skip two slashes before the authority + if ($iri[++$i] === '/' && $iri[++$i] === '/') + // Skip to slash after the authority + while (($pathStart = $i + 1) < $length && $iri[$pathStart] !== '/') + $i = $pathStart; + } + break; + // Don't modify a query string or fragment + case '?': + case '#': + $i = $length; + break; + // Handle '/.' or '/..' path segments + case '/': + if ($iri[$i + 1] === '.') { + $next = $iri[++$i + 1]; + switch ($next) { + // Remove a '/.' segment + case '/': + $result .= substr($iri, $segmentStart, $i - 1); + $segmentStart = $i + 1; + break; + // Remove a trailing '/.' segment + case undefined: + case '?': + case '#': + return $result . substr($iri, $segmentStart, $i) . substr($iri,$i + 1); + // Remove a '/..' segment + case '.': + if (!isset($iri[++$i + 1]) || $iri[++$i + 1] === '/' || $iri[++$i + 1] === '?' || $iri[++$i + 1] === '#') { + $next = $iri[++$i + 1]; + $result .= substr($iri, $segmentStart, $i - 2); + // Try to remove the parent path from result + if (($segmentStart = $result.lastIndexOf('/')) >= $pathStart) //TODO + $result = substr($result,0, $segmentStart); + // Remove a trailing '/..' segment + if ($next !== '/') + return $result . '/' . substr($iri,$i + 1); + $segmentStart = $i + 1; + } + } + } + } + $next = $iri[++$i]; + } + return $result . substr($iri, $segmentStart); + }; + } + + // ## Public methods + + // ### `parse` parses the N3 input and emits each parsed triple through the callback + public function parse($input, $tripleCallback = null, $prefixCallback = null) { + $self = $this; + // The read callback is the next function to be executed when a token arrives. + // We start reading in the top context. + $this->readCallback = $this->readInTopContext; + $this->sparqlStyle = false; + $this->prefixes = []; + $this->prefixes["_"] = isset($this->blankNodePrefix)?$this->blankNodePrefix:'_:b' . $this->blankNodePrefix++ . '_'; + $this->prefixCallback = isset($prefixCallback)?$prefixCallback:function () {}; + $this->inversePredicate = false; + $this->quantified = []; + + // Parse synchronously if no triple callback is given + if (!isset($tripleCallback)) { + $triples = []; + $error= null; + $this->callback = function ($e, $t = null) use (&$triples, &$error) { + if (!$e && $t) { + array_push($triples,$t); + } else if (!$e) { + //DONE + } else { + $error = $e; + } + }; + $tokens = $this->lexer->tokenize($input); + foreach($tokens as $token) { + $this->readCallback = call_user_func($this->readCallback,$token); + } + if ($error) throw $error; + return $triples; + } + // Parse asynchronously otherwise, executing the read callback when a token arrives + $this->callback = $tripleCallback; + try { + $tokens = $this->lexer->tokenize($input); + foreach($tokens as $token) { + $this->readCallback = call_user_func($this->readCallback, $token); + } + } catch (Exception $e) { + call_user_func($this->callback,$error, null); + $this->callback = function () {}; + } + } + +} \ No newline at end of file diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 039db06..ed89dca 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -1,7 +1,6 @@ prefixMatch); + unset($this->prefixMatch); // Write the triple try { $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . (isset($graph) ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); @@ -156,7 +155,7 @@ private function encodeIriOrBlankNode ($entity) { // Try to represent the IRI as prefixed name preg_match($this->prefixRegex, $entity, $prefixMatch); if (!isset($prefixMatch[1]) && !isset($prefixMatch[2])) { - if (preg_match("/(.*?:)/",$entity,$match) && in_array($match[1], $this->prefixIRIs)) { + if (preg_match("/(.*?:)/",$entity,$match) && isset($this->prefixIRIs) && in_array($match[1], $this->prefixIRIs)) { return $entity; } else { return '<' . $entity . '>'; diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php new file mode 100644 index 0000000..604703f --- /dev/null +++ b/test/TriGParserTest.php @@ -0,0 +1,1943 @@ +shouldParse('' + /* no triples */); + + // ### should parse a whitespace string + $this->shouldParse(" \t \n " + /* no triples */); + + // ### should parse a single triple + $this->shouldParse(' .', + ['a', 'b', 'c']); + + // ### should parse three triples + $this->shouldParse(" .\n .\n .", + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i']); + + // ### should parse a triple with a literal + $this->shouldParse(' "string".', + ['a', 'b', '"string"']); + + // ### should parse a triple with a numeric literal + $this->shouldParse(' 3.0.', + ['a', 'b', '"3.0"^^http://www.w3.org/2001/XMLSchema#decimal']); + + // ### should parse a triple with an integer literal + $this->shouldParse(' 3.', + ['a', 'b', '"3"^^http://www.w3.org/2001/XMLSchema#integer']); + + // ### should parse a triple with a floating point literal + $this->shouldParse(' 1.3e2.', + ['a', 'b', '"1.3e2"^^http://www.w3.org/2001/XMLSchema#double']); + + // ### should parse a triple with a boolean literal + $this->shouldParse(' true.', + ['a', 'b', '"true"^^http://www.w3.org/2001/XMLSchema#boolean']); + + // ### should parse a triple with a literal and a language code + $this->shouldParse(' "string"@en.', + ['a', 'b', '"string"@en']); + + // ### should normalize language codes to lowercase + $this->shouldParse(' "string"@EN.', + ['a', 'b', '"string"@en']); + + // ### should parse a triple with a literal and an IRI type + $this->shouldParse(' "string"^^.', + ['a', 'b', '"string"^^type']); + + // ### should parse a triple with a literal and a prefixed name type + $this->shouldParse('@prefix x: . "string"^^x:z.', + ['a', 'b', '"string"^^y#z']); + + // ### should differentiate between IRI and prefixed name types + $this->shouldParse('@prefix : . :a :b "x"^^. :a :b "x"^^:urn:foo.', + ['noturn:a', 'noturn:b', '"x"^^urn:foo'], + ['noturn:a', 'noturn:b', '"x"^^noturn:urn:foo']); + + // ### should not parse a triple with a literal and a prefixed name type with an inexistent prefix +/* shouldNotParse(' "string"^^x:z.', + 'Undefined prefix "x:" on line 1.'); + */ + + // ### should parse a triple with the "a" shorthand predicate + $this->shouldParse(' a .', + ['a', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 't']); + + // ### should parse triples with prefixes + $this->shouldParse('@prefix : <#>.\n' + + '@prefix a: .\n' + + ':x a:a a:b.', + ['#x', 'a#a', 'a#b']); + + // ### should parse triples with the prefix "prefix" + $this->shouldParse('@prefix prefix: .' + + 'prefix:a prefix:b prefix:c.', + ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); + + // ### should parse triples with the prefix "base" + $this->shouldParse('PREFIX base: ' + + 'base:a base:b base:c.', + ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); + + // ### should parse triples with the prefix "graph" + $this->shouldParse('PREFIX graph: ' + + 'graph:a graph:b graph:c.', + ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); + + // ### should not parse @PREFIX + shouldNotParse('@PREFIX : <#>.', + 'Expected entity but got @PREFIX on line 1.'); + + // ### should parse triples with prefixes and different punctuation + $this->shouldParse('@prefix : <#>.\n' + + '@prefix a: .\n' + + ':x a:a a:b;a:c a:d,a:e.', + ['#x', 'a#a', 'a#b'], + ['#x', 'a#c', 'a#d'], + ['#x', 'a#c', 'a#e']); + + // ### should not parse undefined empty prefix in subject + shouldNotParse(':a ', + 'Undefined prefix ":" on line 1.'); + + // ### should not parse undefined prefix in subject + shouldNotParse('a:a ', + 'Undefined prefix "a:" on line 1.'); + + // ### should not parse undefined prefix in predicate + shouldNotParse(' b:c ', + 'Undefined prefix "b:" on line 1.'); + + // ### should not parse undefined prefix in object + shouldNotParse(' c:d ', + 'Undefined prefix "c:" on line 1.'); + + // ### should not parse undefined prefix in datatype + shouldNotParse(' "c"^^d:e ', + 'Undefined prefix "d:" on line 1.'); + + // ### should parse triples with SPARQL prefixes + $this->shouldParse('PREFIX : <#>\n' + + 'PrEfIX a: ' + + ':x a:a a:b.', + ['#x', 'a#a', 'a#b']); + + // ### should not parse prefix declarations without prefix + shouldNotParse('@prefix ', + 'Expected prefix to follow @prefix on line 1.'); + + // ### should not parse prefix declarations without IRI + shouldNotParse('@prefix : .', + 'Expected IRI to follow prefix ":" on line 1.'); + + // ### should not parse prefix declarations without a dot + shouldNotParse('@prefix : ;', + 'Expected declaration to end with a dot on line 1.'); + + // ### should parse statements with shared subjects + $this->shouldParse(' ;\n .', + ['a', 'b', 'c'], + ['a', 'd', 'e']); + + // ### should parse statements with shared subjects and trailing semicolon + $this->shouldParse(' ;\n ;\n.', + ['a', 'b', 'c'], + ['a', 'd', 'e']); + + // ### should parse statements with shared subjects and multiple semicolons + $this->shouldParse(' ;;\n .', + ['a', 'b', 'c'], + ['a', 'd', 'e']); + + // ### should parse statements with shared subjects and predicates + $this->shouldParse(' , .', + ['a', 'b', 'c'], + ['a', 'b', 'd']); + + // ### should parse diamonds + $this->shouldParse('<> <> <> <>.\n(<>) <> (<>) <>.', + ['', '', '', ''], + ['_:b0', '', '_:b1', ''], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', ''], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', ''], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with named blank nodes + $this->shouldParse('_:a _:c.', + ['_:b0_a', 'b', '_:b0_c']); + + // ### should not parse statements with blank predicates + shouldNotParse(' _:b .', + 'Disallowed blank node as predicate on line 1.'); + + // ### should parse statements with empty blank nodes + $this->shouldParse('[] [].', + ['_:b0', 'b', '_:b1']); + + // ### should parse statements with unnamed blank nodes in the subject + $this->shouldParse('[ ] .', + ['_:b0', 'c', 'd'], + ['_:b0', 'a', 'b']); + + // ### should parse statements with unnamed blank nodes in the object + $this->shouldParse(' [ ].', + ['a', 'b', '_:b0'], + ['_:b0', 'c', 'd']); + + // ### should parse statements with unnamed blank nodes with a string object + $this->shouldParse(' [ "x"].', + ['a', 'b', '_:b0'], + ['_:b0', 'c', '"x"']); + + // ### should not parse a blank node with missing subject + shouldNotParse(' [].', + 'Expected entity but got ] on line 1.'); + + // ### should not parse a blank node with only a semicolon + shouldNotParse(' [;].', + 'Unexpected ] on line 1.'); + + // ### should parse a blank node with a trailing semicolon + $this->shouldParse(' [ ; ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v']); + + // ### should parse a blank node with multiple trailing semicolons + $this->shouldParse(' [ ;;; ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v']); + + // ### should parse a multi-predicate blank node + $this->shouldParse(' [ ; ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'w', 'z']); + + // ### should parse a multi-predicate blank node with multiple semicolons + $this->shouldParse(' [ ;;; ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'w', 'z']); + + // ### should parse a multi-object blank node + $this->shouldParse(' [ , ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'u', 'z']); + + // ### should parse a multi-statement blank node ending with a literal + $this->shouldParse(' [ ; "z" ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'w', '"z"']); + + // ### should parse a multi-statement blank node ending with a typed literal + $this->shouldParse(' [ ; "z"^^ ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'w', '"z"^^t']); + + // ### should parse a multi-statement blank node ending with a string with language + $this->shouldParse(' [ ; "z"^^ ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'w', '"z"^^t']); + + // ### should parse a multi-statement blank node with trailing semicolon + $this->shouldParse(' [ ; ; ].', + ['a', 'b', '_:b0'], + ['_:b0', 'u', 'v'], + ['_:b0', 'w', 'z']); + + // ### should parse statements with nested blank nodes in the subject + $this->shouldParse('[ [ ]] .', + ['_:b0', 'c', 'd'], + ['_:b0', 'a', '_:b1'], + ['_:b1', 'x', 'y']); + + // ### should parse statements with nested blank nodes in the object + $this->shouldParse(' [ [ ]].', + ['a', 'b', '_:b0'], + ['_:b0', 'c', '_:b1'], + ['_:b1', 'd', 'e']); + + // ### should reuse identifiers of blank nodes within and outside of graphs + $this->shouldParse('_:a _:c. { _:a _:c }', + ['_:b0_a', 'b', '_:b0_c'], + ['_:b0_a', 'b', '_:b0_c', 'g']); + + } + + private function shouldParse($createParser, $input = "") + { + $expected = array_slice(func_get_args(),1); + // Shift parameters as necessary + if (is_callable($createParser)) + array_shift($expected); + else { + $input = $createParser; + $createParser = function () { + return new TriGParser(); + }; + } + $results = []; + $items = array_map(function ($item) { + return [ "subject" => $item[0], "predicate"=> $item[1], "object"=> $item[2], "graph"=> isset($item[3])?$item[3]:'' ]; + }, $expected); + $parser = $createParser(); + $parser->_resetBlankNodeIds(); + $parser->parse($input, function ($error, $triple = null) use (&$results, &$items){ + //expect($error).not.to.exist; + if ($triple) + array_push($results, $triple); + else + $this->assertEquals(self::toSortedJSON($items), self::toSortedJSON($results)); + }); + } + + private static function toSortedJSON ($items) + { + $triples = array_map("json_encode", $items); + sort($triples); + return '[\n ' . join('\n ', $triples) . '\n]'; + } +} +/* + + describe('An N3Parser instance', function () { + + // ### should not parse an invalid blank node + shouldNotParse('[ .', + 'Expected punctuation to follow "b" on line 1.')); + + // ### should parse a statements with only an anonymous node + shouldParse('[

].', + ['_:b0', 'p', 'o'])); + + // ### should not parse a statement with only a blank anonymous node + shouldNotParse('[].', + 'Unexpected . on line 1.')); + + // ### should not parse an anonymous node with only an anonymous node inside + shouldNotParse('[[

]].', + 'Expected entity but got [ on line 1.')); + + // ### should parse statements with an empty list in the subject + shouldParse('() .', + ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'a', 'b'])); + + // ### should parse statements with an empty list in the object + shouldParse(' ().', + ['a', 'b', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a single-element list in the subject + shouldParse('() .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a single-element list in the object + shouldParse(' ().', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse a list with a literal + shouldParse(' ("x").', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse a list with a typed literal + shouldParse(' ("x"^^).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"^^y'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse a list with a language-tagged literal + shouldParse(' ("x"@en-GB).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"@en-gb'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a multi-element list in the subject + shouldParse('( ) .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a multi-element list in the object + shouldParse(' ( ).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a multi-element literal list in the object + shouldParse(' ("x" "y"@en-GB "z"^^).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"y"@en-gb'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"z"^^t'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with prefixed names in lists + shouldParse('@prefix a: . (a:x a:y).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a#x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a#y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should not parse statements with undefined prefixes in lists + shouldNotParse(' (a:x a:y).', + 'Undefined prefix "a:" on line 1.')); + + // ### should parse statements with blank nodes in lists + shouldParse(' (_:x _:y).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a nested empty list + shouldParse(' ( ()).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with non-empty nested lists + shouldParse(' ( ()).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a list containing a blank node + shouldParse('([]) .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should parse statements with a list containing multiple blank nodes + shouldParse('([] [ ]) .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b3'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b3', 'x', 'y'])); + + // ### should parse statements with a blank node containing a list + shouldParse('[ ()] .', + ['_:b0', 'c', 'd'], + ['_:b0', 'a', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should not parse an invalid list + shouldNotParse(' (]).', + 'Expected entity but got ] on line 1.')); + + // ### should resolve IRIs against @base + shouldParse('@base .\n' + + ' .\n' + + '@base .\n' + + ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], + ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g'])); + + // ### should not resolve IRIs against @BASE + shouldNotParse('@BASE .', + 'Expected entity but got @BASE on line 1.')); + + // ### should resolve IRIs against SPARQL base + shouldParse('BASE \n' + + ' . ' + + 'BASE ' + + ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], + ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g'])); + + // ### should resolve IRIs against a @base with query string + shouldParse('@base .\n' + + '<> .\n' + + '@base .\n' + + '<> .', + ['http://ex.org/?foo', 'http://ex.org/b', 'http://ex.org/c'], + ['http://ex.org/d/?bar', 'http://ex.org/d/f', 'http://ex.org/d/g'])); + + // ### should resolve IRIs with query string against @base + shouldParse('@base .\n' + + ' .\n' + + '@base .\n' + + ' .' + + '@base .\n' + + '<> .', + ['http://ex.org/?', 'http://ex.org/?a', 'http://ex.org/?a=b'], + ['http://ex.org/d?', 'http://ex.org/d?a', 'http://ex.org/d?a=b'], + ['http://ex.org/d?e', 'http://ex.org/d?a', 'http://ex.org/d?a=b'])); + + // ### should not resolve IRIs with colons + shouldParse('@base .\n' + + ' .\n' + + ' .\n' + + ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], + ['A:', 'b:', 'c:'], + ['a:a', 'b:B', 'C-D:c'])); + + // ### should resolve datatype IRIs against @base + shouldParse('@base .\n' + + ' "c"^^.\n' + + '@base .\n' + + ' "g"^^.', + ['http://ex.org/a', 'http://ex.org/b', '"c"^^http://ex.org/d'], + ['http://ex.org/d/e', 'http://ex.org/d/f', '"g"^^http://ex.org/d/h'])); + + // ### should resolve IRIs against a base with a fragment + shouldParse('@base .\n' + + ' <#c>.\n', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/foo#c'])); + + // ### should resolve IRIs with an empty fragment + shouldParse('@base .\n' + + '<#> <#c>.\n', + ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c'])); + + // ### should not resolve prefixed names + shouldParse('PREFIX ex: \n' + + 'ex:a ex:b ex:c .', + ['http://ex.org/a/bb/ccc/../a', 'http://ex.org/a/bb/ccc/../b', 'http://ex.org/a/bb/ccc/../c'])); + + // ### should parse an empty default graph + shouldParse('{}')); + + // ### should parse a one-triple default graph ending without a dot + shouldParse('{ }', + ['a', 'b', 'c'])); + + // ### should parse a one-triple default graph ending with a dot + shouldParse('{ .}', + ['a', 'b', 'c'])); + + // ### should parse a three-triple default graph ending without a dot + shouldParse('{ ; ,}', + ['a', 'b', 'c'], + ['a', 'd', 'e'], + ['a', 'd', 'f'])); + + // ### should parse a three-triple default graph ending with a dot + shouldParse('{ ; ,.}', + ['a', 'b', 'c'], + ['a', 'd', 'e'], + ['a', 'd', 'f'])); + + // ### should parse a three-triple default graph ending with a semicolon + shouldParse('{ ; ,;}', + ['a', 'b', 'c'], + ['a', 'd', 'e'], + ['a', 'd', 'f'])); + + // ### should parse an empty named graph with an IRI + shouldParse('{}')); + + // ### should parse a one-triple named graph with an IRI ending without a dot + shouldParse(' { }', + ['a', 'b', 'c', 'g'])); + + // ### should parse a one-triple named graph with an IRI ending with a dot + shouldParse('{ .}', + ['a', 'b', 'c', 'g'])); + + // ### should parse a three-triple named graph with an IRI ending without a dot + shouldParse(' { ; ,}', + ['a', 'b', 'c', 'g'], + ['a', 'd', 'e', 'g'], + ['a', 'd', 'f', 'g'])); + + // ### should parse a three-triple named graph with an IRI ending with a dot + shouldParse('{ ; ,.}', + ['a', 'b', 'c', 'g'], + ['a', 'd', 'e', 'g'], + ['a', 'd', 'f', 'g'])); + + // ### should parse an empty named graph with a prefixed name + shouldParse('@prefix g: .\ng:h {}')); + + // ### should parse a one-triple named graph with a prefixed name ending without a dot + shouldParse('@prefix g: .\ng:h { }', + ['a', 'b', 'c', 'g#h'])); + + // ### should parse a one-triple named graph with a prefixed name ending with a dot + shouldParse('@prefix g: .\ng:h{ .}', + ['a', 'b', 'c', 'g#h'])); + + // ### should parse a three-triple named graph with a prefixed name ending without a dot + shouldParse('@prefix g: .\ng:h { ; ,}', + ['a', 'b', 'c', 'g#h'], + ['a', 'd', 'e', 'g#h'], + ['a', 'd', 'f', 'g#h'])); + + // ### should parse a three-triple named graph with a prefixed name ending with a dot + shouldParse('@prefix g: .\ng:h{ ; ,.}', + ['a', 'b', 'c', 'g#h'], + ['a', 'd', 'e', 'g#h'], + ['a', 'd', 'f', 'g#h'])); + + // ### should parse an empty anonymous graph + shouldParse('[] {}')); + + // ### should parse a one-triple anonymous graph ending without a dot + shouldParse('[] { }', + ['a', 'b', 'c', '_:b0'])); + + // ### should parse a one-triple anonymous graph ending with a dot + shouldParse('[]{ .}', + ['a', 'b', 'c', '_:b0'])); + + // ### should parse a three-triple anonymous graph ending without a dot + shouldParse('[] { ; ,}', + ['a', 'b', 'c', '_:b0'], + ['a', 'd', 'e', '_:b0'], + ['a', 'd', 'f', '_:b0'])); + + // ### should parse a three-triple anonymous graph ending with a dot + shouldParse('[]{ ; ,.}', + ['a', 'b', 'c', '_:b0'], + ['a', 'd', 'e', '_:b0'], + ['a', 'd', 'f', '_:b0'])); + + // ### should parse an empty named graph with an IRI and the GRAPH keyword + shouldParse('GRAPH {}')); + + // ### should parse an empty named graph with a prefixed name and the GRAPH keyword + shouldParse('@prefix g: .\nGRAPH g:h {}')); + + // ### should parse an empty anonymous graph and the GRAPH keyword + shouldParse('GRAPH [] {}')); + + // ### should parse a one-triple named graph with an IRI and the GRAPH keyword + shouldParse('GRAPH { }', + ['a', 'b', 'c', 'g'])); + + // ### should parse a one-triple named graph with a prefixed name and the GRAPH keyword + shouldParse('@prefix g: .\nGRAPH g:h { }', + ['a', 'b', 'c', 'g#h'])); + + // ### should parse a one-triple anonymous graph and the GRAPH keyword + shouldParse('GRAPH [] { }', + ['a', 'b', 'c', '_:b0'])); + + // ### should parse a graph with 8-bit unicode escape sequences + shouldParse('<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}\n', + ['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'])); + + // ### should not parse a single closing brace + shouldNotParse('}', + 'Unexpected graph closing on line 1.')); + + // ### should not parse a single opening brace + shouldNotParse('{', + 'Expected entity but got eof on line 1.')); + + // ### should not parse a superfluous closing brace + shouldNotParse('{}}', + 'Unexpected graph closing on line 1.')); + + // ### should not parse a graph with only a dot + shouldNotParse('{.}', + 'Expected entity but got . on line 1.')); + + // ### should not parse a graph with only a semicolon + shouldNotParse('{;}', + 'Expected entity but got ; on line 1.')); + + // ### should not parse an unclosed graph + shouldNotParse('{ .', + 'Unclosed graph on line 1.')); + + // ### should not parse a named graph with a list node as label + shouldNotParse('() {}', + 'Expected entity but got { on line 1.')); + + // ### should not parse a named graph with a non-empty blank node as label + shouldNotParse('[ ] {}', + 'Expected entity but got { on line 1.')); + + // ### should not parse a named graph with the GRAPH keyword and a non-empty blank node as label + shouldNotParse('GRAPH [ ] {}', + 'Invalid graph label on line 1.')); + + // ### should not parse a triple after the GRAPH keyword + shouldNotParse('GRAPH .', + 'Expected graph but got IRI on line 1.')); + + // ### should not parse repeated GRAPH keywords + shouldNotParse('GRAPH GRAPH {}', + 'Invalid graph label on line 1.')); + + // ### should parse a quad with 4 IRIs + shouldParse(' .', + ['a', 'b', 'c', 'g'])); + + // ### should parse a quad with 4 prefixed names + shouldParse('@prefix p: .\np:a p:b p:c p:g.', + ['p#a', 'p#b', 'p#c', 'p#g'])); + + // ### should not parse a quad with an undefined prefix + shouldNotParse(' p:g.', + 'Undefined prefix "p:" on line 1.')); + + // ### should parse a quad with 3 IRIs and a literal + shouldParse(' "c"^^ .', + ['a', 'b', '"c"^^d', 'g'])); + + // ### should parse a quad with 2 blank nodes and a literal + shouldParse('_:a "c"^^ _:g.', + ['_:b0_a', 'b', '"c"^^d', '_:b0_g'])); + + // ### should not parse a quad in a graph + shouldNotParse('{ .}', + 'Expected punctuation to follow "c" on line 1.')); + + // ### should not parse a quad with different punctuation + shouldNotParse(' ;', + 'Expected dot to follow quad on line 1.')); + + // ### should not parse base declarations without IRI + shouldNotParse('@base a: ', + 'Expected IRI to follow base declaration on line 1.')); + + // ### should not parse improperly nested parentheses and brackets + shouldNotParse(' [ (]).', + 'Expected entity but got ] on line 1.')); + + // ### should not parse improperly nested square brackets + shouldNotParse(' [ ]].', + 'Expected entity but got ] on line 1.')); + + // ### should error when an object is not there + shouldNotParse(' .', + 'Expected entity but got . on line 1.')); + + // ### should error when a dot is not there + shouldNotParse(' ', + 'Expected entity but got eof on line 1.')); + + // ### should error with an abbreviation in the subject + shouldNotParse('a .', + 'Expected entity but got abbreviation on line 1.')); + + // ### should error with an abbreviation in the object + shouldNotParse(' a .', + 'Expected entity but got abbreviation on line 1.')); + + // ### should error if punctuation follows a subject + shouldNotParse(' .', + 'Unexpected . on line 1.')); + + // ### should error if an unexpected token follows a subject + shouldNotParse(' [', + 'Expected entity but got [ on line 1.')); + + // ### should not error if there is no triple callback function () { + new N3Parser().parse(''); + }); + + // ### should return prefixes through a callback function (done) { + $prefixes = {}; + new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', + tripleCallback, prefixCallback); + + function tripleCallback($error, $triple) { + expect(error).not.to.exist; + if (!triple) { + Object.keys(prefixes).should.have.length(2); + expect(prefixes).to.have.property('a', 'IRIa'); + expect(prefixes).to.have.property('b', 'IRIb'); + done(); + } + } + + function prefixCallback(prefix, iri) { + expect(prefix).to.exist; + expect(iri).to.exist; + prefixes[prefix] = iri; + } + }); + + // ### should return prefixes through a callback without triple callback function (done) { + $prefixes = {}; + new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', + null, prefixCallback); + + function prefixCallback(prefix, iri) { + expect(prefix).to.exist; + expect(iri).to.exist; + prefixes[prefix] = iri; + if (Object.keys(prefixes).length === 2) + done(); + } + }); + + // ### should return prefixes at the last triple callback function (done) { + new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', tripleCallback); + + function tripleCallback($error, $triple, prefixes) { + expect(error).not.to.exist; + if (triple) + expect(prefixes).not.to.exist; + else { + expect(prefixes).to.exist; + Object.keys(prefixes).should.have.length(2); + expect(prefixes).to.have.property('a', 'IRIa'); + expect(prefixes).to.have.property('b', 'IRIb'); + done(); + } + } + }); + + // ### should parse a string synchronously if no callback is given function () { + $triples = new N3Parser().parse('@prefix a: . a:a a:b a:c.'); + triples.should.deep.equal([{ subject: 'urn:a:a', predicate: 'urn:a:b', object: 'urn:a:c', graph: '' }]); + }); + + // ### should throw on syntax errors if no callback is given function () { + (function () { new N3Parser().parse(' bar '); }) + .should.throw('Unexpected "bar" on line 1.'); + }); + + // ### should throw on grammar errors if no callback is given function () { + (function () { new N3Parser().parse(' '); }) + .should.throw('Expected punctuation to follow "c" on line 1.'); + }); + }); + + describe('An N3Parser instance with a document IRI', function () { + function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f.ttl' }); } + + // ### should resolve IRIs against the document IRI + shouldParse(parser, + '@prefix : <#>.\n' + + ' .\n' + + ':d :e :f :g.', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], + ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g'])); + + // ### should resolve IRIs with a trailing slash against the document IRI + shouldParse(parser, + ' .\n', + ['http://ex.org/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); + + // ### should resolve IRIs starting with ./ against the document IRI + shouldParse(parser, + '<./a> <./a/b> <./a/b/c>.\n', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); + + // ### should resolve IRIs starting with multiple ./ sequences against the document IRI + shouldParse(parser, + '<./././a> <./././././a/b> <././././././a/b/c>.\n', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); + + // ### should resolve IRIs starting with ../ against the document IRI + shouldParse(parser, + '<../a> <../a/b> <../a/b/c>.\n', + ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/yy/a/b/c'])); + + // ### should resolve IRIs starting multiple ../ sequences against the document IRI + shouldParse(parser, + '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.\n', + ['http://ex.org/x/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); + + // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI + shouldParse(parser, + '<.././a> <./.././a/b> <./.././.././a/b/c>.\n', + ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/a/b/c'])); + + // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI + shouldParse(parser, + '<.x/a> <..x/a/b> <.../a/b/c>.\n', + ['http://ex.org/x/yy/zzz/.x/a', 'http://ex.org/x/yy/zzz/..x/a/b', 'http://ex.org/x/yy/zzz/.../a/b/c'])); + + // ### should resolve datatype IRIs against the document IRI + shouldParse(parser, + ' "c"^^.', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', '"c"^^http://ex.org/x/yy/zzz/d'])); + + // ### should resolve IRIs in lists against the document IRI + shouldParse(parser, + '( )

( ).', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/b'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0', 'http://ex.org/x/yy/zzz/p', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/c'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/d'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); + + // ### should respect @base statements + shouldParse(parser, + ' .\n' + + '@base .\n' + + ' .\n' + + '@base .\n' + + ' .\n' + + '@base .\n' + + ' .', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c'], + ['http://ex.org/x/e', 'http://ex.org/x/f', 'http://ex.org/x/g'], + ['http://ex.org/x/d/h', 'http://ex.org/x/d/i', 'http://ex.org/x/d/j'], + ['http://ex.org/e/k', 'http://ex.org/e/l', 'http://ex.org/e/m'])); + }); + + describe('An N3Parser instance with a blank node prefix', function () { + function parser() { return new N3Parser({ blankNodePrefix: '_:blank' }); } + + // ### should use the given prefix for blank nodes + shouldParse(parser, + '_:a _:c.\n', + ['_:blanka', 'b', '_:blankc'])); + }); + + describe('An N3Parser instance with an empty blank node prefix', function () { + function parser() { return new N3Parser({ blankNodePrefix: '' }); } + + // ### should not use a prefix for blank nodes + shouldParse(parser, + '_:a _:c.\n', + ['_:a', 'b', '_:c'])); + }); + + describe('An N3Parser instance with a non-string format', function () { + function parser() { return new N3Parser({ format: 1 }); } + + // ### should parse a single triple + shouldParse(parser, ' .', ['a', 'b', 'c'])); + + // ### should parse a graph + shouldParse(parser, '{ }', ['a', 'b', 'c'])); + }); + + describe('An N3Parser instance for the Turtle format', function () { + function parser() { return new N3Parser({ format: 'Turtle' }); } + + // ### should parse a single triple + shouldParse(parser, ' .', ['a', 'b', 'c'])); + + // ### should not parse a default graph + shouldNotParse(parser, '{}', 'Unexpected graph on line 1.')); + + // ### should not parse a named graph + shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); + + // ### should not parse a named graph with the GRAPH keyword + shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); + + // ### should not parse a quad + shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + + // ### should not parse a variable + shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + + // ### should not parse an equality statement + shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + + // ### should not parse a right implication statement + shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + + // ### should not parse a left implication statement + shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + + // ### should not parse a formula as object + shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); + }); + + describe('An N3Parser instance for the TriG format', function () { + function parser() { return new N3Parser({ format: 'TriG' }); } + + // ### should parse a single triple + shouldParse(parser, ' .', ['a', 'b', 'c'])); + + // ### should parse a default graph + shouldParse(parser, '{}')); + + // ### should parse a named graph + shouldParse(parser, ' {}')); + + // ### should parse a named graph with the GRAPH keyword + shouldParse(parser, 'GRAPH {}')); + + // ### should not parse a quad + shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + + // ### should not parse a variable + shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + + // ### should not parse an equality statement + shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + + // ### should not parse a right implication statement + shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + + // ### should not parse a left implication statement + shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + + // ### should not parse a formula as object + shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); + }); + + describe('An N3Parser instance for the N-Triples format', function () { + function parser() { return new N3Parser({ format: 'N-Triples' }); } + + // ### should parse a single triple + shouldParse(parser, ' "c".', + ['http://ex.org/a', 'http://ex.org/b', '"c"'])); + + // ### should not parse a single quad + shouldNotParse(parser, ' "c" .', + 'Expected punctuation to follow ""c"" on line 1.')); + + // ### should not parse relative IRIs + shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); + + // ### should not parse a prefix declaration + shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); + + // ### should not parse a variable + shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + + // ### should not parse an equality statement + shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + + // ### should not parse a right implication statement + shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + + // ### should not parse a left implication statement + shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + + // ### should not parse a formula as object + shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); + }); + + describe('An N3Parser instance for the N-Quads format', function () { + function parser() { return new N3Parser({ format: 'N-Quads' }); } + + // ### should parse a single triple + shouldParse(parser, ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'])); + + // ### should parse a single quad + shouldParse(parser, ' "c" .', + ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g'])); + + // ### should not parse relative IRIs + shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); + + // ### should not parse a prefix declaration + shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); + + // ### should not parse a variable + shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + + // ### should not parse an equality statement + shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + + // ### should not parse a right implication statement + shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + + // ### should not parse a left implication statement + shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + + // ### should not parse a formula as object + shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); + }); + + describe('An N3Parser instance for the N3 format', function () { + function parser() { return new N3Parser({ format: 'N3' }); } + + // ### should parse a single triple + shouldParse(parser, ' .', ['a', 'b', 'c'])); + + // ### should not parse a default graph + shouldNotParse(parser, '{}', 'Expected entity but got eof on line 1.')); + + // ### should not parse a named graph + shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); + + // ### should not parse a named graph with the GRAPH keyword + shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); + + // ### should not parse a quad + shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + + // ### allows a blank node label in predicate position + shouldParse(parser, ' _:b .', ['a', '_:b0_b', 'c'])); + + // ### should parse a variable + shouldParse(parser, '?a ?b ?c.', ['?a', '?b', '?c'])); + + // ### should parse a simple equality + shouldParse(parser, ' = .', + ['a', 'http://www.w3.org/2002/07/owl#sameAs', 'b'])); + + // ### should parse a simple right implication + shouldParse(parser, ' => .', + ['a', 'http://www.w3.org/2000/10/swap/log#implies', 'b'])); + + // ### should parse a simple left implication + shouldParse(parser, ' <= .', + ['b', 'http://www.w3.org/2000/10/swap/log#implies', 'a'])); + + // ### should parse a right implication between one-triple graphs + shouldParse(parser, '{ ?a ?b . } => { ?a }.', + ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', '?a', '_:b1'])); + + // ### should parse a right implication between two-triple graphs + shouldParse(parser, '{ ?a ?b . . } => { ?a, }.', + ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', 'f', '_:b0'], + ['d', 'e', '?a', '_:b1'], + ['d', 'e', 'f', '_:b1'])); + + // ### should parse a left implication between one-triple graphs + shouldParse(parser, '{ ?a ?b . } <= { ?a }.', + ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', '?a', '_:b1'])); + + // ### should parse a left implication between two-triple graphs + shouldParse(parser, '{ ?a ?b . . } <= { ?a, }.', + ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', 'f', '_:b0'], + ['d', 'e', '?a', '_:b1'], + ['d', 'e', 'f', '_:b1'])); + + // ### should parse an equality of one-triple graphs + shouldParse(parser, '{ ?a ?b . } = { ?a }.', + ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', '?a', '_:b1'])); + + // ### should parse an equality of two-triple graphs + shouldParse(parser, '{ ?a ?b . . } = { ?a, }.', + ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', 'f', '_:b0'], + ['d', 'e', '?a', '_:b1'], + ['d', 'e', 'f', '_:b1'])); + + // ### should parse nested implication graphs + shouldParse(parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', + ['_:b3', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', '_:b0'], + ['?a', '?b', '?c', '_:b1'], + ['?d', '?e', '?f', '_:b2'], + ['_:b4', 'http://www.w3.org/2000/10/swap/log#implies', '_:b5', '_:b3'], + ['?g', '?h', '?i', '_:b4'], + ['?j', '?k', '?l', '_:b5'])); + + // ### should not reuse identifiers of blank nodes within and outside of formulas + shouldParse(parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', + ['_:b0_a', '_:b0_b', '_:b0_c'], + ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', ''], + ['_:b0.a', '_:b0.b', '_:b0.c', '_:b0'], + ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b3', '_:b1'], + ['_:b2.a', '_:b2.b', '_:b2.c', '_:b2'], + ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3'])); + + // ### should parse a @forSome statement + shouldParse(parser, '@forSome . .', + ['_:b0', '_:b0', '_:b0'])); + + // ### should parse a @forSome statement with multiple entities + shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + ['_:b0', '_:b1', '_:b2'])); + + // ### should not parse a @forSome statement with an invalid prefix + shouldNotParse(parser, '@forSome a:b.', + 'Undefined prefix "a:" on line 1.')); + + // ### should not parse a @forSome statement with a blank node + shouldNotParse(parser, '@forSome _:a.', + 'Unexpected blank on line 1.')); + + // ### should not parse a @forSome statement with a variable + shouldNotParse(parser, '@forSome ?a.', + 'Unexpected $on line 1.')); + + // ### should correctly scope @forSome statements + shouldParse(parser, '@forSome . { @forSome . . }. .', + ['_:b0', '_:b0', '_:b1'], + ['_:b2', '_:b2', '_:b2', '_:b1'], + ['_:b0', '_:b0', '_:b0'])); + + // ### should parse a @forAll statement + shouldParse(parser, '@forAll . .', + ['?b-0', '?b-0', '?b-0'])); + + // ### should parse a @forAll statement with multiple entities + shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + ['?b-0', '?b-1', '?b-2'])); + + // ### should not parse a @forAll statement with an invalid prefix + shouldNotParse(parser, '@forAll a:b.', + 'Undefined prefix "a:" on line 1.')); + + // ### should not parse a @forAll statement with a blank node + shouldNotParse(parser, '@forAll _:a.', + 'Unexpected blank on line 1.')); + + // ### should not parse a @forAll statement with a variable + shouldNotParse(parser, '@forAll ?a.', + 'Unexpected $on line 1.')); + + // ### should correctly scope @forAll statements + shouldParse(parser, '@forAll . { @forAll . . }. .', + ['?b-0', '?b-0', '_:b1'], + ['?b-2', '?b-2', '?b-2', '_:b1'], + ['?b-0', '?b-0', '?b-0'])); + + // ### should parse a ! path of length 2 as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + ':joe!fam:mother a fam:Person.', + ['ex:joe', 'f:mother', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + // ### should parse a ! path of length 4 as subject + shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + + ':joe!fam:mother!loc:office!loc:zip loc:code 1234.', + ['ex:joe', 'f:mother', '_:b0'], + ['_:b0', 'l:office', '_:b1'], + ['_:b1', 'l:zip', '_:b2'], + ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer'])); + + // ### should parse a ! path of length 2 as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' :joe!fam:mother.', + ['x', 'is', '_:b0'], + ['ex:joe', 'f:mother', '_:b0'])); + + // ### should parse a ! path of length 4 as object + shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + + ' :joe!fam:mother!loc:office!loc:zip.', + ['x', 'is', '_:b2'], + ['ex:joe', 'f:mother', '_:b0'], + ['_:b0', 'l:office', '_:b1'], + ['_:b1', 'l:zip', '_:b2'])); + + // ### should parse a ^ path of length 2 as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + ':joe^fam:son a fam:Person.', + ['_:b0', 'f:son', 'ex:joe'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + // ### should parse a ^ path of length 4 as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + ':joe^fam:son^fam:sister^fam:mother a fam:Person.', + ['_:b0', 'f:son', 'ex:joe'], + ['_:b1', 'f:sister', '_:b0'], + ['_:b2', 'f:mother', '_:b1'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + // ### should parse a ^ path of length 2 as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' :joe^fam:son.', + ['x', 'is', '_:b0'], + ['_:b0', 'f:son', 'ex:joe'])); + + // ### should parse a ^ path of length 4 as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' :joe^fam:son^fam:sister^fam:mother.', + ['x', 'is', '_:b2'], + ['_:b0', 'f:son', 'ex:joe'], + ['_:b1', 'f:sister', '_:b0'], + ['_:b2', 'f:mother', '_:b1'])); + + // ### should parse mixed !/^ paths as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + ':joe!fam:mother^fam:mother a fam:Person.', + ['ex:joe', 'f:mother', '_:b0'], + ['_:b1', 'f:mother', '_:b0'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + // ### should parse mixed !/^ paths as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' :joe!fam:mother^fam:mother.', + ['x', 'is', '_:b1'], + ['ex:joe', 'f:mother', '_:b0'], + ['_:b1', 'f:mother', '_:b0'])); + + // ### should parse a ! path in a blank node as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + '[fam:knows :joe!fam:mother] a fam:Person.', + ['_:b0', 'f:knows', '_:b1'], + ['ex:joe', 'f:mother', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + // ### should parse a ! path in a blank node as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' [fam:knows :joe!fam:mother].', + ['x', 'is', '_:b0'], + ['_:b0', 'f:knows', '_:b1'], + ['ex:joe', 'f:mother', '_:b1'])); + + // ### should parse a ^ path in a blank node as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + '[fam:knows :joe^fam:son] a fam:Person.', + ['_:b0', 'f:knows', '_:b1'], + ['_:b1', 'f:son', 'ex:joe'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + // ### should parse a ^ path in a blank node as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' [fam:knows :joe^fam:son].', + ['x', 'is', '_:b0'], + ['_:b0', 'f:knows', '_:b1'], + ['_:b1', 'f:son', 'ex:joe'])); + + // ### should parse a ! path in a list as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + '( :joe!fam:mother ) a :List.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['ex:joe', 'f:mother', '_:b2'])); + + // ### should parse a ! path in a list as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' ( :joe!fam:mother ).', + ['l', 'is', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['ex:joe', 'f:mother', '_:b2'])); + + // ### should parse a ^ path in a list as subject + shouldParse(parser, '@prefix : . @prefix fam: .' + + '( :joe^fam:son ) a :List.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b2', 'f:son', 'ex:joe'])); + + // ### should parse a ^ path in a list as object + shouldParse(parser, '@prefix : . @prefix fam: .' + + ' ( :joe^fam:son ).', + ['l', 'is', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b2', 'f:son', 'ex:joe'])); + + // ### should not parse an invalid ! path + shouldNotParse(parser, '!"invalid" ', 'Expected entity but got literal on line 1.')); + + // ### should not parse an invalid ^ path + shouldNotParse(parser, '^"invalid" ', 'Expected entity but got literal on line 1.')); + }); + + describe('An N3Parser instance for the N3 format with the explicitQuantifiers option', function () { + function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: true }); } + + // ### should parse a @forSome statement + shouldParse(parser, '@forSome . .', + ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x'])); + + // ### should parse a @forSome statement with multiple entities + shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b:y', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['a:x', 'b:y', 'a:z'])); + + // ### should correctly scope @forSome statements + shouldParse(parser, '@forSome . { @forSome . . }. .', + ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', '_:b1'], + ['_:b1', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x', '_:b1'], + ['x', 'x', 'x'])); + + // ### should parse a @forAll statement + shouldParse(parser, '@forAll . .', + ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x'])); + + // ### should parse a @forAll statement with multiple entities + shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b:y', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['a:x', 'b:y', 'a:z'])); + + // ### should correctly scope @forAll statements + shouldParse(parser, '@forAll . { @forAll . . }. .', + ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', '_:b1'], + ['_:b1', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x', '_:b1'], + ['x', 'x', 'x'])); + }); + + describe('IRI resolution', function () { + describe('RFC3986 normal examples', function () { + itShouldResolve('http://a/bb/ccc/d;p?q', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', './g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g/', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/d;p?q', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/d;p?q', '?y', 'http://a/bb/ccc/d;p?y'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y', 'http://a/bb/ccc/g?y'); + itShouldResolve('http://a/bb/ccc/d;p?q', '#s', 'http://a/bb/ccc/d;p?q#s'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s', 'http://a/bb/ccc/g#s'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + itShouldResolve('http://a/bb/ccc/d;p?q', ';x', 'http://a/bb/ccc/;x'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x', 'http://a/bb/ccc/g;x'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/d;p?q', '', 'http://a/bb/ccc/d;p?q'); + itShouldResolve('http://a/bb/ccc/d;p?q', '.', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/d;p?q', './', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/d;p?q', '..', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/d;p?q', '../', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/d;p?q', '../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', '../..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/d;p?q', '../../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/d;p?q', '../../g', 'http://a/g'); + }); + + describe('RFC3986 abnormal examples', function () { + itShouldResolve('http://a/bb/ccc/d;p?q', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g.', 'http://a/bb/ccc/g.'); + itShouldResolve('http://a/bb/ccc/d;p?q', '.g', 'http://a/bb/ccc/.g'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g..', 'http://a/bb/ccc/g..'); + itShouldResolve('http://a/bb/ccc/d;p?q', '..g', 'http://a/bb/ccc/..g'); + itShouldResolve('http://a/bb/ccc/d;p?q', './../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/d;p?q', './g/.', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g/./h', 'http://a/bb/ccc/g/h'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g/../h', 'http://a/bb/ccc/h'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x=1/../y', 'http://a/bb/ccc/y'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + itShouldResolve('http://a/bb/ccc/d;p?q', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with trailing slash in base IRI', function () { + itShouldResolve('http://a/bb/ccc/d/', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/d/', 'g', 'http://a/bb/ccc/d/g'); + itShouldResolve('http://a/bb/ccc/d/', './g', 'http://a/bb/ccc/d/g'); + itShouldResolve('http://a/bb/ccc/d/', 'g/', 'http://a/bb/ccc/d/g/'); + itShouldResolve('http://a/bb/ccc/d/', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d/', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/d/', '?y', 'http://a/bb/ccc/d/?y'); + itShouldResolve('http://a/bb/ccc/d/', 'g?y', 'http://a/bb/ccc/d/g?y'); + itShouldResolve('http://a/bb/ccc/d/', '#s', 'http://a/bb/ccc/d/#s'); + itShouldResolve('http://a/bb/ccc/d/', 'g#s', 'http://a/bb/ccc/d/g#s'); + itShouldResolve('http://a/bb/ccc/d/', 'g?y#s', 'http://a/bb/ccc/d/g?y#s'); + itShouldResolve('http://a/bb/ccc/d/', ';x', 'http://a/bb/ccc/d/;x'); + itShouldResolve('http://a/bb/ccc/d/', 'g;x', 'http://a/bb/ccc/d/g;x'); + itShouldResolve('http://a/bb/ccc/d/', 'g;x?y#s', 'http://a/bb/ccc/d/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/d/', '', 'http://a/bb/ccc/d/'); + itShouldResolve('http://a/bb/ccc/d/', '.', 'http://a/bb/ccc/d/'); + itShouldResolve('http://a/bb/ccc/d/', './', 'http://a/bb/ccc/d/'); + itShouldResolve('http://a/bb/ccc/d/', '..', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/d/', '../', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/d/', '../g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/d/', '../..', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/d/', '../../', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/d/', '../../g', 'http://a/bb/g'); + }); + + describe('RFC3986 abnormal examples with trailing slash in base IRI', function () { + itShouldResolve('http://a/bb/ccc/d/', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d/', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d/', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d/', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d/', 'g.', 'http://a/bb/ccc/d/g.'); + itShouldResolve('http://a/bb/ccc/d/', '.g', 'http://a/bb/ccc/d/.g'); + itShouldResolve('http://a/bb/ccc/d/', 'g..', 'http://a/bb/ccc/d/g..'); + itShouldResolve('http://a/bb/ccc/d/', '..g', 'http://a/bb/ccc/d/..g'); + itShouldResolve('http://a/bb/ccc/d/', './../g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/d/', './g/.', 'http://a/bb/ccc/d/g/'); + itShouldResolve('http://a/bb/ccc/d/', 'g/./h', 'http://a/bb/ccc/d/g/h'); + itShouldResolve('http://a/bb/ccc/d/', 'g/../h', 'http://a/bb/ccc/d/h'); + itShouldResolve('http://a/bb/ccc/d/', 'g;x=1/./y', 'http://a/bb/ccc/d/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/d/', 'g;x=1/../y', 'http://a/bb/ccc/d/y'); + itShouldResolve('http://a/bb/ccc/d/', 'g?y/./x', 'http://a/bb/ccc/d/g?y/./x'); + itShouldResolve('http://a/bb/ccc/d/', 'g?y/../x', 'http://a/bb/ccc/d/g?y/../x'); + itShouldResolve('http://a/bb/ccc/d/', 'g#s/./x', 'http://a/bb/ccc/d/g#s/./x'); + itShouldResolve('http://a/bb/ccc/d/', 'g#s/../x', 'http://a/bb/ccc/d/g#s/../x'); + itShouldResolve('http://a/bb/ccc/d/', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with /. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', './g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '?y', 'http://a/bb/ccc/./d;p?y'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y', 'http://a/bb/ccc/g?y'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '#s', 'http://a/bb/ccc/./d;p?q#s'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s', 'http://a/bb/ccc/g#s'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + itShouldResolve('http://a/bb/ccc/./d;p?q', ';x', 'http://a/bb/ccc/;x'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x', 'http://a/bb/ccc/g;x'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '', 'http://a/bb/ccc/./d;p?q'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '.', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', './', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '..', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '../', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '../..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '../../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '../../g', 'http://a/g'); + }); + + describe('RFC3986 abnormal examples with /. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/./d;p?q', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g.', 'http://a/bb/ccc/g.'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '.g', 'http://a/bb/ccc/.g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g..', 'http://a/bb/ccc/g..'); + itShouldResolve('http://a/bb/ccc/./d;p?q', '..g', 'http://a/bb/ccc/..g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', './../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/./d;p?q', './g/.', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/./h', 'http://a/bb/ccc/g/h'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/../h', 'http://a/bb/ccc/h'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x=1/../y', 'http://a/bb/ccc/y'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + itShouldResolve('http://a/bb/ccc/./d;p?q', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with /.. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', './g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/', 'http://a/bb/g/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '?y', 'http://a/bb/ccc/../d;p?y'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y', 'http://a/bb/g?y'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '#s', 'http://a/bb/ccc/../d;p?q#s'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s', 'http://a/bb/g#s'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y#s', 'http://a/bb/g?y#s'); + itShouldResolve('http://a/bb/ccc/../d;p?q', ';x', 'http://a/bb/;x'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x', 'http://a/bb/g;x'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x?y#s', 'http://a/bb/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '', 'http://a/bb/ccc/../d;p?q'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '.', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', './', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '../..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '../../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '../../g', 'http://a/g'); + }); + + describe('RFC3986 abnormal examples with /.. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/../d;p?q', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g.', 'http://a/bb/g.'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '.g', 'http://a/bb/.g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g..', 'http://a/bb/g..'); + itShouldResolve('http://a/bb/ccc/../d;p?q', '..g', 'http://a/bb/..g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', './../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/../d;p?q', './g/.', 'http://a/bb/g/'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/./h', 'http://a/bb/g/h'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/../h', 'http://a/bb/h'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x=1/./y', 'http://a/bb/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x=1/../y', 'http://a/bb/y'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y/./x', 'http://a/bb/g?y/./x'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y/../x', 'http://a/bb/g?y/../x'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s/./x', 'http://a/bb/g#s/./x'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s/../x', 'http://a/bb/g#s/../x'); + itShouldResolve('http://a/bb/ccc/../d;p?q', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with trailing /. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/.', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/.', 'g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/.', './g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/.', 'g/', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/.', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/.', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/.', '?y', 'http://a/bb/ccc/.?y'); + itShouldResolve('http://a/bb/ccc/.', 'g?y', 'http://a/bb/ccc/g?y'); + itShouldResolve('http://a/bb/ccc/.', '#s', 'http://a/bb/ccc/.#s'); + itShouldResolve('http://a/bb/ccc/.', 'g#s', 'http://a/bb/ccc/g#s'); + itShouldResolve('http://a/bb/ccc/.', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + itShouldResolve('http://a/bb/ccc/.', ';x', 'http://a/bb/ccc/;x'); + itShouldResolve('http://a/bb/ccc/.', 'g;x', 'http://a/bb/ccc/g;x'); + itShouldResolve('http://a/bb/ccc/.', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/.', '', 'http://a/bb/ccc/.'); + itShouldResolve('http://a/bb/ccc/.', '.', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/.', './', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/.', '..', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/.', '../', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/.', '../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/.', '../..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/.', '../../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/.', '../../g', 'http://a/g'); + }); + + describe('RFC3986 abnormal examples with trailing /. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/.', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/.', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/.', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/.', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/.', 'g.', 'http://a/bb/ccc/g.'); + itShouldResolve('http://a/bb/ccc/.', '.g', 'http://a/bb/ccc/.g'); + itShouldResolve('http://a/bb/ccc/.', 'g..', 'http://a/bb/ccc/g..'); + itShouldResolve('http://a/bb/ccc/.', '..g', 'http://a/bb/ccc/..g'); + itShouldResolve('http://a/bb/ccc/.', './../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/.', './g/.', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/.', 'g/./h', 'http://a/bb/ccc/g/h'); + itShouldResolve('http://a/bb/ccc/.', 'g/../h', 'http://a/bb/ccc/h'); + itShouldResolve('http://a/bb/ccc/.', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/.', 'g;x=1/../y', 'http://a/bb/ccc/y'); + itShouldResolve('http://a/bb/ccc/.', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + itShouldResolve('http://a/bb/ccc/.', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + itShouldResolve('http://a/bb/ccc/.', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + itShouldResolve('http://a/bb/ccc/.', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + itShouldResolve('http://a/bb/ccc/.', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with trailing /.. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/..', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/..', 'g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/..', './g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/..', 'g/', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/..', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/..', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/..', '?y', 'http://a/bb/ccc/..?y'); + itShouldResolve('http://a/bb/ccc/..', 'g?y', 'http://a/bb/ccc/g?y'); + itShouldResolve('http://a/bb/ccc/..', '#s', 'http://a/bb/ccc/..#s'); + itShouldResolve('http://a/bb/ccc/..', 'g#s', 'http://a/bb/ccc/g#s'); + itShouldResolve('http://a/bb/ccc/..', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + itShouldResolve('http://a/bb/ccc/..', ';x', 'http://a/bb/ccc/;x'); + itShouldResolve('http://a/bb/ccc/..', 'g;x', 'http://a/bb/ccc/g;x'); + itShouldResolve('http://a/bb/ccc/..', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/..', '', 'http://a/bb/ccc/..'); + itShouldResolve('http://a/bb/ccc/..', '.', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/..', './', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/..', '..', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/..', '../', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/..', '../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/..', '../..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/..', '../../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/..', '../../g', 'http://a/g'); + }); + + describe('RFC3986 abnormal examples with trailing /.. in the base IRI', function () { + itShouldResolve('http://a/bb/ccc/..', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/..', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/..', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/..', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/..', 'g.', 'http://a/bb/ccc/g.'); + itShouldResolve('http://a/bb/ccc/..', '.g', 'http://a/bb/ccc/.g'); + itShouldResolve('http://a/bb/ccc/..', 'g..', 'http://a/bb/ccc/g..'); + itShouldResolve('http://a/bb/ccc/..', '..g', 'http://a/bb/ccc/..g'); + itShouldResolve('http://a/bb/ccc/..', './../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/..', './g/.', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/..', 'g/./h', 'http://a/bb/ccc/g/h'); + itShouldResolve('http://a/bb/ccc/..', 'g/../h', 'http://a/bb/ccc/h'); + itShouldResolve('http://a/bb/ccc/..', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/..', 'g;x=1/../y', 'http://a/bb/ccc/y'); + itShouldResolve('http://a/bb/ccc/..', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + itShouldResolve('http://a/bb/ccc/..', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + itShouldResolve('http://a/bb/ccc/..', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + itShouldResolve('http://a/bb/ccc/..', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + itShouldResolve('http://a/bb/ccc/..', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with fragment in base IRI', function () { + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g:h', 'g:h'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', './g', 'http://a/bb/ccc/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '/g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '//g', 'http://g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '?y', 'http://a/bb/ccc/d;p?y'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y', 'http://a/bb/ccc/g?y'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '#s', 'http://a/bb/ccc/d;p?q#s'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s', 'http://a/bb/ccc/g#s'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', ';x', 'http://a/bb/ccc/;x'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x', 'http://a/bb/ccc/g;x'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '', 'http://a/bb/ccc/d;p?q'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '.', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', './', 'http://a/bb/ccc/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '..', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../', 'http://a/bb/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../..', 'http://a/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../', 'http://a/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../g', 'http://a/g'); + }); + + describe('RFC3986 abnormal examples with fragment in base IRI', function () { + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../../../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '/./g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '/../g', 'http://a/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g.', 'http://a/bb/ccc/g.'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '.g', 'http://a/bb/ccc/.g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g..', 'http://a/bb/ccc/g..'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', '..g', 'http://a/bb/ccc/..g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', './../g', 'http://a/bb/g'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', './g/.', 'http://a/bb/ccc/g/'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/./h', 'http://a/bb/ccc/g/h'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/../h', 'http://a/bb/ccc/h'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x=1/../y', 'http://a/bb/ccc/y'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + itShouldResolve('http://a/bb/ccc/d;p?q#f', 'http:g', 'http:g'); + }); + + describe('RFC3986 normal examples with file path', function () { + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g:h', 'g:h'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g', 'file:///a/bb/ccc/g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', './g', 'file:///a/bb/ccc/g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/', 'file:///a/bb/ccc/g/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '/g', 'file:///g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '//g', 'file://g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '?y', 'file:///a/bb/ccc/d;p?y'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y', 'file:///a/bb/ccc/g?y'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '#s', 'file:///a/bb/ccc/d;p?q#s'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s', 'file:///a/bb/ccc/g#s'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y#s', 'file:///a/bb/ccc/g?y#s'); + itShouldResolve('file:///a/bb/ccc/d;p?q', ';x', 'file:///a/bb/ccc/;x'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x', 'file:///a/bb/ccc/g;x'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x?y#s', 'file:///a/bb/ccc/g;x?y#s'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '', 'file:///a/bb/ccc/d;p?q'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '.', 'file:///a/bb/ccc/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', './', 'file:///a/bb/ccc/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '..', 'file:///a/bb/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '../', 'file:///a/bb/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '../g', 'file:///a/bb/g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '../..', 'file:///a/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '../../', 'file:///a/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '../../g', 'file:///a/g'); + }); + + describe('RFC3986 abnormal examples with file path', function () { + itShouldResolve('file:///a/bb/ccc/d;p?q', '../../../g', 'file:///g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '../../../../g', 'file:///g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '/./g', 'file:///g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '/../g', 'file:///g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g.', 'file:///a/bb/ccc/g.'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '.g', 'file:///a/bb/ccc/.g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g..', 'file:///a/bb/ccc/g..'); + itShouldResolve('file:///a/bb/ccc/d;p?q', '..g', 'file:///a/bb/ccc/..g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', './../g', 'file:///a/bb/g'); + itShouldResolve('file:///a/bb/ccc/d;p?q', './g/.', 'file:///a/bb/ccc/g/'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/./h', 'file:///a/bb/ccc/g/h'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/../h', 'file:///a/bb/ccc/h'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x=1/./y', 'file:///a/bb/ccc/g;x=1/y'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x=1/../y', 'file:///a/bb/ccc/y'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y/./x', 'file:///a/bb/ccc/g?y/./x'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y/../x', 'file:///a/bb/ccc/g?y/../x'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s/./x', 'file:///a/bb/ccc/g#s/./x'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s/../x', 'file:///a/bb/ccc/g#s/../x'); + itShouldResolve('file:///a/bb/ccc/d;p?q', 'http:g', 'http:g'); + }); + + describe('additional cases', function () { + // relative paths ending with '.' + itShouldResolve('http://abc/', '.', 'http://abc/'); + itShouldResolve('http://abc/def/ghi', '.', 'http://abc/def/'); + itShouldResolve('http://abc/def/ghi', '.?a=b', 'http://abc/def/?a=b'); + itShouldResolve('http://abc/def/ghi', '.#a=b', 'http://abc/def/#a=b'); + + // relative paths ending with '..' + itShouldResolve('http://abc/', '..', 'http://abc/'); + itShouldResolve('http://abc/def/ghi', '..', 'http://abc/'); + itShouldResolve('http://abc/def/ghi', '..?a=b', 'http://abc/?a=b'); + itShouldResolve('http://abc/def/ghi', '..#a=b', 'http://abc/#a=b'); + + // base path with empty subpaths (double slashes) + itShouldResolve('http://ab//de//ghi', 'xyz', 'http://ab//de//xyz'); + itShouldResolve('http://ab//de//ghi', './xyz', 'http://ab//de//xyz'); + itShouldResolve('http://ab//de//ghi', '../xyz', 'http://ab//de/xyz'); + + // base path with colon (possible confusion with scheme) + itShouldResolve('http://abc/d:f/ghi', 'xyz', 'http://abc/d:f/xyz'); + itShouldResolve('http://abc/d:f/ghi', './xyz', 'http://abc/d:f/xyz'); + itShouldResolve('http://abc/d:f/ghi', '../xyz', 'http://abc/xyz'); + + // base path consisting of '..' and/or '../' sequences + itShouldResolve('./', 'abc', '/abc'); + itShouldResolve('../', 'abc', '/abc'); + itShouldResolve('./././', '././abc', '/abc'); + itShouldResolve('../../../', '../../abc', '/abc'); + itShouldResolve('.../././', '././abc', '.../abc'); + + // base path without authority + itShouldResolve('a:b:c/', 'def/../', 'a:b:c/'); + itShouldResolve('a:b:c', '/def', 'a:/def'); + itShouldResolve('a:b/c', '/def', 'a:/def'); + itShouldResolve('a:', '/.', 'a:/'); + itShouldResolve('a:', '/..', 'a:/'); + + // base path with slashes in query string + itShouldResolve('http://abc/def/ghi?q=xx/yyy/z', 'jjj', 'http://abc/def/jjj'); + itShouldResolve('http://abc/def/ghi?q=xx/y?y/z', 'jjj', 'http://abc/def/jjj'); + }); + }); +}); + +function shouldParse(createParser, input) { + $expected = Array.prototype.slice.call(arguments, 1); + // Shift parameters as necessary + if (createParser.call) + expected.shift(); + else + input = createParser, createParser = N3Parser; + + return function (done) { + $results = []; + $items = expected.map(function (item) { + return { subject: item[0], predicate: item[1], object: item[2], graph: item[3] || '' }; + }); + N3Parser._resetBlankNodeIds(); + createParser().parse(input, function ($error, $triple) { + expect(error).not.to.exist; + if (triple) + results.push(triple); + else + toSortedJSON(results).should.equal(toSortedJSON(items)), done(); + }); + }; +} + + +function shouldNotParse(createParser, input, expectedError) { + // Shift parameters if necessary + if (!createParser.call) + expectedError = input, input = createParser, createParser = N3Parser; + + return function (done) { + createParser().parse(input, function ($error, $triple) { + if (error) { + expect(triple).not.to.exist; + error.should.be.an.instanceof(Error); + error.message.should.eql(expectedError); + done(); + } + else if (!triple) + throw new Error('Expected error ' + expectedError); + }); + }; +} + +function itShouldResolve(baseIri, relativeIri, expected) { + $result; + describe('resolving <' + relativeIri + '> against <' + baseIri + '>', function () { + before(function (done) { + try { + $doc = ' <' + relativeIri + '>.'; + new N3Parser({ documentIRI: baseIri }).parse(doc, function ($error, $triple) { + if (done) + result = triple, done(error); + done = null; + }); + } + catch (error) { done(error); } + }); + it('should result in ' + expected, function () { + expect(result.object).to.equal(expected); + }); + }); +} +*/ \ No newline at end of file From 1b71b999f5fa5e0d878b6d90204d972efc766bcc Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 5 Apr 2017 23:11:31 +0200 Subject: [PATCH 15/72] Line number mode --- src/N3Lexer.php | 9 +++++---- src/TriGParser.php | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 5e052d9..1398fd8 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -27,7 +27,7 @@ class N3Lexer public function __construct($options = []) { // In line mode (N-Triples or N-Quads), only simple features may be parsed - if (isset($options["lineMode"])) { + if ($options["lineMode"]) { // Don't tokenize special literals $this->tripleQuotedString = '/$0^/'; $this->number = '/$0^/'; @@ -52,7 +52,7 @@ public function __construct($options = []) { // ## Regular expressions private $iri ='/^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/'; // IRI with escape sequences; needs sanity check after unescaping private $unescapedIri = '/^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/'; // IRI without escape sequences; no unescaping - private $unescapedString= '/^"[^"\\]+"(?=[^"\\])/'; // non-empty string without escape sequences + private $unescapedString= '/^"[^"\\\]+"(?=[^"\\\])/'; // non-empty string without escape sequences private $singleQuotedString= '/^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^\'[^\'\\]*(?:\\.[^\'\\]*)*\'(?=[^\'\\])/'; private $tripleQuotedString = '/^""("[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*")""|^\'\'(\'[^\'\\]*(?:(?:\\.|\'(?!\'\'))[^\'\\]*)*\')\'\'/'; private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i'; @@ -64,7 +64,8 @@ public function __construct($options = []) { private $variable = '/^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?=[.,;!\^\s#()\[\]\{\}"\'<])/'; private $blank = '/^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?:[ \t]+|(?=\.?[,;:\s#()\[\]\{\}"\'<]))/'; - private $number = '/^[\-+]?(?:\d+\.?\d*([eE](?:[\-\+])?\d+)|\d*\.?\d+)(?=[.,;:\s#()\[\]\{\}"\'<])/'; + //TODO: this doesn't work + private $number = "/^[\-+]?(?:\d+\.?\d*([eE](?:[\-\+])?\d+)|\d*\.?\d+)(?=[.,;:\s#()\[\]\{\}\"'<])/"; private $boolean = '/^(?:true|false)(?=[.,;\s#()\[\]\{\}"\'<])/'; private $keyword = '/^@[a-z]+(?=[\s#<])/i'; private $sparqlKeyword= '/^(?:PREFIX|BASE|GRAPH)(?=[\s#<])/i'; @@ -255,7 +256,7 @@ private function tokenizeToEnd($callback, $inputFinished) { // Try to find a number if (preg_match($this->number, $input, $match)) { $type = 'literal'; - $value = '"' . $match[0] . '"^^http://www.w3.org/2001/XMLSchema#' . ($match[1] ? 'double' : (preg_match("/^[+\-]?\d+$/",$match[0]) ? 'integer' : 'decimal')); + $value = '"' . $match[0] . '"^^http://www.w3.org/2001/XMLSchema#' . (isset($match[1]) ? 'double' : (preg_match("/^[+\-]?\d+$/",$match[0]) ? 'integer' : 'decimal')); } break; case 'B': diff --git a/src/TriGParser.php b/src/TriGParser.php index 09d4d36..cdd953f 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -481,12 +481,12 @@ private function initReaders () // ### `_readDataTypeOrLang` reads an _optional_ data type or language $this->readDataTypeOrLang = function ($token) { - return $this->completeLiteral($token, false); + return call_user_func($this->completeLiteral,$token, false); }; // ### `_readListItemDataTypeOrLang` reads an _optional_ data type or language in a list $this->readListItemDataTypeOrLang = function ($token) { - return $this->completeLiteral($token, true); + return call_user_func($this->completeLiteral,$token, true); }; // ### `_completeLiteral` completes the object with a data type or language @@ -497,7 +497,7 @@ private function initReaders () case 'type': case 'typeIRI': $suffix = true; - $this->object .= '^^' . $this->readEntity($token); + $this->object .= '^^' . call_user_func($this->readEntity,$token); break; // Add an "@lang" suffix for language tags case 'langcode': @@ -508,30 +508,30 @@ private function initReaders () // If this literal was part of a list, write the item // (we could also check the context stack, but passing in a flag is faster) if ($listItem) - $this->triple($this->subject, self::RDF_FIRST, $this->object, $this->graph); + call_user_func($this->triple,$this->subject, self::RDF_FIRST, $this->object, $this->graph); // Continue with the rest of the input if ($suffix) - return $this->getContextEndReader(); + return call_user_func($this->getContextEndReader); else { - $this->readCallback = $this->getContextEndReader(); - return $this->readCallback($token); + $this->readCallback = call_user_func($this->getContextEndReader); + return call_user_func($this->readCallback,$token); } }; // ### `_readFormulaTail` reads the end of a formula $this->readFormulaTail = function ($token) { if ($token["type"] !== '}') - return $this->readPunctuation($token); + return call_user_func($this->readPunctuation,$token); // Store the last triple of the formula if ($this->subject !== null) - $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + call_user_func($this->triple,$this->subject, $this->predicate, $this->object, $this->graph); // Restore the parent context containing this formula $this->restoreContext(); // If the formula was the subject, continue reading the predicate. // If the formula was the object, read punctuation. - return $this->object === null ? $this->readPredicate : $this->getContextEndReader(); + return $this->object === null ? $this->readPredicate : call_user_func($this->getContextEndReader); }; // ### `_readPunctuation` reads punctuation between triples or triple parts @@ -951,7 +951,7 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { // Parse asynchronously otherwise, executing the read callback when a token arrives $this->callback = $tripleCallback; try { - $tokens = $this->lexer->tokenize($input); + $tokens = $this->lexer->tokenize($input); foreach($tokens as $token) { $this->readCallback = call_user_func($this->readCallback, $token); } From 9c313f60804ee6a32ec52cf1acf4178d6e698956 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 5 Apr 2017 23:24:35 +0200 Subject: [PATCH 16/72] Async callbacks fixes --- src/N3Lexer.php | 10 +++++----- src/TriGParser.php | 4 ++-- test/TriGParserTest.php | 14 +++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 1398fd8..8420174 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -9,7 +9,7 @@ class N3Lexer // Regular expression and replacement string to escape N3 strings. // Note how we catch invalid unicode sequences separately (they will trigger an error). - //private $escapeSequence = "/\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{8})|\\[uU]|\\(.)/g"; + private $escapeSequence = '/\\[uU]|\\\(.)/'; private $escapeReplacements = [ '\\' => '\\', "'"=> "'", '"' => '"', 'n' => '\n', 'r' => '\r', 't' => '\t', 'f' => '\f', 'b' => '\b', @@ -56,14 +56,14 @@ public function __construct($options = []) { private $singleQuotedString= '/^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^\'[^\'\\]*(?:\\.[^\'\\]*)*\'(?=[^\'\\])/'; private $tripleQuotedString = '/^""("[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*")""|^\'\'(\'[^\'\\]*(?:(?:\\.|\'(?!\'\'))[^\'\\]*)*\')\'\'/'; private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i'; - private $prefix = '/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:(?=[#\s<])/'; + private $prefix = '/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:(?=[#\s<])/'; private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; - //private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; - private $variable = '/^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?=[.,;!\^\s#()\[\]\{\}"\'<])/'; + //private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; + private $variable = '/^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)(?=[.,;!\^\s#()\[\]\{\}"\'<])/'; - private $blank = '/^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?:[ \t]+|(?=\.?[,;:\s#()\[\]\{\}"\'<]))/'; + private $blank = '/^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)(?:[ \t]+|(?=\.?[,;:\s#()\[\]\{\}"\'<]))/'; //TODO: this doesn't work private $number = "/^[\-+]?(?:\d+\.?\d*([eE](?:[\-\+])?\d+)|\d*\.?\d+)(?=[.,;:\s#()\[\]\{\}\"'<])/"; private $boolean = '/^(?:true|false)(?=[.,;\s#()\[\]\{\}"\'<])/'; diff --git a/src/TriGParser.php b/src/TriGParser.php index cdd953f..50d932d 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -622,9 +622,9 @@ private function initReaders () $this->readPrefixIRI = function ($token) { if ($token["type"] !== 'IRI') return $this->error('Expected IRI to follow prefix "' . $this->prefix . ':"', $token); - $prefixIRI = $this->readEntity($token); + $prefixIRI = call_user_func($this->readEntity,$token); $this->prefixes[$this->prefix] = $prefixIRI; - $this->prefixCallback($this->prefix, $prefixIRI); + call_user_func($this->prefixCallback,$this->prefix, $prefixIRI); return $this->readDeclarationPunctuation; }; diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 604703f..95efd22 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -79,23 +79,23 @@ public function testZeroOrMoreTriples () ['a', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 't']); // ### should parse triples with prefixes - $this->shouldParse('@prefix : <#>.\n' + - '@prefix a: .\n' + + $this->shouldParse('@prefix : <#>.\n' . + '@prefix a: .\n' . ':x a:a a:b.', ['#x', 'a#a', 'a#b']); // ### should parse triples with the prefix "prefix" - $this->shouldParse('@prefix prefix: .' + + $this->shouldParse('@prefix prefix: .' . 'prefix:a prefix:b prefix:c.', ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); // ### should parse triples with the prefix "base" - $this->shouldParse('PREFIX base: ' + + $this->shouldParse('PREFIX base: ' . 'base:a base:b base:c.', ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); // ### should parse triples with the prefix "graph" - $this->shouldParse('PREFIX graph: ' + + $this->shouldParse('PREFIX graph: ' . 'graph:a graph:b graph:c.', ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); @@ -104,8 +104,8 @@ public function testZeroOrMoreTriples () 'Expected entity but got @PREFIX on line 1.'); // ### should parse triples with prefixes and different punctuation - $this->shouldParse('@prefix : <#>.\n' + - '@prefix a: .\n' + + $this->shouldParse('@prefix : <#>.\n' . + '@prefix a: .\n' . ':x a:a a:b;a:c a:d,a:e.', ['#x', 'a#a', 'a#b'], ['#x', 'a#c', 'a#d'], From 57317024e9d6dac85904b78efee4efd5fa69e28a Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 6 Apr 2017 10:08:52 +0200 Subject: [PATCH 17/72] Fixed error handling --- src/N3Lexer.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 8420174..c9415c3 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -81,9 +81,14 @@ private function tokenizeToEnd($callback, $inputFinished) { // Continue parsing as far as possible; the loop will return eventually $input = $this->input; - // Signals the syntax error through the callback - $reportSyntaxError = function ($self) use ($callback, $input) { $callback($self->syntaxError(preg_match("/^\S*/", $input)[0]), null); }; + + // Signals the syntax error through the callback + $reportSyntaxError = function ($self) use ($callback, $input) { + preg_match("/^\S*/", $input, $match); + $callback($self->syntaxError($match[0], $self->line), null); + }; + $outputComments = $this->comments; while (true) { //TODO // Count and skip whitespace lines From 2d9de011fa42f7924a50c385adeb30cd59f5c9e8 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 6 Apr 2017 10:14:16 +0200 Subject: [PATCH 18/72] Fixed error handling --- src/N3Lexer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index c9415c3..ada3105 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -226,6 +226,8 @@ private function tokenizeToEnd($callback, $inputFinished) { break; case '@': + var_dump($input); + // Try to find a language code if ($this->prevTokenType === 'literal' && preg_match($this->langcode, $input, $match)){ $type = 'langcode'; From 130725ccbf9185f261ef2e06c509c4ca3e3a282c Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 6 Apr 2017 10:48:47 +0200 Subject: [PATCH 19/72] Fixed errors and should not parse --- src/N3Lexer.php | 3 +- src/TriGParser.php | 77 ++++++------ test/TriGParserTest.php | 273 ++++++++++++++++++++-------------------- 3 files changed, 181 insertions(+), 172 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index ada3105..4279055 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -129,6 +129,7 @@ private function tokenizeToEnd($callback, $inputFinished) { $matchLength = 0; $unescaped = null; $inconclusive = false; + switch ($firstChar) { case '^': // We need at least 3 tokens lookahead to distinguish ^^ and ^^pre:fixed @@ -226,8 +227,6 @@ private function tokenizeToEnd($callback, $inputFinished) { break; case '@': - var_dump($input); - // Try to find a language code if ($this->prevTokenType === 'literal' && preg_match($this->langcode, $input, $match)){ $type = 'langcode'; diff --git a/src/TriGParser.php b/src/TriGParser.php index 50d932d..8758423 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -189,9 +189,9 @@ private function initReaders () case 'type': case 'blank': case 'prefixed': - $prefix = $this->prefixes[$token["prefix"]]; - if (!isset($prefix)) + if (!isset($this->prefixes[$token["prefix"]])) return call_user_func($this->error,'Undefined prefix "' . $token["prefix"] . ':"', $token); + $prefix = $this->prefixes[$token["prefix"]]; $value = $prefix . $token["value"]; break; // Read a variable @@ -199,7 +199,7 @@ private function initReaders () return $token["value"]; // Everything else is not an entity default: - return $this->error('Expected entity but got ' . $token["type"], $token); + return call_user_func($this->error,'Expected entity but got ' . $token["type"], $token); } // In N3 mode, replace the entity if it is quantified if (!isset($quantifier) && isset($this->n3Mode) && isset($this->quantified[$value])) @@ -224,7 +224,7 @@ private function initReaders () case '{': // Start a new formula if (!$this->n3Mode) - return $this->error('Unexpected graph', $token); + return call_user_func($this->error,'Unexpected graph', $token); $this->saveContext('formula', $this->graph, $this->graph = '_:b' . $this->blankNodeCount++, null, null); return $this->readSubject; @@ -270,7 +270,7 @@ private function initReaders () case '}': // Expected predicate didn't come, must have been trailing semicolon if ($this->predicate === null) - return $this->error('Unexpected ' . $type, $token); + return call_user_func($this->error,'Unexpected ' . $type, $token); $this->subject = null; return $type === ']' ? $this->readBlankNodeTail($token) : $this->readPunctuation($token); case ';': @@ -278,7 +278,7 @@ private function initReaders () return $this->readPredicate; case 'blank': if (!$this->n3Mode) - return $this->error('Disallowed blank node as predicate', $token); + return call_user_func($this->error,'Disallowed blank node as predicate', $token); default: $this->predicate = call_user_func($this->readEntity,$token); if ($this->predicate == null) @@ -307,7 +307,7 @@ private function initReaders () case '{': // Start a new formula if (!$this->n3Mode) - return $this->error('Unexpected graph', $token); + return call_user_func($this->error,'Unexpected graph', $token); $this->saveContext('formula', $this->graph, $this->subject, $this->predicate, $this->graph = '_:b' . $this->blankNodeCount++); return $this->readSubject; @@ -331,7 +331,7 @@ private function initReaders () // ### `_readGraph` reads a graph $this->readGraph = function ($token) { if ($token["type"] !== '{') - return $this->error('Expected graph but got ' . $token["type"], $token); + return call_user_func($this->error,'Expected graph but got ' . $token["type"], $token); // The "subject" we read is actually the GRAPH's label $this->graph = $this->subject; $this->subject = null; @@ -544,7 +544,7 @@ private function initReaders () // A closing brace ends a graph case '}': if ($this->graph === null) - return $this->error('Unexpected graph closing', $token); + return call_user_func($this->error,'Unexpected graph closing', $token); if ($this->n3Mode) return $this->readFormulaTail($token); $this->graph = null; @@ -569,7 +569,7 @@ private function initReaders () $next = $this->readQuadPunctuation; break; } - return $this->error('Expected punctuation to follow "' . $this->object . '"', $token); + return call_user_func($this->error,'Expected punctuation to follow "' . $this->object . '"', $token); } // A triple has been completed now, so return it if ($subject !== null) { @@ -596,7 +596,7 @@ private function initReaders () $next = $this->readObject; break; default: - return $this->error('Expected punctuation to follow "' . $this->object . '"', $token); + return call_user_func($this->error,'Expected punctuation to follow "' . $this->object . '"', $token); } // A triple has been completed now, so return it $this->triple($this->subject, $this->predicate, $this->object, $this->graph); @@ -613,7 +613,7 @@ private function initReaders () // ### `_readPrefix` reads the prefix of a prefix declaration $this->readPrefix = function ($token) { if ($token["type"] !== 'prefix') - return $this->error('Expected prefix to follow @prefix', $token); + return call_user_func($this->error,'Expected prefix to follow @prefix', $token); $this->prefix = $token["value"]; return $this->readPrefixIRI; }; @@ -621,7 +621,7 @@ private function initReaders () // ### `_readPrefixIRI` reads the IRI of a prefix declaration $this->readPrefixIRI = function ($token) { if ($token["type"] !== 'IRI') - return $this->error('Expected IRI to follow prefix "' . $this->prefix . ':"', $token); + return call_user_func($this->error,'Expected IRI to follow prefix "' . $this->prefix . ':"', $token); $prefixIRI = call_user_func($this->readEntity,$token); $this->prefixes[$this->prefix] = $prefixIRI; call_user_func($this->prefixCallback,$this->prefix, $prefixIRI); @@ -631,7 +631,7 @@ private function initReaders () // ### `_readBaseIRI` reads the IRI of a base declaration $this->readBaseIRI = function ($token) { if ($token["type"] !== 'IRI') - return $this->error('Expected IRI to follow base declaration', $token); + return call_user_func($this->error,'Expected IRI to follow base declaration', $token); $this->setBase($this->base === null || preg_match($this->absoluteIRI,$token["value"]) ? $token["value"] : $this->resolveIRI($token)); return $this->readDeclarationPunctuation; @@ -644,18 +644,18 @@ private function initReaders () case 'blank': case 'prefixed': $this->readGraph; //TODO: what’s this? - return $this->readSubject($token); + return call_user_func($this->readSubject,$token); case '[': return $this->readNamedGraphBlankLabel; default: - return $this->error('Invalid graph label', $token); + return call_user_func($this->error,'Invalid graph label', $token); } }; // ### `_readNamedGraphLabel` reads a blank node label of a named graph $this->readNamedGraphBlankLabel = function ($token) { if ($token["type"] !== ']') - return $this->error('Invalid graph label', $token); + return call_user_func($this->error,'Invalid graph label', $token); $this->subject = '_:b' . $this->blankNodeCount++; return $this->readGraph; }; @@ -665,11 +665,11 @@ private function initReaders () // SPARQL-style declarations don't have punctuation if ($this->sparqlStyle) { $this->sparqlStyle = false; - return $this->readInTopContext($token); + return call_user_func($this->readInTopContext,$token); } if ($token["type"] !== '.') - return $this->error('Expected declaration to end with a dot', $token); + return call_user_func($this->error,'Expected declaration to end with a dot', $token); return $this->readInTopContext; }; @@ -679,11 +679,11 @@ private function initReaders () switch ($token["type"]) { case 'IRI': case 'prefixed': - $entity = $this->readEntity($token, true); + $entity = call_user_func($this->readEntity,$token, true); if (!$entity) break; default: - return $this->error('Unexpected ' . $token["type"], $token); + return call_user_func($this->error,'Unexpected ' . $token["type"], $token); } // Without explicit quantifiers, map entities to a quantified entity if (!$this->explicitQuantifiers) @@ -691,16 +691,16 @@ private function initReaders () // With explicit quantifiers, output the reified quantifier else { // If this is the first item, start a new quantifier list - if ($this->subject === null) { + if ($this->subject === null) { $this->subject = '_:b' . $this->blankNodeCount++; - $this->triple(isset($this->graph)?$this->graph:'', $this->predicate, $this->subject, self::QUANTIFIERS_GRAPH); + call_user_func($this->triple,isset($this->graph)?$this->graph:'', $this->predicate, $this->subject, self::QUANTIFIERS_GRAPH); } // Otherwise, continue the previous list else - $this->triple($this->subject, self::RDF_REST, + call_user_func($this->triple,$this->subject, self::RDF_REST, $this->subject = '_:b' . $this->blankNodeCount++, self::QUANTIFIERS_GRAPH); // Output the list item - $this->triple($this->subject, self::RDF_FIRST, $entity, self::QUANTIFIERS_GRAPH); + call_user_func($this->triple,$this->subject, self::RDF_FIRST, $entity, self::QUANTIFIERS_GRAPH); } return $this->readQuantifierPunctuation; }; @@ -714,12 +714,12 @@ private function initReaders () else { // With explicit quantifiers, close the quantifier list if ($this->explicitQuantifiers) { - $this->triple($this->subject, self::RDF_REST, self::RDF_NIL, self::QUANTIFIERS_GRAPH); + call_user_func($this->triple,$this->subject, self::RDF_REST, self::RDF_NIL, self::QUANTIFIERS_GRAPH); $this->subject = null; } // Read a dot - $this->readCallback = $this->getContextEndReader(); - return $this->readCallback($token); + $this->readCallback = call_user_func($this->getContextEndReader); + return call_user_func($this->readCallback, $token); } }; @@ -747,9 +747,9 @@ private function initReaders () // Switch back to the context of the list $this->restoreContext(); // Output the list item - $this->triple($this->subject, self::RDF_FIRST, $item, $this->graph); + call_user_func($this->triple,$this->subject, self::RDF_FIRST, $item, $this->graph); } - return $this->afterPath($token); + return call_user_func($this->afterPath,$token); } }; @@ -757,7 +757,7 @@ private function initReaders () $this->readForwardPath = function ($token) { $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; // The next token is the predicate - $predicate = $this->readEntity($token); + $predicate = call_user_func($this->readEntity,$token); if (!$predicate) return; // If we were reading a subject, replace the subject by the path's object @@ -771,7 +771,7 @@ private function initReaders () $this->object = $object; } // Emit the path's current triple and read its next section - $this->triple($subject, $predicate, $object, $this->graph); + call_user_func($this->triple,$subject, $predicate, $object, $this->graph); return $this->readPath; }; @@ -780,7 +780,7 @@ private function initReaders () $subject = '_:b' . $this->blankNodeCount++; $predicate; $object; // The next token is the predicate - $predicate = $this->readEntity($token); + $predicate = call_user_func($this->readEntity,$token); if ($predicate) return; // If we were reading a subject, replace the subject by the path's subject @@ -794,7 +794,7 @@ private function initReaders () $this->object = $subject; } // Emit the path's current triple and read its next section - $this->triple($subject, $predicate, $object, $this->graph); + call_user_func($this->triple,$subject, $predicate, $object, $this->graph); return $this->readPath; }; @@ -841,7 +841,7 @@ private function initReaders () return ($iri[1] === '/' ? $this->baseScheme : $this->baseRoot) . $this->removeDotSegments($iri); // Resolve all other IRIs at the base IRI's path default: - return $this->removeDotSegments($this->basePath . $iri); + return call_user_func($this->removeDotSegments, $this->basePath . $iri); } }; @@ -953,7 +953,12 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { try { $tokens = $this->lexer->tokenize($input); foreach($tokens as $token) { - $this->readCallback = call_user_func($this->readCallback, $token); + if (isset($this->readCallback)) { + $this->readCallback = call_user_func($this->readCallback, $token); + } else { + //error occured in parser + break; + } } } catch (Exception $e) { call_user_func($this->callback,$error, null); diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 95efd22..ddae271 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -79,9 +79,9 @@ public function testZeroOrMoreTriples () ['a', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 't']); // ### should parse triples with prefixes - $this->shouldParse('@prefix : <#>.\n' . - '@prefix a: .\n' . - ':x a:a a:b.', + $this->shouldParse("@prefix : <#>.\n" . + "@prefix a: .\n" . + ":x a:a a:b.", ['#x', 'a#a', 'a#b']); // ### should parse triples with the prefix "prefix" @@ -100,67 +100,67 @@ public function testZeroOrMoreTriples () ['http://prefix.cc/a', 'http://prefix.cc/b', 'http://prefix.cc/c']); // ### should not parse @PREFIX - shouldNotParse('@PREFIX : <#>.', + $this->shouldNotParse('@PREFIX : <#>.', 'Expected entity but got @PREFIX on line 1.'); // ### should parse triples with prefixes and different punctuation - $this->shouldParse('@prefix : <#>.\n' . - '@prefix a: .\n' . + $this->shouldParse("@prefix : <#>.\n" . + "@prefix a: .\n" . ':x a:a a:b;a:c a:d,a:e.', ['#x', 'a#a', 'a#b'], ['#x', 'a#c', 'a#d'], ['#x', 'a#c', 'a#e']); // ### should not parse undefined empty prefix in subject - shouldNotParse(':a ', + $this->shouldNotParse(':a ', 'Undefined prefix ":" on line 1.'); // ### should not parse undefined prefix in subject - shouldNotParse('a:a ', + $this->shouldNotParse('a:a ', 'Undefined prefix "a:" on line 1.'); // ### should not parse undefined prefix in predicate - shouldNotParse(' b:c ', + $this->shouldNotParse(' b:c ', 'Undefined prefix "b:" on line 1.'); // ### should not parse undefined prefix in object - shouldNotParse(' c:d ', + $this->shouldNotParse(' c:d ', 'Undefined prefix "c:" on line 1.'); // ### should not parse undefined prefix in datatype - shouldNotParse(' "c"^^d:e ', + $this->shouldNotParse(' "c"^^d:e ', 'Undefined prefix "d:" on line 1.'); // ### should parse triples with SPARQL prefixes - $this->shouldParse('PREFIX : <#>\n' + - 'PrEfIX a: ' + + $this->shouldParse("PREFIX : <#>\n" . + 'PrEfIX a: ' . ':x a:a a:b.', ['#x', 'a#a', 'a#b']); // ### should not parse prefix declarations without prefix - shouldNotParse('@prefix ', + $this->shouldNotParse('@prefix ', 'Expected prefix to follow @prefix on line 1.'); // ### should not parse prefix declarations without IRI - shouldNotParse('@prefix : .', + $this->shouldNotParse('@prefix : .', 'Expected IRI to follow prefix ":" on line 1.'); // ### should not parse prefix declarations without a dot - shouldNotParse('@prefix : ;', + $this->shouldNotParse('@prefix : ;', 'Expected declaration to end with a dot on line 1.'); // ### should parse statements with shared subjects - $this->shouldParse(' ;\n .', + $this->shouldParse(" ;\n .", ['a', 'b', 'c'], ['a', 'd', 'e']); // ### should parse statements with shared subjects and trailing semicolon - $this->shouldParse(' ;\n ;\n.', + $this->shouldParse(" ;\n ;\n.", ['a', 'b', 'c'], ['a', 'd', 'e']); // ### should parse statements with shared subjects and multiple semicolons - $this->shouldParse(' ;;\n .', + $this->shouldParse(" ;;\n .", ['a', 'b', 'c'], ['a', 'd', 'e']); @@ -170,7 +170,7 @@ public function testZeroOrMoreTriples () ['a', 'b', 'd']); // ### should parse diamonds - $this->shouldParse('<> <> <> <>.\n(<>) <> (<>) <>.', + $this->shouldParse("<> <> <> <>.\n(<>) <> (<>) <>.", ['', '', '', ''], ['_:b0', '', '_:b1', ''], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', ''], @@ -183,7 +183,7 @@ public function testZeroOrMoreTriples () ['_:b0_a', 'b', '_:b0_c']); // ### should not parse statements with blank predicates - shouldNotParse(' _:b .', + $this->shouldNotParse(' _:b .', 'Disallowed blank node as predicate on line 1.'); // ### should parse statements with empty blank nodes @@ -206,11 +206,11 @@ public function testZeroOrMoreTriples () ['_:b0', 'c', '"x"']); // ### should not parse a blank node with missing subject - shouldNotParse(' [].', + $this->shouldNotParse(' [].', 'Expected entity but got ] on line 1.'); // ### should not parse a blank node with only a semicolon - shouldNotParse(' [;].', + $this->shouldNotParse(' [;].', 'Unexpected ] on line 1.'); // ### should parse a blank node with a trailing semicolon @@ -311,11 +311,35 @@ private function shouldParse($createParser, $input = "") }); } + + function shouldNotParse($createParser, $input, $expectedError = null) { + $expected = array_slice(func_get_args(),1); + // Shift parameters as necessary + if (!is_callable($createParser)) { + $expectedError = $input; + $input = $createParser; + $createParser = function () { + return new TriGParser(); + }; + } + $parser = $createParser(); + $parser->_resetBlankNodeIds(); + $parser->parse($input, function ($error, $triple = null) use ($expectedError){ + //expect($error).not.to.exist; + if (isset($error)) { + $this->assertEquals($expectedError, $error->getMessage()); + } else if (!isset($triple)) { + throw new \Exception('Expected error ' . $expectedError); + } + }); + } + + private static function toSortedJSON ($items) { $triples = array_map("json_encode", $items); sort($triples); - return '[\n ' . join('\n ', $triples) . '\n]'; + return "[\n " . join("\n ", $triples) . "\n]"; } } /* @@ -323,7 +347,7 @@ private static function toSortedJSON ($items) describe('An N3Parser instance', function () { // ### should not parse an invalid blank node - shouldNotParse('[ .', + $this->shouldNotParse('[ .', 'Expected punctuation to follow "b" on line 1.')); // ### should parse a statements with only an anonymous node @@ -331,11 +355,11 @@ private static function toSortedJSON ($items) ['_:b0', 'p', 'o'])); // ### should not parse a statement with only a blank anonymous node - shouldNotParse('[].', + $this->shouldNotParse('[].', 'Unexpected . on line 1.')); // ### should not parse an anonymous node with only an anonymous node inside - shouldNotParse('[[

]].', + $this->shouldNotParse('[[

]].', 'Expected entity but got [ on line 1.')); // ### should parse statements with an empty list in the subject @@ -411,7 +435,7 @@ private static function toSortedJSON ($items) ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); // ### should not parse statements with undefined prefixes in lists - shouldNotParse(' (a:x a:y).', + $this->shouldNotParse(' (a:x a:y).', 'Undefined prefix "a:" on line 1.')); // ### should parse statements with blank nodes in lists @@ -463,57 +487,57 @@ private static function toSortedJSON ($items) ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); // ### should not parse an invalid list - shouldNotParse(' (]).', + $this->shouldNotParse(' (]).', 'Expected entity but got ] on line 1.')); // ### should resolve IRIs against @base - shouldParse('@base .\n' + - ' .\n' + - '@base .\n' + - ' .', + shouldParse("@base .\n" . + " .\n" . + "@base .\n" . + " .", ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g'])); // ### should not resolve IRIs against @BASE - shouldNotParse('@BASE .', + $this->shouldNotParse('@BASE .', 'Expected entity but got @BASE on line 1.')); // ### should resolve IRIs against SPARQL base - shouldParse('BASE \n' + - ' . ' + - 'BASE ' + + shouldParse("BASE \n" . + ' . ' . + 'BASE ' . ' .', ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g'])); // ### should resolve IRIs against a @base with query string - shouldParse('@base .\n' + - '<> .\n' + - '@base .\n' + + shouldParse("@base .\n" . + "<> .\n" . + "@base .\n" . '<> .', ['http://ex.org/?foo', 'http://ex.org/b', 'http://ex.org/c'], ['http://ex.org/d/?bar', 'http://ex.org/d/f', 'http://ex.org/d/g'])); // ### should resolve IRIs with query string against @base - shouldParse('@base .\n' + - ' .\n' + - '@base .\n' + - ' .' + - '@base .\n' + + shouldParse("@base .\n" . + " .\n". + "@base .\n" . + " .". + "@base .\n" . '<> .', ['http://ex.org/?', 'http://ex.org/?a', 'http://ex.org/?a=b'], ['http://ex.org/d?', 'http://ex.org/d?a', 'http://ex.org/d?a=b'], ['http://ex.org/d?e', 'http://ex.org/d?a', 'http://ex.org/d?a=b'])); // ### should not resolve IRIs with colons - shouldParse('@base .\n' + - ' .\n' + - ' .\n' + - ' .', + shouldParse("@base .\n" . + " .\n" . + " .\n" . + " .", ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], ['A:', 'b:', 'c:'], ['a:a', 'b:B', 'C-D:c'])); - +//TODO : solve plus to dot and newlines // ### should resolve datatype IRIs against @base shouldParse('@base .\n' + ' "c"^^.\n' + @@ -661,47 +685,47 @@ private static function toSortedJSON ($items) ['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'])); // ### should not parse a single closing brace - shouldNotParse('}', + $this->shouldNotParse('}', 'Unexpected graph closing on line 1.')); // ### should not parse a single opening brace - shouldNotParse('{', + $this->shouldNotParse('{', 'Expected entity but got eof on line 1.')); // ### should not parse a superfluous closing brace - shouldNotParse('{}}', + $this->shouldNotParse('{}}', 'Unexpected graph closing on line 1.')); // ### should not parse a graph with only a dot - shouldNotParse('{.}', + $this->shouldNotParse('{.}', 'Expected entity but got . on line 1.')); // ### should not parse a graph with only a semicolon - shouldNotParse('{;}', + $this->shouldNotParse('{;}', 'Expected entity but got ; on line 1.')); // ### should not parse an unclosed graph - shouldNotParse('{ .', + $this->shouldNotParse('{ .', 'Unclosed graph on line 1.')); // ### should not parse a named graph with a list node as label - shouldNotParse('() {}', + $this->shouldNotParse('() {}', 'Expected entity but got { on line 1.')); // ### should not parse a named graph with a non-empty blank node as label - shouldNotParse('[ ] {}', + $this->shouldNotParse('[ ] {}', 'Expected entity but got { on line 1.')); // ### should not parse a named graph with the GRAPH keyword and a non-empty blank node as label - shouldNotParse('GRAPH [ ] {}', + $this->shouldNotParse('GRAPH [ ] {}', 'Invalid graph label on line 1.')); // ### should not parse a triple after the GRAPH keyword - shouldNotParse('GRAPH .', + $this->shouldNotParse('GRAPH .', 'Expected graph but got IRI on line 1.')); // ### should not parse repeated GRAPH keywords - shouldNotParse('GRAPH GRAPH {}', + $this->shouldNotParse('GRAPH GRAPH {}', 'Invalid graph label on line 1.')); // ### should parse a quad with 4 IRIs @@ -713,7 +737,7 @@ private static function toSortedJSON ($items) ['p#a', 'p#b', 'p#c', 'p#g'])); // ### should not parse a quad with an undefined prefix - shouldNotParse(' p:g.', + $this->shouldNotParse(' p:g.', 'Undefined prefix "p:" on line 1.')); // ### should parse a quad with 3 IRIs and a literal @@ -725,47 +749,47 @@ private static function toSortedJSON ($items) ['_:b0_a', 'b', '"c"^^d', '_:b0_g'])); // ### should not parse a quad in a graph - shouldNotParse('{ .}', + $this->shouldNotParse('{ .}', 'Expected punctuation to follow "c" on line 1.')); // ### should not parse a quad with different punctuation - shouldNotParse(' ;', + $this->shouldNotParse(' ;', 'Expected dot to follow quad on line 1.')); // ### should not parse base declarations without IRI - shouldNotParse('@base a: ', + $this->shouldNotParse('@base a: ', 'Expected IRI to follow base declaration on line 1.')); // ### should not parse improperly nested parentheses and brackets - shouldNotParse(' [ (]).', + $this->shouldNotParse(' [ (]).', 'Expected entity but got ] on line 1.')); // ### should not parse improperly nested square brackets - shouldNotParse(' [ ]].', + $this->shouldNotParse(' [ ]].', 'Expected entity but got ] on line 1.')); // ### should error when an object is not there - shouldNotParse(' .', + $this->shouldNotParse(' .', 'Expected entity but got . on line 1.')); // ### should error when a dot is not there - shouldNotParse(' ', + $this->shouldNotParse(' ', 'Expected entity but got eof on line 1.')); // ### should error with an abbreviation in the subject - shouldNotParse('a .', + $this->shouldNotParse('a .', 'Expected entity but got abbreviation on line 1.')); // ### should error with an abbreviation in the object - shouldNotParse(' a .', + $this->shouldNotParse(' a .', 'Expected entity but got abbreviation on line 1.')); // ### should error if punctuation follows a subject - shouldNotParse(' .', + $this->shouldNotParse(' .', 'Unexpected . on line 1.')); // ### should error if an unexpected token follows a subject - shouldNotParse(' [', + $this->shouldNotParse(' [', 'Expected entity but got [ on line 1.')); // ### should not error if there is no triple callback function () { @@ -955,31 +979,31 @@ function parser() { return new N3Parser({ format: 'Turtle' }); } shouldParse(parser, ' .', ['a', 'b', 'c'])); // ### should not parse a default graph - shouldNotParse(parser, '{}', 'Unexpected graph on line 1.')); + $this->shouldNotParse(parser, '{}', 'Unexpected graph on line 1.')); // ### should not parse a named graph - shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); + $this->shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); // ### should not parse a named graph with the GRAPH keyword - shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); + $this->shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); // ### should not parse a quad - shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); // ### should not parse a variable - shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); // ### should not parse an equality statement - shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); // ### should not parse a right implication statement - shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); // ### should not parse a left implication statement - shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); // ### should not parse a formula as object - shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); + $this->shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); }); describe('An N3Parser instance for the TriG format', function () { @@ -998,22 +1022,22 @@ function parser() { return new N3Parser({ format: 'TriG' }); } shouldParse(parser, 'GRAPH {}')); // ### should not parse a quad - shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); // ### should not parse a variable - shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); // ### should not parse an equality statement - shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); // ### should not parse a right implication statement - shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); // ### should not parse a left implication statement - shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); // ### should not parse a formula as object - shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); + $this->shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); }); describe('An N3Parser instance for the N-Triples format', function () { @@ -1024,29 +1048,29 @@ function parser() { return new N3Parser({ format: 'N-Triples' }); } ['http://ex.org/a', 'http://ex.org/b', '"c"'])); // ### should not parse a single quad - shouldNotParse(parser, ' "c" .', + $this->shouldNotParse(parser, ' "c" .', 'Expected punctuation to follow ""c"" on line 1.')); // ### should not parse relative IRIs - shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); + $this->shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); // ### should not parse a prefix declaration - shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); + $this->shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); // ### should not parse a variable - shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); // ### should not parse an equality statement - shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); // ### should not parse a right implication statement - shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); // ### should not parse a left implication statement - shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); // ### should not parse a formula as object - shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); + $this->shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); }); describe('An N3Parser instance for the N-Quads format', function () { @@ -1061,25 +1085,25 @@ function parser() { return new N3Parser({ format: 'N-Quads' }); } ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g'])); // ### should not parse relative IRIs - shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); + $this->shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); // ### should not parse a prefix declaration - shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); + $this->shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); // ### should not parse a variable - shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); // ### should not parse an equality statement - shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); // ### should not parse a right implication statement - shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); // ### should not parse a left implication statement - shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); // ### should not parse a formula as object - shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); + $this->shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); }); describe('An N3Parser instance for the N3 format', function () { @@ -1089,16 +1113,16 @@ function parser() { return new N3Parser({ format: 'N3' }); } shouldParse(parser, ' .', ['a', 'b', 'c'])); // ### should not parse a default graph - shouldNotParse(parser, '{}', 'Expected entity but got eof on line 1.')); + $this->shouldNotParse(parser, '{}', 'Expected entity but got eof on line 1.')); // ### should not parse a named graph - shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); + $this->shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); // ### should not parse a named graph with the GRAPH keyword - shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); + $this->shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); // ### should not parse a quad - shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); // ### allows a blank node label in predicate position shouldParse(parser, ' _:b .', ['a', '_:b0_b', 'c'])); @@ -1188,15 +1212,15 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b0', '_:b1', '_:b2'])); // ### should not parse a @forSome statement with an invalid prefix - shouldNotParse(parser, '@forSome a:b.', + $this->shouldNotParse(parser, '@forSome a:b.', 'Undefined prefix "a:" on line 1.')); // ### should not parse a @forSome statement with a blank node - shouldNotParse(parser, '@forSome _:a.', + $this->shouldNotParse(parser, '@forSome _:a.', 'Unexpected blank on line 1.')); // ### should not parse a @forSome statement with a variable - shouldNotParse(parser, '@forSome ?a.', + $this->shouldNotParse(parser, '@forSome ?a.', 'Unexpected $on line 1.')); // ### should correctly scope @forSome statements @@ -1214,15 +1238,15 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['?b-0', '?b-1', '?b-2'])); // ### should not parse a @forAll statement with an invalid prefix - shouldNotParse(parser, '@forAll a:b.', + $this->shouldNotParse(parser, '@forAll a:b.', 'Undefined prefix "a:" on line 1.')); // ### should not parse a @forAll statement with a blank node - shouldNotParse(parser, '@forAll _:a.', + $this->shouldNotParse(parser, '@forAll _:a.', 'Unexpected blank on line 1.')); // ### should not parse a @forAll statement with a variable - shouldNotParse(parser, '@forAll ?a.', + $this->shouldNotParse(parser, '@forAll ?a.', 'Unexpected $on line 1.')); // ### should correctly scope @forAll statements @@ -1378,10 +1402,10 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b2', 'f:son', 'ex:joe'])); // ### should not parse an invalid ! path - shouldNotParse(parser, '!"invalid" ', 'Expected entity but got literal on line 1.')); + $this->shouldNotParse(parser, '!"invalid" ', 'Expected entity but got literal on line 1.')); // ### should not parse an invalid ^ path - shouldNotParse(parser, '^"invalid" ', 'Expected entity but got literal on line 1.')); + $this->shouldNotParse(parser, '^"invalid" ', 'Expected entity but got literal on line 1.')); }); describe('An N3Parser instance for the N3 format with the explicitQuantifiers option', function () { @@ -1902,25 +1926,6 @@ function shouldParse(createParser, input) { } -function shouldNotParse(createParser, input, expectedError) { - // Shift parameters if necessary - if (!createParser.call) - expectedError = input, input = createParser, createParser = N3Parser; - - return function (done) { - createParser().parse(input, function ($error, $triple) { - if (error) { - expect(triple).not.to.exist; - error.should.be.an.instanceof(Error); - error.message.should.eql(expectedError); - done(); - } - else if (!triple) - throw new Error('Expected error ' + expectedError); - }); - }; -} - function itShouldResolve(baseIri, relativeIri, expected) { $result; describe('resolving <' + relativeIri + '> against <' + baseIri + '>', function () { From a32f13b5d9e4c65296758cad70bfae3ecb6fa87a Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 12:20:55 +0200 Subject: [PATCH 20/72] fixed count bug --- src/N3Lexer.php | 3 +- src/TriGParser.php | 66 ++++++++++++++++++++--------------------- test/TriGParserTest.php | 16 +++++++--- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 4279055..b1b3b25 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -181,8 +181,7 @@ private function tokenizeToEnd($callback, $inputFinished) { // Try to find a blank node. Since it can contain (but not end with) a dot, // we always need a non-dot character before deciding it is a prefixed name. // Therefore, try inserting a space if we're at the end of the $input. - if ((preg_match($this->blank, $input, $match)) || - $inputFinished && (preg_match($this->blank, $input . ' ', $match))) { + if ((preg_match($this->blank, $input, $match)) || $inputFinished && (preg_match($this->blank, $input . ' ', $match))) { $type = 'blank'; $prefix = '_'; $value = $match[1]; diff --git a/src/TriGParser.php b/src/TriGParser.php index 8758423..6066846 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -18,7 +18,7 @@ class TriGParser private $dotSegments = '/(?:^|\/)\.\.?(?:$|[\/#?])/'; // The next ID for new blank nodes - private $blankNodePrefix = 0; + private $blankNodePrefix; private $blankNodeCount = 0; private $contextStack; @@ -73,7 +73,7 @@ public function __construct($options = []) { // ## Private class methods // ### `_resetBlankNodeIds` restarts blank node identification public function _resetBlankNodeIds () { - $this->blankNodePrefix = 0; + $this->blankNodePrefix = null; $this->blankNodeCount = 0; } @@ -98,13 +98,13 @@ private function setBase ($baseIRI = null) { // ### `_saveContext` stores the current parsing context // when entering a new scope (list, blank node, formula) private function saveContext($type, $graph, $subject, $predicate, $object) { - $n3Mode = $this->n3Mode; - $this->contextStack.push([ + $n3Mode = isset($this->n3Mode)?$this->n3Mode:null; + array_push($this->contextStack,[ "subject"=> $subject, "predicate"=> $predicate,"object"=> $object, "graph" => $graph, "type"=> $type, - "inverse" => $n3Mode ? $this->inversePredicate : false, - "blankPrefix"=> $n3Mode ? $this->prefixes["_"] : '', - "quantified"=> $n3Mode ? $this->quantified : null + "inverse" => $n3Mode ? clone $this->inversePredicate : false, + "blankPrefix"=> $n3Mode ? clone $this->prefixes["_"] : '', + "quantified"=> $n3Mode ? clone $this->quantified : null ]); // The settings below only apply to N3 streams if ($n3Mode) { @@ -114,15 +114,15 @@ private function saveContext($type, $graph, $subject, $predicate, $object) { // (using a dot as separator, as a blank node label cannot start with it) $this->prefixes["_"] = $this->graph . '.'; // Quantifiers are scoped to a formula TODO: is this correct? - //$this->quantified = Object.create($this->quantified); + $this->quantified = clone $this->quantified; } } // ### `_restoreContext` restores the parent context // when leaving a scope (list, blank node, formula) private function restoreContext() { - $context = $this->contextStack.pop(); - $n3Mode = $this->n3Mode; + $context = array_pop($this->contextStack); + $n3Mode = isset($this->n3Mode)?$this->n3Mode:null; $this->subject = $context["subject"]; $this->predicate = $context["predicate"]; $this->object = $context["object"]; @@ -135,7 +135,6 @@ private function restoreContext() { } } - private function initReaders () { // ### `_readInTopContext` reads a token when in the top context @@ -189,8 +188,10 @@ private function initReaders () case 'type': case 'blank': case 'prefixed': - if (!isset($this->prefixes[$token["prefix"]])) + if (!isset($this->prefixes[$token["prefix"]])) { return call_user_func($this->error,'Undefined prefix "' . $token["prefix"] . ':"', $token); + } + $prefix = $this->prefixes[$token["prefix"]]; $value = $prefix . $token["value"]; break; @@ -209,12 +210,11 @@ private function initReaders () // ### `_readSubject` reads a triple's subject $this->readSubject = function ($token) { - $this->predicate = null; + $this->predicate = null; switch ($token["type"]) { case '[': // Start a new triple with a new blank node as subject - $this->saveContext('blank', $this->graph, - $this->subject = '_:b' . $this->blankNodeCount++, null, null); + $this->saveContext('blank', $this->graph,$this->subject = '_:b' . $this->blankNodeCount++, null, null); return $this->readBlankNodeHead; case '(': // Start a new list @@ -272,12 +272,12 @@ private function initReaders () if ($this->predicate === null) return call_user_func($this->error,'Unexpected ' . $type, $token); $this->subject = null; - return $type === ']' ? $this->readBlankNodeTail($token) : $this->readPunctuation($token); + return $type === ']' ? call_user_func($this->readBlankNodeTail,$token) : call_user_func($this->readPunctuation,$token); case ';': // Extra semicolons can be safely ignored return $this->readPredicate; case 'blank': - if (!$this->n3Mode) + if (!isset($this->n3Mode)) return call_user_func($this->error,'Disallowed blank node as predicate', $token); default: $this->predicate = call_user_func($this->readEntity,$token); @@ -290,7 +290,7 @@ private function initReaders () // ### `_readObject` reads a triple's object $this->readObject = function ($token) { - switch ($token["type"]) { + switch ($token["type"]) { case 'literal': $this->object = $token["value"]; return $this->readDataTypeOrLang; @@ -342,22 +342,22 @@ private function initReaders () $this->readBlankNodeHead = function ($token) { if ($token["type"] === ']') { $this->subject = null; - return $this->readBlankNodeTail($token); + return call_user_func($this->readBlankNodeTail,$token); } else { $this->predicate = null; - return $this->readPredicate($token); + return call_user_func($this->readPredicate,$token); } }; // ### `_readBlankNodeTail` reads the end of a blank node $this->readBlankNodeTail = function ($token) { if ($token["type"] !== ']') - return $this->readBlankNodePunctuation($token); + return call_user_func($this->readBlankNodePunctuation,$token); // Store blank node triple if ($this->subject !== null) - $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + call_user_func($this->triple,$this->subject, $this->predicate, $this->object, $this->graph); // Restore the parent context containing this blank node $empty = $this->predicate === null; @@ -368,7 +368,7 @@ private function initReaders () return $empty ? $this->readPredicateOrNamedGraph : $this->readPredicateAfterBlank; // If the blank node was the object, restore previous context and read punctuation else - return $this->getContextEndReader(); + return call_user_func($this->getContextEndReader); }; // ### `_readPredicateAfterBlank` reads a predicate after an anonymous blank node @@ -376,14 +376,14 @@ private function initReaders () // If a dot follows a blank node in top context, there is no predicate if ($token["type"] === '.' && sizeof($this->contextStack) === 0) { $this->subject = null; // cancel the current triple - return $this->readPunctuation($token); + return call_user_func($this->readPunctuation, $token); } - return $this->readPredicate($token); + return call_user_func($this->readPredicate, $token); }; // ### `_readListItem` reads items from a list $this->readListItem = function ($token) { - $item = null; // The item of the list + $item = null; // The item of the list $list = null; // The list itself $prevList = $this->subject; // The previous list that contains this list $stack = $this->contextStack; // The stack of parent contexts @@ -545,7 +545,7 @@ private function initReaders () case '}': if ($this->graph === null) return call_user_func($this->error,'Unexpected graph closing', $token); - if ($this->n3Mode) + if (isset($this->n3Mode)) return $this->readFormulaTail($token); $this->graph = null; // A dot just ends the statement, without sharing anything with the next @@ -599,7 +599,7 @@ private function initReaders () return call_user_func($this->error,'Expected punctuation to follow "' . $this->object . '"', $token); } // A triple has been completed now, so return it - $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + call_user_func($this->triple, $this->subject, $this->predicate, $this->object, $this->graph); return $next; }; @@ -654,7 +654,7 @@ private function initReaders () // ### `_readNamedGraphLabel` reads a blank node label of a named graph $this->readNamedGraphBlankLabel = function ($token) { - if ($token["type"] !== ']') + if ($token["type"] !== ']') return call_user_func($this->error,'Invalid graph label', $token); $this->subject = '_:b' . $this->blankNodeCount++; return $this->readGraph; @@ -675,7 +675,7 @@ private function initReaders () // Reads a list of quantified symbols from a @forSome or @forAll statement $this->readQuantifierList = function ($token) { - $entity; + $entity; switch ($token["type"]) { case 'IRI': case 'prefixed': @@ -755,7 +755,7 @@ private function initReaders () // ### `_readForwardPath` reads a '!' path $this->readForwardPath = function ($token) { - $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; + $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; // The next token is the predicate $predicate = call_user_func($this->readEntity,$token); if (!$predicate) @@ -777,7 +777,7 @@ private function initReaders () // ### `_readBackwardPath` reads a '^' path $this->readBackwardPath = function ($token) { - $subject = '_:b' . $this->blankNodeCount++; + $subject = '_:b' . $this->blankNodeCount++; $predicate; $object; // The next token is the predicate $predicate = call_user_func($this->readEntity,$token); @@ -923,7 +923,7 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { $this->readCallback = $this->readInTopContext; $this->sparqlStyle = false; $this->prefixes = []; - $this->prefixes["_"] = isset($this->blankNodePrefix)?$this->blankNodePrefix:'_:b' . $this->blankNodePrefix++ . '_'; + $this->prefixes["_"] = isset($this->blankNodePrefix)?$this->blankNodePrefix:'_:b' . $this->blankNodeCount . '_'; $this->prefixCallback = isset($prefixCallback)?$prefixCallback:function () {}; $this->inversePredicate = false; $this->quantified = []; diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index ddae271..e0aab23 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -169,6 +169,10 @@ public function testZeroOrMoreTriples () ['a', 'b', 'c'], ['a', 'b', 'd']); + } + + public function testBlankNodes () + { // ### should parse diamonds $this->shouldParse("<> <> <> <>.\n(<>) <> (<>) <>.", ['', '', '', ''], @@ -189,7 +193,7 @@ public function testZeroOrMoreTriples () // ### should parse statements with empty blank nodes $this->shouldParse('[] [].', ['_:b0', 'b', '_:b1']); - + // ### should parse statements with unnamed blank nodes in the subject $this->shouldParse('[ ] .', ['_:b0', 'c', 'd'], @@ -309,6 +313,7 @@ private function shouldParse($createParser, $input = "") else $this->assertEquals(self::toSortedJSON($items), self::toSortedJSON($results)); }); + $parser->_resetBlankNodeIds(); } @@ -324,11 +329,14 @@ function shouldNotParse($createParser, $input, $expectedError = null) { } $parser = $createParser(); $parser->_resetBlankNodeIds(); - $parser->parse($input, function ($error, $triple = null) use ($expectedError){ + //hackish way so we only act upon first error + $callbackCount = 0; + $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$callbackCount){ //expect($error).not.to.exist; - if (isset($error)) { + if (isset($error) && $callbackCount === 0) { $this->assertEquals($expectedError, $error->getMessage()); - } else if (!isset($triple)) { + $callbackCount ++; + } else if (!isset($triple) && $callbackCount === 0) { throw new \Exception('Expected error ' . $expectedError); } }); From b890e369faf660db63ebb5d6c2976fb0a3d5952e Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 15:43:55 +0200 Subject: [PATCH 21/72] Added new example --- examples/parseAndWrite.php | 2 +- src/TriGParser.php | 32 +- test/TriGParserTest.php | 1288 ++++++++++++++++++------------------ 3 files changed, 648 insertions(+), 674 deletions(-) diff --git a/examples/parseAndWrite.php b/examples/parseAndWrite.php index 4a8afc3..5149283 100644 --- a/examples/parseAndWrite.php +++ b/examples/parseAndWrite.php @@ -6,7 +6,7 @@ echo "--- First, simple implementation ---\n"; $parser = new TriGParser(); $writer = new TriGWriter(["format"=>"trig"]); -$triples = $parser->parse(" ."); +$triples = $parser->parse("() ."); $writer->addTriples($triples); echo $writer->end(); diff --git a/src/TriGParser.php b/src/TriGParser.php index 6066846..d3cfd39 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -107,7 +107,7 @@ private function saveContext($type, $graph, $subject, $predicate, $object) { "quantified"=> $n3Mode ? clone $this->quantified : null ]); // The settings below only apply to N3 streams - if ($n3Mode) { + if (isset($n3Mode)) { // Every new scope resets the predicate direction $this->inversePredicate = false; // In N3, blank nodes are scoped to a formula @@ -210,13 +210,13 @@ private function initReaders () // ### `_readSubject` reads a triple's subject $this->readSubject = function ($token) { - $this->predicate = null; + $this->predicate = null; switch ($token["type"]) { case '[': // Start a new triple with a new blank node as subject - $this->saveContext('blank', $this->graph,$this->subject = '_:b' . $this->blankNodeCount++, null, null); + $this->saveContext('blank', $this->graph, $this->subject = '_:b' . $this->blankNodeCount++, null, null); return $this->readBlankNodeHead; - case '(': + case '(':; // Start a new list $this->saveContext('list', $this->graph, self::RDF_NIL, null, null); $this->subject = null; @@ -383,14 +383,14 @@ private function initReaders () // ### `_readListItem` reads items from a list $this->readListItem = function ($token) { - $item = null; // The item of the list + $item = null; // The item of the list $list = null; // The list itself $prevList = $this->subject; // The previous list that contains this list $stack = $this->contextStack; // The stack of parent contexts $parent = $stack[sizeof($stack) - 1];// The parent containing the current list $next = $this->readListItem; // The next function to execute $itemComplete = true; // Whether the item has been read fully - + switch ($token["type"]) { case '[': // Stack the current list triple and start a new triple with a blank node as subject @@ -401,17 +401,16 @@ private function initReaders () break; case '(': // Stack the current list triple and start a new list - $this->saveContext('list', $this->graph, $list = '_:b' . $this->blankNodeCount++, - self::RDF_FIRST, self::RDF_NIL); + $this->saveContext('list', $this->graph, $list = '_:b' . $this->blankNodeCount++, self::RDF_FIRST, self::RDF_NIL); $this->subject = null; break; case ')': // Closing the list; restore the parent context $this->restoreContext(); // If this list is contained within a parent list, return the membership triple here. - // This will be ` rdf:first .`. + // This will be ` rdf:first .`. if (sizeof($stack) !== 0 && $stack[sizeof($stack) - 1] === 'list') - $this->triple($this->subject, $this->predicate, $this->object, $this->graph); + call_user_func($this->triple, $this->subject, $this->predicate, $this->object, $this->graph); // Was this list the parent's subject? if ($this->predicate === null) { // The next token is the predicate @@ -422,7 +421,7 @@ private function initReaders () } // The list was in the parent context's object else { - $next = $this->getContextEndReader(); + $next = call_user_func($this->getContextEndReader); // No list tail if this was an empty list if ($this->object === self::RDF_NIL) return $next; @@ -436,7 +435,7 @@ private function initReaders () $next = $this->readListItemDataTypeOrLang; break; default: - $item = $this->readEntity($token); + $item = call_user_func($this->readEntity, $token); if ($item == null) return; } @@ -445,7 +444,6 @@ private function initReaders () if ($list === null) $list = '_:b' . $this->blankNodeCount++; $this->subject = $list; - // Is this the first element of the list? if ($prevList === null) { // This list is either the subject or the object of its parent @@ -456,12 +454,12 @@ private function initReaders () } else { // Continue the previous list with the current list - $this->triple($prevList, self::RDF_REST, $list, $this->graph); + call_user_func($this->triple,$prevList, self::RDF_REST, $list, $this->graph); } // Add the item's value if ($item !== null) { // In N3 mode, the item might be a path - if ($this->n3Mode && ($token["type"] === 'IRI' || $token["type"] === 'prefixed')) { + if (isset($this->n3Mode) && ($token["type"] === 'IRI' || $token["type"] === 'prefixed')) { // Create a new context to add the item's path $this->saveContext('item', $this->graph, $list, self::RDF_FIRST, $item); $this->subject = $item; @@ -471,7 +469,7 @@ private function initReaders () } // Output the item if it is complete if ($itemComplete) - $this->triple($list, self::RDF_FIRST, $item, $this->graph); + call_user_func($this->triple, $list, self::RDF_FIRST, $item, $this->graph); // Otherwise, save it for completion else $this->object = $item; @@ -941,7 +939,7 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { $error = $e; } }; - $tokens = $this->lexer->tokenize($input); + $tokens = $this->lexer->tokenize($input); foreach($tokens as $token) { $this->readCallback = call_user_func($this->readCallback,$token); } diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index e0aab23..00b6596 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -71,8 +71,8 @@ public function testZeroOrMoreTriples () // ### should not parse a triple with a literal and a prefixed name type with an inexistent prefix /* shouldNotParse(' "string"^^x:z.', - 'Undefined prefix "x:" on line 1.'); - */ + 'Undefined prefix "x:" on line 1.'); +*/ // ### should parse a triple with the "a" shorthand predicate $this->shouldParse(' a .', @@ -285,8 +285,453 @@ public function testBlankNodes () $this->shouldParse('_:a _:c. { _:a _:c }', ['_:b0_a', 'b', '_:b0_c'], ['_:b0_a', 'b', '_:b0_c', 'g']); - - } + + // ### should not parse an invalid blank node + $this->shouldNotParse('[ .', + 'Expected punctuation to follow "b" on line 1.'); + + // ### should parse a statements with only an anonymous node + $this->shouldParse('[

].', + ['_:b0', 'p', 'o']); + + // ### should not parse a statement with only a blank anonymous node + $this->shouldNotParse('[].', + 'Unexpected . on line 1.'); + + // ### should not parse an anonymous node with only an anonymous node inside + $this->shouldNotParse('[[

]].', + 'Expected entity but got [ on line 1.'); + + // ### should parse statements with an empty list in the subject + $this->shouldParse('() .', + ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'a', 'b']); + + // ### should parse statements with an empty list in the object + $this->shouldParse(' ().', + ['a', 'b', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a single-element list in the subject + $this->shouldParse('() .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a single-element list in the object + $this->shouldParse(' ().', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse a list with a literal + $this->shouldParse(' ("x").', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse a list with a typed literal + $this->shouldParse(' ("x"^^).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"^^y'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse a list with a language-tagged literal + $this->shouldParse(' ("x"@en-GB).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"@en-gb'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a multi-element list in the subject + $this->shouldParse('( ) .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a multi-element list in the object + $this->shouldParse(' ( ).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a multi-element literal list in the object + $this->shouldParse(' ("x" "y"@en-GB "z"^^).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"y"@en-gb'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"z"^^t'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with prefixed names in lists + $this->shouldParse('@prefix a: . (a:x a:y).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a#x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a#y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should not parse statements with undefined prefixes in lists + $this->shouldNotParse(' (a:x a:y).', + 'Undefined prefix "a:" on line 1.'); + + // ### should parse statements with blank nodes in lists + $this->shouldParse(' (_:x _:y).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_y'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a nested empty list + $this->shouldParse(' ( ()).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with non-empty nested lists + $this->shouldParse(' ( ()).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a list containing a blank node + $this->shouldParse('([]) .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should parse statements with a list containing multiple blank nodes + $this->shouldParse('([] [ ]) .', + ['_:b0', 'a', 'b'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b3'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b3', 'x', 'y']); + + // ### should parse statements with a blank node containing a list + $this->shouldParse('[ ()] .', + ['_:b0', 'c', 'd'], + ['_:b0', 'a', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should not parse an invalid list + $this->shouldNotParse(' (]).', + 'Expected entity but got ] on line 1.'); + + // ### should resolve IRIs against @base + $this->shouldParse("@base .\n" . + " .\n" . + "@base .\n" . + " .", + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], + ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g']); + + // ### should not resolve IRIs against @BASE + $this->shouldNotParse('@BASE .', + 'Expected entity but got @BASE on line 1.'); + + // ### should resolve IRIs against SPARQL base + $this->shouldParse("BASE \n" . + ' . ' . + 'BASE ' . + ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], + ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g']); + + // ### should resolve IRIs against a @base with query string + $this->shouldParse("@base .\n" . + "<> .\n" . + "@base .\n" . + '<> .', + ['http://ex.org/?foo', 'http://ex.org/b', 'http://ex.org/c'], + ['http://ex.org/d/?bar', 'http://ex.org/d/f', 'http://ex.org/d/g']); + + // ### should resolve IRIs with query string against @base + $this->shouldParse("@base .\n" . + " .\n". + "@base .\n" . + " .". + "@base .\n" . + '<> .', + ['http://ex.org/?', 'http://ex.org/?a', 'http://ex.org/?a=b'], + ['http://ex.org/d?', 'http://ex.org/d?a', 'http://ex.org/d?a=b'], + ['http://ex.org/d?e', 'http://ex.org/d?a', 'http://ex.org/d?a=b']); + + // ### should not resolve IRIs with colons + $this->shouldParse("@base .\n" . + " .\n" . + " .\n" . + " .", + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], + ['A:', 'b:', 'c:'], + ['a:a', 'b:B', 'C-D:c']); +//TODO : solve plus to dot and newlines + // ### should resolve datatype IRIs against @base + $this->shouldParse('@base .\n' + + ' "c"^^.\n' + + '@base .\n' + + ' "g"^^.', + ['http://ex.org/a', 'http://ex.org/b', '"c"^^http://ex.org/d'], + ['http://ex.org/d/e', 'http://ex.org/d/f', '"g"^^http://ex.org/d/h']); + + // ### should resolve IRIs against a base with a fragment + $this->shouldParse('@base .\n' + + ' <#c>.\n', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/foo#c']); + + // ### should resolve IRIs with an empty fragment + $this->shouldParse('@base .\n' + + '<#> <#c>.\n', + ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c']); + + // ### should not resolve prefixed names + $this->shouldParse('PREFIX ex: \n' + + 'ex:a ex:b ex:c .', + ['http://ex.org/a/bb/ccc/../a', 'http://ex.org/a/bb/ccc/../b', 'http://ex.org/a/bb/ccc/../c']); + + // ### should parse an empty default graph + $this->shouldParse('{}'); + + // ### should parse a one-triple default graph ending without a dot + $this->shouldParse('{ }', + ['a', 'b', 'c']); + + // ### should parse a one-triple default graph ending with a dot + $this->shouldParse('{ .}', + ['a', 'b', 'c']); + + // ### should parse a three-triple default graph ending without a dot + $this->shouldParse('{ ; ,}', + ['a', 'b', 'c'], + ['a', 'd', 'e'], + ['a', 'd', 'f']); + + // ### should parse a three-triple default graph ending with a dot + $this->shouldParse('{ ; ,.}', + ['a', 'b', 'c'], + ['a', 'd', 'e'], + ['a', 'd', 'f']); + + // ### should parse a three-triple default graph ending with a semicolon + $this->shouldParse('{ ; ,;}', + ['a', 'b', 'c'], + ['a', 'd', 'e'], + ['a', 'd', 'f']); + + // ### should parse an empty named graph with an IRI + $this->shouldParse('{}'); + + // ### should parse a one-triple named graph with an IRI ending without a dot + $this->shouldParse(' { }', + ['a', 'b', 'c', 'g']); + + // ### should parse a one-triple named graph with an IRI ending with a dot + $this->shouldParse('{ .}', + ['a', 'b', 'c', 'g']); + + // ### should parse a three-triple named graph with an IRI ending without a dot + $this->shouldParse(' { ; ,}', + ['a', 'b', 'c', 'g'], + ['a', 'd', 'e', 'g'], + ['a', 'd', 'f', 'g']); + + // ### should parse a three-triple named graph with an IRI ending with a dot + $this->shouldParse('{ ; ,.}', + ['a', 'b', 'c', 'g'], + ['a', 'd', 'e', 'g'], + ['a', 'd', 'f', 'g']); + + // ### should parse an empty named graph with a prefixed name + $this->shouldParse('@prefix g: .\ng:h {}'); + + // ### should parse a one-triple named graph with a prefixed name ending without a dot + $this->shouldParse('@prefix g: .\ng:h { }', + ['a', 'b', 'c', 'g#h']); + + // ### should parse a one-triple named graph with a prefixed name ending with a dot + $this->shouldParse('@prefix g: .\ng:h{ .}', + ['a', 'b', 'c', 'g#h']); + + // ### should parse a three-triple named graph with a prefixed name ending without a dot + $this->shouldParse('@prefix g: .\ng:h { ; ,}', + ['a', 'b', 'c', 'g#h'], + ['a', 'd', 'e', 'g#h'], + ['a', 'd', 'f', 'g#h']); + + // ### should parse a three-triple named graph with a prefixed name ending with a dot + $this->shouldParse('@prefix g: .\ng:h{ ; ,.}', + ['a', 'b', 'c', 'g#h'], + ['a', 'd', 'e', 'g#h'], + ['a', 'd', 'f', 'g#h']); + + // ### should parse an empty anonymous graph + $this->shouldParse('[] {}'); + + // ### should parse a one-triple anonymous graph ending without a dot + $this->shouldParse('[] { }', + ['a', 'b', 'c', '_:b0']); + + // ### should parse a one-triple anonymous graph ending with a dot + $this->shouldParse('[]{ .}', + ['a', 'b', 'c', '_:b0']); + + // ### should parse a three-triple anonymous graph ending without a dot + $this->shouldParse('[] { ; ,}', + ['a', 'b', 'c', '_:b0'], + ['a', 'd', 'e', '_:b0'], + ['a', 'd', 'f', '_:b0']); + + // ### should parse a three-triple anonymous graph ending with a dot + $this->shouldParse('[]{ ; ,.}', + ['a', 'b', 'c', '_:b0'], + ['a', 'd', 'e', '_:b0'], + ['a', 'd', 'f', '_:b0']); + + // ### should parse an empty named graph with an IRI and the GRAPH keyword + $this->shouldParse('GRAPH {}'); + + // ### should parse an empty named graph with a prefixed name and the GRAPH keyword + $this->shouldParse('@prefix g: .\nGRAPH g:h {}'); + + // ### should parse an empty anonymous graph and the GRAPH keyword + $this->shouldParse('GRAPH [] {}'); + + // ### should parse a one-triple named graph with an IRI and the GRAPH keyword + $this->shouldParse('GRAPH { }', + ['a', 'b', 'c', 'g']); + + // ### should parse a one-triple named graph with a prefixed name and the GRAPH keyword + $this->shouldParse('@prefix g: .\nGRAPH g:h { }', + ['a', 'b', 'c', 'g#h']); + + // ### should parse a one-triple anonymous graph and the GRAPH keyword + $this->shouldParse('GRAPH [] { }', + ['a', 'b', 'c', '_:b0']); + + // ### should parse a graph with 8-bit unicode escape sequences + $this->shouldParse('<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}\n', + ['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00']); + + // ### should not parse a single closing brace + $this->shouldNotParse('}', + 'Unexpected graph closing on line 1.'); + + // ### should not parse a single opening brace + $this->shouldNotParse('{', + 'Expected entity but got eof on line 1.'); + + // ### should not parse a superfluous closing brace + $this->shouldNotParse('{}}', + 'Unexpected graph closing on line 1.'); + + // ### should not parse a graph with only a dot + $this->shouldNotParse('{.}', + 'Expected entity but got . on line 1.'); + + // ### should not parse a graph with only a semicolon + $this->shouldNotParse('{;}', + 'Expected entity but got ; on line 1.'); + + // ### should not parse an unclosed graph + $this->shouldNotParse('{ .', + 'Unclosed graph on line 1.'); + + // ### should not parse a named graph with a list node as label + $this->shouldNotParse('() {}', + 'Expected entity but got { on line 1.'); + + // ### should not parse a named graph with a non-empty blank node as label + $this->shouldNotParse('[ ] {}', + 'Expected entity but got { on line 1.'); + + // ### should not parse a named graph with the GRAPH keyword and a non-empty blank node as label + $this->shouldNotParse('GRAPH [ ] {}', + 'Invalid graph label on line 1.'); + + // ### should not parse a triple after the GRAPH keyword + $this->shouldNotParse('GRAPH .', + 'Expected graph but got IRI on line 1.'); + + // ### should not parse repeated GRAPH keywords + $this->shouldNotParse('GRAPH GRAPH {}', + 'Invalid graph label on line 1.'); + + // ### should parse a quad with 4 IRIs + $this->shouldParse(' .', + ['a', 'b', 'c', 'g']); + + // ### should parse a quad with 4 prefixed names + $this->shouldParse('@prefix p: .\np:a p:b p:c p:g.', + ['p#a', 'p#b', 'p#c', 'p#g']); + + // ### should not parse a quad with an undefined prefix + $this->shouldNotParse(' p:g.', + 'Undefined prefix "p:" on line 1.'); + + // ### should parse a quad with 3 IRIs and a literal + $this->shouldParse(' "c"^^ .', + ['a', 'b', '"c"^^d', 'g']); + + // ### should parse a quad with 2 blank nodes and a literal + $this->shouldParse('_:a "c"^^ _:g.', + ['_:b0_a', 'b', '"c"^^d', '_:b0_g']); + + // ### should not parse a quad in a graph + $this->shouldNotParse('{ .}', + 'Expected punctuation to follow "c" on line 1.'); + + // ### should not parse a quad with different punctuation + $this->shouldNotParse(' ;', + 'Expected dot to follow quad on line 1.'); + + // ### should not parse base declarations without IRI + $this->shouldNotParse('@base a: ', + 'Expected IRI to follow base declaration on line 1.'); + + // ### should not parse improperly nested parentheses and brackets + $this->shouldNotParse(' [ (]).', + 'Expected entity but got ] on line 1.'); + + // ### should not parse improperly nested square brackets + $this->shouldNotParse(' [ ]].', + 'Expected entity but got ] on line 1.'); + + // ### should error when an object is not there + $this->shouldNotParse(' .', + 'Expected entity but got . on line 1.'); + + // ### should error when a dot is not there + $this->shouldNotParse(' ', + 'Expected entity but got eof on line 1.'); + + // ### should error with an abbreviation in the subject + $this->shouldNotParse('a .', + 'Expected entity but got abbreviation on line 1.'); + + // ### should error with an abbreviation in the object + $this->shouldNotParse(' a .', + 'Expected entity but got abbreviation on line 1.'); + + // ### should error if punctuation follows a subject + $this->shouldNotParse(' .', + 'Unexpected . on line 1.'); + + // ### should error if an unexpected token follows a subject + $this->shouldNotParse(' [', + 'Expected entity but got [ on line 1.'); + } private function shouldParse($createParser, $input = "") { @@ -354,457 +799,12 @@ private static function toSortedJSON ($items) describe('An N3Parser instance', function () { - // ### should not parse an invalid blank node - $this->shouldNotParse('[ .', - 'Expected punctuation to follow "b" on line 1.')); - - // ### should parse a statements with only an anonymous node - shouldParse('[

].', - ['_:b0', 'p', 'o'])); - - // ### should not parse a statement with only a blank anonymous node - $this->shouldNotParse('[].', - 'Unexpected . on line 1.')); - - // ### should not parse an anonymous node with only an anonymous node inside - $this->shouldNotParse('[[

]].', - 'Expected entity but got [ on line 1.')); - - // ### should parse statements with an empty list in the subject - shouldParse('() .', - ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'a', 'b'])); - - // ### should parse statements with an empty list in the object - shouldParse(' ().', - ['a', 'b', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a single-element list in the subject - shouldParse('() .', - ['_:b0', 'a', 'b'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a single-element list in the object - shouldParse(' ().', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse a list with a literal - shouldParse(' ("x").', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse a list with a typed literal - shouldParse(' ("x"^^).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"^^y'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse a list with a language-tagged literal - shouldParse(' ("x"@en-GB).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"@en-gb'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a multi-element list in the subject - shouldParse('( ) .', - ['_:b0', 'a', 'b'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a multi-element list in the object - shouldParse(' ( ).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a multi-element literal list in the object - shouldParse(' ("x" "y"@en-GB "z"^^).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"y"@en-gb'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"z"^^t'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with prefixed names in lists - shouldParse('@prefix a: . (a:x a:y).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a#x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a#y'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should not parse statements with undefined prefixes in lists - $this->shouldNotParse(' (a:x a:y).', - 'Undefined prefix "a:" on line 1.')); - - // ### should parse statements with blank nodes in lists - shouldParse(' (_:x _:y).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_y'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a nested empty list - shouldParse(' ( ()).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - // ### should parse statements with non-empty nested lists - shouldParse(' ( ()).', - ['a', 'b', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a list containing a blank node - shouldParse('([]) .', - ['_:b0', 'a', 'b'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should parse statements with a list containing multiple blank nodes - shouldParse('([] [ ]) .', - ['_:b0', 'a', 'b'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b3'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b3', 'x', 'y'])); - - // ### should parse statements with a blank node containing a list - shouldParse('[ ()] .', - ['_:b0', 'c', 'd'], - ['_:b0', 'a', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should not parse an invalid list - $this->shouldNotParse(' (]).', - 'Expected entity but got ] on line 1.')); - - // ### should resolve IRIs against @base - shouldParse("@base .\n" . - " .\n" . - "@base .\n" . - " .", - ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], - ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g'])); - - // ### should not resolve IRIs against @BASE - $this->shouldNotParse('@BASE .', - 'Expected entity but got @BASE on line 1.')); - - // ### should resolve IRIs against SPARQL base - shouldParse("BASE \n" . - ' . ' . - 'BASE ' . - ' .', - ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], - ['http://ex.org/d/e', 'http://ex.org/d/f', 'http://ex.org/d/g'])); - - // ### should resolve IRIs against a @base with query string - shouldParse("@base .\n" . - "<> .\n" . - "@base .\n" . - '<> .', - ['http://ex.org/?foo', 'http://ex.org/b', 'http://ex.org/c'], - ['http://ex.org/d/?bar', 'http://ex.org/d/f', 'http://ex.org/d/g'])); - - // ### should resolve IRIs with query string against @base - shouldParse("@base .\n" . - " .\n". - "@base .\n" . - " .". - "@base .\n" . - '<> .', - ['http://ex.org/?', 'http://ex.org/?a', 'http://ex.org/?a=b'], - ['http://ex.org/d?', 'http://ex.org/d?a', 'http://ex.org/d?a=b'], - ['http://ex.org/d?e', 'http://ex.org/d?a', 'http://ex.org/d?a=b'])); - - // ### should not resolve IRIs with colons - shouldParse("@base .\n" . - " .\n" . - " .\n" . - " .", - ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], - ['A:', 'b:', 'c:'], - ['a:a', 'b:B', 'C-D:c'])); -//TODO : solve plus to dot and newlines - // ### should resolve datatype IRIs against @base - shouldParse('@base .\n' + - ' "c"^^.\n' + - '@base .\n' + - ' "g"^^.', - ['http://ex.org/a', 'http://ex.org/b', '"c"^^http://ex.org/d'], - ['http://ex.org/d/e', 'http://ex.org/d/f', '"g"^^http://ex.org/d/h'])); - - // ### should resolve IRIs against a base with a fragment - shouldParse('@base .\n' + - ' <#c>.\n', - ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/foo#c'])); - - // ### should resolve IRIs with an empty fragment - shouldParse('@base .\n' + - '<#> <#c>.\n', - ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c'])); - - // ### should not resolve prefixed names - shouldParse('PREFIX ex: \n' + - 'ex:a ex:b ex:c .', - ['http://ex.org/a/bb/ccc/../a', 'http://ex.org/a/bb/ccc/../b', 'http://ex.org/a/bb/ccc/../c'])); - - // ### should parse an empty default graph - shouldParse('{}')); - - // ### should parse a one-triple default graph ending without a dot - shouldParse('{ }', - ['a', 'b', 'c'])); - - // ### should parse a one-triple default graph ending with a dot - shouldParse('{ .}', - ['a', 'b', 'c'])); - - // ### should parse a three-triple default graph ending without a dot - shouldParse('{ ; ,}', - ['a', 'b', 'c'], - ['a', 'd', 'e'], - ['a', 'd', 'f'])); - - // ### should parse a three-triple default graph ending with a dot - shouldParse('{ ; ,.}', - ['a', 'b', 'c'], - ['a', 'd', 'e'], - ['a', 'd', 'f'])); - - // ### should parse a three-triple default graph ending with a semicolon - shouldParse('{ ; ,;}', - ['a', 'b', 'c'], - ['a', 'd', 'e'], - ['a', 'd', 'f'])); - - // ### should parse an empty named graph with an IRI - shouldParse('{}')); - - // ### should parse a one-triple named graph with an IRI ending without a dot - shouldParse(' { }', - ['a', 'b', 'c', 'g'])); - - // ### should parse a one-triple named graph with an IRI ending with a dot - shouldParse('{ .}', - ['a', 'b', 'c', 'g'])); - - // ### should parse a three-triple named graph with an IRI ending without a dot - shouldParse(' { ; ,}', - ['a', 'b', 'c', 'g'], - ['a', 'd', 'e', 'g'], - ['a', 'd', 'f', 'g'])); - - // ### should parse a three-triple named graph with an IRI ending with a dot - shouldParse('{ ; ,.}', - ['a', 'b', 'c', 'g'], - ['a', 'd', 'e', 'g'], - ['a', 'd', 'f', 'g'])); - - // ### should parse an empty named graph with a prefixed name - shouldParse('@prefix g: .\ng:h {}')); - - // ### should parse a one-triple named graph with a prefixed name ending without a dot - shouldParse('@prefix g: .\ng:h { }', - ['a', 'b', 'c', 'g#h'])); - - // ### should parse a one-triple named graph with a prefixed name ending with a dot - shouldParse('@prefix g: .\ng:h{ .}', - ['a', 'b', 'c', 'g#h'])); - - // ### should parse a three-triple named graph with a prefixed name ending without a dot - shouldParse('@prefix g: .\ng:h { ; ,}', - ['a', 'b', 'c', 'g#h'], - ['a', 'd', 'e', 'g#h'], - ['a', 'd', 'f', 'g#h'])); - - // ### should parse a three-triple named graph with a prefixed name ending with a dot - shouldParse('@prefix g: .\ng:h{ ; ,.}', - ['a', 'b', 'c', 'g#h'], - ['a', 'd', 'e', 'g#h'], - ['a', 'd', 'f', 'g#h'])); - - // ### should parse an empty anonymous graph - shouldParse('[] {}')); - - // ### should parse a one-triple anonymous graph ending without a dot - shouldParse('[] { }', - ['a', 'b', 'c', '_:b0'])); - - // ### should parse a one-triple anonymous graph ending with a dot - shouldParse('[]{ .}', - ['a', 'b', 'c', '_:b0'])); - - // ### should parse a three-triple anonymous graph ending without a dot - shouldParse('[] { ; ,}', - ['a', 'b', 'c', '_:b0'], - ['a', 'd', 'e', '_:b0'], - ['a', 'd', 'f', '_:b0'])); - - // ### should parse a three-triple anonymous graph ending with a dot - shouldParse('[]{ ; ,.}', - ['a', 'b', 'c', '_:b0'], - ['a', 'd', 'e', '_:b0'], - ['a', 'd', 'f', '_:b0'])); - - // ### should parse an empty named graph with an IRI and the GRAPH keyword - shouldParse('GRAPH {}')); - - // ### should parse an empty named graph with a prefixed name and the GRAPH keyword - shouldParse('@prefix g: .\nGRAPH g:h {}')); - - // ### should parse an empty anonymous graph and the GRAPH keyword - shouldParse('GRAPH [] {}')); - - // ### should parse a one-triple named graph with an IRI and the GRAPH keyword - shouldParse('GRAPH { }', - ['a', 'b', 'c', 'g'])); - - // ### should parse a one-triple named graph with a prefixed name and the GRAPH keyword - shouldParse('@prefix g: .\nGRAPH g:h { }', - ['a', 'b', 'c', 'g#h'])); - - // ### should parse a one-triple anonymous graph and the GRAPH keyword - shouldParse('GRAPH [] { }', - ['a', 'b', 'c', '_:b0'])); - - // ### should parse a graph with 8-bit unicode escape sequences - shouldParse('<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}\n', - ['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00'])); - - // ### should not parse a single closing brace - $this->shouldNotParse('}', - 'Unexpected graph closing on line 1.')); - - // ### should not parse a single opening brace - $this->shouldNotParse('{', - 'Expected entity but got eof on line 1.')); - - // ### should not parse a superfluous closing brace - $this->shouldNotParse('{}}', - 'Unexpected graph closing on line 1.')); - - // ### should not parse a graph with only a dot - $this->shouldNotParse('{.}', - 'Expected entity but got . on line 1.')); - - // ### should not parse a graph with only a semicolon - $this->shouldNotParse('{;}', - 'Expected entity but got ; on line 1.')); - - // ### should not parse an unclosed graph - $this->shouldNotParse('{ .', - 'Unclosed graph on line 1.')); - - // ### should not parse a named graph with a list node as label - $this->shouldNotParse('() {}', - 'Expected entity but got { on line 1.')); - - // ### should not parse a named graph with a non-empty blank node as label - $this->shouldNotParse('[ ] {}', - 'Expected entity but got { on line 1.')); - - // ### should not parse a named graph with the GRAPH keyword and a non-empty blank node as label - $this->shouldNotParse('GRAPH [ ] {}', - 'Invalid graph label on line 1.')); - - // ### should not parse a triple after the GRAPH keyword - $this->shouldNotParse('GRAPH .', - 'Expected graph but got IRI on line 1.')); - - // ### should not parse repeated GRAPH keywords - $this->shouldNotParse('GRAPH GRAPH {}', - 'Invalid graph label on line 1.')); - - // ### should parse a quad with 4 IRIs - shouldParse(' .', - ['a', 'b', 'c', 'g'])); - - // ### should parse a quad with 4 prefixed names - shouldParse('@prefix p: .\np:a p:b p:c p:g.', - ['p#a', 'p#b', 'p#c', 'p#g'])); - - // ### should not parse a quad with an undefined prefix - $this->shouldNotParse(' p:g.', - 'Undefined prefix "p:" on line 1.')); - - // ### should parse a quad with 3 IRIs and a literal - shouldParse(' "c"^^ .', - ['a', 'b', '"c"^^d', 'g'])); - - // ### should parse a quad with 2 blank nodes and a literal - shouldParse('_:a "c"^^ _:g.', - ['_:b0_a', 'b', '"c"^^d', '_:b0_g'])); - - // ### should not parse a quad in a graph - $this->shouldNotParse('{ .}', - 'Expected punctuation to follow "c" on line 1.')); - - // ### should not parse a quad with different punctuation - $this->shouldNotParse(' ;', - 'Expected dot to follow quad on line 1.')); - - // ### should not parse base declarations without IRI - $this->shouldNotParse('@base a: ', - 'Expected IRI to follow base declaration on line 1.')); - - // ### should not parse improperly nested parentheses and brackets - $this->shouldNotParse(' [ (]).', - 'Expected entity but got ] on line 1.')); - - // ### should not parse improperly nested square brackets - $this->shouldNotParse(' [ ]].', - 'Expected entity but got ] on line 1.')); - - // ### should error when an object is not there - $this->shouldNotParse(' .', - 'Expected entity but got . on line 1.')); - - // ### should error when a dot is not there - $this->shouldNotParse(' ', - 'Expected entity but got eof on line 1.')); - - // ### should error with an abbreviation in the subject - $this->shouldNotParse('a .', - 'Expected entity but got abbreviation on line 1.')); - - // ### should error with an abbreviation in the object - $this->shouldNotParse(' a .', - 'Expected entity but got abbreviation on line 1.')); - - // ### should error if punctuation follows a subject - $this->shouldNotParse(' .', - 'Unexpected . on line 1.')); - - // ### should error if an unexpected token follows a subject - $this->shouldNotParse(' [', - 'Expected entity but got [ on line 1.')); - - // ### should not error if there is no triple callback function () { + // ### should not error if there is no triple callback function () { new N3Parser().parse(''); }); - // ### should return prefixes through a callback function (done) { + // ### should return prefixes through a callback function (done) { $prefixes = {}; new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', tripleCallback, prefixCallback); @@ -826,7 +826,7 @@ function prefixCallback(prefix, iri) { } }); - // ### should return prefixes through a callback without triple callback function (done) { + // ### should return prefixes through a callback without triple callback function (done) { $prefixes = {}; new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', null, prefixCallback); @@ -840,7 +840,7 @@ function prefixCallback(prefix, iri) { } }); - // ### should return prefixes at the last triple callback function (done) { + // ### should return prefixes at the last triple callback function (done) { new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', tripleCallback); function tripleCallback($error, $triple, prefixes) { @@ -857,17 +857,17 @@ function tripleCallback($error, $triple, prefixes) { } }); - // ### should parse a string synchronously if no callback is given function () { + // ### should parse a string synchronously if no callback is given function () { $triples = new N3Parser().parse('@prefix a: . a:a a:b a:c.'); triples.should.deep.equal([{ subject: 'urn:a:a', predicate: 'urn:a:b', object: 'urn:a:c', graph: '' }]); }); - // ### should throw on syntax errors if no callback is given function () { + // ### should throw on syntax errors if no callback is given function () { (function () { new N3Parser().parse(' bar '); }) .should.throw('Unexpected "bar" on line 1.'); }); - // ### should throw on grammar errors if no callback is given function () { + // ### should throw on grammar errors if no callback is given function () { (function () { new N3Parser().parse(' '); }) .should.throw('Expected punctuation to follow "c" on line 1.'); }); @@ -876,56 +876,56 @@ function tripleCallback($error, $triple, prefixes) { describe('An N3Parser instance with a document IRI', function () { function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f.ttl' }); } - // ### should resolve IRIs against the document IRI - shouldParse(parser, + // ### should resolve IRIs against the document IRI + $this->shouldParse(parser, '@prefix : <#>.\n' + ' .\n' + ':d :e :f :g.', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g'])); - // ### should resolve IRIs with a trailing slash against the document IRI - shouldParse(parser, + // ### should resolve IRIs with a trailing slash against the document IRI + $this->shouldParse(parser, ' .\n', ['http://ex.org/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); - // ### should resolve IRIs starting with ./ against the document IRI - shouldParse(parser, + // ### should resolve IRIs starting with ./ against the document IRI + $this->shouldParse(parser, '<./a> <./a/b> <./a/b/c>.\n', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); - // ### should resolve IRIs starting with multiple ./ sequences against the document IRI - shouldParse(parser, + // ### should resolve IRIs starting with multiple ./ sequences against the document IRI + $this->shouldParse(parser, '<./././a> <./././././a/b> <././././././a/b/c>.\n', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); - // ### should resolve IRIs starting with ../ against the document IRI - shouldParse(parser, + // ### should resolve IRIs starting with ../ against the document IRI + $this->shouldParse(parser, '<../a> <../a/b> <../a/b/c>.\n', ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/yy/a/b/c'])); - // ### should resolve IRIs starting multiple ../ sequences against the document IRI - shouldParse(parser, + // ### should resolve IRIs starting multiple ../ sequences against the document IRI + $this->shouldParse(parser, '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.\n', ['http://ex.org/x/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); - // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI - shouldParse(parser, + // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI + $this->shouldParse(parser, '<.././a> <./.././a/b> <./.././.././a/b/c>.\n', ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/a/b/c'])); - // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI - shouldParse(parser, + // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI + $this->shouldParse(parser, '<.x/a> <..x/a/b> <.../a/b/c>.\n', ['http://ex.org/x/yy/zzz/.x/a', 'http://ex.org/x/yy/zzz/..x/a/b', 'http://ex.org/x/yy/zzz/.../a/b/c'])); - // ### should resolve datatype IRIs against the document IRI - shouldParse(parser, + // ### should resolve datatype IRIs against the document IRI + $this->shouldParse(parser, ' "c"^^.', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', '"c"^^http://ex.org/x/yy/zzz/d'])); - // ### should resolve IRIs in lists against the document IRI - shouldParse(parser, + // ### should resolve IRIs in lists against the document IRI + $this->shouldParse(parser, '( )

( ).', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/a'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], @@ -937,8 +937,8 @@ function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/d'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - // ### should respect @base statements - shouldParse(parser, + // ### should respect @base statements + $this->shouldParse(parser, ' .\n' + '@base .\n' + ' .\n' + @@ -955,8 +955,8 @@ function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f describe('An N3Parser instance with a blank node prefix', function () { function parser() { return new N3Parser({ blankNodePrefix: '_:blank' }); } - // ### should use the given prefix for blank nodes - shouldParse(parser, + // ### should use the given prefix for blank nodes + $this->shouldParse(parser, '_:a _:c.\n', ['_:blanka', 'b', '_:blankc'])); }); @@ -964,8 +964,8 @@ function parser() { return new N3Parser({ blankNodePrefix: '_:blank' }); } describe('An N3Parser instance with an empty blank node prefix', function () { function parser() { return new N3Parser({ blankNodePrefix: '' }); } - // ### should not use a prefix for blank nodes - shouldParse(parser, + // ### should not use a prefix for blank nodes + $this->shouldParse(parser, '_:a _:c.\n', ['_:a', 'b', '_:c'])); }); @@ -973,227 +973,227 @@ function parser() { return new N3Parser({ blankNodePrefix: '' }); } describe('An N3Parser instance with a non-string format', function () { function parser() { return new N3Parser({ format: 1 }); } - // ### should parse a single triple - shouldParse(parser, ' .', ['a', 'b', 'c'])); + // ### should parse a single triple + $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); - // ### should parse a graph - shouldParse(parser, '{ }', ['a', 'b', 'c'])); + // ### should parse a graph + $this->shouldParse(parser, '{ }', ['a', 'b', 'c'])); }); describe('An N3Parser instance for the Turtle format', function () { function parser() { return new N3Parser({ format: 'Turtle' }); } - // ### should parse a single triple - shouldParse(parser, ' .', ['a', 'b', 'c'])); + // ### should parse a single triple + $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); - // ### should not parse a default graph + // ### should not parse a default graph $this->shouldNotParse(parser, '{}', 'Unexpected graph on line 1.')); - // ### should not parse a named graph + // ### should not parse a named graph $this->shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); - // ### should not parse a named graph with the GRAPH keyword + // ### should not parse a named graph with the GRAPH keyword $this->shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); - // ### should not parse a quad + // ### should not parse a quad $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); - // ### should not parse a variable + // ### should not parse a variable $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); - // ### should not parse an equality statement + // ### should not parse an equality statement $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); - // ### should not parse a right implication statement + // ### should not parse a right implication statement $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); - // ### should not parse a left implication statement + // ### should not parse a left implication statement $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); - // ### should not parse a formula as object + // ### should not parse a formula as object $this->shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); }); describe('An N3Parser instance for the TriG format', function () { function parser() { return new N3Parser({ format: 'TriG' }); } - // ### should parse a single triple - shouldParse(parser, ' .', ['a', 'b', 'c'])); + // ### should parse a single triple + $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); - // ### should parse a default graph - shouldParse(parser, '{}')); + // ### should parse a default graph + $this->shouldParse(parser, '{}')); - // ### should parse a named graph - shouldParse(parser, ' {}')); + // ### should parse a named graph + $this->shouldParse(parser, ' {}')); - // ### should parse a named graph with the GRAPH keyword - shouldParse(parser, 'GRAPH {}')); + // ### should parse a named graph with the GRAPH keyword + $this->shouldParse(parser, 'GRAPH {}')); - // ### should not parse a quad + // ### should not parse a quad $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); - // ### should not parse a variable + // ### should not parse a variable $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); - // ### should not parse an equality statement + // ### should not parse an equality statement $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); - // ### should not parse a right implication statement + // ### should not parse a right implication statement $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); - // ### should not parse a left implication statement + // ### should not parse a left implication statement $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); - // ### should not parse a formula as object + // ### should not parse a formula as object $this->shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); }); describe('An N3Parser instance for the N-Triples format', function () { function parser() { return new N3Parser({ format: 'N-Triples' }); } - // ### should parse a single triple - shouldParse(parser, ' "c".', + // ### should parse a single triple + $this->shouldParse(parser, ' "c".', ['http://ex.org/a', 'http://ex.org/b', '"c"'])); - // ### should not parse a single quad + // ### should not parse a single quad $this->shouldNotParse(parser, ' "c" .', 'Expected punctuation to follow ""c"" on line 1.')); - // ### should not parse relative IRIs + // ### should not parse relative IRIs $this->shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); - // ### should not parse a prefix declaration + // ### should not parse a prefix declaration $this->shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); - // ### should not parse a variable + // ### should not parse a variable $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); - // ### should not parse an equality statement + // ### should not parse an equality statement $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); - // ### should not parse a right implication statement + // ### should not parse a right implication statement $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); - // ### should not parse a left implication statement + // ### should not parse a left implication statement $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); - // ### should not parse a formula as object + // ### should not parse a formula as object $this->shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); }); describe('An N3Parser instance for the N-Quads format', function () { function parser() { return new N3Parser({ format: 'N-Quads' }); } - // ### should parse a single triple - shouldParse(parser, ' .', + // ### should parse a single triple + $this->shouldParse(parser, ' .', ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'])); - // ### should parse a single quad - shouldParse(parser, ' "c" .', + // ### should parse a single quad + $this->shouldParse(parser, ' "c" .', ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g'])); - // ### should not parse relative IRIs + // ### should not parse relative IRIs $this->shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); - // ### should not parse a prefix declaration + // ### should not parse a prefix declaration $this->shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); - // ### should not parse a variable + // ### should not parse a variable $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); - // ### should not parse an equality statement + // ### should not parse an equality statement $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); - // ### should not parse a right implication statement + // ### should not parse a right implication statement $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); - // ### should not parse a left implication statement + // ### should not parse a left implication statement $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); - // ### should not parse a formula as object + // ### should not parse a formula as object $this->shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); }); describe('An N3Parser instance for the N3 format', function () { function parser() { return new N3Parser({ format: 'N3' }); } - // ### should parse a single triple - shouldParse(parser, ' .', ['a', 'b', 'c'])); + // ### should parse a single triple + $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); - // ### should not parse a default graph + // ### should not parse a default graph $this->shouldNotParse(parser, '{}', 'Expected entity but got eof on line 1.')); - // ### should not parse a named graph + // ### should not parse a named graph $this->shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); - // ### should not parse a named graph with the GRAPH keyword + // ### should not parse a named graph with the GRAPH keyword $this->shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); - // ### should not parse a quad + // ### should not parse a quad $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); - // ### allows a blank node label in predicate position - shouldParse(parser, ' _:b .', ['a', '_:b0_b', 'c'])); + // ### allows a blank node label in predicate position + $this->shouldParse(parser, ' _:b .', ['a', '_:b0_b', 'c'])); - // ### should parse a variable - shouldParse(parser, '?a ?b ?c.', ['?a', '?b', '?c'])); + // ### should parse a variable + $this->shouldParse(parser, '?a ?b ?c.', ['?a', '?b', '?c'])); - // ### should parse a simple equality - shouldParse(parser, ' = .', + // ### should parse a simple equality + $this->shouldParse(parser, ' = .', ['a', 'http://www.w3.org/2002/07/owl#sameAs', 'b'])); - // ### should parse a simple right implication - shouldParse(parser, ' => .', + // ### should parse a simple right implication + $this->shouldParse(parser, ' => .', ['a', 'http://www.w3.org/2000/10/swap/log#implies', 'b'])); - // ### should parse a simple left implication - shouldParse(parser, ' <= .', + // ### should parse a simple left implication + $this->shouldParse(parser, ' <= .', ['b', 'http://www.w3.org/2000/10/swap/log#implies', 'a'])); - // ### should parse a right implication between one-triple graphs - shouldParse(parser, '{ ?a ?b . } => { ?a }.', + // ### should parse a right implication between one-triple graphs + $this->shouldParse(parser, '{ ?a ?b . } => { ?a }.', ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', '?a', '_:b1'])); - // ### should parse a right implication between two-triple graphs - shouldParse(parser, '{ ?a ?b . . } => { ?a, }.', + // ### should parse a right implication between two-triple graphs + $this->shouldParse(parser, '{ ?a ?b . . } => { ?a, }.', ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', 'f', '_:b0'], ['d', 'e', '?a', '_:b1'], ['d', 'e', 'f', '_:b1'])); - // ### should parse a left implication between one-triple graphs - shouldParse(parser, '{ ?a ?b . } <= { ?a }.', + // ### should parse a left implication between one-triple graphs + $this->shouldParse(parser, '{ ?a ?b . } <= { ?a }.', ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', '?a', '_:b1'])); - // ### should parse a left implication between two-triple graphs - shouldParse(parser, '{ ?a ?b . . } <= { ?a, }.', + // ### should parse a left implication between two-triple graphs + $this->shouldParse(parser, '{ ?a ?b . . } <= { ?a, }.', ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', 'f', '_:b0'], ['d', 'e', '?a', '_:b1'], ['d', 'e', 'f', '_:b1'])); - // ### should parse an equality of one-triple graphs - shouldParse(parser, '{ ?a ?b . } = { ?a }.', + // ### should parse an equality of one-triple graphs + $this->shouldParse(parser, '{ ?a ?b . } = { ?a }.', ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', '?a', '_:b1'])); - // ### should parse an equality of two-triple graphs - shouldParse(parser, '{ ?a ?b . . } = { ?a, }.', + // ### should parse an equality of two-triple graphs + $this->shouldParse(parser, '{ ?a ?b . . } = { ?a, }.', ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', 'f', '_:b0'], ['d', 'e', '?a', '_:b1'], ['d', 'e', 'f', '_:b1'])); - // ### should parse nested implication graphs - shouldParse(parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', + // ### should parse nested implication graphs + $this->shouldParse(parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', ['_:b3', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', '_:b0'], ['?a', '?b', '?c', '_:b1'], @@ -1202,8 +1202,8 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['?g', '?h', '?i', '_:b4'], ['?j', '?k', '?l', '_:b5'])); - // ### should not reuse identifiers of blank nodes within and outside of formulas - shouldParse(parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', + // ### should not reuse identifiers of blank nodes within and outside of formulas + $this->shouldParse(parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', ['_:b0_a', '_:b0_b', '_:b0_c'], ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', ''], ['_:b0.a', '_:b0.b', '_:b0.c', '_:b0'], @@ -1211,158 +1211,158 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b2.a', '_:b2.b', '_:b2.c', '_:b2'], ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3'])); - // ### should parse a @forSome statement - shouldParse(parser, '@forSome . .', + // ### should parse a @forSome statement + $this->shouldParse(parser, '@forSome . .', ['_:b0', '_:b0', '_:b0'])); - // ### should parse a @forSome statement with multiple entities - shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + // ### should parse a @forSome statement with multiple entities + $this->shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', ['_:b0', '_:b1', '_:b2'])); - // ### should not parse a @forSome statement with an invalid prefix + // ### should not parse a @forSome statement with an invalid prefix $this->shouldNotParse(parser, '@forSome a:b.', 'Undefined prefix "a:" on line 1.')); - // ### should not parse a @forSome statement with a blank node + // ### should not parse a @forSome statement with a blank node $this->shouldNotParse(parser, '@forSome _:a.', 'Unexpected blank on line 1.')); - // ### should not parse a @forSome statement with a variable + // ### should not parse a @forSome statement with a variable $this->shouldNotParse(parser, '@forSome ?a.', 'Unexpected $on line 1.')); - // ### should correctly scope @forSome statements - shouldParse(parser, '@forSome . { @forSome . . }. .', + // ### should correctly scope @forSome statements + $this->shouldParse(parser, '@forSome . { @forSome . . }. .', ['_:b0', '_:b0', '_:b1'], ['_:b2', '_:b2', '_:b2', '_:b1'], ['_:b0', '_:b0', '_:b0'])); - // ### should parse a @forAll statement - shouldParse(parser, '@forAll . .', + // ### should parse a @forAll statement + $this->shouldParse(parser, '@forAll . .', ['?b-0', '?b-0', '?b-0'])); - // ### should parse a @forAll statement with multiple entities - shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + // ### should parse a @forAll statement with multiple entities + $this->shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', ['?b-0', '?b-1', '?b-2'])); - // ### should not parse a @forAll statement with an invalid prefix + // ### should not parse a @forAll statement with an invalid prefix $this->shouldNotParse(parser, '@forAll a:b.', 'Undefined prefix "a:" on line 1.')); - // ### should not parse a @forAll statement with a blank node + // ### should not parse a @forAll statement with a blank node $this->shouldNotParse(parser, '@forAll _:a.', 'Unexpected blank on line 1.')); - // ### should not parse a @forAll statement with a variable + // ### should not parse a @forAll statement with a variable $this->shouldNotParse(parser, '@forAll ?a.', 'Unexpected $on line 1.')); - // ### should correctly scope @forAll statements - shouldParse(parser, '@forAll . { @forAll . . }. .', + // ### should correctly scope @forAll statements + $this->shouldParse(parser, '@forAll . { @forAll . . }. .', ['?b-0', '?b-0', '_:b1'], ['?b-2', '?b-2', '?b-2', '_:b1'], ['?b-0', '?b-0', '?b-0'])); - // ### should parse a ! path of length 2 as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ! path of length 2 as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ':joe!fam:mother a fam:Person.', ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); - // ### should parse a ! path of length 4 as subject - shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + + // ### should parse a ! path of length 4 as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + ':joe!fam:mother!loc:office!loc:zip loc:code 1234.', ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'l:office', '_:b1'], ['_:b1', 'l:zip', '_:b2'], ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer'])); - // ### should parse a ! path of length 2 as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ! path of length 2 as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' :joe!fam:mother.', ['x', 'is', '_:b0'], ['ex:joe', 'f:mother', '_:b0'])); - // ### should parse a ! path of length 4 as object - shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + + // ### should parse a ! path of length 4 as object + $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + ' :joe!fam:mother!loc:office!loc:zip.', ['x', 'is', '_:b2'], ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'l:office', '_:b1'], ['_:b1', 'l:zip', '_:b2'])); - // ### should parse a ^ path of length 2 as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path of length 2 as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ':joe^fam:son a fam:Person.', ['_:b0', 'f:son', 'ex:joe'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); - // ### should parse a ^ path of length 4 as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path of length 4 as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ':joe^fam:son^fam:sister^fam:mother a fam:Person.', ['_:b0', 'f:son', 'ex:joe'], ['_:b1', 'f:sister', '_:b0'], ['_:b2', 'f:mother', '_:b1'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); - // ### should parse a ^ path of length 2 as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path of length 2 as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' :joe^fam:son.', ['x', 'is', '_:b0'], ['_:b0', 'f:son', 'ex:joe'])); - // ### should parse a ^ path of length 4 as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path of length 4 as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' :joe^fam:son^fam:sister^fam:mother.', ['x', 'is', '_:b2'], ['_:b0', 'f:son', 'ex:joe'], ['_:b1', 'f:sister', '_:b0'], ['_:b2', 'f:mother', '_:b1'])); - // ### should parse mixed !/^ paths as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse mixed !/^ paths as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ':joe!fam:mother^fam:mother a fam:Person.', ['ex:joe', 'f:mother', '_:b0'], ['_:b1', 'f:mother', '_:b0'], ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); - // ### should parse mixed !/^ paths as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse mixed !/^ paths as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' :joe!fam:mother^fam:mother.', ['x', 'is', '_:b1'], ['ex:joe', 'f:mother', '_:b0'], ['_:b1', 'f:mother', '_:b0'])); - // ### should parse a ! path in a blank node as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ! path in a blank node as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + '[fam:knows :joe!fam:mother] a fam:Person.', ['_:b0', 'f:knows', '_:b1'], ['ex:joe', 'f:mother', '_:b1'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); - // ### should parse a ! path in a blank node as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ! path in a blank node as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' [fam:knows :joe!fam:mother].', ['x', 'is', '_:b0'], ['_:b0', 'f:knows', '_:b1'], ['ex:joe', 'f:mother', '_:b1'])); - // ### should parse a ^ path in a blank node as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path in a blank node as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + '[fam:knows :joe^fam:son] a fam:Person.', ['_:b0', 'f:knows', '_:b1'], ['_:b1', 'f:son', 'ex:joe'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); - // ### should parse a ^ path in a blank node as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path in a blank node as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' [fam:knows :joe^fam:son].', ['x', 'is', '_:b0'], ['_:b0', 'f:knows', '_:b1'], ['_:b1', 'f:son', 'ex:joe'])); - // ### should parse a ! path in a list as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ! path in a list as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + '( :joe!fam:mother ) a :List.', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1373,8 +1373,8 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], ['ex:joe', 'f:mother', '_:b2'])); - // ### should parse a ! path in a list as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ! path in a list as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' ( :joe!fam:mother ).', ['l', 'is', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1385,8 +1385,8 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], ['ex:joe', 'f:mother', '_:b2'])); - // ### should parse a ^ path in a list as subject - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path in a list as subject + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + '( :joe^fam:son ) a :List.', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1397,8 +1397,8 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], ['_:b2', 'f:son', 'ex:joe'])); - // ### should parse a ^ path in a list as object - shouldParse(parser, '@prefix : . @prefix fam: .' + + // ### should parse a ^ path in a list as object + $this->shouldParse(parser, '@prefix : . @prefix fam: .' + ' ( :joe^fam:son ).', ['l', 'is', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1409,25 +1409,25 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], ['_:b2', 'f:son', 'ex:joe'])); - // ### should not parse an invalid ! path + // ### should not parse an invalid ! path $this->shouldNotParse(parser, '!"invalid" ', 'Expected entity but got literal on line 1.')); - // ### should not parse an invalid ^ path + // ### should not parse an invalid ^ path $this->shouldNotParse(parser, '^"invalid" ', 'Expected entity but got literal on line 1.')); }); describe('An N3Parser instance for the N3 format with the explicitQuantifiers option', function () { function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: true }); } - // ### should parse a @forSome statement - shouldParse(parser, '@forSome . .', + // ### should parse a @forSome statement + $this->shouldParse(parser, '@forSome . .', ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], ['x', 'x', 'x'])); - // ### should parse a @forSome statement with multiple entities - shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + // ### should parse a @forSome statement with multiple entities + $this->shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], @@ -1437,8 +1437,8 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], ['a:x', 'b:y', 'a:z'])); - // ### should correctly scope @forSome statements - shouldParse(parser, '@forSome . { @forSome . . }. .', + // ### should correctly scope @forSome statements + $this->shouldParse(parser, '@forSome . { @forSome . . }. .', ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], @@ -1449,15 +1449,15 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['x', 'x', 'x', '_:b1'], ['x', 'x', 'x'])); - // ### should parse a @forAll statement - shouldParse(parser, '@forAll . .', + // ### should parse a @forAll statement + $this->shouldParse(parser, '@forAll . .', ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], ['x', 'x', 'x'])); - // ### should parse a @forAll statement with multiple entities - shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + // ### should parse a @forAll statement with multiple entities + $this->shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], @@ -1467,8 +1467,8 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], ['a:x', 'b:y', 'a:z'])); - // ### should correctly scope @forAll statements - shouldParse(parser, '@forAll . { @forAll . . }. .', + // ### should correctly scope @forAll statements + $this->shouldParse(parser, '@forAll . { @forAll . . }. .', ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], @@ -1909,30 +1909,6 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru }); }); -function shouldParse(createParser, input) { - $expected = Array.prototype.slice.call(arguments, 1); - // Shift parameters as necessary - if (createParser.call) - expected.shift(); - else - input = createParser, createParser = N3Parser; - - return function (done) { - $results = []; - $items = expected.map(function (item) { - return { subject: item[0], predicate: item[1], object: item[2], graph: item[3] || '' }; - }); - N3Parser._resetBlankNodeIds(); - createParser().parse(input, function ($error, $triple) { - expect(error).not.to.exist; - if (triple) - results.push(triple); - else - toSortedJSON(results).should.equal(toSortedJSON(items)), done(); - }); - }; -} - function itShouldResolve(baseIri, relativeIri, expected) { $result; From 7a4cd329fb60fcca749a4c464746c3a0cefd744c Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 16:20:30 +0200 Subject: [PATCH 22/72] Fixed assign by ref --- src/TriGParser.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index d3cfd39..72e9a5f 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -102,9 +102,9 @@ private function saveContext($type, $graph, $subject, $predicate, $object) { array_push($this->contextStack,[ "subject"=> $subject, "predicate"=> $predicate,"object"=> $object, "graph" => $graph, "type"=> $type, - "inverse" => $n3Mode ? clone $this->inversePredicate : false, - "blankPrefix"=> $n3Mode ? clone $this->prefixes["_"] : '', - "quantified"=> $n3Mode ? clone $this->quantified : null + "inverse" => $n3Mode ? $this->inversePredicate : false, + "blankPrefix"=> $n3Mode ? $this->prefixes["_"] : '', + "quantified"=> $n3Mode ? $this->quantified : null ]); // The settings below only apply to N3 streams if (isset($n3Mode)) { @@ -128,7 +128,7 @@ private function restoreContext() { $this->object = $context["object"]; $this->graph = $context["graph"]; // The settings below only apply to N3 streams - if ($n3Mode) { + if (isset($n3Mode)) { $this->inversePredicate = $context["inverse"]; $this->prefixes["_"] = $context["blankPrefix"]; $this->quantified = $context["quantified"]; @@ -386,8 +386,8 @@ private function initReaders () $item = null; // The item of the list $list = null; // The list itself $prevList = $this->subject; // The previous list that contains this list - $stack = $this->contextStack; // The stack of parent contexts - $parent = $stack[sizeof($stack) - 1];// The parent containing the current list + $stack = &$this->contextStack; // The stack of parent contexts + $parent = &$stack[sizeof($stack) - 1];// The parent containing the current list $next = $this->readListItem; // The next function to execute $itemComplete = true; // Whether the item has been read fully @@ -409,8 +409,9 @@ private function initReaders () $this->restoreContext(); // If this list is contained within a parent list, return the membership triple here. // This will be ` rdf:first .`. - if (sizeof($stack) !== 0 && $stack[sizeof($stack) - 1] === 'list') + if (sizeof($stack) !== 0 && $stack[sizeof($stack) - 1]["type"] === 'list') { call_user_func($this->triple, $this->subject, $this->predicate, $this->object, $this->graph); + } // Was this list the parent's subject? if ($this->predicate === null) { // The next token is the predicate @@ -441,9 +442,10 @@ private function initReaders () } // Create a new blank node if no item head was assigned yet - if ($list === null) + if ($list === null) { $list = '_:b' . $this->blankNodeCount++; - $this->subject = $list; + $this->subject = $list; + } // Is this the first element of the list? if ($prevList === null) { // This list is either the subject or the object of its parent From 15e248aedb26245d604b94a60e0c75029b752761 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 17:18:24 +0200 Subject: [PATCH 23/72] Fixed preg matching once --- src/TriGParser.php | 33 ++++++++++++++++++--------------- test/TriGParserTest.php | 23 +++++++++++------------ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 72e9a5f..5407bda 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -88,8 +88,8 @@ private function setBase ($baseIRI = null) { $baseIRI = substr($baseIRI,0, $fragmentPos); // Set base IRI and its components $this->base = $baseIRI; - $this->basePath = strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); - preg_match($schemeAuthority, $baseIRI, $matches); + $this->basePath = !strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); + preg_match($this->schemeAuthority, $baseIRI, $matches); $this->baseRoot = $matches[0]; $this->baseScheme = $matches[1]; } @@ -182,7 +182,7 @@ private function initReaders () // Read a relative or absolute IRI case 'IRI': case 'typeIRI': - $value = ($this->base === null || preg_match($this->absoluteIRI,$token["value"])) ? $token["value"] : $this->resolveIRI($token); + $value = ($this->base === null || preg_match($this->absoluteIRI,$token["value"])) ? $token["value"] : call_user_func($this->resolveIRI,$token); break; // Read a blank node or prefixed name case 'type': @@ -394,9 +394,10 @@ private function initReaders () switch ($token["type"]) { case '[': // Stack the current list triple and start a new triple with a blank node as subject + $list = '_:b' . $this->blankNodeCount++; $item = '_:b' . $this->blankNodeCount++; - $this->subject = $item; - $this->saveContext('blank', $this->graph, $list = '_:b' . $this->blankNodeCount++, self::RDF_FIRST, $this->subject); + $this->subject = $item; + $this->saveContext('blank', $this->graph, $list, self::RDF_FIRST, $this->subject); $next = $this->readBlankNodeHead; break; case '(': @@ -633,7 +634,7 @@ private function initReaders () if ($token["type"] !== 'IRI') return call_user_func($this->error,'Expected IRI to follow base declaration', $token); $this->setBase($this->base === null || preg_match($this->absoluteIRI,$token["value"]) ? - $token["value"] : $this->resolveIRI($token)); + $token["value"] : call_user_func($this->resolveIRI,$token)); return $this->readDeclarationPunctuation; }; @@ -828,18 +829,20 @@ private function initReaders () // assuming that a base path has been set and that the IRI is indeed relative $this->resolveIRI = function ($token) { $iri = $token["value"]; + if (!isset($iri[0])) // An empty relative IRI indicates the base IRI + return $this->base; + switch ($iri[0]) { - // An empty relative IRI indicates the base IRI - case undefined: return $this->base; - // Resolve relative fragment IRIs against the base IRI + // Resolve relative fragment IRIs against the base IRI case '#': return $this->base . $iri; - // Resolve relative query string IRIs by replacing the query string - case '?': return preg_replace('/(?:\?.*)?$/', $iri, $this->base); - // Resolve root-relative IRIs at the root of the base IRI + // Resolve relative query string IRIs by replacing the query string + case '?': + return preg_replace('/(?:\?.*)?$/', $iri, $this->base, 1); + // Resolve root-relative IRIs at the root of the base IRI case '/': - // Resolve scheme-relative IRIs to the scheme - return ($iri[1] === '/' ? $this->baseScheme : $this->baseRoot) . $this->removeDotSegments($iri); - // Resolve all other IRIs at the base IRI's path + // Resolve scheme-relative IRIs to the scheme + return ($iri[1] === '/' ? $this->baseScheme : $this->baseRoot) . call_user_func($this->removeDotSegments,$iri); + // Resolve all other IRIs at the base IRI's path default: return call_user_func($this->removeDotSegments, $this->basePath . $iri); } diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 00b6596..69a397d 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -477,25 +477,24 @@ public function testBlankNodes () ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], ['A:', 'b:', 'c:'], ['a:a', 'b:B', 'C-D:c']); -//TODO : solve plus to dot and newlines // ### should resolve datatype IRIs against @base - $this->shouldParse('@base .\n' + - ' "c"^^.\n' + - '@base .\n' + + $this->shouldParse("@base .\n" . + " \"c\"^^.\n" . + "@base .\n" . ' "g"^^.', ['http://ex.org/a', 'http://ex.org/b', '"c"^^http://ex.org/d'], ['http://ex.org/d/e', 'http://ex.org/d/f', '"g"^^http://ex.org/d/h']); // ### should resolve IRIs against a base with a fragment - $this->shouldParse('@base .\n' + - ' <#c>.\n', + $this->shouldParse('@base .\n' . + " <#c>.\n", ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/foo#c']); // ### should resolve IRIs with an empty fragment - $this->shouldParse('@base .\n' + - '<#> <#c>.\n', + $this->shouldParse("@base .\n" . + "<#> <#c>.\n", ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c']); - +//TODO : solve plus to dot and newlines // ### should not resolve prefixed names $this->shouldParse('PREFIX ex: \n' + 'ex:a ex:b ex:c .', @@ -554,14 +553,14 @@ public function testBlankNodes () ['a', 'd', 'f', 'g']); // ### should parse an empty named graph with a prefixed name - $this->shouldParse('@prefix g: .\ng:h {}'); + $this->shouldParse("@prefix g: .\ng:h {}"); // ### should parse a one-triple named graph with a prefixed name ending without a dot - $this->shouldParse('@prefix g: .\ng:h { }', + $this->shouldParse("@prefix g: .\ng:h { }", ['a', 'b', 'c', 'g#h']); // ### should parse a one-triple named graph with a prefixed name ending with a dot - $this->shouldParse('@prefix g: .\ng:h{ .}', + $this->shouldParse("@prefix g: .\ng:h{ .}", ['a', 'b', 'c', 'g#h']); // ### should parse a three-triple named graph with a prefixed name ending without a dot From db95c56f3c6f47d8d9ae1a946158b65b49393d85 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 17:20:50 +0200 Subject: [PATCH 24/72] Added comment --- src/TriGParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 5407bda..e5cdb68 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -836,7 +836,7 @@ private function initReaders () // Resolve relative fragment IRIs against the base IRI case '#': return $this->base . $iri; // Resolve relative query string IRIs by replacing the query string - case '?': + case '?': //should only replace the first occurence return preg_replace('/(?:\?.*)?$/', $iri, $this->base, 1); // Resolve root-relative IRIs at the root of the base IRI case '/': From e2f5612b175f989d5fcb6f0101c71ec8539e2823 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 17:22:23 +0200 Subject: [PATCH 25/72] in tests + to . --- test/TriGParserTest.php | 62 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 69a397d..5729d9c 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -486,7 +486,7 @@ public function testBlankNodes () ['http://ex.org/d/e', 'http://ex.org/d/f', '"g"^^http://ex.org/d/h']); // ### should resolve IRIs against a base with a fragment - $this->shouldParse('@base .\n' . + $this->shouldParse("@base .\n" . " <#c>.\n", ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/foo#c']); @@ -496,7 +496,7 @@ public function testBlankNodes () ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c']); //TODO : solve plus to dot and newlines // ### should not resolve prefixed names - $this->shouldParse('PREFIX ex: \n' + + $this->shouldParse('PREFIX ex: \n' . 'ex:a ex:b ex:c .', ['http://ex.org/a/bb/ccc/../a', 'http://ex.org/a/bb/ccc/../b', 'http://ex.org/a/bb/ccc/../c']); @@ -877,8 +877,8 @@ function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f // ### should resolve IRIs against the document IRI $this->shouldParse(parser, - '@prefix : <#>.\n' + - ' .\n' + + '@prefix : <#>.\n' . + ' .\n' . ':d :e :f :g.', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g'])); @@ -938,12 +938,12 @@ function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f // ### should respect @base statements $this->shouldParse(parser, - ' .\n' + - '@base .\n' + - ' .\n' + - '@base .\n' + - ' .\n' + - '@base .\n' + + ' .\n' . + '@base .\n' . + ' .\n' . + '@base .\n' . + ' .\n' . + '@base .\n' . ' .', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c'], ['http://ex.org/x/e', 'http://ex.org/x/f', 'http://ex.org/x/g'], @@ -1263,13 +1263,13 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['?b-0', '?b-0', '?b-0'])); // ### should parse a ! path of length 2 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ':joe!fam:mother a fam:Person.', ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); // ### should parse a ! path of length 4 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' . ':joe!fam:mother!loc:office!loc:zip loc:code 1234.', ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'l:office', '_:b1'], @@ -1277,13 +1277,13 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer'])); // ### should parse a ! path of length 2 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' :joe!fam:mother.', ['x', 'is', '_:b0'], ['ex:joe', 'f:mother', '_:b0'])); // ### should parse a ! path of length 4 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' . ' :joe!fam:mother!loc:office!loc:zip.', ['x', 'is', '_:b2'], ['ex:joe', 'f:mother', '_:b0'], @@ -1291,13 +1291,13 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b1', 'l:zip', '_:b2'])); // ### should parse a ^ path of length 2 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ':joe^fam:son a fam:Person.', ['_:b0', 'f:son', 'ex:joe'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); // ### should parse a ^ path of length 4 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ':joe^fam:son^fam:sister^fam:mother a fam:Person.', ['_:b0', 'f:son', 'ex:joe'], ['_:b1', 'f:sister', '_:b0'], @@ -1305,13 +1305,13 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); // ### should parse a ^ path of length 2 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' :joe^fam:son.', ['x', 'is', '_:b0'], ['_:b0', 'f:son', 'ex:joe'])); // ### should parse a ^ path of length 4 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' :joe^fam:son^fam:sister^fam:mother.', ['x', 'is', '_:b2'], ['_:b0', 'f:son', 'ex:joe'], @@ -1319,49 +1319,49 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b2', 'f:mother', '_:b1'])); // ### should parse mixed !/^ paths as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ':joe!fam:mother^fam:mother a fam:Person.', ['ex:joe', 'f:mother', '_:b0'], ['_:b1', 'f:mother', '_:b0'], ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); // ### should parse mixed !/^ paths as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' :joe!fam:mother^fam:mother.', ['x', 'is', '_:b1'], ['ex:joe', 'f:mother', '_:b0'], ['_:b1', 'f:mother', '_:b0'])); // ### should parse a ! path in a blank node as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . '[fam:knows :joe!fam:mother] a fam:Person.', ['_:b0', 'f:knows', '_:b1'], ['ex:joe', 'f:mother', '_:b1'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); // ### should parse a ! path in a blank node as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' [fam:knows :joe!fam:mother].', ['x', 'is', '_:b0'], ['_:b0', 'f:knows', '_:b1'], ['ex:joe', 'f:mother', '_:b1'])); // ### should parse a ^ path in a blank node as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . '[fam:knows :joe^fam:son] a fam:Person.', ['_:b0', 'f:knows', '_:b1'], ['_:b1', 'f:son', 'ex:joe'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); // ### should parse a ^ path in a blank node as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' [fam:knows :joe^fam:son].', ['x', 'is', '_:b0'], ['_:b0', 'f:knows', '_:b1'], ['_:b1', 'f:son', 'ex:joe'])); // ### should parse a ! path in a list as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . '( :joe!fam:mother ) a :List.', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1373,7 +1373,7 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['ex:joe', 'f:mother', '_:b2'])); // ### should parse a ! path in a list as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' ( :joe!fam:mother ).', ['l', 'is', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1385,7 +1385,7 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['ex:joe', 'f:mother', '_:b2'])); // ### should parse a ^ path in a list as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . '( :joe^fam:son ) a :List.', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1397,7 +1397,7 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b2', 'f:son', 'ex:joe'])); // ### should parse a ^ path in a list as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' + + $this->shouldParse(parser, '@prefix : . @prefix fam: .' . ' ( :joe^fam:son ).', ['l', 'is', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1911,10 +1911,10 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru function itShouldResolve(baseIri, relativeIri, expected) { $result; - describe('resolving <' + relativeIri + '> against <' + baseIri + '>', function () { + describe('resolving <' . relativeIri . '> against <' . baseIri . '>', function () { before(function (done) { try { - $doc = ' <' + relativeIri + '>.'; + $doc = ' <' . relativeIri . '>.'; new N3Parser({ documentIRI: baseIri }).parse(doc, function ($error, $triple) { if (done) result = triple, done(error); @@ -1923,7 +1923,7 @@ function itShouldResolve(baseIri, relativeIri, expected) { } catch (error) { done(error); } }); - it('should result in ' + expected, function () { + it('should result in ' . expected, function () { expect(result.object).to.equal(expected); }); }); From 1837bd0ca20e00ccc3db6b74f74e651f5efe5db9 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 17:27:49 +0200 Subject: [PATCH 26/72] PHP newlines in tests --- test/TriGParserTest.php | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 5729d9c..dcdf2c0 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -496,7 +496,7 @@ public function testBlankNodes () ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c']); //TODO : solve plus to dot and newlines // ### should not resolve prefixed names - $this->shouldParse('PREFIX ex: \n' . + $this->shouldParse('PREFIX ex: ' . "\n" . 'ex:a ex:b ex:c .', ['http://ex.org/a/bb/ccc/../a', 'http://ex.org/a/bb/ccc/../b', 'http://ex.org/a/bb/ccc/../c']); @@ -620,7 +620,7 @@ public function testBlankNodes () ['a', 'b', 'c', '_:b0']); // ### should parse a graph with 8-bit unicode escape sequences - $this->shouldParse('<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}\n', + $this->shouldParse('<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}' . "\n", ['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00']); // ### should not parse a single closing brace @@ -877,45 +877,45 @@ function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f // ### should resolve IRIs against the document IRI $this->shouldParse(parser, - '@prefix : <#>.\n' . - ' .\n' . + '@prefix : <#>.' . "\n" . + ' .' . "\n" . ':d :e :f :g.', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g'])); // ### should resolve IRIs with a trailing slash against the document IRI $this->shouldParse(parser, - ' .\n', + ' .' . "\n", ['http://ex.org/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); // ### should resolve IRIs starting with ./ against the document IRI $this->shouldParse(parser, - '<./a> <./a/b> <./a/b/c>.\n', + '<./a> <./a/b> <./a/b/c>.' . "\n", ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); // ### should resolve IRIs starting with multiple ./ sequences against the document IRI $this->shouldParse(parser, - '<./././a> <./././././a/b> <././././././a/b/c>.\n', + '<./././a> <./././././a/b> <././././././a/b/c>.' . "\n", ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); // ### should resolve IRIs starting with ../ against the document IRI $this->shouldParse(parser, - '<../a> <../a/b> <../a/b/c>.\n', + '<../a> <../a/b> <../a/b/c>.' . "\n", ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/yy/a/b/c'])); // ### should resolve IRIs starting multiple ../ sequences against the document IRI $this->shouldParse(parser, - '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.\n', + '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.' . "\n", ['http://ex.org/x/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI $this->shouldParse(parser, - '<.././a> <./.././a/b> <./.././.././a/b/c>.\n', + '<.././a> <./.././a/b> <./.././.././a/b/c>.' . "\n", ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/a/b/c'])); // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI $this->shouldParse(parser, - '<.x/a> <..x/a/b> <.../a/b/c>.\n', + '<.x/a> <..x/a/b> <.../a/b/c>.' . "\n", ['http://ex.org/x/yy/zzz/.x/a', 'http://ex.org/x/yy/zzz/..x/a/b', 'http://ex.org/x/yy/zzz/.../a/b/c'])); // ### should resolve datatype IRIs against the document IRI @@ -938,12 +938,12 @@ function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f // ### should respect @base statements $this->shouldParse(parser, - ' .\n' . - '@base .\n' . - ' .\n' . - '@base .\n' . - ' .\n' . - '@base .\n' . + ' .' . "\n" . + '@base .' . "\n" . + ' .' . "\n" . + '@base .' . "\n" . + ' .' . "\n" . + '@base .' . "\n" . ' .', ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c'], ['http://ex.org/x/e', 'http://ex.org/x/f', 'http://ex.org/x/g'], @@ -956,7 +956,7 @@ function parser() { return new N3Parser({ blankNodePrefix: '_:blank' }); } // ### should use the given prefix for blank nodes $this->shouldParse(parser, - '_:a _:c.\n', + '_:a _:c.' . "\n", ['_:blanka', 'b', '_:blankc'])); }); @@ -965,7 +965,7 @@ function parser() { return new N3Parser({ blankNodePrefix: '' }); } // ### should not use a prefix for blank nodes $this->shouldParse(parser, - '_:a _:c.\n', + '_:a _:c.' . "\n", ['_:a', 'b', '_:c'])); }); From a0966a950f71ce5b359e70ec2deb3f7200b3d8cf Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 17:29:11 +0200 Subject: [PATCH 27/72] Call user func for callbacks --- src/TriGParser.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index e5cdb68..92dce07 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -230,7 +230,7 @@ private function initReaders () return $this->readSubject; case '}': // No subject; the graph in which we are reading is closed instead - return $this->readPunctuation($token); + return call_user_func($this->readPunctuation, $token); case '@forSome': $this->subject = null; $this->predicate = 'http://www.w3.org/2000/10/swap/reify#forSome'; @@ -248,7 +248,7 @@ private function initReaders () return; // In N3 mode, the subject might be a path if (isset($this->n3Mode)) - return $this->getPathReader($this->readPredicateOrNamedGraph); + return call_user_func($this->getPathReader,$this->readPredicateOrNamedGraph); } // The next token must be a predicate, @@ -318,7 +318,7 @@ private function initReaders () return; // In N3 mode, the object might be a path if (isset($this->n3Mode)) - return $this->getPathReader($this->getContextEndReader()); + return call_user_func($this->getPathReader,$this->getContextEndReader()); } return call_user_func($this->getContextEndReader); }; @@ -468,7 +468,7 @@ private function initReaders () $this->subject = $item; $this->predicate = null; // _readPath will restore the context and output the item - return $this->getPathReader($this->readListItem); + return call_user_func($this->getPathReader,$this->readListItem); } // Output the item if it is complete if ($itemComplete) From c27c751d80de2497d57f7889430c950bfc0c380b Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 17:51:24 +0200 Subject: [PATCH 28/72] Fixed should not parse function --- src/TriGParser.php | 4 ++-- test/TriGParserTest.php | 38 ++++++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 92dce07..5238113 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -146,7 +146,7 @@ private function initReaders () // If an EOF token arrives in the top context, signal that we're done case 'eof': if ($this->graph !== null) - return $this->error('Unclosed graph', $token); + return call_user_func($this->error,'Unclosed graph', $token); unset($this->prefixes["_"]); return call_user_func($this->callback,null, null, $this->prefixes); // It could be a prefix declaration @@ -538,7 +538,7 @@ private function initReaders () // ### `_readPunctuation` reads punctuation between triples or triple parts $this->readPunctuation = function ($token) { $next; - $subject = $this->subject; + $subject = isset($this->subject)?$this->subject:null; $graph = $this->graph; $inversePredicate = $this->inversePredicate; switch ($token["type"]) { diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index dcdf2c0..d67c283 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -120,15 +120,15 @@ public function testZeroOrMoreTriples () 'Undefined prefix "a:" on line 1.'); // ### should not parse undefined prefix in predicate - $this->shouldNotParse(' b:c ', + $this->shouldNotParse(' b:c .', 'Undefined prefix "b:" on line 1.'); // ### should not parse undefined prefix in object - $this->shouldNotParse(' c:d ', + $this->shouldNotParse(' c:d .', 'Undefined prefix "c:" on line 1.'); // ### should not parse undefined prefix in datatype - $this->shouldNotParse(' "c"^^d:e ', + $this->shouldNotParse(' "c"^^d:e .', 'Undefined prefix "d:" on line 1.'); // ### should parse triples with SPARQL prefixes @@ -564,13 +564,13 @@ public function testBlankNodes () ['a', 'b', 'c', 'g#h']); // ### should parse a three-triple named graph with a prefixed name ending without a dot - $this->shouldParse('@prefix g: .\ng:h { ; ,}', + $this->shouldParse('@prefix g: .'."\n" . 'g:h { ; ,}', ['a', 'b', 'c', 'g#h'], ['a', 'd', 'e', 'g#h'], ['a', 'd', 'f', 'g#h']); // ### should parse a three-triple named graph with a prefixed name ending with a dot - $this->shouldParse('@prefix g: .\ng:h{ ; ,.}', + $this->shouldParse('@prefix g: .'."\n".'g:h{ ; ,.}', ['a', 'b', 'c', 'g#h'], ['a', 'd', 'e', 'g#h'], ['a', 'd', 'f', 'g#h']); @@ -602,7 +602,7 @@ public function testBlankNodes () $this->shouldParse('GRAPH {}'); // ### should parse an empty named graph with a prefixed name and the GRAPH keyword - $this->shouldParse('@prefix g: .\nGRAPH g:h {}'); + $this->shouldParse('@prefix g: .'."\n".'GRAPH g:h {}'); // ### should parse an empty anonymous graph and the GRAPH keyword $this->shouldParse('GRAPH [] {}'); @@ -612,17 +612,23 @@ public function testBlankNodes () ['a', 'b', 'c', 'g']); // ### should parse a one-triple named graph with a prefixed name and the GRAPH keyword - $this->shouldParse('@prefix g: .\nGRAPH g:h { }', + $this->shouldParse('@prefix g: .'."\n".'GRAPH g:h { }', ['a', 'b', 'c', 'g#h']); // ### should parse a one-triple anonymous graph and the GRAPH keyword $this->shouldParse('GRAPH [] { }', ['a', 'b', 'c', '_:b0']); + } + public function testUnicodeSequences () + { // ### should parse a graph with 8-bit unicode escape sequences - $this->shouldParse('<\\U0001d400> {\n<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>\n}' . "\n", - ['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00']); + // $this->shouldParse('<\\U0001d400> {'."\n".'<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>'."\n".'}' . "\n", + //['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00']); + } + public function testParseErrors () + { // ### should not parse a single closing brace $this->shouldNotParse('}', 'Unexpected graph closing on line 1.'); @@ -672,7 +678,7 @@ public function testBlankNodes () ['a', 'b', 'c', 'g']); // ### should parse a quad with 4 prefixed names - $this->shouldParse('@prefix p: .\np:a p:b p:c p:g.', + $this->shouldParse('@prefix p: .'."\n".'p:a p:b p:c p:g.', ['p#a', 'p#b', 'p#c', 'p#g']); // ### should not parse a quad with an undefined prefix @@ -774,14 +780,14 @@ function shouldNotParse($createParser, $input, $expectedError = null) { $parser = $createParser(); $parser->_resetBlankNodeIds(); //hackish way so we only act upon first error - $callbackCount = 0; - $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$callbackCount){ + $errorReceived = false; + $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$errorReceived){ //expect($error).not.to.exist; - if (isset($error) && $callbackCount === 0) { + if (isset($error) && !$errorReceived) { $this->assertEquals($expectedError, $error->getMessage()); - $callbackCount ++; - } else if (!isset($triple) && $callbackCount === 0) { - throw new \Exception('Expected error ' . $expectedError); + $errorReceived = true; + } else if (!isset($triple) && !$errorReceived) { + $this->fail("Expected error: " . $expectedError); } }); } From fb3022f579fa5d5bcd1d563d2633e78d3228c7e0 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 7 Apr 2017 18:09:21 +0200 Subject: [PATCH 29/72] Fixed namedGraphLabel parsing --- src/TriGParser.php | 5 +++-- test/TriGParserTest.php | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 5238113..1508ff1 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -211,6 +211,7 @@ private function initReaders () // ### `_readSubject` reads a triple's subject $this->readSubject = function ($token) { $this->predicate = null; + switch ($token["type"]) { case '[': // Start a new triple with a new blank node as subject @@ -644,8 +645,8 @@ private function initReaders () case 'IRI': case 'blank': case 'prefixed': - $this->readGraph; //TODO: what’s this? - return call_user_func($this->readSubject,$token); + call_user_func($this->readSubject,$token); + return $this->readGraph; case '[': return $this->readNamedGraphBlankLabel; default: diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index d67c283..ac2cc6c 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -622,8 +622,8 @@ public function testBlankNodes () public function testUnicodeSequences () { - // ### should parse a graph with 8-bit unicode escape sequences - // $this->shouldParse('<\\U0001d400> {'."\n".'<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>'."\n".'}' . "\n", + // ### should parse a graph with 8-bit unicode escape sequences //TODO: no idea how we can fix this in PHP + //$this->shouldParse('<\\U0001d400> {'."\n".'<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>'."\n".'}' . "\n", //['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00']); } @@ -787,7 +787,7 @@ function shouldNotParse($createParser, $input, $expectedError = null) { $this->assertEquals($expectedError, $error->getMessage()); $errorReceived = true; } else if (!isset($triple) && !$errorReceived) { - $this->fail("Expected error: " . $expectedError); + $this->fail("Expected this error to be thrown (but it wasn't): " . $expectedError); } }); } From 839628a9af369aa4c855ca34f758a74c43787284 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sat, 8 Apr 2017 20:38:10 +0200 Subject: [PATCH 30/72] substring in JS is not substr in PHP --- src/TriGParser.php | 42 ++++++-- test/TriGParserTest.php | 223 ++++++++++++++++++++++++++++------------ 2 files changed, 188 insertions(+), 77 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 1508ff1..f4529c6 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -862,7 +862,17 @@ private function initReaders () $pathStart = -1; $segmentStart = 0; $next = '/'; - + + // a function we will need here to fetch the last occurence + //search backwards for needle in haystack, and return its position + $rstrpos = function ($haystack, $needle, $offset = 0){ + $size = strlen ($haystack); + $pos = strpos (strrev($haystack), $needle, $size - $offset); + if ($pos === false) + return false; + return $size - $pos; + }; + while ($i < $length) { switch ($next) { // The path starts with the first slash after the authority @@ -880,28 +890,37 @@ private function initReaders () case '#': $i = $length; break; - // Handle '/.' or '/..' path segments + // Handle '/.' or '/..' path segments case '/': if ($iri[$i + 1] === '.') { - $next = $iri[++$i + 1]; + if (isset($iri[++$i + 1])) { + $next = $iri[$i + 1]; + } else + $next = null; switch ($next) { // Remove a '/.' segment case '/': - $result .= substr($iri, $segmentStart, $i - 1); + $result .= substr($iri, $segmentStart, $i - 1 - $segmentStart); $segmentStart = $i + 1; break; // Remove a trailing '/.' segment - case undefined: + case null: case '?': case '#': - return $result . substr($iri, $segmentStart, $i) . substr($iri,$i + 1); + return $result . substr($iri, $segmentStart, $i - $segmentStart) . substr($iri,$i + 1); // Remove a '/..' segment case '.': - if (!isset($iri[++$i + 1]) || $iri[++$i + 1] === '/' || $iri[++$i + 1] === '?' || $iri[++$i + 1] === '#') { + var_dump("WERE NOT IN: " . $iri . " WITH ID $i"); + if (isset($iri[++$i + 1])) { + $next = $iri[$i + 1]; + } else { + $next = null; + } + if ($next === null || $next === '/' || $next === '?' || $next === '#') { $next = $iri[++$i + 1]; - $result .= substr($iri, $segmentStart, $i - 2); + $result .= substr($iri, $segmentStart, $i - 2 - $segmentStart); // Try to remove the parent path from result - if (($segmentStart = $result.lastIndexOf('/')) >= $pathStart) //TODO + if (($segmentStart = $rstrpos($result,"/")) >= $pathStart) $result = substr($result,0, $segmentStart); // Remove a trailing '/..' segment if ($next !== '/') @@ -911,8 +930,11 @@ private function initReaders () } } } - $next = $iri[++$i]; + if (++$i < $length) { + $next = $iri[$i]; + } } + return $result . substr($iri, $segmentStart); }; } diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index ac2cc6c..43be482 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -738,74 +738,9 @@ public function testParseErrors () 'Expected entity but got [ on line 1.'); } - private function shouldParse($createParser, $input = "") + public function testInterface() { - $expected = array_slice(func_get_args(),1); - // Shift parameters as necessary - if (is_callable($createParser)) - array_shift($expected); - else { - $input = $createParser; - $createParser = function () { - return new TriGParser(); - }; - } - $results = []; - $items = array_map(function ($item) { - return [ "subject" => $item[0], "predicate"=> $item[1], "object"=> $item[2], "graph"=> isset($item[3])?$item[3]:'' ]; - }, $expected); - $parser = $createParser(); - $parser->_resetBlankNodeIds(); - $parser->parse($input, function ($error, $triple = null) use (&$results, &$items){ - //expect($error).not.to.exist; - if ($triple) - array_push($results, $triple); - else - $this->assertEquals(self::toSortedJSON($items), self::toSortedJSON($results)); - }); - $parser->_resetBlankNodeIds(); - } - - - function shouldNotParse($createParser, $input, $expectedError = null) { - $expected = array_slice(func_get_args(),1); - // Shift parameters as necessary - if (!is_callable($createParser)) { - $expectedError = $input; - $input = $createParser; - $createParser = function () { - return new TriGParser(); - }; - } - $parser = $createParser(); - $parser->_resetBlankNodeIds(); - //hackish way so we only act upon first error - $errorReceived = false; - $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$errorReceived){ - //expect($error).not.to.exist; - if (isset($error) && !$errorReceived) { - $this->assertEquals($expectedError, $error->getMessage()); - $errorReceived = true; - } else if (!isset($triple) && !$errorReceived) { - $this->fail("Expected this error to be thrown (but it wasn't): " . $expectedError); - } - }); - } - - - private static function toSortedJSON ($items) - { - $triples = array_map("json_encode", $items); - sort($triples); - return "[\n " . join("\n ", $triples) . "\n]"; - } -} -/* - - describe('An N3Parser instance', function () { - - - // ### should not error if there is no triple callback function () { + /* // ### should not error if there is no triple callback function () { new N3Parser().parse(''); }); @@ -877,6 +812,160 @@ function tripleCallback($error, $triple, prefixes) { .should.throw('Expected punctuation to follow "c" on line 1.'); }); }); +*/ + } + + + public function testParserWithIRI() + { + $parser = function () { return new TriGParser([ "documentIRI" => 'http://ex.org/x/yy/zzz/f.ttl' ]); }; + + + // ### should resolve IRIs against the document IRI + $this->shouldParse($parser, + '@prefix : <#>.' . "\n" . + ' .' . "\n" . + ':d :e :f :g.', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], + ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g']); + + // ### should resolve IRIs with a trailing slash against the document IRI + $this->shouldParse($parser, + ' .' . "\n", + ['http://ex.org/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c']); + + // ### should resolve IRIs starting with ./ against the document IRI + $this->shouldParse($parser, + '<./a> <./a/b> <./a/b/c>.' . "\n", + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c']); + + // ### should resolve IRIs starting with multiple ./ sequences against the document IRI + $this->shouldParse($parser, + '<./././a> <./././././a/b> <././././././a/b/c>.' . "\n", + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c']); + + // ### should resolve IRIs starting with ../ against the document IRI + $this->shouldParse($parser, + '<../a> <../a/b> <../a/b/c>.' . "\n", + ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/yy/a/b/c']); + + // ### should resolve IRIs starting multiple ../ sequences against the document IRI + $this->shouldParse($parser, + '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.' . "\n", + ['http://ex.org/x/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c']); + + // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI + $this->shouldParse($parser, + '<.././a> <./.././a/b> <./.././.././a/b/c>.' . "\n", + ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/a/b/c']); + + // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI + $this->shouldParse($parser, + '<.x/a> <..x/a/b> <.../a/b/c>.' . "\n", + ['http://ex.org/x/yy/zzz/.x/a', 'http://ex.org/x/yy/zzz/..x/a/b', 'http://ex.org/x/yy/zzz/.../a/b/c']); + + // ### should resolve datatype IRIs against the document IRI + $this->shouldParse($parser, + ' "c"^^.', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', '"c"^^http://ex.org/x/yy/zzz/d']); + + // ### should resolve IRIs in lists against the document IRI + $this->shouldParse($parser, + '( )

( ).', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/b'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0', 'http://ex.org/x/yy/zzz/p', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/c'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/d'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should respect @base statements + $this->shouldParse($parser, + ' .' . "\n" . + '@base .' . "\n" . + ' .' . "\n" . + '@base .' . "\n" . + ' .' . "\n" . + '@base .' . "\n" . + ' .', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c'], + ['http://ex.org/x/e', 'http://ex.org/x/f', 'http://ex.org/x/g'], + ['http://ex.org/x/d/h', 'http://ex.org/x/d/i', 'http://ex.org/x/d/j'], + ['http://ex.org/e/k', 'http://ex.org/e/l', 'http://ex.org/e/m']); + + } + + + private function shouldParse($createParser, $input = "") + { + $expected = array_slice(func_get_args(),1); + // Shift parameters as necessary + if (is_callable($createParser)) + array_shift($expected); + else { + $input = $createParser; + $createParser = function () { + return new TriGParser(); + }; + } + $results = []; + $items = array_map(function ($item) { + return [ "subject" => $item[0], "predicate"=> $item[1], "object"=> $item[2], "graph"=> isset($item[3])?$item[3]:'' ]; + }, $expected); + $parser = $createParser(); + $parser->_resetBlankNodeIds(); + $parser->parse($input, function ($error, $triple = null) use (&$results, &$items){ + //expect($error).not.to.exist; + if ($triple) + array_push($results, $triple); + else + $this->assertEquals(self::toSortedJSON($items), self::toSortedJSON($results)); + }); + $parser->_resetBlankNodeIds(); + } + + + function shouldNotParse($createParser, $input, $expectedError = null) { + $expected = array_slice(func_get_args(),1); + // Shift parameters as necessary + if (!is_callable($createParser)) { + $expectedError = $input; + $input = $createParser; + $createParser = function () { + return new TriGParser(); + }; + } + $parser = $createParser(); + $parser->_resetBlankNodeIds(); + //hackish way so we only act upon first error + $errorReceived = false; + $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$errorReceived){ + //expect($error).not.to.exist; + if (isset($error) && !$errorReceived) { + $this->assertEquals($expectedError, $error->getMessage()); + $errorReceived = true; + } else if (!isset($triple) && !$errorReceived) { + $this->fail("Expected this error to be thrown (but it wasn't): " . $expectedError); + } + }); + } + + + private static function toSortedJSON ($items) + { + $triples = array_map("json_encode", $items); + sort($triples); + return "[\n " . join("\n ", $triples) . "\n]"; + } +} +/* + + describe('An N3Parser instance', function () { + + describe('An N3Parser instance with a document IRI', function () { function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f.ttl' }); } From 9a34c52a7b34b643dc50db813d5af8c736dc48f1 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sat, 8 Apr 2017 23:09:19 +0200 Subject: [PATCH 31/72] Fixed IRI parsing --- src/TriGParser.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index f4529c6..12ce82d 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -865,12 +865,12 @@ private function initReaders () // a function we will need here to fetch the last occurence //search backwards for needle in haystack, and return its position - $rstrpos = function ($haystack, $needle, $offset = 0){ + $rstrpos = function ($haystack, $needle){ $size = strlen ($haystack); - $pos = strpos (strrev($haystack), $needle, $size - $offset); + $pos = strpos (strrev($haystack), $needle); if ($pos === false) return false; - return $size - $pos; + return $size - $pos -1; }; while ($i < $length) { @@ -910,18 +910,17 @@ private function initReaders () return $result . substr($iri, $segmentStart, $i - $segmentStart) . substr($iri,$i + 1); // Remove a '/..' segment case '.': - var_dump("WERE NOT IN: " . $iri . " WITH ID $i"); if (isset($iri[++$i + 1])) { - $next = $iri[$i + 1]; + $next = $iri[$i + 1]; } else { $next = null; } if ($next === null || $next === '/' || $next === '?' || $next === '#') { - $next = $iri[++$i + 1]; $result .= substr($iri, $segmentStart, $i - 2 - $segmentStart); // Try to remove the parent path from result - if (($segmentStart = $rstrpos($result,"/")) >= $pathStart) + if (($segmentStart = $rstrpos($result,"/")) >= $pathStart) { $result = substr($result,0, $segmentStart); + } // Remove a trailing '/..' segment if ($next !== '/') return $result . '/' . substr($iri,$i + 1); From b490c02bfca2c2242151ba07172319e32460b6a7 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sat, 8 Apr 2017 23:13:01 +0200 Subject: [PATCH 32/72] With document IRI works as well --- test/TriGParserTest.php | 157 +++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 76 deletions(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 43be482..8a98a57 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -898,6 +898,87 @@ public function testParserWithIRI() } + public function testParserWithDocumentIRI () + { + $parser = function () { + return new TriGParser(["documentIRI" => 'http://ex.org/x/yy/zzz/f.ttl' ]); + }; + + // ### should resolve IRIs against the document IRI + $this->shouldParse($parser, + '@prefix : <#>.' . "\n" . + ' .' . "\n" . + ':d :e :f :g.', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], + ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g']); + + // ### should resolve IRIs with a trailing slash against the document IRI + $this->shouldParse($parser, + ' .' . "\n", + ['http://ex.org/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c']); + + // ### should resolve IRIs starting with ./ against the document IRI + $this->shouldParse($parser, + '<./a> <./a/b> <./a/b/c>.' . "\n", + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c']); + + // ### should resolve IRIs starting with multiple ./ sequences against the document IRI + $this->shouldParse($parser, + '<./././a> <./././././a/b> <././././././a/b/c>.' . "\n", + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c']); + + // ### should resolve IRIs starting with ../ against the document IRI + $this->shouldParse($parser, + '<../a> <../a/b> <../a/b/c>.' . "\n", + ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/yy/a/b/c']); + + // ### should resolve IRIs starting multiple ../ sequences against the document IRI + $this->shouldParse($parser, + '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.' . "\n", + ['http://ex.org/x/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c']); + + // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI + $this->shouldParse($parser, + '<.././a> <./.././a/b> <./.././.././a/b/c>.' . "\n", + ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/a/b/c']); + + // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI + $this->shouldParse($parser, + '<.x/a> <..x/a/b> <.../a/b/c>.' . "\n", + ['http://ex.org/x/yy/zzz/.x/a', 'http://ex.org/x/yy/zzz/..x/a/b', 'http://ex.org/x/yy/zzz/.../a/b/c']); + + // ### should resolve datatype IRIs against the document IRI + $this->shouldParse($parser, + ' "c"^^.', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', '"c"^^http://ex.org/x/yy/zzz/d']); + + // ### should resolve IRIs in lists against the document IRI + $this->shouldParse($parser, + '( )

( ).', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/b'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0', 'http://ex.org/x/yy/zzz/p', '_:b2'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/c'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/d'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']); + + // ### should respect @base statements + $this->shouldParse($parser, + ' .' . "\n" . + '@base .' . "\n" . + ' .' . "\n" . + '@base .' . "\n" . + ' .' . "\n" . + '@base .' . "\n" . + ' .', + ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c'], + ['http://ex.org/x/e', 'http://ex.org/x/f', 'http://ex.org/x/g'], + ['http://ex.org/x/d/h', 'http://ex.org/x/d/i', 'http://ex.org/x/d/j'], + ['http://ex.org/e/k', 'http://ex.org/e/l', 'http://ex.org/e/m']); + } private function shouldParse($createParser, $input = "") { @@ -968,82 +1049,6 @@ private static function toSortedJSON ($items) describe('An N3Parser instance with a document IRI', function () { - function parser() { return new N3Parser({ documentIRI: 'http://ex.org/x/yy/zzz/f.ttl' }); } - - // ### should resolve IRIs against the document IRI - $this->shouldParse(parser, - '@prefix : <#>.' . "\n" . - ' .' . "\n" . - ':d :e :f :g.', - ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c', 'http://ex.org/x/yy/zzz/g'], - ['http://ex.org/x/yy/zzz/f.ttl#d', 'http://ex.org/x/yy/zzz/f.ttl#e', 'http://ex.org/x/yy/zzz/f.ttl#f', 'http://ex.org/x/yy/zzz/f.ttl#g'])); - - // ### should resolve IRIs with a trailing slash against the document IRI - $this->shouldParse(parser, - ' .' . "\n", - ['http://ex.org/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); - - // ### should resolve IRIs starting with ./ against the document IRI - $this->shouldParse(parser, - '<./a> <./a/b> <./a/b/c>.' . "\n", - ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); - - // ### should resolve IRIs starting with multiple ./ sequences against the document IRI - $this->shouldParse(parser, - '<./././a> <./././././a/b> <././././././a/b/c>.' . "\n", - ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/a/b', 'http://ex.org/x/yy/zzz/a/b/c'])); - - // ### should resolve IRIs starting with ../ against the document IRI - $this->shouldParse(parser, - '<../a> <../a/b> <../a/b/c>.' . "\n", - ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/yy/a/b/c'])); - - // ### should resolve IRIs starting multiple ../ sequences against the document IRI - $this->shouldParse(parser, - '<../../a> <../../../a/b> <../../../../../../../../a/b/c>.' . "\n", - ['http://ex.org/x/a', 'http://ex.org/a/b', 'http://ex.org/a/b/c'])); - - // ### should resolve IRIs starting with mixes of ./ and ../ sequences against the document IRI - $this->shouldParse(parser, - '<.././a> <./.././a/b> <./.././.././a/b/c>.' . "\n", - ['http://ex.org/x/yy/a', 'http://ex.org/x/yy/a/b', 'http://ex.org/x/a/b/c'])); - - // ### should resolve IRIs starting with .x, ..x, or .../ against the document IRI - $this->shouldParse(parser, - '<.x/a> <..x/a/b> <.../a/b/c>.' . "\n", - ['http://ex.org/x/yy/zzz/.x/a', 'http://ex.org/x/yy/zzz/..x/a/b', 'http://ex.org/x/yy/zzz/.../a/b/c'])); - - // ### should resolve datatype IRIs against the document IRI - $this->shouldParse(parser, - ' "c"^^.', - ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', '"c"^^http://ex.org/x/yy/zzz/d'])); - - // ### should resolve IRIs in lists against the document IRI - $this->shouldParse(parser, - '( )

( ).', - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/a'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/b'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b0', 'http://ex.org/x/yy/zzz/p', '_:b2'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/c'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'http://ex.org/x/yy/zzz/d'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'])); - - // ### should respect @base statements - $this->shouldParse(parser, - ' .' . "\n" . - '@base .' . "\n" . - ' .' . "\n" . - '@base .' . "\n" . - ' .' . "\n" . - '@base .' . "\n" . - ' .', - ['http://ex.org/x/yy/zzz/a', 'http://ex.org/x/yy/zzz/b', 'http://ex.org/x/yy/zzz/c'], - ['http://ex.org/x/e', 'http://ex.org/x/f', 'http://ex.org/x/g'], - ['http://ex.org/x/d/h', 'http://ex.org/x/d/i', 'http://ex.org/x/d/j'], - ['http://ex.org/e/k', 'http://ex.org/e/l', 'http://ex.org/e/m'])); }); describe('An N3Parser instance with a blank node prefix', function () { From d309d2b2703af0e96557ae5ccaaea4101cc501ef Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sat, 8 Apr 2017 23:26:25 +0200 Subject: [PATCH 33/72] todo: fix blank node prefix --- test/TriGParserTest.php | 377 ++++++++++++++++++++-------------------- 1 file changed, 186 insertions(+), 191 deletions(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 8a98a57..f255844 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -979,6 +979,33 @@ public function testParserWithDocumentIRI () ['http://ex.org/x/d/h', 'http://ex.org/x/d/i', 'http://ex.org/x/d/j'], ['http://ex.org/e/k', 'http://ex.org/e/l', 'http://ex.org/e/m']); } + + public function testDifferentSettings() + { + $parser = function () { return new TriGParser([ "blankNodePrefix" => '_:blank' ]); }; + + // ### should use the given prefix for blank nodes + $this->shouldParse($parser, + '_:a _:c.' . "\n", + ['_:blanka', 'b', '_:blankc']); + + $parser = function () { return new TriGParser([ 'blankNodePrefix' => '' ]); }; + + // ### should not use a prefix for blank nodes + $this->shouldParse($parser, + '_:a _:c.' . "\n", + ['_:a', 'b', '_:c']); + + $parser = function () { return new TriGParser([ "format" => 1 ]); }; + + // ### should parse a single triple + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); + + // ### should parse a graph + $this->shouldParse($parser, '{ }', ['a', 'b', 'c']); + + } + private function shouldParse($createParser, $input = "") { @@ -1044,424 +1071,392 @@ private static function toSortedJSON ($items) } /* - describe('An N3Parser instance', function () { - - - - describe('An N3Parser instance with a document IRI', function () { - }); - describe('An N3Parser instance with a blank node prefix', function () { - function parser() { return new N3Parser({ blankNodePrefix: '_:blank' }); } - - // ### should use the given prefix for blank nodes - $this->shouldParse(parser, - '_:a _:c.' . "\n", - ['_:blanka', 'b', '_:blankc'])); - }); - describe('An N3Parser instance with an empty blank node prefix', function () { - function parser() { return new N3Parser({ blankNodePrefix: '' }); } - - // ### should not use a prefix for blank nodes - $this->shouldParse(parser, - '_:a _:c.' . "\n", - ['_:a', 'b', '_:c'])); - }); - - describe('An N3Parser instance with a non-string format', function () { - function parser() { return new N3Parser({ format: 1 }); } - - // ### should parse a single triple - $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); - - // ### should parse a graph - $this->shouldParse(parser, '{ }', ['a', 'b', 'c'])); - }); - describe('An N3Parser instance for the Turtle format', function () { - function parser() { return new N3Parser({ format: 'Turtle' }); } + describe('An TriGParser instance for the Turtle format', function () { + $parser = function () { return new TriGParser([ format: 'Turtle' ]); }; // ### should parse a single triple - $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); // ### should not parse a default graph - $this->shouldNotParse(parser, '{}', 'Unexpected graph on line 1.')); + $this->shouldNotParse($parser, '{}', 'Unexpected graph on line 1.'); // ### should not parse a named graph - $this->shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); + $this->shouldNotParse($parser, ' {}', 'Expected entity but got { on line 1.'); // ### should not parse a named graph with the GRAPH keyword - $this->shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); + $this->shouldNotParse($parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.'); // ### should not parse a quad - $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); // ### should not parse a variable - $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); // ### should not parse an equality statement - $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); // ### should not parse a right implication statement - $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); // ### should not parse a left implication statement - $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); // ### should not parse a formula as object - $this->shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); + $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); }); - describe('An N3Parser instance for the TriG format', function () { - function parser() { return new N3Parser({ format: 'TriG' }); } + describe('An TriGParser instance for the TriG format', function () { + $parser = function () { return new TriGParser([ format: 'TriG' ]); }; // ### should parse a single triple - $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); // ### should parse a default graph - $this->shouldParse(parser, '{}')); + $this->shouldParse($parser, '{}'); // ### should parse a named graph - $this->shouldParse(parser, ' {}')); + $this->shouldParse($parser, ' {}'); // ### should parse a named graph with the GRAPH keyword - $this->shouldParse(parser, 'GRAPH {}')); + $this->shouldParse($parser, 'GRAPH {}'); // ### should not parse a quad - $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); // ### should not parse a variable - $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); // ### should not parse an equality statement - $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); // ### should not parse a right implication statement - $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); // ### should not parse a left implication statement - $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); // ### should not parse a formula as object - $this->shouldNotParse(parser, ' {}.', 'Unexpected graph on line 1.')); + $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); }); - describe('An N3Parser instance for the N-Triples format', function () { - function parser() { return new N3Parser({ format: 'N-Triples' }); } + describe('An TriGParser instance for the N-Triples format', function () { + $parser = function () { return new TriGParser([ format: 'N-Triples' ]); }; // ### should parse a single triple - $this->shouldParse(parser, ' "c".', - ['http://ex.org/a', 'http://ex.org/b', '"c"'])); + $this->shouldParse($parser, ' "c".', + ['http://ex.org/a', 'http://ex.org/b', '"c"']); // ### should not parse a single quad - $this->shouldNotParse(parser, ' "c" .', - 'Expected punctuation to follow ""c"" on line 1.')); + $this->shouldNotParse($parser, ' "c" .', + 'Expected punctuation to follow ""c"" on line 1.'); // ### should not parse relative IRIs - $this->shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); + $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); // ### should not parse a prefix declaration - $this->shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); + $this->shouldNotParse($parser, '@prefix : .', 'Unexpected "@prefix" on line 1.'); // ### should not parse a variable - $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); // ### should not parse an equality statement - $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); // ### should not parse a right implication statement - $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); // ### should not parse a left implication statement - $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); // ### should not parse a formula as object - $this->shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); + $this->shouldNotParse($parser, ' {}.', 'Unexpected "{" on line 1.'); }); - describe('An N3Parser instance for the N-Quads format', function () { - function parser() { return new N3Parser({ format: 'N-Quads' }); } + describe('An TriGParser instance for the N-Quads format', function () { + $parser = function () { return new TriGParser([ format: 'N-Quads' ]); }; // ### should parse a single triple - $this->shouldParse(parser, ' .', - ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'])); + $this->shouldParse($parser, ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c']); // ### should parse a single quad - $this->shouldParse(parser, ' "c" .', - ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g'])); + $this->shouldParse($parser, ' "c" .', + ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g']); // ### should not parse relative IRIs - $this->shouldNotParse(parser, ' .', 'Disallowed relative IRI on line 1.')); + $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); // ### should not parse a prefix declaration - $this->shouldNotParse(parser, '@prefix : .', 'Unexpected "@prefix" on line 1.')); + $this->shouldNotParse($parser, '@prefix : .', 'Unexpected "@prefix" on line 1.'); // ### should not parse a variable - $this->shouldNotParse(parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.')); + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); // ### should not parse an equality statement - $this->shouldNotParse(parser, ' = .', 'Unexpected "=" on line 1.')); + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); // ### should not parse a right implication statement - $this->shouldNotParse(parser, ' => .', 'Unexpected "=>" on line 1.')); + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); // ### should not parse a left implication statement - $this->shouldNotParse(parser, ' <= .', 'Unexpected "<=" on line 1.')); + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); // ### should not parse a formula as object - $this->shouldNotParse(parser, ' {}.', 'Unexpected "{" on line 1.')); + $this->shouldNotParse($parser, ' {}.', 'Unexpected "{" on line 1.'); }); - describe('An N3Parser instance for the N3 format', function () { - function parser() { return new N3Parser({ format: 'N3' }); } + describe('An TriGParser instance for the N3 format', function () { + $parser = function () { return new TriGParser([ format: 'N3' ]); }; // ### should parse a single triple - $this->shouldParse(parser, ' .', ['a', 'b', 'c'])); + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); // ### should not parse a default graph - $this->shouldNotParse(parser, '{}', 'Expected entity but got eof on line 1.')); + $this->shouldNotParse($parser, '{}', 'Expected entity but got eof on line 1.'); // ### should not parse a named graph - $this->shouldNotParse(parser, ' {}', 'Expected entity but got { on line 1.')); + $this->shouldNotParse($parser, ' {}', 'Expected entity but got { on line 1.'); // ### should not parse a named graph with the GRAPH keyword - $this->shouldNotParse(parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.')); + $this->shouldNotParse($parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.'); // ### should not parse a quad - $this->shouldNotParse(parser, ' .', 'Expected punctuation to follow "c" on line 1.')); + $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); // ### allows a blank node label in predicate position - $this->shouldParse(parser, ' _:b .', ['a', '_:b0_b', 'c'])); + $this->shouldParse($parser, ' _:b .', ['a', '_:b0_b', 'c']); // ### should parse a variable - $this->shouldParse(parser, '?a ?b ?c.', ['?a', '?b', '?c'])); + $this->shouldParse($parser, '?a ?b ?c.', ['?a', '?b', '?c']); // ### should parse a simple equality - $this->shouldParse(parser, ' = .', - ['a', 'http://www.w3.org/2002/07/owl#sameAs', 'b'])); + $this->shouldParse($parser, ' = .', + ['a', 'http://www.w3.org/2002/07/owl#sameAs', 'b']); // ### should parse a simple right implication - $this->shouldParse(parser, ' => .', - ['a', 'http://www.w3.org/2000/10/swap/log#implies', 'b'])); + $this->shouldParse($parser, ' => .', + ['a', 'http://www.w3.org/2000/10/swap/log#implies', 'b']); // ### should parse a simple left implication - $this->shouldParse(parser, ' <= .', - ['b', 'http://www.w3.org/2000/10/swap/log#implies', 'a'])); + $this->shouldParse($parser, ' <= .', + ['b', 'http://www.w3.org/2000/10/swap/log#implies', 'a']); // ### should parse a right implication between one-triple graphs - $this->shouldParse(parser, '{ ?a ?b . } => { ?a }.', + $this->shouldParse($parser, '{ ?a ?b . } => { ?a }.', ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], ['?a', '?b', 'c', '_:b0'], - ['d', 'e', '?a', '_:b1'])); + ['d', 'e', '?a', '_:b1']); // ### should parse a right implication between two-triple graphs - $this->shouldParse(parser, '{ ?a ?b . . } => { ?a, }.', + $this->shouldParse($parser, '{ ?a ?b . . } => { ?a, }.', ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', 'f', '_:b0'], ['d', 'e', '?a', '_:b1'], - ['d', 'e', 'f', '_:b1'])); + ['d', 'e', 'f', '_:b1']); // ### should parse a left implication between one-triple graphs - $this->shouldParse(parser, '{ ?a ?b . } <= { ?a }.', + $this->shouldParse($parser, '{ ?a ?b . } <= { ?a }.', ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], ['?a', '?b', 'c', '_:b0'], - ['d', 'e', '?a', '_:b1'])); + ['d', 'e', '?a', '_:b1']); // ### should parse a left implication between two-triple graphs - $this->shouldParse(parser, '{ ?a ?b . . } <= { ?a, }.', + $this->shouldParse($parser, '{ ?a ?b . . } <= { ?a, }.', ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', 'f', '_:b0'], ['d', 'e', '?a', '_:b1'], - ['d', 'e', 'f', '_:b1'])); + ['d', 'e', 'f', '_:b1']); // ### should parse an equality of one-triple graphs - $this->shouldParse(parser, '{ ?a ?b . } = { ?a }.', + $this->shouldParse($parser, '{ ?a ?b . } = { ?a }.', ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], ['?a', '?b', 'c', '_:b0'], - ['d', 'e', '?a', '_:b1'])); + ['d', 'e', '?a', '_:b1']); // ### should parse an equality of two-triple graphs - $this->shouldParse(parser, '{ ?a ?b . . } = { ?a, }.', + $this->shouldParse($parser, '{ ?a ?b . . } = { ?a, }.', ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], ['?a', '?b', 'c', '_:b0'], ['d', 'e', 'f', '_:b0'], ['d', 'e', '?a', '_:b1'], - ['d', 'e', 'f', '_:b1'])); + ['d', 'e', 'f', '_:b1']); // ### should parse nested implication graphs - $this->shouldParse(parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', + $this->shouldParse($parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', ['_:b3', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', '_:b0'], ['?a', '?b', '?c', '_:b1'], ['?d', '?e', '?f', '_:b2'], ['_:b4', 'http://www.w3.org/2000/10/swap/log#implies', '_:b5', '_:b3'], ['?g', '?h', '?i', '_:b4'], - ['?j', '?k', '?l', '_:b5'])); + ['?j', '?k', '?l', '_:b5']); // ### should not reuse identifiers of blank nodes within and outside of formulas - $this->shouldParse(parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', + $this->shouldParse($parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', ['_:b0_a', '_:b0_b', '_:b0_c'], ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', ''], ['_:b0.a', '_:b0.b', '_:b0.c', '_:b0'], ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b3', '_:b1'], ['_:b2.a', '_:b2.b', '_:b2.c', '_:b2'], - ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3'])); + ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3']); // ### should parse a @forSome statement - $this->shouldParse(parser, '@forSome . .', - ['_:b0', '_:b0', '_:b0'])); + $this->shouldParse($parser, '@forSome . .', + ['_:b0', '_:b0', '_:b0']); // ### should parse a @forSome statement with multiple entities - $this->shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', - ['_:b0', '_:b1', '_:b2'])); + $this->shouldParse($parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + ['_:b0', '_:b1', '_:b2']); // ### should not parse a @forSome statement with an invalid prefix - $this->shouldNotParse(parser, '@forSome a:b.', - 'Undefined prefix "a:" on line 1.')); + $this->shouldNotParse($parser, '@forSome a:b.', + 'Undefined prefix "a:" on line 1.'); // ### should not parse a @forSome statement with a blank node - $this->shouldNotParse(parser, '@forSome _:a.', - 'Unexpected blank on line 1.')); + $this->shouldNotParse($parser, '@forSome _:a.', + 'Unexpected blank on line 1.'); // ### should not parse a @forSome statement with a variable - $this->shouldNotParse(parser, '@forSome ?a.', - 'Unexpected $on line 1.')); + $this->shouldNotParse($parser, '@forSome ?a.', + 'Unexpected $on line 1.'); // ### should correctly scope @forSome statements - $this->shouldParse(parser, '@forSome . { @forSome . . }. .', + $this->shouldParse($parser, '@forSome . { @forSome . . }. .', ['_:b0', '_:b0', '_:b1'], ['_:b2', '_:b2', '_:b2', '_:b1'], - ['_:b0', '_:b0', '_:b0'])); + ['_:b0', '_:b0', '_:b0']); // ### should parse a @forAll statement - $this->shouldParse(parser, '@forAll . .', - ['?b-0', '?b-0', '?b-0'])); + $this->shouldParse($parser, '@forAll . .', + ['?b-0', '?b-0', '?b-0']); // ### should parse a @forAll statement with multiple entities - $this->shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', - ['?b-0', '?b-1', '?b-2'])); + $this->shouldParse($parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + ['?b-0', '?b-1', '?b-2']); // ### should not parse a @forAll statement with an invalid prefix - $this->shouldNotParse(parser, '@forAll a:b.', - 'Undefined prefix "a:" on line 1.')); + $this->shouldNotParse($parser, '@forAll a:b.', + 'Undefined prefix "a:" on line 1.'); // ### should not parse a @forAll statement with a blank node - $this->shouldNotParse(parser, '@forAll _:a.', - 'Unexpected blank on line 1.')); + $this->shouldNotParse($parser, '@forAll _:a.', + 'Unexpected blank on line 1.'); // ### should not parse a @forAll statement with a variable - $this->shouldNotParse(parser, '@forAll ?a.', - 'Unexpected $on line 1.')); + $this->shouldNotParse($parser, '@forAll ?a.', + 'Unexpected $on line 1.'); // ### should correctly scope @forAll statements - $this->shouldParse(parser, '@forAll . { @forAll . . }. .', + $this->shouldParse($parser, '@forAll . { @forAll . . }. .', ['?b-0', '?b-0', '_:b1'], ['?b-2', '?b-2', '?b-2', '_:b1'], - ['?b-0', '?b-0', '?b-0'])); + ['?b-0', '?b-0', '?b-0']); // ### should parse a ! path of length 2 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ':joe!fam:mother a fam:Person.', ['ex:joe', 'f:mother', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); // ### should parse a ! path of length 4 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: . @prefix loc: .' . ':joe!fam:mother!loc:office!loc:zip loc:code 1234.', ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'l:office', '_:b1'], ['_:b1', 'l:zip', '_:b2'], - ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer'])); + ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer']); // ### should parse a ! path of length 2 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' :joe!fam:mother.', ['x', 'is', '_:b0'], - ['ex:joe', 'f:mother', '_:b0'])); + ['ex:joe', 'f:mother', '_:b0']); // ### should parse a ! path of length 4 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: . @prefix loc: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: . @prefix loc: .' . ' :joe!fam:mother!loc:office!loc:zip.', ['x', 'is', '_:b2'], ['ex:joe', 'f:mother', '_:b0'], ['_:b0', 'l:office', '_:b1'], - ['_:b1', 'l:zip', '_:b2'])); + ['_:b1', 'l:zip', '_:b2']); // ### should parse a ^ path of length 2 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ':joe^fam:son a fam:Person.', ['_:b0', 'f:son', 'ex:joe'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); // ### should parse a ^ path of length 4 as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ':joe^fam:son^fam:sister^fam:mother a fam:Person.', ['_:b0', 'f:son', 'ex:joe'], ['_:b1', 'f:sister', '_:b0'], ['_:b2', 'f:mother', '_:b1'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); // ### should parse a ^ path of length 2 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' :joe^fam:son.', ['x', 'is', '_:b0'], - ['_:b0', 'f:son', 'ex:joe'])); + ['_:b0', 'f:son', 'ex:joe']); // ### should parse a ^ path of length 4 as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' :joe^fam:son^fam:sister^fam:mother.', ['x', 'is', '_:b2'], ['_:b0', 'f:son', 'ex:joe'], ['_:b1', 'f:sister', '_:b0'], - ['_:b2', 'f:mother', '_:b1'])); + ['_:b2', 'f:mother', '_:b1']); // ### should parse mixed !/^ paths as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ':joe!fam:mother^fam:mother a fam:Person.', ['ex:joe', 'f:mother', '_:b0'], ['_:b1', 'f:mother', '_:b0'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); // ### should parse mixed !/^ paths as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' :joe!fam:mother^fam:mother.', ['x', 'is', '_:b1'], ['ex:joe', 'f:mother', '_:b0'], - ['_:b1', 'f:mother', '_:b0'])); + ['_:b1', 'f:mother', '_:b0']); // ### should parse a ! path in a blank node as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . '[fam:knows :joe!fam:mother] a fam:Person.', ['_:b0', 'f:knows', '_:b1'], ['ex:joe', 'f:mother', '_:b1'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); // ### should parse a ! path in a blank node as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' [fam:knows :joe!fam:mother].', ['x', 'is', '_:b0'], ['_:b0', 'f:knows', '_:b1'], - ['ex:joe', 'f:mother', '_:b1'])); + ['ex:joe', 'f:mother', '_:b1']); // ### should parse a ^ path in a blank node as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . '[fam:knows :joe^fam:son] a fam:Person.', ['_:b0', 'f:knows', '_:b1'], ['_:b1', 'f:son', 'ex:joe'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); // ### should parse a ^ path in a blank node as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' [fam:knows :joe^fam:son].', ['x', 'is', '_:b0'], ['_:b0', 'f:knows', '_:b1'], - ['_:b1', 'f:son', 'ex:joe'])); + ['_:b1', 'f:son', 'ex:joe']); // ### should parse a ! path in a list as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . '( :joe!fam:mother ) a :List.', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1470,10 +1465,10 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['ex:joe', 'f:mother', '_:b2'])); + ['ex:joe', 'f:mother', '_:b2']); // ### should parse a ! path in a list as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' ( :joe!fam:mother ).', ['l', 'is', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1482,10 +1477,10 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['ex:joe', 'f:mother', '_:b2'])); + ['ex:joe', 'f:mother', '_:b2']); // ### should parse a ^ path in a list as subject - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . '( :joe^fam:son ) a :List.', ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1494,10 +1489,10 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b2', 'f:son', 'ex:joe'])); + ['_:b2', 'f:son', 'ex:joe']); // ### should parse a ^ path in a list as object - $this->shouldParse(parser, '@prefix : . @prefix fam: .' . + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . ' ( :joe^fam:son ).', ['l', 'is', '_:b0'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], @@ -1506,27 +1501,27 @@ function parser() { return new N3Parser({ format: 'N3' }); } ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b2', 'f:son', 'ex:joe'])); + ['_:b2', 'f:son', 'ex:joe']); // ### should not parse an invalid ! path - $this->shouldNotParse(parser, '!"invalid" ', 'Expected entity but got literal on line 1.')); + $this->shouldNotParse($parser, '!"invalid" ', 'Expected entity but got literal on line 1.'); // ### should not parse an invalid ^ path - $this->shouldNotParse(parser, '^"invalid" ', 'Expected entity but got literal on line 1.')); + $this->shouldNotParse($parser, '^"invalid" ', 'Expected entity but got literal on line 1.'); }); - describe('An N3Parser instance for the N3 format with the explicitQuantifiers option', function () { - function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: true }); } + describe('An TriGParser instance for the N3 format with the explicitQuantifiers option', function () { + $parser = function () { return new TriGParser([ "format"=> 'N3', "explicitQuantifiers": true ]); } // ### should parse a @forSome statement - $this->shouldParse(parser, '@forSome . .', + $this->shouldParse($parser, '@forSome . .', ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', 'x'])); + ['x', 'x', 'x']); // ### should parse a @forSome statement with multiple entities - $this->shouldParse(parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + $this->shouldParse($parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], @@ -1534,10 +1529,10 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['a:x', 'b:y', 'a:z'])); + ['a:x', 'b:y', 'a:z']); // ### should correctly scope @forSome statements - $this->shouldParse(parser, '@forSome . { @forSome . . }. .', + $this->shouldParse($parser, '@forSome . { @forSome . . }. .', ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], @@ -1546,17 +1541,17 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], ['x', 'x', 'x', '_:b1'], - ['x', 'x', 'x'])); + ['x', 'x', 'x']); // ### should parse a @forAll statement - $this->shouldParse(parser, '@forAll . .', + $this->shouldParse($parser, '@forAll . .', ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', 'x'])); + ['x', 'x', 'x']); // ### should parse a @forAll statement with multiple entities - $this->shouldParse(parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + $this->shouldParse($parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], @@ -1564,10 +1559,10 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['a:x', 'b:y', 'a:z'])); + ['a:x', 'b:y', 'a:z']); // ### should correctly scope @forAll statements - $this->shouldParse(parser, '@forAll . { @forAll . . }. .', + $this->shouldParse($parser, '@forAll . { @forAll . . }. .', ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], @@ -1576,7 +1571,7 @@ function parser() { return new N3Parser({ format: 'N3', explicitQuantifiers: tru ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], ['x', 'x', 'x', '_:b1'], - ['x', 'x', 'x'])); + ['x', 'x', 'x']); }); describe('IRI resolution', function () { @@ -2015,13 +2010,13 @@ function itShouldResolve(baseIri, relativeIri, expected) { before(function (done) { try { $doc = ' <' . relativeIri . '>.'; - new N3Parser({ documentIRI: baseIri }).parse(doc, function ($error, $triple) { + new TriGParser([ "documentIRI" : $baseIri]).parse(doc, function ($error, $triple) { if (done) result = triple, done(error); done = null; }); } - catch (error) { done(error); } + catch (\Exception $error) { $this->fail($error->getMessage()); } }); it('should result in ' . expected, function () { expect(result.object).to.equal(expected); From 9e54643992a478ce47e9ce70e1570d316f2bd9e0 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sat, 8 Apr 2017 23:32:05 +0200 Subject: [PATCH 34/72] TODO: Error not caught in Turtle mode --- src/TriGParser.php | 9 +++--- test/TriGParserTest.php | 70 +++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 12ce82d..24ced40 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -31,7 +31,10 @@ class TriGParser public function __construct($options = []) { $this->contextStack = []; $this->graph = null; - + + //This will initiate the callback methods + $this->initReaders(); + // Set the document IRI $this->setBase(isset($options["documentIRI"]) ? $options["documentIRI"]:null); @@ -66,8 +69,6 @@ public function __construct($options = []) { // Disable explicit quantifiers by default - TODO: is !! same in PHP? $this->explicitQuantifiers = isset($options["explicitQuantifiers"])?$options["explicitQuantifiers"]:null; - //This will initiate the callback methods - $this->initReaders(); } // ## Private class methods @@ -224,7 +225,7 @@ private function initReaders () return $this->readListItem; case '{': // Start a new formula - if (!$this->n3Mode) + if (!isset($this->n3Mode)) return call_user_func($this->error,'Unexpected graph', $token); $this->saveContext('formula', $this->graph, $this->graph = '_:b' . $this->blankNodeCount++, null, null); diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index f255844..b6ebce2 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1005,6 +1005,42 @@ public function testDifferentSettings() $this->shouldParse($parser, '{ }', ['a', 'b', 'c']); } + + public function testTurtle () + { + $parser = function () { return new TriGParser([ "format" => 'Turtle' ]); }; + + // ### should parse a single triple + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); + + // ### should not parse a default graph + $this->shouldNotParse($parser, '{}', 'Unexpected graph on line 1.'); + + // ### should not parse a named graph + $this->shouldNotParse($parser, ' {}', 'Expected entity but got { on line 1.'); + + // ### should not parse a named graph with the GRAPH keyword + $this->shouldNotParse($parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.'); + + // ### should not parse a quad + $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); + + // ### should not parse a variable + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); + + // ### should not parse an equality statement + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); + + // ### should not parse a right implication statement + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); + + // ### should not parse a left implication statement + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); + + // ### should not parse a formula as object + $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); + + } private function shouldParse($createParser, $input = "") @@ -1071,41 +1107,7 @@ private static function toSortedJSON ($items) } /* - describe('An N3Parser instance with a blank node prefix', function () { - - describe('An TriGParser instance for the Turtle format', function () { - $parser = function () { return new TriGParser([ format: 'Turtle' ]); }; - - // ### should parse a single triple - $this->shouldParse($parser, ' .', ['a', 'b', 'c']); - - // ### should not parse a default graph - $this->shouldNotParse($parser, '{}', 'Unexpected graph on line 1.'); - - // ### should not parse a named graph - $this->shouldNotParse($parser, ' {}', 'Expected entity but got { on line 1.'); - - // ### should not parse a named graph with the GRAPH keyword - $this->shouldNotParse($parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.'); - - // ### should not parse a quad - $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); - - // ### should not parse a variable - $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); - - // ### should not parse an equality statement - $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); - - // ### should not parse a right implication statement - $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); - - // ### should not parse a left implication statement - $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); - - // ### should not parse a formula as object - $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); }); describe('An TriGParser instance for the TriG format', function () { From 393137c79b6a117c8b81be0f4be6d8127451fe57 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sun, 9 Apr 2017 11:14:33 +0200 Subject: [PATCH 35/72] Fixed error handling --- src/TriGParser.php | 4 ++-- test/TriGParserTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 24ced40..a4f128f 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -986,8 +986,8 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { break; } } - } catch (Exception $e) { - call_user_func($this->callback,$error, null); + } catch (\Exception $e) { + call_user_func($this->callback, $e, null); $this->callback = function () {}; } } diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index b6ebce2..56d44cd 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1045,6 +1045,7 @@ public function testTurtle () private function shouldParse($createParser, $input = "") { + $expected = array_slice(func_get_args(),1); // Shift parameters as necessary if (is_callable($createParser)) @@ -1068,7 +1069,6 @@ private function shouldParse($createParser, $input = "") else $this->assertEquals(self::toSortedJSON($items), self::toSortedJSON($results)); }); - $parser->_resetBlankNodeIds(); } From 86d8031e343dbd94d343ff16f31b04faa5103a8a Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sun, 9 Apr 2017 11:21:00 +0200 Subject: [PATCH 36/72] Fix blank node identification --- src/TriGParser.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index a4f128f..3611568 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -62,8 +62,11 @@ public function __construct($options = []) { $this->subject = null; }; } - $this->blankNodePrefix = !isset($options["blankNodePrefix"]) ? '' : - '_:' . preg_replace('/^_:/', '', $options["blankNodePrefix"]); + $this->blankNodePrefix = null; + if (isset($options["blankNodePrefix"])) { + $this->blankNodePrefix = '_:' . preg_replace('/^_:/', '', $options["blankNodePrefix"]); + } + $this->lexer = isset($options["lexer"])? $options["lexer"] : new N3Lexer([ "lineMode"=> $isLineMode, "n3"=> $isN3 ]); // Disable explicit quantifiers by default - TODO: is !! same in PHP? @@ -74,7 +77,6 @@ public function __construct($options = []) { // ## Private class methods // ### `_resetBlankNodeIds` restarts blank node identification public function _resetBlankNodeIds () { - $this->blankNodePrefix = null; $this->blankNodeCount = 0; } From 24a5947a2e7e585713a7d780d914825da6cf8b40 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sun, 9 Apr 2017 17:02:08 +0200 Subject: [PATCH 37/72] Fixed turtle mode --- src/N3Lexer.php | 5 +- src/TriGParser.php | 8 +- test/TriGParserTest.php | 935 ++++++++++++++++++++-------------------- 3 files changed, 473 insertions(+), 475 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index b1b3b25..44ba4dd 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -45,6 +45,7 @@ public function __construct($options = []) { } // Enable N3 functionality by default $this->n3Mode = $options["n3"] !== false; + // Disable comment tokens by default $this->comments = isset($options["comments"])?$options["comments"]:null; } @@ -84,13 +85,13 @@ private function tokenizeToEnd($callback, $inputFinished) { // Signals the syntax error through the callback - $reportSyntaxError = function ($self) use ($callback, $input) { + $reportSyntaxError = function ($self) use ($callback, &$input) { preg_match("/^\S*/", $input, $match); $callback($self->syntaxError($match[0], $self->line), null); }; $outputComments = $this->comments; - while (true) { //TODO + while (true) { // Count and skip whitespace lines $whiteSpaceMatch; $comment; diff --git a/src/TriGParser.php b/src/TriGParser.php index 3611568..fa89990 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -93,8 +93,8 @@ private function setBase ($baseIRI = null) { $this->base = $baseIRI; $this->basePath = !strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); preg_match($this->schemeAuthority, $baseIRI, $matches); - $this->baseRoot = $matches[0]; - $this->baseScheme = $matches[1]; + $this->baseRoot = isset($matches[0])?$matches[0]:''; + $this->baseScheme = isset($matches[1])?$matches[1]:''; } } @@ -310,7 +310,7 @@ private function initReaders () return $this->readListItem; case '{': // Start a new formula - if (!$this->n3Mode) + if (!isset($this->n3Mode)) return call_user_func($this->error,'Unexpected graph', $token); $this->saveContext('formula', $this->graph, $this->subject, $this->predicate, $this->graph = '_:b' . $this->blankNodeCount++); @@ -895,7 +895,7 @@ private function initReaders () break; // Handle '/.' or '/..' path segments case '/': - if ($iri[$i + 1] === '.') { + if (isset($iri[$i + 1]) && $iri[$i + 1] === '.') { if (isset($iri[++$i + 1])) { $next = $iri[$i + 1]; } else diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 56d44cd..a5a3e51 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1042,6 +1042,438 @@ public function testTurtle () } + + + + public function testResolve() + { + //describe('IRI resolution', function () { + //describe('RFC3986 normal examples', function () { + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', './g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g/', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '?y', 'http://a/bb/ccc/d;p?y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y', 'http://a/bb/ccc/g?y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '#s', 'http://a/bb/ccc/d;p?q#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s', 'http://a/bb/ccc/g#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', ';x', 'http://a/bb/ccc/;x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x', 'http://a/bb/ccc/g;x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '', 'http://a/bb/ccc/d;p?q'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '.', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', './', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '..', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../../g', 'http://a/g'); + + + //describe('RFC3986 abnormal examples', function () { + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g.', 'http://a/bb/ccc/g.'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '.g', 'http://a/bb/ccc/.g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g..', 'http://a/bb/ccc/g..'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', '..g', 'http://a/bb/ccc/..g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', './../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', './g/.', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g/./h', 'http://a/bb/ccc/g/h'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g/../h', 'http://a/bb/ccc/h'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x=1/../y', 'http://a/bb/ccc/y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with trailing slash in base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/d/', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g', 'http://a/bb/ccc/d/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', './g', 'http://a/bb/ccc/d/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g/', 'http://a/bb/ccc/d/g/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/d/', '?y', 'http://a/bb/ccc/d/?y'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g?y', 'http://a/bb/ccc/d/g?y'); + $this->itShouldResolve('http://a/bb/ccc/d/', '#s', 'http://a/bb/ccc/d/#s'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g#s', 'http://a/bb/ccc/d/g#s'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g?y#s', 'http://a/bb/ccc/d/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/d/', ';x', 'http://a/bb/ccc/d/;x'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g;x', 'http://a/bb/ccc/d/g;x'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g;x?y#s', 'http://a/bb/ccc/d/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/d/', '', 'http://a/bb/ccc/d/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '.', 'http://a/bb/ccc/d/'); + $this->itShouldResolve('http://a/bb/ccc/d/', './', 'http://a/bb/ccc/d/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '..', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '../', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '../g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', '../..', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '../../', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/d/', '../../g', 'http://a/bb/g'); + + + //describe('RFC3986 abnormal examples with trailing slash in base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/d/', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g.', 'http://a/bb/ccc/d/g.'); + $this->itShouldResolve('http://a/bb/ccc/d/', '.g', 'http://a/bb/ccc/d/.g'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g..', 'http://a/bb/ccc/d/g..'); + $this->itShouldResolve('http://a/bb/ccc/d/', '..g', 'http://a/bb/ccc/d/..g'); + $this->itShouldResolve('http://a/bb/ccc/d/', './../g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/d/', './g/.', 'http://a/bb/ccc/d/g/'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g/./h', 'http://a/bb/ccc/d/g/h'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g/../h', 'http://a/bb/ccc/d/h'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g;x=1/./y', 'http://a/bb/ccc/d/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g;x=1/../y', 'http://a/bb/ccc/d/y'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g?y/./x', 'http://a/bb/ccc/d/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g?y/../x', 'http://a/bb/ccc/d/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g#s/./x', 'http://a/bb/ccc/d/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'g#s/../x', 'http://a/bb/ccc/d/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/d/', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with /. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', './g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '?y', 'http://a/bb/ccc/./d;p?y'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y', 'http://a/bb/ccc/g?y'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '#s', 'http://a/bb/ccc/./d;p?q#s'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s', 'http://a/bb/ccc/g#s'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', ';x', 'http://a/bb/ccc/;x'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x', 'http://a/bb/ccc/g;x'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '', 'http://a/bb/ccc/./d;p?q'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '.', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', './', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '..', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../../g', 'http://a/g'); + + + //describe('RFC3986 abnormal examples with /. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g.', 'http://a/bb/ccc/g.'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '.g', 'http://a/bb/ccc/.g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g..', 'http://a/bb/ccc/g..'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', '..g', 'http://a/bb/ccc/..g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', './../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', './g/.', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/./h', 'http://a/bb/ccc/g/h'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/../h', 'http://a/bb/ccc/h'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x=1/../y', 'http://a/bb/ccc/y'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/./d;p?q', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with /.. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', './g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/', 'http://a/bb/g/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '?y', 'http://a/bb/ccc/../d;p?y'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y', 'http://a/bb/g?y'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '#s', 'http://a/bb/ccc/../d;p?q#s'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s', 'http://a/bb/g#s'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y#s', 'http://a/bb/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', ';x', 'http://a/bb/;x'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x', 'http://a/bb/g;x'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x?y#s', 'http://a/bb/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '', 'http://a/bb/ccc/../d;p?q'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '.', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', './', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../../g', 'http://a/g'); + + + //describe('RFC3986 abnormal examples with /.. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g.', 'http://a/bb/g.'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '.g', 'http://a/bb/.g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g..', 'http://a/bb/g..'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', '..g', 'http://a/bb/..g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', './../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', './g/.', 'http://a/bb/g/'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/./h', 'http://a/bb/g/h'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/../h', 'http://a/bb/h'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x=1/./y', 'http://a/bb/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x=1/../y', 'http://a/bb/y'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y/./x', 'http://a/bb/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y/../x', 'http://a/bb/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s/./x', 'http://a/bb/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s/../x', 'http://a/bb/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/../d;p?q', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with trailing /. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/.', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/.', './g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g/', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/.', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/.', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/.', '?y', 'http://a/bb/ccc/.?y'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g?y', 'http://a/bb/ccc/g?y'); + $this->itShouldResolve('http://a/bb/ccc/.', '#s', 'http://a/bb/ccc/.#s'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g#s', 'http://a/bb/ccc/g#s'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/.', ';x', 'http://a/bb/ccc/;x'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g;x', 'http://a/bb/ccc/g;x'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/.', '', 'http://a/bb/ccc/.'); + $this->itShouldResolve('http://a/bb/ccc/.', '.', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/.', './', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/.', '..', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/.', '../', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/.', '../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/.', '../..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/.', '../../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/.', '../../g', 'http://a/g'); + + + //describe('RFC3986 abnormal examples with trailing /. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/.', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/.', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/.', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/.', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g.', 'http://a/bb/ccc/g.'); + $this->itShouldResolve('http://a/bb/ccc/.', '.g', 'http://a/bb/ccc/.g'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g..', 'http://a/bb/ccc/g..'); + $this->itShouldResolve('http://a/bb/ccc/.', '..g', 'http://a/bb/ccc/..g'); + $this->itShouldResolve('http://a/bb/ccc/.', './../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/.', './g/.', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g/./h', 'http://a/bb/ccc/g/h'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g/../h', 'http://a/bb/ccc/h'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g;x=1/../y', 'http://a/bb/ccc/y'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/.', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/.', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with trailing /.. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/..', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/..', './g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g/', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/..', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/..', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/..', '?y', 'http://a/bb/ccc/..?y'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g?y', 'http://a/bb/ccc/g?y'); + $this->itShouldResolve('http://a/bb/ccc/..', '#s', 'http://a/bb/ccc/..#s'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g#s', 'http://a/bb/ccc/g#s'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/..', ';x', 'http://a/bb/ccc/;x'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g;x', 'http://a/bb/ccc/g;x'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/..', '', 'http://a/bb/ccc/..'); + $this->itShouldResolve('http://a/bb/ccc/..', '.', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/..', './', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/..', '..', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/..', '../', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/..', '../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/..', '../..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/..', '../../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/..', '../../g', 'http://a/g'); + + + //describe('RFC3986 abnormal examples with trailing /.. in the base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/..', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/..', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/..', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/..', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g.', 'http://a/bb/ccc/g.'); + $this->itShouldResolve('http://a/bb/ccc/..', '.g', 'http://a/bb/ccc/.g'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g..', 'http://a/bb/ccc/g..'); + $this->itShouldResolve('http://a/bb/ccc/..', '..g', 'http://a/bb/ccc/..g'); + $this->itShouldResolve('http://a/bb/ccc/..', './../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/..', './g/.', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g/./h', 'http://a/bb/ccc/g/h'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g/../h', 'http://a/bb/ccc/h'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g;x=1/../y', 'http://a/bb/ccc/y'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/..', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/..', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with fragment in base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g:h', 'g:h'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', './g', 'http://a/bb/ccc/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '/g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '//g', 'http://g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '?y', 'http://a/bb/ccc/d;p?y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y', 'http://a/bb/ccc/g?y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '#s', 'http://a/bb/ccc/d;p?q#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s', 'http://a/bb/ccc/g#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y#s', 'http://a/bb/ccc/g?y#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', ';x', 'http://a/bb/ccc/;x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x', 'http://a/bb/ccc/g;x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '', 'http://a/bb/ccc/d;p?q'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '.', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', './', 'http://a/bb/ccc/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '..', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../', 'http://a/bb/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../..', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../', 'http://a/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../g', 'http://a/g'); + + + //describe('RFC3986 abnormal examples with fragment in base IRI', function () { + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../../../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '/./g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '/../g', 'http://a/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g.', 'http://a/bb/ccc/g.'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '.g', 'http://a/bb/ccc/.g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g..', 'http://a/bb/ccc/g..'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', '..g', 'http://a/bb/ccc/..g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', './../g', 'http://a/bb/g'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', './g/.', 'http://a/bb/ccc/g/'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/./h', 'http://a/bb/ccc/g/h'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/../h', 'http://a/bb/ccc/h'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x=1/../y', 'http://a/bb/ccc/y'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); + $this->itShouldResolve('http://a/bb/ccc/d;p?q#f', 'http:g', 'http:g'); + + + //describe('RFC3986 normal examples with file path', function () { + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g:h', 'g:h'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g', 'file:///a/bb/ccc/g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', './g', 'file:///a/bb/ccc/g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/', 'file:///a/bb/ccc/g/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '/g', 'file:///g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '//g', 'file://g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '?y', 'file:///a/bb/ccc/d;p?y'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y', 'file:///a/bb/ccc/g?y'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '#s', 'file:///a/bb/ccc/d;p?q#s'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s', 'file:///a/bb/ccc/g#s'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y#s', 'file:///a/bb/ccc/g?y#s'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', ';x', 'file:///a/bb/ccc/;x'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x', 'file:///a/bb/ccc/g;x'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x?y#s', 'file:///a/bb/ccc/g;x?y#s'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '', 'file:///a/bb/ccc/d;p?q'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '.', 'file:///a/bb/ccc/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', './', 'file:///a/bb/ccc/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '..', 'file:///a/bb/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../', 'file:///a/bb/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../g', 'file:///a/bb/g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../..', 'file:///a/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../../', 'file:///a/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../../g', 'file:///a/g'); + + + //describe('RFC3986 abnormal examples with file path', function () { + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../../../g', 'file:///g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '../../../../g', 'file:///g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '/./g', 'file:///g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '/../g', 'file:///g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g.', 'file:///a/bb/ccc/g.'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '.g', 'file:///a/bb/ccc/.g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g..', 'file:///a/bb/ccc/g..'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', '..g', 'file:///a/bb/ccc/..g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', './../g', 'file:///a/bb/g'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', './g/.', 'file:///a/bb/ccc/g/'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/./h', 'file:///a/bb/ccc/g/h'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/../h', 'file:///a/bb/ccc/h'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x=1/./y', 'file:///a/bb/ccc/g;x=1/y'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x=1/../y', 'file:///a/bb/ccc/y'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y/./x', 'file:///a/bb/ccc/g?y/./x'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y/../x', 'file:///a/bb/ccc/g?y/../x'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s/./x', 'file:///a/bb/ccc/g#s/./x'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s/../x', 'file:///a/bb/ccc/g#s/../x'); + $this->itShouldResolve('file:///a/bb/ccc/d;p?q', 'http:g', 'http:g'); + + + //describe('additional cases', function () { + // relative paths ending with '.' + $this->itShouldResolve('http://abc/', '.', 'http://abc/'); + $this->itShouldResolve('http://abc/def/ghi', '.', 'http://abc/def/'); + $this->itShouldResolve('http://abc/def/ghi', '.?a=b', 'http://abc/def/?a=b'); + $this->itShouldResolve('http://abc/def/ghi', '.#a=b', 'http://abc/def/#a=b'); + + // relative paths ending with '..' + $this->itShouldResolve('http://abc/', '..', 'http://abc/'); + $this->itShouldResolve('http://abc/def/ghi', '..', 'http://abc/'); + $this->itShouldResolve('http://abc/def/ghi', '..?a=b', 'http://abc/?a=b'); + $this->itShouldResolve('http://abc/def/ghi', '..#a=b', 'http://abc/#a=b'); + + // base path with empty subpaths (double slashes) + $this->itShouldResolve('http://ab//de//ghi', 'xyz', 'http://ab//de//xyz'); + $this->itShouldResolve('http://ab//de//ghi', './xyz', 'http://ab//de//xyz'); + $this->itShouldResolve('http://ab//de//ghi', '../xyz', 'http://ab//de/xyz'); + + // base path with colon (possible confusion with scheme) + $this->itShouldResolve('http://abc/d:f/ghi', 'xyz', 'http://abc/d:f/xyz'); + $this->itShouldResolve('http://abc/d:f/ghi', './xyz', 'http://abc/d:f/xyz'); + $this->itShouldResolve('http://abc/d:f/ghi', '../xyz', 'http://abc/xyz'); + + // base path consisting of '..' and/or '../' sequences + $this->itShouldResolve('./', 'abc', '/abc'); + $this->itShouldResolve('../', 'abc', '/abc'); + $this->itShouldResolve('./././', '././abc', '/abc'); + $this->itShouldResolve('../../../', '../../abc', '/abc'); + $this->itShouldResolve('.../././', '././abc', '.../abc'); + + // base path without authority + $this->itShouldResolve('a:b:c/', 'def/../', 'a:b:c/'); + $this->itShouldResolve('a:b:c', '/def', 'a:/def'); + $this->itShouldResolve('a:b/c', '/def', 'a:/def'); + $this->itShouldResolve('a:', '/.', 'a:/'); + $this->itShouldResolve('a:', '/..', 'a:/'); + + // base path with slashes in query string + $this->itShouldResolve('http://abc/def/ghi?q=xx/yyy/z', 'jjj', 'http://abc/def/jjj'); + $this->itShouldResolve('http://abc/def/ghi?q=xx/y?y/z', 'jjj', 'http://abc/def/jjj'); + } + private function shouldParse($createParser, $input = "") { @@ -1097,6 +1529,24 @@ function shouldNotParse($createParser, $input, $expectedError = null) { }); } + function itShouldResolve($baseIri, $relativeIri, $expected) { + $done = false; + try { + $doc = ' <' . $relativeIri . '>.'; + $parser = new TriGParser([ "documentIRI" => $baseIri]); + $parser->parse($doc, function ($error, $triple) use ($done, $expected) { + if (!$done && $triple) { + $this->assertEquals($expected, $triple["object"]); + } + if (isset($error)) { + $this->fail($error); + } + $done = true; + }); + } + catch (\Exception $error) { throw $error; $this->fail("Resolving <$relativeIri> against <$baseIri>.\nError message: " . $error->getMessage()); } + } + private static function toSortedJSON ($items) { @@ -1105,57 +1555,54 @@ private static function toSortedJSON ($items) return "[\n " . join("\n ", $triples) . "\n]"; } } -/* - - describe('An TriGParser instance for the Turtle format', function () { - }); +/* describe('An TriGParser instance for the TriG format', function () { - $parser = function () { return new TriGParser([ format: 'TriG' ]); }; + $parser = function () { return new TriGParser([ format: 'TriG' ]); }; - // ### should parse a single triple + // ### should parse a single triple $this->shouldParse($parser, ' .', ['a', 'b', 'c']); - // ### should parse a default graph + // ### should parse a default graph $this->shouldParse($parser, '{}'); - // ### should parse a named graph + // ### should parse a named graph $this->shouldParse($parser, ' {}'); - // ### should parse a named graph with the GRAPH keyword + // ### should parse a named graph with the GRAPH keyword $this->shouldParse($parser, 'GRAPH {}'); - // ### should not parse a quad + // ### should not parse a quad $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); - // ### should not parse a variable + // ### should not parse a variable $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); - // ### should not parse an equality statement + // ### should not parse an equality statement $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); - // ### should not parse a right implication statement + // ### should not parse a right implication statement $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); - // ### should not parse a left implication statement + // ### should not parse a left implication statement $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); - // ### should not parse a formula as object + // ### should not parse a formula as object $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); }); describe('An TriGParser instance for the N-Triples format', function () { - $parser = function () { return new TriGParser([ format: 'N-Triples' ]); }; + $parser = function () { return new TriGParser([ format: 'N-Triples' ]); }; - // ### should parse a single triple + // ### should parse a single triple $this->shouldParse($parser, ' "c".', ['http://ex.org/a', 'http://ex.org/b', '"c"']); - // ### should not parse a single quad + // ### should not parse a single quad $this->shouldNotParse($parser, ' "c" .', 'Expected punctuation to follow ""c"" on line 1.'); - // ### should not parse relative IRIs + // ### should not parse relative IRIs $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); // ### should not parse a prefix declaration @@ -1575,454 +2022,4 @@ private static function toSortedJSON ($items) ['x', 'x', 'x', '_:b1'], ['x', 'x', 'x']); }); - - describe('IRI resolution', function () { - describe('RFC3986 normal examples', function () { - itShouldResolve('http://a/bb/ccc/d;p?q', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', './g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g/', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/d;p?q', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/d;p?q', '?y', 'http://a/bb/ccc/d;p?y'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y', 'http://a/bb/ccc/g?y'); - itShouldResolve('http://a/bb/ccc/d;p?q', '#s', 'http://a/bb/ccc/d;p?q#s'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s', 'http://a/bb/ccc/g#s'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y#s', 'http://a/bb/ccc/g?y#s'); - itShouldResolve('http://a/bb/ccc/d;p?q', ';x', 'http://a/bb/ccc/;x'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x', 'http://a/bb/ccc/g;x'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/d;p?q', '', 'http://a/bb/ccc/d;p?q'); - itShouldResolve('http://a/bb/ccc/d;p?q', '.', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/d;p?q', './', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/d;p?q', '..', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/d;p?q', '../', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/d;p?q', '../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', '../..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/d;p?q', '../../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/d;p?q', '../../g', 'http://a/g'); - }); - - describe('RFC3986 abnormal examples', function () { - itShouldResolve('http://a/bb/ccc/d;p?q', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g.', 'http://a/bb/ccc/g.'); - itShouldResolve('http://a/bb/ccc/d;p?q', '.g', 'http://a/bb/ccc/.g'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g..', 'http://a/bb/ccc/g..'); - itShouldResolve('http://a/bb/ccc/d;p?q', '..g', 'http://a/bb/ccc/..g'); - itShouldResolve('http://a/bb/ccc/d;p?q', './../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/d;p?q', './g/.', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g/./h', 'http://a/bb/ccc/g/h'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g/../h', 'http://a/bb/ccc/h'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g;x=1/../y', 'http://a/bb/ccc/y'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); - itShouldResolve('http://a/bb/ccc/d;p?q', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with trailing slash in base IRI', function () { - itShouldResolve('http://a/bb/ccc/d/', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/d/', 'g', 'http://a/bb/ccc/d/g'); - itShouldResolve('http://a/bb/ccc/d/', './g', 'http://a/bb/ccc/d/g'); - itShouldResolve('http://a/bb/ccc/d/', 'g/', 'http://a/bb/ccc/d/g/'); - itShouldResolve('http://a/bb/ccc/d/', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d/', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/d/', '?y', 'http://a/bb/ccc/d/?y'); - itShouldResolve('http://a/bb/ccc/d/', 'g?y', 'http://a/bb/ccc/d/g?y'); - itShouldResolve('http://a/bb/ccc/d/', '#s', 'http://a/bb/ccc/d/#s'); - itShouldResolve('http://a/bb/ccc/d/', 'g#s', 'http://a/bb/ccc/d/g#s'); - itShouldResolve('http://a/bb/ccc/d/', 'g?y#s', 'http://a/bb/ccc/d/g?y#s'); - itShouldResolve('http://a/bb/ccc/d/', ';x', 'http://a/bb/ccc/d/;x'); - itShouldResolve('http://a/bb/ccc/d/', 'g;x', 'http://a/bb/ccc/d/g;x'); - itShouldResolve('http://a/bb/ccc/d/', 'g;x?y#s', 'http://a/bb/ccc/d/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/d/', '', 'http://a/bb/ccc/d/'); - itShouldResolve('http://a/bb/ccc/d/', '.', 'http://a/bb/ccc/d/'); - itShouldResolve('http://a/bb/ccc/d/', './', 'http://a/bb/ccc/d/'); - itShouldResolve('http://a/bb/ccc/d/', '..', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/d/', '../', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/d/', '../g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/d/', '../..', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/d/', '../../', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/d/', '../../g', 'http://a/bb/g'); - }); - - describe('RFC3986 abnormal examples with trailing slash in base IRI', function () { - itShouldResolve('http://a/bb/ccc/d/', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d/', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d/', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d/', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d/', 'g.', 'http://a/bb/ccc/d/g.'); - itShouldResolve('http://a/bb/ccc/d/', '.g', 'http://a/bb/ccc/d/.g'); - itShouldResolve('http://a/bb/ccc/d/', 'g..', 'http://a/bb/ccc/d/g..'); - itShouldResolve('http://a/bb/ccc/d/', '..g', 'http://a/bb/ccc/d/..g'); - itShouldResolve('http://a/bb/ccc/d/', './../g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/d/', './g/.', 'http://a/bb/ccc/d/g/'); - itShouldResolve('http://a/bb/ccc/d/', 'g/./h', 'http://a/bb/ccc/d/g/h'); - itShouldResolve('http://a/bb/ccc/d/', 'g/../h', 'http://a/bb/ccc/d/h'); - itShouldResolve('http://a/bb/ccc/d/', 'g;x=1/./y', 'http://a/bb/ccc/d/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/d/', 'g;x=1/../y', 'http://a/bb/ccc/d/y'); - itShouldResolve('http://a/bb/ccc/d/', 'g?y/./x', 'http://a/bb/ccc/d/g?y/./x'); - itShouldResolve('http://a/bb/ccc/d/', 'g?y/../x', 'http://a/bb/ccc/d/g?y/../x'); - itShouldResolve('http://a/bb/ccc/d/', 'g#s/./x', 'http://a/bb/ccc/d/g#s/./x'); - itShouldResolve('http://a/bb/ccc/d/', 'g#s/../x', 'http://a/bb/ccc/d/g#s/../x'); - itShouldResolve('http://a/bb/ccc/d/', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with /. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', './g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '?y', 'http://a/bb/ccc/./d;p?y'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y', 'http://a/bb/ccc/g?y'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '#s', 'http://a/bb/ccc/./d;p?q#s'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s', 'http://a/bb/ccc/g#s'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y#s', 'http://a/bb/ccc/g?y#s'); - itShouldResolve('http://a/bb/ccc/./d;p?q', ';x', 'http://a/bb/ccc/;x'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x', 'http://a/bb/ccc/g;x'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '', 'http://a/bb/ccc/./d;p?q'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '.', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', './', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '..', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '../', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '../..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '../../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '../../g', 'http://a/g'); - }); - - describe('RFC3986 abnormal examples with /. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/./d;p?q', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g.', 'http://a/bb/ccc/g.'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '.g', 'http://a/bb/ccc/.g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g..', 'http://a/bb/ccc/g..'); - itShouldResolve('http://a/bb/ccc/./d;p?q', '..g', 'http://a/bb/ccc/..g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', './../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/./d;p?q', './g/.', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/./h', 'http://a/bb/ccc/g/h'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g/../h', 'http://a/bb/ccc/h'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g;x=1/../y', 'http://a/bb/ccc/y'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); - itShouldResolve('http://a/bb/ccc/./d;p?q', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with /.. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', './g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/', 'http://a/bb/g/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '?y', 'http://a/bb/ccc/../d;p?y'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y', 'http://a/bb/g?y'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '#s', 'http://a/bb/ccc/../d;p?q#s'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s', 'http://a/bb/g#s'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y#s', 'http://a/bb/g?y#s'); - itShouldResolve('http://a/bb/ccc/../d;p?q', ';x', 'http://a/bb/;x'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x', 'http://a/bb/g;x'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x?y#s', 'http://a/bb/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '', 'http://a/bb/ccc/../d;p?q'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '.', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', './', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '../..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '../../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '../../g', 'http://a/g'); - }); - - describe('RFC3986 abnormal examples with /.. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/../d;p?q', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g.', 'http://a/bb/g.'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '.g', 'http://a/bb/.g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g..', 'http://a/bb/g..'); - itShouldResolve('http://a/bb/ccc/../d;p?q', '..g', 'http://a/bb/..g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', './../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/../d;p?q', './g/.', 'http://a/bb/g/'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/./h', 'http://a/bb/g/h'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g/../h', 'http://a/bb/h'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x=1/./y', 'http://a/bb/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g;x=1/../y', 'http://a/bb/y'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y/./x', 'http://a/bb/g?y/./x'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g?y/../x', 'http://a/bb/g?y/../x'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s/./x', 'http://a/bb/g#s/./x'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'g#s/../x', 'http://a/bb/g#s/../x'); - itShouldResolve('http://a/bb/ccc/../d;p?q', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with trailing /. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/.', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/.', 'g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/.', './g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/.', 'g/', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/.', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/.', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/.', '?y', 'http://a/bb/ccc/.?y'); - itShouldResolve('http://a/bb/ccc/.', 'g?y', 'http://a/bb/ccc/g?y'); - itShouldResolve('http://a/bb/ccc/.', '#s', 'http://a/bb/ccc/.#s'); - itShouldResolve('http://a/bb/ccc/.', 'g#s', 'http://a/bb/ccc/g#s'); - itShouldResolve('http://a/bb/ccc/.', 'g?y#s', 'http://a/bb/ccc/g?y#s'); - itShouldResolve('http://a/bb/ccc/.', ';x', 'http://a/bb/ccc/;x'); - itShouldResolve('http://a/bb/ccc/.', 'g;x', 'http://a/bb/ccc/g;x'); - itShouldResolve('http://a/bb/ccc/.', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/.', '', 'http://a/bb/ccc/.'); - itShouldResolve('http://a/bb/ccc/.', '.', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/.', './', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/.', '..', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/.', '../', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/.', '../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/.', '../..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/.', '../../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/.', '../../g', 'http://a/g'); - }); - - describe('RFC3986 abnormal examples with trailing /. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/.', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/.', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/.', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/.', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/.', 'g.', 'http://a/bb/ccc/g.'); - itShouldResolve('http://a/bb/ccc/.', '.g', 'http://a/bb/ccc/.g'); - itShouldResolve('http://a/bb/ccc/.', 'g..', 'http://a/bb/ccc/g..'); - itShouldResolve('http://a/bb/ccc/.', '..g', 'http://a/bb/ccc/..g'); - itShouldResolve('http://a/bb/ccc/.', './../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/.', './g/.', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/.', 'g/./h', 'http://a/bb/ccc/g/h'); - itShouldResolve('http://a/bb/ccc/.', 'g/../h', 'http://a/bb/ccc/h'); - itShouldResolve('http://a/bb/ccc/.', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/.', 'g;x=1/../y', 'http://a/bb/ccc/y'); - itShouldResolve('http://a/bb/ccc/.', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); - itShouldResolve('http://a/bb/ccc/.', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); - itShouldResolve('http://a/bb/ccc/.', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); - itShouldResolve('http://a/bb/ccc/.', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); - itShouldResolve('http://a/bb/ccc/.', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with trailing /.. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/..', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/..', 'g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/..', './g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/..', 'g/', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/..', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/..', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/..', '?y', 'http://a/bb/ccc/..?y'); - itShouldResolve('http://a/bb/ccc/..', 'g?y', 'http://a/bb/ccc/g?y'); - itShouldResolve('http://a/bb/ccc/..', '#s', 'http://a/bb/ccc/..#s'); - itShouldResolve('http://a/bb/ccc/..', 'g#s', 'http://a/bb/ccc/g#s'); - itShouldResolve('http://a/bb/ccc/..', 'g?y#s', 'http://a/bb/ccc/g?y#s'); - itShouldResolve('http://a/bb/ccc/..', ';x', 'http://a/bb/ccc/;x'); - itShouldResolve('http://a/bb/ccc/..', 'g;x', 'http://a/bb/ccc/g;x'); - itShouldResolve('http://a/bb/ccc/..', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/..', '', 'http://a/bb/ccc/..'); - itShouldResolve('http://a/bb/ccc/..', '.', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/..', './', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/..', '..', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/..', '../', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/..', '../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/..', '../..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/..', '../../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/..', '../../g', 'http://a/g'); - }); - - describe('RFC3986 abnormal examples with trailing /.. in the base IRI', function () { - itShouldResolve('http://a/bb/ccc/..', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/..', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/..', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/..', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/..', 'g.', 'http://a/bb/ccc/g.'); - itShouldResolve('http://a/bb/ccc/..', '.g', 'http://a/bb/ccc/.g'); - itShouldResolve('http://a/bb/ccc/..', 'g..', 'http://a/bb/ccc/g..'); - itShouldResolve('http://a/bb/ccc/..', '..g', 'http://a/bb/ccc/..g'); - itShouldResolve('http://a/bb/ccc/..', './../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/..', './g/.', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/..', 'g/./h', 'http://a/bb/ccc/g/h'); - itShouldResolve('http://a/bb/ccc/..', 'g/../h', 'http://a/bb/ccc/h'); - itShouldResolve('http://a/bb/ccc/..', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/..', 'g;x=1/../y', 'http://a/bb/ccc/y'); - itShouldResolve('http://a/bb/ccc/..', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); - itShouldResolve('http://a/bb/ccc/..', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); - itShouldResolve('http://a/bb/ccc/..', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); - itShouldResolve('http://a/bb/ccc/..', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); - itShouldResolve('http://a/bb/ccc/..', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with fragment in base IRI', function () { - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g:h', 'g:h'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', './g', 'http://a/bb/ccc/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '/g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '//g', 'http://g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '?y', 'http://a/bb/ccc/d;p?y'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y', 'http://a/bb/ccc/g?y'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '#s', 'http://a/bb/ccc/d;p?q#s'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s', 'http://a/bb/ccc/g#s'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y#s', 'http://a/bb/ccc/g?y#s'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', ';x', 'http://a/bb/ccc/;x'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x', 'http://a/bb/ccc/g;x'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x?y#s', 'http://a/bb/ccc/g;x?y#s'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '', 'http://a/bb/ccc/d;p?q'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '.', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', './', 'http://a/bb/ccc/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '..', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../', 'http://a/bb/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../..', 'http://a/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../', 'http://a/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../g', 'http://a/g'); - }); - - describe('RFC3986 abnormal examples with fragment in base IRI', function () { - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '../../../../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '/./g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '/../g', 'http://a/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g.', 'http://a/bb/ccc/g.'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '.g', 'http://a/bb/ccc/.g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g..', 'http://a/bb/ccc/g..'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', '..g', 'http://a/bb/ccc/..g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', './../g', 'http://a/bb/g'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', './g/.', 'http://a/bb/ccc/g/'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/./h', 'http://a/bb/ccc/g/h'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g/../h', 'http://a/bb/ccc/h'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x=1/./y', 'http://a/bb/ccc/g;x=1/y'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g;x=1/../y', 'http://a/bb/ccc/y'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y/./x', 'http://a/bb/ccc/g?y/./x'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g?y/../x', 'http://a/bb/ccc/g?y/../x'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s/./x', 'http://a/bb/ccc/g#s/./x'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'g#s/../x', 'http://a/bb/ccc/g#s/../x'); - itShouldResolve('http://a/bb/ccc/d;p?q#f', 'http:g', 'http:g'); - }); - - describe('RFC3986 normal examples with file path', function () { - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g:h', 'g:h'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g', 'file:///a/bb/ccc/g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', './g', 'file:///a/bb/ccc/g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/', 'file:///a/bb/ccc/g/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '/g', 'file:///g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '//g', 'file://g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '?y', 'file:///a/bb/ccc/d;p?y'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y', 'file:///a/bb/ccc/g?y'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '#s', 'file:///a/bb/ccc/d;p?q#s'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s', 'file:///a/bb/ccc/g#s'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y#s', 'file:///a/bb/ccc/g?y#s'); - itShouldResolve('file:///a/bb/ccc/d;p?q', ';x', 'file:///a/bb/ccc/;x'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x', 'file:///a/bb/ccc/g;x'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x?y#s', 'file:///a/bb/ccc/g;x?y#s'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '', 'file:///a/bb/ccc/d;p?q'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '.', 'file:///a/bb/ccc/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', './', 'file:///a/bb/ccc/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '..', 'file:///a/bb/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '../', 'file:///a/bb/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '../g', 'file:///a/bb/g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '../..', 'file:///a/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '../../', 'file:///a/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '../../g', 'file:///a/g'); - }); - - describe('RFC3986 abnormal examples with file path', function () { - itShouldResolve('file:///a/bb/ccc/d;p?q', '../../../g', 'file:///g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '../../../../g', 'file:///g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '/./g', 'file:///g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '/../g', 'file:///g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g.', 'file:///a/bb/ccc/g.'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '.g', 'file:///a/bb/ccc/.g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g..', 'file:///a/bb/ccc/g..'); - itShouldResolve('file:///a/bb/ccc/d;p?q', '..g', 'file:///a/bb/ccc/..g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', './../g', 'file:///a/bb/g'); - itShouldResolve('file:///a/bb/ccc/d;p?q', './g/.', 'file:///a/bb/ccc/g/'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/./h', 'file:///a/bb/ccc/g/h'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g/../h', 'file:///a/bb/ccc/h'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x=1/./y', 'file:///a/bb/ccc/g;x=1/y'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g;x=1/../y', 'file:///a/bb/ccc/y'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y/./x', 'file:///a/bb/ccc/g?y/./x'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g?y/../x', 'file:///a/bb/ccc/g?y/../x'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s/./x', 'file:///a/bb/ccc/g#s/./x'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'g#s/../x', 'file:///a/bb/ccc/g#s/../x'); - itShouldResolve('file:///a/bb/ccc/d;p?q', 'http:g', 'http:g'); - }); - - describe('additional cases', function () { - // relative paths ending with '.' - itShouldResolve('http://abc/', '.', 'http://abc/'); - itShouldResolve('http://abc/def/ghi', '.', 'http://abc/def/'); - itShouldResolve('http://abc/def/ghi', '.?a=b', 'http://abc/def/?a=b'); - itShouldResolve('http://abc/def/ghi', '.#a=b', 'http://abc/def/#a=b'); - - // relative paths ending with '..' - itShouldResolve('http://abc/', '..', 'http://abc/'); - itShouldResolve('http://abc/def/ghi', '..', 'http://abc/'); - itShouldResolve('http://abc/def/ghi', '..?a=b', 'http://abc/?a=b'); - itShouldResolve('http://abc/def/ghi', '..#a=b', 'http://abc/#a=b'); - - // base path with empty subpaths (double slashes) - itShouldResolve('http://ab//de//ghi', 'xyz', 'http://ab//de//xyz'); - itShouldResolve('http://ab//de//ghi', './xyz', 'http://ab//de//xyz'); - itShouldResolve('http://ab//de//ghi', '../xyz', 'http://ab//de/xyz'); - - // base path with colon (possible confusion with scheme) - itShouldResolve('http://abc/d:f/ghi', 'xyz', 'http://abc/d:f/xyz'); - itShouldResolve('http://abc/d:f/ghi', './xyz', 'http://abc/d:f/xyz'); - itShouldResolve('http://abc/d:f/ghi', '../xyz', 'http://abc/xyz'); - - // base path consisting of '..' and/or '../' sequences - itShouldResolve('./', 'abc', '/abc'); - itShouldResolve('../', 'abc', '/abc'); - itShouldResolve('./././', '././abc', '/abc'); - itShouldResolve('../../../', '../../abc', '/abc'); - itShouldResolve('.../././', '././abc', '.../abc'); - - // base path without authority - itShouldResolve('a:b:c/', 'def/../', 'a:b:c/'); - itShouldResolve('a:b:c', '/def', 'a:/def'); - itShouldResolve('a:b/c', '/def', 'a:/def'); - itShouldResolve('a:', '/.', 'a:/'); - itShouldResolve('a:', '/..', 'a:/'); - - // base path with slashes in query string - itShouldResolve('http://abc/def/ghi?q=xx/yyy/z', 'jjj', 'http://abc/def/jjj'); - itShouldResolve('http://abc/def/ghi?q=xx/y?y/z', 'jjj', 'http://abc/def/jjj'); - }); - }); -}); - - -function itShouldResolve(baseIri, relativeIri, expected) { - $result; - describe('resolving <' . relativeIri . '> against <' . baseIri . '>', function () { - before(function (done) { - try { - $doc = ' <' . relativeIri . '>.'; - new TriGParser([ "documentIRI" : $baseIri]).parse(doc, function ($error, $triple) { - if (done) - result = triple, done(error); - done = null; - }); - } - catch (\Exception $error) { $this->fail($error->getMessage()); } - }); - it('should result in ' . expected, function () { - expect(result.object).to.equal(expected); - }); - }); -} */ \ No newline at end of file From d70dc979ef65c2783a4423f32a6cd27096751fd8 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sun, 9 Apr 2017 17:30:52 +0200 Subject: [PATCH 38/72] Converted the rest of the tests --- src/TriGParser.php | 4 +- test/TriGParserTest.php | 941 ++++++++++++++++++++-------------------- 2 files changed, 474 insertions(+), 471 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index fa89990..b5b9474 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -57,7 +57,7 @@ public function __construct($options = []) { if ($isLineMode) { $this->base = ''; $this->resolveIRI = function ($token) { - $this->error('Disallowed relative IRI', $token); + call_user_func($this->error, 'Disallowed relative IRI', $token); return $this->callback = function () {}; $this->subject = null; }; @@ -760,7 +760,7 @@ private function initReaders () // ### `_readForwardPath` reads a '!' path $this->readForwardPath = function ($token) { - $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; + $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; // The next token is the predicate $predicate = call_user_func($this->readEntity,$token); if (!$predicate) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index a5a3e51..3b704bc 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1042,9 +1042,480 @@ public function testTurtle () } + public function testTriGFormat () + { + $parser = function () { return new TriGParser([ "format"=> 'TriG' ]); }; + + // ### should parse a single triple + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); + + // ### should parse a default graph + $this->shouldParse($parser, '{}'); + + // ### should parse a named graph + $this->shouldParse($parser, ' {}'); + + // ### should parse a named graph with the GRAPH keyword + $this->shouldParse($parser, 'GRAPH {}'); + + // ### should not parse a quad + $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); + + // ### should not parse a variable + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); + + // ### should not parse an equality statement + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); + + // ### should not parse a right implication statement + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); + + // ### should not parse a left implication statement + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); + + // ### should not parse a formula as object + $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); + + } + + public function testNTriplesFormat () + { + $parser = function () { return new TriGParser([ "format"=> 'N-Triples' ]); }; + + // ### should parse a single triple + $this->shouldParse($parser, ' "c".', + ['http://ex.org/a', 'http://ex.org/b', '"c"']); + + // ### should not parse a single quad + $this->shouldNotParse($parser, ' "c" .', + 'Expected punctuation to follow ""c"" on line 1.'); + + // ### should not parse relative IRIs + $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); + + // ### should not parse a prefix declaration + $this->shouldNotParse($parser, '@prefix : .', 'Unexpected "@prefix" on line 1.'); + + // ### should not parse a variable + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); + + // ### should not parse an equality statement + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); + + // ### should not parse a right implication statement + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); + + // ### should not parse a left implication statement + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); + + // ### should not parse a formula as object + $this->shouldNotParse($parser, ' {}.', 'Unexpected "{" on line 1.'); + + } + + public function testNQuadsFormat () + { + $parser = function () { return new TriGParser([ "format"=> 'N-Quads' ]); }; + + // ### should parse a single triple + $this->shouldParse($parser, ' .', + ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c']); + + // ### should parse a single quad + $this->shouldParse($parser, ' "c" .', + ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g']); + + // ### should not parse relative IRIs + $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); + + // ### should not parse a prefix declaration + $this->shouldNotParse($parser, '@prefix : .', 'Unexpected "@prefix" on line 1.'); + + // ### should not parse a variable + $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); + + // ### should not parse an equality statement + $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); + + // ### should not parse a right implication statement + $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); + + // ### should not parse a left implication statement + $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); + + // ### should not parse a formula as object + $this->shouldNotParse($parser, ' {}.', 'Unexpected "{" on line 1.'); + + } + + public function testN3Format () + { + $parser = function () { return new TriGParser([ "format"=> 'N3' ]); }; + + // ### should parse a single triple + $this->shouldParse($parser, ' .', ['a', 'b', 'c']); + + // ### should not parse a default graph + $this->shouldNotParse($parser, '{}', 'Expected entity but got eof on line 1.'); + + // ### should not parse a named graph + $this->shouldNotParse($parser, ' {}', 'Expected entity but got { on line 1.'); + + // ### should not parse a named graph with the GRAPH keyword + $this->shouldNotParse($parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.'); + + // ### should not parse a quad + $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); + + // ### allows a blank node label in predicate position + $this->shouldParse($parser, ' _:b .', ['a', '_:b0_b', 'c']); + + // ### should parse a variable + $this->shouldParse($parser, '?a ?b ?c.', ['?a', '?b', '?c']); + + // ### should parse a simple equality + $this->shouldParse($parser, ' = .', + ['a', 'http://www.w3.org/2002/07/owl#sameAs', 'b']); + + // ### should parse a simple right implication + $this->shouldParse($parser, ' => .', + ['a', 'http://www.w3.org/2000/10/swap/log#implies', 'b']); + + // ### should parse a simple left implication + $this->shouldParse($parser, ' <= .', + ['b', 'http://www.w3.org/2000/10/swap/log#implies', 'a']); + + // ### should parse a right implication between one-triple graphs + $this->shouldParse($parser, '{ ?a ?b . } => { ?a }.', + ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', '?a', '_:b1']); + + // ### should parse a right implication between two-triple graphs + $this->shouldParse($parser, '{ ?a ?b . . } => { ?a, }.', + ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', 'f', '_:b0'], + ['d', 'e', '?a', '_:b1'], + ['d', 'e', 'f', '_:b1']); + + // ### should parse a left implication between one-triple graphs + $this->shouldParse($parser, '{ ?a ?b . } <= { ?a }.', + ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', '?a', '_:b1']); + + // ### should parse a left implication between two-triple graphs + $this->shouldParse($parser, '{ ?a ?b . . } <= { ?a, }.', + ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', 'f', '_:b0'], + ['d', 'e', '?a', '_:b1'], + ['d', 'e', 'f', '_:b1']); + + // ### should parse an equality of one-triple graphs + $this->shouldParse($parser, '{ ?a ?b . } = { ?a }.', + ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', '?a', '_:b1']); + + // ### should parse an equality of two-triple graphs + $this->shouldParse($parser, '{ ?a ?b . . } = { ?a, }.', + ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], + ['?a', '?b', 'c', '_:b0'], + ['d', 'e', 'f', '_:b0'], + ['d', 'e', '?a', '_:b1'], + ['d', 'e', 'f', '_:b1']); + + // ### should parse nested implication graphs + $this->shouldParse($parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', + ['_:b3', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', '_:b0'], + ['?a', '?b', '?c', '_:b1'], + ['?d', '?e', '?f', '_:b2'], + ['_:b4', 'http://www.w3.org/2000/10/swap/log#implies', '_:b5', '_:b3'], + ['?g', '?h', '?i', '_:b4'], + ['?j', '?k', '?l', '_:b5']); + + // ### should not reuse identifiers of blank nodes within and outside of formulas + $this->shouldParse($parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', + ['_:b0_a', '_:b0_b', '_:b0_c'], + ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', ''], + ['_:b0.a', '_:b0.b', '_:b0.c', '_:b0'], + ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b3', '_:b1'], + ['_:b2.a', '_:b2.b', '_:b2.c', '_:b2'], + ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3']); + + // ### should parse a @forSome statement + $this->shouldParse($parser, '@forSome . .', + ['_:b0', '_:b0', '_:b0']); + + // ### should parse a @forSome statement with multiple entities + $this->shouldParse($parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + ['_:b0', '_:b1', '_:b2']); + + // ### should not parse a @forSome statement with an invalid prefix + $this->shouldNotParse($parser, '@forSome a:b.', + 'Undefined prefix "a:" on line 1.'); + + // ### should not parse a @forSome statement with a blank node + $this->shouldNotParse($parser, '@forSome _:a.', + 'Unexpected blank on line 1.'); + + // ### should not parse a @forSome statement with a variable + $this->shouldNotParse($parser, '@forSome ?a.', + 'Unexpected $on line 1.'); + // ### should correctly scope @forSome statements + $this->shouldParse($parser, '@forSome . { @forSome . . }. .', + ['_:b0', '_:b0', '_:b1'], + ['_:b2', '_:b2', '_:b2', '_:b1'], + ['_:b0', '_:b0', '_:b0']); + // ### should parse a @forAll statement + $this->shouldParse($parser, '@forAll . .', + ['?b-0', '?b-0', '?b-0']); + // ### should parse a @forAll statement with multiple entities + $this->shouldParse($parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + ['?b-0', '?b-1', '?b-2']); + + // ### should not parse a @forAll statement with an invalid prefix + $this->shouldNotParse($parser, '@forAll a:b.', + 'Undefined prefix "a:" on line 1.'); + + // ### should not parse a @forAll statement with a blank node + $this->shouldNotParse($parser, '@forAll _:a.', + 'Unexpected blank on line 1.'); + + // ### should not parse a @forAll statement with a variable + $this->shouldNotParse($parser, '@forAll ?a.', + 'Unexpected $on line 1.'); + + // ### should correctly scope @forAll statements + $this->shouldParse($parser, '@forAll . { @forAll . . }. .', + ['?b-0', '?b-0', '_:b1'], + ['?b-2', '?b-2', '?b-2', '_:b1'], + ['?b-0', '?b-0', '?b-0']); + + // ### should parse a ! path of length 2 as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ':joe!fam:mother a fam:Person.', + ['ex:joe', 'f:mother', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); + + // ### should parse a ! path of length 4 as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: . @prefix loc: .' . + ':joe!fam:mother!loc:office!loc:zip loc:code 1234.', + ['ex:joe', 'f:mother', '_:b0'], + ['_:b0', 'l:office', '_:b1'], + ['_:b1', 'l:zip', '_:b2'], + ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer']); + + // ### should parse a ! path of length 2 as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' :joe!fam:mother.', + ['x', 'is', '_:b0'], + ['ex:joe', 'f:mother', '_:b0']); + + // ### should parse a ! path of length 4 as object + $this->shouldParse($parser, '@prefix : . @prefix fam: . @prefix loc: .' . + ' :joe!fam:mother!loc:office!loc:zip.', + ['x', 'is', '_:b2'], + ['ex:joe', 'f:mother', '_:b0'], + ['_:b0', 'l:office', '_:b1'], + ['_:b1', 'l:zip', '_:b2']); + + // ### should parse a ^ path of length 2 as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ':joe^fam:son a fam:Person.', + ['_:b0', 'f:son', 'ex:joe'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); + + // ### should parse a ^ path of length 4 as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ':joe^fam:son^fam:sister^fam:mother a fam:Person.', + ['_:b0', 'f:son', 'ex:joe'], + ['_:b1', 'f:sister', '_:b0'], + ['_:b2', 'f:mother', '_:b1'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); + + // ### should parse a ^ path of length 2 as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' :joe^fam:son.', + ['x', 'is', '_:b0'], + ['_:b0', 'f:son', 'ex:joe']); + + // ### should parse a ^ path of length 4 as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' :joe^fam:son^fam:sister^fam:mother.', + ['x', 'is', '_:b2'], + ['_:b0', 'f:son', 'ex:joe'], + ['_:b1', 'f:sister', '_:b0'], + ['_:b2', 'f:mother', '_:b1']); + + // ### should parse mixed !/^ paths as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ':joe!fam:mother^fam:mother a fam:Person.', + ['ex:joe', 'f:mother', '_:b0'], + ['_:b1', 'f:mother', '_:b0'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); + + // ### should parse mixed !/^ paths as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' :joe!fam:mother^fam:mother.', + ['x', 'is', '_:b1'], + ['ex:joe', 'f:mother', '_:b0'], + ['_:b1', 'f:mother', '_:b0']); + + // ### should parse a ! path in a blank node as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + '[fam:knows :joe!fam:mother] a fam:Person.', + ['_:b0', 'f:knows', '_:b1'], + ['ex:joe', 'f:mother', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); + + // ### should parse a ! path in a blank node as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' [fam:knows :joe!fam:mother].', + ['x', 'is', '_:b0'], + ['_:b0', 'f:knows', '_:b1'], + ['ex:joe', 'f:mother', '_:b1']); + + // ### should parse a ^ path in a blank node as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + '[fam:knows :joe^fam:son] a fam:Person.', + ['_:b0', 'f:knows', '_:b1'], + ['_:b1', 'f:son', 'ex:joe'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); + + // ### should parse a ^ path in a blank node as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' [fam:knows :joe^fam:son].', + ['x', 'is', '_:b0'], + ['_:b0', 'f:knows', '_:b1'], + ['_:b1', 'f:son', 'ex:joe']); + + // ### should parse a ! path in a list as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + '( :joe!fam:mother ) a :List.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['ex:joe', 'f:mother', '_:b2']); + + // ### should parse a ! path in a list as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' ( :joe!fam:mother ).', + ['l', 'is', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['ex:joe', 'f:mother', '_:b2']); + + // ### should parse a ^ path in a list as subject + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + '( :joe^fam:son ) a :List.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b2', 'f:son', 'ex:joe']); + + // ### should parse a ^ path in a list as object + $this->shouldParse($parser, '@prefix : . @prefix fam: .' . + ' ( :joe^fam:son ).', + ['l', 'is', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], + ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b2', 'f:son', 'ex:joe']); + + // ### should not parse an invalid ! path + $this->shouldNotParse($parser, '!"invalid" ', 'Expected entity but got literal on line 1.'); + + // ### should not parse an invalid ^ path + $this->shouldNotParse($parser, '^"invalid" ', 'Expected entity but got literal on line 1.'); + } + + public function testN3ExplicitQuantifiers () + { + $parser = function () { return new TriGParser([ "format"=> 'N3', "explicitQuantifiers" => true ]); }; + + // ### should parse a @forSome statement + $this->shouldParse($parser, '@forSome . .', + ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x']); + + // ### should parse a @forSome statement with multiple entities + $this->shouldParse($parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', + ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b:y', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['a:x', 'b:y', 'a:z']); + + // ### should correctly scope @forSome statements + $this->shouldParse($parser, '@forSome . { @forSome . . }. .', + ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', '_:b1'], + ['_:b1', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x', '_:b1'], + ['x', 'x', 'x']); + + // ### should parse a @forAll statement + $this->shouldParse($parser, '@forAll . .', + ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x']); + + // ### should parse a @forAll statement with multiple entities + $this->shouldParse($parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', + ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b:y', 'urn:n3:quantifiers'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['a:x', 'b:y', 'a:z']); + + // ### should correctly scope @forAll statements + $this->shouldParse($parser, '@forAll . { @forAll . . }. .', + ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', '_:b1'], + ['_:b1', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b2', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], + ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], + ['x', 'x', 'x', '_:b1'], + ['x', 'x', 'x']); + } + public function testResolve() { //describe('IRI resolution', function () { @@ -1554,472 +2025,4 @@ private static function toSortedJSON ($items) sort($triples); return "[\n " . join("\n ", $triples) . "\n]"; } -} - -/* - describe('An TriGParser instance for the TriG format', function () { - $parser = function () { return new TriGParser([ format: 'TriG' ]); }; - - // ### should parse a single triple - $this->shouldParse($parser, ' .', ['a', 'b', 'c']); - - // ### should parse a default graph - $this->shouldParse($parser, '{}'); - - // ### should parse a named graph - $this->shouldParse($parser, ' {}'); - - // ### should parse a named graph with the GRAPH keyword - $this->shouldParse($parser, 'GRAPH {}'); - - // ### should not parse a quad - $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); - - // ### should not parse a variable - $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); - - // ### should not parse an equality statement - $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); - - // ### should not parse a right implication statement - $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); - - // ### should not parse a left implication statement - $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); - - // ### should not parse a formula as object - $this->shouldNotParse($parser, ' {}.', 'Unexpected graph on line 1.'); - }); - - describe('An TriGParser instance for the N-Triples format', function () { - $parser = function () { return new TriGParser([ format: 'N-Triples' ]); }; - - // ### should parse a single triple - $this->shouldParse($parser, ' "c".', - ['http://ex.org/a', 'http://ex.org/b', '"c"']); - - // ### should not parse a single quad - $this->shouldNotParse($parser, ' "c" .', - 'Expected punctuation to follow ""c"" on line 1.'); - - // ### should not parse relative IRIs - $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); - - // ### should not parse a prefix declaration - $this->shouldNotParse($parser, '@prefix : .', 'Unexpected "@prefix" on line 1.'); - - // ### should not parse a variable - $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); - - // ### should not parse an equality statement - $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); - - // ### should not parse a right implication statement - $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); - - // ### should not parse a left implication statement - $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); - - // ### should not parse a formula as object - $this->shouldNotParse($parser, ' {}.', 'Unexpected "{" on line 1.'); - }); - - describe('An TriGParser instance for the N-Quads format', function () { - $parser = function () { return new TriGParser([ format: 'N-Quads' ]); }; - - // ### should parse a single triple - $this->shouldParse($parser, ' .', - ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c']); - - // ### should parse a single quad - $this->shouldParse($parser, ' "c" .', - ['http://ex.org/a', 'http://ex.org/b', '"c"', 'http://ex.org/g']); - - // ### should not parse relative IRIs - $this->shouldNotParse($parser, ' .', 'Disallowed relative IRI on line 1.'); - - // ### should not parse a prefix declaration - $this->shouldNotParse($parser, '@prefix : .', 'Unexpected "@prefix" on line 1.'); - - // ### should not parse a variable - $this->shouldNotParse($parser, '?a ?b ?c.', 'Unexpected "?a" on line 1.'); - - // ### should not parse an equality statement - $this->shouldNotParse($parser, ' = .', 'Unexpected "=" on line 1.'); - - // ### should not parse a right implication statement - $this->shouldNotParse($parser, ' => .', 'Unexpected "=>" on line 1.'); - - // ### should not parse a left implication statement - $this->shouldNotParse($parser, ' <= .', 'Unexpected "<=" on line 1.'); - - // ### should not parse a formula as object - $this->shouldNotParse($parser, ' {}.', 'Unexpected "{" on line 1.'); - }); - - describe('An TriGParser instance for the N3 format', function () { - $parser = function () { return new TriGParser([ format: 'N3' ]); }; - - // ### should parse a single triple - $this->shouldParse($parser, ' .', ['a', 'b', 'c']); - - // ### should not parse a default graph - $this->shouldNotParse($parser, '{}', 'Expected entity but got eof on line 1.'); - - // ### should not parse a named graph - $this->shouldNotParse($parser, ' {}', 'Expected entity but got { on line 1.'); - - // ### should not parse a named graph with the GRAPH keyword - $this->shouldNotParse($parser, 'GRAPH {}', 'Expected entity but got GRAPH on line 1.'); - - // ### should not parse a quad - $this->shouldNotParse($parser, ' .', 'Expected punctuation to follow "c" on line 1.'); - - // ### allows a blank node label in predicate position - $this->shouldParse($parser, ' _:b .', ['a', '_:b0_b', 'c']); - - // ### should parse a variable - $this->shouldParse($parser, '?a ?b ?c.', ['?a', '?b', '?c']); - - // ### should parse a simple equality - $this->shouldParse($parser, ' = .', - ['a', 'http://www.w3.org/2002/07/owl#sameAs', 'b']); - - // ### should parse a simple right implication - $this->shouldParse($parser, ' => .', - ['a', 'http://www.w3.org/2000/10/swap/log#implies', 'b']); - - // ### should parse a simple left implication - $this->shouldParse($parser, ' <= .', - ['b', 'http://www.w3.org/2000/10/swap/log#implies', 'a']); - - // ### should parse a right implication between one-triple graphs - $this->shouldParse($parser, '{ ?a ?b . } => { ?a }.', - ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], - ['?a', '?b', 'c', '_:b0'], - ['d', 'e', '?a', '_:b1']); - - // ### should parse a right implication between two-triple graphs - $this->shouldParse($parser, '{ ?a ?b . . } => { ?a, }.', - ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1'], - ['?a', '?b', 'c', '_:b0'], - ['d', 'e', 'f', '_:b0'], - ['d', 'e', '?a', '_:b1'], - ['d', 'e', 'f', '_:b1']); - - // ### should parse a left implication between one-triple graphs - $this->shouldParse($parser, '{ ?a ?b . } <= { ?a }.', - ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], - ['?a', '?b', 'c', '_:b0'], - ['d', 'e', '?a', '_:b1']); - - // ### should parse a left implication between two-triple graphs - $this->shouldParse($parser, '{ ?a ?b . . } <= { ?a, }.', - ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], - ['?a', '?b', 'c', '_:b0'], - ['d', 'e', 'f', '_:b0'], - ['d', 'e', '?a', '_:b1'], - ['d', 'e', 'f', '_:b1']); - - // ### should parse an equality of one-triple graphs - $this->shouldParse($parser, '{ ?a ?b . } = { ?a }.', - ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], - ['?a', '?b', 'c', '_:b0'], - ['d', 'e', '?a', '_:b1']); - - // ### should parse an equality of two-triple graphs - $this->shouldParse($parser, '{ ?a ?b . . } = { ?a, }.', - ['_:b0', 'http://www.w3.org/2002/07/owl#sameAs', '_:b1'], - ['?a', '?b', 'c', '_:b0'], - ['d', 'e', 'f', '_:b0'], - ['d', 'e', '?a', '_:b1'], - ['d', 'e', 'f', '_:b1']); - - // ### should parse nested implication graphs - $this->shouldParse($parser, '{ { ?a ?b ?c }<={ ?d ?e ?f }. } <= { { ?g ?h ?i } => { ?j ?k ?l } }.', - ['_:b3', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], - ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', '_:b0'], - ['?a', '?b', '?c', '_:b1'], - ['?d', '?e', '?f', '_:b2'], - ['_:b4', 'http://www.w3.org/2000/10/swap/log#implies', '_:b5', '_:b3'], - ['?g', '?h', '?i', '_:b4'], - ['?j', '?k', '?l', '_:b5']); - - // ### should not reuse identifiers of blank nodes within and outside of formulas - $this->shouldParse($parser, '_:a _:b _:c. { _:a _:b _:c } => { { _:a _:b _:c } => { _:a _:b _:c } }.', - ['_:b0_a', '_:b0_b', '_:b0_c'], - ['_:b0', 'http://www.w3.org/2000/10/swap/log#implies', '_:b1', ''], - ['_:b0.a', '_:b0.b', '_:b0.c', '_:b0'], - ['_:b2', 'http://www.w3.org/2000/10/swap/log#implies', '_:b3', '_:b1'], - ['_:b2.a', '_:b2.b', '_:b2.c', '_:b2'], - ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3']); - - // ### should parse a @forSome statement - $this->shouldParse($parser, '@forSome . .', - ['_:b0', '_:b0', '_:b0']); - - // ### should parse a @forSome statement with multiple entities - $this->shouldParse($parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', - ['_:b0', '_:b1', '_:b2']); - - // ### should not parse a @forSome statement with an invalid prefix - $this->shouldNotParse($parser, '@forSome a:b.', - 'Undefined prefix "a:" on line 1.'); - - // ### should not parse a @forSome statement with a blank node - $this->shouldNotParse($parser, '@forSome _:a.', - 'Unexpected blank on line 1.'); - - // ### should not parse a @forSome statement with a variable - $this->shouldNotParse($parser, '@forSome ?a.', - 'Unexpected $on line 1.'); - - // ### should correctly scope @forSome statements - $this->shouldParse($parser, '@forSome . { @forSome . . }. .', - ['_:b0', '_:b0', '_:b1'], - ['_:b2', '_:b2', '_:b2', '_:b1'], - ['_:b0', '_:b0', '_:b0']); - - // ### should parse a @forAll statement - $this->shouldParse($parser, '@forAll . .', - ['?b-0', '?b-0', '?b-0']); - - // ### should parse a @forAll statement with multiple entities - $this->shouldParse($parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', - ['?b-0', '?b-1', '?b-2']); - - // ### should not parse a @forAll statement with an invalid prefix - $this->shouldNotParse($parser, '@forAll a:b.', - 'Undefined prefix "a:" on line 1.'); - - // ### should not parse a @forAll statement with a blank node - $this->shouldNotParse($parser, '@forAll _:a.', - 'Unexpected blank on line 1.'); - - // ### should not parse a @forAll statement with a variable - $this->shouldNotParse($parser, '@forAll ?a.', - 'Unexpected $on line 1.'); - - // ### should correctly scope @forAll statements - $this->shouldParse($parser, '@forAll . { @forAll . . }. .', - ['?b-0', '?b-0', '_:b1'], - ['?b-2', '?b-2', '?b-2', '_:b1'], - ['?b-0', '?b-0', '?b-0']); - - // ### should parse a ! path of length 2 as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ':joe!fam:mother a fam:Person.', - ['ex:joe', 'f:mother', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); - - // ### should parse a ! path of length 4 as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: . @prefix loc: .' . - ':joe!fam:mother!loc:office!loc:zip loc:code 1234.', - ['ex:joe', 'f:mother', '_:b0'], - ['_:b0', 'l:office', '_:b1'], - ['_:b1', 'l:zip', '_:b2'], - ['_:b2', 'l:code', '"1234"^^http://www.w3.org/2001/XMLSchema#integer']); - - // ### should parse a ! path of length 2 as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' :joe!fam:mother.', - ['x', 'is', '_:b0'], - ['ex:joe', 'f:mother', '_:b0']); - - // ### should parse a ! path of length 4 as object - $this->shouldParse($parser, '@prefix : . @prefix fam: . @prefix loc: .' . - ' :joe!fam:mother!loc:office!loc:zip.', - ['x', 'is', '_:b2'], - ['ex:joe', 'f:mother', '_:b0'], - ['_:b0', 'l:office', '_:b1'], - ['_:b1', 'l:zip', '_:b2']); - - // ### should parse a ^ path of length 2 as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ':joe^fam:son a fam:Person.', - ['_:b0', 'f:son', 'ex:joe'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); - - // ### should parse a ^ path of length 4 as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ':joe^fam:son^fam:sister^fam:mother a fam:Person.', - ['_:b0', 'f:son', 'ex:joe'], - ['_:b1', 'f:sister', '_:b0'], - ['_:b2', 'f:mother', '_:b1'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); - - // ### should parse a ^ path of length 2 as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' :joe^fam:son.', - ['x', 'is', '_:b0'], - ['_:b0', 'f:son', 'ex:joe']); - - // ### should parse a ^ path of length 4 as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' :joe^fam:son^fam:sister^fam:mother.', - ['x', 'is', '_:b2'], - ['_:b0', 'f:son', 'ex:joe'], - ['_:b1', 'f:sister', '_:b0'], - ['_:b2', 'f:mother', '_:b1']); - - // ### should parse mixed !/^ paths as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ':joe!fam:mother^fam:mother a fam:Person.', - ['ex:joe', 'f:mother', '_:b0'], - ['_:b1', 'f:mother', '_:b0'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); - - // ### should parse mixed !/^ paths as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' :joe!fam:mother^fam:mother.', - ['x', 'is', '_:b1'], - ['ex:joe', 'f:mother', '_:b0'], - ['_:b1', 'f:mother', '_:b0']); - - // ### should parse a ! path in a blank node as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - '[fam:knows :joe!fam:mother] a fam:Person.', - ['_:b0', 'f:knows', '_:b1'], - ['ex:joe', 'f:mother', '_:b1'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); - - // ### should parse a ! path in a blank node as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' [fam:knows :joe!fam:mother].', - ['x', 'is', '_:b0'], - ['_:b0', 'f:knows', '_:b1'], - ['ex:joe', 'f:mother', '_:b1']); - - // ### should parse a ^ path in a blank node as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - '[fam:knows :joe^fam:son] a fam:Person.', - ['_:b0', 'f:knows', '_:b1'], - ['_:b1', 'f:son', 'ex:joe'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person']); - - // ### should parse a ^ path in a blank node as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' [fam:knows :joe^fam:son].', - ['x', 'is', '_:b0'], - ['_:b0', 'f:knows', '_:b1'], - ['_:b1', 'f:son', 'ex:joe']); - - // ### should parse a ! path in a list as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - '( :joe!fam:mother ) a :List.', - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['ex:joe', 'f:mother', '_:b2']); - - // ### should parse a ! path in a list as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' ( :joe!fam:mother ).', - ['l', 'is', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['ex:joe', 'f:mother', '_:b2']); - - // ### should parse a ^ path in a list as subject - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - '( :joe^fam:son ) a :List.', - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b2', 'f:son', 'ex:joe']); - - // ### should parse a ^ path in a list as object - $this->shouldParse($parser, '@prefix : . @prefix fam: .' . - ' ( :joe^fam:son ).', - ['l', 'is', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b3'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'y'], - ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], - ['_:b2', 'f:son', 'ex:joe']); - - // ### should not parse an invalid ! path - $this->shouldNotParse($parser, '!"invalid" ', 'Expected entity but got literal on line 1.'); - - // ### should not parse an invalid ^ path - $this->shouldNotParse($parser, '^"invalid" ', 'Expected entity but got literal on line 1.'); - }); - - describe('An TriGParser instance for the N3 format with the explicitQuantifiers option', function () { - $parser = function () { return new TriGParser([ "format"=> 'N3', "explicitQuantifiers": true ]); } - - // ### should parse a @forSome statement - $this->shouldParse($parser, '@forSome . .', - ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', 'x']); - - // ### should parse a @forSome statement with multiple entities - $this->shouldParse($parser, '@prefix a: . @base . @forSome a:x, , a:z. a:x a:z.', - ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b:y', 'urn:n3:quantifiers'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['a:x', 'b:y', 'a:z']); - - // ### should correctly scope @forSome statements - $this->shouldParse($parser, '@forSome . { @forSome . . }. .', - ['', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b0', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', '_:b1'], - ['_:b1', 'http://www.w3.org/2000/10/swap/reify#forSome', '_:b2', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', 'x', '_:b1'], - ['x', 'x', 'x']); - - // ### should parse a @forAll statement - $this->shouldParse($parser, '@forAll . .', - ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', 'x']); - - // ### should parse a @forAll statement with multiple entities - $this->shouldParse($parser, '@prefix a: . @base . @forAll a:x, , a:z. a:x a:z.', - ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:x', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b1', 'urn:n3:quantifiers'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b:y', 'urn:n3:quantifiers'], - ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'a:z', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['a:x', 'b:y', 'a:z']); - - // ### should correctly scope @forAll statements - $this->shouldParse($parser, '@forAll . { @forAll . . }. .', - ['', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b0', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', '_:b1'], - ['_:b1', 'http://www.w3.org/2000/10/swap/reify#forAll', '_:b2', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'x', 'urn:n3:quantifiers'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'urn:n3:quantifiers'], - ['x', 'x', 'x', '_:b1'], - ['x', 'x', 'x']); - }); -*/ \ No newline at end of file +} \ No newline at end of file From 021a78d4fa89a264ecd7dab7236692da286c7d6e Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sun, 9 Apr 2017 22:33:54 +0200 Subject: [PATCH 39/72] Fixed relative base iri --- src/TriGParser.php | 10 ++++++++-- test/TriGParserTest.php | 5 +---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index b5b9474..17cf016 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -91,6 +91,7 @@ private function setBase ($baseIRI = null) { $baseIRI = substr($baseIRI,0, $fragmentPos); // Set base IRI and its components $this->base = $baseIRI; + var_dump($baseIRI, preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI)); $this->basePath = !strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); preg_match($this->schemeAuthority, $baseIRI, $matches); $this->baseRoot = isset($matches[0])?$matches[0]:''; @@ -833,6 +834,7 @@ private function initReaders () // assuming that a base path has been set and that the IRI is indeed relative $this->resolveIRI = function ($token) { $iri = $token["value"]; + if (!isset($iri[0])) // An empty relative IRI indicates the base IRI return $this->base; @@ -854,6 +856,7 @@ private function initReaders () // ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986 $this->removeDotSegments = function ($iri) { + var_dump('THIS: '.$iri); // Don't modify the IRI if it does not contain any dot segments if (!preg_match($this->dotSegments,$iri)) return $iri; @@ -897,13 +900,15 @@ private function initReaders () case '/': if (isset($iri[$i + 1]) && $iri[$i + 1] === '.') { if (isset($iri[++$i + 1])) { + var_dump("this4: ". $iri. " " . $i+1); $next = $iri[$i + 1]; } else $next = null; switch ($next) { // Remove a '/.' segment case '/': - $result .= substr($iri, $segmentStart, $i - 1 - $segmentStart); + if (($i - 1 - $segmentStart) > 0) + $result .= substr($iri, $segmentStart, $i - 1 - $segmentStart); $segmentStart = $i + 1; break; // Remove a trailing '/.' segment @@ -919,7 +924,8 @@ private function initReaders () $next = null; } if ($next === null || $next === '/' || $next === '?' || $next === '#') { - $result .= substr($iri, $segmentStart, $i - 2 - $segmentStart); + if ($i - 2 - $segmentStart > 0) + $result .= substr($iri, $segmentStart, $i - 2 - $segmentStart); // Try to remove the parent path from result if (($segmentStart = $rstrpos($result,"/")) >= $pathStart) { $result = substr($result,0, $segmentStart); diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 3b704bc..6bcb95a 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1943,8 +1943,7 @@ public function testResolve() // base path with slashes in query string $this->itShouldResolve('http://abc/def/ghi?q=xx/yyy/z', 'jjj', 'http://abc/def/jjj'); $this->itShouldResolve('http://abc/def/ghi?q=xx/y?y/z', 'jjj', 'http://abc/def/jjj'); - } - + } private function shouldParse($createParser, $input = "") { @@ -1973,7 +1972,6 @@ private function shouldParse($createParser, $input = "") $this->assertEquals(self::toSortedJSON($items), self::toSortedJSON($results)); }); } - function shouldNotParse($createParser, $input, $expectedError = null) { $expected = array_slice(func_get_args(),1); @@ -2018,7 +2016,6 @@ function itShouldResolve($baseIri, $relativeIri, $expected) { catch (\Exception $error) { throw $error; $this->fail("Resolving <$relativeIri> against <$baseIri>.\nError message: " . $error->getMessage()); } } - private static function toSortedJSON ($items) { $triples = array_map("json_encode", $items); From dbe64349fd60b11c462374435c86ddae6146aa4b Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Sun, 9 Apr 2017 22:35:33 +0200 Subject: [PATCH 40/72] Removed debugging --- src/TriGParser.php | 3 --- test/TriGParserTest.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 17cf016..c2ccc3f 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -91,7 +91,6 @@ private function setBase ($baseIRI = null) { $baseIRI = substr($baseIRI,0, $fragmentPos); // Set base IRI and its components $this->base = $baseIRI; - var_dump($baseIRI, preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI)); $this->basePath = !strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); preg_match($this->schemeAuthority, $baseIRI, $matches); $this->baseRoot = isset($matches[0])?$matches[0]:''; @@ -856,7 +855,6 @@ private function initReaders () // ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986 $this->removeDotSegments = function ($iri) { - var_dump('THIS: '.$iri); // Don't modify the IRI if it does not contain any dot segments if (!preg_match($this->dotSegments,$iri)) return $iri; @@ -900,7 +898,6 @@ private function initReaders () case '/': if (isset($iri[$i + 1]) && $iri[$i + 1] === '.') { if (isset($iri[++$i + 1])) { - var_dump("this4: ". $iri. " " . $i+1); $next = $iri[$i + 1]; } else $next = null; diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 6bcb95a..3a5742c 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -2013,7 +2013,7 @@ function itShouldResolve($baseIri, $relativeIri, $expected) { $done = true; }); } - catch (\Exception $error) { throw $error; $this->fail("Resolving <$relativeIri> against <$baseIri>.\nError message: " . $error->getMessage()); } + catch (\Exception $error) { $this->fail("Resolving <$relativeIri> against <$baseIri>.\nError message: " . $error->getMessage()); } } private static function toSortedJSON ($items) From 0f08a06e8e59fa828f5f4706672237ca1b67a671 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 10 Apr 2017 08:40:08 +0200 Subject: [PATCH 41/72] Tokenize can now be hotswapped --- src/N3Lexer.php | 44 +++++++++++++++++++++++------------------ src/TriGParser.php | 5 ++--- test/TriGParserTest.php | 5 +++-- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 44ba4dd..7a048cf 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -25,7 +25,7 @@ class N3Lexer private $prevTokenType; public function __construct($options = []) { - + $this->initTokenize(); // In line mode (N-Triples or N-Quads), only simple features may be parsed if ($options["lineMode"]) { // Don't tokenize special literals @@ -33,15 +33,15 @@ public function __construct($options = []) { $this->number = '/$0^/'; $this->boolean = '/$0^/'; // Swap the tokenize method for a restricted version - /*$this->tokenize = $this->tokenize; //TODO: what was this originally? - $this->tokenize = function ($input, $callback) { - $this->tokenize($input, function ($error, $token) { + $this->_oldTokenize = $this->_tokenize; + $this->_tokenize = function ($input, $callback) { + $this->_oldTokenize($input, function ($error, $token) { if (!$error && preg_match('/^(?:IRI|prefixed|literal|langcode|type|\.|eof)$/',$token["type"])) $callback && callback($error, $token); else - $callback && callback($error || self::_syntaxError($token['type'], $callback = null)); + $callback && callback($error || $this->syntaxError($token['type'], $callback = null)); }); - };*/ + }; } // Enable N3 functionality by default $this->n3Mode = $options["n3"] !== false; @@ -65,7 +65,6 @@ public function __construct($options = []) { private $variable = '/^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)(?=[.,;!\^\s#()\[\]\{\}"\'<])/'; private $blank = '/^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)(?:[ \t]+|(?=\.?[,;:\s#()\[\]\{\}"\'<]))/'; - //TODO: this doesn't work private $number = "/^[\-+]?(?:\d+\.?\d*([eE](?:[\-\+])?\d+)|\d*\.?\d+)(?=[.,;:\s#()\[\]\{\}\"'<])/"; private $boolean = '/^(?:true|false)(?=[.,;\s#()\[\]\{\}"\'<])/'; private $keyword = '/^@[a-z]+(?=[\s#<])/i'; @@ -407,24 +406,31 @@ private function syntaxError($issue, $line = 0) { return new \Exception('Unexpected "' . $issue . '" on line ' . $line . '.'); } + // When handling tokenize as a variable, we can hotswap its functionality when dealing with various serializations + private function initTokenize() + { + $this->_tokenize = function ($input, $finalize) { + // If the input is a string, continuously emit tokens through the callback until the end + $this->input = $input; + $tokens = []; + $error = ""; + $this->tokenizeToEnd(function ($e, $t) use (&$tokens,&$error) { + if (isset($e)) { + $error = $e; + } + array_push($tokens, $t); + }, $finalize); + if ($error) throw $error; + return $tokens; + }; + } // ## Public methods // ### `tokenize` starts the transformation of an N3 document into an array of tokens. // The input can be a string or a stream. public function tokenize($input, $finalize = true) { - // If the input is a string, continuously emit tokens through the callback until the end - $this->input = $input; - $tokens = []; - $error = ""; - $this->tokenizeToEnd(function ($e, $t) use (&$tokens,&$error) { - if (isset($e)) { - $error = $e; - } - array_push($tokens, $t); - }, $finalize); - if ($error) throw $error; - return $tokens; + return call_user_func($this->_tokenize, $input, $finalize); } // Adds the data chunk to the buffer and parses as far as possible diff --git a/src/TriGParser.php b/src/TriGParser.php index c2ccc3f..89abd87 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -67,9 +67,8 @@ public function __construct($options = []) { $this->blankNodePrefix = '_:' . preg_replace('/^_:/', '', $options["blankNodePrefix"]); } - $this->lexer = isset($options["lexer"])? $options["lexer"] : new N3Lexer([ "lineMode"=> $isLineMode, "n3"=> $isN3 ]); - // Disable explicit quantifiers by default - TODO: is !! same in PHP? + // Disable explicit quantifiers by default $this->explicitQuantifiers = isset($options["explicitQuantifiers"])?$options["explicitQuantifiers"]:null; } @@ -826,7 +825,7 @@ private function initReaders () // ### `_error` emits an error message through the callback $this->error = function ($message, $token) { - call_user_func($this->callback, new \Exception($message . ' on line ' . $token['line'] . '.')); + call_user_func($this->callback, new \Exception($message . ' on line ' . $token['line'] . '.'),null); }; // ### `_resolveIRI` resolves a relative IRI token against the base path, diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 3a5742c..20d7522 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1987,13 +1987,14 @@ function shouldNotParse($createParser, $input, $expectedError = null) { $parser->_resetBlankNodeIds(); //hackish way so we only act upon first error $errorReceived = false; - $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$errorReceived){ + $parser->parse($input, function ($error, $triple = null) use (&$expectedError, &$errorReceived){ //expect($error).not.to.exist; if (isset($error) && !$errorReceived) { $this->assertEquals($expectedError, $error->getMessage()); $errorReceived = true; } else if (!isset($triple) && !$errorReceived) { $this->fail("Expected this error to be thrown (but it wasn't): " . $expectedError); + $errorReceived = true; } }); } @@ -2003,7 +2004,7 @@ function itShouldResolve($baseIri, $relativeIri, $expected) { try { $doc = ' <' . $relativeIri . '>.'; $parser = new TriGParser([ "documentIRI" => $baseIri]); - $parser->parse($doc, function ($error, $triple) use ($done, $expected) { + $parser->parse($doc, function ($error, $triple) use (&$done, &$expected) { if (!$done && $triple) { $this->assertEquals($expected, $triple["object"]); } From 43e9e7fcdc50bedb80e8155dc81b1d9f2aa1a14f Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 10 Apr 2017 10:42:03 +0200 Subject: [PATCH 42/72] Fixed n-triples and n-quads --- src/N3Lexer.php | 24 +++++++++++++++--------- src/TriGParser.php | 2 +- test/TriGParserTest.php | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 7a048cf..ab2c848 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -34,13 +34,15 @@ public function __construct($options = []) { $this->boolean = '/$0^/'; // Swap the tokenize method for a restricted version $this->_oldTokenize = $this->_tokenize; - $this->_tokenize = function ($input, $callback) { - $this->_oldTokenize($input, function ($error, $token) { - if (!$error && preg_match('/^(?:IRI|prefixed|literal|langcode|type|\.|eof)$/',$token["type"])) - $callback && callback($error, $token); - else - $callback && callback($error || $this->syntaxError($token['type'], $callback = null)); - }); + $self = $this; + $this->_tokenize = function ($input, $finalize = true) use ($self) { + $tokens = call_user_func($this->_oldTokenize, $input, $finalize); + foreach ($tokens as $token) { + if (!preg_match('/^(?:IRI|prefixed|literal|langcode|type|\.|eof)$/',$token["type"])) { + throw $self->syntaxError($token['type'], $token['line']); + } + } + return $tokens; }; } // Enable N3 functionality by default @@ -421,7 +423,7 @@ private function initTokenize() array_push($tokens, $t); }, $finalize); if ($error) throw $error; - return $tokens; + return $tokens; }; } @@ -430,7 +432,11 @@ private function initTokenize() // ### `tokenize` starts the transformation of an N3 document into an array of tokens. // The input can be a string or a stream. public function tokenize($input, $finalize = true) { - return call_user_func($this->_tokenize, $input, $finalize); + try { + return call_user_func($this->_tokenize, $input, $finalize); + } catch (\Exception $e) { + throw $e; + } } // Adds the data chunk to the buffer and parses as far as possible diff --git a/src/TriGParser.php b/src/TriGParser.php index 89abd87..9363d89 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -981,7 +981,7 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { // Parse asynchronously otherwise, executing the read callback when a token arrives $this->callback = $tripleCallback; try { - $tokens = $this->lexer->tokenize($input); + $tokens = $this->lexer->tokenize($input); foreach($tokens as $token) { if (isset($this->readCallback)) { $this->readCallback = call_user_func($this->readCallback, $token); diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 20d7522..e92e50f 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -1987,7 +1987,7 @@ function shouldNotParse($createParser, $input, $expectedError = null) { $parser->_resetBlankNodeIds(); //hackish way so we only act upon first error $errorReceived = false; - $parser->parse($input, function ($error, $triple = null) use (&$expectedError, &$errorReceived){ + $parser->parse($input, function ($error, $triple = null) use ($expectedError, &$errorReceived){ //expect($error).not.to.exist; if (isset($error) && !$errorReceived) { $this->assertEquals($expectedError, $error->getMessage()); From 6c76d8a11fb0f1df254dbce31f3becdcc5921bd9 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 10 Apr 2017 11:20:07 +0200 Subject: [PATCH 43/72] made sure n3Mode is interpreted correctly --- src/TriGParser.php | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index 9363d89..d09409b 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -46,9 +46,10 @@ public function __construct($options = []) { $isTurtle = $format === 'turtle'; $isTriG = $format === 'trig'; - $isNTriples = strpos($format,"triple")?true:false; - $isNQuads = strpos($format, "quad")?true:false; - $isN3 = strpos($format, "n3")?true:false; + $isNTriples = strpos($format,"triple")!==false?true:false; + $isNQuads = strpos($format, "quad")!==false?true:false; + $isN3 = strpos($format, "n3")!==false?true:false; + $this->n3Mode = $isN3; $isLineMode = $isNTriples || $isNQuads; if (!($this->supportsNamedGraphs = !($isTurtle || $isN3))) $this->readPredicateOrNamedGraph = $this->readPredicate; @@ -86,11 +87,11 @@ private function setBase ($baseIRI = null) { else { // Remove fragment if present $fragmentPos = strpos($baseIRI,'#'); - if ($fragmentPos) + if ($fragmentPos !== false) $baseIRI = substr($baseIRI,0, $fragmentPos); // Set base IRI and its components $this->base = $baseIRI; - $this->basePath = !strpos($baseIRI,'/') ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); + $this->basePath = strpos($baseIRI,'/') === false ? $baseIRI : preg_replace('/[^\/?]*(?:\?.*)?$/', '',$baseIRI); preg_match($this->schemeAuthority, $baseIRI, $matches); $this->baseRoot = isset($matches[0])?$matches[0]:''; $this->baseScheme = isset($matches[1])?$matches[1]:''; @@ -100,7 +101,7 @@ private function setBase ($baseIRI = null) { // ### `_saveContext` stores the current parsing context // when entering a new scope (list, blank node, formula) private function saveContext($type, $graph, $subject, $predicate, $object) { - $n3Mode = isset($this->n3Mode)?$this->n3Mode:null; + $n3Mode = $this->n3Mode?$this->n3Mode:null; array_push($this->contextStack,[ "subject"=> $subject, "predicate"=> $predicate,"object"=> $object, "graph" => $graph, "type"=> $type, @@ -109,14 +110,14 @@ private function saveContext($type, $graph, $subject, $predicate, $object) { "quantified"=> $n3Mode ? $this->quantified : null ]); // The settings below only apply to N3 streams - if (isset($n3Mode)) { + if ($n3Mode) { // Every new scope resets the predicate direction $this->inversePredicate = false; // In N3, blank nodes are scoped to a formula // (using a dot as separator, as a blank node label cannot start with it) $this->prefixes["_"] = $this->graph . '.'; // Quantifiers are scoped to a formula TODO: is this correct? - $this->quantified = clone $this->quantified; + $this->quantified = $this->quantified; } } @@ -124,13 +125,13 @@ private function saveContext($type, $graph, $subject, $predicate, $object) { // when leaving a scope (list, blank node, formula) private function restoreContext() { $context = array_pop($this->contextStack); - $n3Mode = isset($this->n3Mode)?$this->n3Mode:null; + $n3Mode = $this->n3Mode; $this->subject = $context["subject"]; $this->predicate = $context["predicate"]; $this->object = $context["object"]; $this->graph = $context["graph"]; // The settings below only apply to N3 streams - if (isset($n3Mode)) { + if ($n3Mode) { $this->inversePredicate = $context["inverse"]; $this->prefixes["_"] = $context["blankPrefix"]; $this->quantified = $context["quantified"]; @@ -205,7 +206,7 @@ private function initReaders () return call_user_func($this->error,'Expected entity but got ' . $token["type"], $token); } // In N3 mode, replace the entity if it is quantified - if (!isset($quantifier) && isset($this->n3Mode) && isset($this->quantified[$value])) + if (!isset($quantifier) && $this->n3Mode && isset($this->quantified[$value])) $value = $this->quantified[$value]; return $value; }; @@ -213,7 +214,6 @@ private function initReaders () // ### `_readSubject` reads a triple's subject $this->readSubject = function ($token) { $this->predicate = null; - switch ($token["type"]) { case '[': // Start a new triple with a new blank node as subject @@ -226,10 +226,9 @@ private function initReaders () return $this->readListItem; case '{': // Start a new formula - if (!isset($this->n3Mode)) + if (!$this->n3Mode) return call_user_func($this->error,'Unexpected graph', $token); - $this->saveContext('formula', $this->graph, - $this->graph = '_:b' . $this->blankNodeCount++, null, null); + $this->saveContext('formula', $this->graph, $this->graph = '_:b' . $this->blankNodeCount++, null, null); return $this->readSubject; case '}': // No subject; the graph in which we are reading is closed instead @@ -250,7 +249,7 @@ private function initReaders () if ($this->subject == null) return; // In N3 mode, the subject might be a path - if (isset($this->n3Mode)) + if ($this->n3Mode) return call_user_func($this->getPathReader,$this->readPredicateOrNamedGraph); } @@ -280,7 +279,7 @@ private function initReaders () // Extra semicolons can be safely ignored return $this->readPredicate; case 'blank': - if (!isset($this->n3Mode)) + if (!$this->n3Mode) return call_user_func($this->error,'Disallowed blank node as predicate', $token); default: $this->predicate = call_user_func($this->readEntity,$token); @@ -309,7 +308,7 @@ private function initReaders () return $this->readListItem; case '{': // Start a new formula - if (!isset($this->n3Mode)) + if (!$this->n3Mode) return call_user_func($this->error,'Unexpected graph', $token); $this->saveContext('formula', $this->graph, $this->subject, $this->predicate, $this->graph = '_:b' . $this->blankNodeCount++); @@ -320,7 +319,7 @@ private function initReaders () if ($this->object == null) return; // In N3 mode, the object might be a path - if (isset($this->n3Mode)) + if ($this->n3Mode) return call_user_func($this->getPathReader,$this->getContextEndReader()); } return call_user_func($this->getContextEndReader); @@ -465,7 +464,7 @@ private function initReaders () // Add the item's value if ($item !== null) { // In N3 mode, the item might be a path - if (isset($this->n3Mode) && ($token["type"] === 'IRI' || $token["type"] === 'prefixed')) { + if ($this->n3Mode && ($token["type"] === 'IRI' || $token["type"] === 'prefixed')) { // Create a new context to add the item's path $this->saveContext('item', $this->graph, $list, self::RDF_FIRST, $item); $this->subject = $item; @@ -549,7 +548,7 @@ private function initReaders () case '}': if ($this->graph === null) return call_user_func($this->error,'Unexpected graph closing', $token); - if (isset($this->n3Mode)) + if ($this->n3Mode) return $this->readFormulaTail($token); $this->graph = null; // A dot just ends the statement, without sharing anything with the next @@ -679,11 +678,11 @@ private function initReaders () // Reads a list of quantified symbols from a @forSome or @forAll statement $this->readQuantifierList = function ($token) { - $entity; + $entity; switch ($token["type"]) { case 'IRI': case 'prefixed': - $entity = call_user_func($this->readEntity,$token, true); + $entity = call_user_func($this->readEntity, $token, true); if (!$entity) break; default: From 4e2c8652133e50788ba40ec4ea52ba303af48378 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 10 Apr 2017 14:08:43 +0200 Subject: [PATCH 44/72] Fixed N3 parsing --- src/TriGParser.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/TriGParser.php b/src/TriGParser.php index d09409b..d92a270 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -320,7 +320,7 @@ private function initReaders () return; // In N3 mode, the object might be a path if ($this->n3Mode) - return call_user_func($this->getPathReader,$this->getContextEndReader()); + return call_user_func($this->getPathReader,call_user_func($this->getContextEndReader)); } return call_user_func($this->getContextEndReader); }; @@ -527,14 +527,14 @@ private function initReaders () return call_user_func($this->readPunctuation,$token); // Store the last triple of the formula - if ($this->subject !== null) + if (isset($this->subject)) call_user_func($this->triple,$this->subject, $this->predicate, $this->object, $this->graph); // Restore the parent context containing this formula $this->restoreContext(); // If the formula was the subject, continue reading the predicate. // If the formula was the object, read punctuation. - return $this->object === null ? $this->readPredicate : call_user_func($this->getContextEndReader); + return !isset($this->object) ? $this->readPredicate : call_user_func($this->getContextEndReader); }; // ### `_readPunctuation` reads punctuation between triples or triple parts @@ -549,7 +549,7 @@ private function initReaders () if ($this->graph === null) return call_user_func($this->error,'Unexpected graph closing', $token); if ($this->n3Mode) - return $this->readFormulaTail($token); + return call_user_func($this->readFormulaTail, $token); $this->graph = null; // A dot just ends the statement, without sharing anything with the next case '.': @@ -683,7 +683,7 @@ private function initReaders () case 'IRI': case 'prefixed': $entity = call_user_func($this->readEntity, $token, true); - if (!$entity) + if (isset($entity)) break; default: return call_user_func($this->error,'Unexpected ' . $token["type"], $token); @@ -742,7 +742,10 @@ private function initReaders () // Not a path; resume reading where we left off default: $stack = $this->contextStack; - $parent = is_array($stack) && $stack[sizeof($stack) - 1]; + $parent = null; + if (is_array($stack) && sizeof($stack) - 1 > 0 && isset($stack[sizeof($stack) - 1])) { + $parent = $stack[sizeof($stack) - 1]; + } // If we were reading a list item, we still need to output it if ($parent && $parent["type"] === 'item') { // The list item is the remaining subejct after reading the path From d571f79ea477d8f5b054dcf20048a3c2196b0f7c Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 10 Apr 2017 14:16:19 +0200 Subject: [PATCH 45/72] Added example, added alias N3Parser and passed all unittests --- examples/validator.php | 22 ++++++++++++++++++++++ src/N3Parser.php | 16 ++++++++++++++++ test/TriGParserTest.php | 4 ++-- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 examples/validator.php create mode 100644 src/N3Parser.php diff --git a/examples/validator.php b/examples/validator.php new file mode 100644 index 0000000..a1dcdc8 --- /dev/null +++ b/examples/validator.php @@ -0,0 +1,22 @@ + "N3"]); +$errored = false; +$tripleCount = 0; +//TODO: change this to STDIO +$wrong = " . . \"aa\"^^foo:bar . ."; +$right = " . . "; +$parser->parse($wrong, function ($e, $triple) use (&$errored, &$tripleCount) { + if (!$e && $triple) { + $tripleCount ++; + } else if ($e) { + $errored = true; + echo $e->getMessage() . "\n"; + } else if (!$triple && !$errored) { + echo "Parsed " . $tripleCount . " triples successfully.\n"; + } +}); diff --git a/src/N3Parser.php b/src/N3Parser.php new file mode 100644 index 0000000..c9cc138 --- /dev/null +++ b/src/N3Parser.php @@ -0,0 +1,16 @@ +shouldNotParse($parser, '@forSome ?a.', - 'Unexpected $on line 1.'); + 'Unexpected var on line 1.'); // ### should correctly scope @forSome statements $this->shouldParse($parser, '@forSome . { @forSome . . }. .', @@ -1290,7 +1290,7 @@ public function testN3Format () // ### should not parse a @forAll statement with a variable $this->shouldNotParse($parser, '@forAll ?a.', - 'Unexpected $on line 1.'); + 'Unexpected var on line 1.'); // ### should correctly scope @forAll statements $this->shouldParse($parser, '@forAll . { @forAll . . }. .', From cb585071e476d622562f0acdb65a256094751ab5 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 00:10:48 +0200 Subject: [PATCH 46/72] Added 2 bin scripts and added better streaming functionality --- bin/convert.php | 32 +++++++++++ bin/validator.php | 28 ++++++++++ examples/validator.php | 22 -------- src/TriGParser.php | 45 +++++++++------ src/TriGWriter.php | 121 ++++++++++++++++++++++------------------ test/TriGParserTest.php | 1 - test/TriGWriterTest.php | 28 ++++++---- 7 files changed, 169 insertions(+), 108 deletions(-) create mode 100644 bin/convert.php create mode 100644 bin/validator.php delete mode 100644 examples/validator.php diff --git a/bin/convert.php b/bin/convert.php new file mode 100644 index 0000000..73c1af3 --- /dev/null +++ b/bin/convert.php @@ -0,0 +1,32 @@ + $informat]); + +$outformat = "n-triples"; +if (isset($argv[2])) + $outformat = $argv[2]; + +$writer = new TriGWriter(["format" => $outformat]); + +$callback = function ($error, $triple) use (&$writer) +{ + if (!isset($error) && !isset($triple)) { + echo $writer->end(); + } else if (!$error) { + $writer->addTriple($triple); + echo $writer->read(); + } else { + fwrite(STDERR, $error->getMessage() . "\n"); + } +}; + +while ($line = fgets(STDIN)) { + $parser->parseChunk($line, $callback); +} +$parser->end($callback); diff --git a/bin/validator.php b/bin/validator.php new file mode 100644 index 0000000..83d5195 --- /dev/null +++ b/bin/validator.php @@ -0,0 +1,28 @@ + $format]); +$errored = false; +$tripleCount = 0; +$finished = false; +while ($finished) { + try { + $line = fgets(STDIN); + if (isset($line)) + $tripleCount += sizeof($parser->parseChunk($line)); + else + $tripleCount += sizeof($parser->end($line)); + } catch (\Exception $e) { + echo $e->getMessage() . "\n"; + $errored = true; + } +} +if (!$errored) { + echo "Parsed " . $tripleCount . " triples successfully.\n"; +} \ No newline at end of file diff --git a/examples/validator.php b/examples/validator.php deleted file mode 100644 index a1dcdc8..0000000 --- a/examples/validator.php +++ /dev/null @@ -1,22 +0,0 @@ - "N3"]); -$errored = false; -$tripleCount = 0; -//TODO: change this to STDIO -$wrong = " . . \"aa\"^^foo:bar . ."; -$right = " . . "; -$parser->parse($wrong, function ($e, $triple) use (&$errored, &$tripleCount) { - if (!$e && $triple) { - $tripleCount ++; - } else if ($e) { - $errored = true; - echo $e->getMessage() . "\n"; - } else if (!$triple && !$errored) { - echo "Parsed " . $tripleCount . " triples successfully.\n"; - } -}); diff --git a/src/TriGParser.php b/src/TriGParser.php index d92a270..6d44ba6 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -1,5 +1,4 @@ lexer = isset($options["lexer"])? $options["lexer"] : new N3Lexer([ "lineMode"=> $isLineMode, "n3"=> $isN3 ]); // Disable explicit quantifiers by default $this->explicitQuantifiers = isset($options["explicitQuantifiers"])?$options["explicitQuantifiers"]:null; + + // The read callback is the next function to be executed when a token arrives. + // We start reading in the top context. + $this->readCallback = $this->readInTopContext; + $this->sparqlStyle = false; + $this->prefixes = []; + $this->prefixes["_"] = isset($this->blankNodePrefix)?$this->blankNodePrefix:'_:b' . $this->blankNodeCount . '_'; + $this->inversePredicate = false; + $this->quantified = []; } @@ -151,7 +159,7 @@ private function initReaders () if ($this->graph !== null) return call_user_func($this->error,'Unclosed graph', $token); unset($this->prefixes["_"]); - return call_user_func($this->callback,null, null, $this->prefixes); + return call_user_func($this->callback, null, null, $this->prefixes); // It could be a prefix declaration case 'PREFIX': $this->sparqlStyle = true; @@ -948,22 +956,12 @@ private function initReaders () // ## Public methods // ### `parse` parses the N3 input and emits each parsed triple through the callback - public function parse($input, $tripleCallback = null, $prefixCallback = null) { - $self = $this; - // The read callback is the next function to be executed when a token arrives. - // We start reading in the top context. - $this->readCallback = $this->readInTopContext; - $this->sparqlStyle = false; - $this->prefixes = []; - $this->prefixes["_"] = isset($this->blankNodePrefix)?$this->blankNodePrefix:'_:b' . $this->blankNodeCount . '_'; + public function parse($input, $tripleCallback = null, $prefixCallback = null, $finalize = true) { $this->prefixCallback = isset($prefixCallback)?$prefixCallback:function () {}; - $this->inversePredicate = false; - $this->quantified = []; - // Parse synchronously if no triple callback is given if (!isset($tripleCallback)) { $triples = []; - $error= null; + $error = null; $this->callback = function ($e, $t = null) use (&$triples, &$error) { if (!$e && $t) { array_push($triples,$t); @@ -971,11 +969,12 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { //DONE } else { $error = $e; - } + } }; - $tokens = $this->lexer->tokenize($input); + $tokens = $this->lexer->tokenize($input, $finalize); foreach($tokens as $token) { - $this->readCallback = call_user_func($this->readCallback,$token); + if (isset($this->readCallback)) + $this->readCallback = call_user_func($this->readCallback, $token); } if ($error) throw $error; return $triples; @@ -983,7 +982,7 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { // Parse asynchronously otherwise, executing the read callback when a token arrives $this->callback = $tripleCallback; try { - $tokens = $this->lexer->tokenize($input); + $tokens = $this->lexer->tokenize($input, $finalize); foreach($tokens as $token) { if (isset($this->readCallback)) { $this->readCallback = call_user_func($this->readCallback, $token); @@ -997,5 +996,15 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null) { $this->callback = function () {}; } } + + public function parseChunk($input, $tripleCallback = null, $prefixCallback = null) { + return $this->parse($input, $tripleCallback, $prefixCallback, false); + } + + public function end($tripleCallback = null, $prefixCallback = null) + { + return $this->parse("",$tripleCallback, $prefixCallback, true); + } + } \ No newline at end of file diff --git a/src/TriGWriter.php b/src/TriGWriter.php index ed89dca..6302c7b 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -29,6 +29,7 @@ class TriGWriter public function __construct($options = []) { + $this->initWriter (); /* Initialize writer, depending on the format*/ $this->subject = null; if (!isset($options["format"]) || !(preg_match("/triple|quad/i", $options["format"]))) { @@ -37,8 +38,10 @@ public function __construct($options = []) if (isset($options["prefixes"])) { $this->addPrefixes($options["prefixes"]); } + } else { + $this->writeTriple = $this->writeTripleLine; } - + // TODO: I think we could do without this... $this->characterReplacer = function ($character) { // Replace a single character by its escaped version @@ -64,6 +67,65 @@ public function __construct($options = []) }; } + private function initWriter () + { + // ### `_writeTriple` writes the triple to the output stream + $this->writeTriple = function($subject, $predicate, $object, $graph, $done = null) { + try { + if (isset($graph) && $graph === "") { + $graph = null; + } + // Write the graph's label if it has changed + if ($this->graph !== $graph) { + // Close the previous graph and start the new one + $this->write(($this->subject === null ? '' : ($this->graph ? "\n}\n" : ".\n")) . (isset($graph) ? $this->encodeIriOrBlankNode($graph) . " {\n" : '')); + $this->subject = null; + // Don't treat identical blank nodes as repeating graphs + $this->graph = $graph[0] !== '[' ? $graph : ']'; + } + // Don't repeat the subject if it's the same + if ($this->subject === $subject) { + // Don't repeat the predicate if it's the same + if ($this->predicate === $predicate) + $this->write(', ' . $this->encodeObject($object), $done); + // Same subject, different predicate + else { + $this->predicate = $predicate; + $this->write(";\n " . $this->encodePredicate($predicate) . ' ' . $this->encodeObject($object), $done); + } + } + // Different subject; write the whole triple + else { + $this->write(($this->subject === null ? '' : ".\n") . $this->encodeSubject($this->subject = $subject) . ' ' . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); + } + } catch (\Exception $error) { + if (isset($done)) { + $done($error); + } + } + }; + + + // ### `_writeTripleLine` writes the triple or quad to the output stream as a single line + $this->writeTripleLine = function ($subject, $predicate, $object, $graph, $done = null) { + if (isset($graph) && $graph === "") { + $graph = null; + } + // Don't use prefixes + unset($this->prefixMatch); + // Write the triple + try { + $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . (isset($graph) ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); + } catch (\Exception $error) { + if (isset($done)) { + $done($error); + } + } + }; + + } + + // ### `_write` writes the argument to the output stream private function write ($string, $callback = null) { //this._outputStream.write(string, 'utf8', callback); @@ -90,57 +152,6 @@ public function read () return $string; } } - - // ### `_writeTriple` writes the triple to the output stream - private function writeTriple ($subject, $predicate, $object, $graph, $done = null) { - try { - if (isset($graph) && $graph === ""){ - $graph = null; - } - // Write the graph's label if it has changed - if ($this->graph !== $graph) { - // Close the previous graph and start the new one - $this->write(($this->subject === null ? '' : ($this->graph ? "\n}\n" : ".\n")) . (isset($graph) ? $this->encodeIriOrBlankNode($graph) . " {\n" : '')); - $this->subject = null; - // Don't treat identical blank nodes as repeating graphs - $this->graph = $graph[0] !== '[' ? $graph : ']'; - } - // Don't repeat the subject if it's the same - if ($this->subject === $subject) { - // Don't repeat the predicate if it's the same - if ($this->predicate === $predicate) - $this->write(', ' . $this->encodeObject($object), $done); - // Same subject, different predicate - else { - $this->predicate = $predicate; - $this->write(";\n " . $this->encodePredicate($predicate) . ' ' . $this->encodeObject($object), $done); - } - } - // Different subject; write the whole triple - else { - $this->write(($this->subject === null ? '' : ".\n") . $this->encodeSubject($this->subject = $subject) . ' ' . $this->encodePredicate($this->predicate = $predicate) . ' ' . $this->encodeObject($object), $done); - } - } catch (\Exception $error) { - if (isset($done)) { - $done($error); - } - } - } - - // ### `_writeTripleLine` writes the triple or quad to the output stream as a single line - private function writeTripleLine ($subject, $predicate, $object, $graph, $done = null) { - // Don't use prefixes - unset($this->prefixMatch); - // Write the triple - try { - $this->write($this->encodeIriOrBlankNode($subject) . ' ' .$this->encodeIriOrBlankNode($predicate) . ' ' . $this->encodeObject($object) . (isset($graph) ? ' ' . $this->encodeIriOrBlankNode($graph) . ".\n" : ".\n"), $done); - } catch (\Exception $error) { - if (isset($done)) { - $done($error); - } - } - } - // ### `_encodeIriOrBlankNode` represents an IRI or blank node private function encodeIriOrBlankNode ($entity) { @@ -232,14 +243,14 @@ public function addTriple ($subject, $predicate = null, $object = null, $graph = // The triple was given as a triple object, so shift parameters if (is_array($subject)) { $g = isset($subject["graph"])?$subject["graph"]:null; - $this->writeTriple($subject["subject"], $subject["predicate"], $subject["object"], $g, $predicate); + call_user_func($this->writeTriple, $subject["subject"], $subject["predicate"], $subject["object"], $g, $predicate); } // The optional `graph` parameter was not provided else if (!is_string($graph)) - $this->writeTriple($subject, $predicate, $object, '', $graph); + call_user_func($this->writeTriple, $subject, $predicate, $object, '', $graph); // The `graph` parameter was provided else - $this->writeTriple($subject, $predicate, $object, $graph, $done); + call_user_func($this->writeTriple, $subject, $predicate, $object, $graph, $done); } // ### `addTriples` adds the triples to the output stream diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index e724164..9cd5852 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -494,7 +494,6 @@ public function testBlankNodes () $this->shouldParse("@base .\n" . "<#> <#c>.\n", ['http://ex.org/foo#', 'http://ex.org/b#', 'http://ex.org/foo#c']); -//TODO : solve plus to dot and newlines // ### should not resolve prefixed names $this->shouldParse('PREFIX ex: ' . "\n" . 'ex:a ex:b ex:c .', diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 4295cf4..2855394 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -103,19 +103,17 @@ public function testBlankNodes() public function testWrongLiterals() { -/* //should not serialize a literal in the subject', - shouldNotSerialize(['"a"', 'b', '"c"'], + $this->shouldNotSerialize(['"a"', 'b', '"c"'], 'A literal as subject is not allowed: "a"'); //should not serialize a literal in the predicate', - shouldNotSerialize(['a', '"b"', '"c"'], + $this->shouldNotSerialize(['a', '"b"', '"c"'], 'A literal as predicate is not allowed: "b"'); //should not serialize an invalid object literal', - shouldNotSerialize(['a', 'b', '"c'], + $this->shouldNotSerialize(['a', 'b', '"c'], 'Invalid literal: "c'); -*/ } public function testPrefixes () @@ -458,6 +456,18 @@ public function testTriplesBulk () }); } + public function testNTriples () + { + //should write simple triples in N-Triples mode', function (done) { + $writer = new TriGWriter(['format' => 'N-Triples' ]); + $writer->addTriple('a', 'b', 'c'); + $writer->addTriple('a', 'b', 'd'); + $writer->end(function ($error, $output) { + $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); + }); + } + + /* //should not allow writing after end', function (done) { $writer = new TriGWriter(); @@ -468,13 +478,7 @@ public function testTriplesBulk () error.should.have.property('message', 'Cannot write because the writer has been closed.'); }); - //should write simple triples in N-Triples mode', function (done) { -$writer = new TriGWriter({'format' => 'N-Triples' }); -$writer->addTriple('a', 'b', 'c'); -$writer->addTriple('a', 'b', 'd'); -$writer->end(function ($error, $output) { - $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); -}); + //should not write an invalid literal in N-Triples mode', function (done) { $writer = new TriGWriter({'format' => 'N-Triples' }); From c82bb2e4941e8ffc68a8ede25b878c6ab6faa609 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 00:17:45 +0200 Subject: [PATCH 47/72] fixed warning when using without callback --- bin/validator.php | 12 +++++++----- src/TriGParser.php | 4 +++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bin/validator.php b/bin/validator.php index 83d5195..be50cc1 100644 --- a/bin/validator.php +++ b/bin/validator.php @@ -9,15 +9,17 @@ $parser = new TriGParser(["format" => $format]); $errored = false; -$tripleCount = 0; $finished = false; -while ($finished) { +$tripleCount = 0; +while (!$finished) { try { $line = fgets(STDIN); - if (isset($line)) + if ($line) $tripleCount += sizeof($parser->parseChunk($line)); - else - $tripleCount += sizeof($parser->end($line)); + else { + $tripleCount += sizeof($parser->end($line)); + $finished = true; + } } catch (\Exception $e) { echo $e->getMessage() . "\n"; $errored = true; diff --git a/src/TriGParser.php b/src/TriGParser.php index 6d44ba6..8b1549f 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -159,7 +159,9 @@ private function initReaders () if ($this->graph !== null) return call_user_func($this->error,'Unclosed graph', $token); unset($this->prefixes["_"]); - return call_user_func($this->callback, null, null, $this->prefixes); + if ($this->callback) { + return call_user_func($this->callback, null, null, $this->prefixes); + } // It could be a prefix declaration case 'PREFIX': $this->sparqlStyle = true; From 681f02f3cff3b0a878a71b01fceb35fc73f6956b Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 00:26:37 +0200 Subject: [PATCH 48/72] Extended README.md --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5d14b9c..5b95e6e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ $writer = new TriGWriter([ "geo" =>"http://www.w3.org/2003/01/geo/wgs84_pos#", "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdfs"=> "http://www.w3.org/2000/01/rdf-schema#" - ] + ], + "format" => "n-quads" //Other possible values: n-quads, trig or turtle ]); $writer->addPrefix("ex","http://example.org/"); @@ -108,24 +109,27 @@ $writer->addPrefixes($prefixes); $writer->blank($predicate, $object); //Creates rdf:list with $elements $list = $writer->list($elements); + //Returns the current output it is already able to create and clear the internal memory use (useful for streaming) $out .= $writer->read(); + //Call this at the end. The return value will be the full triple output, or the rest of the output such as closing dots and brackets $out .= $writer->end(); ``` ### TriGParser class -Next to TriG, the TriGParser class also parses Turtle, N-Triples, N-Quads and the W3C “Team Submission” N3 +Next to [TriG](https://www.w3.org/TR/trig/), the TriGParser class also parses [Turtle](https://www.w3.org/TR/turtle/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/) and the [W3C Team Submission N3](https://www.w3.org/TeamSubmission/n3/) Basic example: + ```php use pietercolpaert\hardf\TriGParser; use pietercolpaert\hardf\TriGWriter; echo "--- First, simple implementation ---\n"; -$parser = new TriGParser(); -$writer = new TriGWriter(["format"=>"trig"]); +$parser = new TriGParser(["format" => "n-quads"]); //also parser n-triples, n3, turtle and trig. Format is optional +$writer = new TriGWriter(); $triples = $parser->parse(" ."); $writer->addTriples($triples); echo $writer->end(); @@ -143,3 +147,8 @@ $parser->parse(" . }); ``` +Parse chunks until end also works this way: +```php +$parser->parseChunk(chunk, callback); +$parser->end(callback); +``` From 3c3fa1272323ca7a0fee31893b5cb0274b2ee105 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 00:49:04 +0200 Subject: [PATCH 49/72] Extended README --- README.md | 72 +++++++++++++++++++++++++---------------------- bin/convert.php | 1 + bin/validator.php | 9 +++--- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 5b95e6e..ec2d1a3 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ # hardf [![Build Status](https://travis-ci.org/pietercolpaert/hardf.svg?branch=master)](https://travis-ci.org/pietercolpaert/hardf) -Current Status: early port of [N3.js](https://github.com/RubenVerborgh/N3.js) to PHP +**hardf** is a PHP library that lets you handle RDF easily. It offers: + - [**Parsing**](#parsing) triples/quads from [Turtle](http://www.w3.org/TR/turtle/), [TriG](http://www.w3.org/TR/trig/), [N-Triples](http://www.w3.org/TR/n-triples/), [N-Quads](http://www.w3.org/TR/n-quads/), and [Notation3 (N3)](https://www.w3.org/TeamSubmission/n3/) + - [**Writing**](#writing) triples/quads to [Turtle](http://www.w3.org/TR/turtle/), [TriG](http://www.w3.org/TR/trig/), [N-Triples](http://www.w3.org/TR/n-triples/), and [N-Quads](http://www.w3.org/TR/n-quads/) -Basic PHP library for RDF1.1. Currently provides simple tools (an Util library) for an array of triples/quads. +Both the parser as the serializer have _streaming_ support. -For now, [EasyRDF](https://github.com/njh/easyrdf) is the best PHP library for RDF (naming of this library is a contraction of "Hard" and "RDF", in which we try to make the point that you should at this point only use hardf when you know what you’re doing). -The EasyRDF library is a high-level library which abstracts all the difficult parts of dealing with RDF. -Hardf on the other hand, aims at a high performance for triple representations. -We will only support formats such as turtle or trig and n-triples or n-quads. -If you want other other formats, you will have to write some logic to load the triples into memory according to our triple representation (e.g., for JSON-LD, check out [ml/json-ld](https://github.com/lanthaler/JsonLD)). +_This library is a port of [N3.js](https://github.com/RubenVerborgh/N3.js) to PHP_ ## Triple Representation @@ -43,31 +41,7 @@ Install this library using [composer](http://getcomposer.org): composer install pietercolpaert/hardf ``` -### Util class -```php -use pietercolpaert\hardf\Util; -``` - -A static class with a couple of helpful functions for handling our specific triple representation. It will help you to create and evaluate literals, IRIs, and expand prefixes. - -```php -$bool = isIRI($term); -$bool = isLiteral($term); -$bool = isBlank($term); -$bool = isDefaultGraph($term); -$bool = inDefaultGraph($triple); -$value = getLiteralValue($literal); -$literalType = getLiteralType($literal); -$lang = getLiteralLanguage($literal); -$bool = isPrefixedName($term); -$expanded = expandPrefixedName($prefixedName, $prefixes); -$iri = createIRI($iri); -$literalObject = createLiteral($value, $modifier = null); -``` - -See the documentation at https://github.com/RubenVerborgh/N3.js#utility for more information about what the functions exactly do. - -### TriGWriter class +### Writing ```php use pietercolpaert\hardf\TriGWriter; ``` @@ -117,7 +91,7 @@ $out .= $writer->read(); $out .= $writer->end(); ``` -### TriGParser class +### Parsing Next to [TriG](https://www.w3.org/TR/trig/), the TriGParser class also parses [Turtle](https://www.w3.org/TR/turtle/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/) and the [W3C Team Submission N3](https://www.w3.org/TeamSubmission/n3/) @@ -152,3 +126,35 @@ Parse chunks until end also works this way: $parser->parseChunk(chunk, callback); $parser->end(callback); ``` + +### Utility +```php +use pietercolpaert\hardf\Util; +``` + +A static class with a couple of helpful functions for handling our specific triple representation. It will help you to create and evaluate literals, IRIs, and expand prefixes. + +```php +$bool = isIRI($term); +$bool = isLiteral($term); +$bool = isBlank($term); +$bool = isDefaultGraph($term); +$bool = inDefaultGraph($triple); +$value = getLiteralValue($literal); +$literalType = getLiteralType($literal); +$lang = getLiteralLanguage($literal); +$bool = isPrefixedName($term); +$expanded = expandPrefixedName($prefixedName, $prefixes); +$iri = createIRI($iri); +$literalObject = createLiteral($value, $modifier = null); +``` + +See the documentation at https://github.com/RubenVerborgh/N3.js#utility for more information. + +## Two executables + +We also offer 2 simple tools in `bin/` as an example implementation: one validator and one translator. Try for example: +```bash +curl -H "accept: application/trig" http://fragments.dbpedia.org/2015/en | php bin/validator.php trig +curl -H "accept: application/trig" http://fragments.dbpedia.org/2015/en | php bin/convert.php trig n-triples +``` diff --git a/bin/convert.php b/bin/convert.php index 73c1af3..ef8200d 100644 --- a/bin/convert.php +++ b/bin/convert.php @@ -1,3 +1,4 @@ +#!/usr/bin/php $format]); $errored = false; $finished = false; @@ -27,4 +27,5 @@ } if (!$errored) { echo "Parsed " . $tripleCount . " triples successfully.\n"; -} \ No newline at end of file +} + From 934e7be389c46476659c38d81749515ccc6ab0a9 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 00:50:53 +0200 Subject: [PATCH 50/72] Added contributing and license info --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ec2d1a3..7b194ec 100644 --- a/README.md +++ b/README.md @@ -158,3 +158,9 @@ We also offer 2 simple tools in `bin/` as an example implementation: one validat curl -H "accept: application/trig" http://fragments.dbpedia.org/2015/en | php bin/validator.php trig curl -H "accept: application/trig" http://fragments.dbpedia.org/2015/en | php bin/convert.php trig n-triples ``` +## License, status and contributions +The N3.js library is copyrighted by [Ruben Verborgh](http://ruben.verborgh.org/) and [Pieter Colpaert](https://pietercolpaert.be) +and released under the [MIT License](https://github.com/RubenVerborgh/N3.js/blob/master/LICENSE.md). + +Contributions are welcome, and bug reports or pull requests are always helpful. +If you plan to implement a larger feature, it's best to discuss this first by filing an issue. From c81113f8990a60b208200a1e45a55d7ca859a2ff Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 01:48:58 +0200 Subject: [PATCH 51/72] Added streaming performance test for parser --- perf/parser-streaming-perf.php | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 perf/parser-streaming-perf.php diff --git a/perf/parser-streaming-perf.php b/perf/parser-streaming-perf.php new file mode 100644 index 0000000..803713e --- /dev/null +++ b/perf/parser-streaming-perf.php @@ -0,0 +1,38 @@ + $base ]); +$callback = function ($error, $triple) use (&$count, $TEST, $filename) { + if ($triple) { + $count++; + } + else { + echo '- Parsing file ' . $filename . ': ' . (microtime(true) - $TEST) . "s\n"; + echo '* Triples parsed: ' . $count . "\n"; + echo '* Memory usage: ' . (memory_get_usage() / 1024 / 1024) . "MB\n"; + } +}; + +$handle = fopen($filename, "r"); +if ($handle) { + while (($line = fgets($handle)) !== false) { + $parser->parseChunk($line, $callback); + } + $parser->end($callback); + fclose($handle); +} else { + // error opening the file. + echo "File not found " . $filename; +} From 57ff3ebe6f6ff782294e2606a585528638e4e1de Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 01:50:45 +0200 Subject: [PATCH 52/72] Added different PHP versions --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 470c926..799f946 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: php php: - - "7.0" + - '5.6' + - '7.0' + - '7.1' + - hhvm + - nightly sudo: false env: before_script: From bc0344b1d121185bc8b30a65ed9949ad449e5a6a Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 01:53:51 +0200 Subject: [PATCH 53/72] Fixed small variable bug --- src/N3Lexer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index ab2c848..6841f24 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -165,7 +165,7 @@ private function tokenizeToEnd($callback, $inputFinished) { // Try to find a full IRI with escape sequences else if (preg_match($this->iri, $input, $match)) { - $unescaped = $this->unescape(match[1]); + $unescaped = $this->unescape($match[1]); if ($unescaped === null || preg_match($illegalIriChars,$unescaped)) return $reportSyntaxError($this); $type = 'IRI'; From 1e434bdbc38c38d8f53b107201f4843ee2e2bb5d Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 02:01:51 +0200 Subject: [PATCH 54/72] Removing zombie code --- src/N3Lexer.php | 2 +- src/TriGWriter.php | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 6841f24..bdf755e 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -396,7 +396,7 @@ private function unescape($item) { else { $replacement = escapeReplacements[$escapedChar]; if (!$replacement) - throw new Error(); + throw new \Exception(); return $replacement; } },$item); diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 6302c7b..028c80f 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -43,11 +43,11 @@ public function __construct($options = []) } // TODO: I think we could do without this... - $this->characterReplacer = function ($character) { + /*$this->characterReplacer = function ($character) { // Replace a single character by its escaped version $character = $character[0]; - if (isset($character) && isset(self::ESCAPEREPLACEMENTS[$character])) { - return self::ESCAPEREPLACEMENTS[$character]; + if (strlen($character) > 0 && isset(self::ESCAPEREPLACEMENTS[$character[0]])) { + return self::ESCAPEREPLACEMENTS[$character[0]]; } else { // Replace a single character with its 4-bit unicode escape sequence $result = ""; @@ -64,7 +64,7 @@ public function __construct($options = []) } return $result; } - }; + };*/ } private function initWriter () @@ -161,8 +161,9 @@ private function encodeIriOrBlankNode ($entity) { return $entity; } // Escape special characters - if (preg_match(self::ESCAPE, $entity)) - $entity = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer,$entity); + //if (preg_match(self::ESCAPE, $entity)) + // $entity = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer,$entity); + // Try to represent the IRI as prefixed name preg_match($this->prefixRegex, $entity, $prefixMatch); if (!isset($prefixMatch[1]) && !isset($prefixMatch[2])) { From 25a84fad8cfb778879de18ba2fa8b50d5dfad1a7 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 02:09:42 +0200 Subject: [PATCH 55/72] Fixed unescape code in lexer --- src/N3Lexer.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index bdf755e..cfbd2f6 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -385,16 +385,17 @@ private function unescape($item) { return preg_replace_callback($this->escapeSequence, function ($sequence, $unicode4, $unicode8, $escapedChar) { $charCode; if ($unicode4) { - $charCode = parseInt($unicode4, 16); - return fromCharCode($charCode); + $charCode = intval($unicode4, 16); + return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES'); } else if ($unicode8) { - $charCode = parseInt($unicode8, 16); - if ($charCode <= 0xFFFF) return fromCharCode($charCode); - return fromCharCode(0xD800 . (($charCode -= 0x10000) / 0x400), 0xDC00 . ($charCode & 0x3FF)); + $charCode = intval($unicode8, 16); + return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES'); + //if ($charCode <= 0xFFFF) return fromCharCode($charCode); + //return fromCharCode(0xD800 . (($charCode -= 0x10000) / 0x400), 0xDC00 . ($charCode & 0x3FF)); } else { - $replacement = escapeReplacements[$escapedChar]; + $replacement = $this->escapeReplacements[$escapedChar]; if (!$replacement) throw new \Exception(); return $replacement; From 23133d7ae59de3759a22cf0e096afe3c338d491e Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 02:13:19 +0200 Subject: [PATCH 56/72] Name of list changed in favour addList --- README.md | 2 +- src/TriGWriter.php | 2 +- test/TriGWriterTest.php | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7b194ec..6a38cf9 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ $writer->addPrefixes($prefixes); //Creates blank node($predicate and/or $object are optional) $writer->blank($predicate, $object); //Creates rdf:list with $elements -$list = $writer->list($elements); +$list = $writer->addList($elements); //Returns the current output it is already able to create and clear the internal memory use (useful for streaming) $out .= $writer->read(); diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 028c80f..abb6a3c 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -350,7 +350,7 @@ public function blank ($predicate = null, $object = null) { } // ### `list` creates a list node with the given content - public function list ($elements = null) { + public function addList ($elements = null) { $length = 0; if (isset($elements)) { $length = sizeof($elements); diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 2855394..087fe9a 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -386,47 +386,47 @@ public function testLists () { //should serialize triples with an empty list as object', function (done) { $writer = new TriGWriter(); - $writer->addTriple('a1', 'b', $writer->list()); - $writer->addTriple('a2', 'b', $writer->list([])); + $writer->addTriple('a1', 'b', $writer->addList()); + $writer->addTriple('a2', 'b', $writer->addList([])); $writer->end(function ($error, $output) { $this->assertEquals(' ().' . "\n" . ' ().' . "\n",$output); }); //should serialize triples with a one-element list as object', function (done) { $writer = new TriGWriter(); - $writer->addTriple('a1', 'b', $writer->list(['c'])); - $writer->addTriple('a2', 'b', $writer->list(['"c"'])); + $writer->addTriple('a1', 'b', $writer->addList(['c'])); + $writer->addTriple('a2', 'b', $writer->addList(['"c"'])); $writer->end(function ($error, $output) { $this->assertEquals(' ().' . "\n" . ' ("c").' . "\n",$output); }); //should serialize triples with a three-element list as object', function (done) { $writer = new TriGWriter(); - $writer->addTriple('a1', 'b', $writer->list(['c', 'd', 'e'])); - $writer->addTriple('a2', 'b', $writer->list(['"c"', '"d"', '"e"'])); + $writer->addTriple('a1', 'b', $writer->addList(['c', 'd', 'e'])); + $writer->addTriple('a2', 'b', $writer->addList(['"c"', '"d"', '"e"'])); $writer->end(function ($error, $output) { $this->assertEquals(' ( ).' . "\n" . ' ("c" "d" "e").' . "\n",$output); }); //should serialize triples with an empty list as subject', function (done) { $writer = new TriGWriter(); - $writer->addTriple($writer->list(), 'b1', 'c'); - $writer->addTriple($writer->list([]), 'b2', 'c'); + $writer->addTriple($writer->addList(), 'b1', 'c'); + $writer->addTriple($writer->addList([]), 'b2', 'c'); $writer->end(function ($error, $output) { $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); }); //should serialize triples with a one-element list as subject', function (done) { $writer = new TriGWriter(); - $writer->addTriple($writer->list(['a']), 'b1', 'c'); - $writer->addTriple($writer->list(['a']), 'b2', 'c'); + $writer->addTriple($writer->addList(['a']), 'b1', 'c'); + $writer->addTriple($writer->addList(['a']), 'b2', 'c'); $writer->end(function ($error, $output) { $this->assertEquals('() ;' . "\n" . ' .' . "\n",$output); }); //should serialize triples with a three-element list as subject', function (done) { $writer = new TriGWriter(); - $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); + $writer->addTriple($writer->addList(['a', '"b"', '"c"']), 'd', 'e'); $output = $writer->end(); $this->assertEquals('( "b" "c") .' . "\n",$output); } @@ -436,7 +436,7 @@ public function testPartialRead () { //should only partially output the already given data and then continue writing until end $writer = new TriGWriter(); - $writer->addTriple($writer->list(['a', '"b"', '"c"']), 'd', 'e'); + $writer->addTriple($writer->addList(['a', '"b"', '"c"']), 'd', 'e'); $output = $writer->read(); $this->assertEquals('( "b" "c") ', $output); $writer->addTriple('a', 'b', 'c'); From 6a83b7802a81b67a8ec338b4fe7e245ac570ce6f Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 02:19:36 +0200 Subject: [PATCH 57/72] Const array changed for HHVM --- src/TriGWriter.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index abb6a3c..fd4a6c4 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -14,10 +14,8 @@ class TriGWriter // Characters in literals that require escaping CONST ESCAPE = "/[\"\\\t\n\r\f]/u"; #/u'; CONST ESCAPEALL = "/[\"\\\t\n\r\b\f]/u"; - CONST ESCAPEREPLACEMENTS = [ - '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", - "\n" => '\\n', "\r" => "\\r", "\b"=> "\\b", "\f"=> "\\f" - ]; + //HHVM does not allow this to be a constant + private $ESCAPEREPLACEMENTS; // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes private $prefixRegex = "/$0^/"; @@ -29,6 +27,10 @@ class TriGWriter public function __construct($options = []) { + $this->ESCAPEREPLACEMENTS = [ + '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", + "\n" => '\\n', "\r" => "\\r", "\b"=> "\\b", "\f"=> "\\f" + ]; $this->initWriter (); /* Initialize writer, depending on the format*/ $this->subject = null; From dacf294191ed870a191f4da84b990f80d99e02f3 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 02:23:21 +0200 Subject: [PATCH 58/72] Removed unicode tests --- test/TriGWriterTest.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 087fe9a..02c4f22 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -81,17 +81,17 @@ public function testLiterals() $this->shouldSerialize(['a', 'b', '"c' . "\f" . 'de"'], ' """c' . "\f" . 'de""".' . "\n"); - //should serialize a literal containing a line separator', - $this->shouldSerialize(['a', 'b', "\"c\u{2028}de\""], - ' "c' . "\u{2028}" . 'de".' . "\n"); + //should serialize a literal containing a line separator', - These tests willl not work for PHP5.6, hence commented. PHP7 only introduced the unicode escape sequence. + //$this->shouldSerialize(['a', 'b', "\"c\u{2028}de\""], + //' "c' . "\u{2028}" . 'de".' . "\n"); //should serialize a literal containing a paragraph separator', - $this->shouldSerialize(['a', 'b', "\"c\u{2029}de\""], - ' "c' . "\u{2029}" .'de".' . "\n"); + //$this->shouldSerialize(['a', 'b', "\"c\u{2029}de\""], + //' "c' . "\u{2029}" .'de".' . "\n"); //should serialize a literal containing special unicode characters', - $this->shouldSerialize(['a', 'b', "\"c\u{0000}\u{0001}\""], - ' "c'."\u{0000}\u{0001}" . '".' . "\n"); + //$this->shouldSerialize(['a', 'b', "\"c\u{0000}\u{0001}\""], + //' "c'."\u{0000}\u{0001}" . '".' . "\n"); } public function testBlankNodes() @@ -211,9 +211,9 @@ public function testQuads () '<\\U0001d400> {' . "\n" . '<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>' . "\n" . '}' . "\n"); */ //should not use escape sequences in blank nodes', - $this->shouldSerialize(["_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}"], - "_:\u{d835}\u{dc00} {" . "\n" . "_:\u{d835}\u{dc00} _:\u{d835}\u{dc00} _:\u{d835}\u{dc00}" . "\n" . '}' . "\n"); - } + //$this->shouldSerialize(["_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}", "_:\u{d835}\u{dc00}"], + //"_:\u{d835}\u{dc00} {" . "\n" . "_:\u{d835}\u{dc00} _:\u{d835}\u{dc00} _:\u{d835}\u{dc00}" . "\n" . '}' . "\n"); + } public function testCallbackOnEnd () { //sends output through end From 7cdb6994691e1f594bd6613898312faddda3cfe6 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 02:28:44 +0200 Subject: [PATCH 59/72] Removed HHVM for now --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 799f946..27490ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ php: - '5.6' - '7.0' - '7.1' - - hhvm - nightly sudo: false env: From d92fb5e612c75a8afed6c05af3e187d023eabd83 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Wed, 12 Apr 2017 23:45:43 +0200 Subject: [PATCH 60/72] Added PHP5.6+ to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a38cf9..59f579c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # hardf [![Build Status](https://travis-ci.org/pietercolpaert/hardf.svg?branch=master)](https://travis-ci.org/pietercolpaert/hardf) -**hardf** is a PHP library that lets you handle RDF easily. It offers: +**hardf** is a PHP5.6+ library that lets you handle RDF easily. It offers: - [**Parsing**](#parsing) triples/quads from [Turtle](http://www.w3.org/TR/turtle/), [TriG](http://www.w3.org/TR/trig/), [N-Triples](http://www.w3.org/TR/n-triples/), [N-Quads](http://www.w3.org/TR/n-quads/), and [Notation3 (N3)](https://www.w3.org/TeamSubmission/n3/) - [**Writing**](#writing) triples/quads to [Turtle](http://www.w3.org/TR/turtle/), [TriG](http://www.w3.org/TR/trig/), [N-Triples](http://www.w3.org/TR/n-triples/), and [N-Quads](http://www.w3.org/TR/n-quads/) From 15a30b4b7f8c9bb3954b9639f10b15afc626162e Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 13 Apr 2017 20:14:19 +0200 Subject: [PATCH 61/72] Added test for triple quotes --- bin/validator.php | 3 ++- src/N3Lexer.php | 14 ++++++++------ src/TriGParser.php | 10 ++++++++-- test/TriGParserTest.php | 11 ++++++++++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bin/validator.php b/bin/validator.php index 9fed8c3..ff20bb0 100644 --- a/bin/validator.php +++ b/bin/validator.php @@ -11,7 +11,8 @@ $errored = false; $finished = false; $tripleCount = 0; -while (!$finished) { +$line = true; +while (!$finished && $line) { try { $line = fgets(STDIN); if ($line) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index cfbd2f6..2d6c413 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -55,9 +55,11 @@ public function __construct($options = []) { // ## Regular expressions private $iri ='/^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/'; // IRI with escape sequences; needs sanity check after unescaping private $unescapedIri = '/^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/'; // IRI without escape sequences; no unescaping - private $unescapedString= '/^"[^"\\\]+"(?=[^"\\\])/'; // non-empty string without escape sequences - private $singleQuotedString= '/^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^\'[^\'\\]*(?:\\.[^\'\\]*)*\'(?=[^\'\\])/'; - private $tripleQuotedString = '/^""("[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*")""|^\'\'(\'[^\'\\]*(?:(?:\\.|\'(?!\'\'))[^\'\\]*)*\')\'\'/'; + // _unescapedString: /^"[^"\\]+"(?=[^"\\])/, // non-empty string without escape sequences + private $unescapedString= '/^"[^\\"]+"(?=[^\\"])/'; // non-empty string without escape sequences + // _singleQuotedString: /^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^'[^'\\]*(?:\\.[^'\\]*)*'(?=[^'\\])/, + private $singleQuotedString= '/^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^\'[^\\\']*(?:\\.[^\'\\]*)*\'(?=[^\\\'])/'; + private $tripleQuotedString = '/^""("[^\\"]*(?:(?:\\.|"(?!""))[^\\"]*)*")""|^\'\'(\'[^\\\']*(?:(?:\\.|\'(?!\'\'))[^\\\']*)*\')\'\'/'; private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i'; private $prefix = '/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:(?=[#\s<])/'; @@ -198,7 +200,7 @@ private function tokenizeToEnd($callback, $inputFinished) { $type = 'literal'; $value = $match[0]; } - // Try to find any other literal wrapped in a pair of single or double quotes + // Try to find any other literal wrapped in a pair of single or double quotes else if (preg_match($this->singleQuotedString, $input, $match)) { $unescaped = $this->unescape($match[0]); if ($unescaped === null) @@ -210,12 +212,12 @@ private function tokenizeToEnd($callback, $inputFinished) { else if (preg_match($this->tripleQuotedString, $input, $match)) { $unescaped = isset($match[1])?$match[1]:$match[2]; // Count the newlines and advance line counter - $this->line .= strlen(preg_split('/\r\n|\r|\n/',$unescaped)) - 1; + $this->line += sizeof(preg_split('/\r\n|\r|\n/',$unescaped)) - 1; $unescaped = $this->unescape($unescaped); if ($unescaped === null) return $reportSyntaxError($this); $type = 'literal'; - $value = preg_replace("/^'|'$/g", '"',$unescaped); + $value = preg_replace("/^'|'$/", '"',$unescaped); } break; diff --git a/src/TriGParser.php b/src/TriGParser.php index 8b1549f..b3b55c2 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -837,7 +837,10 @@ private function initReaders () // ### `_error` emits an error message through the callback $this->error = function ($message, $token) { - call_user_func($this->callback, new \Exception($message . ' on line ' . $token['line'] . '.'),null); + if ($this->callback) + call_user_func($this->callback, new \Exception($message . ' on line ' . $token['line'] . '.'),null); + else + throw new \Exception($message . ' on line ' . $token['line'] . '.'); }; // ### `_resolveIRI` resolves a relative IRI token against the base path, @@ -994,7 +997,10 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null, $f } } } catch (\Exception $e) { - call_user_func($this->callback, $e, null); + if ($this->callback) + call_user_func($this->callback, $e, null); + else + throw $e; $this->callback = function () {}; } } diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 9cd5852..d30ba4c 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -477,6 +477,7 @@ public function testBlankNodes () ['http://ex.org/a', 'http://ex.org/b', 'http://ex.org/c'], ['A:', 'b:', 'c:'], ['a:a', 'b:B', 'C-D:c']); + // ### should resolve datatype IRIs against @base $this->shouldParse("@base .\n" . " \"c\"^^.\n" . @@ -484,7 +485,7 @@ public function testBlankNodes () ' "g"^^.', ['http://ex.org/a', 'http://ex.org/b', '"c"^^http://ex.org/d'], ['http://ex.org/d/e', 'http://ex.org/d/f', '"g"^^http://ex.org/d/h']); - + // ### should resolve IRIs against a base with a fragment $this->shouldParse("@base .\n" . " <#c>.\n", @@ -617,6 +618,14 @@ public function testBlankNodes () // ### should parse a one-triple anonymous graph and the GRAPH keyword $this->shouldParse('GRAPH [] { }', ['a', 'b', 'c', '_:b0']); + + } + + public function testLiterals () + { + // ### should parse triple quotes + $this->shouldParse(" \"\"\" abc \"\"\".", + ['a', 'b', '" abc "']); } public function testUnicodeSequences () From fdb890c3092d797ab21248db18150bf291b664b9 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 13 Apr 2017 21:24:09 +0200 Subject: [PATCH 62/72] Unicode parsing works --- src/N3Lexer.php | 26 +++++++++++++++----------- test/TriGParserTest.php | 13 +++++++++---- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 2d6c413..f067e6a 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -9,7 +9,7 @@ class N3Lexer // Regular expression and replacement string to escape N3 strings. // Note how we catch invalid unicode sequences separately (they will trigger an error). - private $escapeSequence = '/\\[uU]|\\\(.)/'; + private $escapeSequence = '/\\\\u([a-fA-F0-9]{4})|\\\\U([a-fA-F0-9]{8})|\\\\[uU]|\\\\(.)/'; private $escapeReplacements = [ '\\' => '\\', "'"=> "'", '"' => '"', 'n' => '\n', 'r' => '\r', 't' => '\t', 'f' => '\f', 'b' => '\b', @@ -53,12 +53,15 @@ public function __construct($options = []) { } // ## Regular expressions - private $iri ='/^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/'; // IRI with escape sequences; needs sanity check after unescaping - private $unescapedIri = '/^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/'; // IRI without escape sequences; no unescaping + //_iri: /^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/, // IRI with escape sequences; needs sanity check after unescaping + private $iri ='/^<((?:[^ <>{}\\\\]|\\\\[uU])+)>[ \\t]*/'; // IRI with escape sequences; needs sanity check after unescaping + // _unescapedIri: /^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/, // IRI without escape sequences; no unescaping + private $unescapedIri = '/^<([^\\x00-\\x20<>\\\\"\\{\\}\\|\\^\\`]*)>[ \\t]*/'; // IRI without escape sequences; no unescaping // _unescapedString: /^"[^"\\]+"(?=[^"\\])/, // non-empty string without escape sequences - private $unescapedString= '/^"[^\\"]+"(?=[^\\"])/'; // non-empty string without escape sequences + private $unescapedString= '/^"[^\\\\"]+"(?=[^\\\\"])/'; // non-empty string without escape sequences // _singleQuotedString: /^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^'[^'\\]*(?:\\.[^'\\]*)*'(?=[^'\\])/, - private $singleQuotedString= '/^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^\'[^\\\']*(?:\\.[^\'\\]*)*\'(?=[^\\\'])/'; + private $singleQuotedString= '/^"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(?=[^"\\\\])|^\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'(?=[^\'\\\\])/'; + //TODO private $tripleQuotedString = '/^""("[^\\"]*(?:(?:\\.|"(?!""))[^\\"]*)*")""|^\'\'(\'[^\\\']*(?:(?:\\.|\'(?!\'\'))[^\\\']*)*\')\'\'/'; private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i'; private $prefix = '/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:(?=[#\s<])/'; @@ -168,7 +171,7 @@ private function tokenizeToEnd($callback, $inputFinished) { // Try to find a full IRI with escape sequences else if (preg_match($this->iri, $input, $match)) { $unescaped = $this->unescape($match[1]); - if ($unescaped === null || preg_match($illegalIriChars,$unescaped)) + if ($unescaped === null || preg_match($this->illegalIriChars,$unescaped)) return $reportSyntaxError($this); $type = 'IRI'; $value = $unescaped; @@ -179,7 +182,6 @@ private function tokenizeToEnd($callback, $inputFinished) { $matchLength = 2; $value = 'http://www.w3.org/2000/10/swap/log#implies'; } - break; case '_': // Try to find a blank node. Since it can contain (but not end with) a dot, @@ -206,7 +208,7 @@ private function tokenizeToEnd($callback, $inputFinished) { if ($unescaped === null) return $reportSyntaxError($this); $type = 'literal'; - $value = preg_replace('/^'|'$/g', '"',$unescaped); + $value = preg_replace('/^\'|\'$/', '"',$unescaped); } // Try to find a literal wrapped in three pairs of single or double quotes else if (preg_match($this->tripleQuotedString, $input, $match)) { @@ -384,7 +386,11 @@ private function tokenizeToEnd($callback, $inputFinished) { // ### `_unescape` replaces N3 escape codes by their corresponding characters private function unescape($item) { - return preg_replace_callback($this->escapeSequence, function ($sequence, $unicode4, $unicode8, $escapedChar) { + return preg_replace_callback($this->escapeSequence, function ($match) { + $sequence = $match[0]; + $unicode4 = isset($match[1])?$match[1]:null; + $unicode8 = isset($match[2])?$match[2]:null; + $escapedChar = isset($match[3])?$match[3]:null; $charCode; if ($unicode4) { $charCode = intval($unicode4, 16); @@ -393,8 +399,6 @@ private function unescape($item) { else if ($unicode8) { $charCode = intval($unicode8, 16); return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES'); - //if ($charCode <= 0xFFFF) return fromCharCode($charCode); - //return fromCharCode(0xD800 . (($charCode -= 0x10000) / 0x400), 0xDC00 . ($charCode & 0x3FF)); } else { $replacement = $this->escapeReplacements[$escapedChar]; diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index d30ba4c..7957f27 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -626,13 +626,18 @@ public function testLiterals () // ### should parse triple quotes $this->shouldParse(" \"\"\" abc \"\"\".", ['a', 'b', '" abc "']); + + + // ### should parse triple quotes with a newline + $this->shouldParse(" \"\"\" abc\nabc \"\"\".", + ['a', 'b', '" abc' . "\n" . 'abc "']); } - public function testUnicodeSequences () + public function testUnicodeSequences () { - // ### should parse a graph with 8-bit unicode escape sequences //TODO: no idea how we can fix this in PHP - //$this->shouldParse('<\\U0001d400> {'."\n".'<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>'."\n".'}' . "\n", - //['\ud835\udC00', '\ud835\udc00', '"\ud835\udc00"^^\ud835\udc00', '\ud835\udc00']); + // ### should parse a graph with 8-bit unicode escape sequences + $this->shouldParse('<\\U0001d400> {'."\n".'<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>'."\n".'}' . "\n", + ['𝐀', '𝐀', '"𝐀"^^𝐀', '𝐀']); } public function testParseErrors () From 53ee550dd9407f5dfe698bb0ee4bd8ae5131d29f Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Thu, 13 Apr 2017 21:27:23 +0200 Subject: [PATCH 63/72] Added extra unittest --- test/TriGParserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 7957f27..3fa109b 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -70,9 +70,9 @@ public function testZeroOrMoreTriples () ['noturn:a', 'noturn:b', '"x"^^noturn:urn:foo']); // ### should not parse a triple with a literal and a prefixed name type with an inexistent prefix -/* shouldNotParse(' "string"^^x:z.', + $this->shouldNotParse(' "string"^^x:z.', 'Undefined prefix "x:" on line 1.'); -*/ + // ### should parse a triple with the "a" shorthand predicate $this->shouldParse(' a .', From 42a9c5f53c445cf6a745f5b7a462d9dc3d7f340c Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 14 Apr 2017 11:57:51 +0200 Subject: [PATCH 64/72] Character escaping works properly now. N-Triples and N-Quads should be spec compliant now --- src/N3Lexer.php | 4 ++-- src/TriGWriter.php | 50 ++++++++++++++++++----------------------- test/TriGWriterTest.php | 10 ++++----- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index f067e6a..9a1dbaa 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -401,9 +401,9 @@ private function unescape($item) { return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES'); } else { - $replacement = $this->escapeReplacements[$escapedChar]; - if (!$replacement) + if (!isset($this->escapeReplacements[$escapedChar])) throw new \Exception(); + $replacement = $this->escapeReplacements[$escapedChar]; return $replacement; } },$item); diff --git a/src/TriGWriter.php b/src/TriGWriter.php index fd4a6c4..27db6d5 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -6,14 +6,13 @@ class TriGWriter { // Matches a literal as represented in memory by the N3 library - CONST LITERALMATCHER = '/^"(.*)"(?:\^\^(.+)|@([\-a-z]+))?$/is'; + CONST LITERALMATCHER = '/^"(.*)"(?:\\^\\^(.+)|@([\\-a-z]+))?$/is'; // rdf:type predicate (for 'a' abbreviation) CONST RDF_PREFIX = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; CONST RDF_TYPE = self::RDF_PREFIX . 'type'; // Characters in literals that require escaping - CONST ESCAPE = "/[\"\\\t\n\r\f]/u"; #/u'; - CONST ESCAPEALL = "/[\"\\\t\n\r\b\f]/u"; + CONST ESCAPE = "/[\"\\\\\\t\\n\\r\\b\\f]/"; //HHVM does not allow this to be a constant private $ESCAPEREPLACEMENTS; @@ -44,29 +43,16 @@ public function __construct($options = []) $this->writeTriple = $this->writeTripleLine; } - // TODO: I think we could do without this... - /*$this->characterReplacer = function ($character) { + $this->characterReplacer = function ($character) { // Replace a single character by its escaped version $character = $character[0]; - if (strlen($character) > 0 && isset(self::ESCAPEREPLACEMENTS[$character[0]])) { - return self::ESCAPEREPLACEMENTS[$character[0]]; + if (strlen($character) > 0 && isset($this->ESCAPEREPLACEMENTS[$character[0]])) { + return $this->ESCAPEREPLACEMENTS[$character[0]]; } else { - // Replace a single character with its 4-bit unicode escape sequence - $result = ""; - if (strlen($character) === 1) { - //TODO - //$result = $character.charCodeAt(0).toString(16); - //$result = \'\\u0000\'.substr(0, 6 - strlen($result)) + $result; - } - // Replace a surrogate pair with its 8-bit unicode escape sequence - else { - //$result = (($character.charCodeAt(0) - 0xD800) * 0x400 + - //$character.charCodeAt(1) + 0x2400).toString(16); - //$result = \'\\U00000000\'.substr(0, 10 - strlen($result)) + $result; - } - return $result; + throw new \Exception('Something is wrong in regex to replace strings'); + return $result; //no escaping necessary, should not happen, or something is wrong in our regex } - };*/ + }; } private function initWriter () @@ -163,8 +149,8 @@ private function encodeIriOrBlankNode ($entity) { return $entity; } // Escape special characters - //if (preg_match(self::ESCAPE, $entity)) - // $entity = preg_replace_callback(self::ESCAPEALL, $this->characterReplacer,$entity); + if (preg_match(self::ESCAPE, $entity)) + $entity = preg_replace_callback(self::ESCAPE, $this->characterReplacer,$entity); // Try to represent the IRI as prefixed name preg_match($this->prefixRegex, $entity, $prefixMatch); @@ -181,8 +167,15 @@ private function encodeIriOrBlankNode ($entity) { // ### `_encodeLiteral` represents a literal private function encodeLiteral ($value, $type = null, $language = null) { + //TODO: change back to a single quote and escape all the other things // Escape special characters - TODO: unicode characters? - if (preg_match('/[\t\n\r\f]/',$value)) { + + // Escape special characters + if (preg_match(self::ESCAPE, $value)) + $value = preg_replace_callback(self::ESCAPE, $this->characterReplacer,$value); + + + /*if (preg_match('/[\\t\\n\\r\\f]/',$value)) { $value = str_replace(array('\\', '"""'), array('\\\\', '\\"""'), $value); @@ -194,10 +187,11 @@ private function encodeLiteral ($value, $type = null, $language = null) { } // enclose between 3 double quotes $value = '"""' . $value . '"""'; - } else { + } else {*/ // enclose in double quotes, while escaping back slashes - $value = '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $value) . '"'; - } +// $value = '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $value) . '"'; +// } + $value = '"' . $value . '"'; // Write the literal, possibly with type or language if (isset($language)) diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index 02c4f22..a13986d 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -61,25 +61,25 @@ public function testLiterals() //should serialize a literal containing a tab character', $this->shouldSerialize(['a', 'b', "\"c\tde\""], - " \"\"\"c\tde\"\"\".\n"); + " \"c\\tde\".\n"); //should serialize a literal containing a newline character', /* shouldSerialize(['a', 'b', '"c\nde"'], ' "c\\nde".\n'));*/ $this->shouldSerialize(['a', 'b', '"c' . "\n" . 'de"'], - ' """c' . "\n" . 'de""".' . "\n"); + ' "c\\nde".' . "\n"); //should serialize a literal containing a cariage return character', $this->shouldSerialize(['a', 'b', '"c' . "\r" . 'de"'], - ' """' . "c\rde" . '""".' . "\n"); + ' "c\\rde".' ."\n"); //should serialize a literal containing a backspace character', /*$this->shouldSerialize(['a', 'b', '"c' . "\b" . 'de"'], - ' """' . "c\bde". '""".' . "\n");*/ //→ TODO: Doesn’t work properly + ' "' . "c\bde". '".' . "\n");*/ //→ TODO: Doesn’t work properly //should serialize a literal containing a form feed character', $this->shouldSerialize(['a', 'b', '"c' . "\f" . 'de"'], - ' """c' . "\f" . 'de""".' . "\n"); + ' "c\\fde".' . "\n"); //should serialize a literal containing a line separator', - These tests willl not work for PHP5.6, hence commented. PHP7 only introduced the unicode escape sequence. //$this->shouldSerialize(['a', 'b', "\"c\u{2028}de\""], From ab6afee150116084a85bba119b756ef36d2b3946 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 14 Apr 2017 12:09:03 +0200 Subject: [PATCH 65/72] Support backspace character escape in Writer --- src/TriGWriter.php | 2 +- test/TriGWriterTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 27db6d5..feb1255 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -28,7 +28,7 @@ public function __construct($options = []) { $this->ESCAPEREPLACEMENTS = [ '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", - "\n" => '\\n', "\r" => "\\r", "\b"=> "\\b", "\f"=> "\\f" + "\n" => '\\n', "\r" => "\\r", chr(8) => "\\b", "\f"=> "\\f" ]; $this->initWriter (); /* Initialize writer, depending on the format*/ diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php index a13986d..519b610 100644 --- a/test/TriGWriterTest.php +++ b/test/TriGWriterTest.php @@ -74,8 +74,8 @@ public function testLiterals() ' "c\\rde".' ."\n"); //should serialize a literal containing a backspace character', - /*$this->shouldSerialize(['a', 'b', '"c' . "\b" . 'de"'], - ' "' . "c\bde". '".' . "\n");*/ //→ TODO: Doesn’t work properly + $this->shouldSerialize(['a', 'b', '"c' . chr(8) . 'de"'], + ' "' . "c\bde". '".' . "\n"); //→ TODO: Doesn’t work properly //should serialize a literal containing a form feed character', $this->shouldSerialize(['a', 'b', '"c' . "\f" . 'de"'], From 00901de3d3e32c0e864ab70918f05289b81d4433 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 14 Apr 2017 12:10:57 +0200 Subject: [PATCH 66/72] Removed throwing exception when logic error would happen --- src/TriGWriter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TriGWriter.php b/src/TriGWriter.php index feb1255..7f6013a 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -49,7 +49,6 @@ public function __construct($options = []) if (strlen($character) > 0 && isset($this->ESCAPEREPLACEMENTS[$character[0]])) { return $this->ESCAPEREPLACEMENTS[$character[0]]; } else { - throw new \Exception('Something is wrong in regex to replace strings'); return $result; //no escaping necessary, should not happen, or something is wrong in our regex } }; From 30a0f90a1da0e6aad5aa3102a0816e5912e57e52 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Fri, 14 Apr 2017 12:34:27 +0200 Subject: [PATCH 67/72] In memory should never contain escapes --- src/N3Lexer.php | 55 +++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 9a1dbaa..753a700 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -5,18 +5,10 @@ // **N3Lexer** tokenizes N3 documents. class N3Lexer { - //private $fromCharCode = String.fromCharCode; //TODO - // Regular expression and replacement string to escape N3 strings. // Note how we catch invalid unicode sequences separately (they will trigger an error). private $escapeSequence = '/\\\\u([a-fA-F0-9]{4})|\\\\U([a-fA-F0-9]{8})|\\\\[uU]|\\\\(.)/'; - private $escapeReplacements = [ - '\\' => '\\', "'"=> "'", '"' => '"', - 'n' => '\n', 'r' => '\r', 't' => '\t', 'f' => '\f', 'b' => '\b', - '_' => '_', '~' => '~', '.' => '.', '-' => '-', '!' => '!', '$' => '$', '&' => '&', - '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', ';' => ';', '=' => '=', - '/' => '/', '?' => '?', '#' => '#', '@' => '@', '%' => '%' - ]; + private $escapeReplacements; private $illegalIriChars = '/[\x00-\x20<>\\"\{\}\|\^\`]/'; private $input; @@ -26,6 +18,13 @@ class N3Lexer public function __construct($options = []) { $this->initTokenize(); + $this->escapeReplacements = [ + '\\' => '\\', "'"=> "'", '"' => '"', + 'n' => "\n", 'r' => "\r", 't' => "\t", 'f' => "\f", 'b' => chr(8), + '_' => '_', '~' => '~', '.' => '.', '-' => '-', '!' => '!', '$' => '$', '&' => '&', + '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', ';' => ';', '=' => '=', + '/' => '/', '?' => '?', '#' => '#', '@' => '@', '%' => '%' + ]; // In line mode (N-Triples or N-Quads), only simple features may be parsed if ($options["lineMode"]) { // Don't tokenize special literals @@ -61,26 +60,25 @@ public function __construct($options = []) { private $unescapedString= '/^"[^\\\\"]+"(?=[^\\\\"])/'; // non-empty string without escape sequences // _singleQuotedString: /^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^'[^'\\]*(?:\\.[^'\\]*)*'(?=[^'\\])/, private $singleQuotedString= '/^"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(?=[^"\\\\])|^\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'(?=[^\'\\\\])/'; - //TODO - private $tripleQuotedString = '/^""("[^\\"]*(?:(?:\\.|"(?!""))[^\\"]*)*")""|^\'\'(\'[^\\\']*(?:(?:\\.|\'(?!\'\'))[^\\\']*)*\')\'\'/'; - private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i'; - private $prefix = '/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:(?=[#\s<])/'; - - private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; + // _tripleQuotedString: /^""("[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*")""|^''('[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*')''/, + private $tripleQuotedString = '/^""("[^\\\\"]*(?:(?:\\\\.|"(?!""))[^\\\\"]*)*")""|^\'\'(\'[^\\\\\']*(?:(?:\\\\.|\'(?!\'\'))[^\\\\\']*)*\')\'\'/'; + private $langcode = '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\\-])/i'; + private $prefix = '/^((?:[A-Za-z\\xc0-\\xd6\\xd8-\\xf6])(?:\\.?[\\-0-9A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)?:(?=[#\\s<])/'; - //private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; - private $variable = '/^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)(?=[.,;!\^\s#()\[\]\{\}"\'<])/'; + private $prefixed = "/^((?:[A-Za-z\\xc0-\\xd6\\xd8-\\xf6])(?:\\.?[\\-0-9A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)?:((?:(?:[0-:A-Z_a-z\\xc0-\\xd6\\xd8-\\xf6]|%[0-9a-fA-F]{2}|\\\\[!#-\\/;=?\\-@_~])(?:(?:[\\.\\-0-:A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6]|%[0-9a-fA-F]{2}|\\\\[!#-\\/;=?\\-@_~])*(?:[\\-0-:A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6]|%[0-9a-fA-F]{2}|\\\\[!#-\\/;=?\\-@_~]))?)?)(?:[ \\t]+|(?=\.?[,;!\\^\\s#()\\[\\]\\{\\}\"'<]))/"; + //OLD VERSION private $prefixed = "/^((?:[A-Za-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}\"'<]))/"; + private $variable = '/^\\?(?:(?:[A-Z_a-z\\xc0-\\xd6\\xd8-\\xf6])(?:[\\-0-:A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)(?=[.,;!\\^\\s#()\\[\\]\\{\\}"\'<])/'; - private $blank = '/^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6])*)(?:[ \t]+|(?=\.?[,;:\s#()\[\]\{\}"\'<]))/'; - private $number = "/^[\-+]?(?:\d+\.?\d*([eE](?:[\-\+])?\d+)|\d*\.?\d+)(?=[.,;:\s#()\[\]\{\}\"'<])/"; - private $boolean = '/^(?:true|false)(?=[.,;\s#()\[\]\{\}"\'<])/'; - private $keyword = '/^@[a-z]+(?=[\s#<])/i'; - private $sparqlKeyword= '/^(?:PREFIX|BASE|GRAPH)(?=[\s#<])/i'; - private $shortPredicates= '/^a(?=\s+|<)/'; - private $newline= '/^[ \t]*(?:#[^\n\r]*)?(?:\r\n|\n|\r)[ \t]*/'; - private $comment= '/#([^\n\r]*)/'; - private $whitespace= '/^[ \t]+/'; - private $endOfFile= '/^(?:#[^\n\r]*)?$/'; + private $blank = '/^_:((?:[0-9A-Z_a-z\\xc0-\\xd6\\xd8-\\xf6])(?:\\.?[\\-0-9A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)(?:[ \\t]+|(?=\\.?[,;:\\s#()\\[\\]\\{\\}"\'<]))/'; + private $number = "/^[\\-+]?(?:\\d+\\.?\\d*([eE](?:[\\-\\+])?\\d+)|\\d*\\.?\\d+)(?=[.,;:\\s#()\\[\\]\\{\\}\"'<])/"; + private $boolean = '/^(?:true|false)(?=[.,;\\s#()\\[\\]\\{\\}"\'<])/'; + private $keyword = '/^@[a-z]+(?=[\\s#<])/i'; + private $sparqlKeyword= '/^(?:PREFIX|BASE|GRAPH)(?=[\\s#<])/i'; + private $shortPredicates= '/^a(?=\\s+|<)/'; + private $newline= '/^[ \\t]*(?:#[^\\n\\r]*)?(?:\\r\\n|\\n|\\r)[ \\t]*/'; + private $comment= '/#([^\\n\\r]*)/'; + private $whitespace= '/^[ \\t]+/'; + private $endOfFile= '/^(?:#[^\\n\\r]*)?$/'; // ## Private methods // ### `_tokenizeToEnd` tokenizes as for as possible, emitting tokens through the callback @@ -403,8 +401,7 @@ private function unescape($item) { else { if (!isset($this->escapeReplacements[$escapedChar])) throw new \Exception(); - $replacement = $this->escapeReplacements[$escapedChar]; - return $replacement; + return $this->escapeReplacements[$escapedChar]; } },$item); } From 5fd935797a1da2cde53102a0ae7ef983967099bb Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 17 Apr 2017 11:38:48 +0200 Subject: [PATCH 68/72] More dev friendly streaming support --- README.md | 63 ++++++++++++--- bin/convert.php | 13 ++- bin/validator.php | 2 +- examples/parseAndWrite.php | 8 +- src/TriGParser.php | 82 +++++++++++-------- src/TriGWriter.php | 66 ++++++--------- test/TriGParserTest.php | 161 ++++++++++++++++++++----------------- 7 files changed, 224 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 59f579c..b691984 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ $writer->addTriple("schema:Person","dct:title","\"Person\"@en","http://example.o echo $writer->end(); ``` -All methods (some may throw an exception): +#### All methods ```php //The method names should speak for themselves: $writer = new TriGWriter(["prefixes": [ /* ... */]]); @@ -86,45 +86,84 @@ $list = $writer->addList($elements); //Returns the current output it is already able to create and clear the internal memory use (useful for streaming) $out .= $writer->read(); +//Alternatively, you can listen for new chunks through a callback: +$writer->setReadCallback(function ($output) { echo $output }); -//Call this at the end. The return value will be the full triple output, or the rest of the output such as closing dots and brackets +//Call this at the end. The return value will be the full triple output, or the rest of the output such as closing dots and brackets, unless a callback was set. $out .= $writer->end(); +//OR +$writer->end(); ``` ### Parsing Next to [TriG](https://www.w3.org/TR/trig/), the TriGParser class also parses [Turtle](https://www.w3.org/TR/turtle/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/) and the [W3C Team Submission N3](https://www.w3.org/TeamSubmission/n3/) -Basic example: +#### All methods +```php +$parser = new TriGParser($options, $tripleCallback, $prefixCallback); +$parser->setTripleCallback($function); +$parser->setPrefixCallback($function); +$parser->parse($input, $tripleCallback, $prefixCallback); +$parser->parseChunk($input); +$parser->end(); +``` + +#### Basic examples for small files + +Using return values and passing these to a writer: ```php use pietercolpaert\hardf\TriGParser; use pietercolpaert\hardf\TriGWriter; - -echo "--- First, simple implementation ---\n"; $parser = new TriGParser(["format" => "n-quads"]); //also parser n-triples, n3, turtle and trig. Format is optional $writer = new TriGWriter(); $triples = $parser->parse(" ."); $writer->addTriples($triples); echo $writer->end(); +``` -echo "--- Second streaming implementation with callbacks ---\n"; +Using callbacks and passing these to a writer: +```php $parser = new TriGParser(); $writer = new TriGWriter(["format"=>"trig"]); $parser->parse(" . .", function ($e, $triple) use ($writer) { - if (!$e && $triple) + if (!isset($e) && isset($triple)) { $writer->addTriple($triple); - else if (!$triple) - echo $writer->end(); + echo $writer->read(); //write out what we have so far + } else if (!isset($triple)) // flags the end of the file + echo $writer->end(); //write the end else echo "Error occured: " . $e; }); ``` -Parse chunks until end also works this way: +#### Example using chunks and keeping prefixes + +When you need to parse a large file, you will need to parse only chunks and already process them. You can do that as follows: + ```php -$parser->parseChunk(chunk, callback); -$parser->end(callback); +$writer = new TriGWriter(["format"=>"n-quads"]); +$tripleCallback = function ($error, $triple) use ($writer) { + if (isset($error)) + throw $error; + else if (isset($triple)) { + $writer->write(); + echo $writer->read(); + else if (isset($error)) { + throw $error; + } else { + echo $writer->end(); + } +}; +$prefixCallback = function ($prefix, $iri) use (&$writer) { + $writer->addPrefix($prefix, $iri); +}; +$parser = new TriGParser(["format" => "trig"], $tripleCallback, $prefixCallback); +$parser->parseChunk($chunk); +$parser->parseChunk($chunk); +$parser->parseChunk($chunk); +$parser->end(); //Needs to be called ``` ### Utility diff --git a/bin/convert.php b/bin/convert.php index ef8200d..7c5b2fc 100644 --- a/bin/convert.php +++ b/bin/convert.php @@ -7,17 +7,14 @@ $informat = "turtle"; if (isset($argv[1])) $informat = $argv[1]; -$parser = new TriGParser(["format" => $informat]); $outformat = "n-triples"; if (isset($argv[2])) $outformat = $argv[2]; $writer = new TriGWriter(["format" => $outformat]); - -$callback = function ($error, $triple) use (&$writer) -{ - if (!isset($error) && !isset($triple)) { +$parser = new TriGParser(["format" => $informat], function ($error, $triple) use (&$writer) { + if (!isset($error) && !isset($triple)) { //flags end echo $writer->end(); } else if (!$error) { $writer->addTriple($triple); @@ -25,9 +22,9 @@ } else { fwrite(STDERR, $error->getMessage() . "\n"); } -}; +}); while ($line = fgets(STDIN)) { - $parser->parseChunk($line, $callback); + $parser->parseChunk($line); } -$parser->end($callback); +$parser->end(); diff --git a/bin/validator.php b/bin/validator.php index ff20bb0..63e75ec 100644 --- a/bin/validator.php +++ b/bin/validator.php @@ -18,7 +18,7 @@ if ($line) $tripleCount += sizeof($parser->parseChunk($line)); else { - $tripleCount += sizeof($parser->end($line)); + $tripleCount += sizeof($parser->end()); $finished = true; } } catch (\Exception $e) { diff --git a/examples/parseAndWrite.php b/examples/parseAndWrite.php index 5149283..4e3d546 100644 --- a/examples/parseAndWrite.php +++ b/examples/parseAndWrite.php @@ -4,9 +4,9 @@ use pietercolpaert\hardf\TriGWriter; echo "--- First, simple implementation ---\n"; -$parser = new TriGParser(); +$parser = new TriGParser([]); $writer = new TriGWriter(["format"=>"trig"]); -$triples = $parser->parse("() ."); +$triples = $parser->parse("() . \"\"\"\n\"\"\"."); $writer->addTriples($triples); echo $writer->end(); @@ -15,11 +15,13 @@ $parser = new TriGParser(); $writer = new TriGWriter(["format"=>"trig"]); $error = null; -$parser->parse(" . .", function ($e, $triple) use ($writer) { +$parser->parse("@prefix ex: . . . ex:s ex:p ex:o . ", function ($e, $triple) use (&$writer) { if (!$e && $triple) $writer->addTriple($triple); else if (!$triple) echo $writer->end(); else echo "Error occured: " . $e; +}, function ($prefix, $iri) use (&$writer) { + $writer->addPrefix($prefix,$iri); }); diff --git a/src/TriGParser.php b/src/TriGParser.php index b3b55c2..3da2d78 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -13,8 +13,8 @@ class TriGParser CONST QUANTIFIERS_GRAPH = 'urn:n3:quantifiers'; private $absoluteIRI = '/^[a-z][a-z0-9+.-]*:/i'; - private $schemeAuthority = '/^(?:([a-z][a-z0-9+.-]*:))?(?:\/\/[^\/]*)?/i'; - private $dotSegments = '/(?:^|\/)\.\.?(?:$|[\/#?])/'; + private $schemeAuthority = '/^(?:([a-z][a-z0-9+.-]*:))?(?:\\/\\/[^\\/]*)?/i'; + private $dotSegments = '/(?:^|\\/)\\.\\.?(?:$|[\\/#?])/'; // The next ID for new blank nodes private $blankNodePrefix; @@ -27,7 +27,9 @@ class TriGParser private $readCallback; // ## Constructor - public function __construct($options = []) { + public function __construct($options = [], $tripleCallback = null, $prefixCallback = null) { + $this->setTripleCallback($tripleCallback); + $this->setPrefixCallback($prefixCallback); $this->contextStack = []; $this->graph = null; @@ -565,7 +567,7 @@ private function initReaders () case '.': $this->subject = null; $next = sizeof($this->contextStack) ? $this->readSubject : $this->readInTopContext; - if ($inversePredicate) $this->inversePredicate = false;//TODO: What’s this? + if ($inversePredicate) $this->inversePredicate = false; //TODO: What’s this? break; // Semicolon means the subject is shared; predicate and object are different case ';': @@ -635,9 +637,9 @@ private function initReaders () $this->readPrefixIRI = function ($token) { if ($token["type"] !== 'IRI') return call_user_func($this->error,'Expected IRI to follow prefix "' . $this->prefix . ':"', $token); - $prefixIRI = call_user_func($this->readEntity,$token); + $prefixIRI = call_user_func($this->readEntity, $token); $this->prefixes[$this->prefix] = $prefixIRI; - call_user_func($this->prefixCallback,$this->prefix, $prefixIRI); + call_user_func($this->prefixCallback, $this->prefix, $prefixIRI); return $this->readDeclarationPunctuation; }; @@ -961,10 +963,15 @@ private function initReaders () // ## Public methods // ### `parse` parses the N3 input and emits each parsed triple through the callback - public function parse($input, $tripleCallback = null, $prefixCallback = null, $finalize = true) { - $this->prefixCallback = isset($prefixCallback)?$prefixCallback:function () {}; - // Parse synchronously if no triple callback is given - if (!isset($tripleCallback)) { + public function parse($input, $tripleCallback = null, $prefixCallback = null) { + $this->setTripleCallback($tripleCallback); + $this->setPrefixCallback($prefixCallback); + return $this->parseChunk($input, true); + } + + // ### New method for streaming possibilities: parse only a chunk + public function parseChunk($input, $finalize = false) { + if (!isset($this->tripleCallback)) { $triples = []; $error = null; $this->callback = function ($e, $t = null) use (&$triples, &$error) { @@ -982,37 +989,46 @@ public function parse($input, $tripleCallback = null, $prefixCallback = null, $f $this->readCallback = call_user_func($this->readCallback, $token); } if ($error) throw $error; - return $triples; - } - // Parse asynchronously otherwise, executing the read callback when a token arrives - $this->callback = $tripleCallback; - try { - $tokens = $this->lexer->tokenize($input, $finalize); - foreach($tokens as $token) { - if (isset($this->readCallback)) { - $this->readCallback = call_user_func($this->readCallback, $token); - } else { - //error occured in parser - break; + return $triples; + } else { + // Parse asynchronously otherwise, executing the read callback when a token arrives + $this->callback = $this->tripleCallback; + try { + $tokens = $this->lexer->tokenize($input, $finalize); + foreach($tokens as $token) { + if (isset($this->readCallback)) { + $this->readCallback = call_user_func($this->readCallback, $token); + } else { + //error occured in parser + break; + } } + } catch (\Exception $e) { + if ($this->callback) + call_user_func($this->callback, $e, null); + else + throw $e; + $this->callback = function () {}; } - } catch (\Exception $e) { - if ($this->callback) - call_user_func($this->callback, $e, null); - else - throw $e; - $this->callback = function () {}; } } - public function parseChunk($input, $tripleCallback = null, $prefixCallback = null) { - return $this->parse($input, $tripleCallback, $prefixCallback, false); + public function setTripleCallback ($tripleCallback = null) + { + $this->tripleCallback = $tripleCallback; } - public function end($tripleCallback = null, $prefixCallback = null) + public function setPrefixCallback ($prefixCallback = null) { - return $this->parse("",$tripleCallback, $prefixCallback, true); + if (isset($prefixCallback)) + $this->prefixCallback = $prefixCallback; + else { + $this->prefixCallback = function () {}; + } } - + public function end() + { + return $this->parseChunk("", true); + } } \ No newline at end of file diff --git a/src/TriGWriter.php b/src/TriGWriter.php index 7f6013a..0132f94 100644 --- a/src/TriGWriter.php +++ b/src/TriGWriter.php @@ -24,8 +24,9 @@ class TriGWriter // Replaces a character by its escaped version private $characterReplacer; - public function __construct($options = []) + public function __construct($options = [], $readCallback = null) { + $this->setReadCallback($readCallback); $this->ESCAPEREPLACEMENTS = [ '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", "\n" => '\\n', "\r" => "\\r", chr(8) => "\\b", "\f"=> "\\f" @@ -54,6 +55,12 @@ public function __construct($options = []) }; } + public function setReadCallback($readCallback) + { + $this->readCallback = $readCallback; + } + + private function initWriter () { // ### `_writeTriple` writes the triple to the output stream @@ -114,13 +121,12 @@ private function initWriter () // ### `_write` writes the argument to the output stream - private function write ($string, $callback = null) { - //this._outputStream.write(string, 'utf8', callback); + private function write ($string) { if ($this->blocked) { throw new \Exception('Cannot write because the writer has been closed.'); } else { - if (isset($callback)) { - $callback($string); + if (isset($this->readCallback)) { + call_user_func($this->readCallback, $string); } else { //buffer all $this->string .= $string; @@ -131,13 +137,9 @@ private function write ($string, $callback = null) { // ### Reads a bit of the string public function read () { - if ($this->blocked) { - throw new \Exception('Cannot write because the writer has been closed.'); - } else { - $string = $this->string; - $this->string = ""; - return $string; - } + $string = $this->string; + $this->string = ""; + return $string; } // ### `_encodeIriOrBlankNode` represents an IRI or blank node @@ -166,39 +168,17 @@ private function encodeIriOrBlankNode ($entity) { // ### `_encodeLiteral` represents a literal private function encodeLiteral ($value, $type = null, $language = null) { - //TODO: change back to a single quote and escape all the other things - // Escape special characters - TODO: unicode characters? - // Escape special characters if (preg_match(self::ESCAPE, $value)) $value = preg_replace_callback(self::ESCAPE, $this->characterReplacer,$value); - - - /*if (preg_match('/[\\t\\n\\r\\f]/',$value)) { - - $value = str_replace(array('\\', '"""'), array('\\\\', '\\"""'), $value); - - // Check if the last character is a trailing double quote, if so, escape it. - $pos = strrpos($value, '"'); - if ($pos !== false && $pos + 1 == strlen($value)) { - $value = substr($value, 0, -1); - $value .= '\"'; - } - // enclose between 3 double quotes - $value = '"""' . $value . '"""'; - } else {*/ - // enclose in double quotes, while escaping back slashes -// $value = '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $value) . '"'; -// } - $value = '"' . $value . '"'; - + $value = $value ; // Write the literal, possibly with type or language if (isset($language)) - return $value . '@' . $language; + return '"' . $value . '"@' . $language; else if (isset($type)) - return $value . '^^' . $this->encodeIriOrBlankNode($type); + return '"' . $value . '"^^' . $this->encodeIriOrBlankNode($type); else - return $value; + return '"' . $value . '"'; } // ### `_encodeSubject` represents a subject @@ -358,17 +338,19 @@ public function addList ($elements = null) { } // ### `end` signals the end of the output stream - public function end($callback = null) + public function end() { // Finish a possible pending triple if ($this->subject !== null) { $this->write($this->graph ? "\n}\n" : ".\n"); $this->subject = null; } + if (isset($this->readCallbacks)) + call_user_func($this->readCallback, $this->string); + // Disallow further writing $this->blocked = true; - if ($callback) - $callback(null,$this->string); - return $this->string; + if (!isset($this->readCallback)) + return $this->string; } } \ No newline at end of file diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 3fa109b..ff82ab5 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -753,81 +753,98 @@ public function testParseErrors () public function testInterface() { - /* // ### should not error if there is no triple callback function () { - new N3Parser().parse(''); - }); - - // ### should return prefixes through a callback function (done) { - $prefixes = {}; - new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', - tripleCallback, prefixCallback); - - function tripleCallback($error, $triple) { - expect(error).not.to.exist; - if (!triple) { - Object.keys(prefixes).should.have.length(2); - expect(prefixes).to.have.property('a', 'IRIa'); - expect(prefixes).to.have.property('b', 'IRIb'); - done(); + $prefixes = []; + $tripleCallback = function ($error, $triple) use (&$prefixes) { + //when end of stream + if (!isset($triple)) { + $this->assertEquals(2, sizeof(array_keys($prefixes))); + } + }; + + $prefixCallback = function ($prefix, $iri) use (&$prefixes) { + //$this->assertExists($prefix); + //$this->assertExists($iri); + $prefixes[$prefix] = $iri; + }; + + // ### should return prefixes through a callback function + (new TriGParser())->parse('@prefix a: . a:a a:b a:c. @prefix b: .', $tripleCallback, $prefixCallback); + + + // ### should return prefixes through a callback without triple callback function (done) { + $prefixes = []; + $prefixCallback = function ($prefix, $iri) use (&$prefixes) { + $prefixes[$prefix] = $iri; + }; + (new TriGParser())->parse('@prefix a: . a:a a:b a:c. @prefix b: .', null, $prefixCallback); + + $this->assertEquals(2, sizeof(array_keys($prefixes))); + + + // ### should return prefixes at the last triple callback function (done) { + $tripleCallback = function ($error, $triple, $prefixes = null) use (&$prefixes) { + if (!isset($triple)) { + $this->assertEquals(2, sizeof(array_keys($prefixes))); + } + }; + (new TriGParser())->parse('@prefix a: . a:a a:b a:c. @prefix b: .', $tripleCallback); + + // ### should parse a string synchronously if no callback is given function () { + $triples = (new TriGParser())->parse('@prefix a: . a:a a:b a:c.'); + $this->assertEquals([["subject"=> 'urn:a:a', "predicate"=> 'urn:a:b', "object"=> 'urn:a:c', "graph"=> '']], $triples); + } + + public function testParsingChunks () + { + $count = 0; + $parser = new TriGParser([], function ($error, $triple) use (&$count) { + if (isset($triple)) { + $this->assertEquals($triple, ["subject"=>"http://ex.org/a","predicate"=>"http://ex.org/b","object"=>"http://ex.org/c", "graph" => ""]); + $count ++; + } else if (isset($error)) { + throw $error; + } + }); + $parser->parseChunk('@prefix a: . a:a a:b a:c.' . "\n"); + $parser->parseChunk('@prefix a: . a:a a:b a:c.' . "\n"); + $parser->parseChunk('@prefix a: . a:a a:b a:c.' . "\n"); + $this->assertEquals(3, $count); + } + + + public function testParsingWithLiteralNewline () + { + // ### With a newline + $count = 0; + $parser = new TriGParser([], function ($error, $triple) use (&$count) { + if (isset($triple)) { + $this->assertEquals($triple, ["subject"=>"http://ex.org/a","predicate"=>"http://ex.org/b","object"=>"\"\n\"", "graph" => ""]); + $count ++; + } else if (isset($error)) { + throw $error; + } + }); + $parser->parseChunk('@prefix a: . a:a a:b """' . "\n"); + $parser->parseChunk('""".'); + $this->assertEquals(1, $count); + } + + public function testException () + { + // ### should throw on syntax errors if no callback is given function () { + try { + (new TriGParser())->parse(' bar '); + } catch (\Exception $e) { + $this->assertEquals('Unexpected "bar" on line 1.', $e->getMessage()); } - } - - function prefixCallback(prefix, iri) { - expect(prefix).to.exist; - expect(iri).to.exist; - prefixes[prefix] = iri; - } - }); - - // ### should return prefixes through a callback without triple callback function (done) { - $prefixes = {}; - new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', - null, prefixCallback); - - function prefixCallback(prefix, iri) { - expect(prefix).to.exist; - expect(iri).to.exist; - prefixes[prefix] = iri; - if (Object.keys(prefixes).length === 2) - done(); - } - }); - - // ### should return prefixes at the last triple callback function (done) { - new N3Parser().parse('@prefix a: . a:a a:b a:c. @prefix b: .', tripleCallback); - - function tripleCallback($error, $triple, prefixes) { - expect(error).not.to.exist; - if (triple) - expect(prefixes).not.to.exist; - else { - expect(prefixes).to.exist; - Object.keys(prefixes).should.have.length(2); - expect(prefixes).to.have.property('a', 'IRIa'); - expect(prefixes).to.have.property('b', 'IRIb'); - done(); + + // ### should throw on grammar errors if no callback is given function () { + try { + (new TriGParser())->parse(' '); + } catch (\Exception $e) { + $this->assertEquals('Expected punctuation to follow "c" on line 1.', $e->getMessage()); } - } - }); - - // ### should parse a string synchronously if no callback is given function () { - $triples = new N3Parser().parse('@prefix a: . a:a a:b a:c.'); - triples.should.deep.equal([{ subject: 'urn:a:a', predicate: 'urn:a:b', object: 'urn:a:c', graph: '' }]); - }); - - // ### should throw on syntax errors if no callback is given function () { - (function () { new N3Parser().parse(' bar '); }) - .should.throw('Unexpected "bar" on line 1.'); - }); - - // ### should throw on grammar errors if no callback is given function () { - (function () { new N3Parser().parse(' '); }) - .should.throw('Expected punctuation to follow "c" on line 1.'); - }); - }); -*/ } - public function testParserWithIRI() { From ff8c706b72a143a228315bdf6156ae444df98f3d Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 17 Apr 2017 13:18:31 +0200 Subject: [PATCH 69/72] Fixed streaming input split at literal level --- src/N3Lexer.php | 10 ++++++---- src/TriGParser.php | 10 +++++----- test/TriGParserTest.php | 3 ++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/N3Lexer.php b/src/N3Lexer.php index 753a700..664b4c2 100644 --- a/src/N3Lexer.php +++ b/src/N3Lexer.php @@ -365,9 +365,9 @@ private function tokenizeToEnd($callback, $inputFinished) { // We could be in streaming mode, and then we just wait for more input to arrive. // Otherwise, a syntax error has occurred in the input. // One exception: error on an unaccounted linebreak (= not inside a triple-quoted literal). - if ($inputFinished || (!preg_match("/^'''|^\"\"\"/",$input) && preg_match("/\n|\r/",$input))) + if ($inputFinished || (!preg_match('/^\'\'\'|^"""/',$input) && preg_match('/\\n|\\r/',$input))) { return $reportSyntaxError($this); - else { + } else { $this->input = $input; return $input; } @@ -417,10 +417,12 @@ private function initTokenize() { $this->_tokenize = function ($input, $finalize) { // If the input is a string, continuously emit tokens through the callback until the end - $this->input = $input; + if (!isset($this->input)) + $this->input = ""; + $this->input .= $input; $tokens = []; $error = ""; - $this->tokenizeToEnd(function ($e, $t) use (&$tokens,&$error) { + $this->input = $this->tokenizeToEnd(function ($e, $t) use (&$tokens,&$error) { if (isset($e)) { $error = $e; } diff --git a/src/TriGParser.php b/src/TriGParser.php index 3da2d78..ccafcea 100644 --- a/src/TriGParser.php +++ b/src/TriGParser.php @@ -529,7 +529,7 @@ private function initReaders () return call_user_func($this->getContextEndReader); else { $this->readCallback = call_user_func($this->getContextEndReader); - return call_user_func($this->readCallback,$token); + return call_user_func($this->readCallback, $token); } }; @@ -591,9 +591,9 @@ private function initReaders () $predicate = $this->predicate; $object = $this->object; if (!$inversePredicate) - call_user_func($this->triple,$subject, $predicate, $object, $graph); + call_user_func($this->triple, $subject, $predicate, $object, $graph); else - call_user_func($this->triple,$object, $predicate, $subject, $graph); + call_user_func($this->triple, $object, $predicate, $subject, $graph); } return $next; }; @@ -834,7 +834,7 @@ private function initReaders () // ### `_triple` emits a triple through the callback $this->triple = function ($subject, $predicate, $object, $graph) { - call_user_func($this->callback,null, [ 'subject'=> $subject, 'predicate'=> $predicate, 'object'=> $object, 'graph'=> isset($graph)?$graph:'' ]); + call_user_func($this->callback, null, [ 'subject'=> $subject, 'predicate'=> $predicate, 'object'=> $object, 'graph'=> isset($graph)?$graph:'' ]); }; // ### `_error` emits an error message through the callback @@ -1027,7 +1027,7 @@ public function setPrefixCallback ($prefixCallback = null) } } - public function end() + public function end() { return $this->parseChunk("", true); } diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index ff82ab5..5783b19 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -820,12 +820,13 @@ public function testParsingWithLiteralNewline () if (isset($triple)) { $this->assertEquals($triple, ["subject"=>"http://ex.org/a","predicate"=>"http://ex.org/b","object"=>"\"\n\"", "graph" => ""]); $count ++; - } else if (isset($error)) { + } else if (!isset($triple) && isset($error)) { throw $error; } }); $parser->parseChunk('@prefix a: . a:a a:b """' . "\n"); $parser->parseChunk('""".'); + $parser->end(); $this->assertEquals(1, $count); } From cb445ec2d23911347f8c25b4f1631a4a12c28813 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 17 Apr 2017 18:12:31 +0200 Subject: [PATCH 70/72] Fixes tests --- test/TriGParserTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index 5783b19..c400224 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -782,7 +782,7 @@ public function testInterface() // ### should return prefixes at the last triple callback function (done) { - $tripleCallback = function ($error, $triple, $prefixes = null) use (&$prefixes) { + $tripleCallback = function ($error, $triple, null) use (&$prefixes) { if (!isset($triple)) { $this->assertEquals(2, sizeof(array_keys($prefixes))); } From 84d044d205f02550776b9506876039a9175df754 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 17 Apr 2017 18:12:51 +0200 Subject: [PATCH 71/72] Fixes tests --- test/TriGParserTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php index c400224..bc08f1d 100644 --- a/test/TriGParserTest.php +++ b/test/TriGParserTest.php @@ -782,7 +782,7 @@ public function testInterface() // ### should return prefixes at the last triple callback function (done) { - $tripleCallback = function ($error, $triple, null) use (&$prefixes) { + $tripleCallback = function ($error, $triple) use (&$prefixes) { if (!isset($triple)) { $this->assertEquals(2, sizeof(array_keys($prefixes))); } From db373e8f78a56f9630e5a6c97495740289223938 Mon Sep 17 00:00:00 2001 From: Pieter Colpaert Date: Mon, 17 Apr 2017 22:03:27 +0200 Subject: [PATCH 72/72] Added performance info in README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index b691984..ef7bdaf 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,23 @@ We also offer 2 simple tools in `bin/` as an example implementation: one validat curl -H "accept: application/trig" http://fragments.dbpedia.org/2015/en | php bin/validator.php trig curl -H "accept: application/trig" http://fragments.dbpedia.org/2015/en | php bin/convert.php trig n-triples ``` + +## Performance + +We compared the performance on two turtle files, and parsed it with the EasyRDF library in PHP, the N3.js library for NodeJS and with Hardf. These were the results: + +| #triples | framework | time (ms) | memory (MB) | +|----------:|-------------------------|------:|--------:| +|1,866 | __Hardf__ without opcache | 27.6 | 0.722 | +|1,866 | __Hardf__ with opcache | 24.5 | 0.380 | +|1,866 | [EasyRDF](https://github.com/njh/easyrdf) without opcache | 5,166.5 | 2.772 | +|1,866 | [EasyRDF](https://github.com/njh/easyrdf) with opcache | 5,176.2 | 2.421 | +| 1,866 | [N3.js](https://github.com/RubenVerborgh/N3.js) | 24.0 | 28.xxx | +| 3,896,560 | __Hardf__ without opcache | 40,017.7 | 0.722 | +| 3,896,560 | __Hardf__ with opcache | 33,155.3 | 0.380 | +| 3,896,560 | [N3.js](https://github.com/RubenVerborgh/N3.js) | 7,004.0 | 59.xxx | + + ## License, status and contributions The N3.js library is copyrighted by [Ruben Verborgh](http://ruben.verborgh.org/) and [Pieter Colpaert](https://pietercolpaert.be) and released under the [MIT License](https://github.com/RubenVerborgh/N3.js/blob/master/LICENSE.md).