forked from Mirrors/gomuks
hicli/html,web/timeline: add support for LaTeX rendering
This commit is contained in:
parent
44dee015d4
commit
214d4fde53
10 changed files with 166 additions and 19 deletions
4
go.mod
4
go.mod
|
@ -16,7 +16,7 @@ require (
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/tidwall/sjson v1.2.5
|
github.com/tidwall/sjson v1.2.5
|
||||||
github.com/yuin/goldmark v1.7.7
|
github.com/yuin/goldmark v1.7.7
|
||||||
go.mau.fi/util v0.8.2-0.20241027163518-38d54fc87ee3
|
go.mau.fi/util v0.8.2-0.20241030110711-b3e597e16b74
|
||||||
go.mau.fi/zeroconfig v0.1.3
|
go.mau.fi/zeroconfig v0.1.3
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
golang.org/x/image v0.21.0
|
golang.org/x/image v0.21.0
|
||||||
|
@ -24,7 +24,7 @@ require (
|
||||||
golang.org/x/text v0.19.0
|
golang.org/x/text v0.19.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mauflag v1.0.0
|
maunium.net/go/mauflag v1.0.0
|
||||||
maunium.net/go/mautrix v0.21.2-0.20241026115159-a59d4d78677f
|
maunium.net/go/mautrix v0.21.2-0.20241101162620-f606129e732f
|
||||||
mvdan.cc/xurls/v2 v2.5.0
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -61,8 +61,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
|
github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
|
||||||
github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
go.mau.fi/util v0.8.2-0.20241027163518-38d54fc87ee3 h1:9dDTNcVc3y9oU9bYvjpc3xsCupwGzfyYhrppaLy6l9k=
|
go.mau.fi/util v0.8.2-0.20241030110711-b3e597e16b74 h1:hzVVXFEIQWefBlokVlQ2nr7EzRnMdMLF+K+kqWsm6OE=
|
||||||
go.mau.fi/util v0.8.2-0.20241027163518-38d54fc87ee3/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
|
go.mau.fi/util v0.8.2-0.20241030110711-b3e597e16b74/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
|
||||||
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
||||||
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
|
@ -89,7 +89,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.21.2-0.20241026115159-a59d4d78677f h1:fZL9ASp9m4KaC0QUEDkv5ptPwVvRjigy9uPI6NYZAD0=
|
maunium.net/go/mautrix v0.21.2-0.20241101162620-f606129e732f h1:4iO+tXpXS8BNZuJP17BpaPZAURknrufVgkVrFkTHv7Y=
|
||||||
maunium.net/go/mautrix v0.21.2-0.20241026115159-a59d4d78677f/go.mod h1:sjCZR1R/3NET/WjkcXPL6WpAHlWKku9HjRsdOkbM8Qw=
|
maunium.net/go/mautrix v0.21.2-0.20241101162620-f606129e732f/go.mod h1:sjCZR1R/3NET/WjkcXPL6WpAHlWKku9HjRsdOkbM8Qw=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
|
|
|
@ -292,6 +292,7 @@ type LocalContent struct {
|
||||||
HTMLVersion int `json:"html_version,omitempty"`
|
HTMLVersion int `json:"html_version,omitempty"`
|
||||||
WasPlaintext bool `json:"was_plaintext,omitempty"`
|
WasPlaintext bool `json:"was_plaintext,omitempty"`
|
||||||
BigEmoji bool `json:"big_emoji,omitempty"`
|
BigEmoji bool `json:"big_emoji,omitempty"`
|
||||||
|
HasMath bool `json:"has_math,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
|
|
|
@ -79,6 +79,15 @@ func calculateMediaSize(widthInt, heightInt int) (width, height float64, ok bool
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAttribute(attrs []html.Attribute, key string) (string, bool) {
|
||||||
|
for _, attr := range attrs {
|
||||||
|
if attr.Key == key {
|
||||||
|
return attr.Val, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
func parseImgAttributes(attrs []html.Attribute) (src, alt, title string, isCustomEmoji bool, width, height int) {
|
func parseImgAttributes(attrs []html.Attribute) (src, alt, title string, isCustomEmoji bool, width, height int) {
|
||||||
for _, attr := range attrs {
|
for _, attr := range attrs {
|
||||||
switch attr.Key {
|
switch attr.Key {
|
||||||
|
@ -99,7 +108,7 @@ func parseImgAttributes(attrs []html.Attribute) (src, alt, title string, isCusto
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSpanAttributes(attrs []html.Attribute) (bgColor, textColor, spoiler, maths string, isSpoiler bool) {
|
func parseSpanAttributes(attrs []html.Attribute) (bgColor, textColor, spoiler string, isSpoiler bool) {
|
||||||
for _, attr := range attrs {
|
for _, attr := range attrs {
|
||||||
switch attr.Key {
|
switch attr.Key {
|
||||||
case "data-mx-bg-color":
|
case "data-mx-bg-color":
|
||||||
|
@ -113,8 +122,6 @@ func parseSpanAttributes(attrs []html.Attribute) (bgColor, textColor, spoiler, m
|
||||||
case "data-mx-spoiler":
|
case "data-mx-spoiler":
|
||||||
spoiler = attr.Val
|
spoiler = attr.Val
|
||||||
isSpoiler = true
|
isSpoiler = true
|
||||||
case "data-mx-maths":
|
|
||||||
maths = attr.Val
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -430,7 +437,7 @@ func writeImg(w *strings.Builder, attr []html.Attribute) id.ContentURI {
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSpan(w *strings.Builder, attr []html.Attribute) {
|
func writeSpan(w *strings.Builder, attr []html.Attribute) {
|
||||||
bgColor, textColor, spoiler, _, isSpoiler := parseSpanAttributes(attr)
|
bgColor, textColor, spoiler, isSpoiler := parseSpanAttributes(attr)
|
||||||
if isSpoiler && spoiler != "" {
|
if isSpoiler && spoiler != "" {
|
||||||
w.WriteString(`<span class="spoiler-reason">`)
|
w.WriteString(`<span class="spoiler-reason">`)
|
||||||
w.WriteString(spoiler)
|
w.WriteString(spoiler)
|
||||||
|
@ -521,10 +528,6 @@ Loop:
|
||||||
if !tagIsAllowed(token.DataAtom) {
|
if !tagIsAllowed(token.DataAtom) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tagIsSelfClosing := isSelfClosing(token.DataAtom)
|
|
||||||
if token.Type == html.SelfClosingTagToken && !tagIsSelfClosing {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch token.DataAtom {
|
switch token.DataAtom {
|
||||||
case atom.Pre:
|
case atom.Pre:
|
||||||
codeBlock = &strings.Builder{}
|
codeBlock = &strings.Builder{}
|
||||||
|
@ -539,8 +542,22 @@ Loop:
|
||||||
if !mxc.IsEmpty() {
|
if !mxc.IsEmpty() {
|
||||||
inlineImages = append(inlineImages, mxc)
|
inlineImages = append(inlineImages, mxc)
|
||||||
}
|
}
|
||||||
|
case atom.Div:
|
||||||
|
math, ok := getAttribute(token.Attr, "data-mx-maths")
|
||||||
|
if ok {
|
||||||
|
built.WriteString(`<hicli-math displaymode="block"`)
|
||||||
|
writeAttribute(&built, "latex", math)
|
||||||
|
token.DataAtom = atom.Math
|
||||||
|
}
|
||||||
case atom.Span, atom.Font:
|
case atom.Span, atom.Font:
|
||||||
|
math, ok := getAttribute(token.Attr, "data-mx-maths")
|
||||||
|
if ok && token.DataAtom == atom.Span {
|
||||||
|
built.WriteString(`<hicli-math displaymode="inline"`)
|
||||||
|
writeAttribute(&built, "latex", math)
|
||||||
|
token.DataAtom = atom.Math
|
||||||
|
} else {
|
||||||
writeSpan(&built, token.Attr)
|
writeSpan(&built, token.Attr)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
built.WriteByte('<')
|
built.WriteByte('<')
|
||||||
built.WriteString(token.Data)
|
built.WriteString(token.Data)
|
||||||
|
@ -550,18 +567,24 @@ Loop:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if token.Type == html.SelfClosingTagToken {
|
||||||
|
built.WriteByte('/')
|
||||||
|
}
|
||||||
built.WriteByte('>')
|
built.WriteByte('>')
|
||||||
if !tagIsSelfClosing {
|
if !isSelfClosing(token.DataAtom) && token.Type != html.SelfClosingTagToken {
|
||||||
ts.push(token.DataAtom)
|
ts.push(token.DataAtom)
|
||||||
}
|
}
|
||||||
case html.EndTagToken:
|
case html.EndTagToken:
|
||||||
tagName, _ := tz.TagName()
|
tagName, _ := tz.TagName()
|
||||||
tag := atom.Lookup(tagName)
|
tag := atom.Lookup(tagName)
|
||||||
|
if !tagIsAllowed(tag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if tag == atom.Pre && codeBlock != nil {
|
if tag == atom.Pre && codeBlock != nil {
|
||||||
writeCodeBlock(&built, codeBlockLanguage, codeBlock)
|
writeCodeBlock(&built, codeBlockLanguage, codeBlock)
|
||||||
codeBlockLanguage = ""
|
codeBlockLanguage = ""
|
||||||
codeBlock = nil
|
codeBlock = nil
|
||||||
} else if tagIsAllowed(tag) && ts.pop(tag) {
|
} else if ts.pop(tag) {
|
||||||
// TODO instead of only popping when the last tag in the stack matches, this should go through the stack
|
// TODO instead of only popping when the last tag in the stack matches, this should go through the stack
|
||||||
// and close all tags until it finds the matching tag
|
// and close all tags until it finds the matching tag
|
||||||
if tag == atom.Font {
|
if tag == atom.Font {
|
||||||
|
@ -571,6 +594,8 @@ Loop:
|
||||||
built.Write(tagName)
|
built.Write(tagName)
|
||||||
built.WriteByte('>')
|
built.WriteByte('>')
|
||||||
}
|
}
|
||||||
|
} else if (tag == atom.Span || tag == atom.Div) && ts.pop(atom.Math) {
|
||||||
|
built.WriteString("</hicli-math>")
|
||||||
}
|
}
|
||||||
case html.TextToken:
|
case html.TextToken:
|
||||||
if codeBlock != nil {
|
if codeBlock != nil {
|
||||||
|
|
|
@ -367,7 +367,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
}
|
}
|
||||||
if content != nil {
|
if content != nil {
|
||||||
var sanitizedHTML string
|
var sanitizedHTML string
|
||||||
var wasPlaintext, bigEmoji bool
|
var wasPlaintext, hasMath, bigEmoji bool
|
||||||
var inlineImages []id.ContentURI
|
var inlineImages []id.ContentURI
|
||||||
if content.Format == event.FormatHTML && content.FormattedBody != "" {
|
if content.Format == event.FormatHTML && content.FormattedBody != "" {
|
||||||
var err error
|
var err error
|
||||||
|
@ -377,6 +377,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
Stringer("event_id", dbEvt.ID).
|
Stringer("event_id", dbEvt.ID).
|
||||||
Msg("Failed to sanitize HTML")
|
Msg("Failed to sanitize HTML")
|
||||||
}
|
}
|
||||||
|
hasMath = strings.Contains(sanitizedHTML, "<hicli-math")
|
||||||
if len(inlineImages) > 0 && dbEvt.RowID != 0 {
|
if len(inlineImages) > 0 && dbEvt.RowID != 0 {
|
||||||
for _, uri := range inlineImages {
|
for _, uri := range inlineImages {
|
||||||
h.addMediaCache(ctx, dbEvt.RowID, uri.CUString(), nil, nil, "")
|
h.addMediaCache(ctx, dbEvt.RowID, uri.CUString(), nil, nil, "")
|
||||||
|
@ -406,12 +407,13 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
HTMLVersion: CurrentHTMLSanitizerVersion,
|
HTMLVersion: CurrentHTMLSanitizerVersion,
|
||||||
WasPlaintext: wasPlaintext,
|
WasPlaintext: wasPlaintext,
|
||||||
BigEmoji: bigEmoji,
|
BigEmoji: bigEmoji,
|
||||||
|
HasMath: hasMath,
|
||||||
}, inlineImages
|
}, inlineImages
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentHTMLSanitizerVersion = 4
|
const CurrentHTMLSanitizerVersion = 6
|
||||||
|
|
||||||
func (h *HiClient) ReprocessExistingEvent(ctx context.Context, evt *database.Event) {
|
func (h *HiClient) ReprocessExistingEvent(ctx context.Context, evt *database.Event) {
|
||||||
if (evt.Type != event.EventMessage.Type && evt.DecryptedType != event.EventMessage.Type) ||
|
if (evt.Type != event.EventMessage.Type && evt.DecryptedType != event.EventMessage.Type) ||
|
||||||
|
|
34
web/package-lock.json
generated
34
web/package-lock.json
generated
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "npm:types-react@rc",
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
"@types/react-dom": "npm:types-react-dom@rc",
|
||||||
|
"katex": "^0.16.11",
|
||||||
"react": "^19.0.0-rc-0751fac7-20241002",
|
"react": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
"@types/sanitize-html": "^2.13.0",
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
|
@ -1678,6 +1680,13 @@
|
||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/katex": {
|
||||||
|
"version": "0.16.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
|
||||||
|
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"name": "types-react",
|
"name": "types-react",
|
||||||
"version": "19.0.0-rc.1",
|
"version": "19.0.0-rc.1",
|
||||||
|
@ -2285,6 +2294,15 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
@ -3782,6 +3800,22 @@
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/katex": {
|
||||||
|
"version": "0.16.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",
|
||||||
|
"integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://opencollective.com/katex",
|
||||||
|
"https://github.com/sponsors/katex"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^8.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"katex": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "npm:types-react@rc",
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
"@types/react-dom": "npm:types-react-dom@rc",
|
||||||
|
"katex": "^0.16.11",
|
||||||
"react": "^19.0.0-rc-0751fac7-20241002",
|
"react": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
"@types/sanitize-html": "^2.13.0",
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
|
|
|
@ -84,6 +84,7 @@ export interface LocalContent {
|
||||||
html_version?: number
|
html_version?: number
|
||||||
was_plaintext?: boolean
|
was_plaintext?: boolean
|
||||||
big_emoji?: boolean
|
big_emoji?: boolean
|
||||||
|
has_math?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseDBEvent {
|
export interface BaseDBEvent {
|
||||||
|
|
|
@ -32,6 +32,19 @@ const onClickHTML = (evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mathImported = false
|
||||||
|
|
||||||
|
function importMath() {
|
||||||
|
if (mathImported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mathImported = true
|
||||||
|
import("./math.ts").then(
|
||||||
|
() => console.info("Imported math"),
|
||||||
|
err => console.error("Failed to import math", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TextMessageBody = ({ event, sender }: EventContentProps) => {
|
const TextMessageBody = ({ event, sender }: EventContentProps) => {
|
||||||
const content = event.content as MessageEventContent
|
const content = event.content as MessageEventContent
|
||||||
const classNames = ["message-text"]
|
const classNames = ["message-text"]
|
||||||
|
@ -48,6 +61,10 @@ const TextMessageBody = ({ event, sender }: EventContentProps) => {
|
||||||
if (event.local_content?.was_plaintext) {
|
if (event.local_content?.was_plaintext) {
|
||||||
classNames.push("plaintext-body")
|
classNames.push("plaintext-body")
|
||||||
}
|
}
|
||||||
|
if (event.local_content?.has_math) {
|
||||||
|
classNames.push("math-body")
|
||||||
|
importMath()
|
||||||
|
}
|
||||||
if (event.local_content?.sanitized_html) {
|
if (event.local_content?.sanitized_html) {
|
||||||
classNames.push("html-body")
|
classNames.push("html-body")
|
||||||
return <div
|
return <div
|
||||||
|
|
65
web/src/ui/timeline/content/math.ts
Normal file
65
web/src/ui/timeline/content/math.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import katex from "katex"
|
||||||
|
import katexCSS from "katex/dist/katex.min.css?inline"
|
||||||
|
|
||||||
|
const sheet = new CSSStyleSheet()
|
||||||
|
sheet.replaceSync(katexCSS)
|
||||||
|
|
||||||
|
class HicliMath extends HTMLElement {
|
||||||
|
static observedAttributes = ["displaymode", "latex"]
|
||||||
|
#root?: HTMLElement
|
||||||
|
#latex?: string
|
||||||
|
#displayMode?: boolean
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
const root = this.attachShadow({ mode: "open" })
|
||||||
|
root.adoptedStyleSheets = [sheet]
|
||||||
|
// This seems to work fine
|
||||||
|
this.#root = root as unknown as HTMLElement
|
||||||
|
this.#render()
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
||||||
|
if (name === "latex") {
|
||||||
|
this.#latex = newValue
|
||||||
|
} else if (name === "displaymode") {
|
||||||
|
this.#displayMode = newValue === "block"
|
||||||
|
}
|
||||||
|
this.#render()
|
||||||
|
}
|
||||||
|
|
||||||
|
#render() {
|
||||||
|
if (!this.#root || !this.#latex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
katex.render(this.#latex, this.#root, {
|
||||||
|
output: "htmlAndMathml",
|
||||||
|
maxSize: 10,
|
||||||
|
displayMode: this.#displayMode,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to render math", this.#latex, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("hicli-math", HicliMath)
|
Loading…
Add table
Reference in a new issue