dillonkearns / elm-markdown / Markdown.Block


type Block
    = HtmlBlock (Html Block)
    | UnorderedList ListSpacing (List (ListItem Block))
    | OrderedList ListSpacing Basics.Int (List (List Block))
    | BlockQuote (List Block)
    | Heading HeadingLevel (List Inline)
    | Paragraph (List Inline)
    | Table (List { label : List Inline, alignment : Maybe Alignment }) (List (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


type HeadingLevel
    = 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.

headingLevelToInt : HeadingLevel -> Basics.Int

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.


type Alignment
    = AlignLeft
    | AlignRight
    | AlignCenter

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


type ListSpacing
    = Loose
    | Tight

Based on the whitespace around lists, markdown will wrap each list item with a paragraph (if it's a Loose list) or it won't (if it's a Tight list).

https://github.github.com/gfm/#lists

A list is loose if any of its constituent list items are separated by blank lines, or if any of its constituent list items directly contain two block-level elements with a blank line between them. Otherwise a list is tight. (The difference in HTML output is that paragraphs in a loose list are wrapped in

tags, while paragraphs in a tight list are not.)

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.

Inlines


type Inline
    = HtmlInline (Html Block)
    | Link String (Maybe String) (List Inline)
    | Image String (Maybe String) (List Inline)
    | Emphasis (List Inline)
    | Strong (List Inline)
    | Strikethrough (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

, you would have { name = "class", value = "foo" }.

extractInlineText : List Inline -> String

Extract the text from a list of inlines.

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

import Markdown.Block as Block exposing (..)

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

Block.extractInlineText inlines
--> "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" ]
--> ]

walkInlines : (Inline -> Inline) -> Block -> Block

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" ]
-->            ]
-->        ]

validateMapInlines : (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" ]

mapAndAccumulate : (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 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)

inlineFoldl : (Inline -> acc -> acc) -> acc -> List Block -> acc

Fold over all inlines within a list of blocks to yield a value.

import Markdown.Block as Block exposing (..)

pullLinks : List Block -> List String
pullLinks blocks =
    blocks
        |> inlineFoldl
            (\inline links ->
                case inline of
                    Link str mbstr moreinlines ->
                        str :: links
                    _ ->
                        links
            )
            []

[ Heading H1 [ Text "Document" ]
, Heading H2 [ Link "/note/50" (Just "interesting document") [] ]
, Heading H3 [ Text "Subsection" ]
, Heading H2 [ Link "/note/51" (Just "more interesting document") [] ]
]
    |> pullLinks
-->  ["/note/51", "/note/50"]