Skip to content

Commit 5c8dcc9

Browse files
committed
major rewrite: introduced serializers, extractor, and processor, added recursive processing, improved parsing, and added new tests
1 parent 1edf9fe commit 5c8dcc9

18 files changed

Lines changed: 537 additions & 108 deletions

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ No required dependencies, only PHP >=5.4
2929
3030
## Installation
3131

32-
This library is registered on Packagist, so you can just execute
32+
To install it from Packagist execute
3333

3434
```
3535
composer require thunderer/shortcode
@@ -69,7 +69,22 @@ $parser->addCode('sample', function(Shortcode $s) {
6969
echo $parser->parse('[sample argument=value]content[/sample]');
7070
```
7171

72-
If parser finds shortcode that is not supported (no registered handler) it will return whole block without any modification. When opening and closing shortcode do not match, parser ignores closing fragment and considers it as a self-closing shortcode.
72+
Edge cases:
73+
74+
* unsupported shortcodes (no registered handler) will be ignored and left as they are,
75+
* mismatching closing shortcode (`[code]content[/codex]`) will be ignored, opening will be interpreted as self-closing shortcode,
76+
* overlapping shortcodes (`[code]content[inner][/code]content[/inner]`) are not supported and will be interpreted as self-closing, ending fragment will be ignored.
77+
78+
## Ideas
79+
80+
Looking for contribution ideas? Here you are:
81+
82+
* shortcode aliases,
83+
* configurable processor recursion,
84+
* configurable tokens for extractor and parser,
85+
* XML serializer,
86+
* YAML serializer,
87+
* specialized exceptions classes.
7388

7489
## License
7590

src/Extractor.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
namespace Thunder\Shortcode;
3+
4+
/**
5+
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
6+
*/
7+
final class Extractor implements ExtractorInterface
8+
{
9+
const SHORTCODE_REGEX = '/(\[(\w+)(\s+.+?)?\](?:(.+?)\[\/(\2)\])?)/us';
10+
11+
/**
12+
* @param string $text
13+
* @return Match[]
14+
*/
15+
public function extract($text)
16+
{
17+
preg_match_all(self::SHORTCODE_REGEX, $text, $matches, PREG_OFFSET_CAPTURE);
18+
19+
return array_map(function(array $matches) {
20+
return new Match($matches[1], $matches[0]);
21+
}, $matches[0]);
22+
}
23+
}

src/ExtractorInterface.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
namespace Thunder\Shortcode;
3+
4+
/**
5+
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
6+
*/
7+
interface ExtractorInterface
8+
{
9+
/**
10+
* Extract shortcode string matches with their offsets for further analysis
11+
*
12+
* @param string $text Text to extract from
13+
*
14+
* @return Match[]
15+
*/
16+
public function extract($text);
17+
}

src/Match.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
namespace Thunder\Shortcode;
3+
4+
/**
5+
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
6+
*/
7+
final class Match
8+
{
9+
private $position;
10+
private $string;
11+
12+
public function __construct($position, $string)
13+
{
14+
$this->position = $position;
15+
$this->string = $string;
16+
}
17+
18+
public function getLength()
19+
{
20+
return mb_strlen($this->string);
21+
}
22+
23+
public function getPosition()
24+
{
25+
return $this->position;
26+
}
27+
28+
public function getString()
29+
{
30+
return $this->string;
31+
}
32+
}

