Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/reference/enhancements.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ Works with all list types:
block attribute for that following block; the list and item are
not terminated.

**Stripped attribute names:** `start`, `type`, and `reversed` are HTML
attributes valid only on `<ol>`. When authored on a list item they are
silently dropped from the rendered `<li>` to keep output HTML valid.
To set `<ol start="5">`, use a standard block-attribute line **before**
the list: `{start=5}` followed by `1. ...`.

---

### Table Row and Cell Attributes
Expand Down Expand Up @@ -749,6 +755,9 @@ Attributes can be attached to individual `<dl>`, `<dt>`, and `<dd>` elements:
- `{...}` on line after term → applies to that `<dt>`
- `{...}` as last line in definition block → applies to that `<dd>` (consistent with list items)

**Stripped attribute names:** the same `start`, `type`, `reversed`
strip applies to `<dd>` output.

---

### Table Multi-line Cells, Rowspan, and Colspan
Expand Down
13 changes: 11 additions & 2 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ class HtmlRenderer implements RendererInterface
*/
protected array $nodeRenderers = [];

/**
* Attribute names valid on <ol> but not on <li>/<dd>. Stripped from
* <li>/<dd> output to avoid emitting invalid HTML when authors put
* these names on a list item (e.g. as a postfix {start=5} line).
*
* @var array<int, string>
*/
protected const OL_ONLY_ATTRIBUTES = ['start', 'type', 'reversed'];

public function __construct(protected bool $xhtml = false)
{
$this->sharedRenderContext = new RenderContext();
Expand Down Expand Up @@ -741,7 +750,7 @@ protected function renderList(ListBlock $node): string

protected function renderListItem(ListItem $node, bool $tight = true): string
{
$attrs = $this->renderAttributes($node);
$attrs = $this->renderAttributesExcluding($node, self::OL_ONLY_ATTRIBUTES);
$content = $this->renderChildren($node);

if ($tight) {
Expand Down Expand Up @@ -1239,7 +1248,7 @@ protected function renderDefinitionTerm(DefinitionTerm $node): string

protected function renderDefinitionDescription(DefinitionDescription $node): string
{
$attrs = $this->renderAttributes($node);
$attrs = $this->renderAttributesExcluding($node, self::OL_ONLY_ATTRIBUTES);
$content = $this->renderChildren($node);

// Content goes on separate lines
Expand Down
74 changes: 74 additions & 0 deletions tests/TestCase/DjotConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,80 @@ public function testListItemAttributesLooseListUnchanged(): void
$this->assertStringContainsString('<blockquote>', $result);
}

public function testListItemAttributeStartIsStrippedFromLi(): void
{
// G3: `start` is an HTML attribute valid only on <ol>. It must
// be stripped from <li> output, never feed <ol>, and never
// appear in HTML.
$djot = "1. item\n {start=5}\n";

$result = $this->converter->convert($djot);

$this->assertStringContainsString('<li>', $result);
$this->assertStringNotContainsString('<li start=', $result);
$this->assertStringNotContainsString('<ol start="5"', $result);
}

public function testListItemAttributeTypeIsStrippedFromLi(): void
{
// G3: `type` is an HTML attribute valid only on <ol>. Stripped
// from <li>; never overrides marker-derived <ol type=...>.
$djot = "(a) x\n {type=i}\n";

$result = $this->converter->convert($djot);

$this->assertStringContainsString('<ol type="a">', $result);
$this->assertStringNotContainsString('<li type=', $result);
$this->assertStringNotContainsString('type="i"', $result);
}

public function testListItemAttributeReversedIsStrippedFromLi(): void
{
// G3: `reversed` is <ol>-only. Stripped from <li>.
$djot = "1. item\n {reversed}\n";

$result = $this->converter->convert($djot);

$this->assertStringContainsString('<li>', $result);
$this->assertStringNotContainsString('<li reversed', $result);
$this->assertStringNotContainsString('reversed=""', $result);
}

public function testListItemOtherAttributesUnchanged(): void
{
// G3 regression guard: only start/type/reversed are stripped.
// class, id, data-* pass through unchanged on <li>.
$djot = "1. item\n {#anchor .step data-step=\"1\"}\n";

$result = $this->converter->convert($djot);

$this->assertStringContainsString('id="anchor"', $result);
$this->assertStringContainsString('class="step"', $result);
$this->assertStringContainsString('data-step="1"', $result);
}

public function testOlStartFromBlockAttrUnchanged(): void
{
// G3 regression guard: a block-attribute line BEFORE a list
// applies to the <ol> and must still emit start/type as before.
$djot = "{start=5}\n1. item\n2. next\n";

$result = $this->converter->convert($djot);

$this->assertStringContainsString('<ol start="5"', $result);
}

public function testDefinitionListDdStartIsStripped(): void
{
// G3: same strip applies to <dd>.
$djot = ": term\n\n definition\n {start=5}\n";

$result = $this->converter->convert($djot);

$this->assertStringContainsString('<dd>', $result);
$this->assertStringNotContainsString('<dd start=', $result);
}

public function testRomanNumeralList(): void
{
// x. is parsed as Roman numeral 10
Expand Down
Loading