dillonkearns/elm-markdown/4.0.0

NOTE: the latest version is 7.0.1

Markdown.Block

type Block
= HtmlBlock (Html Block)
| UnorderedList (List (ListItem Inline))
| OrderedList Int (List (List Inline))
| BlockQuote (List Block)
| Heading HeadingLevel (List Inline)
| Paragraph (List Inline)
| Table
(List
{ label : List Inline
, alignment : Maybe Alignment
}
)
(List (List Inline))
| CodeBlock
{ body : String
, language : Maybe String
}
| ThematicBreak

This is the AST (abstract syntax tree) that represents your parsed markdown.

In the simplest case, you can pass this directly to a renderer:

module Main exposing (main)


import Markdown.Block exposing (Block)
import Markdown.Parser
import Markdown.Renderer


markdown : String
markdown =
    "# This is a title!\n\nThis is the body."


astResult : Result (List (Advanced.DeadEnd String Parser.Problem)) (List Block)
astResult =
    markdown
        |> Markdown.Parser.parse


main : Html msg
main =
    case
        astResult
            |> Result.mapError deadEndsToString
            |> Result.andThen (\ast -> Markdown.Renderer.render Markdown.Renderer.defaultHtmlRenderer ast)
    of
        Ok rendered ->
            div [] rendered


        Err errors ->
            text errors
= H1
| H2
| H3
| H4
| H5
| H6

Markdown limits headings to level 6 or less. HTML does this, too (<h7> is not supported by most browsers). This type represents the possible heading levels that a Markdown heading block may have.

If you do a heading level above 6, the # characters will be treated as literal #'s.

A convenience function so that you don't have to write a big case statement if you need a heading level as an Int.

List Items

type ListItem children
= ListItem Task (List children)

The value for an unordered list item, which may contain a task.

type Task
= NoTask
| IncompleteTask
| CompletedTask

A task (or no task), which may be contained in a ListItem.

= AlignLeft
| AlignRight
| AlignCenter

Alignment in a header cell in a markdown table. See the Table variant in the Block type.

HTML

type Html children
= HtmlElement String (List HtmlAttribute) (List children)
| HtmlComment String
| ProcessingInstruction String
| HtmlDeclaration String String
| Cdata String

The way HTML is handled is one of the core ideas of this library.

You get the full HTML structure that you can use to process the Blocks before rendering them. Once you render them, all of the raw text within your HTML is parsed as Markdown.

HtmlComments and metadata

HtmlComments contain the raw comment text, completely unprocessed. That means you'll need to trim it if you want to strip the leading or trailing whitespace.

Renderer's do not process HtmlComments. If you want to do any special processing based on HTML comments, you can inspect the Markdown.Block.Blocks before rendering it and perform any special processing based on that. You could even add or remove Blocks, for example, based on the presence of certain comment values.

See <Markdown.Html> for more.

Inlines

type Inline
= HtmlInline (Html Block)
| Link String (Maybe String) (List Inline)
| Image String (Maybe String) (List Inline)
| Emphasis (List Inline)
| Strong (List Inline)
| CodeSpan String
| Text String
| HardLineBreak

An Inline block. Note that HtmlInlines can contain Blocks, not just nested Inlines.

type alias HtmlAttribute =
{ name : String
, value : String
}

An Html attribute. In <div class="foo">, you would have { name = "class", value = "foo" }.

extractInlineText : List Inline -> String

Extract the text from a list of inlines.

inlines : List (Inline)
inlines =
    [ Text "Heading with "
    , Emphasis 1
        [ Text "emphasis" ]
    ]


extractText inlines == "Heading with emphasis"


-- Original string: "Heading with *emphasis*"

Transformations

walk : (Block -> Block) -> Block -> Block

Recursively apply a function to transform each Block.

This example bumps headings down by one level.

import Markdown.Block as Block exposing (..)


bumpHeadingLevel : HeadingLevel -> HeadingLevel
bumpHeadingLevel level =
    case level of
        H1 -> H2
        H2 -> H3
        H3 -> H4
        H4 -> H5
        H5 -> H6
        H6 -> H6


[ Heading H1 [ Text "First heading" ]
, Paragraph [ Text "Paragraph" ]
, BlockQuote
    [ Heading H2 [ Text "Paragraph" ]
    ]
, Heading H1 [ Text "Second heading" ]
]
    |> List.map
        (Block.walk
            (\block ->
                case block of
                    Heading level children ->
                        Heading (bumpHeadingLevel level) children
                    _ ->
                        block
            )
        )
--> [ Heading H2 [ Text "First heading" ]
--> , Paragraph [ Text "Paragraph" ]
--> , BlockQuote
--> [ Heading H3 [ Text "Paragraph" ]
--> ]
--> , Heading H2 [ Text "Second heading" ]
--> ]
import Markdown.Block as Block exposing (..)


