Skip to content

Commit

Permalink
Merge pull request #1 from Moxio/multi-letter
Browse files Browse the repository at this point in the history
Add (optional) support for multi-letter list markers
  • Loading branch information
aboks authored Oct 25, 2022
2 parents 4849c16 + 18bfc6e commit 36972d0
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 49 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,24 @@ Supported configuration options:

Because the ordinal indicator is commonly confused with other characters like the degree symbol, these
characters are tolerated and considered equivalent to the ordinal indicator.
* `allow_multi_letter` - Whether to allow multi-letter alphabetic numerals, to number lists beyond 26
(default: `false`). If this option is enabled,
input like
```markdown
AA. foo
AB. bar
AC. baz
```
will be converted to
```html
<ol type="A" start="27">
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ol>
```
Multi-letter alphabetic numerals can consist of at most 3 characters, which should be enough for a
typical list.

Versioning
----------
Expand Down
10 changes: 5 additions & 5 deletions src/ListParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function parse(ContextInterface $context, Cursor $cursor): bool
$number = null;
$numberingType = null;
$hasOrdinalIndicator = false;
} elseif (($matches = RegexHelper::matchAll('/^(\d{1,9}|[a-z]|[A-Z]|[ivxlcdm]+|[IVXLCDM]+|#)([\x{00BA}\x{00B0}\x{02DA}\x{1D52}]?)([.)])/u', $rest)) && (!($context->getContainer() instanceof Paragraph) || in_array($matches[1], ['1', 'a', 'A', 'i', 'I', '#'], true))) {
} elseif (($matches = RegexHelper::matchAll('/^(\d{1,9}|[a-z]{1,3}|[A-Z]{1,3}|[ivxlcdm]+|[IVXLCDM]+|#)([\x{00BA}\x{00B0}\x{02DA}\x{1D52}]?)([.)])/u', $rest)) && (!($context->getContainer() instanceof Paragraph) || in_array($matches[1], ['1', 'a', 'A', 'i', 'I', '#'], true))) {
$data = new ListData();
$data->markerOffset = $indent;
$data->type = ListBlock::TYPE_ORDERED;
Expand All @@ -102,14 +102,14 @@ public function parse(ContextInterface $context, Cursor $cursor): bool
} catch (RomansLexerException|RomansParserException $e) {
$isValidRoman = false;
}
$isValidAlpha = strlen($matches[1]) === 1;
$isValidAlpha = strlen($matches[1]) === 1 || $this->config->get('allow_multi_letter', false);
$preferRomanOverAlpha = $withinRomanList || (!$withinAlphaList && $matches[1] === 'I');

if ($isValidRoman && (!$isValidAlpha || $preferRomanOverAlpha)) {
$data->start = $parsedRomanNumber;
$numberingType = 'I';
} else if ($isValidAlpha) {
$data->start = ord($matches[1]) - ord('A') + 1;
$data->start = NumberingUtils::convertAlphaMarkerToOrdinalNumber($matches[1]);
$numberingType = 'A';
} else {
return false;
Expand All @@ -123,14 +123,14 @@ public function parse(ContextInterface $context, Cursor $cursor): bool
} catch (RomansLexerException|RomansParserException $e) {
$isValidRoman = false;
}
$isValidAlpha = strlen($matches[1]) === 1;
$isValidAlpha = strlen($matches[1]) === 1 || $this->config->get('allow_multi_letter', false);
$preferRomanOverAlpha = $withinRomanList || (!$withinAlphaList && $matches[1] === 'i');

