package tgfmt import ( "fmt" "strconv" "strings" "time" "git.scuroneko.dev/scuroneko/laniakea/tgapi" ) // Rich is an inline fragment of rich-message HTML (Bot API 10.1). // Raw text enters through NewRich, which escapes it; fragments compose as-is. type Rich string // RichBlock is a block-level fragment of rich-message HTML. Block // constructors accept only Rich arguments, so invalid nesting (a block // inside inline content) does not compile. type RichBlock string // RichItem is any rich-message fragment: Rich or RichBlock. Both are valid // at the top level of a message — Telegram merges adjacent inline content // into paragraphs. type RichItem interface{ richItem() string } func (r Rich) richItem() string { return string(r) } func (r RichBlock) richItem() string { return string(r) } // RichHTML concatenates fragments into the final rich-message HTML string. func RichHTML(items ...RichItem) string { var b strings.Builder for _, item := range items { b.WriteString(item.richItem()) } return b.String() } // RichMessage builds a ready-to-send InputRichMessage from fragments. // SkipEntityDetection is enabled so the server does not add auto-detected // entities; set IsRTL on the result if needed. func RichMessage(items ...RichItem) tgapi.InputRichMessage { return tgapi.InputRichMessage{ HTML: RichHTML(items...), SkipEntityDetection: true, } } func richJoin(items ...Rich) Rich { var out Rich for _, item := range items { out += item } return out } func richJoinSep(sep Rich, items ...Rich) Rich { var out Rich for i, item := range items { if i > 0 { out += sep } out += item } return out } func richBlocksJoin(items ...RichBlock) RichBlock { var out RichBlock for _, item := range items { out += item } return out } // openTag omits the space when there are no attributes. func openTag(name string, attrs []string) string { if len(attrs) == 0 { return "<" + name + ">" } return "<" + name + " " + strings.Join(attrs, " ") + ">" } func cite(credit Rich) Rich { if credit == "" { return "" } return "" + credit + "" } // NewRich escapes raw text and returns it as an inline fragment. func NewRich(text string) Rich { return Rich(escapeHTML(text)) } // Bold wraps the fragment in . func (r Rich) Bold() Rich { return "" + r + "" } // Italic wraps the fragment in . func (r Rich) Italic() Rich { return "" + r + "" } // Underline wraps the fragment in . func (r Rich) Underline() Rich { return "" + r + "" } // Strike wraps the fragment in . func (r Rich) Strike() Rich { return "" + r + "" } // Code wraps the fragment in . func (r Rich) Code() Rich { return "" + r + "" } // Mark wraps the fragment in . func (r Rich) Mark() Rich { return "" + r + "" } // Sub wraps the fragment in . func (r Rich) Sub() Rich { return "" + r + "" } // Sup wraps the fragment in . func (r Rich) Sup() Rich { return "" + r + "" } // Spoiler wraps the fragment in . func (r Rich) Spoiler() Rich { return "" + r + "" } // Link wraps the fragment in a hyperlink to url. func (r Rich) Link(url string) Rich { return `` + r + "" } // Email wraps the fragment in a mailto: link. func (r Rich) Email(email string) Rich { return r.Link("mailto:" + email) } // Phone wraps the fragment in a tel: link. func (r Rich) Phone(phone string) Rich { return r.Link("tel:" + phone) } // Mention wraps the fragment in an inline user mention link. func (r Rich) Mention(userID int64) Rich { return r.Link("tg://user?id=" + strconv.FormatInt(userID, 10)) } // Anchor marks the fragment as a named anchor (). func (r Rich) Anchor(name string) Rich { return `` + r + "" } // AnchorLink wraps the fragment in an in-document link to a named anchor // or reference; the server resolves which one by the target name. func (r Rich) AnchorLink(anchor string) Rich { return r.Link("#" + anchor) } // Ref wraps the fragment in a to the named reference. func (r Rich) Ref(ref string) Rich { return `` + r + "" } // Emoji builds a custom emoji fragment with alt text as fallback. func Emoji(emojiID, alt string) Rich { return `` + escapeRich(alt) + `` } // Time marks the fragment as a bound to t. func (r Rich) Time(t time.Time) Rich { return `` + r + "" } // TimeFormat marks the fragment as a with an explicit display format. func (r Rich) TimeFormat(t time.Time, format string) Rich { return `` + r + "" } // Math wraps the fragment in an inline expression. func (r Rich) Math() Rich { return `` + r + `` } // Br returns a line break fragment. func Br() Rich { return "
" } // H1 builds a level-1 heading block. func H1(items ...Rich) RichBlock { return RichBlock("

