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": []
+}