From 84ef58cee0f14fa6b3007adb60123e251615689c Mon Sep 17 00:00:00 2001 From: JoshyPHP Date: Wed, 8 Apr 2020 20:37:24 +0200 Subject: [PATCH] WiP --- docs/Plugins/TaskLists/Synopsis.md | 90 +++++++++ mkdocs.yml | 2 + phpunit.xml | 3 + scripts/patchDocs.php | 6 + src/Plugins/TaskLists/Configurator.php | 67 +++++++ src/Plugins/TaskLists/Helper.php | 108 ++++++++++ src/Plugins/TaskLists/filterListItem.js | 26 +++ .../RendererGenerators/PHPTest.php | 9 +- .../Plugins/ParsingTestsJavaScriptRunner.php | 2 +- tests/Plugins/TaskLists/ConfiguratorTest.php | 37 ++++ tests/Plugins/TaskLists/HelperTest.php | 188 ++++++++++++++++++ tests/Plugins/TaskLists/ParserTest.php | 133 +++++++++++++ 12 files changed, 666 insertions(+), 5 deletions(-) create mode 100644 docs/Plugins/TaskLists/Synopsis.md create mode 100644 src/Plugins/TaskLists/Configurator.php create mode 100644 src/Plugins/TaskLists/Helper.php create mode 100644 src/Plugins/TaskLists/filterListItem.js create mode 100644 tests/Plugins/TaskLists/ConfiguratorTest.php create mode 100644 tests/Plugins/TaskLists/HelperTest.php create mode 100644 tests/Plugins/TaskLists/ParserTest.php diff --git a/docs/Plugins/TaskLists/Synopsis.md b/docs/Plugins/TaskLists/Synopsis.md new file mode 100644 index 0000000000..582b448ea5 --- /dev/null +++ b/docs/Plugins/TaskLists/Synopsis.md @@ -0,0 +1,90 @@ +This plugin implements task lists, a form of markup compatible with GitHub/GitLab Flavored Markdown and other dialects. + +This plugin requires a `LI` tag to function properly. If there is no `LI` tag defined when the plugin is initialized, the Litedown plugin is automatically loaded. + +Task lists are + + +### Syntax + +```md +- [x] Checked +- [ ] Unchecked +``` + + +### References + + - + - + - + + +## Examples + +Note that all of the reference outputs, random IDs have been replaced with a `...` placeholder for convenience. + +```php +$configurator = new s9e\TextFormatter\Configurator; +$configurator->Litedown; +$configurator->TaskLists; + +// Get an instance of the parser and the renderer +extract($configurator->finalize()); + +$text = "- [x] checked\n" + . "- [X] Checked\n" + . "- [ ] unchecked"; +$xml = $parser->parse($text); +$html = $renderer->render($xml); + +echo $html; +``` +```html +
  • checked
  • +
  • Checked
  • +
  • unchecked
+``` + + +### Allow tasks to be toggled + +Setting the `TASKLISTS_EDITABLE` parameter to a non-empty value will make tasks editable. + +```php +$configurator = new s9e\TextFormatter\Configurator; +$configurator->Litedown; +$configurator->TaskLists; + +extract($configurator->finalize()); + +$text = "- [x] checked\n" + . "- [ ] unchecked"; +$xml = $parser->parse($text); +$html = $renderer->render($xml); + +echo $html, "\n\n"; + +// Render it again but make the tasks editable +$renderer->setParameter('TASKLISTS_EDITABLE', '1'); + +echo $renderer->render($xml); + +``` +```html +
  • checked
  • +
  • unchecked
+ +
  • checked
  • +
  • unchecked
