Skip to content

fix(document): preserve comments, plain-scalar leaders and comment co… - #5

Merged
ralflang merged 1 commit into
horde:FRAMEWORK_6_0from
meezaan:fix/round-trip-fidelity
Oct 2, 2026
Merged

ralflang merged 1 commit into
horde:FRAMEWORK_6_0from
meezaan:fix/round-trip-fidelity

Conversation

@meezaan

@meezaan meezaan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

…lumns on round trip

Three defects that each rewrote a line the author did not write that way, so a load/emit cycle was not byte-identical. All three are in the trivia path; none affects the data model.

  1. Multi-byte characters were truncated in comments and directives.

    The scanner advances by whole UTF-8 characters, but the trivia accumulators appended a single byte, so every continuation byte was dropped: "# see §4.3" was collected as "# see \xC24.3". The orphaned lead byte renders as U+FFFD, which made it look like character substitution rather than truncation.

    currentChar() already existed for exactly this, and its docblock says so - it was used for scalar content but not for trivia. Seven call sites.

  2. "?", ":" and "-" were treated as unconditionally reserved plain-scalar leaders.

    YAML 1.2 ns-plain-first admits them when followed by a plain-safe character; they are indicators only when standing alone or preceding a space. So "--cert-dir=/tmp" and "-single-dash" are valid plain scalars, and quoting them on emit changes the document's text. Values that would reparse as a non-string ("-5") are still quoted by the existing check.

  3. Standalone comments were re-indented to their surrounding container.

    The emitter already honoured CommentNode::getIndent() and the scanner already captured each comment's column, but the parser passed a hardcoded 0.

    The indent is now nullable rather than zero-defaulted, because column 1 is a real position: a flush-left comment inside an indented block is a deliberate style that "0 means unset" could not express. A programmatically created CommentNode has no recorded column and still follows its container.

Tests: 24 cases across three files, covering Latin-1, accented Latin, Arabic, CJK, emoji, end-of-line and indented comments; plain and genuinely-indicator leaders; and comment columns above, below and matching their siblings. 15 of the 24 fail without these changes.

The existing suite is unchanged at 1022 tests with the same single pre-existing error and failure (ConformanceTest fixtures and a PHPUnit 12 data-provider arity issue), neither related to this change.

@meezaan
meezaan force-pushed the fix/round-trip-fidelity branch from 2792bdf to 154cc12 Compare September 15, 2026 06:51
…ns and line endings on round trip

Four defects that each rewrote part of a document the author did not write that
way, so a load/emit cycle was not byte-identical. All four are in the trivia and
formatting paths; none affects the data model.

1. Multi-byte characters were truncated in comments and directives.

   The scanner advances by whole UTF-8 characters, but the trivia accumulators
   appended a single byte, so every continuation byte was dropped: "# see §4.3"
   was collected as "# see \xC24.3". The orphaned lead byte renders as U+FFFD,
   which made it look like character substitution rather than truncation.

   currentChar() already existed for exactly this, and its docblock says so -
   it was used for scalar content but not for trivia. Seven call sites.

2. "?", ":" and "-" were treated as unconditionally reserved plain-scalar
   leaders.

   YAML 1.2 ns-plain-first admits them when followed by a plain-safe character;
   they are indicators only when standing alone or preceding a space. So
   "--cert-dir=/tmp" and "-single-dash" are valid plain scalars, and quoting
   them on emit changes the document's text. Values that would reparse as a
   non-string ("-5") are still quoted by the existing check.

3. Standalone comments were re-indented to their surrounding container.

   The emitter already honoured CommentNode::getIndent() and the scanner
   already captured each comment's column, but the parser passed a hardcoded 0.

   The indent is now nullable rather than zero-defaulted, because column 1 is a
   real position: a flush-left comment inside an indented block is a deliberate
   style that "0 means unset" could not express. A programmatically created
   CommentNode has no recorded column and still follows its container.

4. CRLF documents were rewritten with LF.

   The scanner normalises CRLF to LF so the rest of the parser sees one line
   ending, and the emitter already writes whatever YamlStream::getLineEnding()
   names - but nothing ever called setLineEnding(), so the ending the source
   used was discarded. Pipeline::parse() already derives setTrailingNewline()
   from the raw source and now derives the line ending the same way.

   It is set only when every newline in the source is CRLF. A file with mixed
   endings has no single ending to preserve, and quietly picking one would
   rewrite the other.

   Raw source needed the same treatment: it is captured after normalisation, so
   a block scalar re-emitted verbatim carried LF into a CRLF document. It now
   goes through a helper that rewrites newlines to the document's ending, which
   is a no-op for LF.

Three of the four are omissions rather than disagreements: currentChar() was
written for exactly this truncation, CommentNode::getIndent() was already
honoured by the emitter, and YamlStream::getLineEnding() was already read by it.
In each case the mechanism existed and nothing wired it up. Only 2 changes
behaviour the code had deliberately chosen.

Tests: 32 cases across four files, covering Latin-1, accented Latin, Arabic,
CJK, emoji, end-of-line and indented comments; plain and genuinely-indicator
leaders; comment columns above, below and matching their siblings; and CRLF
with indentation, comments, sequences and block scalars. 21 of the 32 fail
without these changes.

The existing suite is unchanged at 1022 tests with the same single pre-existing
error and failure (ConformanceTest fixtures and a PHPUnit 12 data-provider
arity issue), neither related to these changes.
@meezaan
meezaan force-pushed the fix/round-trip-fidelity branch from 154cc12 to 6ff1a9c Compare September 15, 2026 06:57

@ralflang ralflang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. This is very helpful. I will release today.

@ralflang
ralflang merged commit 46d94b8 into horde:FRAMEWORK_6_0 Oct 2, 2026
1 check passed
@meezaan

meezaan commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Thank you. This is very helpful. I will release today.

Thank you! This package saved me a few days of work, and I'm immensely grateful for that.

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