zim-to-markdown-0.3.0.2: src/ZimToMarkdown/Inline.hs
-- | Shared scanning helpers for inline markup.
module ZimToMarkdown.Inline (
breakOnFirstMarker,
enclosed,
italicBoundary,
renderInlineCode,
renderMarkdownPath,
) where
import Data.Char (isSpace)
import Data.List (minimumBy)
import Data.Ord (comparing)
import Data.Text (Text)
import Data.Text qualified as Text
{- | Finds the earliest occurrence of any marker and returns the text before it
together with the remaining text, which starts with the marker.
-}
breakOnFirstMarker :: [Text] -> Text -> (Text, Maybe Text)
breakOnFirstMarker markers text =
case [ (Text.length prefix, suffix)
| marker <- markers
, let (prefix, suffix) = Text.breakOn marker text
, not (Text.null suffix)
] of
[] -> (text, Nothing)
matches ->
let (index, suffix) = minimumBy (comparing fst) matches
in (Text.take index text, Just suffix)
{- | Splits text that starts with an opening delimiter and contains a closing
delimiter, returning the rendered content and the remaining text.
-}
enclosed :: Text -> Text -> (Text -> Text) -> Text -> Maybe (Text, Text)
enclosed opener closer render text = do
rest <- Text.stripPrefix opener text
let (inner, closing) = Text.breakOn closer rest
if Text.null inner || Text.null closing
then Nothing
else
Just
( render inner
, Text.drop (Text.length closer) closing
)
{- | Tests whether an opening delimiter can start inline emphasis. This keeps
constructs such as URLs and identifiers with underscores intact.
-}
italicBoundary :: Text -> Bool
italicBoundary preceding =
case Text.unsnoc preceding of
Nothing -> True
Just (_, character) ->
isSpace character || character `elem` ("([{<'\"-" :: String)
-- | Renders inline code with a backtick fence long enough to hold its content.
renderInlineCode :: Text -> Text
renderInlineCode content =
let fence = Text.replicate (longestBacktickRun content + 1) "`"
padded
| Text.isPrefixOf "`" content || Text.isSuffixOf "`" content =
" " <> content <> " "
| otherwise = content
in fence <> padded <> fence
{- | Renders a link target, wrapping it in angle brackets when it contains
characters that would break a Markdown destination.
-}
renderMarkdownPath :: Text -> Text
renderMarkdownPath path
| Text.null path = path
| Text.any needsBrackets path = "<" <> path <> ">"
| otherwise = path
where
needsBrackets character =
character == ' ' || character == '(' || character == ')'
longestBacktickRun :: Text -> Int
longestBacktickRun = snd . Text.foldl' step (0, 0)
where
step (current, longest) character
| character == '`' =
let next = current + 1
in (next, max longest next)
| otherwise = (0, longest)