+``` + + +### Styling task lists + +```css +ul > li[data-task-id] +{ + list-style-type: none; +} +``` diff --git a/mkdocs.yml b/mkdocs.yml index 45c97eeb5d..5e003a452a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -102,6 +102,8 @@ nav: - Preg: - Synopsis: Plugins/Preg/Synopsis.md - Practical examples: Plugins/Preg/Practical_examples.md + - TaskLists: + - Synopsis: Plugins/TaskLists/Synopsis.md - Your own plugin: - Plug your own parser: Plugins/Your_own_plugin/Register_parser.md - Create tags: Plugins/Your_own_plugin/Create_tags.md diff --git a/phpunit.xml b/phpunit.xml index 3dc6618c49..96260ded18 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -290,6 +290,9 @@ tests/Plugins/PipeTables/ParserTest.php tests/Plugins/Preg/ConfiguratorTest.php tests/Plugins/Preg/ParserTest.php + tests/Plugins/TaskLists/ConfiguratorTest.php + tests/Plugins/TaskLists/ParserTest.php + tests/Plugins/TaskLists/HelperTest.php tests/Configurator/RendererGenerators/PHP/XPathConvertor/Convertors/BooleanFunctionsTest.php tests/Configurator/RendererGenerators/PHP/XPathConvertor/Convertors/BooleanOperatorsTest.php diff --git a/scripts/patchDocs.php b/scripts/patchDocs.php index feca3e5ee9..db705cbbe7 100755 --- a/scripts/patchDocs.php +++ b/scripts/patchDocs.php @@ -35,6 +35,12 @@ function ($m) eval($php); $output = rtrim(ob_get_clean(), "\n"); + // Replace generated IDs with a placeholder + if (strpos($output, 'task-id') !== false) + { + $output = preg_replace('(task-id="\\K\\w++)', '...', $output); + } + return $m['block'] . "\n" . $m['open'] . "\n" . $output . "\n" . $m['close']; }, $file diff --git a/src/Plugins/TaskLists/Configurator.php b/src/Plugins/TaskLists/Configurator.php new file mode 100644 index 0000000000..e920cc9cb9 --- /dev/null +++ b/src/Plugins/TaskLists/Configurator.php @@ -0,0 +1,67 @@ +configurator->tags['LI'])) + { + $this->configurator->Litedown; + } + + $this->createTaskTag(); + $this->configureListItemTag($this->configurator->tags['LI']); + } + + protected function configureListItemTag(Tag $tag): void + { + $tag->filterChain->append(Helper::class . '::filterListItem') + ->resetParameters() + ->addParameterByName('parser') + ->addParameterByName('tag') + ->addParameterByName('text') + ->setJS(file_get_contents(__DIR__ . '/filterListItem.js')); + + $tag->template = preg_replace( + '(]*+>(?!)\\K)', + ' + + + + + + + ', + $tag->template + ); + } + + protected function createTaskTag(): void + { + $tag = $this->configurator->tags->add('TASK'); + $tag->attributes->add('id')->filterChain->append('#identifier'); + $tag->attributes->add('state')->filterChain->append('#identifier'); + $tag->template = ' + + + '; + } +} \ No newline at end of file diff --git a/src/Plugins/TaskLists/Helper.php b/src/Plugins/TaskLists/Helper.php new file mode 100644 index 0000000000..ba5708af9c --- /dev/null +++ b/src/Plugins/TaskLists/Helper.php @@ -0,0 +1,108 @@ +getPos() + $listItem->getLen(); + $pos += strspn($text, ' ', $pos); + $str = substr($text, $pos, 3); + if (!preg_match('/\\[[ Xx]\\]/', $str)) + { + return; + } + + // Create a tag for the task and assign it a random ID + $taskId = uniqid(); + $taskState = ($str === '[ ]') ? 'incomplete' : 'complete'; + + $task = $parser->addSelfClosingTag('TASK', $pos, 3); + $task->setAttribute('id', $taskId); + $task->setAttribute('state', $taskState); + } + + /** + * Return stats from a parsed representation + * + * @param string $xml Parsed XML + * @return array Number of "complete" and "incomplete" tasks + */ + public static function getStats(string $xml): array + { + $stats = ['complete' => 0, 'incomplete' => 0]; + + preg_match_all('(]*+)>[^<]*+(?=))', + function ($m) use ($state, $marker) + { + preg_match_all('( ([^=]++)="[^"]*+")', $m[1], $m); + + $attributes = array_combine($m[1], $m[0]); + $attributes['state'] = ' state="' . $state . '"'; + ksort($attributes); + + return implode('', $attributes) . '>[' . $marker . ']'; + }, + $xml + ); + } +} \ No newline at end of file diff --git a/src/Plugins/TaskLists/filterListItem.js b/src/Plugins/TaskLists/filterListItem.js new file mode 100644 index 0000000000..5f247ad31a --- /dev/null +++ b/src/Plugins/TaskLists/filterListItem.js @@ -0,0 +1,26 @@ +/** +* @param {!Tag} listItem +* @param {string} text +*/ +function (listItem, text) +{ + // Test whether the list item is followed by a task checkbox + var pos = listItem.getPos() + listItem.getLen(); + while (text.charAt(pos) === ' ') + { + ++pos; + } + var str = text.substr(pos, 3); + if (!/\[[ Xx]\]/.test(str)) + { + return; + } + + // Create a tag for the task and assign it a random ID + var taskId = Math.random().toString(16).substr(2), + taskState = (str === '[ ]') ? 'incomplete' : 'complete', + task = addSelfClosingTag('TASK', pos, 3); + + task.setAttribute('id', taskId); + task.setAttribute('state', taskState); +} \ No newline at end of file diff --git a/tests/Configurator/RendererGenerators/PHPTest.php b/tests/Configurator/RendererGenerators/PHPTest.php index b1029586b4..8fd5a0b758 100644 --- a/tests/Configurator/RendererGenerators/PHPTest.php +++ b/tests/Configurator/RendererGenerators/PHPTest.php @@ -1551,7 +1551,7 @@ public function getVoidTests($type) * @testdox Tests from plugins * @dataProvider getPluginsTests */ - public function testPlugins($pluginName, $original, $expected, array $pluginOptions = [], $setup = null) + public function testPlugins($pluginName, $original, $expected, array $pluginOptions = [], $setup = null, $assertMethod = 'assertSame') { $this->configurator->rendering->engine = 'PHP'; $this->configurator->rendering->engine->enableQuickRenderer = false; @@ -1578,7 +1578,7 @@ public function testPlugins($pluginName, $original, $expected, array $pluginOpti extract($this->configurator->finalize()); - $this->assertSame($expected, $renderer->render($parser->parse($original))); + $this->$assertMethod($expected, $renderer->render($parser->parse($original))); } /** @@ -1587,7 +1587,7 @@ public function testPlugins($pluginName, $original, $expected, array $pluginOpti * @requires extension tokenizer * @covers s9e\TextFormatter\Configurator\RendererGenerators\PHP\Quick */ - public function testPluginsQuick($pluginName, $original, $expected, array $pluginOptions = [], $setup = null) + public function testPluginsQuick($pluginName, $original, $expected, array $pluginOptions = [], $setup = null, $assertMethod = 'assertSame') { $this->testPlugins( $pluginName, @@ -1602,7 +1602,8 @@ function ($configurator, $plugin) use ($setup) { $setup($configurator, $plugin); } - } + }, + $assertMethod ); } diff --git a/tests/Plugins/ParsingTestsJavaScriptRunner.php b/tests/Plugins/ParsingTestsJavaScriptRunner.php index c44293aa08..ff0d7a8a74 100644 --- a/tests/Plugins/ParsingTestsJavaScriptRunner.php +++ b/tests/Plugins/ParsingTestsJavaScriptRunner.php @@ -27,6 +27,6 @@ public function testJavaScriptParsing($original, $expected, array $pluginOptions $setup($this->configurator, $plugin); } - $this->assertJSParsing($original, $expected); + $this->assertJSParsing($original, $expected, $assertMethod); } } \ No newline at end of file diff --git a/tests/Plugins/TaskLists/ConfiguratorTest.php b/tests/Plugins/TaskLists/ConfiguratorTest.php new file mode 100644 index 0000000000..35e6bf5442 --- /dev/null +++ b/tests/Plugins/TaskLists/ConfiguratorTest.php @@ -0,0 +1,37 @@ +configurator->plugins->load('TaskLists'); + $this->assertTrue($this->configurator->tags->exists('TASK')); + + $tag = $this->configurator->tags->get('TASK'); + + $this->assertTrue($tag->attributes->exists('id')); + $this->assertTrue($tag->attributes['id']->filterChain->contains(new IdentifierFilter)); + + $this->assertTrue($tag->attributes->exists('state')); + $this->assertTrue($tag->attributes['state']->filterChain->contains(new IdentifierFilter)); + } + + /** + * @testdox Returns no config + */ + public function testAsConfig() + { + $this->assertNull($this->configurator->TaskLists->asConfig()); + } +} \ No newline at end of file diff --git a/tests/Plugins/TaskLists/HelperTest.php b/tests/Plugins/TaskLists/HelperTest.php new file mode 100644 index 0000000000..70254d296c --- /dev/null +++ b/tests/Plugins/TaskLists/HelperTest.php @@ -0,0 +1,188 @@ +configurator->TaskLists; + } + + /** + * @dataProvider getGetStatsTests + */ + public function testGetStats($text, $expected) + { + $xml = $this->getParser()->parse(implode("\n", $text)); + + $this->assertEquals($expected, Helper::getStats($xml)); + } + + public function getGetStatsTests() + { + return [ + [ + [ + '...' + ], + [ + 'complete' => 0, + 'incomplete' => 0 + ] + ], + [ + [ + '- [x] checked', + '- [X] Checked', + '- [ ] unchecked' + ], + [ + 'complete' => 2, + 'incomplete' => 1 + ] + ], + ]; + } + + /** + * @testdox getStats() counts custom states + */ + public function testGetStatsCustom() + { + $xml = ''; + $expected = ['complete' => 0, 'custom' => 1, 'incomplete' => 0]; + + $this->assertEquals($expected, Helper::getStats($xml)); + } + + /** + * @dataProvider getSetTaskStateTests + */ + public function testSetTaskState($methodName, $xml, $id, $expected) + { + $xml = implode("\n", $xml); + $expected = implode("\n", $expected); + + $this->assertEquals($expected, Helper::$methodName($xml, $id)); + } + + public function getSetTaskStateTests() + { + return [ + [ + 'setTaskComplete', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ], + '345', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [x] unchecked
  • ' + ] + ], + [ + 'setTaskIncomplete', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ], + '234', + [ + '
  • - [x] checked
  • ', + '
  • - [ ] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ], + ], + [ + 'setTaskComplete', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ], + '234', + [ + '
  • - [x] checked
  • ', + '
  • - [x] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ] + ], + [ + 'setTaskComplete', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [?] unchecked
  • ' + ], + '345', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [x] unchecked
  • ' + ] + ], + [ + 'setTaskComplete', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ], + '111', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ] + ], + [ + 'setTaskComplete', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ], + '345', + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [x] unchecked
  • ' + ] + ], + [ + 'setTaskComplete', + [ + '[ ]' + ], + '123', + [ + '[x]' + ] + ], + [ + 'setTaskComplete', + [ + // Cannot happen under normal circumstances + '' + ], + '123', + [ + '' + ] + ], + ]; + } +} \ No newline at end of file diff --git a/tests/Plugins/TaskLists/ParserTest.php b/tests/Plugins/TaskLists/ParserTest.php new file mode 100644 index 0000000000..75bc979698 --- /dev/null +++ b/tests/Plugins/TaskLists/ParserTest.php @@ -0,0 +1,133 @@ +configurator->Litedown; + } + + public function getParsingTests() + { + return self::fixTests([ + [ + [ + '- [x] checked', + '- [X] Checked', + '- [ ] unchecked' + ], + [ + '
  • - [x] checked
  • ', + '
  • - [X] Checked
  • ', + '
  • - [ ] unchecked
  • ' + ] + ], + [ + [ + '- [x] checked', + '- none', + '- [ ] unchecked' + ], + [ + '
  • - [x] checked
  • ', + '
  • - none
  • ', + '
  • - [ ] unchecked
  • ' + ] + ], + [ + [ + '[list]', + '[*][x] checked', + '[*][X] Checked', + '[*][ ] unchecked' + ], + [ + '[list]', + '
  • [*][x] checked
  • ', + '
  • [*][X] Checked
  • ', + '
  • [*][ ] unchecked
  • ' + ], + [], + function ($configurator) + { + $configurator->BBCodes->add('LIST'); + $configurator->BBCodes->add('*')->tagName = 'LI'; + $configurator->rulesGenerator->remove('ManageParagraphs'); + } + ], + ]); + } + + public function getRenderingTests() + { + return self::fixTests([ + [ + [ + '- [x] checked', + '- [X] Checked', + '- [ ] unchecked' + ], + [ + '
    • checked
    • ', + '
    • Checked
    • ', + '
    • unchecked
    ' + ] + ], + [ + [ + '- [x] checked', + '- [ ] unchecked' + ], + [ + '
    • checked
    • ', + '
    • unchecked
    ' + ], + [], + function ($configurator) + { + $configurator->rendering->parameters['TASKLISTS_EDITABLE'] = '1'; + } + ], + ]); + } + + protected static function fixTests($tests) + { + foreach ($tests as &$test) + { + if (is_array($test[0])) + { + $test[0] = implode("\n", $test[0]); + } + if (is_array($test[1])) + { + $test[1] = implode("\n", $test[1]); + } + + $test += [null, null, [], null, null, null]; + $i = preg_match('(^<[rt][ />])', $test[1]) ? 5 : 4; + $test[$i] = 'assertMatchesRegularExpression'; + + $test[1] = '(^' . str_replace('\\?', '\\w++', preg_quote($test[1])) . '$)D'; + } + + return $tests; + } +} \ No newline at end of file