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 #12805
Closes #12801
  • Loading branch information
bep committed Sep 1, 2024
1 parent 0c45342 commit e651d29
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 40 deletions.
20 changes: 17 additions & 3 deletions docs/content/en/render-hooks/blockquotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ Blockquote render hook templates receive the following [context]:

(`string`) Applicable when [`Type`](#type) is `alert`, this is the alert type converted to lowercase. See the [alerts](#alerts) section below.

###### AlertTitle

{{< new-in 0.134.0 >}}

(`hstring.HTML`) Applicable when [`Type`](#type) is `alert` when using [Obsidian callouts] syntax, this is the alert title converted to HTML.

###### AlertSign

{{< new-in 0.134.0 >}}

(`string`) Applicable when [`Type`](#type) is `alert` when using [Obsidian callouts] syntax, this is one of "+", "-" or "" (empty string) to indicate the presence of a foldable sign.

[Obsidian callouts]: https://help.obsidian.md/Editing+and+formatting/Callouts

###### Attributes

(`map`) The [Markdown attributes], available if you configure your site as follows:
Expand Down Expand Up @@ -117,13 +131,13 @@ Also known as _callouts_ or _admonitions_, alerts are blockquotes used to emphas

{{% note %}}
This syntax is compatible with the GitHub Alert Markdown extension.
This syntax is compatible with both the GitHub Alert Markdown extension and Obsidian's callout syntax.
But note that GitHub will not recognize callouts with one of Obsidian's extensions (e.g. callout title or the foldable sign).
{{% /note %}}


The first line of each alert is an alert designator consisting of an exclamation point followed by the alert type, wrapped within brackets.

The blockquote render hook below renders a multilingual alert if an alert desginator is present, otherwise it renders a blockquote according to the CommonMark specification.
The blockquote render hook below renders a multilingual alert if an alert designator is present, otherwise it renders a blockquote according to the CommonMark specification.

{{< code file=layouts/_default/_markup/render-blockquote.html copy=true >}}
{{ $emojis := dict
Expand Down
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\r\n]?([^\n]*)\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
}
45 changes: 45 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,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|",
)
}
38 changes: 23 additions & 15 deletions markup/goldmark/blockquotes/blockquotes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

0 comments on commit e651d29

Please sign in to comment.