package markup import ( "bytes" "fmt" "html" "net/url" "sort" "strings" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/hl" "github.com/diamondburned/cchat/text" "github.com/diamondburned/imgutil" ) // Hyphenate controls whether or not texts should have hyphens on wrap. var Hyphenate = false func hyphenate(text string) string { return fmt.Sprintf(`%s`, Hyphenate, text) } // RenderOutput is the output of a render. type RenderOutput struct { Markup string Input string // useless to keep parts, as Go will keep all alive anyway Mentions []text.Mentioner } // f_Mention is used to print and parse mention URIs. const f_Mention = "cchat://mention/%d" // %d == Mentions[i] // IsMention returns the mention if the URI is correct, or nil if none. func (r RenderOutput) IsMention(uri string) text.Mentioner { var i int if _, err := fmt.Sscanf(uri, f_Mention, &i); err != nil { return nil } if i >= len(r.Mentions) { return nil } return r.Mentions[i] } func Render(content text.Rich) string { return RenderCmplx(content).Markup } // RenderCmplx renders content into a complete output. func RenderCmplx(content text.Rich) RenderOutput { return RenderCmplxWithConfig(content, RenderConfig{}) } type RenderConfig struct { // NoMentionLinks prevents the renderer from wrapping mentions with a // hyperlink. This prevents invalid colors. NoMentionLinks bool } // NoMentionLinks is the config to render author names. It disables author // mention links, as there's no way to make normal names not appear blue. var NoMentionLinks = RenderConfig{ NoMentionLinks: true, } func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput { // Fast path. if len(content.Segments) == 0 { return RenderOutput{ Markup: hyphenate(html.EscapeString(content.Content)), Input: content.Content, } } buf := bytes.Buffer{} buf.Grow(len(content.Content)) // Sort so that all ending points are sorted decrementally. We probably // don't need SliceStable here, as we're sorting again. sort.Slice(content.Segments, func(i, j int) bool { _, i = content.Segments[i].Bounds() _, j = content.Segments[j].Bounds() return i > j }) // Sort so that all starting points are sorted incrementally. sort.SliceStable(content.Segments, func(i, j int) bool { i, _ = content.Segments[i].Bounds() j, _ = content.Segments[j].Bounds() return i < j }) // map to append strings to indices var appended = attrmap.NewAppendedMap() // map to store mentions var mentions []text.Mentioner // Parse all segments. for _, segment := range content.Segments { start, end := segment.Bounds() if segment, ok := segment.(text.Linker); ok { appended.Anchor(start, end, segment.Link()) } if segment, ok := segment.(text.Imager); ok { // Ends don't matter with images. appended.Open(start, composeImageMarkup(segment)) } if segment, ok := segment.(text.Avatarer); ok { // Ends don't matter with images. appended.Open(start, composeAvatarMarkup(segment)) } if segment, ok := segment.(text.Colorer); ok { appended.Span(start, end, fmt.Sprintf("color=\"#%06X\"", segment.Color())) } // Mentioner needs to be before colorer, as we'd want the below color // segment to also highlight the full mention as well as make the // padding part of the hyperlink. if segment, ok := segment.(text.Mentioner); ok { // Render the mention into "cchat://mention:0" or such. Other // components will take care of showing the information. if !cfg.NoMentionLinks { appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions))) } // Add the mention segment into the list regardless of hyperlinks. mentions = append(mentions, segment) if segment, ok := segment.(text.Colorer); ok { // Add a dimmed background highlight and pad the button-like // link. appended.Span( start, end, "bgalpha=\"10%\"", fmt.Sprintf("bgcolor=\"#%06X\"", segment.Color()), ) appended.Pad(start, end) } } if segment, ok := segment.(text.Attributor); ok { appended.Span(start, end, markupAttr(segment.Attribute())) } if segment, ok := segment.(text.Codeblocker); ok { // Syntax highlight the codeblock. hl.Segments(&appended, content.Content, segment) } // TODO: make this not shit. Maybe make it somehow not rely on green // arrows. Or maybe. if _, ok := segment.(text.Quoteblocker); ok { appended.Span(start, end, `color="#789922"`) } } var lastIndex = 0 for _, index := range appended.Finalize(len(content.Content)) { // Write the content. buf.WriteString(html.EscapeString(content.Content[lastIndex:index])) // Write the tags. buf.WriteString(appended.Get(index)) // Set the last index. lastIndex = index } return RenderOutput{ Markup: hyphenate(buf.String()), Input: content.Content, Mentions: mentions, } } func color(c uint32, bg bool) []string { var hex = fmt.Sprintf("#%06X", c) var attrs = []string{ fmt.Sprintf(`color="%s"`, hex), } if bg { attrs = append( attrs, `bgalpha="10%"`, fmt.Sprintf(`bgcolor="%s"`, hex), ) } return attrs } const ( // string constant for formatting width and height in URL fragments f_FragmentSize = "w=%d;h=%d" f_AnchorNoUnderline = `%s` ) func composeImageMarkup(imager text.Imager) string { u, err := url.Parse(imager.Image()) if err != nil { // If the URL is invalid, then just write a normal text. return html.EscapeString(imager.ImageText()) } // Override the URL fragment with our own. if w, h := imager.ImageSize(); w > 0 && h > 0 { u.Fragment = fmt.Sprintf(f_FragmentSize, w, h) } return fmt.Sprintf( f_AnchorNoUnderline, html.EscapeString(u.String()), html.EscapeString(imager.ImageText()), ) } func composeAvatarMarkup(avatarer text.Avatarer) string { u, err := url.Parse(avatarer.Avatar()) if err != nil { // If the URL is invalid, then just write a normal text. return html.EscapeString(avatarer.AvatarText()) } // Override the URL fragment with our own. if size := avatarer.AvatarSize(); size > 0 { u.Fragment = fmt.Sprintf(f_FragmentSize, size, size) + ";round" } return fmt.Sprintf( f_AnchorNoUnderline, html.EscapeString(u.String()), html.EscapeString(avatarer.AvatarText()), ) } // FragmentImageSize tries to parse the width and height encoded in the URL // fragment, which is inserted by the markup renderer. A pair of zero values are // returned if there is none. The returned width and height will be the minimum // of the given maxes and the encoded sizes. func FragmentImageSize(URL string, maxw, maxh int) (w, h int, round bool) { u, err := url.Parse(URL) if err != nil { return } // Ignore the error, as we can check for the integers. fmt.Sscanf(u.Fragment, f_FragmentSize, &w, &h) round = strings.HasSuffix(u.Fragment, ";round") if w > 0 && h > 0 { w, h = imgutil.MaxSize(w, h, maxw, maxh) return } return maxw, maxh, round } func span(key, value string) string { return "" } func markupAttr(attr text.Attribute) string { // meme fast path if attr == 0 { return "" } var attrs = make([]string, 0, 1) if attr.Has(text.AttrBold) { attrs = append(attrs, `weight="bold"`) } if attr.Has(text.AttrItalics) { attrs = append(attrs, `style="italic"`) } if attr.Has(text.AttrUnderline) { attrs = append(attrs, `underline="single"`) } if attr.Has(text.AttrStrikethrough) { attrs = append(attrs, `strikethrough="true"`) } if attr.Has(text.AttrSpoiler) { attrs = append(attrs, `alpha="35%"`) // no fancy click here } if attr.Has(text.AttrMonospace) { attrs = append(attrs, `font_family="monospace"`) } if attr.Has(text.AttrDimmed) { attrs = append(attrs, `alpha="35%"`) } return strings.Join(attrs, " ") }