Diffstat (limited to 'internal/transcoder/markdown.go')
| -rw-r--r-- | internal/transcoder/markdown.go | 164 |
1 files changed, 0 insertions, 164 deletions
diff --git a/internal/transcoder/markdown.go b/internal/transcoder/markdown.go deleted file mode 100644 index 975c3bc..0000000 --- a/internal/transcoder/markdown.go +++ /dev/null @@ -1,164 +0,0 @@ -package transcoder - -import ( - "fmt" - "strings" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/extension" - xast "github.com/yuin/goldmark/extension/ast" - "github.com/yuin/goldmark/text" -) - -// telegramMD parses CommonMark once and is reused across calls. -var telegramMD = goldmark.New(goldmark.WithExtensions(extension.Strikethrough, extension.Linkify)) - -// markdownToTelegramHTML converts CommonMark source into the limited HTML -// subset Telegram accepts (parse_mode=HTML). Telegram supports only -// b/i/u/s/a/code/pre/blockquote — block constructs with no Telegram tag -// (paragraphs, headings, lists) are flattened to text + newlines. Anything -// outside the subset is dropped rather than emitted, so a message is never -// rejected for an unsupported tag. -func markdownToTelegramHTML(src string) string { - source := []byte(src) - doc := telegramMD.Parser().Parse(text.NewReader(source)) - - var b strings.Builder - renderNodes(&b, doc, source) - - // Collapse the runs of blank lines block rendering can leave behind. - out := strings.TrimSpace(b.String()) - for strings.Contains(out, "\n\n\n") { - out = strings.ReplaceAll(out, "\n\n\n", "\n\n") - } - return out -} - -func renderNodes(b *strings.Builder, parent ast.Node, source []byte) { - for n := parent.FirstChild(); n != nil; n = n.NextSibling() { - renderNode(b, n, source) - } -} - -func renderNode(b *strings.Builder, n ast.Node, source []byte) { - switch node := n.(type) { - case *ast.Document: - renderNodes(b, node, source) - - case *ast.Paragraph, *ast.TextBlock: - renderNodes(b, node, source) - b.WriteString("\n\n") - - case *ast.Heading: - // Telegram has no headings; render the line in bold. - b.WriteString("<b>") - renderNodes(b, node, source) - b.WriteString("</b>\n\n") - - case *ast.Blockquote: - b.WriteString("<blockquote>") - renderNodes(b, node, source) - trimTrailingNewlines(b) - b.WriteString("</blockquote>\n\n") - - case *ast.List: - renderList(b, node, source) - b.WriteString("\n") - - case *ast.FencedCodeBlock, *ast.CodeBlock: - b.WriteString("<pre>") - writeRawLines(b, n, source) - b.WriteString("</pre>\n\n") - - case *ast.ThematicBreak: - // horizontal rule — nothing meaningful in a Telegram message - - // --- inline --- - case *ast.Text: - b.WriteString(escapeHTML(string(node.Segment.Value(source)))) - if node.HardLineBreak() || node.SoftLineBreak() { - b.WriteByte('\n') - } - case *ast.String: - b.WriteString(escapeHTML(string(node.Value))) - - case *ast.Emphasis: - tag := "i" - if node.Level == 2 { - tag = "b" - } - fmt.Fprintf(b, "<%s>", tag) - renderNodes(b, node, source) - fmt.Fprintf(b, "</%s>", tag) - - case *xast.Strikethrough: - b.WriteString("<s>") - renderNodes(b, node, source) - b.WriteString("</s>") - - case *ast.CodeSpan: - b.WriteString("<code>") - renderNodes(b, node, source) - b.WriteString("</code>") - - case *ast.Link: - fmt.Fprintf(b, `<a href="%s">`, escapeHTML(string(node.Destination))) - renderNodes(b, node, source) - b.WriteString("</a>") - - case *ast.AutoLink: - url := string(node.URL(source)) - fmt.Fprintf(b, `<a href="%s">%s</a>`, escapeHTML(url), escapeHTML(url)) - - case *ast.Image: - // Images can't render inline in text; keep the alt text only. - renderNodes(b, node, source) - - case *ast.RawHTML, *ast.HTMLBlock: - // Drop raw HTML — it is almost certainly not in Telegram's tag subset. - - default: - // Unknown node: recurse so inline text inside it is not lost. - renderNodes(b, n, source) - } -} - -func renderList(b *strings.Builder, list *ast.List, source []byte) { - i := list.Start - for item := list.FirstChild(); item != nil; item = item.NextSibling() { - if list.IsOrdered() { - fmt.Fprintf(b, "%d. ", i) - i++ - } else { - b.WriteString("• ") - } - renderNodes(b, item, source) - trimTrailingNewlines(b) - b.WriteByte('\n') - } -} - -func writeRawLines(b *strings.Builder, n ast.Node, source []byte) { - lines := n.Lines() - for i := 0; i < lines.Len(); i++ { - seg := lines.At(i) - b.WriteString(escapeHTML(string(seg.Value(source)))) - } -} - -func trimTrailingNewlines(b *strings.Builder) { - s := strings.TrimRight(b.String(), "\n") - b.Reset() - b.WriteString(s) -} - -// escapeHTML escapes the three characters Telegram's HTML parser treats as -// markup. Quotes are escaped too so the value is safe inside an href="...". -func escapeHTML(s string) string { - s = strings.ReplaceAll(s, "&", "&") - s = strings.ReplaceAll(s, "<", "<") - s = strings.ReplaceAll(s, ">", ">") - s = strings.ReplaceAll(s, `"`, """) - return s -} |