8000 oak/lib/md.oak at main · thesephist/oak · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
676 lines (616 loc) · 15.6 KB

md.oak

File metadata and controls

676 lines (616 loc) · 15.6 KB
with map() fn(line) line |> slice(1)
fn readUntilPrefix(prefix) lineReader.readUntilPrefix('>' << c) |>
with map() fn(line) line |> slice(1)
fn readUntilMatchingDelim ? // NOTE: not implemented
{
peek: peek
last: last
back: back
next: next
expect?: expect?
readUntil: readUntil
readUntilPrefix: readUntilPrefix
readUntilEnd: lineReader.readUntilEnd
readUntilMatchingDelim: readUntilMatchingDelim
}
}
{
tag: :blockquote
children: BlockQuoteLineReader(lineReader) |> parseDoc()
}
}
fn parseCodeBlock(lineReader) {
peek := lineReader.peek
next := lineReader.next
startTag := next() // eat starting pre tag
lang := if rest := startTag |> slice(3) {
'' -> ''
_ -> rest
}
fn sub(lines) if lineNodeType(peek()) {
:pre, ? -> lines
_ -> sub(lines << next())
}
children := sub([])
next() // eat ending pre tag
{
tag: :pre
children: [{
tag: :code
lang: lang
children: unifyTextNodes(children, '\n')
}]
}
}
fn parseRawHTML(lineReader) {
peek := lineReader.peek
next := lineReader.next
startMarkLine := next()
firstLine := startMarkLine |> slice(len('!html '))
fn sub(lines) if lineNodeType(peek()) {
:empty, ? -> lines
_ -> sub(lines << next())
}
children := sub([firstLine])
{
tag: :rawHTML
children: unifyTextNodes(children, '\n')
}
}
fn parseList(lineReader, listType) {
peek := lineReader.peek
next := lineReader.next
fn sub(items) if listItemLine?(peek()) {
false -> items
_ -> {
// TODO: provide a way for one listItem to contain 2+ paragraphs
// The current convention seems to be that if there is at least one
// multi-paragraph listItem in a UL, every listItem in the UL gets
// <p>s rather than inline text nodes as content.
line := next()
lineType := lineNodeType(line)
reader := Reader(line)
trimmer := if lineType {
:ul -> trimUListGetLevel
:ol -> trimOListGetLevel
}
level := trimmer(reader)
text := reader.readUntilEnd()
listItem := {
tag: :li
level: level
children: tokenizeText(text) |> parseText()
}
// handle list items that have distinct levels
if lastItem := items.(len(items) - 1) {
? -> sub(items << listItem)
_ -> if {
lastItem.level = level -> if lineType {
// continue if same list type; otherwise re-parse
listType -> sub(items << listItem)
_ -> {
lineReader.back()
items
}
}
lastItem.level < level -> {
// indent in: begin parsing a separate list
lineReader.back()
list := parseList(lineReader, lineType)
lastItem.children << list
sub(items)
}
_ -> {
// indent out: give up control in this parsing depth
lineReader.back()
items
}
}
}
}
}
children := sub([])
// remove the level annotations
children |> each(fn(child) if child.tag = :li -> child.level := _)
{
tag: listType
children: children
}
}
fn parseParagraph(lineReader) {
peek := lineReader.peek
next := lineReader.next
fn sub(lines) if lineNodeType(peek()) {
:p -> {
text := next()
if [text |> endsWith?(' '), text.(len(text) - 1)] {
[true, _] -> {
lines |> append(text |> take(len(text) - 2) |>
tokenizeText() |> parseText())
sub(lines << { tag: :br })
}
[_, '\\'] -> {
lines |> append(text |> take(len(text) - 1) |>
tokenizeText() |> parseText())
sub(lines << { tag: :br })
}
_ -> sub(lines |> append(tokenizeText(text) |> parseText()))
}
}
_ -> lines
}
{
tag: :p
children: sub([]) |> unifyTextNodes(' ')
}
}
// compile transforms a Markdown AST node to HTML
fn compile(nodes) nodes |> map(compileNode) |> join()
fn wrap(tag, node) '<' << tag << '>' << compile(node.children) << '</' << tag << '>'
fn sanitizeAttr(attr) {
attr |> map(fn(c) if c {
'<' -> '&lt;'
'\'' -> '&apos;'
'"' -> '&quot;'
_ -> c
})
}
fn sanitizeURL(url) {
encodedURL := url |> percentDecode() |> map(fn(c) if {
word?(c), c = '/', c = ':' -> c
_ -> ''
}) |> lower()
if {
encodedURL |> startsWith?('javascript:')
encodedURL |> startsWith?('data:') -> ''
_ -> sanitizeAttr(url)
}
}
// compileNode transforms an individual Markdown AST node into HTML
fn compileNode(node) if type(node) {
:string -> node |> map(fn(c) if c {
'&' -> '&amp;'
'<' -> '&lt;'
_ -> c
})
_ -> if node.tag {
:p, :em, :strong, :strike
:h1, :h2, :h3, :h4, :h5, :h6
:pre, :ul, :ol, :li, :blockquote -> wrap(node.tag |> string(), node)
:a -> '<a href="{{0}}">{{1}}</a>' |> format(sanitizeURL(node.href), compile(node.children))
:img -> '<img alt="{{0}}" src="{{1}}"/>' |> format(sanitizeAttr(node.alt), sanitizeURL(node.src))
:code -> if node.lang {
'', ? -> wrap('code', node)
_ -> '<code data-lang="{{0}}">{{1}}</code>' |>
format(sanitizeAttr(node.lang), compile(node.children))
}
:checkbox -> '<input type="checkbox" ' << if node.checked {
true -> 'checked'
_ -> ''
} << '/>'
:br -> '<br/>'
:hr -> '<hr/>'
:rawHTML -> node.children.0
_ -> '<span style="color:red">Unknown Markdown node {{0}}</span>' |> format(string(node) |> compileNode())
}
}
// transform wraps the Markdown parser and compiler into a single function to be
// invoked by the library consumer.
fn transform(text) text |> parse() |> compile()
0