#! /usr/bin/env python
# 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
import codecs
import math
import os
import re
import sys
import time
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
class MMD_globals:
pass
def MMD(input_stream, output_directory):
self = MMD_globals()
self.input_stream = input_stream
self.output_stream = False
self.output_directory = output_directory
self.line_number = 0
self.title_prefix = False
self.page_number = 0
self.current_chapter_name = False
self.page_titles = [] #[[chapter [section subsection...]...]...]
self.all_references = [] #[[see_arg href]...]
self.all_anchors = [] #{"chapter#anchor"...}
self.pre_html = []
self.post_html = []
self.this_page_file = False
self.previous_page_file = False
self.current_mode = 'normal'
self.nesting_level = 0
self.code_style = default_code_style
return self
dot_handlers = dict() # maps a string to a list of before, main, after function
# Define the handler(s) with def first, then call this
# Handler arguments are (g, line, directive_name)
def defdot(directive_name, handler, before=None, after=None):
dot_handlers[directive_name] = [before, handler, after]
# Main Program
def main(argv):
if len(argv) < 2 or len(argv) > 3:
print("Usage: mmd input_pathname [output_directory]")
return 1
else:
input_pathname = argv[1]
output_directory = None
if len(argv) == 3:
output_directory = argv[2]
else:
output_directory = os.path.join(os.path.dirname(input_pathname), "HTML")
g = MMD(codecs.open(input_pathname, mode='r', encoding='utf-8'), output_directory)
try:
os.mkdir(g.output_directory)
except OSError:
pass
try:
process_lines(g)
return 0
finally:
g.input_stream.close()
if g.output_stream:
g.output_stream.close()
# Process lines until end of input file
def process_lines(g):
while True:
line = next_line(g)
if line == False:
break
line2 = line.lstrip() # Remove leading whitespace
if len(line2) == 0:
# Blank line separates paragraphs
if g.output_stream:
change_mode(g, 'normal')
output_raw(g, "
")
elif line2[0] == '.':
directive_line(g, line2)
elif line2[0] == '*':
list_line(g, line2, 'UL')
elif line2[0] == '#':
list_line(g, line2, 'OL')
elif line2[0] == '|':
table_line(g, line2)
else:
change_mode(g, 'normal')
output_with_flags_and_translations(g, line)
# Input file exhausted
end_page(g, False)
# Check for broken links
for x in g.all_references:
see_arg = x[0]
href = x[1]
if not (href in g.all_anchors):
error_output("Broken link: .see " + see_arg + "\n")
# Generate index.html file
index_path = os.path.join(g.output_directory, "index.html")
g.output_stream = open(index_path, "w")
output_index(g)
index_path
# Line Handlers
def undefined_handler(g, line, directive_name):
error("Unrecognized directive ." + directive_name + " on line " + str(g.line_number))
undefined_handlers = [undefined_handler, undefined_handler, undefined_handler]
def directive_line(g, line):
change_mode(g, 'normal')
space = line.find(" ")
directive_end = len(line)
if space >= 0:
directive_end = space
directive_name = line[1 : directive_end] # 1 skips initial period
dot_name = directive_name.lower()
handler = undefined_handlers
if dot_name in dot_handlers:
handler = dot_handlers[dot_name]
before = handler[0]
main_handler = handler[1]
after = handler[2]
if before:
before(g, line, directive_name)
if space >= 0:
main_handler(g, line[space+1 : len(line)], directive_name)
else:
end_line = ".end " + directive_name
while True:
this_line = next_line(g)
if this_line == False:
break
if this_line == end_line:
break
main_handler(g, this_line, directive_name)
if after:
after(g, line, directive_name)
def list_line(g, line, mode):
change_mode(g, mode)
# number of leading stars or splats
level = 0
while level < len(line) and line[level] == line[0]:
level = level + 1
while g.nesting_level > level:
output_raw(g, "" + mode + ">")
g.nesting_level = g.nesting_level - 1
while g.nesting_level < level:
type = '1'
if g.nesting_level < 3:
type = "1ai"[g.nesting_level]
if mode == 'UL':
output_raw(g, "
")
def enter_OL_mode(g):
output_raw(g, "")
def leave_OL_mode(g):
output_raw(g, "")
def table_line(g, line):
change_mode(g, 'TABLE')
if not line.endswith("|"):
error("Missing closing | in " + line + " on line " + str(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
start = 1 # position after leading |
end = len(line)
columns = []
next = start
while next < end:
next1 = line.find('|', next)
next2 = line.find('`', next)
if next1 < 0 or (next2 >= 0 and next2 < next1):
next = next2
else:
next = next1
if next < 0:
error("Parsing error after position " + str(start) + " in " + line + " on line " + str(g.line_number))
elif next == start and line[next] == '|':
# Skip over second | in ||
next = next + 1
elif line[next] == '`':
# Skip over | inside of ` pair
next = line.find('`', next + 2) + 1
else:
# | at next is end of column
columns.append(line[start : next].strip())
start = next + 1
next = start
# Output this row
output_raw(g, "
")
for column in columns:
if column.startswith("|"):
# This is a heading
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.
def chapter(g, line, _):
if g.output_stream:
end_page(g, chapter_name(line))
g.page_titles.append([line, []]) # [chapter]
g.current_chapter_name = chapter_name(line)
set_anchor(g, line)
set_anchor(g, "")
start_page(g, line)
output_raw(g, "