Skip to content

[BUGFIX] Read project version from the DOM so "0.10" is not coerced to 0.1 - #1345

Open
CybotTM wants to merge 2 commits into
phpDocumentor:mainfrom
netresearch:fix/xml-version-string-coercion
Open

[BUGFIX] Read project version from the DOM so "0.10" is not coerced to 0.1#1345
CybotTM wants to merge 2 commits into
phpDocumentor:mainfrom
netresearch:fix/xml-version-string-coercion

Conversation

@CybotTM

@CybotTM CybotTM commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Problem

A guides.xml with <project version="0.10"> is rendered with the version 0.1 (title, objects.inv, every |version| substitution). Any round / trailing-zero two-part version is mangled:

guides.xml parsed as
0.10 0.1
1.0 1
1.10 1.1
13.0 13
0.9, 13.4, 1.2.3, main unchanged

It surfaced on docs.typo3.org for netresearch/nr-vault and netresearch/nr-llm, whose 0.10 / 0.12 releases were published under 0.1.

Cause

XmlFileLoader parses guides.xml with XmlUtils::convertDomElementToArray(), which runs phpize() on every attribute value. phpize("0.10") returns the float 0.1 (it is is_numeric), which then stringifies to "0.1".

There was already a workaround — an escaped version syntax (version="'3.0'", single-quoted to dodge phpize) plus a beforeNormalization that stripped the quotes again. But that runs after phpize has already discarded the digit (0.100.1), so it can never recover it.

Fix

Read the <project> attributes (all strings) directly from the DOM, and detach the element before convertDomElementToArray() so phpize never touches them. The version is read correctly at the source, instead of being coerced and patched up afterwards. The <guides> attributes that genuinely want coercion (links-are-relative, max-menu-depth, …) are unaffected.

Because the root cause is fixed, the beforeNormalization workaround in the Symfony config is removed — escaping is no longer needed to write a correct version.

Backward compatibility

Projects that adopted the escaped version workaround for this very bug may still have <project version="'3.0'"> in their guides.xml. To keep those rendering 3.0 (and not the literal '3.0'), the surrounding single quotes are still stripped for version and release — the same trim($value, "'") the removed beforeNormalization did, now applied in XmlFileLoader when the attributes are read. New files don't need it: write the version directly, version="0.10" / version="3.0".

Before / after

<project version="0.10"> as loaded by XmlFileLoader:

Before

before

After

after

Tests

Two integration fixtures under tests/Integration/tests/meta/:

  • version-from-guides-xml — unescaped version="0.10" (with release="3.0.0" as a non-coerced control); asserts the rendered |version| stays 0.10. Passes with the fix and fails without it (without it: version 0.1).
  • version-from-guides-xml-quoted — the legacy quoted form version="'3.0'" / release="'3.0.0'"; asserts it still renders 3.0 / 3.0.0, covering the backward-compatibility path above.

Integration suite passes locally (228 tests). CI is green across PHP 8.1–8.3 (highest / locked / lowest): unit, integration, functional, Coding Standards, PHPStan and architecture checks.

Update: two defects in the first commit

A review of this branch found two problems with reading <project> from the DOM, both reproduced before fixing and both addressed in the second commit.

Detaching <project> can leave an empty root element behind, and XmlUtils::convertDomElementToArray() returns null rather than an empty array for one. assert(is_array($rootConfig)) then fails for any guides.xml that holds nothing but a project — which both fixtures above are. It stays invisible under the default and the CI php.ini, both of which run with zend.assertions=-1, where the following write to $rootConfig['project'] auto-vivifies null; with -d zend.assertions=1 four integration data sets fail. The assertion is now an is_array() check.

getElementsByTagName('project') searches the whole subtree, and the schema lets an <extension> carry arbitrary child elements (xsd:any processContents="lax"). A nested <project> was therefore read as the project configuration and removed from that extension, while the real one at root level was dropped. Only direct children are considered now.

The new version-from-guides-xml-nested-project fixture covers the second defect and fails against the previous state of this branch. The first is only observable with assertions enabled, where the two existing fixtures already cover it — worth knowing that CI does not gate it.

