From 80dae5f8269bbb9d8408e89ca564e7e77e4416f6 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Mon, 17 Oct 2022 10:37:30 +0000 Subject: [PATCH] Add color preview to markdown Signed-off-by: Yarden Shoham --- modules/markup/markdown/ast.go | 36 +++++++++++++++++++++++++++++ modules/markup/markdown/goldmark.go | 24 +++++++++++++++++++ modules/markup/sanitizer.go | 8 +++++-- web_src/less/_base.less | 16 +++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index 5191d94cdd85..c82d5e5e7339 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -144,3 +144,39 @@ func IsIcon(node ast.Node) bool { _, ok := node.(*Icon) return ok } + +// ColorPreview is an inline for a color preview +type ColorPreview struct { + ast.BaseInline + Color []byte +} + +// Dump implements Node.Dump. +func (n *ColorPreview) Dump(source []byte, level int) { + m := map[string]string{} + m["Color"] = string(n.Color) + ast.DumpHelper(n, source, level, m, nil) +} + +// KindColorPreview is the NodeKind for ColorPreview +var KindColorPreview = ast.NewNodeKind("ColorPreview") + +// Kind implements Node.Kind. +func (n *ColorPreview) Kind() ast.NodeKind { + return KindColorPreview +} + +// NewColorPreview returns a new Span node. +func NewColorPreview(color []byte) *ColorPreview { + return &ColorPreview{ + BaseInline: ast.BaseInline{}, + Color: color, + } +} + +// IsColorPreview returns true if the given node implements the ColorPreview interface, +// otherwise false. +func IsColorPreview(node ast.Node) bool { + _, ok := node.(*ColorPreview) + return ok +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 8417019ddbad..38438b5604e6 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -27,6 +27,8 @@ import ( var byteMailto = []byte("mailto:") +var cssColorRegex = regexp.MustCompile(`(?i)(#(?:[0-9a-f]{2}){2,4}$|(#[0-9a-f]{3}$)|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$`) + // ASTTransformer is a default transformer of the goldmark tree. type ASTTransformer struct{} @@ -178,6 +180,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) } } + case *ast.CodeSpan: + colorContent := n.Text(reader.Source()) + if cssColorRegex.Match(colorContent) { + v.Parent().InsertAfter(v.Parent(), v, NewColorPreview(colorContent)) + } } return ast.WalkContinue, nil }) @@ -266,6 +273,7 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindDetails, r.renderDetails) reg.Register(KindSummary, r.renderSummary) reg.Register(KindIcon, r.renderIcon) + reg.Register(KindColorPreview, r.renderColorPreview) reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) } @@ -356,6 +364,22 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node return ast.WalkContinue, nil } +func (r *HTMLRenderer) renderColorPreview(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + var err error + n := node.(*ColorPreview) + if entering { + _, err = w.WriteString(fmt.Sprintf(``, string(n.Color))) + } else { + _, err = w.WriteString("") + } + + if err != nil { + return ast.WalkStop, err + } + + return ast.WalkContinue, nil +} + func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*TaskCheckBoxListItem) if entering { diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 807a8a7892b3..4d0df289154a 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -55,6 +55,10 @@ func createDefaultPolicy() *bluemonday.Policy { // For JS code copy and Mermaid loading state policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre") + // For color preview + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^repo-icon rounded$`)).OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^color-preview$")).OnElements("code") + // For Chroma markdown plugin policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code") @@ -88,8 +92,8 @@ func createDefaultPolicy() *bluemonday.Policy { // Allow 'style' attribute on text elements. policy.AllowAttrs("style").OnElements("span", "p") - // Allow 'color' property for the style attribute on text elements. - policy.AllowStyles("color").OnElements("span", "p") + // Allow 'color' and 'background-color' properties for the style attribute on text elements. + policy.AllowStyles("color", "background-color").OnElements("span", "p") // Allow generally safe attributes generalSafeAttrs := []string{ diff --git a/web_src/less/_base.less b/web_src/less/_base.less index bfc6e0cf96cb..f66ad969a4a2 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -1371,6 +1371,22 @@ a.ui.card:hover, border-color: var(--color-secondary); } +.color-preview { + padding-left: 0 !important; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + span { + height: 7px; + width: 7px; + } +} + +code:has(+ .color-preview) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + padding-right: 0.2em; +} + footer { background-color: var(--color-footer); border-top: 1px solid var(--color-secondary);