Skip to content

Commit

Permalink
Start supporting Obsidian-style callouts (#466)
Browse files Browse the repository at this point in the history
  • Loading branch information
srid authored Dec 5, 2023
1 parent ea2dca2 commit 7a6150a
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 2 deletions.
29 changes: 29 additions & 0 deletions docs/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,35 @@ You can highlight any ==inline text== by wraping them in `==` (ie. `==inline tex

[^prop]: See original proposal for this syntax [here](https://talk.commonmark.org/t/highlighting-text-with-the-mark-element/840).

## Callouts

Emanote supports [Obsidian-style callouts](https://help.obsidian.md/Editing+and+formatting/Callouts).[^callout] To customizing their structure and styling, change `callout.tpl` (and `base.tpl`) in [[html-template|HTML templates]].

[^callout]: Not all of Obsidian spec may yet be supported. See https://github.com/srid/emanote/issues/465 for details.

> [!note]
> This is a note callout
>
> Lorem **ipsum** dolor sit *amet*, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
> [!info]
> This is an info callout
>
> Lorem **ipsum** dolor sit *amet*, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
> [!tip] Callouts can have *custom* titles
> Like this one.
>
> Lorem **ipsum** dolor sit *amet*, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
> [!warning]
>
> Lorem **ipsum** dolor sit *amet*, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
> [!failure]
>
> Lorem **ipsum** dolor sit *amet*, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
## More extensions

:::{.flex-row .space-y-8}
Expand Down
49 changes: 48 additions & 1 deletion emanote/default/templates/base.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,53 @@
font-family: monospace;
}

/* Callouts */
div.callout {
background-color: #f5f5f5;
padding: 1em 1em 0.5em;
border-radius: 0.5em;
margin-bottom: 1em;
}

.callout[data-callout="note"] {
--callout-color: 8, 109, 221;
}

.callout[data-callout="info"] {
--callout-color: 8, 109, 221;
}

.callout[data-callout="tip"] {
--callout-color: 8, 191, 188;
}

.callout[data-callout="warning"] {
--callout-color: 236, 117, 0;
}

.callout[data-callout="failure"] {
--callout-color: 233, 49, 71;
}

div.callout {
background-color: rgba(var(--callout-color), 0.1);
}

.callout .callout-title {
color: rgb(var(--callout-color));
}

div.callout-title {
display: flex;
align-items: center;
margin-bottom: 0.5em;
font-variation-settings: 'wght' 600;
}

div.callout-title div.callout-title-inner {
margin-left: 0.5em;
}

/* External link icon */
a[data-linkicon=""]::after {
content: ""
Expand Down Expand Up @@ -92,4 +139,4 @@
</ema:metadata>
</body>

</html>
</html>
2 changes: 2 additions & 0 deletions emanote/default/templates/filters/callout-icon-failure.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
3 changes: 3 additions & 0 deletions emanote/default/templates/filters/callout-icon-info.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
2 changes: 2 additions & 0 deletions emanote/default/templates/filters/callout-icon-note.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<line x1="18" y1="2" x2="22" y2="6"></line>
<path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path>
3 changes: 3 additions & 0 deletions emanote/default/templates/filters/callout-icon-tip.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<path
d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z">
</path>
3 changes: 3 additions & 0 deletions emanote/default/templates/filters/callout-icon-warning.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
20 changes: 20 additions & 0 deletions emanote/default/templates/filters/callout.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div data-callout-metadata="" data-callout-fold="" data-callout="${callout:type}"
class="callout bg-opacity-10">
<path
d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z">
</path>
<div class="callout-title">
<div class="callout-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<apply template="/templates/filters/callout-icon-${callout:type}" />
</svg>
</div>
<div class="callout-title-inner">
<callout:title />
</div>
</div>
<div class="callout-content">
<callout:body />
</div>
</div>
4 changes: 3 additions & 1 deletion emanote/emanote.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.4
name: emanote
version: 1.2.0.0
version: 1.2.1.0
license: AGPL-3.0-only
copyright: 2022 Sridhar Ratnakumar
maintainer: [email protected]
Expand Down Expand Up @@ -186,6 +186,7 @@ library
Emanote.Pandoc.Markdown.Syntax.HashTag
Emanote.Pandoc.Markdown.Syntax.Highlight
Emanote.Pandoc.Renderer
Emanote.Pandoc.Renderer.Callout
Emanote.Pandoc.Renderer.Embed
Emanote.Pandoc.Renderer.Query
Emanote.Pandoc.Renderer.Url
Expand Down Expand Up @@ -244,3 +245,4 @@ test-suite test
Emanote.Model.Link.RelSpec
Emanote.Model.QuerySpec
Emanote.Pandoc.ExternalLinkSpec
Emanote.Pandoc.Renderer.CalloutSpec
2 changes: 2 additions & 0 deletions emanote/src/Emanote.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Emanote.Model.Link.Rel (ResolvedRelTarget (..))
import Emanote.Model.Type (modelCompileTailwind)
import Emanote.Model.Type qualified as Model
import Emanote.Pandoc.Renderer
import Emanote.Pandoc.Renderer.Callout qualified as PF
import Emanote.Pandoc.Renderer.Embed qualified as PF
import Emanote.Pandoc.Renderer.Query qualified as PF
import Emanote.Pandoc.Renderer.Url qualified as PF
Expand Down Expand Up @@ -144,6 +145,7 @@ defaultEmanotePandocRenderers =
[ PF.embedBlockWikiLinkResolvingSplice
, PF.embedBlockRegularLinkResolvingSplice
, PF.queryResolvingSplice
, PF.calloutResolvingSplice
]
inlineRenderers =
PandocRenderers
Expand Down
113 changes: 113 additions & 0 deletions emanote/src/Emanote/Pandoc/Renderer/Callout.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{-# LANGUAGE RecordWildCards #-}

module Emanote.Pandoc.Renderer.Callout (
calloutResolvingSplice,

-- * For tests
CalloutType (..),
Callout (..),
parseCalloutType,
) where

import Control.Monad (msum)
import Data.Default (Default (def))
import Data.Map.Syntax ((##))
import Data.Text qualified as T
import Emanote.Model (Model)
import Emanote.Model.Title qualified as Tit
import Emanote.Pandoc.Renderer (PandocBlockRenderer)
import Emanote.Route (LMLRoute)
import Heist.Extra qualified as HE
import Heist.Extra.Splices.Pandoc qualified as HP
import Heist.Interpreted qualified as HI
import Relude
import Text.Megaparsec qualified as M
import Text.Megaparsec.Char qualified as M
import Text.Pandoc.Definition qualified as B

calloutResolvingSplice :: PandocBlockRenderer Model LMLRoute
calloutResolvingSplice _model _nr ctx _noteRoute blk = do
B.BlockQuote blks <- pure blk
callout <- parseCallout blks
pure $ do
tpl <- HE.lookupHtmlTemplateMust "/templates/filters/callout"
HE.runCustomTemplate tpl $ do
"callout:type" ## HI.textSplice (T.toLower $ show $ type_ callout)
"callout:title" ## Tit.titleSplice ctx id $ Tit.fromInlines (title callout)
"callout:body" ## HP.pandocSplice ctx $ B.Pandoc mempty (body callout)
"query" ##
HI.textSplice (show blks)

{- | Obsidian callout type
TODO: Add the rest, from https://help.obsidian.md/Editing+and+formatting/Callouts#Supported%20types
-}
data CalloutType
= Note
| Info
| Tip
| Warning
| Failure
deriving stock (Eq, Ord, Show, Enum, Bounded)

instance Default CalloutType where
def = Note

data Callout = Callout
{ type_ :: CalloutType
, title :: [B.Inline]
, body :: [B.Block]
}
deriving stock (Eq, Ord, Show)

-- | Parse `Callout` from blockquote blocks
parseCallout :: [B.Block] -> Maybe Callout
parseCallout = parseObsidianCallout

-- | Parse according to https://help.obsidian.md/Editing+and+formatting/Callouts
parseObsidianCallout :: [B.Block] -> Maybe Callout
parseObsidianCallout blks = do
B.Para (B.Str calloutType : inlines) : body' <- pure blks
type_ <- parseCalloutType calloutType
let (title', mFirstPara) = disrespectSoftbreak inlines
title = if null title' then defaultTitle type_ else title'
body = maybe body' (: body') mFirstPara
pure $ Callout {..}

{- | If there is a `B.SoftBreak`, treat it as paragraph break.
We do this to support Obsidian callouts where the first paragraph can start
immediately after the callout heading without a newline break in between.
-}
disrespectSoftbreak :: [B.Inline] -> ([B.Inline], Maybe B.Block)
disrespectSoftbreak = \case
[] -> ([], Nothing)
(B.SoftBreak : rest) -> ([], Just (B.Para rest))
(x : xs) ->
let (a, b) = disrespectSoftbreak xs
in (x : a, b)

defaultTitle :: CalloutType -> [B.Inline]
defaultTitle t =
[B.Str $ show t]

-- | Parse, for example, "[!tip]" into 'Tip'.
parseCalloutType :: Text -> Maybe CalloutType
parseCalloutType =
rightToMaybe . parse parser "<callout:type>"
where
parser :: M.Parsec Void Text CalloutType
parser = do
void $ M.string "[!"
s <- T.toLower . toText <$> M.some M.letterChar
void $ M.string "]"
maybe (fail "Unknown") pure $ parseType s
parseType :: Text -> Maybe CalloutType
parseType s =
msum $ flip fmap (universe @CalloutType) $ \t -> do
guard $ s == T.toLower (show @Text t)
Just t
parse :: M.Parsec Void Text a -> String -> Text -> Either Text a
parse p fn =
first (toText . M.errorBundlePretty)
. M.parse (p <* M.eof) fn
15 changes: 15 additions & 0 deletions emanote/test/Emanote/Pandoc/Renderer/CalloutSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Emanote.Pandoc.Renderer.CalloutSpec where

import Emanote.Pandoc.Renderer.Callout
import Hedgehog
import Relude
import Test.Hspec
import Test.Hspec.Hedgehog

spec :: Spec
spec = do
describe "callout" $ do
it "type" . hedgehog $ do
parseCalloutType "[!tip]" === Just Tip
parseCalloutType "[!Note]" === Just Note
parseCalloutType "[!INFO]" === Just Info

0 comments on commit 7a6150a

Please sign in to comment.