Assisted by claude-code:claude-opus-5 — Session

@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch 2 times, most recently from ffab307 to fcc475c Compare June 16, 2026 22:01
@CybotTM
CybotTM marked this pull request as draft June 17, 2026 05:38
@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch from fcc475c to 7d907c6 Compare June 17, 2026 05:51
@CybotTM CybotTM changed the title Keep version strings like "0.10" intact when loading guides.xml [BUGFIX] Read project version from the DOM so "0.10" is not coerced to 0.1 Jun 17, 2026
@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch 2 times, most recently from 6b70577 to 6503239 Compare June 17, 2026 06:19
@CybotTM
CybotTM marked this pull request as ready for review June 17, 2026 06:28
@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch from 6503239 to c921c2c Compare June 18, 2026 20:11
@garvinhicking

Copy link
Copy Markdown
Contributor

Thanks - I can't really judge the impact of this and hope @jaapio can give some feedback. I remember having stabbed at this and not being able to resolve this.

@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch 2 times, most recently from f346063 to c748708 Compare June 24, 2026 15:26
…o 0.1

A <project version="0.10"> in guides.xml was rendered as version 0.1
(title, objects.inv, every |version| substitution). XmlFileLoader parses
guides.xml with XmlUtils::convertDomElementToArray(), which runs phpize() on
every attribute value, coercing version-like strings into numbers: "0.10"
becomes the float 0.1, "1.0" becomes 1, "13.0" becomes 13.

Read the <project> attributes (all strings) straight from the DOM, and detach
the element before the conversion so phpize never sees them. The version is now
read correctly at the source instead of being coerced and patched up afterwards,
so the beforeNormalization workaround in the Symfony config is removed.

Writing the version directly (version="0.10") now just works. The previous
"escaped version" workaround -- version="'3.0'" with single quotes to dodge
phpize -- is no longer necessary, but existing guides.xml files may still use
it, so the surrounding single quotes are still stripped (for version and
release) to keep those files rendering 3.0 rather than the literal '3.0'.
A regression fixture covers the quoted form.

Reported on docs.typo3.org for netresearch/nr-vault and nr-llm (0.10 / 0.12).

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch from c748708 to 555a67e Compare July 1, 2026 12:01
linawolf pushed a commit to TYPO3-Documentation/render-guides that referenced this pull request Jul 14, 2026
## Problem

