telegramify

package module
v0.0.0-...-352a3a9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 31 Imported by: 0

README

telegramify-md-go

A native Go port of sudoskys/telegramify-markdown, based on version 1.4.0. Requires Go 1.24 or newer. The package name is telegramify.

Goldmark parses CommonMark, tables, task lists and footnotes. Telegram-specific rendering, spoilers, math, UTF-16 offsets, message splitting, Mermaid handling and streaming are implemented in Go. There is no Python runtime dependency.

import telegramify "github.com/discapes/telegramify-md-go"

text, entities := telegramify.Convert("Hello **world** 😀", nil)
// text: "Hello world 😀"
// entities: [{Type:"bold", Offset:6, Length:5}]

Pass text and entities to your bot SDK's message API. Entity structs have Telegram-compatible JSON tags. No parse mode is needed. For middleware requiring MarkdownV2:

markdown := telegramify.Markdownify("Hello **world**!", nil)
// Hello *world*\!

API

Python Go
convert Convert(markdown, options) → text, entities
convert_with_segments ConvertWithSegments(markdown, options) → text, entities, segments
markdownify, standardize Markdownify, Standardize
entities_to_markdownv2 EntitiesToMarkdownV2(text, entities)
utf16_len, count_text UTF16Len, CountText
split_entities SplitEntities(text, entities, limit) → chunks, error
split_markdownv2 SplitMarkdownV2(text, entities, limit) → strings, error
telegramify Telegramify(ctx, markdown, options) → content, error
richify Richify(markdown, options) → payload, error
split_rich SplitRich(payload, options) → payloads, error
telegramify_rich TelegramifyRich(markdown, options) → rich messages, error
LatexToUnicodeHelper.convert LatexToUnicode(latex)
get_filename, get_ext GetFilename, GetExt
Mermaid URL/render helpers GeneratePako, MermaidLiveURL, MermaidInkURL, RenderMermaid
StreamCore, DraftStream, EditStream stream.NewCore, stream.NewDraftStream, stream.NewEditStream

nil options select defaults. DefaultConfig() returns an independent value; there is no mutable global configuration. Start with that value when customizing symbols:

config := telegramify.DefaultConfig()
config.Symbols.UnorderedListItem = "-"
config.CiteExpandable = false
text, entities := telegramify.Convert(markdown, &telegramify.ConvertOptions{
    Config: &config,
})

The default heading symbols and styles match Python. H1–H2 use bold and underline, H3–H4 use bold, and H5–H6 use italic. Tables become fixed-width pre entities. Code regions preserve literal spoiler delimiters. LaTeX conversion supports the original symbol/style tables, fractions, roots, scripts, accents and environments.

DisableLatexEscape disables preprocessing of \(...\) and \[...\]. Dollar-delimited math remains supported, matching Python. Rich rendering retains LaTeX in <tg-math> and <tg-math-block> by default.

Content pipeline

options := telegramify.DefaultTelegramifyOptions()
options.MaxMessageLength = 4096
options.MinFileLines = 3
options.DisableMermaid = true

parts, err := telegramify.Telegramify(ctx, markdown, &options)
if err != nil {
    return err
}
for _, part := range parts {
    switch item := part.(type) {
    case telegramify.Text:
        // sendMessage: item.Text, item.Entities
    case telegramify.File:
        // sendDocument: item.FileName, item.FileData
    case telegramify.Photo:
        // sendPhoto: item.FileData, item.CaptionText, item.CaptionEntities
    }
}

The default pipeline extracts code blocks with at least one line. Set MinFileLines to zero to keep code inline. DefaultTelegramifyOptions() makes those defaults explicit; a struct literal with MinFileLines: 0 disables extraction.

Mermaid rendering defaults to the public mermaid.ink service and sends diagram source to that service. Disable it with DisableMermaid, or set the RenderMermaid callback for a local renderer. HTTP requests accept cancellation and use a ten-second timeout by default. Rendering failures produce invalid_mermaid.txt; cancellation returns the context error. Successful photos carry an “Edit on mermaid.live” caption link. Unit tests use a local HTTP server and injected renderers.

Rich messages

payload, err := telegramify.Richify(markdown, nil)
if err != nil {
    return err
}
parts, err := telegramify.SplitRich(payload, nil)
// Each InputRichMessage serializes to {"html":"..."}.

RichOptions{Mode: "markdown"} passes through Markdown. IsRTL and SkipEntityDetection are optional *bool fields. Exactly one of InputRichMessage.HTML and .Markdown must be set, including for empty bodies; validation runs when splitting or JSON-encoding.