if ($isValidRoman && (!$isValidAlpha || $preferRomanOverAlpha)) {
$data->start = $parsedRomanNumber;
$numberingType = 'i';
} else if ($isValidAlpha) {
$data->start = ord($matches[1]) - ord('a') + 1;
$data->start = NumberingUtils::convertAlphaMarkerToOrdinalNumber($matches[1]);
$numberingType = 'a';
} else {
return false;
Expand Down
16 changes: 16 additions & 0 deletions src/NumberingUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
namespace Moxio\CommonMark\Extension\FancyLists;

class NumberingUtils
{
public static function convertAlphaMarkerToOrdinalNumber(string $alphaMarker): int
{
$lastLetterValue = ord(strtolower($alphaMarker[-1])) - ord('a') + 1;
if (strlen($alphaMarker) > 1) {
$prefixValue = self::convertAlphaMarkerToOrdinalNumber(substr($alphaMarker, 0, -1));
return $prefixValue * 26 + $lastLetterValue;
} else {
return $lastLetterValue;
}
}
}
26 changes: 26 additions & 0 deletions test/AbstractIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
namespace Moxio\CommonMark\Extension\FancyLists\Test;

use League\CommonMark\DocParser;
use League\CommonMark\Environment;
use League\CommonMark\HtmlRenderer;
use Moxio\CommonMark\Extension\FancyLists\FancyListsExtension;
use PHPUnit\Framework\TestCase;

abstract class AbstractIntegrationTest extends TestCase
{
protected function assertMarkdownIsConvertedTo(string $expectedHtml, string $markdown, ?array $config = null): void
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new FancyListsExtension());
if ($config !== null) {
$environment->setConfig($config);
}

$parser = new DocParser($environment);
$renderer = new HtmlRenderer($environment);
$actualOutput = $renderer->renderBlock($parser->parse($markdown));

$this->assertXmlStringEqualsXmlString("<html>$expectedHtml</html>", "<html>$actualOutput</html>");
}
}
23 changes: 1 addition & 22 deletions test/IntegrationTest.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
<?php
namespace Moxio\CommonMark\Extension\FancyLists\Test;

use League\CommonMark\DocParser;
use League\CommonMark\Environment;
use League\CommonMark\HtmlRenderer;
use Moxio\CommonMark\Extension\FancyLists\FancyListsExtension;
use PHPUnit\Framework\TestCase;

class IntegrationTest extends TestCase
class IntegrationTest extends AbstractIntegrationTest
{
// Testcase from https://spec.commonmark.org/0.29/#example-272
public function testDoesNotAlterOrdinaryOrderedListSyntax(): void
Expand Down Expand Up @@ -471,19 +465,4 @@ public function testRequiresTwoSpacesAfterACapitalLetterAndAPeriod(): void

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown);
}

public function assertMarkdownIsConvertedTo(string $expectedHtml, string $markdown, ?array $config = null): void
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new FancyListsExtension());
if ($config !== null) {
$environment->setConfig($config);
}

$parser = new DocParser($environment);
$renderer = new HtmlRenderer($environment);
$actualOutput = $renderer->renderBlock($parser->parse($markdown));

$this->assertXmlStringEqualsXmlString("<html>$expectedHtml</html>", "<html>$actualOutput</html>");
}
}
117 changes: 117 additions & 0 deletions test/MultiLetterIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
namespace Moxio\CommonMark\Extension\FancyLists\Test;

class MultiLetterIntegrationTest extends AbstractIntegrationTest
{
public function testDoesNotSupportMultiLetterListMarkersByDefault(): void
{
$markdown = <<<MD
AA) foo
AB) bar
AC) baz
MD;
$expectedHtml = <<<HTML
<p>AA) foo
AB) bar
AC) baz</p>
HTML;

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown);
}

public function testSupportsMultiLetterListMarkersIfEnabledInConfiguration(): void
{
$markdown = <<<MD
AA) foo
AB) bar
AC) baz
MD;
$expectedHtml = <<<HTML
<ol type="A" start="27">
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ol>
HTML;

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown, [
'allow_multi_letter' => true,
]);
}

public function testSupportsContinuingASingleLetterListWithMultiLetterListMarkers(): void
{
$markdown = <<<MD
Z) foo
AA) bar
AB) baz
MD;
$expectedHtml = <<<HTML
<ol type="A" start="26">
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ol>
HTML;

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown, [
'allow_multi_letter' => true,
]);
}

public function testSupportsLowercaseMultiLetterListMarkers(): void
{
$markdown = <<<MD
aa) foo
ab) bar
ac) baz
MD;
$expectedHtml = <<<HTML
<ol type="a" start="27">
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ol>
HTML;

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown, [
'allow_multi_letter' => true,
]);
}

public function testAllowsAtMost3CharactersForMultiLetterListMarkers(): void
{
$markdown = <<<MD
AAAA) foo
AAAB) bar
AAAC) baz
MD;
$expectedHtml = <<<HTML
<p>AAAA) foo
AAAB) bar
AAAC) baz</p>
HTML;

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown, [
'allow_multi_letter' => true,
]);
}

