Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions ini_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1676,3 +1676,110 @@ key2 =
assert.Equal(t, "value3", f.Section("section2").Key("key3").String())
})
}

func TestQuotedInlineComments(t *testing.T) {
t.Run("single and double quoted values with comment symbols inside quotes", func(t *testing.T) {
f, err := Load([]byte(`
[background]
primary-color = '#e18477'
secondary-color = '#000000'
accent-color = "#123456"
symbol-hash = '#'
symbol-semi = ';'
symbol-hash-double = "#"
symbol-semi-double = ";"

[urls]
endpoint-single = 'http://example.com/api;v=1'
endpoint-double = "http://example.com/api;v=1"
`))
require.NoError(t, err)
require.NotNil(t, f)

bg := f.Section("background")
assert.Equal(t, "#e18477", bg.Key("primary-color").String())
assert.Equal(t, "#000000", bg.Key("secondary-color").String())
assert.Equal(t, "#123456", bg.Key("accent-color").String())
assert.Equal(t, "#", bg.Key("symbol-hash").String())
assert.Equal(t, ";", bg.Key("symbol-semi").String())
assert.Equal(t, "#", bg.Key("symbol-hash-double").String())
assert.Equal(t, ";", bg.Key("symbol-semi-double").String())

urls := f.Section("urls")
assert.Equal(t, "http://example.com/api;v=1", urls.Key("endpoint-single").String())
assert.Equal(t, "http://example.com/api;v=1", urls.Key("endpoint-double").String())
})

t.Run("quoted values with comment symbols followed by real comments", func(t *testing.T) {
f, err := Load([]byte(`
[background]
primary-color = '#e18477' # primary color comment
secondary-color = "#000000" ; secondary color comment
mixed = 'val # 1' ; comment with # and ; symbols
`))
require.NoError(t, err)
require.NotNil(t, f)

bg := f.Section("background")
assert.Equal(t, "#e18477", bg.Key("primary-color").String())
assert.Equal(t, "# primary color comment", bg.Key("primary-color").Comment)
assert.Equal(t, "#000000", bg.Key("secondary-color").String())
assert.Equal(t, "; secondary color comment", bg.Key("secondary-color").Comment)
assert.Equal(t, "val # 1", bg.Key("mixed").String())
assert.Equal(t, "; comment with # and ; symbols", bg.Key("mixed").Comment)
})

t.Run("quoted values with PreserveSurroundedQuote", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
PreserveSurroundedQuote: true,
}, []byte(`
[background]
primary-color = '#e18477'
secondary-color = "#000000"
with-comment = '#e18477' # color
`))
require.NoError(t, err)
require.NotNil(t, f)

bg := f.Section("background")
assert.Equal(t, "'#e18477'", bg.Key("primary-color").String())
assert.Equal(t, `"#000000"`, bg.Key("secondary-color").String())
assert.Equal(t, "'#e18477'", bg.Key("with-comment").String())
assert.Equal(t, "# color", bg.Key("with-comment").Comment)
})

t.Run("quoted values with SpaceBeforeInlineComment", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
SpaceBeforeInlineComment: true,
}, []byte(`
[colors]
with-space = '#e18477' # has space
without-space = '#e18477'#no space
`))
require.NoError(t, err)
require.NotNil(t, f)

sec := f.Section("colors")
assert.Equal(t, "#e18477", sec.Key("with-space").String())
assert.Equal(t, "# has space", sec.Key("with-space").Comment)
assert.Equal(t, `'#e18477'#no space`, sec.Key("without-space").String())
assert.Equal(t, "", sec.Key("without-space").Comment)
})

t.Run("unquoted apostrophe and escaped quotes", func(t *testing.T) {
f, err := Load([]byte(`
[notes]
apostrophe = don't drop this # important note
escaped = 'it\'s #value' # real comment
`))
require.NoError(t, err)
require.NotNil(t, f)

sec := f.Section("notes")
assert.Equal(t, "don't drop this", sec.Key("apostrophe").String())
assert.Equal(t, "# important note", sec.Key("apostrophe").Comment)
assert.Equal(t, `'it\'s #value'`, sec.Key("escaped").String())
assert.Equal(t, "# real comment", sec.Key("escaped").Comment)
})
}

65 changes: 53 additions & 12 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strconv"
"strings"
"unicode"
"unicode/utf8"
)

const minReaderBufferSize = 4096
Expand Down Expand Up @@ -233,6 +234,57 @@ func hasSurroundedQuote(in string, quote byte) bool {
strings.IndexByte(in[1:], quote) == len(in)-2
}