src/Parser.php

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,26 @@
44
/**
55
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
66
*/
7-
final class Parser
7+
final class Parser implements ParserInterface
88
{
9-
const SHORTCODE_REGEX = '/(\[(\w+)(\s+.+?)?\](?:(.+)\[\/(\2)\])?)/us';
9+
const SHORTCODE_REGEX = '/^(\[(\w+)(\s+.+?)?\](?:(.+?)\[\/(\2)\])?)$/us';
1010
const ARGUMENTS_REGEX = '/(?:\s+(\w+(?:(?=\s|$)|=\w+|=".+")))/us';
1111

12-
private $codes = array();
13-
14-
public function __construct()
12+
public function parse($text)
1513
{
16-
}
14+
$count = preg_match(self::SHORTCODE_REGEX, $text, $matches);
1715

18-
public function addCode($name, callable $handler)
19-
{
20-
if($this->hasCode($name))
16+
if(!$count)
2117
{
22-
$msg = 'Code %s already exists!';
23-
throw new \RuntimeException(sprintf($msg, $name));
18+
$msg = 'Failed to match single shortcode in text "%s"!';
19+
throw new \RuntimeException(sprintf($msg, $text));
2420
}
2521

26-
$this->codes[$name] = $handler;
27-
}
28-
29-
public function parse($text)
30-
{
31-
return preg_replace_callback(self::SHORTCODE_REGEX, function(array $matches) {
32-
return $this->hasCode($matches[2])
33-
? call_user_func_array($this->codes[$matches[2]], array(new Shortcode(
34-
$matches[2],
35-
isset($matches[3]) ? $this->parseParameters($matches[3]) : array(),
36-
isset($matches[4]) ? $matches[4] : null
37-
)))
38-
: $matches[0];
39-
}, $text);
22+
return new Shortcode(
23+
$matches[2],
24+
isset($matches[3]) ? $this->parseParameters($matches[3]) : array(),
25+
isset($matches[4]) ? $matches[4] : null
26+
);
4027
}
4128

4229
private function parseParameters($text)
@@ -54,9 +41,4 @@ private function parseParameters($text)
5441
return $state;
5542
}, array());
5643
}
57-
58-
private function hasCode($name)
59-
{
60-
return array_key_exists($name, $this->codes);
61-
}
6244
}

src/ParserInterface.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
namespace Thunder\Shortcode;
3+
4+
/**
5+
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
6+
*/
7+
interface ParserInterface
8+
{
9+
/**
10+
* Parse single shortcode match into object
11+
*
12+
* @param string $text
13+
*
14+
* @return Shortcode
15+
*/
16+
public function parse($text);
17+
}

src/Processor.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
namespace Thunder\Shortcode;
3+
4+
/**
5+
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
6+
*/
7+
final class Processor implements ProcessorInterface
8+
{
9+
private $handlers = array();
10+
private $extractor;
11+
private $parser;
12+
private $defaultHandler;
13+
14+
public function __construct(ExtractorInterface $extractor, ParserInterface $parser)
15+
{
16+
$this->extractor = $extractor;
17+
$this->parser = $parser;
18+
}
19+
20+
public function addHandler($name, callable $handler)
21+
{
22+
if($this->hasHandler($name))
23+
{
24+
$msg = 'Cannot register duplicate shortcode handler for %s!';
25+
throw new \RuntimeException(sprintf($msg, $name));
26+
}
27+
28+
$this->handlers[$name] = $handler;
29+
30+
return $this;
31+
}
32+
33+
public function setDefaultHandler(callable $handler)
34+
{
35+
$this->defaultHandler = $handler;
36+
}
37+
38+
/**
39+
* Expects matches sorted by position returned from Extractor. Matches are
40+
* processed from the last to the first to avoid replace position errors.
41+
* Edge cases are described in README.
42+
*
43+
* @param string $text
44+
*
45+
* @return string
46+
*/
47+
public function process($text)
48+
{
49+
/** @var $matches Match[] */
50+
$matches = array_reverse($this->extractor->extract($text));
51+
52+
foreach($matches as $match)
53+
{
54+
$shortcode = $this->parser->parse($match->getString());
55+
$shortcode = new Shortcode(
56+
$shortcode->getName(),
57+
$shortcode->getParameters(),
58+
$shortcode->hasContent() ? $this->process($shortcode->getContent()) : null
59+
);
60+
$handler = $this->getHandler($shortcode->getName());
61+
if($handler)
62+
{
63+
$replace = call_user_func_array($handler, array($shortcode));
64+
$text = substr_replace($text, $replace, $match->getPosition(), $match->getLength());
65+
}
66+
}
67+
68+
return $text;
69+
}
70+
71+
private function getHandler($name)
72+
{
73+
return $this->hasHandler($name)
74+
? $this->handlers[$name]
75+
: ($this->defaultHandler ? $this->defaultHandler : null);
76+
}
77+
78+
private function hasHandler($name)
79+
{
80+
return array_key_exists($name, $this->handlers);
81+
}
82+
}

src/ProcessorInterface.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
namespace Thunder\Shortcode;
3+
4+
/**
5+
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
6+
*/
7+
interface ProcessorInterface
8+
{
9+
/**
10+
* Register shortcode callback handler
11+
*
12+
* @param string $name
13+
* @param callable $handler
14+
*
15+
* @return self
16+
*/
17+
public function addHandler($name, callable $handler);
18+
19+
/**
20+
* Process text using registered shortcode handlers
21+
*
22+
* @param string $text
23+
*
24+
* @return string
25+
*/
26+
public function process($text);
27+
}

src/Serializer/JsonSerializer.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
namespace Thunder\Shortcode\Serializer;
3+
4+
use Thunder\Shortcode\SerializerInterface;
5+
use Thunder\Shortcode\Shortcode;
6+
7+
final class JsonSerializer implements SerializerInterface
8+
{
9+
public function serialize(Shortcode $s)
10+
{
11+
return json_encode(array(
12+
'name' => $s->getName(),
13+
'parameters' => $s->getParameters(),
14+
'content' => $s->getContent(),
15+
));
16+
}
17+
18+
public function unserialize($text)
19+
{
20+
$data = json_decode($text, true);
21+
22+
if(!is_array($data))
23+
{
24+
throw new \RuntimeException('Invalid JSON, cannot unserialize Shortcode!');
25+
}
26+
if(!array_diff_key($data, array('name', 'parameters', 'content')))
27+
{
28+
throw new \RuntimeException('Malformed Shortcode JSON, expected name, parameters, and content!');
29+
}
30+
31+
return new Shortcode($data['name'], $data['parameters'], $data['content']);
32+
}
33+
}

src/Serializer/TextSerializer.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
namespace Thunder\Shortcode\Serializer;
3+
4+
use Thunder\Shortcode\Parser;
5+
use Thunder\Shortcode\SerializerInterface;
6+
use Thunder\Shortcode\Shortcode;
7+
8+
final class TextSerializer implements SerializerInterface
9+
{
10+
private $open;
11+
private $close;
12+
private $slash;
13+
private $equals;
14+
private $string;
15+
16+
public function __construct($open = '[', $close = ']', $slash = '/', $equals = '=', $string = '"')
17+
{
18+
$this->open = $open;
19+
$this->close = $close;
20+
$this->slash = $slash;
21+
$this->equals = $equals;
22+
$this->string = $string;
23+
}
24+
25+
public function serialize(Shortcode $s)
26+
{
27+
return
28+
$this->open.$s->getName().$this->serializeParameters($s->getParameters()).$this->close
29+
.(null === $s->getContent() ? '' : $s->getContent().$this->open.$this->slash.$s->getName().$this->close);
30+
}
31+
32+
private function serializeParameters(array $parameters)
33+
{
34+
$return = '';
35+
foreach($parameters as $key => $value)
36+
{
37+
$return .= ' '.$key;
38+
if(null !== $value)
39+
{
40+
$return .= $this->equals.(preg_match('/^\w+$/us', $value)
41+
? $value
42+
: $this->string.$value.$this->string);
43+
}
44+
}
45+
46+
return $return;
47+
}
48+
49+
public function unserialize($text)
50+
{
51+
$parser = new Parser();
52+
53+
return $parser->parse($text);
54+
}
55+
}

0 commit comments

Comments
 (0)