public function testDoesNotSupportMixingUppercaseAndLowercaseLetters(): void
{
$markdown = <<<MD
Aa) foo
Ab) bar
Ac) baz
MD;
$expectedHtml = <<<HTML
<p>Aa) foo
Ab) bar
Ac) baz</p>
HTML;

$this->assertMarkdownIsConvertedTo($expectedHtml, $markdown, [
'allow_multi_letter' => true,
]);
}
}
34 changes: 34 additions & 0 deletions test/NumberingUtilsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
namespace Moxio\CommonMark\Extension\FancyLists\Test;

use Moxio\CommonMark\Extension\FancyLists\NumberingUtils;
use PHPUnit\Framework\TestCase;

class NumberingUtilsTest extends TestCase
{
public function testCanConvertAlphaListMarkersToOrdinalNumber(): void
{
$this->assertSame(1, NumberingUtils::convertAlphaMarkerToOrdinalNumber("A"));
$this->assertSame(2, NumberingUtils::convertAlphaMarkerToOrdinalNumber("B"));
$this->assertSame(26, NumberingUtils::convertAlphaMarkerToOrdinalNumber("Z"));
$this->assertSame(27, NumberingUtils::convertAlphaMarkerToOrdinalNumber("AA"));
$this->assertSame(52, NumberingUtils::convertAlphaMarkerToOrdinalNumber("AZ"));
$this->assertSame(53, NumberingUtils::convertAlphaMarkerToOrdinalNumber("BA"));
$this->assertSame(677, NumberingUtils::convertAlphaMarkerToOrdinalNumber("ZA"));
$this->assertSame(702, NumberingUtils::convertAlphaMarkerToOrdinalNumber("ZZ"));
$this->assertSame(703, NumberingUtils::convertAlphaMarkerToOrdinalNumber("AAA"));
}

public function testCanAlsoConvertLowercaseAlphaListMarkers(): void
{
$this->assertSame(1, NumberingUtils::convertAlphaMarkerToOrdinalNumber("a"));
$this->assertSame(2, NumberingUtils::convertAlphaMarkerToOrdinalNumber("b"));
$this->assertSame(26, NumberingUtils::convertAlphaMarkerToOrdinalNumber("z"));
$this->assertSame(27, NumberingUtils::convertAlphaMarkerToOrdinalNumber("aa"));
$this->assertSame(52, NumberingUtils::convertAlphaMarkerToOrdinalNumber("az"));
$this->assertSame(53, NumberingUtils::convertAlphaMarkerToOrdinalNumber("ba"));
$this->assertSame(677, NumberingUtils::convertAlphaMarkerToOrdinalNumber("za"));
$this->assertSame(702, NumberingUtils::convertAlphaMarkerToOrdinalNumber("zz"));
$this->assertSame(703, NumberingUtils::convertAlphaMarkerToOrdinalNumber("aaa"));
}
}
23 changes: 1 addition & 22 deletions test/OrdinalIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
<?php
namespace Moxio\CommonMark\Extension\FancyLists\Test;

use League\CommonMark\DocParser;
use League\CommonMark\Environment;
use League\CommonMark\HtmlRenderer;
use Moxio\CommonMark\Extension\FancyLists\FancyListsExtension;
use PHPUnit\Framework\TestCase;

class OrdinalIntegrationTest extends TestCase
class OrdinalIntegrationTest extends AbstractIntegrationTest
{
public function testDoesNotSupportAnOrdinalIndicatorByDefault(): void
{
Expand Down Expand Up @@ -112,19 +106,4 @@ public function testToleratesCharactersCommonlyMistakenForOrdinalIndicators(): v
'allow_ordinal' => true,
]);
}

public function assertMarkdownIsConvertedTo(string $expectedHtml, string $markdown, ?array $config = null): void
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new FancyListsExtension());
if ($config !== null) {
$environment->setConfig($config);
}

$parser = new DocParser($environment);
$renderer = new HtmlRenderer($environment);
$actualOutput = $renderer->renderBlock($parser->parse($markdown));

$this->assertXmlStringEqualsXmlString("<html>$expectedHtml</html>", "<html>$actualOutput</html>");
}
}

0 comments on commit 36972d0

Please sign in to comment.