]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add support for Obsidian type blockquote alerts
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 1 Sep 2024 10:00:13 +0000 (12:00 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 1 Sep 2024 16:12:26 +0000 (18:12 +0200)
* 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
markup/converter/hooks/hooks.go
markup/goldmark/blockquotes/blockquotes.go
markup/goldmark/blockquotes/blockquotes_integration_test.go
markup/goldmark/blockquotes/blockquotes_test.go

index e0eda5c51c9835fa947d1b55f3b83cf2d4b6e3bb..607514f043c852771c3a177ef47dcda4c03e727e 100755 (executable)
@@ -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
index 96c165321cea5e6169a49dafaea4acfb7df54caa..1fc513acb23d1eeb0f05f1fdb7a722469fda24e0 100644 (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 {
index bf1e848b807ef055b454ded23190d15f590cba46..f6d1a590ee072ada8cb78f839264f277b266f476 100644 (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)\]`)
-
-// 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
 }
index f12600b4207c1ac9aa31a080d69ec857b8774990..ae52b9ba790924c34a04e0ad6d0b088f3c9093a4 100644 (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|",
+       )
+}
index 5b2680a0dbd849010f937c6b4b04262a61318fd4..8b948af08c1674151431ba4f6cca67a23ded3cf3 100644 (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))
        }
 }