From e0f1e095ed4a3265b8605684b9d170a07c33a888 Mon Sep 17 00:00:00 2001 From: baraline Date: Wed, 7 Oct 2026 14:04:53 +0200 Subject: [PATCH 1/2] fix(content): links written into GLPI open in a new window (0.6.2) Every link to_transport writes now carries target="_blank" rel="noopener noreferrer". GLPI's own editor writes target="_blank" on a link, and without it a link opens in place of the page showing it. Write models render through the same function, so a followup, task, solution or ticket body written through them carries it too. The twin of easyvista-python-client 0.4.2 (baraline/easyvista_python_client#9), which makes the same change after a live measurement on EasyVista's memo view; the two converters keep writing the same HTML. noreferrer is written here too for that reason. Reading ignores both attributes, so from_transport(to_transport(m)) is unchanged for every link: plain, titled, autolink and an image inside a link. A link shown as code stays text. The rewrite matches only the anchor shape cmark-gfm writes (href, optional title). Version 0.6.2 in pyproject.toml, __version__ and all eleven skills; the changelog section; the user guide's writing paragraph. Measured: full suite 1770 passed, 55 skipped, 1 xfailed; mypy and ruff clean. Removing the rewrite fails 6 tests, dropping the title branch fails 1. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 +++++ docs/user_guide.rst | 4 +- glpi_python_client/__init__.py | 2 +- glpi_python_client/content/conversion.py | 17 ++++- .../content/tests/test_conversion.py | 66 ++++++++++++++++++- pyproject.toml | 2 +- skills/glpi-asset-workflow/SKILL.md | 2 +- skills/glpi-client-setup/SKILL.md | 2 +- skills/glpi-contract-workflow/SKILL.md | 2 +- skills/glpi-document-workflow/SKILL.md | 2 +- skills/glpi-knowledge-base/SKILL.md | 2 +- skills/glpi-plugin-fields/SKILL.md | 2 +- skills/glpi-reporting-and-context/SKILL.md | 2 +- skills/glpi-team-members/SKILL.md | 2 +- skills/glpi-ticket-timeline/SKILL.md | 2 +- skills/glpi-ticket-workflow/SKILL.md | 2 +- .../glpi-user-location-provisioning/SKILL.md | 2 +- 17 files changed, 112 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea91ed..ceda2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## 0.6.2 — 2026-10-07 + +A patch release of the converter's writing: a link written into GLPI opens in +a new window. Reading is unchanged. `easyvista-python-client` 0.4.2 makes the +same change, so the two converters still write the same HTML. + +### Changed + +- **Every link `to_transport` writes carries `target="_blank" + rel="noopener noreferrer"`**, as a link written in GLPI's own editor carries + `target="_blank"`. Without it a link opens in place of the page showing it. + Write models render through the same function, so a followup, task, + solution or ticket body written through them carries it too. Reading + ignores both attributes, so `from_transport(to_transport(m))` is unchanged + for every link. + ## 0.6.1 — 2026-10-02 The reader now gives the same Markdown as `easyvista-python-client` 0.4.1, diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 9498d7b..d537927 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -1838,7 +1838,9 @@ becomes: Writing, your Markdown is rendered by cmark-gfm. A newline is a line break, GFM tables work, and raw HTML passes through, so put a placeholder such as -```` in backticks. A write model keeps your Markdown as written, +```` in backticks. A link opens in a new window, as a link written in +GLPI's editor does: each ```` the renderer writes carries +``target="_blank" rel="noopener noreferrer"``, which reading ignores. A write model keeps your Markdown as written, stripped at both ends, unless it starts with ``<`` and holds an HTML element anywhere, in which case it is read as HTML. Stripping unindents the first line of a body that opens with an indented code block, which then diff --git a/glpi_python_client/__init__.py b/glpi_python_client/__init__.py index 329d868..3bdb976 100644 --- a/glpi_python_client/__init__.py +++ b/glpi_python_client/__init__.py @@ -131,7 +131,7 @@ date_window, ) -__version__ = "0.6.1" +__version__ = "0.6.2" __all__ = [ "AsyncGlpiClient", diff --git a/glpi_python_client/content/conversion.py b/glpi_python_client/content/conversion.py index 00df323..2b55f3e 100644 --- a/glpi_python_client/content/conversion.py +++ b/glpi_python_client/content/conversion.py @@ -715,12 +715,27 @@ def html_to_markdown(html: str) -> str: return str(_FORMATTER.render(_CONVERTER.convert_soup(soup))).strip() +#: A link as cmark-gfm writes one: an ``href``, an optional ``title``, nothing else. +_RENDERED_LINK = re.compile(r'') + +#: What GLPI's own editor writes on a link, so that it opens in a new window +#: rather than in place of the page showing it. ``noreferrer`` too, as +#: ``easyvista-python-client``'s twin writes, so the two render alike. +_NEW_WINDOW = ' target="_blank" rel="noopener noreferrer"' + + def markdown_to_html(markdown: str) -> str: - """Render Markdown as HTML: CommonMark with GFM tables, through cmark-gfm.""" + """Render Markdown as HTML: CommonMark with GFM tables, through cmark-gfm. + + Every link opens in a new window, the way a link written in GLPI's editor + does: ``target="_blank" rel="noopener noreferrer"`` is added to each link + cmark-gfm writes. Reading ignores both attributes. + """ html: str = cmarkgfm.markdown_to_html_with_extensions( markdown, options=_RENDER_OPTIONS, extensions=["table"] ) + html = _RENDERED_LINK.sub(lambda link: link.group(0)[:-1] + _NEW_WINDOW + ">", html) return html.strip() diff --git a/glpi_python_client/content/tests/test_conversion.py b/glpi_python_client/content/tests/test_conversion.py index 6c70668..e69b9e4 100644 --- a/glpi_python_client/content/tests/test_conversion.py +++ b/glpi_python_client/content/tests/test_conversion.py @@ -17,6 +17,9 @@ read = GlpiContentConverter.from_transport render = GlpiContentConverter.to_transport +#: What every rendered link carries: GLPI's editor writes it on a link it makes. +NEW_WINDOW = ' target="_blank" rel="noopener noreferrer"' + def test_content_is_markdown_in_python_and_html_for_glpi() -> None: assert read("

The printer is offline.

") == ( @@ -217,12 +220,12 @@ def test_deeply_nested_markdown_renders() -> None: ), pytest.param( "[x](javascript:alert(1))", - '

x

', + f'

x

', id="link-target", ), pytest.param( # python-markdown, before 0.6.0, left this as raw markup "", - '

javascript:alert(1)

', + f'

javascript:alert(1)

', id="autolink", ), ], @@ -242,3 +245,62 @@ def test_markup_a_body_displays_as_text_stays_text_both_ways() -> None: assert markdown == "\\