;;; MMD by David A. Moon is licensed under a ;;; Creative Commons Attribution-ShareAlike 4.0 International License ;;; http://creativecommons.org/licenses/by-sa/4.0/ ;;; ;;; Please inform me if you find this useful, or any of the ideas embedded in it. ;;; Comments and criticisms to dave underscore moon atsign alum dot mit dot edu. ;;; Incorporates details_shim by Tyler Uebele licensed as follows: ;;; ;;; Copyright (c) 2013 Tyler Uebele ;;; ;;; Permission is hereby granted, free of charge, to any person obtaining a copy ;;; of this software and associated documentation files (the "Software"), to ;;; deal in the Software without restriction, including without limitation the ;;; rights to use, copy, modify, merge, publish, distribute, sublicense, and/or ;;; sell copies of the Software, and to permit persons to whom the Software is ;;; furnished to do so, subject to the following conditions: ;;; ;;; The above copyright notice and this permission notice shall be included in ;;; all copies or substantial portions of the Software. ;;; ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ;;; THE SOFTWARE. ;;; Convert MMD to HTML module: defmodule MMD import: command.main, command.error_output import: file import: time.localtime def default_code_style = "display:table; border:1px solid black; background-color:whitesmoke; padding-left:5px; padding-right:5px; padding-top:5px; padding-bottom:5px" ;; Global state ;; g is bound to it in most functions defclass MMD(input_stream in_stream[character], output_directory string) input_stream = input_stream output_stream := false out_stream[character] | false output_directory = output_directory line_number := 0 integer title_prefix := false string | false page_number := 0 integer current_chapter_name := false string | false page_titles = stack() ;[[chapter, [section, subsection...]...]...] all_references = stack[list]() ;[[see_arg, href]...] all_anchors = set![string]() ;{"chapter#anchor"...} pre_html = stack[string]() post_html = stack[string]() this_page_file := false string | false previous_page_file := false string | false current_mode := #normal ; normal, TABLE, UL, or OL nesting_level := 0 integer code_style := default_code_style string def dot_handlers = map![string, list[function]]() defmacro defdot name "(" [ line_name [ directive_name ] ] ")" [ "before:" before_expression ] [ "after:" after_expression ] body => def before_name = name("before-dot-$name") def main_name = name("handle-dot-$name") def after_name = name("after-dot-$name") def g = name("g", previous_context) `do def $main_name($g MMD, $(line_name or `line`) string, $(directive_name or `directive`) string) $body def $before_name($g MMD) $(before_expression or `false`) def $after_name($g MMD) $(after_expression or `false`) dot_handlers[$(string(name))] := [$before_name, $main_name, $after_name]` ;;; Main Program def main(input_pathname string, optional: output_directory string | false) def g = MMD(file.open(input_pathname, character), output_directory or file.join(file.directory(input_pathname), "HTML")) file.make_directory(g.output_directory) block process_lines(g) finally: file.close(g.input_stream) if g.output_stream then file.close(g.output_stream) def main(ignore...) error("Usage: mmd input_pathname [output_directory]")) ;; Process lines until end of input file def process_lines(g MMD) for line = next_line(g) while line def first = positionf(_ ~= ' ', line) if not first ;; Blank line separates paragraphs if g.output_stream change_mode(g, #normal) output_raw(g, "
")
else case line[first]
'.' => directive_line(g, trim(' ', line))
'*' => list_line(g, trim(' ', line), #UL)
'#' => list_line(g, trim(' ', line), #OL)
'|' => table_line(g, trim(' ', line))
default:
change_mode(g, #normal)
output_with_flags_and_translations(g, line)
;; Input file exhausted
end_page(g, false)
;; Check for broken links
for [see_arg, href] in g.all_references
if not (href in g.all_anchors)
error_output("Broken link: .see $see_arg\n")
;; Generate index.html file
def index_path = file.join(g.output_directory, "index.html")
g.output_stream := file.open(index_path, character, #output)
output_index(g)
index_path
;;; Line Handlers
def undefined_handler(g MMD, line, directive_name)
error("Unrecognized directive .$directive_name on line $(g.line_number)")
def undefined_handlers = [undefined_handler, undefined_handler, undefined_handler]
def directive_line(g MMD, line)
change_mode(g, #normal)
def space = position(' ', line)
def directive_name = line[1 ..< (space or line.length)] ; 1 skips initial period
def dot_name = string(downcase(directive_name))
def [before, main, after] = dot_handlers[dot_name, default: undefined_handlers]
before(g, line, directive_name)
if space
main(g, line[space+1.. ")
output_raw(g, "", newline: false)
output_raw(g, "Previous page", newline: false)
output_raw(g, "", newline: false)
output_raw(g, " Table of Contents ", newline: false)
if next_chapter_name
output_raw(g, "", newline: false)
output_raw(g, "Next page", newline: false)
output_raw(g, "", newline: false)
output_raw(g, "" else "
")
g.nesting_level := g.nesting_level + 1
output_raw(g, "
")
def leave_UL_mode(g MMD)
output_raw(g, "
")
def enter_OL_mode(g MMD)
output_raw(g, "")
def leave_OL_mode(g MMD)
output_raw(g, "
")
def table_line(g MMD, line)
change_mode(g, #TABLE)
if not ends?(line, "|")
error("Missing closing | in $line on line $(g.line_number)")
;; Parse out the columns of this row
;; A header column will retain an extra | at the start
;; This relies on our special characters all taking one UTF-8 byte to encode
def start := 1 ; position after leading |
def end = line.end_position
def columns = stack[string]()
def next := start
while next < end
def next := positionf(_ in "`|", line, next)
if not next
error("Parsing error after position $start in $line on line $(g.line_number)")
else if next = start and line[next] = '|'
;; Skip over second | in ||
next = next + 1
else if line[next] = '`'
;; Skip over | inside of ` pair
next := position('`', line, next + 2) + 1
else
;; | at next is end of column
push!(columns, string(trim(' ', line[start ..< next])))
next := start := next + 1
;; Output this row
output_raw(g, "")
for column in columns
if begins?(column, "|")
;; This is a heading
output_raw(g, " ")
def enter_table_mode(g MMD)
output_raw(g, "", newline: false)
output_with_flags_and_translations(g, column[1 ..< column.end_position])
output_raw(g, " ")
else
;; This is a normal row
output_raw(g, "", newline: false)
output_with_flags_and_translations(g, column)
output_raw(g, " ")
output_raw(g, "")
def leave_table_mode(g MMD)
output_raw(g, "
")
;;; Directives
;; .chapter, .section, and .subsection produce a table of contents
;; in the index.html file. Each chapter goes in its own .html file.
defdot chapter(line)
if g.output_stream
end_page(g, chapter_name(line))
push!(g.page_titles, [line, stack()]) ;[chapter]
g.current_chapter_name := chapter_name(line)
set_anchor(g, line)
set_anchor(g, "")
start_page(g, line)
output_raw(g, "$line
")
defdot section(line)
push!(g.page_titles.top[1], [line, stack()]) ;[chapter, [section]]
set_anchor(g, line)
output_raw(g, "$line
")
defdot subsection(line)
push!(g.page_titles.top[1].top[1], line) ;[chapter, [section, [subsection]]]
set_anchor(g, line)
output_raw(g, "$line
")
defdot anchor(line)
set_anchor(g, line)
output_raw(g, "")
def set_anchor(g MMD, line)
push!(g.all_anchors, "$(g.current_chapter_name).html#$(anchor_name(line))")
;; Remove all but letters and numbers, change spaces to underscores
def anchor_name(title)
for char in title using collect string
if char.code < 128 and (alpha_char?(char) or digit_char?(char))
collect downcase(char)
else if char = ' '
collect '_'
;; Remove all but letters and numbers, change spaces to underscores, can't be "index"
def chapter_name(title)
def anchor = anchor_name(title)
if anchor = "index" then "index_" else anchor
;; chapter
;; #anchor
;; chapter#anchor
;; Returns [chapter, anchor] where chapter can be false
def decode_see(line)
def sharp = position('#', line)
def first = line[0 ..< (sharp or line.end_position)]
def second = sharp and line[sharp + 1 ..< line.end_position]
def chapter = if not sharp then line
else if sharp > 0 then first
else false
def anchor = if sharp then second else ""
[chapter, anchor]
def make_href(g MMD, line)
def [chapter, anchor] = decode_see(line)
def file = if chapter then chapter_name(chapter) else g.current_chapter_name
"$file.html#$(anchor_name(anchor))"
;; .see chapter#anchor
;; or .See, output preserves the alphabetic case of the directive
defdot see(line, directive)
def [chapter, anchor] = decode_see(line)
def href = make_href(g, line)
def text = if anchor = "" then chapter else anchor
push!(g.all_references, [line, href])
output_raw(g, "$directive $text")
;; .code displays a block of code
defdot code(line)
before: output_raw(g, "")
after: output_raw(g, "
")
output_with_translations(g, if begins?(line, ".QUOTE!! ")
line[9 ..< line.end_position]
else line)
defdot codestyle(line)
g.code_style := line
;; .comment comments-out its directed lines
defdot comment()
;; .html inserts raw HTML directly into the formatted output
defdot html(line)
output_raw(g, line)
defdot prehtml(line)
push!(g.pre_html, line)
defdot posthtml(line)
push!(g.post_html, line)
;; .title inserts a prefix and a colon space before each page title
defdot title(line)
g.title_prefix := line
;;; Subroutines
;;; Begin or end a table or list
def change_mode(g MMD, new_mode)
if new_mode ~= g.current_mode
case g.current_mode
#TABLE => leave_table_mode(g)
#UL => leave_UL_mode(g)
#OL => leave_OL_mode(g)
g.current_mode := new_mode
g.nesting_level := 1
case new_mode
#TABLE => enter_table_mode(g)
#UL => enter_UL_mode(g)
#OL => enter_OL_mode(g)
;;; Read line, strip trailing spaces, convert tabs, false at EOF
def next_line(g MMD)
def line = next_line!(g.input_stream)
if line
g.line_number := g.line_number + 1
def column := 0
def whitespace?(char) char = ' ' or char = '\t'
for char in right_trimf(whitespace?, line) using collect string, append string
if char = '\t'
append (8 - column % 8) * ' '
column := column + 8 - column % 8
else
collect char
column := column + 1
;; Start a non-index page
def start_page(g MMD, title)
g.page_number := g.page_number + 1
g.previous_page_file := g.this_page_file
g.this_page_file := chapter_name(title)
g.output_stream := file.open(file.join(g.output_directory, "$(g.this_page_file).html"),
character, #output)
output_raw(g, "")
output_raw(g, "")
output_raw(g, "
")
output_raw(g, "Table of Contents
")
for [title, sections] in g.page_titles, page_no = 1 then page_no + 1
if empty?(sections)
output_raw(g, "")
def page_name = chapter_name(title)
output_raw(g, "$page_no. $title
", newline: false)
if empty?(sections)
output_raw(g, "
")
for html in g.post_html
output_raw(g, html)
output_raw(g, "")
;; Output HTML directly
def output_raw(g MMD, line, named: newline = true)
def bad = positionf(fun(char) char.code < 32 or char.code > 126, line)
if bad
error("Invalid character code $(line[bad].code) on line $(g.line_number)")
append!(g.output_stream, line)
if newline
push!(g.output_stream, '\n')
;; Output with character translations
def output_with_translations(g MMD, line, named: start = iterate(line), end = line.end_position, newline = true)
for pos = start then iterate(line, pos) while pos < end
def char = line[pos]
if char.code < 32
error("Invalid character code $(char.code) on line $(g.line_number)")
else if char = '<'
output_raw(g, "<", newline: false)
else if char = '>'
output_raw(g, ">", newline: false)
else if char = '&'
output_raw(g, "&", newline: false)
else if char.code > 126
output_raw(g, "", newline: false)
for shift = 12 then shift - 4 while shift >= 0
def higit = 15 & (char.code >> shift)
push!(g.output_stream, character(higit + (if higit < 10 then '0'.code else 'A'.code - 10)))
push!(g.output_stream, ';')
else
push!(g.output_stream, char)
if newline
push!(g.output_stream, '\n')
;; Output with style flags and character translations
;; Depends on ` and \ being encoded in one UTF-8 byte
def output_with_flags_and_translations(g MMD, line, named: start = iterate(line), end = line.end_position, newline = true)
block exit: return
def pos := start
while pos < end
def next = positionf(_ in "`\\", line, pos, end)
if not next
return(output_with_translations(g, line, start: pos, end: end, newline: newline))
else
if next > pos
output_with_translations(g, line, start: pos, end: next, newline: false)
pos := next
def nextnext = position(line[pos], line, iterate(line, pos + 1), end) ; iterate to allow ```
if not nextnext
error("Mismatched $(line[pos]) in $line on line $(g.line_number)")
if line[pos] = '`'
output_raw(g, "", newline: false)
output_with_translations(g, line, start: pos + 1, end: nextnext, newline: false)
output_raw(g, "", newline: false)
pos := nextnext + 1
else if line[pos + 1] = '\\'
if not (nextnext < end and line[nextnext + 1] = '\\')
error("Mismatched \\\\ in $line on line $(g.line_number)")
output_raw(g, "", newline: false)
output_with_flags_and_translations(g, line, start: pos + 2, end: nextnext, newline: false)
output_raw(g, "", newline: false)
pos := nextnext + 2
else
output_raw(g, "", newline: false)
output_with_flags_and_translations(g, line, start: pos + 1, end: nextnext, newline: false)
output_raw(g, "", newline: false)
pos := nextnext + 1
if newline
push!(g.output_stream, '\n')