Skip to content

Commit

Permalink
WiP
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshyPHP committed Apr 8, 2020
1 parent 9bdaea6 commit 84ef58c
Show file tree
Hide file tree
Showing 12 changed files with 666 additions and 5 deletions.
90 changes: 90 additions & 0 deletions 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

- <https://docs.gitlab.com/ee/user/markdown.html#task-lists>
- <https://github.com/mity/md4c/wiki/Markdown-Syntax:-Task-Lists>
- <https://github.github.com/gfm/#task-list-items-extension->


## 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
<ul><li data-task-id="..."><input data-task-id="..." type="checkbox" checked disabled> checked</li>
<li data-task-id="..."><input data-task-id="..." type="checkbox" checked disabled> Checked</li>
<li data-task-id="..."><input data-task-id="..." type="checkbox" disabled> unchecked</li></ul>
```


### 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
<ul><li data-task-id="..."><input data-task-id="..." type="checkbox" checked disabled> checked</li>
<li data-task-id="..."><input data-task-id="..." type="checkbox" disabled> unchecked</li></ul>

<ul><li data-task-id="..."><input data-task-id="..." type="checkbox" checked> checked</li>
<li data-task-id="..."><input data-task-id="..." type="checkbox"> unchecked</li></ul>
```


### Styling task lists

```css
ul > li[data-task-id]
{
list-style-type: none;
}
```
2 changes: 2 additions & 0 deletions mkdocs.yml
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Expand Up @@ -290,6 +290,9 @@
<file>tests/Plugins/PipeTables/ParserTest.php</file>
<file>tests/Plugins/Preg/ConfiguratorTest.php</file>
<file>tests/Plugins/Preg/ParserTest.php</file>
<file>tests/Plugins/TaskLists/ConfiguratorTest.php</file>
<file>tests/Plugins/TaskLists/ParserTest.php</file>
<file>tests/Plugins/TaskLists/HelperTest.php</file>

<file>tests/Configurator/RendererGenerators/PHP/XPathConvertor/Convertors/BooleanFunctionsTest.php</file>
<file>tests/Configurator/RendererGenerators/PHP/XPathConvertor/Convertors/BooleanOperatorsTest.php</file>
Expand Down
6 changes: 6 additions & 0 deletions scripts/patchDocs.php
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions src/Plugins/TaskLists/Configurator.php
@@ -0,0 +1,67 @@
<?php declare(strict_types=1);

/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2020 The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Plugins\TaskLists;

use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Plugins\ConfiguratorBase;

class Configurator extends ConfiguratorBase
{
/**
* {@inheritdoc}
*/
public function asConfig()
{
return;
}

protected function setUp(): void
{
if (!isset($this->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(
'(<li[^>]*+>(?!<xsl:if test="TASK">)\\K)',
'<xsl:if test="TASK">
<xsl:attribute name="data-task-id">
<xsl:value-of select="TASK/@id"/>
</xsl:attribute>
<xsl:attribute name="data-task-state">
<xsl:value-of select="TASK/@state"/>
</xsl:attribute>
</xsl:if>',
$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 = '<input data-task-id="{@id}" type="checkbox">
<xsl:if test="@state = \'complete\'"><xsl:attribute name="checked"/></xsl:if>
<xsl:if test="not($TASKLISTS_EDITABLE)"><xsl:attribute name="disabled"/></xsl:if>
</input>';
}
}
108 changes: 108 additions & 0 deletions src/Plugins/TaskLists/Helper.php
@@ -0,0 +1,108 @@
<?php declare(strict_types=1);

/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2020 The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Plugins\TaskLists;

use s9e\TextFormatter\Parser;
use s9e\TextFormatter\Parser\Tag;

class Helper
{
public static function filterListItem(Parser $parser, Tag $listItem, string $text): void
{
// Test whether the list item is followed by a task checkbox
$pos = $listItem->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('(<TASK(?: [^=]++="[^"]*+")*? state="\\K\\w++)', $xml, $m);
foreach ($m[0] as $state)
{
if (!isset($stats[$state]))
{
$stats[$state] = 0;
}
++$stats[$state];
}

return $stats;
}

/**
* Mark given task complete in XML
*
* @param string $xml Parsed XML
* @param string $id Task's ID
* @return string Updated XML
*/
public static function setTaskComplete(string $xml, string $id): string
{
return self::setTaskState($xml, $id, 'complete', 'x');
}

/**
* Mark given task incomplete in XML
*
* @param string $xml Parsed XML
* @param string $id Task's ID
* @return string Updated XML
*/
public static function setTaskIncomplete(string $xml, string $id): string
{
return self::setTaskState($xml, $id, 'incomplete', ' ');
}

/**
* Change the state and marker of given task in XML
*
* @param string $xml Parsed XML
* @param string $id Task's ID
* @param string $state Task's state ("complete" or "incomplete")
* @param string $marker Task marker ("x" or " ")
* @return string Updated XML
*/
protected static function setTaskState(string $xml, string $id, string $state, string $marker): string
{
return preg_replace_callback(
'(<TASK(?: [^=]++="[^"]*+")*? id="' . $id . '"\\K([^>]*+)>[^<]*+(?=</TASK>))',
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
);
}
}
26 changes: 26 additions & 0 deletions 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);
}
9 changes: 5 additions & 4 deletions tests/Configurator/RendererGenerators/PHPTest.php
Expand Up @@ -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;
Expand All @@ -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)));
}

/**
Expand All @@ -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,
Expand All @@ -1602,7 +1602,8 @@ function ($configurator, $plugin) use ($setup)
{
$setup($configurator, $plugin);
}
}
},
$assertMethod
);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Plugins/ParsingTestsJavaScriptRunner.php
Expand Up @@ -27,6 +27,6 @@ public function testJavaScriptParsing($original, $expected, array $pluginOptions
$setup($this->configurator, $plugin);
}

$this->assertJSParsing($original, $expected);
$this->assertJSParsing($original, $expected, $assertMethod);
}
}

0 comments on commit 84ef58c

Please sign in to comment.