diff --git a/CodexEditor/CodexEditorException.php b/CodexEditor/CodexEditorException.php deleted file mode 100644 index 5049131..0000000 --- a/CodexEditor/CodexEditorException.php +++ /dev/null @@ -1,16 +0,0 @@ -rules = new ConfigLoader($configuration); - $this->sanitizer = $sanitizer; } /** @@ -44,7 +37,7 @@ public function __construct($configuration, $sanitizer) * @param string $blockType * @param array $blockData * - * @throws CodexEditorException + * @throws EditorJSException * * @return bool */ @@ -54,7 +47,7 @@ public function validateBlock($blockType, $blockData) * Default action for blocks that are not mentioned in a configuration */ if (!array_key_exists($blockType, $this->rules->tools)) { - throw new CodexEditorException("Tool `$blockType` not found in the configuration"); + throw new EditorJSException("Tool `$blockType` not found in the configuration"); } $rule = $this->rules->tools[$blockType]; @@ -68,17 +61,18 @@ public function validateBlock($blockType, $blockData) * @param string $blockType * @param array $blockData * - * @throws CodexEditorException + * @throws EditorJSException * * @return array|bool */ - public function sanitizeBlock($blockType, $blockData) + public function sanitizeBlock($blockType, $blockData, $blockTunes) { $rule = $this->rules->tools[$blockType]; return [ 'type' => $blockType, - 'data' => $this->sanitize($rule, $blockData) + 'data' => $this->sanitize($rule, $blockData), + 'tunes' => $blockTunes ]; } @@ -88,7 +82,7 @@ public function sanitizeBlock($blockType, $blockData) * @param array $rules * @param array $blockData * - * @throws CodexEditorException + * @throws EditorJSException * * @return bool */ @@ -100,7 +94,7 @@ private function validate($rules, $blockData) foreach ($rules as $key => $value) { if (($key != BlockHandler::DEFAULT_ARRAY_KEY) && (isset($value['required']) ? $value['required'] : true)) { if (!isset($blockData[$key])) { - throw new CodexEditorException("Not found required param `$key`"); + throw new EditorJSException("Not found required param `$key`"); } } } @@ -110,7 +104,7 @@ private function validate($rules, $blockData) */ foreach ($blockData as $key => $value) { if (!is_integer($key) && !isset($rules[$key])) { - throw new CodexEditorException("Found extra param `$key`"); + throw new EditorJSException("Found extra param `$key`"); } } @@ -126,6 +120,9 @@ private function validate($rules, $blockData) } $rule = $rules[$key]; + + $rule = $this->expandToolSettings($rule); + $elementType = $rule['type']; /** @@ -133,8 +130,19 @@ private function validate($rules, $blockData) */ if (isset($rule['canBeOnly'])) { if (!in_array($value, $rule['canBeOnly'])) { - throw new CodexEditorException("Option '$key' with value `$value` has invalid value. Check canBeOnly param."); + throw new EditorJSException("Option '$key' with value `$value` has invalid value. Check canBeOnly param."); } + + // Do not perform additional elements validation in any case + continue; + } + + /** + * Do not check element type if it is not required and null + */ + if (isset($rule['required']) && $rule['required'] === false && + isset($rule['allow_null']) && $rule['allow_null'] === true && $value === null) { + continue; } /** @@ -143,14 +151,14 @@ private function validate($rules, $blockData) switch ($elementType) { case 'string': if (!is_string($value)) { - throw new CodexEditorException("Option '$key' with value `$value` must be string"); + throw new EditorJSException("Option '$key' with value `$value` must be string"); } break; case 'integer': case 'int': if (!is_integer($value)) { - throw new CodexEditorException("Option '$key' with value `$value` must be integer"); + throw new EditorJSException("Option '$key' with value `$value` must be integer"); } break; @@ -161,12 +169,12 @@ private function validate($rules, $blockData) case 'boolean': case 'bool': if (!is_bool($value)) { - throw new CodexEditorException("Option '$key' with value `$value` must be boolean"); + throw new EditorJSException("Option '$key' with value `$value` must be boolean"); } break; default: - throw new CodexEditorException("Unhandled type `$elementType`"); + throw new EditorJSException("Unhandled type `$elementType`"); } } @@ -179,7 +187,7 @@ private function validate($rules, $blockData) * @param array $rules * @param array $blockData * - * @throws CodexEditorException + * @throws EditorJSException * * @return array */ @@ -189,7 +197,16 @@ private function sanitize($rules, $blockData) * Sanitize every key in data block */ foreach ($blockData as $key => $value) { - $rule = $rules[$key]; + /** + * PHP Array has integer keys + */ + if (is_integer($key)) { + $rule = $rules[BlockHandler::DEFAULT_ARRAY_KEY]; + } else { + $rule = $rules[$key]; + } + + $rule = $this->expandToolSettings($rule); $elementType = $rule['type']; /** @@ -197,7 +214,9 @@ private function sanitize($rules, $blockData) */ if ($elementType == 'string') { $allowedTags = isset($rule['allowedTags']) ? $rule['allowedTags'] : ''; - $blockData[$key] = $this->getPurifier($allowedTags)->purify($value); + if ($allowedTags !== '*') { + $blockData[$key] = $this->getPurifier($allowedTags)->purify($value); + } } /** @@ -220,11 +239,85 @@ private function sanitize($rules, $blockData) */ private function getPurifier($allowedTags) { - $sanitizer = clone $this->sanitizer; + $sanitizer = $this->getDefaultPurifier(); + $sanitizer->set('HTML.Allowed', $allowedTags); + /** + * Define custom HTML Definition for mark tool + */ + if ($def = $sanitizer->maybeGetRawHTMLDefinition()) { + $def->addElement('mark', 'Inline', 'Inline', 'Common'); + } + $purifier = new \HTMLPurifier($sanitizer); return $purifier; } + + /** + * Initialize HTML Purifier with default settings + */ + private function getDefaultPurifier() + { + $sanitizer = \HTMLPurifier_Config::createDefault(); + + $sanitizer->set('HTML.TargetBlank', true); + $sanitizer->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true, 'tel' => true]); + $sanitizer->set('AutoFormat.RemoveEmpty', true); + $sanitizer->set('HTML.DefinitionID', 'html5-definitions'); + + $cacheDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'purifier'; + if (!is_dir($cacheDirectory)) { + mkdir($cacheDirectory, 0777, true); + } + + $sanitizer->set('Cache.SerializerPath', $cacheDirectory); + + return $sanitizer; + } + + /** + * Check whether the array is associative or sequential + * + * @param array $arr – array to check + * + * @return bool – true if the array is associative + */ + private function isAssoc(array $arr) + { + if ([] === $arr) { + return false; + } + + return array_keys($arr) !== range(0, count($arr) - 1); + } + + /** + * Expand shortified tool settings + * + * @param $rule – tool settings + * + * @throws EditorJSException + * + * @return array – expanded tool settings + */ + private function expandToolSettings($rule) + { + if (is_string($rule)) { + // 'blockName': 'string' – tool with string type and default settings + $expandedRule = ["type" => $rule]; + } elseif (is_array($rule)) { + if ($this->isAssoc($rule)) { + $expandedRule = $rule; + } else { + // 'blockName': [] – tool with canBeOnly and default settings + $expandedRule = ["type" => "string", "canBeOnly" => $rule]; + } + } else { + throw new EditorJSException("Cannot determine element type of the rule `$rule`."); + } + + return $expandedRule; + } } diff --git a/CodexEditor/ConfigLoader.php b/EditorJS/ConfigLoader.php similarity index 73% rename from CodexEditor/ConfigLoader.php rename to EditorJS/ConfigLoader.php index 47adc79..477feb8 100644 --- a/CodexEditor/ConfigLoader.php +++ b/EditorJS/ConfigLoader.php @@ -1,11 +1,11 @@ $toolData) { if (isset($this->tools[$toolName])) { - throw new CodexEditorException("Duplicate tool $toolName in configuration"); + throw new EditorJSException("Duplicate tool $toolName in configuration"); } $this->tools[$toolName] = $this->loadTool($toolData); diff --git a/CodexEditor/CodexEditor.php b/EditorJS/EditorJS.php similarity index 59% rename from CodexEditor/CodexEditor.php rename to EditorJS/EditorJS.php index f4baa94..0daaec8 100644 --- a/CodexEditor/CodexEditor.php +++ b/EditorJS/EditorJS.php @@ -1,13 +1,13 @@ initPurifier(); - $this->handler = new BlockHandler($configuration, $this->sanitizer); + $this->handler = new BlockHandler($configuration); /** * Check if json string is empty */ if (empty($json)) { - throw new CodexEditorException('JSON is empty'); + throw new EditorJSException('JSON is empty'); } /** @@ -59,40 +53,40 @@ public function __construct($json, $configuration) * Handle decoding JSON error */ if (json_last_error()) { - throw new CodexEditorException('Wrong JSON format: ' . json_last_error_msg()); + throw new EditorJSException('Wrong JSON format: ' . json_last_error_msg()); } /** * Check if data is null */ if (is_null($data)) { - throw new CodexEditorException('Input is null'); + throw new EditorJSException('Input is null'); } /** * Count elements in data array */ if (count($data) === 0) { - throw new CodexEditorException('Input array is empty'); + throw new EditorJSException('Input array is empty'); } /** * Check if blocks param is missing in data */ if (!isset($data['blocks'])) { - throw new CodexEditorException('Field `blocks` is missing'); + throw new EditorJSException('Field `blocks` is missing'); } if (!is_array($data['blocks'])) { - throw new CodexEditorException('Blocks is not an array'); + throw new EditorJSException('Blocks is not an array'); } foreach ($data['blocks'] as $blockData) { if (is_array($blockData)) { array_push($this->blocks, $blockData); } else { - throw new CodexEditorException('Block must be an Array'); + throw new EditorJSException('Block must be an Array'); } } @@ -102,24 +96,6 @@ public function __construct($json, $configuration) $this->validateBlocks(); } - /** - * Initialize HTML Purifier with default settings - */ - private function initPurifier() - { - $this->sanitizer = \HTMLPurifier_Config::createDefault(); - - $this->sanitizer->set('HTML.TargetBlank', true); - $this->sanitizer->set('URI.AllowedSchemes', ['http' => true, 'https' => true]); - $this->sanitizer->set('AutoFormat.RemoveEmpty', true); - - if (!is_dir('/tmp/purifier')) { - mkdir('/tmp/purifier', 0777, true); - } - - $this->sanitizer->set('Cache.SerializerPath', '/tmp/purifier'); - } - /** * Sanitize and return array of blocks according to the Handler's rules. * @@ -130,7 +106,11 @@ public function getBlocks() $sanitizedBlocks = []; foreach ($this->blocks as $block) { - $sanitizedBlock = $this->handler->sanitizeBlock($block['type'], $block['data']); + $sanitizedBlock = $this->handler->sanitizeBlock( + $block['type'], + $block['data'], + $block["tunes"] ?? [] + ); if (!empty($sanitizedBlock)) { array_push($sanitizedBlocks, $sanitizedBlock); } diff --git a/EditorJS/EditorJSException.php b/EditorJS/EditorJSException.php new file mode 100644 index 0000000..84ecd24 --- /dev/null +++ b/EditorJS/EditorJSException.php @@ -0,0 +1,16 @@ +getBlocks(); -} catch (\CodexEditorException $e) { +} catch (\EditorJSException $e) { // process exception } ``` -CodexEditor constructor has the following arguments: +Editor.js constructor has the following arguments: `$data` — JSON string with data from CodeX Editor frontend. @@ -48,7 +46,8 @@ CodexEditor constructor has the following arguments: # Configuration file -You can configure validation rules for different types of CodeX Editor tools (header, paragraph, list, quote and other). +You can manually configure validation rules for different types of Editor.js tools (header, paragraph, list, quote and other). +You can also extend configuration with new tools. Sample validation rule set: @@ -63,7 +62,7 @@ Sample validation rule set: }, "level": { "type": "int", - "canBeOnly: [2, 3, 4] + "canBeOnly": [2, 3, 4] } } } @@ -72,7 +71,7 @@ Sample validation rule set: Where: -`tools` — array of supported CodeX Editor tools. +`tools` — array of supported Editor.js tools. `header` — defines `header` tool settings. @@ -82,16 +81,201 @@ Where: `level` is an **optional** *integer* that can be only 0, 1 or 2. -`allowedTags` param should follow [HTMLPurifier](https://github.com/ezyang/htmlpurifier]) format. +`allowedTags` param should follow [HTMLPurifier](https://github.com/ezyang/htmlpurifier) format. + +#### There are three common parameters for every block: + +1. `type` (**required**) — type of the block + +|value|description| +|---|---| +|`string`|field with string value| +|`int`/`integer`|field with integer value| +|`bool`/`boolean`|field with boolean value| +|`array`|field with nested fields| + +2. `allowedTags` (optional) — HTML tags in string that won't be removed + + |value|default|description| +|---|---|---| +|`empty`|yes|all tags will be removed| +|`*`|no|all tags are allowed| + +Other values are allowed according to the [HTMLPurifier](https://github.com/ezyang/htmlpurifier) format. + +Example: +``` +"paragraph": { + "text": { + "type": "string", + "allowedTags": "i,b,u,a[href]" + } +} +``` + +3. `canBeOnly` (optional) — define set of allowed values + +Example: +``` +"quote": { + "text": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "alignment": { + "type": "string", + "canBeOnly": ["left", "center"] + } + } +``` + +### Short settings syntax + +Some syntax sugar has been added. + +Tool settings can be a `string`. It defines tool's type with default settings. +```json +"header": { + "text": "string", + "level": "int" +} +``` + +It evaluates to: +```json +"header": { + "text": { + "type": "string", + "allowedTags": "", + "required": true + }, + "level": { + "type": "int", + "allowedTags": "", + "required": true + } +} +``` + +Tool settings can be an `array`. It defines a set of allowed values without sanitizing. +```json +"quote": { + "alignment": ["left", "center"], + "caption": "string" +} +``` + +It evaluates to: +```json +"quote": { + "alignment": { + "type": "string", + "canBeOnly": ["left", "center"] + }, + "caption": { + "type": "string", + "allowedTags": "", + "required": true + } +} +``` + +Another configuration example: [/tests/samples/syntax-sugar.json](/tests/samples/syntax-sugar.json) + +### Nested tools + +Tools can contain nested values. It is possible with the `array` type. + +Let the JSON input be the following: +``` +{ + "blocks": [ + "type": list, + "data": { + "items": [ + "first", "second", "third" + ], + "style": { + "background-color": "red", + "font-color": "black" + } + } + ] +} +``` + +We can define validation rules for this input in the config: +``` +"list": { + "items": { + "type": "array", + "data": { + "-": { + "type": "string", + "allowedTags": "i,b,u" + } + } + }, + "style": { + "type": "array", + "data": { + "background-color": { + "type": "string", + "canBeOnly": ["red", "blue", "green"] + }, + "font-color": { + "type": "string", + "canBeOnly": ["black", "white"] + } + } + } +} +``` + +where `data` is the container for values of the array and `-` is the special shortcut for values if the array is sequential. + + Another configuration example: [/tests/samples/test-config.json](/tests/samples/test-config.json) +# Exceptions + +### EditorJS class +| Exception text | Cause +| ----------------------------- | ------------------------------------------------ +| JSON is empty | EditorJS initiated with empty `$json` argument +| Wrong JSON format: `error` | `json_decode` failed during `$json` processing +| Input is null | `json_decode` returned null `$data` object +| Input array is empty | `$data` is an empty array +| Field \`blocks\` is missing | `$data` doesn't contain 'blocks' key +| Blocks is not an array | `$data['blocks']` is not an array +| Block must be an Array | one element in `$data['blocks']` is not an array + +### BlockHandler class +| Exception text | Cause +| --------------------- | ----------------------------------------------- +| Tool \`**TOOL_NAME**\` not found in the configuration | Configuration file doesn't contain **TOOL_NAME** in `tools{}` dictionary +| Not found required param \`**key**\` | **key** tool param exists in configuration but doesn't exist in input data. *(Params are always required by default unless `required: false` is set)* +| Found extra param \`**key**\` | Param **key** exists in input data but doesn't defined in configuration +| Option \`**key**\` with value \`**value**\` has invalid value. Check canBeOnly param. | Parameter must have one of the values from **canBeOnly** array in tool configuration +| Option \`**key**\` with value \`**value**\` must be **TYPE** | Param must have type which is defined in tool configuration *(string, integer, boolean)* +| Unhandled type \`**elementType**\` | Param type in configuration is invalid + +### ConfigLoader class +| Exception text | Cause +| ----------------------------- | ------------------------------------------------ +| Configuration data is empty | EditorJS initiated with empty `$configuration` argument +| Tools not found in configuration | Configuration file doesn't contain `tools` key +| Duplicate tool \`**toolName**\` in configuration | Configuration file has different tools with the same name + # Make Tools If you connect a new Tool on the frontend-side, then you should create a configuration rule for that Tool to validate it on server-side. ## Repository -https://github.com/codex-team/codex.editor.backend/ +https://github.com/codex-editor/editorjs-php/ ## About CodeX diff --git a/composer.json b/composer.json index 1b1c63b..0ac1795 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,8 @@ { - "name": "codex-team/codex.editor", + "name": "codex-team/editor.js", "type": "library", - "description": "Codex Editor server side example", + "description": "PHP backend implementation for the Editor.js", "license": "MIT", - "version": "2.0.0", "authors": [ { "name": "CodeX Team", @@ -19,7 +18,7 @@ "friendsofphp/php-cs-fixer": "^2.13" }, "autoload": { - "psr-4": {"CodexEditor\\": "CodexEditor"} + "psr-4": {"EditorJS\\": "EditorJS"} }, "scripts": { "test": "vendor/bin/phpunit", diff --git a/tests/BlockHandlerTest.php b/tests/BlockHandlerTest.php index a6aa4c1..bdf5d59 100644 --- a/tests/BlockHandlerTest.php +++ b/tests/BlockHandlerTest.php @@ -1,7 +1,7 @@ configuration); + new EditorJS(BlockHandlerTest::SAMPLE_VALID_DATA, $this->configuration); } public function testSanitizing() { $data = '{"blocks":[{"type":"header","data":{"text":"CodeX Editor", "level": 2}}]}'; - $editor = new CodexEditor($data, $this->configuration); + $editor = new EditorJS($data, $this->configuration); $result = $editor->getBlocks(); $this->assertEquals('CodeX Editor', $result[0]['data']['text']); @@ -45,7 +45,7 @@ public function testSanitizingAllowedTags() { $data = '{"blocks":[{"type":"paragraph","data":{"text":"CodeX Editor ifmo.su"}}]}'; - $editor = new CodexEditor($data, $this->configuration); + $editor = new EditorJS($data, $this->configuration); $result = $editor->getBlocks(); $this->assertEquals('CodeX Editor ifmo.su', $result[0]['data']['text']); @@ -54,9 +54,21 @@ public function testSanitizingAllowedTags() public function testCanBeOnly() { $callable = function () { - new CodexEditor('{"blocks":[{"type":"header","data":{"text":"test","level":5}}]}', $this->configuration); + new EditorJS('{"blocks":[{"type":"header","data":{"text":"test","level":5}}]}', $this->configuration); }; - $this->assertException($callable, CodexEditorException::class, null, 'Option \'level\' with value `5` has invalid value. Check canBeOnly param.'); + $this->assertException($callable, EditorJSException::class, null, 'Option \'level\' with value `5` has invalid value. Check canBeOnly param.'); + } + + public function testListTool() + { + $data = '{"time":1539180803359,"blocks":[{"type":"list","data":{"style":"ordered","items":["first","second","third"]}}],"version":"2.1.1"}'; + $editor = new EditorJS($data, $this->configuration); + $result = $editor->getBlocks(); + + $this->assertEquals(3, count($result[0]['data']['items'])); + $this->assertEquals("first", $result[0]['data']['items'][0]); + $this->assertEquals("second", $result[0]['data']['items'][1]); + $this->assertEquals("third", $result[0]['data']['items'][2]); } } diff --git a/tests/GeneralTest.php b/tests/GeneralTest.php index 4dc3921..b5e472f 100644 --- a/tests/GeneralTest.php +++ b/tests/GeneralTest.php @@ -1,8 +1,8 @@ config); + new EditorJS(GeneralTest::SAMPLE_VALID_DATA, $this->config); } public function testNullInput() { $callable = function () { - new CodexEditor('', $this->config); + new EditorJS('', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'JSON is empty'); + $this->assertException($callable, EditorJSException::class, null, 'JSON is empty'); } public function testEmptyArray() { $callable = function () { - new CodexEditor('{}', $this->config); + new EditorJS('{}', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'Input array is empty'); + $this->assertException($callable, EditorJSException::class, null, 'Input array is empty'); } public function testWrongJson() { $callable = function () { - new CodexEditor('{[{', $this->config); + new EditorJS('{[{', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'Wrong JSON format: Syntax error'); + $this->assertException($callable, EditorJSException::class, null, 'Wrong JSON format: Syntax error'); } public function testValidConfig() @@ -64,36 +64,53 @@ public function testValidConfig() public function testItemsMissed() { $callable = function () { - new CodexEditor('{"s":""}', $this->config); + new EditorJS('{"s":""}', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'Field `blocks` is missing'); + $this->assertException($callable, EditorJSException::class, null, 'Field `blocks` is missing'); } public function testUnicode() { $callable = function () { - new CodexEditor('{"s":"😀"}', $this->config); + new EditorJS('{"s":"😀"}', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'Field `blocks` is missing'); + $this->assertException($callable, EditorJSException::class, null, 'Field `blocks` is missing'); } public function testInvalidBlock() { $callable = function () { - new CodexEditor('{"blocks":""}', $this->config); + new EditorJS('{"blocks":""}', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'Blocks is not an array'); + $this->assertException($callable, EditorJSException::class, null, 'Blocks is not an array'); } public function testBlocksContent() { $callable = function () { - new CodexEditor('{"blocks":["",""]}', $this->config); + new EditorJS('{"blocks":["",""]}', $this->config); }; - $this->assertException($callable, CodexEditorException::class, null, 'Block must be an Array'); + $this->assertException($callable, EditorJSException::class, null, 'Block must be an Array'); + } + + public function testNested() + { + $data = '{"blocks":[{"type":"table","data":{"header": {"description":"a table", "author": "codex"}, "rows": [["name", "age", "sex"],["Paul", "24", "male"],["Ann", "26", "female"]]}}]}'; + $editor = new EditorJS($data, $this->config); + $result = $editor->getBlocks(); + + $valid_rows = [["name", "age", "sex"],["Paul", "24", "male"],["Ann", "26", "female"]]; + + $this->assertEquals('a table', $result[0]['data']['header']['description']); + $this->assertEquals('codex', $result[0]['data']['header']['author']); + $this->assertEquals(3, count($result[0]['data']['rows'])); + + $this->assertEquals('name', $result[0]['data']['rows'][0][0]); + $this->assertEquals('24', $result[0]['data']['rows'][1][1]); + $this->assertEquals('female', $result[0]['data']['rows'][2][2]); } } diff --git a/tests/PurifierTest.php b/tests/PurifierTest.php new file mode 100644 index 0000000..98d48f6 --- /dev/null +++ b/tests/PurifierTest.php @@ -0,0 +1,55 @@ +configuration = file_get_contents(PurifierTest::CONFIGURATION_FILE); + } + + public function testHtmlPurifier() + { + $data = '{"time":1539180803359,"blocks":[{"type":"header","data":{"text":"test","level":2}}, {"type":"quote","data":{"text":"test","caption":"", "alignment":"left"}}]}'; + $editor = new EditorJS($data, $this->configuration); + $result = $editor->getBlocks(); + + $this->assertEquals(2, count($result)); + $this->assertEquals('test', $result[0]['data']['text']); + $this->assertEquals('test', $result[1]['data']['text']); + } + + public function testCustomTagPurifier() + { + $data = '{"time":1539180803359,"blocks":[{"type":"header","data":{"text":"test","level":2}}]}'; + $editor = new EditorJS($data, $this->configuration); + $result = $editor->getBlocks(); + + $this->assertEquals('test', $result[0]['data']['text']); + } + + public function testAllTagsPurifier() + { + $data = '{"time":1539180803359,"blocks":[{"type":"raw","data":{"html": "
Any HTML code
"}}]}'; + $editor = new EditorJS($data, $this->configuration); + $result = $editor->getBlocks(); + + $this->assertEquals('
Any HTML code
', $result[0]['data']['html']); + } +} diff --git a/tests/SyntaxSugarTest.php b/tests/SyntaxSugarTest.php new file mode 100644 index 0000000..d4f3a3e --- /dev/null +++ b/tests/SyntaxSugarTest.php @@ -0,0 +1,83 @@ +configuration = file_get_contents(SyntaxSugarTest::CONFIGURATION_FILE); + } + + public function testShortTypeField() + { + $data = '{"blocks":[{"type":"header","data":{"text":"CodeX Editor", "level": 2}}]}'; + + $editor = new EditorJS($data, $this->configuration); + $result = $editor->getBlocks(); + + $this->assertEquals('CodeX Editor', $result[0]['data']['text']); + $this->assertEquals(2, $result[0]['data']['level']); + } + + public function testShortTypeFieldCanBeOnly() + { + $callable = function () { + new EditorJS('{"blocks":[{"type":"header","data":{"text":"CodeX Editor", "level": 5}}]}', + $this->configuration); + }; + + $this->assertException($callable, EditorJSException::class, null, 'Option \'level\' with value `5` has invalid value. Check canBeOnly param.'); + } + + public function testShortIntValid() + { + new EditorJS('{"blocks":[{"type":"subtitle","data":{"text": "string", "level": 1337}}]}', $this->configuration); + } + + public function testShortIntNotValid() + { + $callable = function () { + new EditorJS('{"blocks":[{"type":"subtitle","data":{"text": "test", "level": "string"}}]}', $this->configuration); + }; + + $this->assertException($callable, EditorJSException::class, null, 'Option \'level\' with value `string` must be integer'); + } + + public function testInvalidType() + { + $callable = function () { + $invalid_configuration = '{"tools": {"header": {"title": "invalid_type"}}}'; + new EditorJS('{"blocks":[{"type":"header","data":{"title": "test"}}]}', $invalid_configuration); + }; + + $this->assertException($callable, EditorJSException::class, null, 'Unhandled type `invalid_type`'); + } + + public function testMixedStructure() + { + $data = '{"time":1539180803359,"blocks":[{"type":"header","data":{"text":"test","level":2}}, {"type":"quote","data":{"text":"test","caption":"", "alignment":"left"}}]}'; + $editor = new EditorJS($data, $this->configuration); + $result = $editor->getBlocks(); + + $this->assertEquals(2, count($result)); + $this->assertEquals('test', $result[0]['data']['text']); + $this->assertEquals('test', $result[1]['data']['text']); + } +} diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 19e150a..7074067 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -1,7 +1,7 @@ configuration); + new EditorJS('{"blocks":[{"type":"test","data":{"bool_test":"not boolean"}}]}', $this->configuration); }; - $this->assertException($callable_not_bool, CodexEditorException::class, null, 'Option \'bool_test\' with value `not boolean` must be boolean'); + $this->assertException($callable_not_bool, EditorJSException::class, null, 'Option \'bool_test\' with value `not boolean` must be boolean'); } public function testBooleanValid() { - new CodexEditor('{"blocks":[{"type":"test","data":{"bool_test":true}}]}', $this->configuration); + new EditorJS('{"blocks":[{"type":"test","data":{"bool_test":true}}]}', $this->configuration); } public function testIntegerValid() { - new CodexEditor('{"blocks":[{"type":"test","data":{"int_test": 5}}]}', $this->configuration); + new EditorJS('{"blocks":[{"type":"test","data":{"int_test": 5}}]}', $this->configuration); } public function testIntegerFailed() { $callable = function () { - new CodexEditor('{"blocks":[{"type":"test","data":{"int_test": "not integer"}}]}', $this->configuration); + new EditorJS('{"blocks":[{"type":"test","data":{"int_test": "not integer"}}]}', $this->configuration); }; - $this->assertException($callable, CodexEditorException::class, null, 'Option \'int_test\' with value `not integer` must be integer'); + $this->assertException($callable, EditorJSException::class, null, 'Option \'int_test\' with value `not integer` must be integer'); } public function testStringValid() { - new CodexEditor('{"blocks":[{"type":"test","data":{"string_test": "string"}}]}', $this->configuration); + new EditorJS('{"blocks":[{"type":"test","data":{"string_test": "string"}}]}', $this->configuration); } public function testStringFailed() { $callable = function () { - new CodexEditor('{"blocks":[{"type":"test","data":{"string_test": 17}}]}', $this->configuration); + new EditorJS('{"blocks":[{"type":"test","data":{"string_test": 17}}]}', $this->configuration); }; - $this->assertException($callable, CodexEditorException::class, null, 'Option \'string_test\' with value `17` must be string'); + $this->assertException($callable, EditorJSException::class, null, 'Option \'string_test\' with value `17` must be string'); + } + + public function testAllowedNullNotRequired() + { + new EditorJS('{"blocks":[{"type":"test","data":{"int_test": null}}]}', $this->configuration); + } + + public function testDisallowedNullNotRequired() + { + $callable = function () { + new EditorJS('{"blocks":[{"type":"test","data":{"string_test": null}}]}', $this->configuration); + }; + + $this->assertException($callable, EditorJSException::class, null, 'string_test\' with value `` must be string'); + } + + public function testNullRequired() + { + new EditorJS('{"blocks":[{"type":"test","data":{"string_test": "qwe"}}]}', file_get_contents(TypeTest::CONFIGURATION_FILE_REQUIRED)); + + $callable = function () { + new EditorJS('{"blocks":[{"type":"test","data":{"string_test": null}}]}', file_get_contents(TypeTest::CONFIGURATION_FILE_REQUIRED)); + }; + $this->assertException($callable, EditorJSException::class, null, 'Not found required param `string_test`'); } } diff --git a/tests/samples/purify-test-config.json b/tests/samples/purify-test-config.json new file mode 100644 index 0000000..8f9dc83 --- /dev/null +++ b/tests/samples/purify-test-config.json @@ -0,0 +1,33 @@ +{ + "tools": { + "header": { + "text": { + "type": "string", + "allowedTags": "mark" + }, + "level": { + "type": "int", + "canBeOnly": [2, 3, 4] + } + }, + "quote": { + "text": { + "type": "string", + "allowedTags": "i,b,u" + }, + "caption": { + "type": "string" + }, + "alignment": { + "type": "string", + "canBeOnly": ["left", "center"] + } + }, + "raw": { + "html": { + "type": "string", + "allowedTags": "*" + } + } + } +} \ No newline at end of file diff --git a/tests/samples/syntax-sugar.json b/tests/samples/syntax-sugar.json new file mode 100644 index 0000000..cb1f8a3 --- /dev/null +++ b/tests/samples/syntax-sugar.json @@ -0,0 +1,20 @@ +{ + "tools": { + "header": { + "text": "string", + "level": [2, 3, 4] + }, + "subtitle": { + "text": "string", + "level": "int" + }, + "quote": { + "text": { + "type": "string", + "allowedTags": "i,b" + }, + "caption": "string", + "alignment": ["left", "center"] + } + } +} \ No newline at end of file diff --git a/tests/samples/test-config.json b/tests/samples/test-config.json index ef26367..bb00c55 100644 --- a/tests/samples/test-config.json +++ b/tests/samples/test-config.json @@ -19,7 +19,7 @@ "list": { "style": { "type": "string", - "canBeOnly": ["ordered", "numbered"] + "canBeOnly": ["ordered", "unordered"] }, "items": { "type": "array", @@ -33,14 +33,41 @@ }, "quote": { "text": { - "type": "string" + "type": "string", + "allowedTags": "i,b,u" }, "caption": { "type": "string" }, - "size": { + "alignment": { "type": "string", - "canBeOnly": ["left", "right"] + "canBeOnly": ["left", "center"] + } + }, + "table": { + "header": { + "type": "array", + "data": { + "description": { + "type": "string" + }, + "author": { + "type": "string" + } + } + }, + "rows": { + "type": "array", + "data": { + "-": { + "type": "array", + "data": { + "-": { + "type": "string" + } + } + } + } } } } diff --git a/tests/samples/type-test-config-required.json b/tests/samples/type-test-config-required.json new file mode 100644 index 0000000..c92aec7 --- /dev/null +++ b/tests/samples/type-test-config-required.json @@ -0,0 +1,11 @@ +{ + "tools": { + "test": { + "string_test": { + "type": "string", + "required": true, + "allow_null": true + } + } + } +} \ No newline at end of file diff --git a/tests/samples/type-test-config.json b/tests/samples/type-test-config.json index 286e834..e2b1fed 100644 --- a/tests/samples/type-test-config.json +++ b/tests/samples/type-test-config.json @@ -7,11 +7,13 @@ }, "int_test": { "type": "integer", - "required": false + "required": false, + "allow_null": true }, "string_test": { "type": "string", - "required": false + "required": false, + "allow_null": false } } }