diff --git a/src/DocumentParser.php b/src/DocumentParser.php new file mode 100644 index 0000000..03238dc --- /dev/null +++ b/src/DocumentParser.php @@ -0,0 +1,99 @@ +registerDefaultParsers(); + } + + /** + * Parse the given Markdown text into a document tree. + * + * @param string $markdown + * @return Document + */ + public function parse($markdown) + { + $target = $root = new Document(); + $parser = $this->buildParserStack(); + + $lines = explode("\n", $markdown); + foreach ($lines as $line) { + $target = call_user_func($parser, $line, $target); + } + + return $root; + } + + /** + * Register all standard parsers. + * + * @return void + */ + protected function registerDefaultParsers() + { + $this->parsers = [ + new BlockquoteParser(), + new ParagraphParser(), + ]; + } + + /** + * Build the nested stack of closures that executes the parsers in the correct order. + * + * @return callable + */ + protected function buildParserStack() + { + $parsers = array_reverse($this->parsers); + $initial = $this->getInitialClosure(); + + return array_reduce($parsers, $this->getParserClosure(), $initial); + } + + /** + * Create the closure that returns another closure to be passed to each parser. + * + * @return callable + */ + protected function getParserClosure() + { + return function ($stack, ParserInterface $parser) { + return function ($line, NodeAcceptorInterface $target) use ($stack, $parser) { + return $parser->parseLine($line, $target, $stack); + }; + }; + } + + /** + * Create the fallback closure that simply returns the target node and throws away any content. + * + * @return callable + */ + protected function getInitialClosure() + { + return function ($line, NodeAcceptorInterface $target) { + return $target; + }; + } + +} diff --git a/src/Node/Block.php b/src/Node/Block.php new file mode 100644 index 0000000..787e83e --- /dev/null +++ b/src/Node/Block.php @@ -0,0 +1,61 @@ +isOpen() ? '-> ' : '') . parent::toString(); + } + + public function push(Node $child) + { + if ($this->isOpen()) { + if ($this->canContain($child)) { + $this->addChild($child); + return; + } else { + $this->close(); + } + } else { + $this->getParent()->push($child); + } + } + + public function isOpen() + { + return $this->open; + } + + public function close() + { + $this->open = false; + } + + /* + * Block acceptor methods + */ + + public function accept(NodeInterface $node) + { + return $node->proposeTo($this); + } + + public function acceptParagraph(Paragraph $paragraph) + { + return $this->getParent()->acceptParagraph($paragraph); + } + + public function acceptBlockquote(Blockquote $blockquote) + { + return $this->getParent()->acceptBlockquote($blockquote); + } + +} diff --git a/src/Node/Blockquote.php b/src/Node/Blockquote.php new file mode 100644 index 0000000..3623229 --- /dev/null +++ b/src/Node/Blockquote.php @@ -0,0 +1,42 @@ +getType() == 'paragraph'; + } + + public function accepts(Node $block) + { + return $block->getType() == 'paragraph'; + } + + public function proposeTo(NodeAcceptorInterface $block) + { + return $block->acceptBlockquote($this); + } + + public function acceptParagraph(Paragraph $paragraph) + { + $this->addChild($paragraph); + + return $paragraph; + } + + public function acceptBlockquote(Blockquote $blockquote) + { + $this->merge($blockquote); + + return $this; + } + +} diff --git a/src/Node/Document.php b/src/Node/Document.php new file mode 100644 index 0000000..5aaa761 --- /dev/null +++ b/src/Node/Document.php @@ -0,0 +1,37 @@ +addChild($paragraph); + + return $paragraph; + } + + public function acceptBlockquote(Blockquote $blockquote) + { + $this->addChild($blockquote); + + return $blockquote; + } + +} diff --git a/src/Node/LeafBlock.php b/src/Node/LeafBlock.php new file mode 100644 index 0000000..92e96d1 --- /dev/null +++ b/src/Node/LeafBlock.php @@ -0,0 +1,28 @@ +proposeTo($this); + } + + public function acceptParagraph(Paragraph $paragraph) + { + return $this->parent->acceptParagraph($paragraph); + } + + public function acceptBlockquote(Blockquote $blockquote) + { + return $this->parent->acceptBlockquote($blockquote); + } + +} diff --git a/src/Node/Node.php b/src/Node/Node.php new file mode 100644 index 0000000..f0cca22 --- /dev/null +++ b/src/Node/Node.php @@ -0,0 +1,61 @@ +getType(); + } + + public function setParent(Node $parent) + { + $this->parent = $parent; + } + + public function getChildNodes() + { + return $this->children; + } + + public function addChild(Node $child) + { + $this->children[] = $child; + $child->setParent($this); + + return $this; + } + + public function removeChild(Node $child) + { + $this->children = array_filter($this->children, function (Node $element) use ($child) { + return $child != $element; + }); + } + + public function merge(Node $sibling) + { + $this->children = array_merge($this->children, $sibling->children); + } + + /** + * @return Node + */ + public function getParent() + { + return $this->parent; + } + +} diff --git a/src/Node/NodeAcceptorInterface.php b/src/Node/NodeAcceptorInterface.php new file mode 100644 index 0000000..e2e1916 --- /dev/null +++ b/src/Node/NodeAcceptorInterface.php @@ -0,0 +1,22 @@ +text = $text; + } + + public function getType() + { + return 'paragraph'; + } + + public function toString() + { + return parent::toString() . '("' . str_replace("\n", ' ', $this->text) . '")'; + } + + public function canContain(Node $other) + { + return true; + } + + public function proposeTo(NodeAcceptorInterface $block) + { + return $block->acceptParagraph($this); + } + + public function acceptParagraph(Paragraph $paragraph) + { + $this->text .= "\n" . $paragraph->text; + + return $this; + } + +} diff --git a/src/Parser/BlockquoteParser.php b/src/Parser/BlockquoteParser.php new file mode 100644 index 0000000..84b31ca --- /dev/null +++ b/src/Parser/BlockquoteParser.php @@ -0,0 +1,23 @@ +') { + $line = trim(substr($line, 1)); + + $quote = new Blockquote(); + $target = $target->accept($quote); + } + + return $next($line, $target); + } + +} diff --git a/src/Parser/ParagraphParser.php b/src/Parser/ParagraphParser.php new file mode 100644 index 0000000..d796f39 --- /dev/null +++ b/src/Parser/ParagraphParser.php @@ -0,0 +1,18 @@ +accept($paragraph); + } + +} diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php new file mode 100644 index 0000000..b9921dc --- /dev/null +++ b/src/Parser/ParserInterface.php @@ -0,0 +1,25 @@ +