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 support for Obsidian type blockquote alerts #12815

Merged
merged 1 commit into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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))
}
}
Loading