Skip to content

Commit

Permalink
Start work on creating a new document parser.
Browse files Browse the repository at this point in the history
The DocumentParser class takes a Markdown-formatted string and turns it into a tree of objects, as described in the CommonMark spec.

Refs #1.
  • Loading branch information
franzliedke committed Dec 10, 2014
1 parent 27f0d88 commit ada2106
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 0 deletions.
99 changes: 99 additions & 0 deletions src/DocumentParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace FluxBB\Markdown;

use FluxBB\Markdown\Node\Document;
use FluxBB\Markdown\Node\NodeAcceptorInterface;
use FluxBB\Markdown\Parser\BlockquoteParser;
use FluxBB\Markdown\Parser\ParagraphParser;
use FluxBB\Markdown\Parser\ParserInterface;

class DocumentParser
{

/**
* @var ParserInterface[]
*/
protected $parsers = [];


/**
* Create a parser instance.
*/
public function __construct()
{
$this->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;
};
}

}
61 changes: 61 additions & 0 deletions src/Node/Block.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace FluxBB\Markdown\Node;

abstract class Block extends Node implements NodeAcceptorInterface
{

protected $open = true;


abstract public function canContain(Node $other);

public function toString()
{
return ($this->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);
}

}
42 changes: 42 additions & 0 deletions src/Node/Blockquote.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace FluxBB\Markdown\Node;

class Blockquote extends Block implements NodeInterface, NodeAcceptorInterface
{

public function getType()
{
return 'block_quote';
}

public function canContain(Node $other)
{
return $other->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;
}

}
37 changes: 37 additions & 0 deletions src/Node/Document.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace FluxBB\Markdown\Node;

class Document extends Block
{

public function canContain(Node $other)
{
return true;
}

public function isOpen()
{
return true;
}

public function getType()
{
return 'document';
}

public function acceptParagraph(Paragraph $paragraph)
{
$this->addChild($paragraph);

return $paragraph;
}

public function acceptBlockquote(Blockquote $blockquote)
{
$this->addChild($blockquote);

return $blockquote;
}

}
28 changes: 28 additions & 0 deletions src/Node/LeafBlock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace FluxBB\Markdown\Node;

abstract class LeafBlock extends Node implements NodeAcceptorInterface
{

public function canContain(Node $other)
{
return false;
}

public function accept(NodeInterface $node)
{
return $node->proposeTo($this);
}

public function acceptParagraph(Paragraph $paragraph)
{
return $this->parent->acceptParagraph($paragraph);
}

public function acceptBlockquote(Blockquote $blockquote)
{
return $this->parent->acceptBlockquote($blockquote);
}

}
61 changes: 61 additions & 0 deletions src/Node/Node.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace FluxBB\Markdown\Node;

abstract class Node
{

protected $children = [];

/**
* @var Node
*/
protected $parent = null;


abstract public function getType();

public function toString()
{
return $this->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;
}

}
22 changes: 22 additions & 0 deletions src/Node/NodeAcceptorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace FluxBB\Markdown\Node;

interface NodeAcceptorInterface
{

/**
* Accept the given node as a child.
*
* This should ask the node for its type, and then call the appropriate method.
*
* @param NodeInterface $node
* @return Node
*/
public function accept(NodeInterface $node);

public function acceptParagraph(Paragraph $paragraph);

public function acceptBlockquote(Blockquote $blockquote);

}
14 changes: 14 additions & 0 deletions src/Node/NodeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace FluxBB\Markdown\Node;

interface NodeInterface
{

/**
* @param NodeAcceptorInterface $block
* @return Node
*/
public function proposeTo(NodeAcceptorInterface $block);

}
Loading

0 comments on commit ada2106

Please sign in to comment.