The defaults are 32,768 UTF-8 bytes and 500 top-level blocks. Splitting closes and reopens inline tags, preserves escaped characters, and continues ordered-list numbering. As in Python, unsplittable atomic blocks (for example, a single oversized table row) are returned alone and logged with slog.Warn; inspect their size if strict rejection is required before sending.

Streaming

The stream subpackage uses callbacks, so it works with any Telegram SDK. Each stream owns one goroutine. Feed is concurrency-safe; callbacks run serially. Call Finish, Cancel, or cancel the construction context to release the worker. Callbacks must honor their context and must not synchronously call Finish or Cancel on their own stream.

import "github.com/discapes/telegramify-md-go/stream"

s, err := stream.NewDraftStream(ctx, sendDraft, sendFinal, &stream.DraftOptions{
    Options: stream.Options{Mode: stream.EntityMode},
    DraftID: 123,
})
if err != nil {
    return err
}
defer s.Cancel()

for token := range tokens {
    if err := s.Feed(token); err != nil {
        return err
    }
}
return s.Finish()

sendDraft and sendFinal have signature func(context.Context, stream.Payload) error. Draft payloads include DraftID; final payloads omit it. Consume(ctx, tokens) can consume a <-chan string and finish automatically.

Defaults: rich mode, a 300ms draft interval, a 500ms thinking delay, and a 25s keepalive. Entity drafts retain the final 4,096 UTF-16 units, dropping entities when truncated. Rich drafts retain the last chunk from SplitRich. Finalization sends the complete buffer as one payload; use the content pipelines in your callback if final messages require splitting. Three consecutive emission failures disable interim emissions; finalization still runs. Inspect Degraded() and LastError() for delivery failures.

NewEditStream takes send and edit callbacks (func(context.Context, Payload) (int64, error) and func(context.Context, int64, Payload) error). It sends once and edits the returned message ID thereafter. Its minimum interval is one second. MessageID() reports the ID once sent. Both strategies copy their supplied render configuration when constructed.

Compatibility and validation

