Skip to content

Commit

Permalink
first pass at handling yaml blocks
Browse files Browse the repository at this point in the history
the triple quotes heredocs break yaml validation.
unless tell gets its own validators, its heredocs are... aspirational.
until then: support a leading pipe (|) as an "ambiguous" heredoc -- that depends on the closing quotes to know whether to escape and how to indent the block.
it's not yaml -- but its compatible enough with it for validation.
  • Loading branch information
ionous committed May 20, 2024
1 parent 2bfc064 commit 945f2cb
Show file tree
Hide file tree
Showing 15 changed files with 345 additions and 145 deletions.
14 changes: 14 additions & 0 deletions charmed/charmedEscape.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import (
"github.com/ionous/tell/runes"
)

func escapeString(w runes.RuneWriter, str string) (err error) {
// not going to win any awards for efficiency that's for sure.
return charm.ParseEof(str,
charm.Self("descape", func(self charm.State, q rune) (ret charm.State) {
if q == runes.Escape {
ret = charm.Step(decodeEscape(w), self)
} else if q != runes.Eof {
w.WriteRune(q)
ret = self
}
return
}))
}

// starting after a backslash, read an escape encoded rune.
// the subsequent rune will return unhandled.
// \xFF, \uFFFF, \UffffFFFF
Expand Down
60 changes: 42 additions & 18 deletions charmed/charmedHere.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,8 @@ func decodeHereAfter(out *strings.Builder, quote rune, escape bool) charm.State

