2020-07-14 07:24:55 +00:00
|
|
|
package markup
|
2020-06-19 22:40:06 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"html"
|
2020-06-28 01:35:26 +00:00
|
|
|
"net/url"
|
2020-07-14 07:24:55 +00:00
|
|
|
"sort"
|
2020-06-19 22:40:06 +00:00
|
|
|
"strings"
|
|
|
|
|
2020-06-28 23:01:08 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/hl"
|
2020-06-19 22:40:06 +00:00
|
|
|
"github.com/diamondburned/cchat/text"
|
2020-06-28 01:35:26 +00:00
|
|
|
"github.com/diamondburned/imgutil"
|
2020-06-19 22:40:06 +00:00
|
|
|
)
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
// Hyphenate controls whether or not texts should have hyphens on wrap.
|
|
|
|
var Hyphenate = false
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
func hyphenate(text string) string {
|
|
|
|
return fmt.Sprintf(`<span insert_hyphens="%t">%s</span>`, Hyphenate, text)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RenderOutput is the output of a render.
|
|
|
|
type RenderOutput struct {
|
|
|
|
Markup string
|
2020-08-13 22:50:51 +00:00
|
|
|
Input string // useless to keep parts, as Go will keep all alive anyway
|
2020-07-14 07:24:55 +00:00
|
|
|
Mentions []text.Mentioner
|
|
|
|
}
|
|
|
|
|
|
|
|
// f_Mention is used to print and parse mention URIs.
|
2020-08-13 22:50:51 +00:00
|
|
|
const f_Mention = "cchat://mention/%d" // %d == Mentions[i]
|
2020-07-14 07:24:55 +00:00
|
|
|
|
|
|
|
// 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
|
2020-06-19 22:40:06 +00:00
|
|
|
}
|
2020-07-14 07:24:55 +00:00
|
|
|
|
|
|
|
if i >= len(r.Mentions) {
|
|
|
|
return nil
|
2020-06-19 22:40:06 +00:00
|
|
|
}
|
2020-07-14 07:24:55 +00:00
|
|
|
|
|
|
|
return r.Mentions[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
func Render(content text.Rich) string {
|
|
|
|
return RenderCmplx(content).Markup
|
2020-06-19 22:40:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
// RenderCmplx renders content into a complete output.
|
|
|
|
func RenderCmplx(content text.Rich) RenderOutput {
|
2020-07-18 07:16:47 +00:00
|
|
|
return RenderCmplxWithConfig(content, RenderConfig{})
|
|
|
|
}
|
|
|
|
|
|
|
|
type RenderConfig struct {
|
|
|
|
// NoMentionLinks prevents the renderer from wrapping mentions with a
|
|
|
|
// hyperlink. This prevents invalid colors.
|
|
|
|
NoMentionLinks bool
|
|
|
|
}
|
|
|
|
|
2020-08-17 00:13:47 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
|
2020-07-18 07:16:47 +00:00
|
|
|
func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
2020-06-19 22:40:06 +00:00
|
|
|
// Fast path.
|
|
|
|
if len(content.Segments) == 0 {
|
2020-07-14 07:24:55 +00:00
|
|
|
return RenderOutput{
|
|
|
|
Markup: hyphenate(html.EscapeString(content.Content)),
|
2020-08-13 22:50:51 +00:00
|
|
|
Input: content.Content,
|
2020-07-14 07:24:55 +00:00
|
|
|
}
|
2020-06-19 22:40:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
buf := bytes.Buffer{}
|
|
|
|
buf.Grow(len(content.Content))
|
|
|
|
|
2020-07-17 00:21:14 +00:00
|
|
|
// 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
|
|
|
|
})
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
// 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
|
|
|
|
})
|
2020-06-19 22:40:06 +00:00
|
|
|
|
|
|
|
// map to append strings to indices
|
2020-06-28 23:01:08 +00:00
|
|
|
var appended = attrmap.NewAppendedMap()
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
// map to store mentions
|
|
|
|
var mentions []text.Mentioner
|
|
|
|
|
2020-06-19 22:40:06 +00:00
|
|
|
// Parse all segments.
|
|
|
|
for _, segment := range content.Segments {
|
|
|
|
start, end := segment.Bounds()
|
|
|
|
|
2020-07-08 09:07:00 +00:00
|
|
|
if segment, ok := segment.(text.Linker); ok {
|
2020-07-18 07:16:47 +00:00
|
|
|
appended.Anchor(start, end, segment.Link())
|
2020-07-08 09:07:00 +00:00
|
|
|
}
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-08 09:07:00 +00:00
|
|
|
if segment, ok := segment.(text.Imager); ok {
|
2020-06-19 22:40:06 +00:00
|
|
|
// Ends don't matter with images.
|
2020-07-09 04:14:56 +00:00
|
|
|
appended.Open(start, composeImageMarkup(segment))
|
2020-07-08 09:07:00 +00:00
|
|
|
}
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
if segment, ok := segment.(text.Avatarer); ok {
|
|
|
|
// Ends don't matter with images.
|
|
|
|
appended.Open(start, composeAvatarMarkup(segment))
|
|
|
|
}
|
|
|
|
|
2020-07-17 00:21:14 +00:00
|
|
|
if segment, ok := segment.(text.Colorer); ok {
|
|
|
|
appended.Span(start, end, fmt.Sprintf("color=\"#%06X\"", segment.Color()))
|
|
|
|
}
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
// 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.
|
2020-08-13 22:50:51 +00:00
|
|
|
if !cfg.NoMentionLinks {
|
2020-07-18 07:16:47 +00:00
|
|
|
appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the mention segment into the list regardless of hyperlinks.
|
2020-07-14 07:24:55 +00:00
|
|
|
mentions = append(mentions, segment)
|
|
|
|
|
2020-07-17 00:21:14 +00:00
|
|
|
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()),
|
|
|
|
)
|
2020-07-14 07:24:55 +00:00
|
|
|
appended.Pad(start, end)
|
2020-07-09 04:14:56 +00:00
|
|
|
}
|
2020-07-08 09:07:00 +00:00
|
|
|
}
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-08 09:07:00 +00:00
|
|
|
if segment, ok := segment.(text.Attributor); ok {
|
2020-06-28 23:01:08 +00:00
|
|
|
appended.Span(start, end, markupAttr(segment.Attribute()))
|
2020-07-08 09:07:00 +00:00
|
|
|
}
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-08 09:07:00 +00:00
|
|
|
if segment, ok := segment.(text.Codeblocker); ok {
|
2020-06-28 23:01:08 +00:00
|
|
|
// Syntax highlight the codeblock.
|
|
|
|
hl.Segments(&appended, content.Content, segment)
|
2020-07-08 09:07:00 +00:00
|
|
|
}
|
2020-06-19 22:40:06 +00:00
|
|
|
|
2020-07-08 09:07:00 +00:00
|
|
|
// TODO: make this not shit. Maybe make it somehow not rely on green
|
|
|
|
// arrows. Or maybe.
|
|
|
|
if _, ok := segment.(text.Quoteblocker); ok {
|
2020-06-28 23:01:08 +00:00
|
|
|
appended.Span(start, end, `color="#789922"`)
|
2020-06-19 22:40:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var lastIndex = 0
|
|
|
|
|
2020-06-28 23:01:08 +00:00
|
|
|
for _, index := range appended.Finalize(len(content.Content)) {
|
2020-06-19 22:40:06 +00:00
|
|
|
// Write the content.
|
|
|
|
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
|
|
|
|
// Write the tags.
|
2020-06-28 23:01:08 +00:00
|
|
|
buf.WriteString(appended.Get(index))
|
2020-06-19 22:40:06 +00:00
|
|
|
// Set the last index.
|
|
|
|
lastIndex = index
|
|
|
|
}
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
return RenderOutput{
|
|
|
|
Markup: hyphenate(buf.String()),
|
2020-08-13 22:50:51 +00:00
|
|
|
Input: content.Content,
|
2020-07-14 07:24:55 +00:00
|
|
|
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
|
2020-06-19 22:40:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-18 07:16:47 +00:00
|
|
|
const (
|
|
|
|
// string constant for formatting width and height in URL fragments
|
|
|
|
f_FragmentSize = "w=%d;h=%d"
|
|
|
|
f_AnchorNoUnderline = `<a href="%s"><span underline="none">%s</span></a>`
|
|
|
|
)
|
2020-06-28 01:35:26 +00:00
|
|
|
|
|
|
|
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(
|
2020-07-18 07:16:47 +00:00
|
|
|
f_AnchorNoUnderline,
|
2020-06-28 01:35:26 +00:00
|
|
|
html.EscapeString(u.String()), html.EscapeString(imager.ImageText()),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
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(
|
2020-07-18 07:16:47 +00:00
|
|
|
f_AnchorNoUnderline,
|
2020-07-14 07:24:55 +00:00
|
|
|
html.EscapeString(u.String()), html.EscapeString(avatarer.AvatarText()),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-06-28 01:35:26 +00:00
|
|
|
// 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.
|
2020-07-14 07:24:55 +00:00
|
|
|
func FragmentImageSize(URL string, maxw, maxh int) (w, h int, round bool) {
|
2020-06-28 01:35:26 +00:00
|
|
|
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)
|
2020-07-14 07:24:55 +00:00
|
|
|
round = strings.HasSuffix(u.Fragment, ";round")
|
2020-06-28 01:35:26 +00:00
|
|
|
|
|
|
|
if w > 0 && h > 0 {
|
2020-07-14 07:24:55 +00:00
|
|
|
w, h = imgutil.MaxSize(w, h, maxw, maxh)
|
|
|
|
return
|
2020-06-28 01:35:26 +00:00
|
|
|
}
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
return maxw, maxh, round
|
2020-06-28 01:35:26 +00:00
|
|
|
}
|
|
|
|
|
2020-06-19 22:40:06 +00:00
|
|
|
func span(key, value string) string {
|
|
|
|
return "<span key=\"" + value + "\">"
|
|
|
|
}
|
2020-07-14 07:24:55 +00:00
|
|
|
|
|
|
|
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, " ")
|
|
|
|
}
|