diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0c17cf9..96e298d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: '1.19' + go-version: 'stable' - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get dependencies run: go mod download @@ -33,15 +33,17 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: - go-version: '1.19' + go-version: 'stable' - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest + # skip cache to avoid flakes (and avoid using gh-action storage) + skip-cache: true + skip-save-cache: true diff --git a/common.go b/common.go index 63ae985..063caaa 100644 --- a/common.go +++ b/common.go @@ -18,11 +18,11 @@ func validStart(pos int, input []rune) bool { } // If the previous char is alphanumeric, it is an invalid start char. - return !(unicode.IsLetter(input[pos-1]) || unicode.IsDigit(input[pos-1])) + return !unicode.IsLetter(input[pos-1]) && !unicode.IsDigit(input[pos-1]) } func validEnd(pos int, input []rune) bool { - // First char is not a valid end char. + // First char is not a valid end char; we do NOT allow empty entities. // If the end char has a space before it, its not valid either. if pos == 0 || unicode.IsSpace(input[pos-1]) { return false @@ -34,17 +34,7 @@ func validEnd(pos int, input []rune) bool { } // If the next char is alphanumeric, it is an invalid end char. - return !(unicode.IsLetter(input[pos+1]) || unicode.IsDigit(input[pos+1])) -} - -func contains(r rune, rr []rune) bool { - for _, x := range rr { - if r == x { - return true - } - } - - return false + return !unicode.IsLetter(input[pos+1]) && !unicode.IsDigit(input[pos+1]) } var link = regexp.MustCompile(`a href="(.*)"`) diff --git a/commonV2.go b/commonV2.go index 8bc9173..527c818 100644 --- a/commonV2.go +++ b/commonV2.go @@ -4,8 +4,22 @@ import ( "strings" ) +func hasPrefix(s string, tgPrefixes []string) bool { + rest, ok := strings.CutPrefix(s, "tg://") + if !ok { + return false + } + + for _, prefix := range tgPrefixes { + if strings.HasPrefix(rest, prefix) { + return true + } + } + return false +} + // finds the middle '](' section of in a link markdown -func findLinkMidSectionIdx(in []rune, emoji bool) int { +func findLinkMidSectionIdx(in []rune, tgSpecial bool) int { var textEnd int var offset int for offset < len(in) { @@ -15,8 +29,8 @@ func findLinkMidSectionIdx(in []rune, emoji bool) int { } textEnd = offset + idx if !IsEscaped(in, textEnd) { - isEmoji := strings.HasPrefix(string(in[textEnd+2:]), "tg://emoji?id=") - if (isEmoji && emoji) || (!isEmoji && !emoji) { + prefixed := hasPrefix(string(in[textEnd+2:]), []string{"emoji?", "time?"}) + if (prefixed && tgSpecial) || (!prefixed && !tgSpecial) { return textEnd } } @@ -44,8 +58,8 @@ func findLinkEndSectionIdx(in []rune) int { } // finds the middle and closing sections of in a link markdown -func findLinkSectionsIdx(in []rune, isEmojiLink bool) (int, int) { - textEnd := findLinkMidSectionIdx(in, isEmojiLink) +func findLinkSectionsIdx(in []rune, tgSpecial bool) (int, int) { + textEnd := findLinkMidSectionIdx(in, tgSpecial) if textEnd < 0 { return -1, -1 } @@ -60,7 +74,7 @@ func findLinkSectionsIdx(in []rune, isEmojiLink bool) (int, int) { // Now, we iterate over the text in between the mid and end sections to see if any other mid sections exist. // If yes, we choose those instead - it would be invalid in a URL anyway. for textEnd < offsetLinkEnd { - newTextEnd := findLinkMidSectionIdx(in[textEnd+1:offsetLinkEnd], isEmojiLink) + newTextEnd := findLinkMidSectionIdx(in[textEnd+1:offsetLinkEnd], tgSpecial) if newTextEnd == -1 { break } @@ -70,9 +84,9 @@ func findLinkSectionsIdx(in []rune, isEmojiLink bool) (int, int) { return textEnd, offsetLinkEnd } -func getLinkContents(in []rune, emoji bool) (bool, []rune, string, int) { +func getLinkContents(in []rune, tgSpecial bool) (bool, []rune, string, int) { // find ]( and then ) - textEndIdx, urlEndIdx := findLinkSectionsIdx(in, emoji) + textEndIdx, urlEndIdx := findLinkSectionsIdx(in, tgSpecial) if textEndIdx < 0 || urlEndIdx < 0 { return false, nil, "", 0 } @@ -82,6 +96,13 @@ func getLinkContents(in []rune, emoji bool) (bool, []rune, string, int) { return true, text, content, urlEndIdx + 1 } +func isCodeBlockNewlineEnd(in []rune, offset int, item string) bool { + if item != "```" { + return false + } + return offset == 0 || in[offset-1] == '\n' +} + func getValidEnd(in []rune, s string) int { offset := 0 for offset < len(in) { @@ -92,7 +113,7 @@ func getValidEnd(in []rune, s string) int { end := offset + idx // validEnd check has double logic to account for multi char strings - if validEnd(end, in) && validEnd(end+len(s)-1, in) && !IsEscaped(in, end) { + if (validEnd(end, in) || isCodeBlockNewlineEnd(in, end, s)) && validEnd(end+len(s)-1, in) && !IsEscaped(in, end) { idx = stringIndex(in[end+1:], s) for idx == 0 { end++ @@ -148,7 +169,7 @@ func isClosingTag(in []rune, pos int) bool { return false } -func getClosingTag(in []rune, tag string) (int, int) { +func getClosingTag(in []rune, openingTag string, closingTag string) (int, int) { offset := 0 subtags := 0 for offset < len(in) { @@ -164,9 +185,9 @@ func getClosingTag(in []rune, tag string) (int, int) { } closingTagIdx := openingTagIdx + 2 + c - if string(in[openingTagIdx+1:closingTagIdx]) == tag { // found a nested tag, this is annoying + if string(in[openingTagIdx+1:closingTagIdx]) == openingTag { // found a nested tag, this is annoying subtags++ - } else if isClosingTag(in, openingTagIdx) && string(in[openingTagIdx+2:closingTagIdx]) == tag { + } else if isClosingTag(in, openingTagIdx) && string(in[openingTagIdx+2:closingTagIdx]) == closingTag { if subtags == 0 { return openingTagIdx, closingTagIdx } diff --git a/common_test.go b/common_test.go new file mode 100644 index 0000000..11c66c3 --- /dev/null +++ b/common_test.go @@ -0,0 +1,14 @@ +package tg_md2html_test + +import "github.com/PaulSonOfLars/gotg_md2html" + +func testConverter() *tg_md2html.ConverterV2 { + return tg_md2html.NewV2(map[string]string{ + "url": "buttonurl", + "text": "buttontext", + }, map[string]string{ + "primary": "primary", + "success": "success", + "danger": "danger", + }) +} diff --git a/go.mod b/go.mod index 77b6cd7..9ac6ff5 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/PaulSonOfLars/gotg_md2html -go 1.19 +go 1.24 -require github.com/stretchr/testify v1.8.4 +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fa4b6e6..c4c1710 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/md2html.go b/md2html.go index c2e1447..0275cb0 100644 --- a/md2html.go +++ b/md2html.go @@ -2,6 +2,8 @@ package tg_md2html import ( "html" + "maps" + "slices" "strings" ) @@ -102,7 +104,7 @@ func (cv *Converter) md2html(input []rune, buttons bool) (string, []Button) { v[char] = append(v[char], pos-offset) containedMDChars = append(containedMDChars, char) case '\\': - if len(input) <= pos+1 || !contains(input[pos+1], allMdChars) { + if len(input) <= pos+1 || !slices.Contains(allMdChars, input[pos+1]) { continue } escaped = true @@ -192,9 +194,7 @@ func (cv *Converter) md2html(input []rune, buttons bool) (string, []Button) { } i = cnt // set i to copy - for x, y := range bkp { - v[x] = y - } + maps.Copy(v, bkp) case '[': nameOpen, rest := posArr[0], posArr[1:] @@ -405,7 +405,7 @@ func (cv *Converter) stripHTML(r []rune) string { func EscapeMarkdown(r []rune, toEscape []rune) string { out := strings.Builder{} for i, x := range r { - if contains(x, toEscape) { + if slices.Contains(toEscape, x) { if i == 0 || i == len(r)-1 || validEnd(i, r) || validStart(i, r) { out.WriteRune('\\') } diff --git a/md2htmlV2.go b/md2htmlV2.go index 56d3a5a..5b1e03b 100644 --- a/md2htmlV2.go +++ b/md2htmlV2.go @@ -2,33 +2,65 @@ package tg_md2html import ( "html" + "net/url" + "slices" "sort" "strings" + "unicode" ) var defaultConverterV2 = ConverterV2{ Prefixes: map[string]string{ - "url": "buttonurl:", + "url": "buttonurl", + }, + Styles: map[string]string{ + "primary": "primary", + "success": "success", + "danger": "danger", }, SameLineSuffix: sameLineSuffix, } -// ButtonV2 identifies a button. It can contain either a URL, or Text, depending on whether it is a buttonURL: or a buttonText: +// ButtonV2 identifies a button. +// The markdown syntax for a button is as such (where <> represents fields) +// [](:) +// [](::) +// [](#