func decodeBody(out *strings.Builder, escape bool, endTag []rune) charm.State {
var lines indentedLines
var lineBuf strings.Builder
return decodeLines(&lineBuf, escape, endTag, func(lineType lineType, lhs, rhs int) (err error) {
switch lineType {
case lineText:
lines.addLine(lhs, rhs, lineBuf.String())
lineBuf.Reset()
case lineClose:
err = lines.writeLines(out, lhs, !escape)
default:
panic("unknown lineType")
}
return
return decodeCustomTag(&lines, endTag, func(_ rune, depth int) error {
return lines.writeLines(out, depth, escape)
})
}

Expand All @@ -49,18 +39,52 @@ type indentedLine struct {
str string
}

type indentedLines []indentedLine
func (el *indentedLine) getLine(escape bool) (ret string, err error) {
if !escape || len(el.str) == 0 {
ret = el.str
} else {
var buf strings.Builder
if e := escapeString(&buf, el.str); e != nil {
err = e
} else {
ret = buf.String()
}
}
return
}

type indentedLines struct {
lines []indentedLine
buf strings.Builder
}

func (ls *indentedLines) WriteRune(r rune) (int, error) {
return ls.buf.WriteRune(r)
}

func (ls *indentedLines) WriteString(str string) (int, error) {
return ls.buf.WriteString(str)
}

func (ls *indentedLines) addLine(lhs, rhs int, str string) {
*ls = append(*ls, indentedLine{lhs, rhs, str})
ls.lines = append(ls.lines, indentedLine{lhs, rhs, str})
}

func (ls *indentedLines) nextLine(lhs, rhs int) {
ls.addLine(lhs, rhs, ls.buf.String())
ls.buf.Reset()
}

// a literalLine means every newline ( except the last ) is a newline.
// otherwise, it takes a fully blank line to write a newline
func (ls indentedLines) writeLines(out *strings.Builder, leftEdge int, literalLines bool) (err error) {
var afterNewLine bool // when writing interpreted lines, we want only a space OR a newline.
for i, el := range ls {
if str := el.str; len(str) == 0 {
func (ls indentedLines) writeLines(out *strings.Builder, leftEdge int, escape bool) (err error) {
literalLines := !escape // these could be tied to different states
var afterNewLine bool // when writing interpreted lines, we want only a space OR a newline.
for i, el := range ls.lines {
if str, e := el.getLine(escape); e != nil {
err = e
break
} else if len(str) == 0 {
out.WriteRune(runes.Newline)
afterNewLine = true
} else if newLhs := el.lhs - leftEdge; newLhs < 0 {
Expand Down
75 changes: 51 additions & 24 deletions charmed/charmedHere_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,52 @@ import (
_ "embed"
"strings"
"testing"
"unicode/utf8"

"github.com/ionous/tell/charm"
"github.com/ionous/tell/runes"
)

//go:embed hereExpected.test
var hereExpected string

//go:embed hereDoc.test
var hereDoc string

//go:embed hereDocRaw.test
var hereDocRaw string

//go:embed hereExpected.test
var hereExpected string
//go:embed yamlBlock.test
var yamlBlock string

//go:embed yamlBlockRaw.test
var yamlBlockRaw string

// test reading a full heredoc
func TestHereNow(t *testing.T) {
if got, e := testHere(hereDocRaw); e != nil {
t.Fatal("failed hereDocRaw", e)
} else if got != hereExpected {
t.Fatalf("hereDocRaw: \nhave: %q\nwant: %q", got, hereExpected)
t.Errorf("hereDocRaw: \nhave: %q\nwant: %q", got, hereExpected)
}
if got, e := testHere(hereDoc); e != nil {
t.Fatal("failed hereDoc", e)
} else if got != hereExpected {
t.Fatalf("hereDoc: \nhave: %q\nwant: %q", got, hereExpected)
t.Errorf("hereDoc: \nhave: %q\nwant: %q", got, hereExpected)
}
}

// test reading a yaml compatibility block
func TestYamlBlocks(t *testing.T) {
if got, e := testYamlBlock(yamlBlockRaw); e != nil {
t.Fatal("failed yamlBlockRaw", e)
} else if got != hereExpected {
t.Errorf("yamlBlockRaw: \nhave: %q\nwant: %q", got, hereExpected)
}
if got, e := testYamlBlock(yamlBlock); e != nil {
t.Fatal("failed yamlBlock", e)
} else if got != hereExpected {
t.Errorf("yamlBlock: \nhave: %q\nwant: %q", got, hereExpected)
}
}

Expand All @@ -37,13 +58,13 @@ func TestHereNow(t *testing.T) {
//
// ex. legal headers allow at most one redirect triplet, and it should always be followed by exactly one word.)
func TestHeader(t *testing.T) {
if got, e := testHeader("yaml<<<END"); e != nil {
if got, e := testHeader("lang<<<END"); e != nil {
t.Fatal(e)
} else if expect := "yaml[headerWord][headerRedirect]END[headerWord]"; got != expect {
} else if expect := "lang[headerWord][headerRedirect]END[headerWord]"; got != expect {
t.Fatal("got:", got)
} else if got, e := testHeader("yaml <<< END"); e != nil {
} else if got, e := testHeader("lang <<< END"); e != nil {
t.Fatal(e)
} else if expect := "yaml[headerWord][headerRedirect]END[headerWord]"; got != expect {
} else if expect := "lang[headerWord][headerRedirect]END[headerWord]"; got != expect {
t.Fatal("got:", got)
} else if got, e := testHeader("<<<"); e != nil {
t.Fatal(e)
Expand Down Expand Up @@ -72,33 +93,33 @@ func TestLiteralLines(t *testing.T) {
ls.addLine(4, 2, "b")
ls.addLine(2, 0, "c")
var buf strings.Builder
ls.writeLines(&buf, 2, false /*literalLine*/)
ls.writeLines(&buf, 2, true)
if got, expect := resolve(&buf),
" a b c"; got != expect {
t.Fatalf("\nhave: %q\nwant: %q", got, expect)
t.Errorf("\nhave: %q\nwant: %q", got, expect)
}
ls.writeLines(&buf, 2, true /*literalLine*/)
ls.writeLines(&buf, 2, false)
if got, expect := resolve(&buf),
" a\n b \nc"; got != expect {
t.Fatalf("\nhave: %q\nwant: %q", got, expect)
t.Errorf("\nhave: %q\nwant: %q", got, expect)
}
}

func TestBody(t *testing.T) {
if got, e := testBody("!!"); e != nil {
t.Fatal(e)
} else if expect := "[lineClose]"; got != expect {
t.Fatalf("\nhave: %q\nwant: %q", got, expect)
} else if expect := ""; got != expect {
t.Errorf("\nhave: %q\nwant: %q", got, expect)
}
if got, e := testBody("boop\nbop\nbeep\n!!"); e != nil {
t.Fatal(e)
} else if expect := "boop[lineText]bop[lineText]beep[lineText][lineClose]"; got != expect {
t.Fatalf("\nhave: %q\nwant: %q", got, expect)
} else if expect := "boop\nbop\nbeep"; got != expect {
t.Errorf("\nhave: %q\nwant: %q", got, expect)
}
if got, e := testBody("!partial!\n!!"); e != nil {
t.Fatal(e)
} else if expect := "!partial![lineText][lineClose]"; got != expect {
t.Fatalf("\nhave: %q\nwant: %q", got, expect)
} else if expect := "!partial!"; got != expect {
t.Errorf("\nhave: %q\nwant: %q", got, expect)
}
}

Expand All @@ -114,16 +135,22 @@ func testHere(str string) (ret string, err error) {
return
}

func testYamlBlock(str string) (ret string, err error) {
var d QuoteDecoder
q, size := utf8.DecodeRuneInString(str)
if e := charm.ParseEof(str[size:], d.Pipe(q)); e != nil {
err = e
} else {
ret = d.String()
}
return
}

func testBody(str string) (ret string, err error) {
var escape bool
var buf strings.Builder
var endTag = []rune{'!', '!'}
if e := parse(str, decodeLines(&buf, escape, endTag, func(cat lineType, lhs, rhs int) (_ error) {
buf.WriteRune('[')
buf.WriteString(cat.String())
buf.WriteRune(']')
return
})); e != nil {
if e := charm.ParseEof(str, decodeBody(&buf, escape, endTag)); e != nil {
err = e
} else {
ret = buf.String()
Expand Down
Loading

0 comments on commit 945f2cb

Please sign in to comment.