[ Paragraph
    [ Link "http://elm-lang.org" Nothing [ Text "elm-lang homepage" ]
    ]
]
    |> List.map
        (Block.walkInlines
            (\inline ->
                case inline of
                    Link destination title inlines ->
                        Link (String.replace "http://" "https://" destination) title inlines
                    _ ->
                        inline
            )
        )
-->        [ Paragraph
-->            [ Link "https://elm-lang.org" Nothing [ Text "elm-lang homepage" ]
-->            ]
-->        ]
(Inline -> Result error Inline)
-> List Block
-> Result (List error) (List Block)

Apply a function to transform each inline recursively. If any of the values are Errs, the entire value will be an Err.

import Markdown.Block as Block exposing (..)


lookupLink : String -> Result String String
lookupLink key =
    case key of
        "elm-lang" ->
            Ok "https://elm-lang.org"
        _ ->
            Err <| "Couldn't find key " ++ key


resolveLinkInInline : Inline -> Result String Inline
resolveLinkInInline inline =
    case inline of
        Link destination title inlines ->
            destination
                |> lookupLink
                |> Result.map (\resolvedLink -> Link resolvedLink title inlines)
        _ ->
            Ok inline


[ Paragraph
    [ Link "angular" Nothing [ Text "elm-lang homepage" ]
    ]
]
    |> Block.validateMapInlines resolveLinkInInline
-->  Err [ "Couldn't find key angular" ]
(soFar -> Block -> ( soFar, mappedValue ))
-> soFar
-> List Block
-> ( soFar, List mappedValue )

Map values, while also tracking state while traversing every block. Think of it as a helper for foldl and map in a single handy function!

In this example, we ne need to keep track of the number of occurrences of a heading name so that we can use a unique slug to link to (exactly like Github does for its heading links). We keep the occurences in a Dict, so this allows us to maintain state rather than just transforming blocks purely based on the current block.

You can see the full end-to-end code for this in examples/src/Slugs.elm.

import Markdown.Block as Block exposing (..)
import Dict
gatherHeadingOccurrences : List Block -> ( Dict.Dict String Int, List ( Block, Maybe String ) )
gatherHeadingOccurrences =
    Block.mapAndAccumulate
        (\soFar block ->
            case block of
                Heading level inlines ->
                    let
                        inlineText : String
                        inlineText =
                            Block.extractInlineText inlines
                        occurenceModifier : String
                        occurenceModifier =
                            soFar
                                |> Dict.get inlineText
                                |> Maybe.map String.fromInt
                                |> Maybe.withDefault ""
                    in
                    ( soFar |> trackOccurence inlineText
                    , ( Heading level inlines, Just (inlineText ++ occurenceModifier) )
                    )
                _ ->
                    ( soFar
                    , ( block, Nothing )
                    )
        )
        Dict.empty
trackOccurence : String -> Dict.Dict String Int -> Dict.Dict String Int
trackOccurence value occurences =
    occurences
        |> Dict.update value
            (\maybeOccurence ->
                case maybeOccurence of
                    Just count ->
                        Just <| count + 1
                    Nothing ->
                        Just 1
            )


[ Heading H1 [ Text "foo" ]
, Heading H1 [ Text "bar" ]
, Heading H1 [ Text "foo" ]
]
|> gatherHeadingOccurrences
--> ( Dict.fromList
-->        [ ( "bar", 1 )
-->        , ( "foo", 2 )
-->        ]
-->    , [ ( Heading H1 [ Text "foo" ], Just "foo" )
-->        , ( Heading H1 [ Text "bar" ], Just "bar" )
-->        , ( Heading H1 [ Text "foo" ], Just "foo1" )
-->        ]
-->    )
foldl : (Block -> acc -> acc) -> acc -> List Block -> acc

Fold over all blocks to yield a value.

import Markdown.Block as Block exposing (..)


maximumHeadingLevel : List Block -> Maybe HeadingLevel
maximumHeadingLevel blocks =
    blocks
        |> Block.foldl
            (\block maxSoFar ->
                case block of
                    Heading level _ ->
                        if Block.headingLevelToInt level > (maxSoFar |> Maybe.map Block.headingLevelToInt |> Maybe.withDefault 0) then
                            Just level
                        else
                            maxSoFar
                    _ ->
                        maxSoFar
            )
            Nothing


[ Heading H1 [ Text "Document" ]
, Heading H2 [ Text "Section A" ]
, Heading H3 [ Text "Subsection" ]
, Heading H2 [ Text "Section B" ]
]
    |> maximumHeadingLevel
-->  (Just H3)