-// markdownEntities converts the Markdown of a reply to plain text and Telegram
-// message entities. It knows bold (** and __), italic (* and _),
-// strikethrough (~~), inline code, fenced code blocks with an optional
-// language, links [text](url), and ATX headings, which become bold. Lists,
-// quotes, and tables stay as their text. A mark that does not close stays as
-// text with no entity, and code holds no entity.
-//
-// Inline marks do not cross a line break, so a mark that does not close cannot
-// change the rest of the reply.
-func markdownEntities(markdown string) (string, []botEntity) {
- var out markdownText
- lines := strings.Split(markdown, "\n")
- for index := 0; index < len(lines); index++ {
- if index > 0 {
- out.add("\n")
- }
-
- line := lines[index]
- language, fence := codeFence(line)
- end := closingFence(lines, index, fence)
- heading := headingText(line)
- switch {
- case end > index:
- start := out.units
- out.add(strings.Join(lines[index+1:end], "\n"))
- out.entity(botEntity{
- Type: "pre",
- Offset: start,
- Length: out.units - start,
- Language: language,
- })
-
- index = end
- case heading != "":
- start := out.units
- out.inline(heading)
- out.entity(botEntity{
- Type: "bold",
- Offset: start,
- Length: out.units - start,
- })
- default:
- out.inline(line)
- }
- }
-
- slices.SortStableFunc(out.entities, func(a, b botEntity) int {
- if a.Offset != b.Offset {
- return a.Offset - b.Offset
- }
-
- return b.Length - a.Length
- })
-
- return out.text.String(), out.entities
-}
-
-// add appends text.
-func (m *markdownText) add(text string) {
- m.text.WriteString(text)
- m.units += utf16Units(text)
-}
-
-// entity keeps entity, unless it is empty, because Telegram refuses an entity
-// of length 0.
-func (m *markdownText) entity(entity botEntity) {
- if entity.Length > 0 {
- m.entities = append(m.entities, entity)
- }
-}
-
-// inline appends one line of Markdown with its inline marks as entities. A
-// backslash before an ASCII punctuation character makes the character plain
-// text.
-func (m *markdownText) inline(line string) {
- plain := 0
- index := 0
- for index < len(line) {
- if line[index] == '\\' && index+1 < len(line) && strings.IndexByte(markdownPunctuation, line[index+1]) >= 0 {
- m.add(line[plain:index])
- plain = index + 1
- index += 2
-
- continue
- }
-
- span, found := markSpan(line, index)
- if !found {
- index++
- continue
- }
-
- m.add(line[plain:index])
- start := m.units
- if span.kind == "code" {
- m.add(span.content)
- } else {
- m.inline(span.content)
- }
-
- m.entity(botEntity{
- Type: span.kind,
- Offset: start,
- Length: m.units - start,
- URL: span.url,
- })
-
- index = span.end
- plain = span.end
- }
-
- m.add(line[plain:])
-}
-
-// codeFence returns the language of a line that opens a fenced code block, and
-// the fence. The fence is "" when line does not open a block.
-func codeFence(line string) (string, string) {