Skip to content

Instantly share code, notes, and snippets.

@dillonkearns
Last active February 10, 2026 18:14
Show Gist options
  • Select an option

  • Save dillonkearns/d4949dc6e99df11abf0b4ce20a94e7ac to your computer and use it in GitHub Desktop.

Select an option

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
{
"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": {}
}
}
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