diff --git a/ArchieML.php b/ArchieML.php new file mode 100644 index 0000000..f557b22 --- /dev/null +++ b/ArchieML.php @@ -0,0 +1,270 @@ +data = new ArrayObject(); + $this->scope = $this->data; + $this->bufferScope = null; + $this->bufferKey = null; + $this->bufferString = ''; + $this->isSkipping = false; + $this->doneParsing = false; + + $this->flushScope(); + } + + public static function load($stream) + { + $self = new self(); + return $self->parse($stream); + } + + public function parse($stream) + { + foreach (preg_split('/(\n)/', $stream) as $line) { + $line = "$line\n"; + + if ($this->doneParsing) { + return $this->data; + } + + if (preg_match(self::COMMAND_KEY, $line, $match)) { + $this->parseCommandKey(strtolower($match[1])); + + } elseif (!$this->isSkipping && (preg_match(self::START_KEY, $line, $match)) && (is_null($this->array) || $this->arrayType != 'simple')) { + $this->parseStartKey($match[1], isset($match[2]) ? $match[2] : ''); + + } elseif (!$this->isSkipping && (preg_match(self::ARRAY_ELEMENT, $line, $match)) && (!is_null($this->array) && $this->arrayType != 'complex')) { + $this->parseArrayElement($match[1]); + + } elseif (!$this->isSkipping && (preg_match(self::SCOPE_PATTERN, $line, $match))) { + $this->parseScope($match[1], $match[2]); + + } else { + $this->bufferString .= $line; + } + } + + $this->flushBuffer(); + return $this->toArray(); + } + + protected function parseStartKey($key, $rest_of_line) + { + $this->flushBuffer(); + + if (!is_null($this->array)) { + $this->arrayType = !is_null($this->arrayType) ? $this->arrayType : 'complex'; + + # Ignore complex keys inside simple arrays + if ($this->arrayType == 'simple') { + return; + } + + if (in_array($this->arrayFirstKey, [null, $key])) { + $this->array[] = $this->scope = new ArrayObject(); + } + + $this->arrayFirstKey = !is_null($this->arrayFirstKey) ? $this->arrayFirstKey : $key; + } + + $this->bufferKey = $key; + $this->bufferString = $rest_of_line; + + $this->flushBufferInto($key, ['replace' => true]); + } + + protected function parseArrayElement($value) + { + $this->flushBuffer(); + + $this->arrayType = !is_null($this->arrayType) ? $this->arrayType : 'simple'; + + # Ignore simple array elements inside complex arrays + if ($this->arrayType == 'complex') { + return; + } + + $this->array[] = ''; + + $this->bufferKey = $this->array; + $this->bufferString = $value; + $this->flushBufferInto($this->array, ['replace' => true]); + } + + protected function parseCommandKey($command) + { + if ($this->isSkipping && !in_array($command, ['endskip', 'ignore'])) { + return $this->flushBuffer(); + } + + switch ($command) { + case 'end': + if ($this->bufferKey) { + $this->flushBufferInto($this->bufferKey, ['replace' => false]); + } + return; + + case 'ignore': + return $this->doneParsing = true; + + case 'skip': + $this->isSkipping = true; + break; + + case 'endskip': + $this->isSkipping = false; + } + } + + protected function parseScope($scopeType, $scopeKey) + { + $this->flushBuffer(); + $this->flushScope(); + + if ($scopeKey == '') { + $this->scope = $this->data; + } + + elseif (in_array($scopeType, ['[', '{'])) { + $keyScope = $this->data; + $keyBits = explode('.', $scopeKey); + $lastBitIndex = count($keyBits) - 1; + $lastBit = $keyBits[$lastBitIndex]; + + for ($i = 0; $i < $lastBitIndex; $i++) { + $bit = $keyBits[$i]; + $keyScope[$bit] = isset($keyScope[$bit]) ? $keyScope[$bit] : new ArrayObject(); + $keyScope = $keyScope[$bit]; + } + + if ($scopeType == '[') { + if (empty($keyScope[$lastBit])) { + $keyScope[$lastBit] = new ArrayObject(); + } + $this->array = $keyScope[$lastBit]; + + if (is_string($this->array)) { + $keyScope[$lastBit] = new ArrayObject(); + $this->array = $keyScope[$lastBit]; + } + + if ($this->array->count() > 0) { + $this->arrayType = is_string($this->array[0]) ? 'simple' : 'complex'; + } + + } elseif ($scopeType == '{') { + if (empty($keyScope[$lastBit])) { + $keyScope[$lastBit] = new ArrayObject(); + } + $this->scope = $keyScope[$lastBit]; + } + } + } + + protected function flushBuffer() + { + $result = $this->bufferString; + $this->bufferString = ''; + return $result; + } + + protected function flushBufferInto($key, array $options) + { + $value = $this->flushBuffer(); + + if ($options['replace']) { + $value = preg_replace('/^\s*/', '', $this->formatValue($value, 'replace')); + preg_match('/\s*$/', $value, $match); + $this->bufferString = $match[0]; + } else { + $value = $this->formatValue($value, 'append'); + } + + if ($key instanceof ArrayObject) { + if ($options['replace']) { + $key[$key->count() - 1] = ''; + } + $key[$key->count() - 1] .= preg_replace('/\s*$/', '', $value); + + } else { + $keyBits = explode('.', $key); + $lastBit = count($keyBits) - 1; + $this->bufferScope = $this->scope; + + for ($i = 0; $i < $lastBit; $i++) { + $bit = $keyBits[$i]; + if (isset($this->bufferScope[$bit]) && is_string($this->bufferScope[$bit])) { # reset + $this->bufferScope[$bit] = new ArrayObject(); + } + if (!isset($this->bufferScope[$bit])) { + $this->bufferScope[$bit] = new ArrayObject(); + } + $this->bufferScope = $this->bufferScope[$bit]; + } + + if ($options['replace']) { + $this->bufferScope[$keyBits[$lastBit]] = ''; + } + if (!isset($this->bufferScope[$keyBits[$lastBit]])) { + $this->bufferScope[$keyBits[$lastBit]] = ''; + } + $this->bufferScope[$keyBits[$lastBit]] .= preg_replace('/\s*$/', '', $value); + } + } + + protected function flushScope() + { + $this->array = $this->arrayType = $this->arrayFirstKey = $this->bufferKey = null; + } + + /** + * type can be either :replace or :append. + * If it's :replace, then the string is assumed to be the first line of a + * value, and no escaping takes place. + * If we're appending to a multi-line string, escape special punctuation + * by prepending the line with a backslash. + * (:, [, {, *, \) surrounding the first token of any line. + */ + protected function formatValue($value, $type) + { + $value = preg_replace('/\[[^\[\]\n\r]*\](?!\])/', '', $value); // remove comments + $value = preg_replace('/\[\[([^\[\]\n\r]*)\]\]/', '[\1]', $value); # [[]] => [] + + if ($type == 'append') { + $value = preg_replace('/^(\s*)\\\\/m', '\1', $value); + } + + return $value; + } + + protected function toArray($array = null) + { + $result = []; + if (is_null($array)) { + $array = $this->data; + } + foreach ($array as $key => $value) { + $result[$key] = $value instanceof ArrayObject ? $this->toArray($value) : $value; + } + return $result; + } +} diff --git a/ArchieMLTest.php b/ArchieMLTest.php new file mode 100644 index 0000000..c343491 --- /dev/null +++ b/ArchieMLTest.php @@ -0,0 +1,235 @@ +assertSame('value', ArchieML::load("key:value")['key'], 'parses key value pairs'); + $this->assertSame('value', ArchieML::load("key:value")['key'], 'parses key value pairs'); + $this->assertSame('value', ArchieML::load(" key :value")['key'], 'ignores spaces on either side of the key'); + $this->assertSame('value', ArchieML::load("\t\tkey\t\t:value")['key'], 'ignores tabs on either side of the key'); + $this->assertSame('value', ArchieML::load("key: value ")['key'], 'ignores spaces on either side of the value'); + $this->assertSame('value', ArchieML::load("key:\t\tvalue\t\t")['key'], 'ignores tabs on either side of the value'); + $this->assertSame('newvalue', ArchieML::load("key:value\nkey:newvalue")['key'], 'dupliate keys are assigned the last given value'); + $this->assertSame(':value', ArchieML::load("key::value")['key'], 'allows non-letter characters at the start of values'); + + $this->assertSame(['key', 'Key'], array_keys(ArchieML::load("key:value\nKey:Value")), 'keys are case sensitive'); + $this->assertSame('value', ArchieML::load("other stuff\nkey:value\nother stuff")['key'], "non-keys don't affect parsing"); + } + + public function testValidKeys() + { + $this->assertSame(ArchieML::load("a-_1:value")['a-_1'], 'value', 'letters, numbers, dashes and underscores are valid key components'); + $this->assertSame(0, count(ArchieML::load("k ey:value")), 'spaces are not allowed in keys'); + $this->assertSame(0, count(ArchieML::load("k&ey:value")), 'symbols are not allowed in keys'); + $this->assertSame('value', ArchieML::load("scope.key:value")['scope']['key'], 'keys can be nested using dot-notation'); + $this->assertSame('value', ArchieML::load("scope.key:value\nscope.otherkey:value")['scope']['key'], "earlier keys within scopes aren't deleted when using dot-notation"); + $this->assertSame('value', ArchieML::load("scope.level:value\nscope.level.level:value")['scope']['level']['level'], 'the value of key that used to be a string object should be replaced with an object if necessary'); + $this->assertSame('value', ArchieML::load("scope.level.level:value\nscope.level:value")['scope']['level'], 'the value of key that used to be a parent object should be replaced with a string if necessary'); + } + + public function testValidValues() + { + $this->assertSame('value', ArchieML::load("key:value")['key'], 'HTML is allowed'); + } + + public function testSkip() + { + $this->assertSame(0, count(ArchieML::load(" :skip \nkey:value\n:endskip")), 'ignores spaces on either side of :skip'); + $this->assertSame(0, count(ArchieML::load("\t\t:skip\t\t\nkey:value\n:endskip")), 'ignores tabs on either side of :skip'); + $this->assertSame(0, count(ArchieML::load(":skip\nkey:value\n :endskip ")), 'ignores spaces on either side of :endskip'); + $this->assertSame(0, count(ArchieML::load(":skip\nkey:value\n\t\t:endskip\t\t")), 'ignores tabs on either side of :endskip'); + + $this->assertSame(1, count(ArchieML::load(":skip\n:endskip\nkey:value")), 'starts parsing again after :endskip'); + $this->assertSame(0, count(ArchieML::load(":sKiP\nkey:value\n:eNdSkIp")), ':skip and :endskip are case insensitive'); + + $this->assertSame(0, count(ArchieML::load(":skipthis\nkey:value\n:endskip")), "parse :skip as a special command even if more is appended to word"); + $this->assertSame(0, count(ArchieML::load(":skip this text \nkey:value\n:endskip")), 'ignores all content on line after :skip + space'); + $this->assertSame(0, count(ArchieML::load(":skip\tthis text\t\t\nkey:value\n:endskip")), 'ignores all content on line after :skip + tab'); + + $this->assertSame(1, count(ArchieML::load(":skip\n:endskiptheabove\nkey:value")), "parse :endskip as a special command even if more is appended to word"); + $this->assertSame(1, count(ArchieML::load(":skip\n:endskip the above\nkey:value")), 'ignores all content on line after :endskip + space'); + $this->assertSame(1, count(ArchieML::load(":skip\n:endskip\tthe above\nkey:value")), 'ignores all content on line after :endskip + tab'); + $this->assertSame(0, count(ArchieML::load(":skip\n:end\tthe above\nkey:value")), 'does not parse :end as an :endskip'); + + $this->assertSame(['key1', 'key2'], array_keys(ArchieML::load("key1:value1\n:skip\nother:value\n\n:endskip\n\nkey2:value2")), 'ignores keys within a skip block'); + } + + public function testIgnore() + { + $this->assertSame('value', ArchieML::load("key:value\n:ignore")['key'], "text before ':ignore' should be included"); + $this->assertFalse(ArchieML::load(":ignore\nkey:value")->offsetExists('key'), "text after ':ignore' should be ignored"); + $this->assertFalse(ArchieML::load(":iGnOrE\nkey:value")->offsetExists('key'), "':ignore' is case insensitive"); + $this->assertFalse(ArchieML::load(" :ignore \nkey:value")->offsetExists('key'), "ignores spaces on either side of :ignore"); + $this->assertFalse(ArchieML::load("\t\t:ignore\t\t\nkey:value")->offsetExists('key'), "ignores tabs on either side of :ignore"); + $this->assertFalse(ArchieML::load(":ignorethis\nkey:value")->offsetExists('key'), "parses :ignore as a special command even if more is appended to word"); + $this->assertFalse(ArchieML::load(":ignore the below\nkey:value")->offsetExists('key'), "ignores all content on line after :ignore + space"); + $this->assertFalse(ArchieML::load(":ignore\tthe below\nkey:value")->offsetExists('key'), "ignores all content on line after :ignore + tab"); + } + + public function testMultiLineValues() + { + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n:end")['key'], 'adds additional lines to value if followed by an \':end\''); + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n:EnD")['key'], '\':end\' is case insensitive'); + $this->assertSame("value\n\n\t \nextra", ArchieML::load("key:value\n\n\t \nextra\n:end")['key'], 'preserves blank lines and whitespace lines in the middle of content'); + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\t \n:end")['key'], "doesn't preserve whitespace at the end of the key"); + $this->assertSame("value\t \nextra", ArchieML::load("key:value\t \nextra\n:end")['key'], 'preserves whitespace at the end of the original line'); + + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n \n\t\n:end")['key'], 'ignores whitespace and newlines before the \':end\''); + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n :end ")['key'], 'ignores spaces on either side of :end'); + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n\t\t:end\t\t")['key'], 'ignores tabs on either side of :end'); + + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n:endthis")['key'], "parses :end as a special command even if more is appended to word"); + $this->assertSame("value", ArchieML::load("key:value\nextra\n:endskip")['key'], "does not parse :endskip as an :end"); + $this->assertSame("value\n:notacommand", ArchieML::load("key:value\n:notacommand\n:end")['key'], "ordinary text that starts with a colon is included"); + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n:end this")['key'], "ignores all content on line after :end + space"); + $this->assertSame("value\nextra", ArchieML::load("key:value\nextra\n:end\tthis")['key'], "ignores all content on line after :end + tab"); + + $this->assertSame(":value", ArchieML::load("key::value\n:end")['key'], "doesn't escape colons on first line"); + $this->assertSame("\\:value", ArchieML::load("key:\\:value\n:end")['key'], "doesn't escape colons on first line"); + $this->assertSame("value\nkey2\\:value", ArchieML::load("key:value\nkey2\\:value\n:end")['key'], 'does not allow escaping keys'); + $this->assertSame("value\nkey2:value", ArchieML::load("key:value\n\\key2:value\n:end")['key'], 'allows escaping key lines with a leading backslash'); + $this->assertSame("value\n:end", ArchieML::load("key:value\n\\:end\n:end")['key'], 'allows escaping commands at the beginning of lines'); + $this->assertSame("value\n:endthis", ArchieML::load("key:value\n\\:endthis\n:end")['key'], 'allows escaping commands with extra text at the beginning of lines'); + $this->assertSame("value\n:notacommand", ArchieML::load("key:value\n\\:notacommand\n:end")['key'], 'allows escaping of non-commands at the beginning of lines'); + + $this->assertSame("value\n* value", ArchieML::load("key:value\n* value\n:end")['key'], 'allows simple array style lines'); + $this->assertSame("value\n* value", ArchieML::load("key:value\n\\* value\n:end")['key'], 'escapes "*" within multi-line values when not in a simple array'); + + $this->assertSame("value\n{scope}", ArchieML::load("key:value\n\\{scope}\n:end")['key'], 'allows escaping {scopes} at the beginning of lines'); + $this->assertSame("value", ArchieML::load("key:value\n\\[comment]\n:end")['key'], 'allows escaping [comments] at the beginning of lines'); + $this->assertSame("value\n[array]", ArchieML::load("key:value\n\\[[array]]\n:end")['key'], 'allows escaping [[arrays]] at the beginning of lines'); + + $this->assertSame("value", ArchieML::load("key:value\ntext\n[array]\nmore text\n:end")['key'], 'arrays within a multi-line value breaks up the value'); + $this->assertSame("value", ArchieML::load("key:value\ntext\n{scope}\nmore text\n:end")['key'], 'objects within a multi-line value breaks up the value'); + $this->assertSame("value\ntext\n* value\nmore text", ArchieML::load("key:value\ntext\n* value\nmore text\n:end")['key'], 'bullets within a multi-line value do not break up the value'); + $this->assertSame("value\ntext\nmore text", ArchieML::load("key:value\ntext\n:skip\n:endskip\nmore text\n:end")['key'], 'skips within a multi-line value do not break up the value'); + + $this->assertSame("value\n\\:end", ArchieML::load("key:value\n\\\\:end\n:end")['key'], 'allows escaping initial backslash at the beginning of lines'); + $this->assertSame("value\n\\\\:end", ArchieML::load("key:value\n\\\\\\:end\n:end")['key'], 'escapes only one initial backslash'); + + $this->assertSame("value\n:end\n:ignore\n:endskip\n:skip", ArchieML::load("key:value\n\\:end\n\\:ignore\n\\:endskip\n\\:skip\n:end")['key'], 'allows escaping multiple lines in a value'); + + $this->assertSame("value\nLorem key2\\:value", ArchieML::load("key:value\nLorem key2\\:value\n:end")['key'], "doesn't escape colons after beginning of lines"); + } + + public function testScopes() + { + $this->assertInternalType('array', ArchieML::load("{scope}")['scope'], '{scope} creates an empty object at "scope"'); + $this->assertTrue(array_key_exists('scope', ArchieML::load(" {scope} ")), 'ignores spaces on either side of {scope}'); + $this->assertTrue(array_key_exists('scope', ArchieML::load("\t\t{scope}\t\t")), 'ignores tabs on either side of {scope}'); + $this->assertTrue(array_key_exists('scope', ArchieML::load("{ scope }")), 'ignores spaces on either side of {scope} variable name'); + $this->assertTrue(array_key_exists('scope', ArchieML::load("{\t\tscope\t\t}")), 'ignores tabs on either side of {scope} variable name'); + $this->assertTrue(array_key_exists('scope', ArchieML::load("{scope}a")), 'ignores text after {scope}'); + + $this->assertSame('value', ArchieML::load("key:value\n{scope}")['key'], 'items before a {scope} are not namespaced'); + $this->assertSame('value', ArchieML::load("{scope}\nkey:value")['scope']['key'], 'items after a {scope} are namespaced'); + $this->assertSame('value', ArchieML::load("{scope.scope}\nkey:value")['scope']['scope']['key'], 'scopes can be nested using dot-notaion'); + $this->assertSame(2, count(ArchieML::load("{scope}\nkey:value\n{}\n{scope}\nother:value")['scope']), 'scopes can be reopened'); + $this->assertSame('value', ArchieML::load("{scope.scope}\nkey:value\n{scope.otherscope}key:value")['scope']['scope']['key'], 'scopes do not overwrite existing values'); + + $this->assertSame('value', ArchieML::load("{scope}\n{}\nkey:value")['key'], '{} resets to the global scope'); + $this->assertSame('value', ArchieML::load("{scope}\n{ }\nkey:value")['key'], 'ignore spaces inside {}'); + $this->assertSame('value', ArchieML::load("{scope}\n{\t\t}\nkey:value")['key'], 'ignore tabs inside {}'); + $this->assertSame('value', ArchieML::load("{scope}\n {} \nkey:value")['key'], 'ignore spaces on either side of {}'); + $this->assertSame('value', ArchieML::load("{scope}\n\t\t{}\t\t\nkey:value")['key'], 'ignore tabs on either side of {}'); + } + + public function testArrays() + { + $this->assertSame(0, count(ArchieML::load("[array]")['array']), '[array] creates an empty array at "array"'); + $this->assertTrue(array_key_exists('array', ArchieML::load(" [array] ")), 'ignores spaces on either side of [array]'); + $this->assertTrue(array_key_exists('array', ArchieML::load("\t\t[array]\t\t")), 'ignores tabs on either side of [array]'); + $this->assertTrue(array_key_exists('array', ArchieML::load("[ array ]")), 'ignores spaces on either side of [array] variable name'); + $this->assertTrue(array_key_exists('array', ArchieML::load("[\t\tarray\t\t]")), 'ignores tabs on either side of [array] variable name'); + $this->assertTrue(array_key_exists('array', ArchieML::load("[array]a")), 'ignores text after [array]'); + + $this->assertSame(0, count(ArchieML::load("[scope.array]")['scope']['array']), 'arrays can be nested using dot-notaion'); + + $this->assertSame([['scope' => ['key' => 'value']], ['scope' => ['key' => 'value']]], ArchieML::load("[array]\nscope.key: value\nscope.key: value")['array'], 'array values can be nested using dot-notaion'); + + $this->assertSame('value', ArchieML::load("[array]\n[]\nkey:value")['key'], '[] resets to the global scope'); + $this->assertSame('value', ArchieML::load("[array]\n[ ]\nkey:value")['key'], 'ignore spaces inside []'); + $this->assertSame('value', ArchieML::load("[array]\n[\t\t]\nkey:value")['key'], 'ignore tabs inside []'); + $this->assertSame('value', ArchieML::load("[array]\n [] \nkey:value")['key'], 'ignore spaces on either side of []'); + $this->assertSame('value', ArchieML::load("[array]\n\t\t[]\t\t\nkey:value")['key'], 'ignore tabs on either side of []'); + } + + public function testSimpleArrays() + { + $this->assertSame('Value', ArchieML::load("[array]\n*Value")['array'][0], 'creates a simple array when an \'*\' is encountered first'); + $this->assertSame('Value', ArchieML::load("[array]\n * Value")['array'][0], 'ignores spaces on either side of \'*\''); + $this->assertSame('Value', ArchieML::load("[array]\n\t\t*\t\tValue")['array'][0], 'ignores tabs on either side of \'*\''); + $this->assertSame(2, count(ArchieML::load("[array]\n*Value1\n*Value2")['array']), 'adds multiple elements'); + $this->assertSame(["Value1", "Value2"], ArchieML::load("[array]\n*Value1\nNon-element\n*Value2")['array'], 'ignores all other text between elements'); + $this->assertSame(["Value1", "Value2"], ArchieML::load("[array]\n*Value1\nkey:value\n*Value2")['array'], 'ignores key:value pairs between elements'); + $this->assertSame("value", ArchieML::load("[array]\n*Value1\n[]\nkey:value")['key'], 'parses key:values normally after an end-array'); + + $this->assertSame("Value1\nextra", ArchieML::load("[array]\n*Value1\nextra\n:end")['array'][0], 'multi-line values are allowed'); + $this->assertSame("Value1\n* extra", ArchieML::load("[array]\n*Value1\n\\* extra\n:end")['array'][0], 'allows escaping of "*" within multi-line values in simple arrays'); + $this->assertSame("Value1\n:end", ArchieML::load("[array]\n*Value1\n\\:end\n:end")['array'][0], 'allows escaping of command keys within multi-line values'); + $this->assertSame("Value1\nkey\\:value", ArchieML::load("[array]\n*Value1\nkey\\:value\n:end")['array'][0], 'does not allow escaping of keys within multi-line values'); + $this->assertSame("Value1\nkey:value", ArchieML::load("[array]\n*Value1\n\\key:value\n:end")['array'][0], 'allows escaping key lines with a leading backslash'); + $this->assertSame("Value1\nword key\\:value", ArchieML::load("[array]\n*Value1\nword key\\:value\n:end")['array'][0], 'does not allow escaping of colons not at the beginning of lines'); + + $this->assertSame('value', ArchieML::load("[array]\n* value\n[array]\nmore text\n:end")['array'][0], 'arrays within a multi-line value breaks up the value'); + $this->assertSame('value', ArchieML::load("[array]\n* value\n{scope}\nmore text\n:end")['array'][0], 'objects within a multi-line value breaks up the value'); + $this->assertSame("value\nkey: value\nmore text", ArchieML::load("[array]\n* value\nkey: value\nmore text\n:end")['array'][0], 'key/values within a multi-line value do not break up the value'); + $this->assertSame('value', ArchieML::load("[array]\n* value\n* value\nmore text\n:end")['array'][0], 'bullets within a multi-line value break up the value'); + $this->assertSame("value\nmore text", ArchieML::load("[array]\n* value\n:skip\n:endskip\nmore text\n:end")['array'][0], 'skips within a multi-line value do not break up the value'); + + $this->assertSame(2, count(ArchieML::load("[array]\n*Value\n[]\n[array]\n*Value")['array']), 'arrays that are reopened add to existing array'); + $this->assertSame(["Value"], ArchieML::load("[array]\n*Value\n[]\n[array]\nkey:value")['array'], 'simple arrays that are reopened remain simple'); + + $this->assertSame('simple value', ArchieML::load("a.b:complex value\n[a.b]\n*simple value")['a']['b'][0], 'simple ararys overwrite existing keys'); + } + + public function testComplexArrays() + { + $this->assertSame('value', ArchieML::load("[array]\nkey:value")['array'][0]['key'], 'keys after an [array] are included as items in the array'); + $this->assertSame('value', ArchieML::load("[array]\nkey:value\nsecond:value")['array'][0]['second'], 'array items can have multiple keys'); + $this->assertSame(2, count(ArchieML::load("[array]\nkey:value\nsecond:value\nkey:value")['array']), 'when a duplicate key is encountered, a new item in the array is started'); + $this->assertSame('second', ArchieML::load("[array]\nkey:first\nkey:second")['array'][1]['key'], 'when a duplicate key is encountered, a new item in the array is started'); + $this->assertSame('second', ArchieML::load("[array]\nscope.key:first\nscope.key:second")['array'][1]['scope']['key'], 'when a duplicate key is encountered, a new item in the array is started'); + + $this->assertSame(1, count(ArchieML::load("[array]\nkey:value\nscope.key:value")['array']), 'duplicate keys must match on dot-notation scope'); + $this->assertSame(1, count(ArchieML::load("[array]\nscope.key:value\nkey:value\notherscope.key:value")['array']), 'duplicate keys must match on dot-notation scope'); + + $this->assertSame('value', ArchieML::load("[array]\nkey:value\n[array]\nmore text\n:end")['array'][0]['key'], 'arrays within a multi-line value breaks up the value'); + $this->assertSame('value', ArchieML::load("[array]\nkey:value\n{scope}\nmore text\n:end")['array'][0]['key'], 'objects within a multi-line value breaks up the value'); + $this->assertSame('value', ArchieML::load("[array]\nkey:value\nother: value\nmore text\n:end")['array'][0]['key'], 'key/values within a multi-line value break up the value'); + $this->assertSame("value\n* value\nmore text", ArchieML::load("[array]\nkey:value\n* value\nmore text\n:end")['array'][0]['key'], 'bullets within a multi-line value do not break up the value'); + $this->assertSame("value\nmore text", ArchieML::load("[array]\nkey:value\n:skip\n:endskip\nmore text\n:end")['array'][0]['key'], 'skips within a multi-line value do not break up the value'); + + $this->assertSame(2, count(ArchieML::load("[array]\nkey:value\n[]\n[array]\nkey:value")['array']), 'arrays that are reopened add to existing array'); + $this->assertSame(["key" => "value"], ArchieML::load("[array]\nkey:value\n[]\n[array]\n*Value")['array'][0], 'complex arrays that are reopened remain complex'); + + $this->assertSame('value', ArchieML::load("a.b:complex value\n[a.b]\nkey:value")['a']['b'][0]['key'], 'complex arrays overwrite existing keys'); + } + + public function testInlineComments() + { + $this->assertSame('value value', ArchieML::load("key:value [inline comments] value")['key'], 'ignore comments inside of [single brackets]'); + $this->assertSame('value value value', ArchieML::load("key:value [inline comments] value [inline comments] value")['key'], 'supports multiple inline comments on a single line'); + $this->assertSame('value value', ArchieML::load("key:value [inline comments] [inline comments] value")['key'], 'supports adjacent comments'); + $this->assertSame('value value', ArchieML::load("key:value [inline comments][inline comments] value")['key'], 'supports no-space adjacent comments'); + $this->assertSame('value', ArchieML::load("key:[inline comments] value")['key'], 'supports comments at beginning of string'); + $this->assertSame('value', ArchieML::load("key:value [inline comments]")['key'], 'supports comments at end of string'); + $this->assertSame('value value', ArchieML::load("key:value [inline comments] value [inline comments]")['key'], 'whitespace before a comment that appears at end of line is ignored'); + + $this->assertSame('value ][ value', ArchieML::load("key:value ][ value")['key'], 'unmatched single brackets are preserved'); + $this->assertSame("value on\nmultiline", ArchieML::load("key:value [inline comments] on\nmultiline\n:end")['key'], 'inline comments are supported on the first of multi-line values'); + $this->assertSame("value\nmultiline", ArchieML::load("key:value\nmultiline [inline comments]\n:end")['key'], 'inline comments are supported on subsequent lines of multi-line values'); + + $this->assertSame("value \n multiline", ArchieML::load("key: [] value [] \n multiline [] \n:end")['key'], 'whitespace around comments is preserved, except at the beinning and end of a value'); + + $this->assertSame("value [inline\ncomments] value", ArchieML::load("key:value [inline\ncomments] value\n:end")['key'], 'inline comments cannot span multiple lines'); + $this->assertSame("value \n[inline\ncomments] value", ArchieML::load("key:value \n[inline\ncomments] value\n:end")['key'], 'inline comments cannot span multiple lines'); + + $this->assertSame("value [brackets] value", ArchieML::load("key:value [[brackets]] value")['key'], 'text inside [[double brackets]] is included as [single brackets]'); + $this->assertSame("value ]][[ value", ArchieML::load("key:value ]][[ value")['key'], 'unmatched double brackets are preserved'); + + $this->assertSame('Value', ArchieML::load("[array]\n*Val[comment]ue")['array'][0], 'comments work in simple arrays'); + $this->assertSame('Val[real]ue', ArchieML::load("[array]\n*Val[[real]]ue")['array'][0], 'double brackets work in simple arrays'); + } +} diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..007652f --- /dev/null +++ b/README.markdown @@ -0,0 +1,21 @@ + +# ArchieML + +Parse Archie Markup Language (ArchieML) documents into PHP arrays. + +Read about the ArchieML specification at [archieml.org](http://archieml.org). + +## Install + +run `composer require 4d47/archieml` + +## Usage + +```php +ArchieML::load("key: value"); // [ 'key' => 'value' ] +``` + +## Test + +run `composer test` + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9d5874c --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "4d47/archieml", + "description": "PHP parser for the Archie Markup Language", + "keywords": ["archie", "markup", "language", "archieml", "aml", "text", "parser"], + "license": "Apache-2.0", + "authors": [ + { + "name": "Mathieu Gagnon", + "email": "mathieu@gagnon.name" + } + ], + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "autoload": { + "psr-0": { "ArchieML": "" } + }, + "scripts": { + "test": "phpunit ArchieMLTest" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..ae2caf3 --- /dev/null +++ b/composer.lock @@ -0,0 +1,977 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "4dc342efbaa411dea0d22fc6b681bed1", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f976e5de371104877ebc89bd8fecb0019ed9c119", + "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "2.0.*@ALPHA" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Instantiator\\": "src" + } + }, + "notification-url": "http://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2014-10-13 12:58:55" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "http://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03 12:10:50" + }, + { + "name": "phpspec/prophecy", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "8724cd239f8ef4c046f55a3b18b4d91cc7f3e4c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8724cd239f8ef4c046f55a3b18b4d91cc7f3e4c5", + "reference": "8724cd239f8ef4c046f55a3b18b4d91cc7f3e4c5", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "phpdocumentor/reflection-docblock": "~2.0", + "sebastian/comparator": "~1.1" + }, + "require-dev": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2015-03-27 19:31:25" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.0.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "934fd03eb6840508231a7f73eb8940cf32c3b66c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/934fd03eb6840508231a7f73eb8940cf32c3b66c", + "reference": "934fd03eb6840508231a7f73eb8940cf32c3b66c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "~1.0", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-04-11 04:35:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a923bb15680d0089e2316f7a4af8f437046e96bb", + "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-04-02 05:19:05" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "Text/" + ] + }, + "notification-url": "http://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2014-01-30 17:20:04" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "http://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2013-08-02 07:42:54" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "eab81d02569310739373308137284e0158424330" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/eab81d02569310739373308137284e0158424330", + "reference": "eab81d02569310739373308137284e0158424330", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-04-08 04:46:07" + }, + { + "name": "phpunit/phpunit", + "version": "4.6.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "163232991e652e6efed2f8470326fffa61e848e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/163232991e652e6efed2f8470326fffa61e848e2", + "reference": "163232991e652e6efed2f8470326fffa61e848e2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "~1.3,>=1.3.1", + "phpunit/php-code-coverage": "~2.0,>=2.0.11", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "~1.0", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.2", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.6.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2015-04-11 05:23:21" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "74ffb87f527f24616f72460e54b595f508dccb5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/74ffb87f527f24616f72460e54b595f508dccb5c", + "reference": "74ffb87f527f24616f72460e54b595f508dccb5c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "~1.0,>=1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-04-02 05:36:41" + }, + { + "name": "sebastian/comparator", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "1dd8869519a225f7f2b9eb663e225298fade819e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dd8869519a225f7f2b9eb663e225298fade819e", + "reference": "1dd8869519a225f7f2b9eb663e225298fade819e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "http://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-01-29 16:28:08" + }, + { + "name": "sebastian/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "http://www.github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-02-22 15:13:53" + }, + { + "name": "sebastian/environment", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5a8c7d31914337b69923db26c4221b81ff5a196e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5a8c7d31914337b69923db26c4221b81ff5a196e", + "reference": "5a8c7d31914337b69923db26c4221b81ff5a196e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2015-01-01 10:01:08" + }, + { + "name": "sebastian/exporter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "84839970d05254c73cde183a721c7af13aede943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/84839970d05254c73cde183a721c7af13aede943", + "reference": "84839970d05254c73cde183a721c7af13aede943", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "http://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2015-01-27 07:23:06" + }, + { + "name": "sebastian/global-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "http://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2014-10-06 09:23:50" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "3989662bbb30a29d20d9faa04a846af79b276252" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/3989662bbb30a29d20d9faa04a846af79b276252", + "reference": "3989662bbb30a29d20d9faa04a846af79b276252", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "http://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-01-24 09:48:32" + }, + { + "name": "sebastian/version", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ab931d46cd0d3204a91e1b9a40c4bc13032b58e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ab931d46cd0d3204a91e1b9a40c4bc13032b58e4", + "reference": "ab931d46cd0d3204a91e1b9a40c4bc13032b58e4", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-02-24 06:35:25" + }, + { + "name": "symfony/yaml", + "version": "v2.6.6", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "174f009ed36379a801109955fc5a71a49fe62dd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/174f009ed36379a801109955fc5a71a49fe62dd4", + "reference": "174f009ed36379a801109955fc5a71a49fe62dd4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com", + "time": "2015-03-30 15:54:10" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3" + }, + "platform-dev": [] +}