From 89d23089559b42b9d38839c4379730092a94c048 Mon Sep 17 00:00:00 2001 From: baraline Date: Fri, 2 Oct 2026 21:31:46 +0200 Subject: [PATCH 1/4] fix(content): drop , or round a block and keep the blocks Fix 11 kept these tags as raw HTML around whatever they held. Markdown has no inline tag round blocks, so a table inside one read as pipe text, a list, a heading, a quote or a rule as its Markdown source, paragraphs were not a fixed point, and
code
left a fence open that turned the rest of the memo into code. A walk marks each such tag that holds a block, innermost first, stopping at one already marked, so each is marked once however deep they nest. convert_u drops a marked tag and keeps its blocks, as glpi_python_client 917f030 read them. Inside a table cell or a heading, whose blocks are written on one line, the tag stays. Around inline content nothing changes. The code is glpi_python_client's matching change, renamed. Tests: 42 in test_fixes section 11, one per block kind and tag, the stray fence, nested tags and the one-line controls. Against 0.4.0's converter 40 fail; the 2 that pass are the controls. The display oracle now counts an
inside an inline element as a rule. Docs: an [Unreleased] changelog entry, content.rst's element list and a known hole for , and round blocks. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 ++ docs/content.rst | 23 +++- easyvista_python_client/content/conversion.py | 41 +++++- .../content/tests/display.py | 8 +- .../content/tests/test_fixes.py | 119 ++++++++++++++++-- 5 files changed, 188 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dfab7c..954a832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,18 @@ is the error. Tags carry no `v` prefix. ## [Unreleased] +### Fixed + +- **A block inside ``, `` or `` read as broken Markdown**, since + 0.4.0 kept these tags raw (fix 11). Markdown has no inline tag round blocks: + a table inside one read as pipe text, a list, a heading, a quote or a rule + as its Markdown source, paragraphs were not a fixed point, and + `
code
` left a fence open that showed the rest of the memo + as code. Round a block the tag is now dropped and the blocks kept, as + `glpi_python_client`'s reader at `917f030` read them. Inside a table cell + or a heading, whose blocks are one line, the tag stays, and around inline + content nothing changes. The Markdown read from such a memo changes once. + ## [0.4.0] - 2026-10-02 Adds Markdown <-> memo HTML conversion as an optional extra, drops Python diff --git a/docs/content.rst b/docs/content.rst index 2acd467..7464449 100644 --- a/docs/content.rst +++ b/docs/content.rst @@ -141,7 +141,11 @@ What each kind of element becomes: stay as those raw tags, and **struck text** (````, ````, ````) as a raw ````. CommonMark has no spelling for any of them, and ``to_transport`` passes the tags through, so the formatting survives. - The cost is raw HTML in the Markdown. + The cost is raw HTML in the Markdown. Markdown has no inline tag round + blocks, so a ````, ```` or ```` holding a table, a list, a + heading, a quote, a ``
``, a rule or paragraphs is dropped and its
+  blocks kept; inside a table cell or a heading, which hold one line, it
+  stays.
 * **Links** become ``[text](https://... "title")``, and a link whose text is
   its own URL, a pasted link, becomes the autolink ````. No link
   target is filtered, ``javascript:`` included (see `It is not a sanitiser`_).
@@ -332,7 +336,12 @@ in the preproduction sample:
 * a table whose ```` and ```` are never closed folds into one cell,
   keeping its words;
 * a definition list (``
``) reads as a ``term`` line and a - ``: definition`` line, so its display gains the colon. + ``: definition`` line, so its display gains the colon; +* bold, italic or struck text (````, ````, ```` and their + synonyms) wrapped round blocks, other than a single paragraph, shows its + markers or its tag as text, and a list, a table or a heading inside it as + Markdown source; round a ``
`` it leaves a fence open, so the rest of
+  the memo shows as code.
 
 Some shapes are not fixed points at the first read, and settle after one more
 cycle. None broke a fixed point in the preproduction sample. Adjacent lists
@@ -414,12 +423,14 @@ proposed to ``glpi_python_client``. The fixes cover:
 * ``
`` inside inline code; * tables inside headings and links, and the spacing of flattened cells; * ``
``; -* underline and highlight, as raw tags; +* underline and highlight, as raw tags, except round a block (corrected in + 0.4.1); * numbering a long ordered list in linear time; * a quadratic pattern on runs of spaces; * unreadable ``colspan`` and ``start`` values; * the CVE-2025-6069 tail. -``CHANGELOG.md`` lists them under 0.4.0. Apart from those fixes, the names and -the error messages, the only difference from ``glpi_python_client`` is that the -libraries are an optional extra here, not dependencies. +``CHANGELOG.md`` lists them under 0.4.0, and the correction under 0.4.1. +Apart from those fixes, the names and the error messages, the only +difference from ``glpi_python_client`` is that the libraries are an optional +extra here, not dependencies. diff --git a/easyvista_python_client/content/conversion.py b/easyvista_python_client/content/conversion.py index 029782d..5e2df5d 100644 --- a/easyvista_python_client/content/conversion.py +++ b/easyvista_python_client/content/conversion.py @@ -138,6 +138,11 @@ _BEFORE, _AFTER = "data-ev-before", "data-ev-after" _NUMBER = "data-ev-number" # an ordered item's number (_Converter.convert_li) +#: The elements kept as raw tags (:meth:`_Converter.convert_u`), and the +#: attribute marking one that holds a block (:func:`_note_blocks`). +_RAW_INLINE = frozenset({"u", "mark", "ins"}) +_HOLDS_BLOCK = "data-ev-block" + #: Text split into leading line breaks and spaces, content, trailing ones. #: The content ends on its last character that is neither, found greedily: #: a lazy ``.*?`` rescanned the run after it at every step, quadratic. @@ -330,6 +335,33 @@ def _note_sides(root: Tag) -> None: element[_AFTER] = " " +def _note_blocks(root: Tag) -> None: + """Mark each ````, ```` or ```` that holds a block. + + Markdown has no inline tag around blocks: such a tag wrapped round them + showed a table, a list or a heading as its Markdown source, and round a + ``
`` left a fence open to the end of the body. One walk: a block
+    marks the open ones from the innermost out and stops at one already
+    marked, whose holders were marked with it, so each is marked once
+    however deeply they nest.
+    """
+
+    holders: list[Tag] = []
+    for node, entering in _walk(root):
+        if not isinstance(node, Tag):
+            continue
+        if node.name in _RAW_INLINE:
+            if entering:
+                holders.append(node)
+            else:
+                holders.pop()
+        elif entering and node.name in _BLOCKS:
+            for holder in reversed(holders):
+                if holder.has_attr(_HOLDS_BLOCK):
+                    break
+                holder[_HOLDS_BLOCK] = ""
+
+
 def _punctuation(char: str) -> bool:
     """Return whether CommonMark counts ``char`` as punctuation."""
 
@@ -426,8 +458,14 @@ def convert_s(self, el: Tag, text: str, parent_tags: set[str]) -> str:
     convert_strike = convert_s
 
     def convert_u(self, el: Tag, text: str, parent_tags: set[str]) -> str:
-        """Underlined or highlighted text: CommonMark has neither, so raw HTML."""
+        """Underlined or highlighted text: CommonMark has neither, so raw HTML.
 
+        Round a block the tag is dropped and the blocks kept, except on the
+        one line of a cell or a heading, where the blocks are inline text.
+        """
+
+        if el.has_attr(_HOLDS_BLOCK) and "_inline" not in parent_tags:
+            return text
         return self._markup(el, text, parent_tags, "", el.name)
 
     convert_mark = convert_ins = convert_u
@@ -705,6 +743,7 @@ def html_to_markdown(html: str) -> str:
     _flatten_nested_tables(soup)
     _drop_trailing_breaks(soup)
     _note_sides(soup)
+    _note_blocks(soup)
     return str(_FORMATTER.render(_CONVERTER.convert_soup(soup))).strip()
 
 
diff --git a/easyvista_python_client/content/tests/display.py b/easyvista_python_client/content/tests/display.py
index 6fb4490..d3cc67d 100644
--- a/easyvista_python_client/content/tests/display.py
+++ b/easyvista_python_client/content/tests/display.py
@@ -23,6 +23,10 @@
 * an ordered list's ``start`` is read with ``isdecimal``, as the converter
   reads it, where ``isdigit`` made the oracle itself raise on ``"²"``.
 
+Since 0.4.1, with ``glpi_python_client`` 0.6.1, a rule (``
``) inside an +inline element counts as a block, where the oracle read it as nothing: a +browser draws ``
`` as a rule. + What the oracle cannot see: underline and highlight (````, ````, ````), struck text, link and image titles, and line breaks inside a heading. @@ -146,8 +150,10 @@ def _inline(node: Tag, words: _Words, fmt: Format) -> None: def _has_block(node: Tag) -> bool: + # Since 0.4.1: a rule is a block too, so
draws its rule. return any( - isinstance(child, Tag) and child.name in _BLOCKS for child in node.descendants + isinstance(child, Tag) and (child.name in _BLOCKS or child.name == "hr") + for child in node.descendants ) diff --git a/easyvista_python_client/content/tests/test_fixes.py b/easyvista_python_client/content/tests/test_fixes.py index 63e9ba8..6142279 100644 --- a/easyvista_python_client/content/tests/test_fixes.py +++ b/easyvista_python_client/content/tests/test_fixes.py @@ -18,7 +18,8 @@ 8. a block in such a table stays on its holder's line; 9. bold at a flattened cell's edge still closes; 10. ``
`` is a block, except inside ``
``;
-11. ````, ```` and ```` stay raw HTML;
+11. ````, ```` and ```` stay raw HTML, except round a block,
+    where they are dropped and the blocks kept;
 12. an ordered list is numbered in linear time, and a ``start`` that is
     not a decimal number counts from 1;
 13. the regex splitting a text's edges is greedy, so linear;
@@ -27,12 +28,20 @@
     guard).
 
 The cost side of 12, 13 and 15 is tested in :mod:`.test_cost`. Run against
-GLPI 917f030 on 2026-10-02 (CPython 3.12.11), 36 of these 54 tests failed.
-Of the 18 that passed there, 17 are controls, guards on a correction to a
-fix, pins of output a fix leaves as it was, or behaviour a cost fix had to
-keep, and each says which. The other is test 15's ``attribute`` case, which
-passed only because 3.12.11 predates CPython's own fix; that test says
-why. Every word is invented and every URL is under ``example.org``.
+GLPI 917f030 on 2026-10-02 (CPython 3.12.11), 36 of the 54 tests first
+written failed. Of the 18 that passed there, 17 are controls, guards on a
+correction to a fix, pins of output a fix leaves as it was, or behaviour a
+cost fix had to keep, and each says which. The other is test 15's
+``attribute`` case, which passed only because 3.12.11 predates CPython's
+own fix; that test says why.
+
+The 42 tests 0.4.1 added to section 11 for a block inside ````,
+```` or ```` pin the correction to fix 11, which wrapped blocks in
+the tag: run against 0.4.0's converter, 40 failed, and the two that passed
+are the one-line controls. They read such a body as GLPI 917f030 did, so on
+that converter only the three that need a tag kept fail.
+
+Every word is invented and every URL is under ``example.org``.
 """
 
 from __future__ import annotations
@@ -395,6 +404,102 @@ def test_11_underline_inside_a_link() -> None:
     assert markdown == "[lien](https://example.org/u)"
 
 
+_TABLE = (
+    ""
+    "
NomValeur
serveursrv01
" +) + + +@pytest.mark.parametrize("tag", ["u", "mark", "ins"]) +@pytest.mark.parametrize( + "body", + [ + pytest.param(f"

Voir :

{_TABLE}

fin

", id="table"), + pytest.param("
  • un
  • deux
", id="list"), + pytest.param("
  1. un
  2. deux
", id="ordered-list"), + pytest.param("

Titre

texte

", id="heading"), + pytest.param("
cite
", id="quote"), + pytest.param("
un\ndeux
", id="pre"), + pytest.param("

a


b

", id="rule"), + pytest.param("

un

deux

", id="paragraphs"), + pytest.param("
un
deux
", id="divs"), + pytest.param("avant

milieu

apres
", id="text-around-a-block"), + pytest.param("
  • un

    deux

", id="in-an-item"), + pytest.param( + "

un

deux

", id="in-a-quote" + ), + ], +) +def test_11_around_a_block_the_tag_is_dropped_and_the_blocks_kept( + body: str, tag: str +) -> None: + """Markdown has no inline tag around blocks: fix 11 wrapped them anyway, + so a table read as pipe text, a list, heading, quote or rule as its + Markdown source, and paragraphs were not a fixed point. The tag is + dropped there and the blocks read as they would without it, as GLPI + 917f030 read them; the underline is lost, which the display oracle cannot see.""" + + html = body.replace("", f"<{tag}>").replace("", f"") + bare = body.replace("", "").replace("", "") + + markdown = assert_survives(html) + + assert markdown == read(bare) + assert f"<{tag}>" not in markdown + + +def test_11_a_pre_inside_underline_leaves_no_stray_fence() -> None: + """The worst of the shapes above: the closing fence took the ``
`` as + its info string, so it opened a new fence and everything after it, + ``apres`` included, displayed as code.""" + + markdown = assert_survives("
code

apres

") + + assert markdown == "```\ncode\n```\n\napres" + + +@pytest.mark.parametrize( + ("html", "expected"), + [ + pytest.param( + "
  • a
", "- a", id="both-around-a-list" + ), + pytest.param( + "x

y

", "x\n\ny", id="inline-sibling" + ), + pytest.param( + "

a

b

c
", "a\n\nb\n\nc", id="inner-after" + ), + ], +) +def test_11_each_tag_around_a_block_is_dropped_and_no_other( + html: str, expected: str +) -> None: + """Nested tags: each one holding a block is dropped, however deep the + block, and one that closed before the block keeps its raw tag.""" + + assert assert_survives(html) == expected + + +@pytest.mark.parametrize( + ("html", "expected"), + [ + pytest.param( + "" + "

a

b

c

", + "| a |\n| -- |\n| b c |", + id="cell", + ), + pytest.param("

titre

", "## titre", id="heading"), + ], +) +def test_11_around_a_block_on_one_line_the_tag_stays(html: str, expected: str) -> None: + """The control: a cell or a heading writes its blocks on its one line, + so the tag wraps inline text there and is kept.""" + + assert assert_survives(html) == expected + + # --------------------------------------------------------------------------- # 12. Ordered-list numbering # --------------------------------------------------------------------------- From 7a70fd778ee01fe60b8600f4b66967ca3c0cf3d6 Mon Sep 17 00:00:00 2001 From: baraline Date: Fri, 2 Oct 2026 21:56:15 +0200 Subject: [PATCH 2/4] test(content): pin four reader guards no test caught A review of glpi_python_client's port of this converter removed each sub-guard of the fifteen fixes in turn and found four whose removal no test noticed; the code is the same here. Each now has a test that fails without it, run in disposable copies against the whole content suite: - fix 5: before a space a second "<" keeps mdformat's single escape ("<< b" reads as "\<< b"); spelling only, but digests depend on it; - fix 6: a header cell (th) is a cell, so a "|" in or stays escaped and a line break in inline code splits the span; - fix 7: an without href is no link, so a table in it stays a table; - fix 12: an ordered item numbered 10 or more indents its content by its bullet's width (four for "10. "), where a fixed three let a nested list or a second paragraph leave the item. test_cost also counts the checks of the walk that finds blocks inside , and : the stop at a tag already marked changes no output, only cost, so the count pins it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 ++ .../content/tests/test_cost.py | 41 +++++++ .../content/tests/test_fixes.py | 101 +++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 954a832..fd90c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,16 @@ is the error. Tags carry no `v` prefix. or a heading, whose blocks are one line, the tag stays, and around inline content nothing changes. The Markdown read from such a memo changes once. +### Notes + +- Four guards of the 0.4.0 fixes that no test caught are pinned, each by a + test that fails without it: a second `<` before a space keeps one escape + (fix 5), a header cell escapes a `|` in `` or `` and splits a + line break in inline code (fix 6), a table inside an `` without `href` + stays a table (fix 7), and an ordered item numbered 10 or more indents its + content by its bullet's width (fix 12). A count pins that the walk finding + blocks inside ``, `` and `` checks each tag about once. + ## [0.4.0] - 2026-10-02 Adds Markdown <-> memo HTML conversion as an optional extra, drops Python diff --git a/easyvista_python_client/content/tests/test_cost.py b/easyvista_python_client/content/tests/test_cost.py index 436abf9..493baae 100644 --- a/easyvista_python_client/content/tests/test_cost.py +++ b/easyvista_python_client/content/tests/test_cost.py @@ -41,7 +41,9 @@ from collections.abc import Callable import pytest +from bs4 import Tag +from easyvista_python_client.content import conversion from easyvista_python_client.content.conversion import EasyvistaContentConverter read = EasyvistaContentConverter.from_transport @@ -194,6 +196,45 @@ def test_a_tail_of_unfinished_tags_converts_in_linear_time() -> None: assert_linear(lambda n: "

r

" + "x
None: + """The walk marking a ````, ```` or ```` that holds a block + stops at the first tag already marked, so a block checks only the tags + marked since the last one. + + Without the stop, every block checked every tag open around it: blocks + times depth. The output is the same either way, and a body nested past + markdownify's stack, which still pays for the walk before it falls back + to text, is where the cost would show. A ratio at sizes a test can + afford does not tell the two apart reliably, so this counts the checks: + about one per tag here, against one per tag per block without the stop. + The floor fails a rewrite that checks some other way, rather than + letting it pass on a count of nothing. + """ + + checks = 0 + has_attr, get = Tag.has_attr, Tag.get + + def counting_has_attr(self: Tag, key: str) -> bool: + nonlocal checks + checks += key == conversion._HOLDS_BLOCK + return has_attr(self, key) + + def counting_get(self: Tag, key: str, default: object = None) -> object: + nonlocal checks + checks += key == conversion._HOLDS_BLOCK + return get(self, key, default) + + monkeypatch.setattr(Tag, "has_attr", counting_has_attr) + monkeypatch.setattr(Tag, "get", counting_get) + depth = 50 + + read("" * depth + "

x

" * depth + "
" * depth) + + assert depth <= checks <= 4 * depth + + def test_a_body_too_deep_to_convert_keeps_its_text() -> None: """markdownify recurses per nesting level; past the stack, the text is kept.""" diff --git a/easyvista_python_client/content/tests/test_fixes.py b/easyvista_python_client/content/tests/test_fixes.py index 6142279..8491700 100644 --- a/easyvista_python_client/content/tests/test_fixes.py +++ b/easyvista_python_client/content/tests/test_fixes.py @@ -41,6 +41,12 @@ are the one-line controls. They read such a body as GLPI 917f030 did, so on that converter only the three that need a tag kept fail. +Twelve more pin four guards a review found no test caught: a second ``<`` +before a space (section 5), a header cell (6), an anchor without ``href`` +(7) and an item numbered 10 or more (12). Each fails when its guard alone +is removed. On GLPI 917f030, which behaved the same there except for fix +6, only the three header-cell cases that need fix 6 fail. + Every word is invented and every URL is under ``example.org``. """ @@ -216,6 +222,24 @@ def test_5_a_second_less_than_stays_text(html: str) -> None: assert "\\<\\<" in markdown +@pytest.mark.parametrize( + ("html", "expected"), + [ + pytest.param("

<< b

", "\\<< b", id="before-a-space"), + pytest.param("

x << <b>

", "x \\<< \\", id="then-a-tag"), + ], +) +def test_5_a_second_less_than_before_a_space_is_not_escaped( + html: str, expected: str +) -> None: + """Before a space a ``<`` opens neither an autolink nor a tag, so the fix + leaves it as mdformat wrote it. Both spellings display the same: this + pins the spelling, which digests of the Markdown depend on and which the + two packages' readers share.""" + + assert assert_survives(html) == expected + + # --------------------------------------------------------------------------- # 6. A line break in inline code # --------------------------------------------------------------------------- @@ -240,6 +264,12 @@ def test_5_a_second_less_than_stays_text(html: str) -> None: "| H |\n| -- |\n| a `un`
`deux` b |", id="cell", ), + pytest.param( + "" + "
a un
deux
b
x
", + "| a `un`
`deux` b |\n| -- |\n| x |", + id="header-cell", + ), ], ) def test_6_a_line_break_in_inline_code_splits_the_span( @@ -261,12 +291,20 @@ def test_6_a_line_break_in_inline_code_in_a_heading_is_a_space() -> None: ) +@pytest.mark.parametrize("cell", ["td", "th"]) @pytest.mark.parametrize("tag", ["code", "kbd", "samp"]) -def test_6_a_pipe_in_inline_code_in_a_cell_stays_in_the_cell(tag: str) -> None: +def test_6_a_pipe_in_inline_code_in_a_cell_stays_in_the_cell( + tag: str, cell: str +) -> None: """GFM splits a row on ``|`` before it reads code. ``code`` is the control: - GLPI 917f030 escaped it there already, but not in ``kbd`` or ``samp``.""" + GLPI 917f030 escaped it there already, but not in ``kbd`` or ``samp``. A + header cell is a cell too.""" - html = f"
H
<{tag}>a|b fin
" + content = f"<{cell}><{tag}>a|b fin" + if cell == "td": + html = f"{content}
H
" + else: + html = f"{content}
x
" markdown = assert_survives(html) @@ -313,6 +351,27 @@ def test_7_a_table_inside_a_cell_is_its_cells_text() -> None: assert markdown == "| A |\n| -- |\n| un deux |" +@pytest.mark.parametrize( + "html", + [ + pytest.param('
TABLE', id="named-anchor"), + pytest.param('

avant

TABLE', id="anchor-with-an-id"), + ], +) +def test_7_a_table_inside_an_anchor_without_a_target_stays_a_table( + html: str, +) -> None: + """The control: an ```` without ``href`` is no link, so it holds no + line, and a table inside it stays a table. Some mail clients wrap a + message's content in such named anchors.""" + + table = "
ab
cd
" + + markdown = assert_survives(html.replace("TABLE", table)) + + assert markdown.endswith("| a | b |\n| -- | -- |\n| c | d |") + + # --------------------------------------------------------------------------- # 8. A block in a flattened table stays on its holder's line # --------------------------------------------------------------------------- @@ -536,6 +595,42 @@ def test_12_an_ordered_item_is_numbered_one_past_the_item_before( assert assert_survives(html) == expected +@pytest.mark.parametrize( + ("html", "expected"), + [ + pytest.param( + '
  1. a
    • b
', + "10. a\n - b", + id="nested-list", + ), + pytest.param( + '
  1. a

    b

', + "10. a\n\n b", + id="second-paragraph", + ), + pytest.param( + '
  1. a
  2. b
    • c
', + "09. a\n10. b\n - c", + id="tenth-item", + ), + pytest.param( + '
  1. a
    • b
', + "100. a\n - b", + id="three-digits", + ), + ], +) +def test_12_an_items_content_is_indented_by_its_bullets_width( + html: str, expected: str +) -> None: + """A line belongs to an item only if indented by its bullet's width: four + for ``10. ``, five for ``100. ``. markdownify indented so; the rewrite + does it itself, and only an item numbered 10 or more tells the bullet's + width from a fixed three, the width of ``1. ``.""" + + assert assert_survives(html) == expected + + def test_12_an_empty_ordered_item_still_counts_for_the_next() -> None: """An empty item takes a number and writes nothing. From 6d6bb2cd706e5cf7b0e2ac4e1b8ed53b10287053 Mon Sep 17 00:00:00 2001 From: baraline Date: Fri, 2 Oct 2026 21:58:44 +0200 Subject: [PATCH 3/4] docs(content): correct the reader's documented edges after review - start: a start that is not a decimal number counts from 1, as a browser counts one holding no digit; a browser reads " 3", "+3" or "3abc" as 3 (the HTML standard's integer rules), which the converter counts from 1. The display oracle reads start as the converter does, and now says so. - ValueError: any ValueError raised while converting degrades the memo to text, not only an unreadable colspan or start. The module and from_transport docstrings and content.rst say so. - CVE-2025-6069 guard: whatever follows a memo's last ">" reads as text, an unterminated comment and a closing tag cut short inside a link included, where a patched CPython drops some of it. Test 15 pins both. - A second write-and-read cycle changes nothing more except for two shapes, reproduced here: adjacent lists with different bullets, and a fence whose info string holds a character reference. content.rst said it changed nothing. - The ticket-actions skill says a round a block is dropped. Each stated behaviour was re-checked on CPython 3.13.14, and in glpi_python_client's identical converter on 3.12.3 too. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 21 ++++++++++ docs/content.rst | 41 ++++++++++++------- easyvista_python_client/content/conversion.py | 13 +++--- .../content/tests/display.py | 5 ++- .../content/tests/test_fixes.py | 17 +++++++- skills/easyvista-ticket-actions/SKILL.md | 3 +- 6 files changed, 76 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd90c4d..169d9a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,27 @@ is the error. Tags carry no `v` prefix. or a heading, whose blocks are one line, the tag stays, and around inline content nothing changes. The Markdown read from such a memo changes once. +### Documentation + +- `docs/content.rst`, corrected after a review of the converter: + - a `start` that is not a decimal number counts from 1 as a browser + counts one holding no digit; a browser reads `" 3"`, `"+3"` or + `"3abc"` as 3, which the converter counts from 1; + - any `ValueError` raised while converting degrades the memo to text, not + only an unreadable `colspan` or `start` (the module and + `from_transport` docstrings say so too); + - the CVE-2025-6069 note says that everything after a memo's last `>` + reads as text, an unterminated comment and a closing tag cut short + inside a link included, where a patched CPython drops some of it; + - a second write-and-read cycle changes nothing more except for two + shapes, adjacent lists with different bullets and a fence whose info + string holds a character reference, where 0.4.0's page and its + changelog said it changes nothing; + - ``, `` and `` round a block, and a known hole for + ``, `` and `` round blocks. +- The `easyvista-ticket-actions` skill says a `` round a block is + dropped. + ### Notes - Four guards of the 0.4.0 fixes that no test caught are pinned, each by a diff --git a/docs/content.rst b/docs/content.rst index 7464449..82e81ef 100644 --- a/docs/content.rst +++ b/docs/content.rst @@ -149,11 +149,13 @@ What each kind of element becomes: * **Links** become ``[text](https://... "title")``, and a link whose text is its own URL, a pasted link, becomes the autolink ````. No link target is filtered, ``javascript:`` included (see `It is not a sanitiser`_). -* **Lists** nest and keep their numbers, including an ``
    ``. - **Tables** become GFM tables, and a ``
    `` inside a cell stays a raw - ``
    ``, since a GFM cell is one line. **Preformatted blocks** become fences - that keep the ``language-`` class ``cmark-gfm`` writes, with a fence longer - than any run of backticks in the code. +* **Lists** nest and keep their numbers, including an ``
      ``. A + ``start`` that is not a decimal number counts from 1, as a browser counts + one holding no digit, such as ``²``; a browser reads ``" 3"``, ``"+3"`` or + ``"3abc"`` as 3. **Tables** become GFM tables, and a ``
      `` inside a cell + stays a raw ``
      ``, since a GFM cell is one line. **Preformatted blocks** + become fences that keep the ``language-`` class ``cmark-gfm`` writes, with a + fence longer than any run of backticks in the code. * ````, ``