From c6b1efb41b790f8487e6924b623d621010e878fa Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:13:40 -0700 Subject: [PATCH] Quote multiline scalars that start with tabs --- internal/libyaml/node.go | 4 ++ internal/libyaml/serializer.go | 4 +- internal/libyaml/testdata/node.yaml | 15 +++++++ yaml_test.go | 67 ++++++++++++++++++++++++++--- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/internal/libyaml/node.go b/internal/libyaml/node.go index 4fee73b3..761dc2c0 100644 --- a/internal/libyaml/node.go +++ b/internal/libyaml/node.go @@ -250,6 +250,10 @@ func shouldUseLiteralStyle(s string) bool { if !strings.Contains(s, "\n") || len(s) < 2 { return false } + withoutLeadingBreaks := strings.TrimLeft(s, "\r\n\u0085\u2028\u2029") + if strings.HasPrefix(withoutLeadingBreaks, "\t") { + return false + } // Must contain at least one non-whitespace character for _, r := range s { if !unicode.IsSpace(r) { diff --git a/internal/libyaml/serializer.go b/internal/libyaml/serializer.go index 4914dce0..db90afa1 100644 --- a/internal/libyaml/serializer.go +++ b/internal/libyaml/serializer.go @@ -215,8 +215,10 @@ func (s *Serializer) node(node *Node, tail string) { style = LITERAL_SCALAR_STYLE case node.Style&FoldedStyle != 0: style = FOLDED_SCALAR_STYLE - case strings.Contains(value, "\n"): + case shouldUseLiteralStyle(value): style = LITERAL_SCALAR_STYLE + case strings.Contains(value, "\n"): + style = DOUBLE_QUOTED_SCALAR_STYLE case forceQuoting: style = s.quotePreference.ScalarStyle() } diff --git a/internal/libyaml/testdata/node.yaml b/internal/libyaml/testdata/node.yaml index c3cc655e..a82c794e 100644 --- a/internal/libyaml/testdata/node.yaml +++ b/internal/libyaml/testdata/node.yaml @@ -298,6 +298,21 @@ from: "hello\nworld" want: true +- name: shouldUseLiteralStyle with leading tab + type: should-literal + from: "\tcontent\nnext" + want: false + +- name: shouldUseLiteralStyle with leading break then tab + type: should-literal + from: "\n\tcontent\nnext" + want: false + +- name: shouldUseLiteralStyle with tab on later line + type: should-literal + from: "first\n\tsecond" + want: true + - name: shouldUseLiteralStyle with single char type: should-literal from: "a" diff --git a/yaml_test.go b/yaml_test.go index 33319910..fe5c190d 100644 --- a/yaml_test.go +++ b/yaml_test.go @@ -2984,29 +2984,39 @@ func TestScalarStyleWithTabs(t *testing.T) { }, { "\tThis starts with tab\nand is long enough\nfor literal style", - "|-\n \tThis starts with tab\n and is long enough\n for literal style\n", + "\"\\tThis starts with tab\\nand is long enough\\nfor literal style\"\n", "Multiline starting with tab", }, { "\tB\n\tC\n", - "|\n \tB\n \tC\n", + "\"\\tB\\n\\tC\\n\"\n", "Tab B newline tab C newline", }, { "\ta\n", - "|\n \ta\n", + "\"\\ta\\n\"\n", "Tab + char + newline", }, { "\thello\n", - "|\n \thello\n", + "\"\\thello\\n\"\n", "Tab + text + newline", }, { "\t\nhello", - "|-\n \t\n hello\n", + "\"\\t\\nhello\"\n", "Tab + newline + text", }, + { + "\n\tthis\nnext", + "\"\\n\\tthis\\nnext\"\n", + "Tab after initial newline", + }, + { + "first\n\tsecond", + "|-\n first\n \tsecond\n", + "Tab after content line", + }, } for i, testCase := range testCases { @@ -3018,6 +3028,53 @@ func TestScalarStyleWithTabs(t *testing.T) { } } +func TestLeadingTabScalarRoundTrip(t *testing.T) { + testCases := []struct { + value string + mapYAML string + scalarYAML string + }{ + { + "\tthis\nis\nmultiline", + "text: \"\\tthis\\nis\\nmultiline\"\n", + "\"\\tthis\\nis\\nmultiline\"\n", + }, + { + "\n\tthis\nnext", + "text: \"\\n\\tthis\\nnext\"\n", + "\"\\n\\tthis\\nnext\"\n", + }, + { + "first\n\tsecond", + "text: |-\n first\n \tsecond\n", + "|-\n first\n \tsecond\n", + }, + } + + for _, testCase := range testCases { + wantMap := map[string]string{"text": testCase.value} + data, err := yaml.Marshal(wantMap) + assert.NoError(t, err) + assert.Equal(t, testCase.mapYAML, string(data)) + + var gotMap map[string]string + err = yaml.Unmarshal(data, &gotMap) + assert.NoError(t, err) + assert.DeepEqual(t, wantMap, gotMap) + + var node yaml.Node + node.SetString(testCase.value) + data, err = yaml.Marshal(&node) + assert.NoError(t, err) + assert.Equal(t, testCase.scalarYAML, string(data)) + + var gotScalar string + err = yaml.Unmarshal(data, &gotScalar) + assert.NoError(t, err) + assert.Equal(t, testCase.value, gotScalar) + } +} + func TestUnicodeWhitespaceHandling(t *testing.T) { // Test cases for Unicode whitespace characters that should be properly handled // by the shouldUseLiteralStyle function using unicode.IsSpace()