diff --git a/.travis.yml b/.travis.yml index 470c926..27490ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: php php: - - "7.0" + - '5.6' + - '7.0' + - '7.1' + - nightly sudo: false env: before_script: diff --git a/README.md b/README.md index 8ff63f2..ef7bdaf 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 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/) -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 @@ -30,7 +28,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 +41,182 @@ 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. +### Writing +```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#" + ], + "format" => "n-quads" //Other possible values: n-quads, trig or turtle +]); + +$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 +//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->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, 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/) + +#### 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; +$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(); +``` + +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 (!isset($e) && isset($triple)) { + $writer->addTriple($triple); + 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; +}); +``` + +#### 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 +$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 +```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 +``` + +## 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 | + -See the documentation at https://github.com/RubenVerborgh/N3.js#utility. Instead of N3Util, you will have to use `pietercolpaert\hardf::Util`. +## 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. diff --git a/bin/convert.php b/bin/convert.php new file mode 100644 index 0000000..7c5b2fc --- /dev/null +++ b/bin/convert.php @@ -0,0 +1,30 @@ +#!/usr/bin/php + $outformat]); +$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); + echo $writer->read(); + } else { + fwrite(STDERR, $error->getMessage() . "\n"); + } +}); + +while ($line = fgets(STDIN)) { + $parser->parseChunk($line); +} +$parser->end(); diff --git a/bin/validator.php b/bin/validator.php new file mode 100644 index 0000000..63e75ec --- /dev/null +++ b/bin/validator.php @@ -0,0 +1,32 @@ +#!/usr/bin/php + $format]); +$errored = false; +$finished = false; +$tripleCount = 0; +$line = true; +while (!$finished && $line) { + try { + $line = fgets(STDIN); + if ($line) + $tripleCount += sizeof($parser->parseChunk($line)); + else { + $tripleCount += sizeof($parser->end()); + $finished = true; + } + } catch (\Exception $e) { + echo $e->getMessage() . "\n"; + $errored = true; + } +} +if (!$errored) { + echo "Parsed " . $tripleCount . " triples successfully.\n"; +} + diff --git a/examples/parseAndWrite.php b/examples/parseAndWrite.php new file mode 100644 index 0000000..4e3d546 --- /dev/null +++ b/examples/parseAndWrite.php @@ -0,0 +1,27 @@ +"trig"]); +$triples = $parser->parse("() . \"\"\"\n\"\"\"."); +$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("@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/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(); 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; +} diff --git a/src/N3Lexer.php b/src/N3Lexer.php new file mode 100644 index 0000000..664b4c2 --- /dev/null +++ b/src/N3Lexer.php @@ -0,0 +1,460 @@ +\\"\{\}\|\^\`]/'; + + private $input; + private $line = 1; + + private $prevTokenType; + + 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 + $this->tripleQuotedString = '/$0^/'; + $this->number = '/$0^/'; + $this->boolean = '/$0^/'; + // Swap the tokenize method for a restricted version + $this->_oldTokenize = $this->_tokenize; + $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 + $this->n3Mode = $options["n3"] !== false; + + // Disable comment tokens by default + $this->comments = isset($options["comments"])?$options["comments"]:null; + } + + // ## Regular expressions + //_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 + // _singleQuotedString: /^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^'[^'\\]*(?:\\.[^'\\]*)*'(?=[^'\\])/, + private $singleQuotedString= '/^"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(?=[^"\\\\])|^\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'(?=[^\'\\\\])/'; + // _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])*)?:((?:(?:[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 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) { + preg_match("/^\S*/", $input, $match); + $callback($self->syntaxError($match[0], $self->line), null); + }; + + $outputComments = $this->comments; + while (true) { + // 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($this->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('/^\'|\'$/', '"',$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 += sizeof(preg_split('/\r\n|\r|\n/',$unescaped)) - 1; + $unescaped = $this->unescape($unescaped); + if ($unescaped === null) + return $reportSyntaxError($this); + $type = 'literal'; + $value = preg_replace("/^'|'$/", '"',$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#' . (isset($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 ($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); + return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES'); + } + else if ($unicode8) { + $charCode = intval($unicode8, 16); + return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES'); + } + else { + if (!isset($this->escapeReplacements[$escapedChar])) + throw new \Exception(); + return $this->escapeReplacements[$escapedChar]; + } + },$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 . '.'); + } + + // 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 + if (!isset($this->input)) + $this->input = ""; + $this->input .= $input; + $tokens = []; + $error = ""; + $this->input = $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) { + 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 + public function tokenizeChunk($input) + { + return $this->tokenize($input, false); + } + + public function end() + { + // Parses the rest + return $this->tokenizeToEnd(true); + } +} + 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 @@ +setTripleCallback($tripleCallback); + $this->setPrefixCallback($prefixCallback); + $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); + + // 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")!==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; + $this->supportsQuads = !($isTurtle || $isTriG || $isNTriples || $isN3); + // Disable relative IRIs in N-Triples or N-Quads mode + if ($isLineMode) { + $this->base = ''; + $this->resolveIRI = function ($token) { + call_user_func($this->error, 'Disallowed relative IRI', $token); + return $this->callback = function () {}; + $this->subject = null; + }; + } + $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 + $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 = []; + + } + + // ## Private class methods + // ### `_resetBlankNodeIds` restarts blank node identification + public function _resetBlankNodeIds () { + $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 !== false) + $baseIRI = substr($baseIRI,0, $fragmentPos); + // Set base IRI and its components + $this->base = $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]:''; + } + } + + // ### `_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->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 + ]); + // 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 = $this->quantified; + } + } + + // ### `_restoreContext` restores the parent context + // when leaving a scope (list, blank node, formula) + private function restoreContext() { + $context = array_pop($this->contextStack); + $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 call_user_func($this->error,'Unclosed graph', $token); + unset($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; + 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"] : call_user_func($this->resolveIRI,$token); + break; + // Read a blank node or prefixed name + case 'type': + case 'blank': + case 'prefixed': + 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 + case 'var': + return $token["value"]; + // Everything else is not an entity + default: + 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) && $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 call_user_func($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 call_user_func($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 ($this->n3Mode) + return call_user_func($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 call_user_func($this->error,'Unexpected ' . $type, $token); + $this->subject = null; + 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) + 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) + 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 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; + 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 ($this->n3Mode) + return call_user_func($this->getPathReader,call_user_func($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 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; + return $this->readSubject; + }; + + // ### `_readBlankNodeHead` reads the head of a blank node + $this->readBlankNodeHead = function ($token) { + if ($token["type"] === ']') { + $this->subject = null; + return call_user_func($this->readBlankNodeTail,$token); + } + else { + $this->predicate = null; + return call_user_func($this->readPredicate,$token); + } + }; + + // ### `_readBlankNodeTail` reads the end of a blank node + $this->readBlankNodeTail = function ($token) { + if ($token["type"] !== ']') + return call_user_func($this->readBlankNodePunctuation,$token); + + // Store blank node triple + if ($this->subject !== null) + 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; + $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 call_user_func($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 call_user_func($this->readPunctuation, $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 + $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 + $list = '_:b' . $this->blankNodeCount++; + $item = '_:b' . $this->blankNodeCount++; + $this->subject = $item; + $this->saveContext('blank', $this->graph, $list, 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]["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 + $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 = call_user_func($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 = call_user_func($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 + 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')) { + // 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 call_user_func($this->getPathReader,$this->readListItem); + } + // Output the item if it is complete + if ($itemComplete) + call_user_func($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 call_user_func($this->completeLiteral,$token, false); + }; + + // ### `_readListItemDataTypeOrLang` reads an _optional_ data type or language in a list + $this->readListItemDataTypeOrLang = function ($token) { + return call_user_func($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 .= '^^' . call_user_func($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) + call_user_func($this->triple,$this->subject, self::RDF_FIRST, $this->object, $this->graph); + // Continue with the rest of the input + if ($suffix) + return call_user_func($this->getContextEndReader); + else { + $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 call_user_func($this->readPunctuation,$token); + + // Store the last triple of the formula + 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 !isset($this->object) ? $this->readPredicate : call_user_func($this->getContextEndReader); + }; + + // ### `_readPunctuation` reads punctuation between triples or triple parts + $this->readPunctuation = function ($token) { + $next; + $subject = isset($this->subject)?$this->subject:null; + $graph = $this->graph; + $inversePredicate = $this->inversePredicate; + switch ($token["type"]) { + // A closing brace ends a graph + case '}': + if ($this->graph === null) + return call_user_func($this->error,'Unexpected graph closing', $token); + if ($this->n3Mode) + return call_user_func($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 call_user_func($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 call_user_func($this->error,'Expected punctuation to follow "' . $this->object . '"', $token); + } + // A triple has been completed now, so return it + call_user_func($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 call_user_func($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 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); + return $this->readDeclarationPunctuation; + }; + + // ### `_readBaseIRI` reads the IRI of a base declaration + $this->readBaseIRI = function ($token) { + 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"] : call_user_func($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': + call_user_func($this->readSubject,$token); + return $this->readGraph; + case '[': + return $this->readNamedGraphBlankLabel; + default: + 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 call_user_func($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 call_user_func($this->readInTopContext,$token); + } + + if ($token["type"] !== '.') + return call_user_func($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 = call_user_func($this->readEntity, $token, true); + if (isset($entity)) + break; + default: + return call_user_func($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++; + call_user_func($this->triple,isset($this->graph)?$this->graph:'', $this->predicate, $this->subject, self::QUANTIFIERS_GRAPH); + } + // Otherwise, continue the previous list + else + call_user_func($this->triple,$this->subject, self::RDF_REST, + $this->subject = '_:b' . $this->blankNodeCount++, self::QUANTIFIERS_GRAPH); + // Output the list item + call_user_func($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) { + call_user_func($this->triple,$this->subject, self::RDF_REST, self::RDF_NIL, self::QUANTIFIERS_GRAPH); + $this->subject = null; + } + // Read a dot + $this->readCallback = call_user_func($this->getContextEndReader); + return call_user_func($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 = 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 + $item = $this->subject; + // Switch back to the context of the list + $this->restoreContext(); + // Output the list item + call_user_func($this->triple,$this->subject, self::RDF_FIRST, $item, $this->graph); + } + return call_user_func($this->afterPath,$token); + } + }; + + // ### `_readForwardPath` reads a '!' path + $this->readForwardPath = function ($token) { + $subject; $predicate; $object = '_:b' . $this->blankNodeCount++; + // The next token is the predicate + $predicate = call_user_func($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 + call_user_func($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 = call_user_func($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 + call_user_func($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) { + 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, + // 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]) { + // Resolve relative fragment IRIs against the base IRI + case '#': return $this->base . $iri; + // Resolve relative query string IRIs by replacing the query string + 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 '/': + // 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); + } + }; + + // ### `_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 = '/'; + + // 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){ + $size = strlen ($haystack); + $pos = strpos (strrev($haystack), $needle); + if ($pos === false) + return false; + return $size - $pos -1; + }; + + 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 (isset($iri[$i + 1]) && $iri[$i + 1] === '.') { + if (isset($iri[++$i + 1])) { + $next = $iri[$i + 1]; + } else + $next = null; + switch ($next) { + // Remove a '/.' segment + case '/': + if (($i - 1 - $segmentStart) > 0) + $result .= substr($iri, $segmentStart, $i - 1 - $segmentStart); + $segmentStart = $i + 1; + break; + // Remove a trailing '/.' segment + case null: + case '?': + case '#': + return $result . substr($iri, $segmentStart, $i - $segmentStart) . substr($iri,$i + 1); + // Remove a '/..' segment + case '.': + if (isset($iri[++$i + 1])) { + $next = $iri[$i + 1]; + } else { + $next = null; + } + if ($next === null || $next === '/' || $next === '?' || $next === '#') { + 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); + } + // Remove a trailing '/..' segment + if ($next !== '/') + return $result . '/' . substr($iri,$i + 1); + $segmentStart = $i + 1; + } + } + } + } + if (++$i < $length) { + $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) { + $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) { + if (!$e && $t) { + array_push($triples,$t); + } else if (!$e) { + //DONE + } else { + $error = $e; + } + }; + $tokens = $this->lexer->tokenize($input, $finalize); + foreach($tokens as $token) { + if (isset($this->readCallback)) + $this->readCallback = call_user_func($this->readCallback, $token); + } + if ($error) throw $error; + 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 () {}; + } + } + } + + public function setTripleCallback ($tripleCallback = null) + { + $this->tripleCallback = $tripleCallback; + } + + public function setPrefixCallback ($prefixCallback = null) + { + 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 new file mode 100644 index 0000000..0132f94 --- /dev/null +++ b/src/TriGWriter.php @@ -0,0 +1,356 @@ +setReadCallback($readCallback); + $this->ESCAPEREPLACEMENTS = [ + '\\' => '\\\\', '"' => '\\"', "\t" => "\\t", + "\n" => '\\n', "\r" => "\\r", chr(8) => "\\b", "\f"=> "\\f" + ]; + $this->initWriter (); + /* 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"]); + } + } else { + $this->writeTriple = $this->writeTripleLine; + } + + $this->characterReplacer = function ($character) { + // Replace a single character by its escaped version + $character = $character[0]; + if (strlen($character) > 0 && isset($this->ESCAPEREPLACEMENTS[$character[0]])) { + return $this->ESCAPEREPLACEMENTS[$character[0]]; + } else { + return $result; //no escaping necessary, should not happen, or something is wrong in our regex + } + }; + } + + public function setReadCallback($readCallback) + { + $this->readCallback = $readCallback; + } + + + 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) { + if ($this->blocked) { + throw new \Exception('Cannot write because the writer has been closed.'); + } else { + if (isset($this->readCallback)) { + call_user_func($this->readCallback, $string); + } else { + //buffer all + $this->string .= $string; + } + } + } + + // ### Reads a bit of the string + public function read () + { + $string = $this->string; + $this->string = ""; + return $string; + } + + // ### `_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::ESCAPE, $this->characterReplacer,$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) && isset($this->prefixIRIs) && in_array($match[1], $this->prefixIRIs)) { + return $entity; + } else { + 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::ESCAPE, $this->characterReplacer,$value); + $value = $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; + 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)) + call_user_func($this->writeTriple, $subject, $predicate, $object, '', $graph); + // The `graph` parameter was provided + else + call_user_func($this->writeTriple, $subject, $predicate, $object, $graph, $done); + } + + // ### `addTriples` adds the triples to the output stream + public function addTriples ($triples) { + for ($i = 0; $i < sizeof($triples); $i++) + $this->addTriple($triples[$i]); + } + + // ### `addPrefix` adds the prefix to the output stream + public function addPrefix($prefix, $iri, $done = null) + { + $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 + $hasPrefixes = false; + foreach ($prefixes as $prefix => $iri) { + + // 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 . ":"; + // 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 ($this->prefixIRIs as $prefixIRI => $iri) { + $IRIlist .= $IRIlist ? '|' . $prefixIRI : $prefixIRI; + $prefixList .= ($prefixList ? '|' : '') . $iri; + } + $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); + } + + // ### `blank` creates a blank node with the given content + 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) && isset($predicate["predicate"])) + $children = [$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]"; + } + } + + // ### `list` creates a list node with the given content + public function addList ($elements = null) { + $length = 0; + if (isset($elements)) { + $length = sizeof($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() + { + // 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 (!isset($this->readCallback)) + return $this->string; + } +} \ No newline at end of file diff --git a/test/TriGParserTest.php b/test/TriGParserTest.php new file mode 100644 index 0000000..bc08f1d --- /dev/null +++ b/test/TriGParserTest.php @@ -0,0 +1,2057 @@ +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 + $this->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 + $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" . + ':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 + $this->shouldNotParse(':a ', + 'Undefined prefix ":" on line 1.'); + + // ### should not parse undefined prefix in subject + $this->shouldNotParse('a:a ', + 'Undefined prefix "a:" on line 1.'); + + // ### should not parse undefined prefix in predicate + $this->shouldNotParse(' b:c .', + 'Undefined prefix "b:" on line 1.'); + + // ### should not parse undefined prefix in object + $this->shouldNotParse(' c:d .', + 'Undefined prefix "c:" on line 1.'); + + // ### should not parse undefined prefix in datatype + $this->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 + $this->shouldNotParse('@prefix ', + 'Expected prefix to follow @prefix on line 1.'); + + // ### should not parse prefix declarations without IRI + $this->shouldNotParse('@prefix : .', + 'Expected IRI to follow prefix ":" on line 1.'); + + // ### should not parse prefix declarations without a dot + $this->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']); + + } + + public function testBlankNodes () + { + // ### 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 + $this->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 + $this->shouldNotParse(' [].', + 'Expected entity but got ] on line 1.'); + + // ### should not parse a blank node with only a semicolon + $this->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']); + + // ### 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']); + + // ### 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: .'."\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: .'."\n".'g: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: .'."\n".'GRAPH 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: .'."\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 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 () + { + // ### should parse a graph with 8-bit unicode escape sequences + $this->shouldParse('<\\U0001d400> {'."\n".'<\\U0001d400> <\\U0001d400> "\\U0001d400"^^<\\U0001d400>'."\n".'}' . "\n", + ['𝐀', '𝐀', '"𝐀"^^𝐀', '𝐀']); + } + + public function testParseErrors () + { + // ### 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: .'."\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 + $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.'); + } + + public function testInterface() + { + $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) 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($triple) && isset($error)) { + throw $error; + } + }); + $parser->parseChunk('@prefix a: . a:a a:b """' . "\n"); + $parser->parseChunk('""".'); + $parser->end(); + $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()); + } + + // ### 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()); + } + } + + 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']); + + } + + 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']); + } + + 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']); + + } + + 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.'); + + } + + 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 var 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 var 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 () { + //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 = "") + { + + $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)); + }); + } + + 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); + $errorReceived = true; + } + }); + } + + 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) { $this->fail("Resolving <$relativeIri> against <$baseIri>.\nError message: " . $error->getMessage()); } + } + + private static function toSortedJSON ($items) + { + $triples = array_map("json_encode", $items); + sort($triples); + return "[\n " . join("\n ", $triples) . "\n]"; + } +} \ No newline at end of file diff --git a/test/TriGWriterTest.php b/test/TriGWriterTest.php new file mode 100644 index 0000000..519b610 --- /dev/null +++ b/test/TriGWriterTest.php @@ -0,0 +1,557 @@ +shouldSerialize(''); + //should serialize 1 triple', + $this->shouldSerialize(['abc', 'def', 'ghi'], + ' .' . "\n"); + + //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"); + } + + public function testLiterals() + { + + //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 backslash' + $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', + /* shouldSerialize(['a', 'b', '"c\nde"'], + ' "c\\nde".\n'));*/ + $this->shouldSerialize(['a', 'b', '"c' . "\n" . 'de"'], + ' "c\\nde".' . "\n"); + + //should serialize a literal containing a cariage return character', + $this->shouldSerialize(['a', 'b', '"c' . "\r" . 'de"'], + ' "c\\rde".' ."\n"); + + //should serialize a literal containing a backspace character', + $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"'], + ' "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\""], + //' "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"); + + //should serialize a literal containing special unicode characters', + //$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', + $this->shouldNotSerialize(['"a"', 'b', '"c"'], + 'A literal as subject is not allowed: "a"'); + + //should not serialize a literal in the predicate', + $this->shouldNotSerialize(['a', '"b"', '"c"'], + 'A literal as predicate is not allowed: "b"'); + + //should not serialize an invalid object literal', + $this->shouldNotSerialize(['a', 'b', '"c'], + 'Invalid literal: "c'); + } + + public function testPrefixes () + { + + //should not leave leading whitespace if the prefix set is empty', + $this->shouldSerialize(["prefixes" => []], + ['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"); + } + + public function testRepitition () + { + //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"); + } + + 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'], + ' {' . "\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', +/* 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(["_:\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); + }); + } + + 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\na:a a:b a:c.\n",$output); + }); + } + + 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) { + $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 = 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 = 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 = 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) { + $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); + + }); + + //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); + + }); + + //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); + + }); + + //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); + }); + + //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); + + }); + + //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); + + }); + + //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); + }); + } + + public function testLists () + { + //should serialize triples with an empty list as object', function (done) { + $writer = new TriGWriter(); + $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->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->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->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->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->addList(['a', '"b"', '"c"']), 'd', 'e'); + $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->addList(['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) { + $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); + + }); + } + + 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(); +$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.'); +}); + + + + //should not write an invalid literal in N-Triples mode', function (done) { +$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'); +}); + + //should write simple quads in N-Quads mode', function (done) { +$writer = new TriGWriter({'format' => 'N-Quads' }); +$writer->addTriple('a', 'b', 'c'); +$writer->addTriple('a', 'b', 'd', 'g'); +$writer->end(function ($error, $output) { + $this->assertEquals(' .' . "\n" . ' .' . "\n",$output); + +}); + + //should not write an invalid literal in N-Quads mode', function (done) { +$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'); +}); + + //should end when the end option is not set', function (done) { +$outputStream = new QuickStream(), writer = new TriGWriter(outputStream, {}); +outputStream.should.have.property('ended', false); +$writer->end(function () { + outputStream.should.have.property('ended', true); +}); + + //should end when the end option is set to true', function (done) { +$outputStream = new QuickStream(), writer = new TriGWriter(outputStream, { end: true }); +outputStream.should.have.property('ended', false); +$writer->end(function () { + outputStream.should.have.property('ended', true); +}); + + //should not end when the end option is set to false', function (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); +}); +*/ + + + /** + **/ + 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() { + //TODO + } + + +} +