mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 22:35:52 +01:00
hicli: add support for sending markdown and rainbows
This commit is contained in:
parent
8f6dec74c7
commit
190760cd65
6 changed files with 220 additions and 2 deletions
120
format/mdext/rainbow/goldmark.go
Normal file
120
format/mdext/rainbow/goldmark.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// Copyright (c) 2024 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package rainbow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"go.mau.fi/util/random"
|
||||
)
|
||||
|
||||
// Extension is a goldmark extension that adds rainbow text coloring to the HTML renderer.
|
||||
var Extension = &extRainbow{}
|
||||
|
||||
type extRainbow struct{}
|
||||
type rainbowRenderer struct {
|
||||
HardWraps bool
|
||||
ColorID string
|
||||
}
|
||||
|
||||
var defaultRB = &rainbowRenderer{HardWraps: true, ColorID: random.String(16)}
|
||||
|
||||
func (er *extRainbow) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(defaultRB, 0)))
|
||||
}
|
||||
|
||||
func (rb *rainbowRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindText, rb.renderText)
|
||||
reg.Register(ast.KindString, rb.renderString)
|
||||
}
|
||||
|
||||
type rainbowBufWriter struct {
|
||||
util.BufWriter
|
||||
ColorID string
|
||||
}
|
||||
|
||||
func (rbw rainbowBufWriter) WriteString(s string) (int, error) {
|
||||
i := 0
|
||||
graphemes := uniseg.NewGraphemes(s)
|
||||
for graphemes.Next() {
|
||||
runes := graphemes.Runes()
|
||||
if len(runes) == 1 && unicode.IsSpace(runes[0]) {
|
||||
i2, err := rbw.BufWriter.WriteRune(runes[0])
|
||||
i += i2
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
i2, err := fmt.Fprintf(rbw.BufWriter, "<font color=\"%s\">%s</font>", rbw.ColorID, graphemes.Str())
|
||||
i += i2
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (rbw rainbowBufWriter) Write(data []byte) (int, error) {
|
||||
return rbw.WriteString(string(data))
|
||||
}
|
||||
|
||||
func (rbw rainbowBufWriter) WriteByte(c byte) error {
|
||||
_, err := rbw.WriteRune(rune(c))
|
||||
return err
|
||||
}
|
||||
|
||||
func (rbw rainbowBufWriter) WriteRune(r rune) (int, error) {
|
||||
if unicode.IsSpace(r) {
|
||||
return rbw.BufWriter.WriteRune(r)
|
||||
} else {
|
||||
return fmt.Fprintf(rbw.BufWriter, "<font color=\"%s\">%c</font>", rbw.ColorID, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *rainbowRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.Text)
|
||||
segment := n.Segment
|
||||
if n.IsRaw() {
|
||||
html.DefaultWriter.RawWrite(rainbowBufWriter{w, rb.ColorID}, segment.Value(source))
|
||||
} else {
|
||||
html.DefaultWriter.Write(rainbowBufWriter{w, rb.ColorID}, segment.Value(source))
|
||||
if n.HardLineBreak() || (n.SoftLineBreak() && rb.HardWraps) {
|
||||
_, _ = w.WriteString("<br>\n")
|
||||
} else if n.SoftLineBreak() {
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (rb *rainbowRenderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.String)
|
||||
if n.IsCode() {
|
||||
_, _ = w.Write(n.Value)
|
||||
} else {
|
||||
if n.IsRaw() {
|
||||
html.DefaultWriter.RawWrite(rainbowBufWriter{w, rb.ColorID}, n.Value)
|
||||
} else {
|
||||
html.DefaultWriter.Write(rainbowBufWriter{w, rb.ColorID}, n.Value)
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
56
format/mdext/rainbow/gradient.go
Normal file
56
format/mdext/rainbow/gradient.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2024 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package rainbow
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
// GradientTable from https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go
|
||||
type GradientTable []struct {
|
||||
Col colorful.Color
|
||||
Pos float64
|
||||
}
|
||||
|
||||
func (gt GradientTable) GetInterpolatedColorFor(t float64) colorful.Color {
|
||||
for i := 0; i < len(gt)-1; i++ {
|
||||
c1 := gt[i]
|
||||
c2 := gt[i+1]
|
||||
if c1.Pos <= t && t <= c2.Pos {
|
||||
t := (t - c1.Pos) / (c2.Pos - c1.Pos)
|
||||
return c1.Col.BlendHcl(c2.Col, t).Clamped()
|
||||
}
|
||||
}
|
||||
return gt[len(gt)-1].Col
|
||||
}
|
||||
|
||||
var Gradient = GradientTable{
|
||||
{colorful.LinearRgb(1, 0, 0), 0 / 11.0},
|
||||
{colorful.LinearRgb(1, 0.5, 0), 1 / 11.0},
|
||||
{colorful.LinearRgb(1, 1, 0), 2 / 11.0},
|
||||
{colorful.LinearRgb(0.5, 1, 0), 3 / 11.0},
|
||||
{colorful.LinearRgb(0, 1, 0), 4 / 11.0},
|
||||
{colorful.LinearRgb(0, 1, 0.5), 5 / 11.0},
|
||||
{colorful.LinearRgb(0, 1, 1), 6 / 11.0},
|
||||
{colorful.LinearRgb(0, 0.5, 1), 7 / 11.0},
|
||||
{colorful.LinearRgb(0, 0, 1), 8 / 11.0},
|
||||
{colorful.LinearRgb(0.5, 0, 1), 9 / 11.0},
|
||||
{colorful.LinearRgb(1, 0, 1), 10 / 11.0},
|
||||
{colorful.LinearRgb(1, 0, 0.5), 11 / 11.0},
|
||||
}
|
||||
|
||||
func ApplyColor(htmlBody string) string {
|
||||
count := strings.Count(htmlBody, defaultRB.ColorID)
|
||||
i := -1
|
||||
return regexp.MustCompile(defaultRB.ColorID).ReplaceAllStringFunc(htmlBody, func(match string) string {
|
||||
i++
|
||||
return Gradient.GetInterpolatedColorFor(float64(i) / float64(count)).Hex()
|
||||
})
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -10,7 +10,9 @@ require (
|
|||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.23
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -19,6 +19,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
|||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
|
|
@ -31,6 +33,8 @@ github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7c
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
|
|
|
|||
|
|
@ -38,7 +38,11 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
|||
return true, nil
|
||||
})
|
||||
case "send_message":
|
||||
return unmarshalAndCall(req.Data, func(params *sendParams) (*database.Event, error) {
|
||||
return unmarshalAndCall(req.Data, func(params *sendMessageParams) (*database.Event, error) {
|
||||
return h.SendMessage(ctx, params.RoomID, params.Text, params.MediaPath)
|
||||
})
|
||||
case "send_event":
|
||||
return unmarshalAndCall(req.Data, func(params *sendEventParams) (*database.Event, error) {
|
||||
return h.Send(ctx, params.RoomID, params.EventType, params.Content)
|
||||
})
|
||||
case "get_event":
|
||||
|
|
@ -100,7 +104,13 @@ type cancelRequestParams struct {
|
|||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type sendParams struct {
|
||||
type sendMessageParams struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
Text string `json:"text"`
|
||||
MediaPath string `json:"media_path"`
|
||||
}
|
||||
|
||||
type sendEventParams struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
EventType event.Type `json:"type"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
|
|
|
|||
|
|
@ -11,17 +11,43 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yuin/goldmark"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/format/mdext/rainbow"
|
||||
"maunium.net/go/mautrix/hicli/database"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
rainbowWithHTML = goldmark.New(format.Extensions, format.HTMLOptions, goldmark.WithExtensions(rainbow.Extension))
|
||||
)
|
||||
|
||||
func (h *HiClient) SendMessage(ctx context.Context, roomID id.RoomID, text, mediaPath string) (*database.Event, error) {
|
||||
var content event.MessageEventContent
|
||||
if strings.HasPrefix(text, "/rainbow ") {
|
||||
text = strings.TrimPrefix(text, "/rainbow ")
|
||||
content = format.RenderMarkdownCustom(text, rainbowWithHTML)
|
||||
content.FormattedBody = rainbow.ApplyColor(content.FormattedBody)
|
||||
} else if strings.HasPrefix(text, "/plain ") {
|
||||
text = strings.TrimPrefix(text, "/plain ")
|
||||
content = format.RenderMarkdown(text, false, false)
|
||||
} else if strings.HasPrefix(text, "/html ") {
|
||||
text = strings.TrimPrefix(text, "/html ")
|
||||
content = format.RenderMarkdown(text, false, true)
|
||||
} else {
|
||||
content = format.RenderMarkdown(text, true, false)
|
||||
}
|
||||
return h.Send(ctx, roomID, event.EventMessage, &content)
|
||||
}
|
||||
|
||||
func (h *HiClient) Send(ctx context.Context, roomID id.RoomID, evtType event.Type, content any) (*database.Event, error) {
|
||||
roomMeta, err := h.DB.Room.Get(ctx, roomID)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue