From e651d29801325095cb6a684fb7ce31934ce2906c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?=
Date: Sun, 1 Sep 2024 12:00:13 +0200
Subject: [PATCH] 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
---
docs/content/en/render-hooks/blockquotes.md | 20 ++++++-
markup/converter/hooks/hooks.go | 10 ++++
markup/goldmark/blockquotes/blockquotes.go | 57 ++++++++++++-------
.../blockquotes_integration_test.go | 45 +++++++++++++++
.../goldmark/blockquotes/blockquotes_test.go | 38 ++++++++-----
5 files changed, 130 insertions(+), 40 deletions(-)
diff --git a/docs/content/en/render-hooks/blockquotes.md b/docs/content/en/render-hooks/blockquotes.md
index e0eda5c51c9..607514f043c 100755
--- a/docs/content/en/render-hooks/blockquotes.md
+++ b/docs/content/en/render-hooks/blockquotes.md
@@ -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:
@@ -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
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 96c165321ce..1fc513acb23 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -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 {
diff --git a/markup/goldmark/blockquotes/blockquotes.go b/markup/goldmark/blockquotes/blockquotes.go
index bf1e848b807..f6d1a590ee0 100644
--- a/markup/goldmark/blockquotes/blockquotes.go
+++ b/markup/goldmark/blockquotes/blockquotes.go
@@ -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)^\[!(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(`^
\[!([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
}
diff --git a/markup/goldmark/blockquotes/blockquotes_integration_test.go b/markup/goldmark/blockquotes/blockquotes_integration_test.go
index f12600b4207..ae52b9ba790 100644
--- a/markup/goldmark/blockquotes/blockquotes_integration_test.go
+++ b/markup/goldmark/blockquotes/blockquotes_integration_test.go
@@ -109,3 +109,48 @@ Content: {{ .Content }}
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Content:
\n
\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
|\nAlertSign: |",
+ "AlertType: faq|\nAlertTitle: Foldable negated callout|\nAlertSign: -|\nText: Yes!",
+ "AlertType: faq|\nAlertTitle: Foldable callout|\nAlertSign: +|",
+ "AlertType: danger|\nAlertTitle: |\nAlertSign: |\nText:
Do not approach or handle without protective gear.
\n|",
+ )
+}
diff --git a/markup/goldmark/blockquotes/blockquotes_test.go b/markup/goldmark/blockquotes/blockquotes_test.go
index 5b2680a0dbd..8b948af08c1 100644
--- a/markup/goldmark/blockquotes/blockquotes_test.go
+++ b/markup/goldmark/blockquotes/blockquotes_test.go
@@ -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(""+test.input), qt.Equals, test.expected)
+ for i, test := range tests {
+ c.Assert(resolveBlockQuoteAlert("
"+test.input), qt.Equals, test.expected, qt.Commentf("Test %d", i))
}
}