Skip to content

Commit 80dae5f

Browse files
committed
Add color preview to markdown
Signed-off-by: Yarden Shoham <[email protected]>
1 parent 11ac14c commit 80dae5f

File tree

4 files changed

+82
-2
lines changed

4 files changed

+82
-2
lines changed

modules/markup/markdown/ast.go

+36
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,39 @@ func IsIcon(node ast.Node) bool {
144144
_, ok := node.(*Icon)
145145
return ok
146146
}
147+
148+
// ColorPreview is an inline for a color preview
149+
type ColorPreview struct {
150+
ast.BaseInline
151+
Color []byte
152+
}
153+
154+
// Dump implements Node.Dump.
155+
func (n *ColorPreview) Dump(source []byte, level int) {
156+
m := map[string]string{}
157+
m["Color"] = string(n.Color)
158+
ast.DumpHelper(n, source, level, m, nil)
159+
}
160+
161+
// KindColorPreview is the NodeKind for ColorPreview
162+
var KindColorPreview = ast.NewNodeKind("ColorPreview")
163+
164+
// Kind implements Node.Kind.
165+
func (n *ColorPreview) Kind() ast.NodeKind {
166+
return KindColorPreview
167+
}
168+
169+
// NewColorPreview returns a new Span node.
170+
func NewColorPreview(color []byte) *ColorPreview {
171+
return &ColorPreview{
172+
BaseInline: ast.BaseInline{},
173+
Color: color,
174+
}
175+
}
176+
177+
// IsColorPreview returns true if the given node implements the ColorPreview interface,
178+
// otherwise false.
179+
func IsColorPreview(node ast.Node) bool {
180+
_, ok := node.(*ColorPreview)
181+
return ok
182+
}

modules/markup/markdown/goldmark.go

+24
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727

2828
var byteMailto = []byte("mailto:")
2929

30+
var cssColorRegex = regexp.MustCompile(`(?i)(#(?:[0-9a-f]{2}){2,4}$|(#[0-9a-f]{3}$)|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$`)
31+
3032
// ASTTransformer is a default transformer of the goldmark tree.
3133
type ASTTransformer struct{}
3234

@@ -178,6 +180,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
178180
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
179181
}
180182
}
183+
case *ast.CodeSpan:
184+
colorContent := n.Text(reader.Source())
185+
if cssColorRegex.Match(colorContent) {
186+
v.Parent().InsertAfter(v.Parent(), v, NewColorPreview(colorContent))
187+
}
181188
}
182189
return ast.WalkContinue, nil
183190
})
@@ -266,6 +273,7 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
266273
reg.Register(KindDetails, r.renderDetails)
267274
reg.Register(KindSummary, r.renderSummary)
268275
reg.Register(KindIcon, r.renderIcon)
276+
reg.Register(KindColorPreview, r.renderColorPreview)
269277
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
270278
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
271279
}
@@ -356,6 +364,22 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
356364
return ast.WalkContinue, nil
357365
}
358366

367+
func (r *HTMLRenderer) renderColorPreview(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
368+
var err error
369+
n := node.(*ColorPreview)
370+
if entering {
371+
_, err = w.WriteString(fmt.Sprintf(`<code class="color-preview"><span class="repo-icon rounded" style="background-color: %v">`, string(n.Color)))
372+
} else {
373+
_, err = w.WriteString("</span></code>")
374+
}
375+
376+
if err != nil {
377+
return ast.WalkStop, err
378+
}
379+
380+
return ast.WalkContinue, nil
381+
}
382+
359383
func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
360384
n := node.(*TaskCheckBoxListItem)
361385
if entering {

modules/markup/sanitizer.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func createDefaultPolicy() *bluemonday.Policy {
5555
// For JS code copy and Mermaid loading state
5656
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
5757

58+
// For color preview
59+
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^repo-icon rounded$`)).OnElements("span")
60+
policy.AllowAttrs("class").Matching(regexp.MustCompile("^color-preview$")).OnElements("code")
61+
5862
// For Chroma markdown plugin
5963
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
6064

@@ -88,8 +92,8 @@ func createDefaultPolicy() *bluemonday.Policy {
8892
// Allow 'style' attribute on text elements.
8993
policy.AllowAttrs("style").OnElements("span", "p")
9094

91-
// Allow 'color' property for the style attribute on text elements.
92-
policy.AllowStyles("color").OnElements("span", "p")
95+
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
96+
policy.AllowStyles("color", "background-color").OnElements("span", "p")
9397

9498
// Allow generally safe attributes
9599
generalSafeAttrs := []string{

web_src/less/_base.less

+16
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,22 @@ a.ui.card:hover,
13711371
border-color: var(--color-secondary);
13721372
}
13731373

1374+
.color-preview {
1375+
padding-left: 0 !important;
1376+
border-top-left-radius: 0 !important;
1377+
border-bottom-left-radius: 0 !important;
1378+
span {
1379+
height: 7px;
1380+
width: 7px;
1381+
}
1382+
}
1383+
1384+
code:has(+ .color-preview) {
1385+
border-top-right-radius: 0;
1386+
border-bottom-right-radius: 0;
1387+
padding-right: 0.2em;
1388+
}
1389+
13741390
footer {
13751391
background-color: var(--color-footer);
13761392
border-top: 1px solid var(--color-secondary);

0 commit comments

Comments
 (0)