Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
188 lines
5.6 KiB
Go
188 lines
5.6 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"html"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/extension"
|
|
astExt "github.com/yuin/goldmark/extension/ast"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
// StripMarkdown removes markdown syntax from text and returns plain text.
|
|
func StripMarkdown(markdown string) (string, error) {
|
|
md := goldmark.New(
|
|
goldmark.WithExtensions(extension.Strikethrough),
|
|
goldmark.WithRenderer(
|
|
renderer.NewRenderer(renderer.WithNodeRenderers(
|
|
util.Prioritized(newNotificationRenderer(), 500),
|
|
)),
|
|
),
|
|
)
|
|
|
|
var buf strings.Builder
|
|
if err := md.Convert([]byte(markdown), &buf); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(buf.String()), nil
|
|
}
|
|
|
|
// StripMarkdownAndDecode removes markdown syntax and decodes HTML entities
|
|
// (both named like < and numeric like <) to their character equivalents.
|
|
// This is useful for plain text contexts like mobile push notifications where
|
|
// HTML will not be rendered.
|
|
//
|
|
// SECURITY NOTE: The output is intended for plain text contexts only.
|
|
// Do NOT use in contexts where HTML could be rendered without proper escaping.
|
|
func StripMarkdownAndDecode(markdown string) (string, error) {
|
|
stripped, err := StripMarkdown(markdown)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return html.UnescapeString(stripped), nil
|
|
}
|
|
|
|
var relLinkReg = regexp.MustCompile(`\[(.*)]\((/.*)\)`)
|
|
var blockquoteReg = regexp.MustCompile(`^|\n(>)`)
|
|
|
|
// MarkdownToHTML takes a string containing Markdown and returns a string with HTML tagged version
|
|
func MarkdownToHTML(markdown, siteURL string) (string, error) {
|
|
// Turn relative links into absolute links
|
|
absLinkMarkdown := relLinkReg.ReplaceAllStringFunc(markdown, func(s string) string {
|
|
return relLinkReg.ReplaceAllString(s, "[$1]("+siteURL+"$2)")
|
|
})
|
|
|
|
// Unescape any blockquote text to be parsed by the markdown parser.
|
|
markdownClean := blockquoteReg.ReplaceAllStringFunc(absLinkMarkdown, func(s string) string {
|
|
return html.UnescapeString(s)
|
|
})
|
|
|
|
md := goldmark.New(
|
|
goldmark.WithExtensions(extension.GFM),
|
|
)
|
|
|
|
var b strings.Builder
|
|
|
|
err := md.Convert([]byte(markdownClean), &b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
type notificationRenderer struct {
|
|
}
|
|
|
|
func newNotificationRenderer() *notificationRenderer {
|
|
return ¬ificationRenderer{}
|
|
}
|
|
|
|
func (r *notificationRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
// block
|
|
reg.Register(ast.KindDocument, r.renderDefault)
|
|
reg.Register(ast.KindHeading, r.renderItem)
|
|
reg.Register(ast.KindBlockquote, r.renderDefault)
|
|
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
|
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
|
|
reg.Register(ast.KindHTMLBlock, r.renderDefault)
|
|
reg.Register(ast.KindList, r.renderDefault)
|
|
reg.Register(ast.KindListItem, r.renderItem)
|
|
reg.Register(ast.KindParagraph, r.renderItem)
|
|
reg.Register(ast.KindTextBlock, r.renderTextBlock)
|
|
reg.Register(ast.KindThematicBreak, r.renderDefault)
|
|
|
|
// inlines
|
|
reg.Register(ast.KindAutoLink, r.renderDefault)
|
|
reg.Register(ast.KindCodeSpan, r.renderDefault)
|
|
reg.Register(ast.KindEmphasis, r.renderDefault)
|
|
reg.Register(ast.KindImage, r.renderDefault)
|
|
reg.Register(ast.KindLink, r.renderDefault)
|
|
reg.Register(ast.KindRawHTML, r.renderDefault)
|
|
reg.Register(ast.KindText, r.renderText)
|
|
reg.Register(ast.KindString, r.renderString)
|
|
|
|
// strikethrough
|
|
reg.Register(astExt.KindStrikethrough, r.renderDefault)
|
|
}
|
|
|
|
// renderDefault renderer function to renderDefault without changes
|
|
func (r *notificationRenderer) renderDefault(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) renderItem(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
if node.NextSibling() != nil {
|
|
_ = w.WriteByte(' ')
|
|
}
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
n := node.(*ast.CodeBlock)
|
|
if entering {
|
|
r.writeLines(w, source, n)
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
n := node.(*ast.FencedCodeBlock)
|
|
if entering {
|
|
r.writeLines(w, source, n)
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) 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
|
|
_, _ = w.Write(segment.Value(source))
|
|
if !n.IsRaw() {
|
|
if n.HardLineBreak() || n.SoftLineBreak() {
|
|
_ = w.WriteByte('\n')
|
|
}
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) renderTextBlock(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
if node.NextSibling() != nil && node.FirstChild() != nil {
|
|
_ = w.WriteByte(' ')
|
|
}
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) renderString(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
n := node.(*ast.String)
|
|
_, _ = w.Write(n.Value)
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *notificationRenderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
|
|
for i := range n.Lines().Len() {
|
|
line := n.Lines().At(i)
|
|
value := line.Value(source)
|
|
_, _ = w.Write(value)
|
|
}
|
|
}
|