func isWordChar(prefix string) bool {
if len(prefix) == 0 {
return false
}
r, _ := utf8.DecodeLastRuneInString(prefix)
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
}

// findInlineComment returns the index of the inline comment delimiter in line,
// or -1 if no inline comment exists outside of quoted string literals.
func findInlineComment(line string, spaceBefore bool) int {
for j := 0; j < len(line); j++ {
ch := line[j]
if ch == '\\' {
j++
continue
}

if (ch == '"' || ch == '\'') && (j == 0 || !isWordChar(line[:j])) {
quote := ch
closed := false
k := j + 1
for ; k < len(line); k++ {
if line[k] == '\\' {
k++
continue
}
if line[k] == quote {
closed = true
break
}
}
if closed {
j = k
continue
}
}

if spaceBefore {
if ch == ' ' && j+1 < len(line) && (line[j+1] == '#' || line[j+1] == ';') {
return j
}
} else {
if ch == '#' || ch == ';' {
return j
}
}
}
return -1
}

func (p *parser) readValue(in []byte, bufferSize int) (string, error) {

line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
Expand Down Expand Up @@ -278,22 +330,11 @@ func (p *parser) readValue(in []byte, bufferSize int) (string, error) {

// Check if ignore inline comment
if !p.options.IgnoreInlineComment {
var i int
if p.options.SpaceBeforeInlineComment {
i = strings.Index(line, " #")
if i == -1 {
i = strings.Index(line, " ;")
}

} else {
i = strings.IndexAny(line, "#;")
}

i := findInlineComment(line, p.options.SpaceBeforeInlineComment)
if i > -1 {
p.comment.WriteString(line[i:])
line = strings.TrimSpace(line[:i])
}

}

// Trim single and double quotes
Expand Down
132 changes: 132 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,135 @@ func TestBadLoad(t *testing.T) {
})
})
}

func Test_findInlineComment(t *testing.T) {
tests := []struct {
name string
line string
spaceBefore bool
expected int
}{
{
name: "no comment",
line: "hello world",
spaceBefore: false,
expected: -1,
},
{
name: "hash comment without space requirement",
line: "hello#world",
spaceBefore: false,
expected: 5,
},
{
name: "hash comment with space before",
line: "hello #world",
spaceBefore: true,
expected: 5,
},
{
name: "hash without space when space required",
line: "hello#world",
spaceBefore: true,
expected: -1,
},
{
name: "semicolon comment without space requirement",
line: "hello;world",
spaceBefore: false,
expected: 5,
},
{
name: "semicolon comment with space before",
line: "hello ;world",
spaceBefore: true,
expected: 5,
},
{
name: "hash inside single quotes",
line: "'#e18477'",
spaceBefore: false,
expected: -1,
},
{
name: "hash inside double quotes",
line: `"#e18477"`,
spaceBefore: false,
expected: -1,
},
{
name: "semicolon inside single quotes",
line: "'val;ue'",
spaceBefore: false,
expected: -1,
},
{
name: "semicolon inside double quotes",
line: `"val;ue"`,
spaceBefore: false,
expected: -1,
},
{
name: "comment after single quoted string",
line: "'#e18477' # comment",
spaceBefore: false,
expected: 10,
},
{
name: "comment after single quoted string with space required",
line: "'#e18477' # comment",
spaceBefore: true,
expected: 9,
},
{
name: "comment after double quoted string",
line: `"#e18477" ; comment`,
spaceBefore: false,
expected: 10,
},
{
name: "comment after double quoted string with space required",
line: `"#e18477" ; comment`,
spaceBefore: true,
expected: 9,
},
{
name: "multiple quoted strings with comment",
line: `'#123', "#456" # comment`,
spaceBefore: false,
expected: 15,
},
{
name: "escaped quote inside quotes",
line: `"hello \"#\" world" # real comment`,
spaceBefore: false,
expected: 20,
},
{
name: "apostrophe in word followed by comment",
line: "don't touch # comment",
spaceBefore: false,
expected: 12,
},
{
name: "unclosed quote falls back to scanning",
line: `"unclosed # comment`,
spaceBefore: false,
expected: 10,
},
{
name: "escaped comment symbol outside quotes",
line: `value\#not_comment # real`,
spaceBefore: false,
expected: 19,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := findInlineComment(tt.line, tt.spaceBefore)
assert.Equal(t, tt.expected, actual)
})
}
}