The port is tested against captured Python outputs, including the upstream repository's Markdown showcase documents. Intentional Go differences:

  • Configuration is passed by value/pointer per request. Deprecated Python 0.x parameters and dynamic compatibility properties are omitted.
  • Segment text positions are UTF-8 byte offsets; entity positions remain UTF-16 units.
  • Invalid limits and budgets too small for a Unicode rune return errors instead of exceeding the limit.
  • Custom emoji links correctly close their entity. Python 1.4.0 loses that entity for [emoji](tg://emoji?id=...); image syntax already works in both versions.
  • Rich drafts split at HTML boundaries; Python truncates raw HTML bytes. Unsplittable atomic blocks retain the rich splitter's behavior described above.
  • Mermaid Pako strings can differ because JSON and compression encodings differ; their decoded diagram and theme are equivalent.

Goldmark and Python's pulldown-cmark parser can differ on obscure or malformed Markdown. Live Telegram delivery is not part of the offline test suite.

cd telegramify-md-go
go test -race ./...
go vet ./...
go test -run '^$' -fuzz FuzzConversionAndSplitting -fuzztime=20s -parallel=2

Python is needed only to regenerate reference fixtures and the symbol tables. From this repository root, use a Python environment with the upstream package installed from a checkout of version 1.4.0. Pass that checkout to the fixture generator:

python scripts/generate_fixtures.py /path/to/telegramify-markdown
python scripts/generate_latex.py
gofmt -w latex_tables.go

Licensed under MIT; the upstream copyright notice is preserved in LICENSE.

Documentation

Overview

Package telegramify converts Markdown into Telegram text, entities and rich messages.

Code generated by scripts/generate_latex.py; DO NOT EDIT.

Index

Examples

Constants

View Source
const RichBlockLimit = 500
View Source
const RichByteLimit = 32768

Variables

This section is empty.

Functions

func ConvertWithSegments

func ConvertWithSegments(markdown string, options *ConvertOptions) (string, []MessageEntity, []Segment)

func CountText

func CountText(text string) int

CountText counts Telegram plain-text length, including astral Unicode characters.

func DownloadImage

func DownloadImage(ctx context.Context, url string, client *http.Client) ([]byte, error)

func EntitiesToMarkdownV2

func EntitiesToMarkdownV2(text string, entities []MessageEntity) string

EntitiesToMarkdownV2 serializes text and entities for parse_mode="MarkdownV2". Entity boundaries inside a UTF-16 surrogate pair or outside text are ignored.

func ExtractValidFilename

func ExtractValidFilename(line string) string

func GeneratePako

func GeneratePako(diagram string, config *RenderConfig) string

GeneratePako encodes diagram source and theme for mermaid.live and mermaid.ink.

func GetExt

func GetExt(language string) string

func GetFilename

func GetFilename(code, language string) string

func IsImage

func IsImage(data []byte) bool

func LatexToUnicode

func LatexToUnicode(input string) string

LatexToUnicode converts mathematical LaTeX into readable Unicode text. Unknown commands are preserved. It does not invoke TeX or external programs.

func Markdownify

func Markdownify(markdown string, options *ConvertOptions) string

func MermaidInkURL

func MermaidInkURL(diagram string, config *RenderConfig) string

func MermaidLiveURL

func MermaidLiveURL(diagram string, config *RenderConfig) string

func RenderMermaid

func RenderMermaid(ctx context.Context, diagram string, client *http.Client, config *RenderConfig) ([]byte, error)

RenderMermaid downloads and validates an image from mermaid.ink. A nil client uses a ten-second timeout. Cancellation always propagates through the request.

func SplitMarkdownV2

func SplitMarkdownV2(text string, entities []MessageEntity, limit int) ([]string, error)

SplitMarkdownV2 includes escapes and formatting markers in its length budget.

func Standardize

func Standardize(markdown string, options *ConvertOptions) string

func UTF16Len

func UTF16Len(text string) int

Types

type Content

type Content interface{ Type() ContentType }

Content is one sendable item from Telegramify or TelegramifyRich. Use a type switch to select sendMessage, sendDocument, sendPhoto or sendRichMessage.

func Telegramify

func Telegramify(ctx context.Context, markdown string, options *TelegramifyOptions) ([]Content, error)

Telegramify converts, extracts code and Mermaid blocks, and splits text. Default Mermaid rendering sends diagram source to mermaid.ink. Set DisableMermaid to keep diagrams inline, or provide a local RenderMermaid.

Example
package main

import (
	"context"
	"fmt"

	telegramify "github.com/discapes/telegramify-md-go"
)

func main() {
	options := telegramify.DefaultTelegramifyOptions()
	options.DisableMermaid = true
	parts, err := telegramify.Telegramify(context.Background(), "Intro\n\n```go\nfmt.Println(1)\n```", &options)
	if err != nil {
		panic(err)
	}
	for _, part := range parts {
		fmt.Println(part.Type())
	}
}
Output:
text
file

type ContentTrace

type ContentTrace struct {
	SourceType string         `json:"source_type"`
	Extra      map[string]any `json:"extra,omitempty"`
}

type ContentType

type ContentType string
const (
	ContentText  ContentType = "text"
	ContentFile  ContentType = "file"
	ContentPhoto ContentType = "photo"
	ContentRich  ContentType = "rich"
)

type ConvertOptions

type ConvertOptions struct {
	Config             *RenderConfig
	DisableLatexEscape bool
}

ConvertOptions uses defaults when nil. DisableLatexEscape affects only \(...\) and \[...\] preprocessing; dollar-delimited math is always recognized.

type File

type File struct {
	FileName        string          `json:"file_name"`
	FileData        []byte          `json:"file_data"`
	ContentTrace    ContentTrace    `json:"content_trace"`
	CaptionText     string          `json:"caption_text,omitempty"`
	CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
}

func (File) Type

func (File) Type() ContentType

type InputRichMessage

type InputRichMessage struct {
	HTML                *string `json:"html,omitempty"`
	Markdown            *string `json:"markdown,omitempty"`
	IsRTL               *bool   `json:"is_rtl,omitempty"`
	SkipEntityDetection *bool   `json:"skip_entity_detection,omitempty"`
}

InputRichMessage holds exactly one body, including when that body is empty.

func Richify

func Richify(markdown string, options *RichOptions) (InputRichMessage, error)

func SplitRich

func SplitRich(message InputRichMessage, options *RichSplitOptions) ([]InputRichMessage, error)

SplitRich splits at top-level blocks, preserving inline formatting and list numbering. Atomic blocks that cannot be split are emitted alone with a warning, matching Python behavior; callers can inspect their byte length before sending.

func (InputRichMessage) MarshalJSON

func (r InputRichMessage) MarshalJSON() ([]byte, error)

func (InputRichMessage) Validate

func (r InputRichMessage) Validate() error

type MermaidConfig

type MermaidConfig struct {
	Theme        string
	Width, Scale int
	ImageType    string
}

type MermaidRenderer

type MermaidRenderer func(context.Context, string, RenderConfig) ([]byte, error)

type MessageEntity

type MessageEntity struct {
	Type           string         `json:"type"`
	Offset         int            `json:"offset"`
	Length         int            `json:"length"`
	URL            string         `json:"url,omitempty"`
	Language       string         `json:"language,omitempty"`
	CustomEmojiID  string         `json:"custom_emoji_id,omitempty"`
	User           map[string]any `json:"user,omitempty"`
	UnixTime       *int64         `json:"unix_time,omitempty"`
	DateTimeFormat *string        `json:"date_time_format,omitempty"`
}

MessageEntity uses UTF-16 code units for Offset and Length, as required by Telegram.

func Convert

func Convert(markdown string, options *ConvertOptions) (string, []MessageEntity)
Example
package main

import (
	"encoding/json"
	"fmt"

	telegramify "github.com/discapes/telegramify-md-go"
)

func main() {
	text, entities := telegramify.Convert("Hello **world** 😀", nil)
	data, _ := json.Marshal(entities)
	fmt.Println(text)
	fmt.Println(string(data))
}
Output:
Hello world 😀
[{"type":"bold","offset":6,"length":5}]

type Photo

type Photo struct {
	FileName        string          `json:"file_name"`
	FileData        []byte          `json:"file_data"`
	ContentTrace    ContentTrace    `json:"content_trace"`
	CaptionText     string          `json:"caption_text,omitempty"`
	CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
}

func (Photo) Type

func (Photo) Type() ContentType

type RenderConfig

type RenderConfig struct {
	Symbols        Symbols
	Mermaid        MermaidConfig
	CiteExpandable bool
}

RenderConfig is a value: copying it produces an independent configuration.

func DefaultConfig

func DefaultConfig() RenderConfig

type RichMessage

type RichMessage struct {
	RichMessage  InputRichMessage `json:"rich_message"`
	ContentTrace ContentTrace     `json:"content_trace"`
}

func TelegramifyRich

func TelegramifyRich(markdown string, options *RichOptions) ([]RichMessage, error)

func (RichMessage) Type

func (RichMessage) Type() ContentType

type RichOptions

type RichOptions struct {
	Mode                       string // "html" (default) or "markdown" (passthrough).
	IsRTL, SkipEntityDetection *bool
	LatexEscape                bool
}

type RichSplitOptions

type RichSplitOptions struct{ ByteLimit, BlockLimit int }

type Segment

type Segment struct {
	Kind       string `json:"kind"`
	TextStart  int    `json:"text_start"`
	TextEnd    int    `json:"text_end"`
	UTF16Start int    `json:"utf16_start"`
	UTF16End   int    `json:"utf16_end"`
	Language   string `json:"language"`
	RawCode    string `json:"raw_code"`
}

Segment records an extractable code block. TextStart/TextEnd are UTF-8 byte offsets; UTF16Start/UTF16End are Telegram entity offsets.

type Symbols

type Symbols struct {
	Headings                                                    [6]string
	Image, Link, TaskCompleted, TaskUncompleted, HorizontalRule string
	UnorderedListItem, OrderedListSuffix                        string
}

Symbols controls the plain-text decorations added during entity conversion.

type TelegramifyOptions

type TelegramifyOptions struct {
	ConvertOptions
	MaxMessageLength int
	DisableMermaid   bool
	// MinFileLines defaults to 1 with nil options; 0 keeps code blocks inline.
	MinFileLines int
	// RenderMermaid overrides the HTTP renderer, for local rendering or tests.
	RenderMermaid MermaidRenderer
}

func DefaultTelegramifyOptions

func DefaultTelegramifyOptions() TelegramifyOptions

type Text

type Text struct {
	Text         string          `json:"text"`
	Entities     []MessageEntity `json:"entities"`
	ContentTrace ContentTrace    `json:"content_trace"`
}

func (Text) Type

func (Text) Type() ContentType

type TextChunk

type TextChunk struct {
	Text     string          `json:"text"`
	Entities []MessageEntity `json:"entities"`
}

func SplitEntities

func SplitEntities(text string, entities []MessageEntity, limit int) ([]TextChunk, error)

SplitEntities prefers newline boundaries and clips entities across chunks. A limit too small for a single rune returns an error instead of invalid UTF-8.

Directories

Path Synopsis
Package stream renders a growing Markdown buffer through caller-provided Telegram callbacks.
Package stream renders a growing Markdown buffer through caller-provided Telegram callbacks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL