(This is called recursion-schemes in other languages, but don't worry, you don't have to write recursive functions (this is the point of all of this ;) )!)
This is module provides a more complicated, but also more powerful and
composable way of rendering markdown than the built-in elm-markdown
Renderer.
If you feel a little overwhelmed with this module at first, I recommend taking a look at the What are reducers? section.
Transform each child of a Block using the given function.
For example, we can transform the lists of words inside each block into concatenated Strings:
wordsToWordlist : Block (List String) -> Block String
wordsToWordlist block =
map (\listOfWords -> String.join ", " listOfWords)
block
Paragraph
[ [ "This", "paragraph", "was", "full", "of", "individual", "words", "once." ]
, [ "It", "also", "contained", "another", "paragraph" ]
]
|> wordsToWordlist
--> Paragraph
--> [ "This, paragraph, was, full, of, individual, words, once."
--> , "It, also, contained, another, paragraph"
--> ]
HardLineBreak |> wordsToWordlist
--> HardLineBreakThe ability to define this function is one of the reasons for our Block definition. If
you try defining map for elm-markdown's Renderer you'll find out it doesn't work.
Block's children are mapped from 0 to n (if n+1 is the amount of children).
Most arguments to the mapping function are therefore [0], [1], ... etc.
All children will get unique List Int arguments.
In some cases like lists, there might be two levels of indices: [0,0], or [1,0].
In these cases, the first integer is the 'closest' index from the point of view of the child.
OrderedList
{ startingIndex = 0
, items =
[ [ (), () ]
, [ (), (), () ]
]
}
|> indexedMap (\indices _ -> indices)
--> OrderedList
--> { startingIndex = 0
--> , items =
--> [ [ [ 0, 0 ], [ 1, 0 ] ]
--> , [ [ 0, 1 ], [ 1, 1 ], [ 2, 1 ] ]
--> ]
--> }These functions are not as composable as transformation building blocks, but might suffice for your use case. Take a look at the other section if you find you need something better.
Use this function if you want to parameterize your view by an environment.
Another way of thinking about this use-case is: use this if you want to 'render to functions'.
Examples for what the environment type variable can be:
Model, for rendering to Model -> Html Msg for view.Usually, for the above usecases you would have to define a function of type
reduceTemplate :
Block (TemplateInfo -> Html msg)
-> (TemplateInfo -> Html msg)for example, so that you can turn it back into a Renderer (Template Info -> Html msg)
for elm-markdown.
If you were to define such a function, you would have to pass around the TemplateInfo
parameter a lot. This function will take care of that for you.
In some cases using this function would be overkill. The alternative to this function is to simply parameterize your whole renderer (and not use this library):
renderMarkdown : List String -> Block (Html Msg) -> Html Msg
renderMarkdown censoredWords markdown =
...
renderer : List String -> Markdown.Renderer (Html Msg)
renderer censoredWords =
toRenderer
{ renderHtml = ...
, renderMarkdown = renderMarkdown censoredWords
}In this example you can see how we pass through the 'censored words'. It behaves kind of like some global context in which we create our renderer.
It is hard to convey the abstract notion of when to use parameterized and when not to.
I'll give it a try: If you want to parse your markdown once and need to quickly render
different versions of it (for example with different Models or different
TemplateInfos), then use this. In other cases, if you probably only want to de-couple
some variable out of your renderer that is pretty static in general (for example censored
words), don't use this.
parameterized over multiple ParametersIf you want to parameterize your renderer over multiple variables, there are two options:
environment type used in this functionAlthough both are possible, I highly recommend the first option, as it is by far easier
to deal with only one call to parameterized, not with two calls that would be required
for option 2.
If this function doesn't quite do what you want, just try to re-create what you need by
using map directly. parameterized basically just documents a pattern that is really
easy to re-create: Its implementation is just 1 line of code.
This transform enables validating the content of your Block before
rendering.
This function's most prominent usecases are linting markdown files, so for example:
https:// linksh1 (alternatively, consider bumping the heading level)But it might also be possible that your view type can't always be reduced from a
Block view to a view, so you need to generate an error in these cases.
If this function doesn't quite do what you need to do, try using foldResults.
The validating definition basically just documents a common pattern. Its implementation
is just 1 line of code.
This transform allows you to perform elm-pages' StaticHttp requests without having to think about how to thread these through your renderer.
Some applications that can be realized like this:
If this function doesn't quite do what you need to do, try using foldStaticHttpRequests.
The wihtStaticHttpRequests definition basically just documents a common pattern.
Its implementation is just 1 line of code.
This will reduce a Block to Html similar to what the
defaultHtmlRenderer in elm-markdown
does. That is, it renders similar to what the CommonMark spec expects.
It also takes a list of attributes for convenience, so if you want to attach styles, id's, classes or events, you can use this.
However, the attributes parameter is ignored for Text nodes.
Extracts all words from the blocks and inlines. Excludes any markup characters, if they had an effect on the markup.
The words are split according to the \s javascript regular expression (regex).
Inline code spans are split, but code blocks fragments are ignored (code spans are included).
If you need something more specific, I highly recommend rolling your own function for this.
This is useful if you need to e.g. create header slugs.
Convert a block of markdown back to markdown text. (See the 'Formatting Markdown' test in the test suite.)
This just renders one particular style of markdown. Your use-case might need something completely different. I recommend taking a look at the source code and adapting it to your needs.
Note: This function doesn't support GFM tables.
The function Markdown.PrettyTables.reducePrettyTable extends this function with table
pretty-printing.
Table pretty-printing is complicated, even when ignoring column sizes. The type
Block String -> String is just "not powerful" enough to render a table to a string in
such a way that it is syntactically valid again.
Reduces a block down to anything that can be accumulated.
You provide two functions
accumulate: Describe how values of type a are combined. Examples: List.concat,
List.sum, etc.extract: Descibe how a blocks generate values that are supposed to be accumulated.For example, this can count the amount of headings in a markdown document:
reduce
{ accumulate = List.sum
, extract =
\block ->
case block of
Heading _ ->
1
_ ->
0
}Or this extracts code blocks:
reduce
{ accumulate = List.concat
, extract =
\block ->
case block of
CodeBlock codeBlock ->
[ codeBlock ]
_ ->
[]
}The special thing about this function is how you don't have to worry about accumulating the other generated values recursively.
Transform a block that contains functions into a function that produces blocks.
One really common use-case is having access to a Model inside your html renderers.
In these cases you want your markdown to be 'rendered to a function'.
So let's say you've got a
Markdown.Html.Renderer
like so:
renderHtml :
Markdown.Html.Renderer
(List (Model -> Html Msg)
-> (Model -> Html Msg)
)It has this type to be able to depend on the Model. Eventually you'll want to render to
Model -> Html Msg.
So now you can define your
Markdown.Renderer.Renderer
like so:
renderer : Markdown.Renderer.Renderer (Model -> Html Msg)
renderer =
toRenderer
{ renderHtml = renderHtml
, renderMarkdown = renderMarkdown
}
renderMarkdown :
Block (Model -> Html Msg)
-> (Model -> Html Msg)
renderMarkdown block model =
foldFunction block
-- ^ result : Model -> Block (Html Msg)
model
-- ^ result : Block (Html Msg)
|> reduceHtml
-- ^ result : Html MsgThread results through your Blocks.
The input is a block that contains possibly failed views. The output becomes Err, if
any of the input block's children had an error (then it's the first error).
If all of the block's children were Ok, then the result is going to be Ok.
Accumulate elm-page's
StaticHttp.Requests
over blocks.
Using this, it is possible to write reducers that produce views as a result of performing static http requests.
Fold your blocks with index information. This uses indexedMap under
the hood.
This is quite advanced, but also very useful. If you're looking for a working example, please take a look at the test for this function.
In this context of the library, we're often working with functions of the type
Block view -> view, where view might be something like Html Msg or String, etc.
or, generally, functions of structure Block a -> b.
I refer to functions of that structure as 'reducers'. (This is somewhat different to the 'real' terminology, but I feel like they capture the nature of 'reducing once' very well.)
If you know List.foldr you already know an example for a reducer (the first argument)!
The reducers in this module are no different, we just write them in different ways.
We can do the same thing we did for this library for lists:
type ListScaffold elem a
= Empty
| Cons elem a
reduceEmpty = 0
reduceCons a b = a + b
handler listElement =
case listElement of
Empty ->
reduceEmpty
Cons elem accumulated ->
reduceCons elem accumulated
foldl : (ListScaffold a b -> b) -> List a -> b
foldl handle list =
case list of
[] -> handle Empty
(x:xs) -> handle (Cons x xs)
foldl handler == List.foldl reduceCons reduceEmptyThe last line illustrates how different ways of writing these reducers relate: For
List.foldl we simply provide the cases (empty or cons) as different arguments,
for reducers in this library, we create a custom type case for empty and cons.
Some functions have similar, but not quite the type that a reducers has. For example:
Block (Request a) -> Request (Block a)Block (Maybe a) -> Maybe (Block a)Block (Result err a) -> Result err (Block a)Block (environment -> a) -> environment -> Block aAll of these examples have the structure Block (F a) -> F (Block a) for some F. You
might have to squint your eyes at the last two of these examples. Especially the last one.
Let me rewrite it with a type alias:
type alias Function a b =
a -> b
foldFunction : Block (Function env a) -> Function env (Block a)You can combine multiple 'reducers' into one. There's no function for doing this, but a pattern you might want to follow.
Let's say you want to accumulate both all the words in your markdown and the Html you
want it to render to, then you can do this:
type alias Rendered =
{ html : Html Msg
, words : List String
}
reduceRendered : Block Rendered -> Rendered
reduceRendered block =
{ html = block |> map .html |> reduceHtml
, words = block |> map .words |> reduceWords
}If you want to render to more things, just add another parameter to the record type and follow the pattern. It is even possible to let the rendered html to depend on the words inside itself (or maybe something else you're additionally reducing to).
Did you already start to write a custom elm-markdown Renderer, but want to use this
library? Don't worry. They're compatible. You can convert between them!
There are two ways of thinking about this function:
Render a Block using the given elm-markdown Renderer.
Extract a function of type (Block view -> view) out of
the elm-markdown Renderer. This is useful if you want to make use
of the utilities present in this library.
Convert a function that works with Block to a Renderer for use with
elm-markdown.
(The second parameter is a Markdown.Html.Renderer)
I mean to aggregate utilites for transforming Blocks in this section.
Bump all Heading elements by given positive amount of levels.
import Markdown.Block as Block
bumpHeadings 2
(Heading
{ level = Block.H1
, rawText = ""
, children = []
}
)
--> Heading
--> { level = Block.H3
--> , rawText = ""
--> , children = []
--> }
bumpHeadings 1
(Heading
{ level = Block.H6
, rawText = ""
, children = []
}
)
--> Heading
--> { level = Block.H6
--> , rawText = ""
--> , children = []
--> }
bumpHeadings -1
(Heading
{ level = Block.H2
, rawText = ""
, children = []
}
)
--> Heading
--> { level = Block.H2
--> , rawText = ""
--> , children = []
--> }
A datatype that enumerates all possible ways markdown could wrap some children.
Kind of like a 'Scaffold' around something that's already built, which will get torn down after building is finished.
This does not include Html tags.
If you look at the left hand sides of all of the functions in the elm-markdown
Renderer, you'll notice a similarity to this custom type, except it's missing a type for 'html'.Defining this data structure has some advantages in composing multiple Renderers.
It has a type parameter
children, which is supposed to be filled withString,Html msgor similar. Take a look at some reducers for examples of this.There are some neat tricks you can do with this data structure, for example,
Block Neverrepresents only non-nested blocks of markdown.