• Stars
    star
    106
  • Rank 325,871 (Top 7 %)
  • Language
    Elm
  • License
    BSD 3-Clause "New...
  • Created about 5 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Extensible markdown parser with custom rendering, in pure Elm.

elm-markdown

All Contributors Build Status Elm package

Extensible markdown parsing in pure elm.

This library extends the basic markdown blocks without actually adding features to the syntax. It simply provides a declarative way to map certain HTML tags to your Elm view functions to render them. For example,

<bio
  name="Dillon Kearns"
  photo="https://avatars2.githubusercontent.com/u/1384166"
  twitter="dillontkearns"
  github="dillonkearns"
>
  Dillon really likes building things with Elm! Here are some links -
  [Articles](https://incrementalelm.com/articles)
</bio>

And you wire up your Elm rendering function like this

Markdown.Html.oneOf
  [ Markdown.Html.tag "bio"
    (\name photoUrl twitter github dribbble renderedChildren ->
      bioView renderedChildren name photoUrl twitter github dribbble
    )
    |> Markdown.Html.withAttribute "name"
    |> Markdown.Html.withAttribute "photo"
    |> Markdown.Html.withOptionalAttribute "twitter"
    |> Markdown.Html.withOptionalAttribute "github"
    |> Markdown.Html.withOptionalAttribute "dribbble"
  ]

Note that it gets the rendered children as an argument. This is rendering the inner contents of the HTML tag using your HTML renderer, so you get all of your rendered lists, code blocks, links, etc. within your tag. You can try a live Ellie demo of this code snippet.

Live Code Demos

Core features

Custom Renderers

You define your own custom renderer, turning your markdown content into any data type with totally customizable logic. You can even pass back an Err to get custom failures (for example, broken links or validations like headings that are too long)!

Here's a snippet from the default HTML renderer that comes built in to give you a sense of how you define a Renderer:

import Html exposing (Html)
import Html.Attributes as Attr
import Markdown.Block as Block exposing (Block)
import Markdown.Html

defaultHtmlRenderer : Renderer (Html msg)
defaultHtmlRenderer =
    { heading =
        \{ level, children } ->
            case level of
                Block.H1 ->
                    Html.h1 [] children

                Block.H2 ->
                    Html.h2 [] children

                Block.H3 ->
                    Html.h3 [] children

                Block.H4 ->
                    Html.h4 [] children

                Block.H5 ->
                    Html.h5 [] children

                Block.H6 ->
                    Html.h6 [] children
    , paragraph = Html.p []
    , hardLineBreak = Html.br [] []
    , blockQuote = Html.blockquote []
    , strong =
        \children -> Html.strong [] children
    , emphasis =
        \children -> Html.em [] children
    , codeSpan =
        \content -> Html.code [] [ Html.text content ]
    , link =
        \link content ->
            case link.title of
                Just title ->
                    Html.a
                        [ Attr.href link.destination
                        , Attr.title title
                        ]
                        content

                Nothing ->
                    Html.a [ Attr.href link.destination ] content
    , image =
        \imageInfo ->
            case imageInfo.title of
                Just title ->
                    Html.img
                        [ Attr.src imageInfo.src
                        , Attr.alt imageInfo.alt
                        , Attr.title title
                        ]
                        []

                Nothing ->
                    Html.img
                        [ Attr.src imageInfo.src
                        , Attr.alt imageInfo.alt
                        ]
                        []
    , text =
        Html.text
    , unorderedList =
        \items ->
            Html.ul []
                (items
                    |> List.map
                        (\item ->
                            case item of
                                Block.ListItem task children ->
                                    let
                                        checkbox =
                                            case task of
                                                Block.NoTask ->
                                                    Html.text ""

                                                Block.IncompleteTask ->
                                                    Html.input
                                                        [ Attr.disabled True
                                                        , Attr.checked False
                                                        , Attr.type_ "checkbox"
                                                        ]
                                                        []

                                                Block.CompletedTask ->
                                                    Html.input
                                                        [ Attr.disabled True
                                                        , Attr.checked True
                                                        , Attr.type_ "checkbox"
                                                        ]
                                                        []
                                    in
                                    Html.li [] (checkbox :: children)
                        )
                )
    , orderedList =
        \startingIndex items ->
            Html.ol
                (case startingIndex of
                    1 ->
                        [ Attr.start startingIndex ]

                    _ ->
                        []
                )
                (items
                    |> List.map
                        (\itemBlocks ->
                            Html.li []
                                itemBlocks
                        )
                )
    , html = Markdown.Html.oneOf []
    , codeBlock =
        \block ->
            Html.pre []
                [ Html.code []
                    [ Html.text block.body
                    ]
                ]
    , thematicBreak = Html.hr [] []
    , table = Html.table []
    , tableHeader = Html.thead []
    , tableBody = Html.tbody []
    , tableRow = Html.tr []
    , tableHeaderCell =
        \maybeAlignment ->
            let
                attrs =
                    maybeAlignment
                        |> Maybe.map
                            (\alignment ->
                                case alignment of
                                    Block.AlignLeft ->
                                        "left"

                                    Block.AlignCenter ->
                                        "center"

                                    Block.AlignRight ->
                                        "right"
                            )
                        |> Maybe.map Attr.align
                        |> Maybe.map List.singleton
                        |> Maybe.withDefault []
            in
            Html.th attrs
    , tableCell = \_ children -> Html.td [] children
    , strikethrough = Html.span [ Attr.style "text-decoration-line" "line-through" ]
    }

Markdown Block Transformation

You get full access to the parsed markdown blocks before passing it to a renderer. That means that you can inspect it, do custom logic on it, perform validations, or even go in and transform it! It's totally customizable, and of course it's all just nice Elm custom types!

Here's a live Ellie example that transforms the AST into a table of contents and renders a TOC data type along with the rendered markdown.

Philosophy & Goals

  • Render markdown to any type (Html, elm-ui Elements, Strings representing ANSI color codes for terminal output... or even a function, allowing you to inject dynamic values into your markdown view)
  • Extend markdown without adding to the syntax using custom HTML renderers, and fail explicitly for unexpected HTML tags, or missing attributes within those tags
  • Allow users to give custom parsing failures with nice error messages (for example, broken links, or custom validation like titles that are too long)

Parsing Goals

This is evolving and I would like input on the direction of parsing. My current thinking is that this library should:

  • Do not add any new syntax, this library has a subset of the features of Github flavored markdown.
  • Only parse the Github-flavored markdown style (not CommonMark or other variants)
  • (This breaks GFM compliance in favor of explicit errors) All markdown is valid in github-flavored markdown and other variants. This library aims to give explicit errors instead of falling back and silently continuing, see example below
  • Only deviate from Github-flavored markdown rules when it helps give better error feedback for "things you probably didn't mean to do." In all other cases, follow the Github-flavored markdown spec.

Current Github-flavored markdown compliance

The test suite for this library runs through all the expected outputs outlined in the GFM spec. It uses the same test suite to test these cases as highlight.js (the library that elm-explorations/elm-markdown uses under the hood).

You can see the latest passing and failing tests from this test suite in the test-results folder (in particular, take a look at the Github-Flavored Markdown failures in in failing/GFM.

Examples of fallback behavior

Github flavored markdown behavior: Links with missing closing parens are are rendered as raw text instead of links

[My link](/home/ wait I forgot to close the link

Renders the raw string instead of a link, like so:

<p>
  [My link](/home/ wait I forgot to close the link
</p>

This library gives an error message here, and aims to do so in similar situations.

Contributors

A huge thanks to Pablo Hirafuji, who was kind enough to allow me to use his InlineParser in this project. It turns out that Markdown inline parsing is a very specialized algorithm, and the elm/parser library isn't suited to solve that particular problem.


Stephen Reddekopp

⚠️ 💻

thomasin

⚠️ 💻

Brian Ginsburg

⚠️ 💻

Philipp Krüger

💻

Folkert de Vries

💻

Thank you @jinjor for your elm-xml-parser package!

I needed to tweak it so I copied it into the project, but it is one of the dependencies and it worked without a hitch!

More Repositories

1

elm-graphql

Autogenerate type-safe GraphQL queries in Elm.
Elm
778
star
2

elm-pages

Hybrid Elm framework with full-stack and static routes.
Elm
655
star
3

mobster

Pair and mob programming timer for Mac, Windows, and Linux.
Elm
308
star
4

elm-typescript-interop

Generate TypeScript declaration files for your elm ports!
Elm
165
star
5

elm-review-html-to-elm

Turn HTML into Elm. With support for elm-tailwind-modules.
Elm
96
star
6

elm-pages-starter

Starter blog for elm-pages
Elm
95
star
7

idiomatic-elm-package-guide

Everything you need to know to publish a useful, idiomatic Elm package!
67
star
8

elm-cli-options-parser

Build type-safe command-line utilities in Elm!
Elm
54
star
9

elm-ts-json

Elm
42
star
10

elm-typescript-starter

Boilerplate for Elm web apps with safe TypeScript interop and hot module replacement.
JavaScript
35
star
11

elm-electron-starter

Build native cross-platform desktop apps in Elm
TypeScript
35
star
12

talks

Elm
23
star
13

elm-pages-v3-beta

Elm
22
star
14

elm-electron

Type-safe interprocess communication for Electron apps built with Elm.
Elm
19
star
15

elm-publish-action

TypeScript
19
star
16

elm-form

Standalone version of the elm-pages Form API.
Elm
19
star
17

elm-package-starter

Starter template for an Elm package.
Elm
13
star
18

graphqelm-demo

Demo package to support Ellie examples of Graphqelm.
Elixir
11
star
19

elm-pages-3-alpha-starter

Elm
9
star
20

elm-decoder-koans

Learn about elm decoders by filling in the blanks in test cases.
Elm
8
star
21

elm-radio.com

Elm
7
star
22

elm-pages-tailwind-starter

Elm
7
star
23

elm-ts-interop-starter

Elm
7
star
24

elm-rss

Generate RSS feeds with Elm.
Elm
7
star
25

elm-pages-realworld

Realworld implementation with elm-pages v3.
Elm
6
star
26

elm-ical

Elm
5
star
27

gitbook-elm-graphql

5
star
28

incrementalelm.com

Elm
4
star
29

elm-katas

Placeholder for elm exercises
Elm
4
star
30

graphqelm

This package has been moved to dillonkearns/elm-graphql
Elm
4
star
31

sb-farmar

Elm
4
star
32

atom-vim-mode-plus-exchange

Exchange two text areas in vim-mode-plus
JavaScript
4
star
33

automated-testing-wiki

Automated Testing Wiki - A Community Guide to Effective TDD
3
star
34

legit

A collection of scripts for common git tasks to simplify and improve workflow.
Ruby
3
star
35

elm-view-transitions

A proof of concept of the View Transitions API in Elm.
Elm
3
star
36

elm-snapshot-test

JavaScript
3
star
37

prisma-example

Elm
3
star
38

elm-program-test-katas

Elm
2
star
39

fishbowl

Elm
2
star
40

ellie-app-cli

JavaScript
2
star
41

elm-pages-init

Elm
2
star
42

elm-form-mdl

elm-mdl helpers for the elm-forms library
Elm
2
star
43

Test-Runner

Python
2
star
44

the_lean_cafe

Elixir
2
star
45

chess-vision

Elm
1
star
46

elm-koan-runner

Elm
1
star
47

elm-pages-blog-tutorial

Elm
1
star
48

elm-oembed

Elm
1
star
49

elm-ts-netlify-starter

JavaScript
1
star
50

advent-posts

1
star
51

elm-sitemap

Generate sitemaps in elm.
Elm
1
star
52

elm-program-test-experiment

Elm
1
star
53

dotfile-linker

Ruby
1
star
54

we-connect

Elm
1
star
55

elm-ts-interop.com

Elm
1
star
56

elm-pages-netlify-cms-starter

Starter kit for elm-pages and Netlify CMS.
Elm
1
star