From 53e61ae39aa32f3d5ef094e476050cb53d49c701 Mon Sep 17 00:00:00 2001 From: Igor M Osipov Date: Thu, 28 Dec 2017 01:35:01 +0300 Subject: [PATCH] Basic implementation --- .gitignore | 9 +- README.md | 51 +++++++++- composer.json | 32 ++++++ phpunit.xml | 12 +++ src/BBCode.php | 73 ++++++++++++++ tests/BBCodeTest.php | 226 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 396 insertions(+), 7 deletions(-) create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/BBCode.php create mode 100644 tests/BBCodeTest.php diff --git a/.gitignore b/.gitignore index c422267..ae9ab10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ -composer.phar /vendor/ - -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock +composer.lock +composer.phar +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index a75c869..598daa7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ -# php5-tiny-bbcode -A tiny BBCode implementation for old PHP5 +# Tiny BBCode implementation for PHP 5.4+ + +This library includes a lightweight implementation of a BBCode subset to HTML translator. + +## Features + +* It's tiny, yep. +* PSR-4 autoloading compliant structure +* Unit-Testing with PHPUnit +* Easy to use to any framework or even a plain php file + +## Installation + +The suggested installation method is via [composer](https://getcomposer.org/): + +```sh +php composer.phar require "igorakaamigo/php5-tiny-bbcode" +``` + +## Usage + +```php +use \Igorakaamigo\Utils\BBCode; + +echo BBCode::convert('[b]A bold string[/b]'); +``` + +### Supported BBCodes + +* [b]Bold string[/b] +* [i]Italic string[/i] +* [u]Underline string[/u] +* [s]Strikethrough string[/s] +* [url]http://www.domain.tld[/url] +* [url=http://www.domain.tld]Another way to render a link[/url] +* [img]http://www.domain.tld/upload/image.png[/img] +* [quote]A quotation[/quote] +* [code]A program code sample[/code] +* [size=12]A text written using a 12px-sized font[/size] +* [size="10pt"]A text written using a 10pt-sized font[/size] +* [color="#33FF33"]A green text line[/color] +* [ul], [ol], [li] – list-related tags +* [table], [tr], [td] – table-related tags + +## Contributing + +OMG! Really? Thanks a lot! + +Fork --> modify --> pull-request diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6d564bf --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "igorakaamigo/php5-tiny-bbcode", + "description": "A tiny BBCode implementation for old PHP5.", + "keywords": [ + "php5", + "library", + "utils", + "bbcode" + ], + "license": "MIT", + "authors": [ + { + "name": "Igor M Osipov", + "email": "osipov.igor.amigo@gmail.com" + } + ], + "type": "library", + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "5.4.*" + }, + "autoload": { + "psr-4": { + "Igorakaamigo\\Utils\\": "src/" + } + }, + "scripts": { + "test": "phpunit" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..f1e6dbb --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + tests + + + diff --git a/src/BBCode.php b/src/BBCode.php new file mode 100644 index 0000000..cdc92aa --- /dev/null +++ b/src/BBCode.php @@ -0,0 +1,73 @@ + '\1', + '#\[i\](.*?)\[/i\]#si' => '\1', + '#\[u\](.*?)\[/u\]#si' => '\1', + '#\[s\](.*?)\[/s\]#si' => '\1', + '#\[url\](.*?)\[/url\]#si' => '\1', + '#\[url=(.*?)\](.*?)\[/url\]#si' => '\2', + '#\[img\](.*?)\[/img\]#si' => '', + '#\[quote(=".*?")?\](.*?)\[/quote\]#si' => '
\2
', + '#\[code\](.*?)\[/code\]#si' => '\1', + '#\[size=(\d+)\](.*?)\[/size\]#si' => '\2', + '#\[size="(.*?)"\](.*?)\[/size\]#si' => '\2', + '#\[color="(.*?)"\](.*?)\[/color\]#si' => '\2', + '#\[li\](.*?)\[/li\]#si' => '
  • \1
  • ', + '#\[ul\](.*?)\[/ul\]#si' => '', + '#\[ol\](.*?)\[/ol\]#si' => '
      \1
    ', + '#\[table\](.*?)\[/table\]#si' => '\1
    ', + '#\[tr\](.*?)\[/tr\]#si' => '\1', + '#\[td\](.*?)\[/td\]#si' => '\1', + '#(^\s+)|(\s+$)#si' => '', + ); + + /** + * BBCode translation + * + * @param string $sourceString A string containing BBCode tags + * @return string HTML string + */ + static function convert($sourceString) + { + return preg_replace( + array_keys(self::$_patterns), + array_values(self::$_patterns), + htmlentities($sourceString) + ); + } +} diff --git a/tests/BBCodeTest.php b/tests/BBCodeTest.php new file mode 100644 index 0000000..2250652 --- /dev/null +++ b/tests/BBCodeTest.php @@ -0,0 +1,226 @@ + +
    +

    A & paragraph

    +
    + + '; + $this->assertEquals($expected, BBCode::convert($source)); + } + + public function testItShouldStripLeadingAndTrailingWhitespaces() + { + $expected = 'A string to be returned'; + $source = " A string to be returned \t\n\r "; + $this->assertEquals($expected, BBCode::convert($source)); + } + + public function testItShouldDealWithNestedTags() + { + $expected = 'A text to be processed'; + $source = ' [s][u][b][i]A text to be processed[/i][/b][/u][/s] '; + $this->assertEquals($expected, BBCode::convert($source)); + + $expected = ''; + $source = '[url=http://www.google.com/][img]http://www.domain.tld/upload/123.png[/img][/url]'; + $this->assertEquals($expected, BBCode::convert($source)); + } + + public function testItShouldDealWithTagB() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('b', function ($v) { + return "$v"; + }); + } + + private function _itShouldBehaveLikeASimpleWrapperTag($tagName, $tagBuilder) + { + $expected = "A leading part {$tagBuilder('A tag value')} A trailing part"; + $source = "A leading part [$tagName]A tag value[/$tagName] A trailing part"; + $this->assertEquals($expected, BBCode::convert($source), 'It should render a correct markup'); + + $uTag = strtoupper($tagName); + $source = "A leading part [$uTag]A tag value[/$uTag] A trailing part"; + $this->assertEquals($expected, BBCode::convert($source), 'Tag names should be case-insensitive'); + + $expected = "A leading part {$tagBuilder(' + A tag value + ')} A trailing part"; + $source = "A leading part [$tagName] + A tag value + [/$tagName] A trailing part"; + $this->assertEquals($expected, BBCode::convert($source), + 'Tag content should be processed as a multiline string'); + } + + public function testItShouldDealWithTagI() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('i', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagU() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('u', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagS() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('s', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagUl() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('ul', function ($v) { + return ""; + }); + } + + public function testItShouldDealWithTagOl() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('ol', function ($v) { + return "
      $v
    "; + }); + } + + public function testItShouldDealWithTagLi() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('li', function ($v) { + return "
  • $v
  • "; + }); + } + + public function testItShouldDealWithTagTable() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('table', function ($v) { + return "$v
    "; + }); + } + + public function testItShouldDealWithTagTr() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('tr', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagTd() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('td', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagUrl() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('url', function ($v) { + return "$v"; + }); + $this->_itShouldBehaveLikeAnAttributedWrapperTag('url', 'http://www.google.com/', function ($v) { + return "$v"; + }); + } + + private function _itShouldBehaveLikeAnAttributedWrapperTag($tagName, $attributeValue, $tagBuilder) + { + $expected = "A leading string {$tagBuilder('A tag string')} A trailing string"; + $source = "A leading string [$tagName=$attributeValue]A tag string[/$tagName] A trailing string"; + $this->assertEquals($expected, BBCode::convert($source), 'It should render a correct markup'); + + $uTag = strtoupper($tagName); + $source = "A leading string [$uTag=$attributeValue]A tag string[/$uTag] A trailing string"; + $this->assertEquals($expected, BBCode::convert($source), 'Tag names should be case-insensitive'); + + $expected = "A leading part {$tagBuilder(' + A tag value + ')} A trailing part"; + $source = "A leading part [$tagName=$attributeValue] + A tag value + [/$tagName] A trailing part"; + $this->assertEquals($expected, BBCode::convert($source), + 'Tag content should be processed as a multiline string'); + } + + public function testItShouldDealWithTagImg() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('img', function ($v) { + return "\"\""; + }); + } + + public function testItShouldDealWithTagQuote() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('quote', function ($v) { + return "
    $v
    "; + }); + $this->_itShouldBehaveLikeAnAttributedWrapperTag('quote', '"Quote author"', function ($v) { + return "
    $v
    "; + }); + } + + public function testItShouldDealWithTagCode() + { + $this->_itShouldBehaveLikeASimpleWrapperTag('code', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagSize() + { + $this->_itShouldBehaveLikeAnAttributedWrapperTag('size', '20', function ($v) { + return "$v"; + }); + $this->_itShouldBehaveLikeAnAttributedWrapperTag('size', '"20em"', function ($v) { + return "$v"; + }); + } + + public function testItShouldDealWithTagColor() + { + $this->_itShouldBehaveLikeAnAttributedWrapperTag('color', '"#AABBCC"', function ($v) { + return "$v"; + }); + } +}