Skip to content

Commit

Permalink
Add support for Obsidian type blockquote alerts
Browse files Browse the repository at this point in the history
* Make the alert type parsing more flexible to support more types
* Add `AlertTitle` and `AlertSign` (for folding)

Note that GitHub will not render callouts with alert title/sign.

See https://help.obsidian.md/Editing+and+formatting/Callouts

Closes gohugoio#12805
Closes gohugoio#12801
  • Loading branch information
bep committed Sep 1, 2024
1 parent 4691248 commit ac7e8f8
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 36 deletions.
10 changes: 10 additions & 0 deletions markup/converter/hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ type BlockquoteContext interface {
// The GitHub alert type converted to lowercase, e.g. "note".
// Only set if Type is "alert".
AlertType() string

// The alert title.
// Currently only relevant for Obsidian alerts.
// GitHub does not suport alert titles and will not render alerts with titles.
AlertTitle() hstring.HTML

// The alert sign, "+" or "-" or "" used to indicate folding.
// Currently only relevant for Obsidian alerts.
// GitHub does not suport alert signs and will not render alerts with signs.
AlertSign() string
}

type PositionerSourceTargetProvider interface {
Expand Down
57 changes: 35 additions & 22 deletions markup/goldmark/blockquotes/blockquotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)

typ := typeRegular
alertType := resolveGitHubAlert(string(text))
if alertType != "" {
alert := resolveBlockQuoteAlert(string(text))
if alert.typ != "" {
typ = typeAlert
}

Expand All @@ -94,7 +94,7 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
bqctx := &blockquoteContext{
BaseContext: render.NewBaseContext(ctx, renderer, n, src, nil, ordinal),
typ: typ,
alertType: alertType,
alert: alert,
text: hstring.HTML(text),
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
}
Expand Down Expand Up @@ -133,11 +133,9 @@ func (r *htmlRenderer) renderBlockquoteDefault(

type blockquoteContext struct {
hooks.BaseContext

text hstring.HTML
alertType string
typ string

text hstring.HTML
typ string
alert blockQuoteAlert
*attributes.AttributesHolder
}

Expand All @@ -146,25 +144,40 @@ func (c *blockquoteContext) Type() string {
}

func (c *blockquoteContext) AlertType() string {
return c.alertType
return c.alert.typ
}

func (c *blockquoteContext) AlertTitle() hstring.HTML {
return hstring.HTML(c.alert.title)
}

func (c *blockquoteContext) AlertSign() string {
return c.alert.sign
}

func (c *blockquoteContext) Text() hstring.HTML {
return c.text
}

// https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
// Five types:
// [!NOTE], [!TIP], [!WARNING], [!IMPORTANT], [!CAUTION]
// Note that GitHub's implementation is case-insensitive.
var gitHubAlertRe = regexp.MustCompile(`(?i)^<p>\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]`)

// resolveGitHubAlert returns one of note, tip, warning, important or caution.
// An empty string if no match.
func resolveGitHubAlert(s string) string {
m := gitHubAlertRe.FindStringSubmatch(s)
if len(m) == 2 {
return strings.ToLower(m[1])
var blockQuoteAlertRe = regexp.MustCompile(`^<p>\[!([a-zA-Z]+)\](-|\+)?\s?(.*)\n?`)

func resolveBlockQuoteAlert(s string) blockQuoteAlert {
m := blockQuoteAlertRe.FindStringSubmatch(s)
if len(m) == 4 {
return blockQuoteAlert{
typ: strings.ToLower(m[1]),
sign: m[2],
title: m[3],
}
}
return ""

return blockQuoteAlert{}
}

// Blockquote alert syntax was introduced by GitHub, but is also used
// by Obsidian which also support some extended attributes: More types, alert titles and a +/- sign for folding.
type blockQuoteAlert struct {
typ string
sign string
title string
}
39 changes: 39 additions & 0 deletions markup/goldmark/blockquotes/blockquotes_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,42 @@ Content: {{ .Content }}
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Content: <blockquote>\n</blockquote>\n")
}

func TestBlockquObsidianWithTitleAndSign(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---
> [!tip] Callouts can have custom titles
> Like this one.
> [!tip] Title-only callout
> [!faq]- Foldable negated callout
> Yes! In a foldable callout, the contents are hidden when the callout is collapsed
> [!faq]+ Foldable callout
> Yes! In a foldable callout, the contents are hidden when the callout is collapsed
-- layouts/index.html --
{{ .Content }}
-- layouts/_default/_markup/render-blockquote.html --
AlertType: {{ .AlertType }}|
AlertTitle: {{ .AlertTitle }}|
AlertSign: {{ .AlertSign | safeHTML }}|
Text: {{ .Text }}|
`

b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"AlertType: tip|\nAlertTitle: Callouts can have custom titles|\nAlertSign: |",
"AlertType: tip|\nAlertTitle: Title-only callout</p>|\nAlertSign: |",
"AlertType: faq|\nAlertTitle: Foldable negated callout|\nAlertSign: -|\nText: <p>Yes!",
"AlertType: faq|\nAlertTitle: Foldable callout|\nAlertSign: +|",
)
}
32 changes: 18 additions & 14 deletions markup/goldmark/blockquotes/blockquotes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,46 @@ import (
qt "github.com/frankban/quicktest"
)

func TestResolveGitHubAlert(t *testing.T) {
func TestResolveBlockQuoteAlert(t *testing.T) {
t.Parallel()

c := qt.New(t)

tests := []struct {
input string
expected string
expected blockQuoteAlert
}{
{
input: "[!NOTE]",
expected: "note",
expected: blockQuoteAlert{typ: "note"},
},
{
input: "[!WARNING]",
expected: "warning",
input: "[!FaQ]",
expected: blockQuoteAlert{typ: "faq"},
},
{
input: "[!TIP]",
expected: "tip",
input: "[!NOTE]+",
expected: blockQuoteAlert{typ: "note", sign: "+"},
},
{
input: "[!IMPORTANT]",
expected: "important",
input: "[!NOTE]-",
expected: blockQuoteAlert{typ: "note", sign: "-"},
},
{
input: "[!CAUTION]",
expected: "caution",
input: "[!NOTE] This is a note",
expected: blockQuoteAlert{typ: "note", title: "This is a note"},
},
{
input: "[!FOO]",
expected: "",
input: "[!NOTE]+ This is a note",
expected: blockQuoteAlert{typ: "note", sign: "+", title: "This is a note"},
},
{
input: "[!NOTE]+ This is a note\nThis is not.",
expected: blockQuoteAlert{typ: "note", sign: "+", title: "This is a note"},
},
}

for _, test := range tests {
c.Assert(resolveGitHubAlert("<p>"+test.input), qt.Equals, test.expected)
c.Assert(resolveBlockQuoteAlert("<p>"+test.input), qt.Equals, test.expected)
}
}

0 comments on commit ac7e8f8

Please sign in to comment.