Last active
February 10, 2026 18:14
-
-
Save dillonkearns/d4949dc6e99df11abf0b4ce20a94e7ac to your computer and use it in GitHub Desktop.
elm-pages script: Render Markdown to ANSI terminal output using dillonkearns/elm-markdown and wolfadex/elm-ansi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "type": "application", | |
| "source-directories": [ | |
| "." | |
| ], | |
| "elm-version": "0.19.1", | |
| "dependencies": { | |
| "direct": { | |
| "dillonkearns/elm-cli-options-parser": "3.2.0", | |
| "dillonkearns/elm-markdown": "7.0.1", | |
| "dillonkearns/elm-pages": "10.3.0", | |
| "elm/core": "1.0.5", | |
| "elm/json": "1.1.4", | |
| "pablohirafuji/elm-syntax-highlight": "3.7.1", | |
| "wolfadex/elm-ansi": "3.0.1" | |
| }, | |
| "indirect": { | |
| "Chadtech/elm-bool-extra": "2.4.2", | |
| "avh4/elm-color": "1.0.0", | |
| "danfishgold/base64-bytes": "1.1.0", | |
| "danyx23/elm-mimetype": "4.0.1", | |
| "dillonkearns/elm-bcp47-language-tag": "2.0.0", | |
| "dillonkearns/elm-date-or-date-time": "2.0.0", | |
| "dillonkearns/elm-form": "3.0.1", | |
| "elm/browser": "1.0.2", | |
| "elm/bytes": "1.0.8", | |
| "elm/file": "1.0.5", | |
| "elm/html": "1.0.1", | |
| "elm/http": "2.0.0", | |
| "elm/parser": "1.1.0", | |
| "elm/random": "1.0.0", | |
| "elm/regex": "1.0.0", | |
| "elm/time": "1.0.0", | |
| "elm/url": "1.0.0", | |
| "elm/virtual-dom": "1.0.5", | |
| "elm-community/basics-extra": "4.1.0", | |
| "elm-community/list-extra": "8.7.0", | |
| "elm-community/maybe-extra": "5.3.0", | |
| "fredcy/elm-parseint": "2.0.1", | |
| "jluckyiv/elm-utc-date-strings": "1.0.0", | |
| "justinmimbs/date": "4.1.0", | |
| "mdgriffith/elm-codegen": "6.0.1", | |
| "miniBill/elm-codec": "2.3.0", | |
| "miniBill/elm-unicode": "1.1.1", | |
| "noahzgordon/elm-color-extra": "1.0.2", | |
| "robinheghan/fnv1a": "1.0.0", | |
| "robinheghan/murmur3": "1.0.0", | |
| "rtfeldman/elm-css": "18.0.0", | |
| "rtfeldman/elm-hex": "1.0.0", | |
| "rtfeldman/elm-iso8601-date-strings": "1.1.4", | |
| "stil4m/elm-syntax": "7.3.9", | |
| "stil4m/structured-writer": "1.0.3", | |
| "the-sett/elm-pretty-printer": "3.3.0", | |
| "the-sett/elm-syntax-dsl": "6.0.3" | |
| } | |
| }, | |
| "test-dependencies": { | |
| "direct": {}, | |
| "indirect": {} | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Main exposing (run) | |
| import Ansi.Color | |
| import Ansi.Font | |
| import BackendTask exposing (BackendTask) | |
| import BackendTask.File as File | |
| import Cli.Option as Option | |
| import Cli.OptionsParser as OptionsParser | |
| import Cli.Program as Program | |
| import FatalError exposing (FatalError) | |
| import Markdown.Block exposing (ListItem(..), Task(..)) | |
| import Markdown.Html | |
| import Markdown.Parser | |
| import Markdown.Renderer exposing (Renderer) | |
| import Pages.Script as Script exposing (Script) | |
| import SyntaxHighlight | |
| run : Script | |
| run = | |
| Script.withCliOptions program | |
| (\{ file } -> | |
| File.rawFile file | |
| |> BackendTask.allowFatal | |
| |> BackendTask.andThen renderMarkdown | |
| ) | |
| renderMarkdown : String -> BackendTask FatalError () | |
| renderMarkdown rawMarkdown = | |
| case | |
| rawMarkdown | |
| |> Markdown.Parser.parse | |
| |> Result.mapError (\errors -> List.map Markdown.Parser.deadEndToString errors |> String.join "\n") | |
| |> Result.andThen (Markdown.Renderer.render ansiRenderer) | |
| of | |
| Ok rendered -> | |
| Script.log (String.join "\n" rendered) | |
| Err error -> | |
| Script.log ("Error rendering markdown:\n" ++ error) | |
| type alias CliOptions = | |
| { file : String | |
| } | |
| program : Program.Config CliOptions | |
| program = | |
| Program.config | |
| |> Program.add | |
| (OptionsParser.build CliOptions | |
| |> OptionsParser.with | |
| (Option.requiredPositionalArg "file") | |
| ) | |
| -- ANSI Renderer | |
| ansiRenderer : Renderer String | |
| ansiRenderer = | |
| { heading = heading | |
| , paragraph = \children -> String.join "" children ++ "\n" | |
| , blockQuote = blockQuote | |
| , html = Markdown.Html.oneOf [] | |
| , text = identity | |
| , codeSpan = \code -> Ansi.Color.fontColor Ansi.Color.cyan code | |
| , strong = \children -> Ansi.Font.bold (String.join "" children) | |
| , emphasis = \children -> Ansi.Font.italic (String.join "" children) | |
| , strikethrough = \children -> Ansi.Font.strikeThrough (String.join "" children) | |
| , hardLineBreak = "\n" | |
| , link = link | |
| , image = image | |
| , unorderedList = unorderedList | |
| , orderedList = orderedList | |
| , codeBlock = codeBlock | |
| , thematicBreak = thematicBreak | |
| , table = \children -> String.join "" children ++ "\n" | |
| , tableHeader = \children -> String.join "" children ++ "\n" | |
| , tableBody = \children -> String.join "" children | |
| , tableRow = \children -> "| " ++ String.join " | " children ++ " |\n" | |
| , tableCell = \_ children -> String.join "" children | |
| , tableHeaderCell = \_ children -> Ansi.Font.bold (String.join "" children) | |
| } | |
| heading : { level : Markdown.Block.HeadingLevel, rawText : String, children : List String } -> String | |
| heading { level, children } = | |
| let | |
| prefix = | |
| case level of | |
| Markdown.Block.H1 -> | |
| "# " | |
| Markdown.Block.H2 -> | |
| "## " | |
| Markdown.Block.H3 -> | |
| "### " | |
| Markdown.Block.H4 -> | |
| "#### " | |
| Markdown.Block.H5 -> | |
| "##### " | |
| Markdown.Block.H6 -> | |
| "###### " | |
| content = | |
| String.join "" children | |
| in | |
| "\n" ++ Ansi.Font.bold (Ansi.Color.fontColor Ansi.Color.magenta (prefix ++ content)) ++ "\n" | |
| blockQuote : List String -> String | |
| blockQuote children = | |
| children | |
| |> String.join "" | |
| |> String.lines | |
| |> List.map (\line -> Ansi.Color.fontColor Ansi.Color.brightBlack (" > " ++ line)) | |
| |> String.join "\n" | |
| |> (\s -> s ++ "\n") | |
| link : { title : Maybe String, destination : String } -> List String -> String | |
| link { destination } children = | |
| let | |
| label = | |
| String.join "" children | |
| in | |
| Ansi.Font.underline (Ansi.Color.fontColor Ansi.Color.blue label) | |
| ++ " (" | |
| ++ Ansi.Color.fontColor Ansi.Color.brightBlack destination | |
| ++ ")" | |
| image : { alt : String, src : String, title : Maybe String } -> String | |
| image { alt, src } = | |
| Ansi.Color.fontColor Ansi.Color.yellow ("[image: " ++ alt ++ "]") | |
| ++ " (" | |
| ++ Ansi.Color.fontColor Ansi.Color.brightBlack src | |
| ++ ")" | |
| unorderedList : List (ListItem String) -> String | |
| unorderedList items = | |
| items | |
| |> List.map | |
| (\(ListItem task children) -> | |
| let | |
| bullet = | |
| case task of | |
| NoTask -> | |
| Ansi.Color.fontColor Ansi.Color.green " * " | |
| IncompleteTask -> | |
| Ansi.Color.fontColor Ansi.Color.yellow " [ ] " | |
| CompletedTask -> | |
| Ansi.Color.fontColor Ansi.Color.green " [x] " | |
| in | |
| bullet ++ String.join "" children | |
| ) | |
| |> String.join "\n" | |
| |> (\s -> s ++ "\n") | |
| orderedList : Int -> List (List String) -> String | |
| orderedList startIndex items = | |
| items | |
| |> List.indexedMap | |
| (\i children -> | |
| let | |
| num = | |
| String.fromInt (startIndex + i) | |
| in | |
| " " | |
| ++ Ansi.Color.fontColor Ansi.Color.green (num ++ ". ") | |
| ++ String.join "" children | |
| ) | |
| |> String.join "\n" | |
| |> (\s -> s ++ "\n") | |
| codeBlock : { body : String, language : Maybe String } -> String | |
| codeBlock { body, language } = | |
| let | |
| header = | |
| case language of | |
| Just lang -> | |
| Ansi.Color.fontColor Ansi.Color.brightBlack (" ``` " ++ lang) ++ "\n" | |
| Nothing -> | |
| Ansi.Color.fontColor Ansi.Color.brightBlack " ```" ++ "\n" | |
| footer = | |
| Ansi.Color.fontColor Ansi.Color.brightBlack " ```" | |
| highlightedBody = | |
| highlightCode language body | |
| in | |
| header ++ highlightedBody ++ "\n" ++ footer ++ "\n" | |
| highlightCode : Maybe String -> String -> String | |
| highlightCode language body = | |
| let | |
| trimmedBody = | |
| String.trimRight body | |
| parser = | |
| case language of | |
| Just "elm" -> | |
| Just SyntaxHighlight.elm | |
| Just "javascript" -> | |
| Just SyntaxHighlight.javascript | |
| Just "js" -> | |
| Just SyntaxHighlight.javascript | |
| Just "json" -> | |
| Just SyntaxHighlight.json | |
| Just "css" -> | |
| Just SyntaxHighlight.css | |
| Just "xml" -> | |
| Just SyntaxHighlight.xml | |
| Just "html" -> | |
| Just SyntaxHighlight.xml | |
| Just "python" -> | |
| Just SyntaxHighlight.python | |
| Just "py" -> | |
| Just SyntaxHighlight.python | |
| Just "sql" -> | |
| Just SyntaxHighlight.sql | |
| Just "go" -> | |
| Just SyntaxHighlight.go | |
| Just "nix" -> | |
| Just SyntaxHighlight.nix | |
| Just "kotlin" -> | |
| Just SyntaxHighlight.kotlin | |
| _ -> | |
| Nothing | |
| fallback = | |
| trimmedBody | |
| |> String.lines | |
| |> List.map (\line -> " " ++ Ansi.Color.fontColor Ansi.Color.yellow line) | |
| |> String.join "\n" | |
| in | |
| case parser of | |
| Just parse -> | |
| case parse trimmedBody of | |
| Ok hcode -> | |
| hcode | |
| |> SyntaxHighlight.toConsole consoleOptions | |
| |> List.map (\line -> " " ++ String.replace "\n" "" line) | |
| |> String.join "\n" | |
| Err _ -> | |
| fallback | |
| Nothing -> | |
| fallback | |
| consoleOptions : SyntaxHighlight.ConsoleOptions | |
| consoleOptions = | |
| { default = Ansi.Color.fontColor Ansi.Color.white | |
| , highlight = Ansi.Color.backgroundColor Ansi.Color.brightBlack | |
| , addition = Ansi.Color.fontColor Ansi.Color.green | |
| , deletion = Ansi.Color.fontColor Ansi.Color.red | |
| , comment = Ansi.Color.fontColor Ansi.Color.brightBlack | |
| , style1 = Ansi.Color.fontColor Ansi.Color.cyan -- numbers | |
| , style2 = Ansi.Color.fontColor Ansi.Color.green -- strings | |
| , style3 = Ansi.Color.fontColor Ansi.Color.magenta -- keywords, tags | |
| , style4 = Ansi.Color.fontColor Ansi.Color.yellow -- group symbols | |
| , style5 = Ansi.Color.fontColor Ansi.Color.blue -- functions | |
| , style6 = Ansi.Color.fontColor Ansi.Color.red -- literal keywords | |
| , style7 = Ansi.Color.fontColor Ansi.Color.brightCyan -- arguments | |
| } | |
| thematicBreak : String | |
| thematicBreak = | |
| Ansi.Color.fontColor Ansi.Color.brightBlack (String.repeat 40 "─") ++ "\n" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment