Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add color previews in markdown #21474

Merged
merged 29 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
80dae5f
Add color preview to markdown
yardenshoham Oct 17, 2022
df33f58
Lint
yardenshoham Oct 17, 2022
7fdc683
Simplify code
yardenshoham Oct 17, 2022
4fa76f7
Merge branch 'main' into color-preview
yardenshoham Oct 17, 2022
374d11c
Merge branch 'main' into color-preview
yardenshoham Oct 17, 2022
705f92b
Override renderCodeSpan
yardenshoham Oct 17, 2022
c43063f
continued
yardenshoham Oct 17, 2022
1dfc0bd
Style
yardenshoham Oct 17, 2022
488ff92
Credit
yardenshoham Oct 17, 2022
199e848
Merge branch 'main' into color-preview
yardenshoham Oct 18, 2022
14394ab
Merge branch 'main' into color-preview
yardenshoham Oct 18, 2022
46f3a29
Add tests
yardenshoham Oct 18, 2022
066b41a
Merge branch 'main' into color-preview
yardenshoham Oct 18, 2022
74ab9de
Simplify CSS color check
yardenshoham Oct 18, 2022
a73a774
Update web_src/less/_base.less
yardenshoham Oct 19, 2022
b9c4e62
Update modules/markup/markdown/goldmark.go
yardenshoham Oct 19, 2022
64b8a08
Use dib class
yardenshoham Oct 19, 2022
117f17e
Merge branch 'main' into color-preview
yardenshoham Oct 19, 2022
56bfbf5
Use dib in negative tests
yardenshoham Oct 19, 2022
e048024
Merge branch 'main' into color-preview
yardenshoham Oct 19, 2022
9b408e4
Merge branch 'main' into color-preview
yardenshoham Oct 19, 2022
4c50bb0
Merge branch 'main' into color-preview
yardenshoham Oct 19, 2022
d337934
Merge branch 'main' into color-preview
yardenshoham Oct 20, 2022
46384bb
Merge branch 'main' into color-preview
yardenshoham Oct 20, 2022
544574b
Merge branch 'main' into color-preview
yardenshoham Oct 20, 2022
ed0dde6
Merge branch 'main' into color-preview
lunny Oct 21, 2022
b71ba24
Update modules/markup/markdown/goldmark.go
yardenshoham Oct 21, 2022
001fc08
Move dib into color-preview
yardenshoham Oct 21, 2022
fd096fb
Merge branch 'main' into color-preview
lunny Oct 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions modules/markup/markdown/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
39 changes: 39 additions & 0 deletions modules/markup/markdown/goldmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
giteautil "code.gitea.io/gitea/modules/util"

"github.com/microcosm-cc/bluemonday/css"
"github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
Expand Down Expand Up @@ -178,6 +179,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 css.ColorHandler(strings.ToLower(string(colorContent))) {
v.AppendChild(v, NewColorPreview(colorContent))
}
yardenshoham marked this conversation as resolved.
Show resolved Hide resolved
}
return ast.WalkContinue, nil
})
Expand Down Expand Up @@ -266,10 +272,43 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(KindDetails, r.renderDetails)
reg.Register(KindSummary, r.renderSummary)
reg.Register(KindIcon, r.renderIcon)
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
}

// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
// See #21474 for reference
func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<code")
html.RenderAttributes(w, n, html.CodeAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<code>")
}
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
switch v := c.(type) {
case *ast.Text:
segment := v.Segment
value := segment.Value(source)
if bytes.HasSuffix(value, []byte("\n")) {
r.Writer.RawWrite(w, value[:len(value)-1])
r.Writer.RawWrite(w, []byte(" "))
} else {
r.Writer.RawWrite(w, value)
}
case *ColorPreview:
_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview dib" style="background-color: %v"></span>`, string(v.Color)))
yardenshoham marked this conversation as resolved.
Show resolved Hide resolved
}
}
return ast.WalkSkipChildren, nil
}
_, _ = w.WriteString("</code>")
return ast.WalkContinue, nil
}

func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Document)

Expand Down
55 changes: 55 additions & 0 deletions modules/markup/markdown/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,61 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
assert.Equal(t, expected, res)
}

func TestColorPreview(t *testing.T) {
const nl = "\n"
positiveTests := []struct {
testcase string
expected string
}{
{ // hex
"`#FF0000`",
`<p><code>#FF0000<span class="color-preview dib" style="background-color: #FF0000"></span></code></p>` + nl,
},
{ // rgb
"`rgb(16, 32, 64)`",
`<p><code>rgb(16, 32, 64)<span class="color-preview dib" style="background-color: rgb(16, 32, 64)"></span></code></p>` + nl,
},
{ // short hex
"This is the color white `#000`",
`<p>This is the color white <code>#000<span class="color-preview dib" style="background-color: #000"></span></code></p>` + nl,
},
{ // hsl
"HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.",
`<p>HSL stands for hue, saturation, and lightness. An example: <code>hsl(0, 100%, 50%)<span class="color-preview dib" style="background-color: hsl(0, 100%, 50%)"></span></code>.</p>` + nl,
},
{ // uppercase hsl
"HSL stands for hue, saturation, and lightness. An example: `HSL(0, 100%, 50%)`.",
`<p>HSL stands for hue, saturation, and lightness. An example: <code>HSL(0, 100%, 50%)<span class="color-preview dib" style="background-color: HSL(0, 100%, 50%)"></span></code>.</p>` + nl,
},
}

for _, test := range positiveTests {
res, err := RenderString(&markup.RenderContext{}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)

yardenshoham marked this conversation as resolved.
Show resolved Hide resolved
}

negativeTests := []string{
// not a color code
"`FF0000`",
// inside a code block
"```javascript" + nl + `const red = "#FF0000";` + nl + "```",
// no backticks
"rgb(166, 32, 64)",
// typo
"`hsI(0, 100%, 50%)`",
// looks like a color but not really
"`hsl(40, 60, 80)`",
}

for _, test := range negativeTests {
res, err := RenderString(&markup.RenderContext{}, test)
assert.NoError(t, err, "Unexpected error in testcase: %q", test)
assert.NotContains(t, res, `<span class="color-preview dib" style="background-color: `, "Unexpected result in testcase %q", test)
}
}

func TestMathBlock(t *testing.T) {
const nl = "\n"
testcases := []struct {
Expand Down
7 changes: 5 additions & 2 deletions modules/markup/sanitizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ 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(`^color-preview dib$`)).OnElements("span")

// For Chroma markdown plugin
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")

Expand Down Expand Up @@ -88,8 +91,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{
Expand Down
7 changes: 7 additions & 0 deletions web_src/less/_base.less
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,13 @@ a.ui.card:hover,
border-color: var(--color-secondary);
}

.color-preview {
margin-left: .4em;
height: .67em;
width: .67em;
border-radius: .15em;
}

footer {
background-color: var(--color-footer);
border-top: 1px solid var(--color-secondary);
Expand Down