{"id":15285969,"url":"https://github.com/pablohirafuji/elm-markdown","last_synced_at":"2026-03-16T05:02:40.406Z","repository":{"id":62418877,"uuid":"78849092","full_name":"pablohirafuji/elm-markdown","owner":"pablohirafuji","description":"Pure Elm markdown parsing and rendering","archived":false,"fork":false,"pushed_at":"2025-03-19T18:18:55.000Z","size":583,"stargazers_count":100,"open_issues_count":5,"forks_count":8,"subscribers_count":8,"default_branch":"master","last_synced_at":"2026-03-15T08:07:43.298Z","etag":null,"topics":["ast","commonmark","elm","markdown","parser","renderer"],"latest_commit_sha":null,"homepage":"http://package.elm-lang.org/packages/pablohirafuji/elm-markdown/latest","language":"Elm","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pablohirafuji.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-01-13T12:53:19.000Z","updated_at":"2026-03-11T21:25:45.000Z","dependencies_parsed_at":"2025-04-13T02:41:41.550Z","dependency_job_id":"d29c61bb-4217-4536-b089-2fa9ab445f06","html_url":"https://github.com/pablohirafuji/elm-markdown","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/pablohirafuji/elm-markdown","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pablohirafuji%2Felm-markdown","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pablohirafuji%2Felm-markdown/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pablohirafuji%2Felm-markdown/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pablohirafuji%2Felm-markdown/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pablohirafuji","download_url":"https://codeload.github.com/pablohirafuji/elm-markdown/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pablohirafuji%2Felm-markdown/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30556885,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-15T23:30:23.986Z","status":"ssl_error","status_checked_at":"2026-03-15T23:28:43.564Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","commonmark","elm","markdown","parser","renderer"],"created_at":"2024-09-30T15:08:51.573Z","updated_at":"2026-03-16T05:02:40.378Z","avatar_url":"https://github.com/pablohirafuji.png","language":"Elm","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Elm Markdown\n\nPure Elm markdown parsing and rendering.\n\nBased on the latest [CommonMark Spec](http://spec.commonmark.org/0.27/), with [some differences](#differences-from-commonmark).\n[Demo](https://pablohirafuji.github.io/elm-markdown/).\n\n- [Basic Usage](#basic-usage)\n- [Supported Syntax](#supported-syntax)\n  - [Heading](#heading)\n  - [Quoting](#quoting)\n  - [Code](#code)\n  - [Link](#link)\n  - [Lists](#lists)\n  - [Paragraphs and line breaks](#paragraphs-and-line-breaks)\n  - [Thematic Break Line](#thematic-break-line)\n  - [Emphasis](#emphasis)\n  - [Image](#image)\n- [Differences from CommonMark](#differences-from-commonmark)\n- [Options](#options)\n- [Customization](#customization)\n- [Performance](#performance)\n- [Advanced Usage](#advanced-usage)\n  - [Implementing GFM Task List](#implementing-gfm-task-list)\n- [Changelog](#changelog)\n\n## Basic Usage\n\n\n```elm\nimport Markdown\n\n\nview : Html msg\nview =\n    div []\n        \u003c| Markdown.toHtml Nothing \"# Heading with *emphasis*\"\n```\n\n\n\n## Supported Syntax\n\n\n\n### Heading\n\nTo create a heading, add one to six `#` symbols before\nyour heading text. The number of `#` you use will determine\nthe size of the heading.\n\n    # Heading 1\n    ## Heading 2\n    ###### Heading 6\n\nYou can also use `=` or `-` after a paragraph for level 1 or 2 heading.\n\n    Heading 1\n    ==========\n\n    Heading 2\n    ----------\n\n\n\n### Quoting\n\nLines starting with a `\u003e` will be a block quote.\n\n\n    \u003e Block quote\n\n\n\n### Code\n\nUse a sequence of single backticks (`` ` ``) to output code.\nThe text within the backticks will not be formatted.\nTo format code or text into its own distinct block, use\nat least three backticks or four spaces or tab.\n\n\n    Example of `inline code`\n    \n        Example of block code\n\n    ```optionalLang\n    Example of block code with defined language\n    ```\n\nIf the language in the fenced code block is defined,\nit will be added a `class=\"language-optionalLang\"` to\nthe code element.\n\n\n\n### Link\n\nYou can create an inline link by wrapping link text in\nbrackets `[ ]`, and then wrapping the URL in parentheses `( )`, with a optional title using single quotes, double quotes or parentheses.\n\n    Do you know the Elm [slack channel](https://elmlang.slack.com/ \"title\")?\n\nOr create a reference link:\n\n    [slackLink]: https://elmlang.slack.com/ 'title'\n\n    Do you know the Elm [slack channel][slackLink]?\n\nOr even:\n\n    [slack channel]: https://elmlang.slack.com/ (title)\n\n    Do you know the Elm [slack channel]?\n\nAll examples output the same html.\n\nAutolinks and emails are supported with `\u003c \u003e`:\n\n    Autolink: \u003chttp://elm-lang.org/\u003e\n    Email link: \u003cgoogle@google.com\u003e\n\n\n\n### Lists\n\nYou can make a list by preceding one or more lines of\ntext with `-` or `*`.\n\n\n    - Unordered list\n      * Nested unordered list\n    5. Ordered list starting at 5\n        1) Nested ordered list starting at 1\n\n\n\n### Paragraphs and line breaks\n\nYou can create a new paragraph by leaving a blank line\nbetween lines of text.\n\n\n    Here's a paragraph with soft break line\n    at the end.\n\n    Here's a paragraph with hard break line\\\n    at the end.\n\n\nBy default, soft line break (`\\n`) will be rendered as it is,\nunless it's preceded by two spaces or `\\`, which will output\nhard break line (`\u003cbr\u003e`).\n\nYou can customize to always render soft line breaks as hard\nline breaks setting `softAsHardLineBreak = True` in the options.\n\n\n### Thematic Break Line\n\nYou can create a thematic break line with a sequence of three\nor more matching `-`, `_`, or `*` characters.\n\n\n    ***\n    ---\n    ___\n\n\n\n### Emphasis\n\nYou can create emphasis using the `*` or `_` characters.\nDouble emphasis is strong emphasis.\n\n\n    *Emphasis*, **strong emphasis**, ***both***\n\n    _Emphasis_, __strong emphasis__, ___both___\n\n\n\n### Image\n\nYou can insert images using the following syntax:\n\n\n    ![alt text](src-url \"title\")\n\n\nFor more information about supported syntax and parsing rules, see [CommonMark Spec](http://spec.commonmark.org/0.27/).\n\n\n\n## Differences from CommonMark\n\n- No html entity encoding support, as Elm does not need it (e.g.: `\u003c` to `\u0026lt;`, `\u003e` to `\u0026gt;`);\n- Limited html entity decoding support, due to compiler issues with large Dicts;\n- No comment tag support (`\u003c!-- --\u003e`);\n- No CDATA tag support (`\u003c![CDATA[ ]]\u003e`);\n- No processing instruction tag support (`\u003c? ?\u003e`);\n- No declaration tag support (`\u003c! \u003e`);\n- No [malformed](http://spec.commonmark.org/0.27/#example-122) html tag support (e.g.: `\u003cdiv class`);\n- No balanced parenthesis in inline link's url support (e.g.: `[link](url() \"title\")`, use `[link](\u003curl()\u003e \"title\")` instead);\n- To create a HTML block, wich is not surrounded by paragraph tag (`\u003cp\u003e`), start and finish a paragraph with the html tag you want the HTML block to be, with no blankline between the start and end tag. E.g.:\n\n        First paragraph.\n\n        \u003ctable\u003e\n            \u003ctr\u003e\n                \u003ctd\u003e\n                    Table element\n                \u003c/td\u003e\n            \u003c/tr\u003e\n        \u003c/table\u003e\n\n        Next paragraph.\n\n\n\n\n## Options\n\nUse `Markdown.toHtml` to specify parsing options:\n\n```elm\nimport Markdown\nimport Markdown.Config exposing (Options, defaultOptions)\n\n\ncustomOptions : Options\ncustomOptions =\n    { defaultOptions\n        | softAsHardLineBreak = True\n    }\n\n\nview : Html msg\nview =\n    div []\n        \u003c| Markdown.toHtml (Just customOptions)\n        \u003c| \"# Heading with *emphasis*\"\n```\n\nThe following options are available:\n\n\n```elm\ntype alias Options =\n    { softAsHardLineBreak : Bool\n    , rawHtml : HtmlOption\n    }\n\n\ntype HtmlOption\n    = ParseUnsafe\n    | Sanitize SanitizeOptions\n    | DontParse\n\n\ntype alias SanitizeOptions =\n    { allowedHtmlElements : List String\n    , allowedHtmlAttributes : List String\n    }\n```\n\n- `softAsHardLineBreak`: Default `False`. If set to `True`, will render `\\n` as `\u003cbr\u003e`.\n- `rawHtml`: Default `Sanitize defaultSanitizeOptions`.\nYou can choose to not parse any html tags (`DontParse`), parse any html tag without any sanitization (`ParseUnsafe`) or parse only specific html elements and attributes (`Sanitize SanitizeOptions`).\n\n\nDefault allowed elements and attributes:\n\n```elm\ndefaultSanitizeOptions : SanitizeOptions\ndefaultSanitizeOptions =\n    { allowedHtmlElements =\n        [ \"address\", \"article\", \"aside\", \"b\", \"blockquote\", \"br\"\n        , \"caption\", \"center\", \"cite\", \"code\", \"col\", \"colgroup\"\n        , \"dd\", \"details\", \"div\", \"dl\", \"dt\", \"figcaption\", \"figure\"\n        , \"footer\", \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"hr\", \"i\"\n        , \"legend\", \"li\", \"menu\", \"menuitem\", \"nav\", \"ol\", \"optgroup\"\n        , \"option\", \"p\", \"pre\", \"section\", \"strike\", \"summary\"\n        , \"small\", \"table\", \"tbody\", \"td\", \"tfoot\", \"th\", \"thead\"\n        , \"tr\", \"ul\" ]\n    , allowedHtmlAttributes =\n        [ \"name\", \"class\" ]\n    }\n```\n\n\u003e **Note:** Only basic sanitization is provided.\nIf you are receiving user submitted content, you should use a specific library to sanitize user input.\n\n\n\n## Customization\n\nYou can customize how each markdown element is rendered by\nfirst parsing the markdown string into blocks, then mapping the resulting blocks through a custom renderer, created with the help of `Blocks.defaultHtml` and/or `Inline.defaultHtml`, then concatenate the resulting list.\n\nExample of rendering:\n- All blockquotes as a detail element;\n- Images using figure and figcaption;\n- Links not starting with `http://elm-lang.org` with a `target=\"_blank\"` attribute.\n\n```elm\nimport Html exposing (..)\nimport Html.Attributes exposing (..)\nimport Markdown.Block as Block exposing (Block(..))\nimport Markdown.Inline as Inline exposing (Inline(..))\n\n\nview : Html msg\nview =\n    myMarkdownString\n        |\u003e Block.parse Nothing -- using Config.defaultOptions\n        |\u003e List.map (customHtmlBlock)\n        |\u003e List.concat\n        |\u003e article []\n\n\ncustomHtmlBlock : Block b i -\u003e List (Html msg)\ncustomHtmlBlock block =\n    case block of\n        BlockQuote blocks -\u003e\n            List.map customHtmlBlock blocks\n                |\u003e List.concat\n                |\u003e details []\n                |\u003e flip (::) []\n\n\n        _ -\u003e\n            Block.defaultHtml\n                (Just customHtmlBlock)\n                (Just customHtmlInline)\n                block\n\n\ncustomHtmlInline : Inline i -\u003e Html msg\ncustomHtmlInline inline =\n    case inline of\n        Image url maybeTitle inlines -\u003e\n            figure []\n                [ img\n                    [ alt (Inline.extractText inlines)\n                    , src url\n                    , title (Maybe.withDefault \"\" maybeTitle)\n                    ] []\n                , figcaption []\n                    [ text (Inline.extractText inlines) ]\n                ]\n\n\n        Link url maybeTitle inlines -\u003e\n            if String.startsWith \"http://elm-lang.org\" url then\n                a [ href url\n                  , title (Maybe.withDefault \"\" maybeTitle)\n                  ] (List.map customHtmlInline inlines)\n\n            else\n                a [ href url\n                  , title (Maybe.withDefault \"\" maybeTitle)\n                  , target \"_blank\"\n                  , rel \"noopener noreferrer\"\n                  ] (List.map customHtmlInline inlines)\n\n\n        _ -\u003e\n            Inline.defaultHtml (Just customHtmlInline) inline\n```\n\n\n## Performance\n\nParsing a 1.2MB (~30k lines) markdown text file to html in my notebook using node:\n\n- [Marked](https://github.com/chjj/marked): ~130ms\n- [CommonMark JS](https://github.com/jgm/commonmark.js): ~250ms\n- This package: ~1150ms\n\n\n## Advanced Usage\n\nGuides to help advanced usage.\n\n### Implementing GFM Task List\n\nLet's implement a [Task List](https://github.github.com/gfm/#task-list-items-extension-), like the GitHub Flavored Markdown\nhas.\n\nFirst, let's create a type for our task:\n\n```elm\ntype GFMInline\n    = Task Bool\n```\n\nWhere `Bool` is where we will store the information about the\n`Task` state, i.e. if is checked or not.\n\nAccording to the [GFM Spec](https://github.github.com/gfm/#task-list-items-extension-)\na task list item occurs only in lists and is the first thing in the\nlist item.\n\nThe `Block.walk` function seens a perfect match for our use case!\nIt will walk the given block and apply a function to every inner\n`Block`, if is a container block, and the `Block` itself.\n\n\nThe `Block.walk` function has this signature:\n`(Block b i -\u003e Block b i) -\u003e Block b i -\u003e Block b i`. The first argument is a function that receives and return a `Block`. So let's\ncreate that function:\n\n```elm\nparseTaskList : Block b GFMInline -\u003e Block b GFMInline\nparseTaskList block =\n    case block of\n        Block.List listBlock items -\u003e\n            List.map parseTaskListItem items\n                |\u003e Block.List listBlock\n\n        _ -\u003e\n            block\n```\n\nNote the function signature: we replaced the `i` in\n`Block b i` for `GFMInline`, the type we created ealier.\n\nThe function will match any `List` block type and map over\nit's items, witch type is `List (List (Block b i))`, applying\nthe `parseTaskListItem` function to each item.\n\nNow let's create the `parseTaskListItem` function:\n\n```elm\nparseTaskListItem : List (Block b GFMInline) -\u003e List (Block b GFMInline)\nparseTaskListItem item =\n    case item of\n        Block.Paragraph rawText (Inline.Text text :: inlinesTail)\n            :: tail -\u003e\n                parseTaskListText text ++ inlinesTail\n                    |\u003e Block.Paragraph rawText\n                    |\u003e flip (::) tail\n\n        Block.PlainInlines (Inline.Text text :: inlinesTail)\n            :: tail -\u003e\n                parseTaskListText text ++ inlinesTail\n                    |\u003e Block.PlainInlines\n                    |\u003e flip (::) tail\n\n        _ -\u003e\n            item\n\n\nparseTaskListText : String -\u003e List (Inline GFMInline)\nparseTaskListText text =\n    if String.startsWith \"[x]\" text then\n        [ Inline.Custom (Task True) []\n        , String.dropLeft 3 text\n            |\u003e String.trimLeft\n            |\u003e Inline.Text\n        ]\n\n    else if String.startsWith \"[ ]\" text then\n        [ Inline.Custom (Task False) []\n        , String.dropLeft 3 text\n            |\u003e String.trimLeft\n            |\u003e Inline.Text\n        ]\n\n    else\n        [ Inline.Text text ]\n```\n\nOur `parseTaskListItem` function will match any `Paragraph` or\n`PlainInlines` type (a list can be loose or tight), extracting\nthe interesting parts (thanks to pattern matching!) that we use\nin the `parseTaskListText` function. Then we check the text for\nvalid Task List and return the appropriate result.\n\n\nNow it's view time! Let's create our `Task`'s view:\n\n```elm\ngfmInlineView : Inline GFMInline -\u003e Html msg\ngfmInlineView inline =\n    case inline of\n        Inline.Custom (Task isChecked) _ -\u003e\n            Html.input\n                [ Html.Attributes.disabled True\n                , Html.Attributes.checked isChecked\n                , Html.Attributes.type_ \"checkbox\"\n                ] []\n\n        _ -\u003e\n            Inline.defaultHtml (Just gfmInlineView) inline\n```\n\nAnd we finish gluing everything together:\n\n```elm\ngfmToHtml : String -\u003e Html msg\ngfmToHtml str =\n    str\n        |\u003e Block.parse Nothing -- using Config.defaultOptions\n        |\u003e List.map (Block.walk parseTaskList)\n        |\u003e List.concatMap gfmBlockView\n        |\u003e Html.div []\n\n\ngfmBlockView : Block b GFMInline -\u003e List (Html msg)\ngfmBlockView block =\n    Block.defaultHtml\n        Nothing -- using Block.defaultHtml to render the inner blocks\n        (Just gfmInlineView)\n        block\n```\n\nThat's it! We implemented a fully functional GFM Task List!\n\n\n## Thanks\n\nThank you John MacFarlane, for creating [CommonMark](http://commonmark.org/) specification and tests.\n\nThank you everyone who gave feedback. Special thanks to Jan Tojnar, for discussing about the API.\n\nThank you Evan for bringing joy to the frontend.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpablohirafuji%2Felm-markdown","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpablohirafuji%2Felm-markdown","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpablohirafuji%2Felm-markdown/lists"}