From 608aa99a51ee0e8e9dd36997fd79276b690d9d12 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)) } }