" + richJoin(items...) + "

") } // H2 builds a level-2 heading block. func H2(items ...Rich) RichBlock { return RichBlock("

" + richJoin(items...) + "

") } // H3 builds a level-3 heading block. func H3(items ...Rich) RichBlock { return RichBlock("

" + richJoin(items...) + "

") } // H4 builds a level-4 heading block. func H4(items ...Rich) RichBlock { return RichBlock("

" + richJoin(items...) + "

") } // H5 builds a level-5 heading block. func H5(items ...Rich) RichBlock { return RichBlock("
" + richJoin(items...) + "
") } // H6 builds a level-6 heading block. func H6(items ...Rich) RichBlock { return RichBlock("
" + richJoin(items...) + "
") } // P builds a paragraph block. func P(items ...Rich) RichBlock { return RichBlock("

" + richJoin(items...) + "

") } // Pre builds a preformatted code block. func Pre(items ...Rich) RichBlock { return RichBlock("
" + richJoin(items...) + "
") } // PreCode builds a preformatted code block tagged with a language. func PreCode(lang string, items ...Rich) RichBlock { return RichBlock(`
` + richJoin(items...) + `
`) } // Footer builds a footer block. func Footer(items ...Rich) RichBlock { return RichBlock("
" + richJoin(items...) + "
") } // Hr builds a divider block. func Hr() RichBlock { return "
" } // AnchorBlock builds a standalone named anchor between blocks: . func AnchorBlock(name string) RichBlock { return RichBlock(``) } // LiItem is a list item under construction for Ul or Ol. type LiItem struct { text Rich value int typ string checkbox bool checked bool } // Li builds a list item from inline fragments. func Li(items ...Rich) LiItem { return LiItem{text: richJoin(items...)} } // LiCheckbox builds a checkbox list item. func LiCheckbox(checked bool, items ...Rich) LiItem { return LiItem{text: richJoin(items...), checkbox: true, checked: checked} } // SetValue sets the explicit ordinal of the item (like
  • ). func (l LiItem) SetValue(val int) LiItem { l.value = val return l } // SetType sets the numbering type of the item: "1", "a", "A", "i", "I". func (l LiItem) SetType(t string) LiItem { l.typ = t return l } func (l LiItem) build() Rich { if l.checkbox { input := Rich(``) if l.checked { input = `` } return "
  • " + input + l.text + "
  • " } attrs := make([]string, 0, 2) if l.value != 0 { attrs = append(attrs, `value="`+strconv.Itoa(l.value)+`"`) } if l.typ != "" { attrs = append(attrs, `type="`+escapeHTML(l.typ)+`"`) } return Rich(openTag("li", attrs)) + l.text + "" } func joinLiItems(items []LiItem) Rich { var out Rich for _, item := range items { out += item.build() } return out } // Ul builds an unordered list block. func Ul(items ...LiItem) RichBlock { return RichBlock(`
      ` + joinLiItems(items) + `
    `) } // OlOpts holds the
      numbering attributes. type OlOpts struct { Start int Type string Reversed bool } // Ol builds an ordered list block. Item labels are rendered by the server. func Ol(opts OlOpts, items ...LiItem) RichBlock { attrs := make([]string, 0, 3) if opts.Start > 0 { attrs = append(attrs, `start="`+strconv.Itoa(opts.Start)+`"`) } if opts.Type != "" { attrs = append(attrs, `type="`+escapeHTML(opts.Type)+`"`) } if opts.Reversed { attrs = append(attrs, "reversed") } return RichBlock(openTag("ol", attrs) + string(joinLiItems(items)) + "
    ") } // Blockquote builds a block quotation: lines are joined with
    (as in the // official HTML example) and credit renders as a trailing . func Blockquote(credit Rich, lines ...Rich) RichBlock { return RichBlock(`
    ` + richJoinSep(Br(), lines...) + cite(credit) + `
    `) } // Aside builds a pull quote (