Add support for Obsidian type blockquote alerts

* 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 #12805
Closes #12801
This commit is contained in:
Bjørn Erik Pedersen
2024-09-01 12:00:13 +02:00
parent 0c453420e6
commit e651d29801
5 changed files with 129 additions and 39 deletions

View File

@@ -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 {

View File

@@ -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
}
@@ -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),
}
@@ -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
}
@@ -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)\]`)
var blockQuoteAlertRe = regexp.MustCompile(`^<p>\[!([a-zA-Z]+)\](-|\+)?[^\S\r\n]?([^\n]*)\n?`)
// 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])
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
}

View File

@@ -109,3 +109,48 @@ 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"
---
> [!danger]
> Do not approach or handle without protective gear.
> [!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: +|",
"AlertType: danger|\nAlertTitle: |\nAlertSign: |\nText: <p>Do not approach or handle without protective gear.</p>\n|",
)
}

View File

@@ -19,42 +19,50 @@ 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 title\nThis is not.",
expected: blockQuoteAlert{typ: "note", sign: "+", title: "This is a title"},
},
{
input: "[!NOTE]\nThis is not.",
expected: blockQuoteAlert{typ: "note"},
},
}
for _, test := range tests {
c.Assert(resolveGitHubAlert("<p>"+test.input), qt.Equals, test.expected)
for i, test := range tests {
c.Assert(resolveBlockQuoteAlert("<p>"+test.input), qt.Equals, test.expected, qt.Commentf("Test %d", i))
}
}