Initial push to gitea
This commit is contained in:
154
internal/output/spotify.go
Normal file
154
internal/output/spotify.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Package output renders summaries to user-visible formats. Markdown is
|
||||
// passed through; Spotify HTML uses the small tag subset that Spotify for
|
||||
// Podcasters' show-notes editor accepts (b, i, a, ul/ol/li, paragraphs).
|
||||
package output
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
reBoldStar = regexp.MustCompile(`\*\*([^*\n]+)\*\*`)
|
||||
reBoldUnder = regexp.MustCompile(`__([^_\n]+)__`)
|
||||
reItalicStar = regexp.MustCompile(`\*([^*\n]+)\*`)
|
||||
reItalicUnder = regexp.MustCompile(`(^|[\s(])_([^_\n]+)_($|[\s).,!?;:])`)
|
||||
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)\s]+)\)`)
|
||||
reInlineCode = regexp.MustCompile("`([^`\n]+)`")
|
||||
)
|
||||
|
||||
// MarkdownToSpotifyHTML converts a markdown summary into the limited HTML
|
||||
// subset Spotify for Podcasters renders. Unknown markdown structures degrade
|
||||
// to plain text rather than producing rejected tags.
|
||||
func MarkdownToSpotifyHTML(md string) string {
|
||||
lines := strings.Split(strings.ReplaceAll(md, "\r\n", "\n"), "\n")
|
||||
|
||||
var out strings.Builder
|
||||
listKind := "" // "ul" or "ol" while we're inside a list
|
||||
flushList := func() {
|
||||
if listKind != "" {
|
||||
out.WriteString("</" + listKind + ">\n")
|
||||
listKind = ""
|
||||
}
|
||||
}
|
||||
openList := func(kind string) {
|
||||
if listKind != kind {
|
||||
flushList()
|
||||
out.WriteString("<" + kind + ">\n")
|
||||
listKind = kind
|
||||
}
|
||||
}
|
||||
|
||||
paragraph := []string{}
|
||||
flushPara := func() {
|
||||
if len(paragraph) == 0 {
|
||||
return
|
||||
}
|
||||
text := strings.Join(paragraph, " ")
|
||||
out.WriteString("<p>" + inline(text) + "</p>\n")
|
||||
paragraph = paragraph[:0]
|
||||
}
|
||||
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimRight(raw, " \t")
|
||||
trim := strings.TrimSpace(line)
|
||||
|
||||
// Blank line: end current paragraph/list block.
|
||||
if trim == "" {
|
||||
flushPara()
|
||||
flushList()
|
||||
continue
|
||||
}
|
||||
|
||||
// Horizontal rule.
|
||||
if trim == "---" || trim == "***" || trim == "___" {
|
||||
flushPara()
|
||||
flushList()
|
||||
continue
|
||||
}
|
||||
|
||||
// Heading -> bold paragraph.
|
||||
if h := headingText(trim); h != "" {
|
||||
flushPara()
|
||||
flushList()
|
||||
out.WriteString("<p><b>" + inline(h) + "</b></p>\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Blockquote -> italic paragraph.
|
||||
if strings.HasPrefix(trim, "> ") {
|
||||
flushPara()
|
||||
flushList()
|
||||
out.WriteString("<p><i>" + inline(strings.TrimPrefix(trim, "> ")) + "</i></p>\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Unordered list item.
|
||||
if strings.HasPrefix(trim, "- ") || strings.HasPrefix(trim, "* ") || strings.HasPrefix(trim, "+ ") {
|
||||
flushPara()
|
||||
openList("ul")
|
||||
out.WriteString(" <li>" + inline(trim[2:]) + "</li>\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Ordered list item like "1. text".
|
||||
if item, ok := orderedItem(trim); ok {
|
||||
flushPara()
|
||||
openList("ol")
|
||||
out.WriteString(" <li>" + inline(item) + "</li>\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Anything else: append to current paragraph.
|
||||
flushList()
|
||||
paragraph = append(paragraph, trim)
|
||||
}
|
||||
|
||||
flushPara()
|
||||
flushList()
|
||||
|
||||
return strings.TrimRight(out.String(), "\n")
|
||||
}
|
||||
|
||||
func headingText(s string) string {
|
||||
// Up to 6 leading '#' followed by a space.
|
||||
hashes := 0
|
||||
for hashes < len(s) && s[hashes] == '#' {
|
||||
hashes++
|
||||
}
|
||||
if hashes == 0 || hashes > 6 || hashes >= len(s) || s[hashes] != ' ' {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(s[hashes+1:])
|
||||
}
|
||||
|
||||
func orderedItem(s string) (string, bool) {
|
||||
i := 0
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
if i == 0 || i+1 >= len(s) || s[i] != '.' || s[i+1] != ' ' {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(s[i+2:]), true
|
||||
}
|
||||
|
||||
func inline(s string) string {
|
||||
s = escapeHTML(s)
|
||||
s = reInlineCode.ReplaceAllString(s, "$1")
|
||||
s = reBoldStar.ReplaceAllString(s, "<b>$1</b>")
|
||||
s = reBoldUnder.ReplaceAllString(s, "<b>$1</b>")
|
||||
s = reItalicStar.ReplaceAllString(s, "<i>$1</i>")
|
||||
s = reItalicUnder.ReplaceAllString(s, "$1<i>$2</i>$3")
|
||||
s = reLink.ReplaceAllString(s, `<a href="$2">$1</a>`)
|
||||
return s
|
||||
}
|
||||
|
||||
func escapeHTML(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
Reference in New Issue
Block a user