For a package with both `0.1` and `0.10` (e.g.
[netresearch/nr-vault](https://docs.typo3.org/p/netresearch/nr-vault/0.10/en-us/))
the version switcher is wrong in two ways:

- **`0.10` is sorted last** instead of first — it should appear right
after `main`.
- **Opening the `0.10` page pre-selects `0.1`** in the dropdown.

## Cause

`versions.js` sorted by `parseFloat(v)`, and `parseFloat("0.10") ===
0.1`, so `0.10` ties with the `0.x` group and lands at the bottom.
Pre-selection compared against the rendered `data-current-version`
attribute, which is `"0.1"` on the `0.10` page because the version
string `"0.10"` is numerically coerced to `"0.1"` server-side (a
separate, deeper bug in the render pipeline).

## Fix

- Sort each dotted version component **numerically** (`main` first, then
highest version), so `0.10` > `0.9` > … > `0.1`.
- Derive the active version from the **page URL** (the authoritative
source, e.g. `…/0.10/en-us/…`) instead of the coercible
`data-current-version` attribute. This makes pre-selection correct
regardless of that server-side coercion.

`resources/public/js/theme.min.js` is rebuilt (grunt uglify).

## Before / after

The version dropdown on the `nr-vault` `0.10` page:

**Before**


![before](https://raw.githubusercontent.com/CybotTM/render-guides/version-sort-screenshots/before.png)

**After**


![after](https://raw.githubusercontent.com/CybotTM/render-guides/version-sort-screenshots/after.png)

## Tests

New `tests/js/versions.test.js` (vitest/jsdom) asserts both the sort
order (`main, 0.10, 0.9, … 0.1`) and that the `0.10` page pre-selects
`0.10`. Both **pass with the fix and fail without it**.

## Related

The wrong pre-selection has a second, deeper cause that this PR does
**not** rely on: the `0.10` page is served with
`data-current-version="0.1"` (and title / meta `0.1`) because
`guides.xml`'s `version="0.10"` is numerically coerced to the float
`0.1` while parsing (Symfony `XmlUtils` phpize). That is tracked in
#1293 and fixed upstream in phpDocumentor/guides#1345. This PR makes the
switcher correct regardless — it derives the active version from the
page URL — so the two fixes are independent.

---------

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
typo3-documentation-team pushed a commit to TYPO3-Documentation/t3docs-typo3-docs-theme that referenced this pull request Jul 14, 2026
## Problem

For a package with both `0.1` and `0.10` (e.g.
[netresearch/nr-vault](https://docs.typo3.org/p/netresearch/nr-vault/0.10/en-us/))
the version switcher is wrong in two ways:

- **`0.10` is sorted last** instead of first — it should appear right
after `main`.
- **Opening the `0.10` page pre-selects `0.1`** in the dropdown.

## Cause

`versions.js` sorted by `parseFloat(v)`, and `parseFloat("0.10") ===
0.1`, so `0.10` ties with the `0.x` group and lands at the bottom.
Pre-selection compared against the rendered `data-current-version`
attribute, which is `"0.1"` on the `0.10` page because the version
string `"0.10"` is numerically coerced to `"0.1"` server-side (a
separate, deeper bug in the render pipeline).

## Fix

- Sort each dotted version component **numerically** (`main` first, then
highest version), so `0.10` > `0.9` > … > `0.1`.
- Derive the active version from the **page URL** (the authoritative
source, e.g. `…/0.10/en-us/…`) instead of the coercible
`data-current-version` attribute. This makes pre-selection correct
regardless of that server-side coercion.

`resources/public/js/theme.min.js` is rebuilt (grunt uglify).

## Before / after

The version dropdown on the `nr-vault` `0.10` page:

**Before**


![before](https://raw.githubusercontent.com/CybotTM/render-guides/version-sort-screenshots/before.png)

**After**


![after](https://raw.githubusercontent.com/CybotTM/render-guides/version-sort-screenshots/after.png)

## Tests

New `tests/js/versions.test.js` (vitest/jsdom) asserts both the sort
order (`main, 0.10, 0.9, … 0.1`) and that the `0.10` page pre-selects
`0.10`. Both **pass with the fix and fail without it**.

## Related

The wrong pre-selection has a second, deeper cause that this PR does
**not** rely on: the `0.10` page is served with
`data-current-version="0.1"` (and title / meta `0.1`) because
`guides.xml`'s `version="0.10"` is numerically coerced to the float
`0.1` while parsing (Symfony `XmlUtils` phpize). That is tracked in
#1293 and fixed upstream in phpDocumentor/guides#1345. This PR makes the
switcher correct regardless — it derives the active version from the
page URL — so the two fixes are independent.

---------

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Two defects in the previous commit of this branch, both found by review and
both reproduced before fixing.

Detaching <project> can leave an empty root element behind, and
`XmlUtils::convertDomElementToArray()` returns null rather than an empty array
for one. `assert(is_array($rootConfig))` then fails for any `guides.xml` that
holds nothing but a project — which the `version-from-guides-xml` fixtures are.
It stays invisible under the default and the CI php.ini, both of which run with
`zend.assertions=-1`, where the following write to `$rootConfig['project']`
auto-vivifies null; with `zend.assertions=1` those two data sets fail.

`getElementsByTagName('project')` searches the whole subtree, and the schema
lets an `<extension>` carry arbitrary child elements. A nested `<project>` was
therefore read as the project configuration and removed from that extension,
while the real one at root level was dropped. Look at direct children only.

The new `version-from-guides-xml-nested-project` fixture covers the second
defect and fails against the previous state. The first one is only observable
with assertions enabled, and the existing fixtures already cover it there.

Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_015QXXkquh2eQNBiTYA39Wss
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
@CybotTM
CybotTM force-pushed the fix/xml-version-string-coercion branch from 702a579 to 55ea0c7 Compare August 15, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants