#!/usr/bin/env python """ jemdoc: light markup. version 0.3.0, November 2007. """ # Copyright (C) 2007 Jacob Mattingley. # # This file is part of jemdoc. # # jemdoc is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation; either version 3 of the License, or (at your option) any later # version. # # jemdoc is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . import sys import os import re import time import StringIO class controlstruct(object): def __init__(self, infile, outfile, conf): self.inf = infile self.outf = outfile self.conf = conf self.linenum = 0 # better checking of arguments? def showhelp(): a = """Usage: jemdoc [OPTIONS] [SOURCEFILE] Produces html markup from a jemdoc SOURCEFILE. Most of the time you can use jemdoc without any additional flags. For example, typing jemdoc index.jemdoc will produce an index.html, using a default configuration. You can change the output file by using -o OUTFILE, for example jemdoc -o html/main.html index.jemdoc Some configuration options can be overridden by specifying a configuration file. You can use jemdoc --show-config to print a sample configuration file (which includes all of the default options). Any or all of the configuration [blocks] can be overwritten by including them in a configuration file, and running, for example, jemdoc -c mywebsite.f.conf index.jemdoc See http://jemdoc.jaboc.net/ for more details. """ b = '' for l in a.splitlines(True): if l.startswith(' '*4): b += l[4:] else: b += l print b def standardconf(): a = """[firstbit] [defaultcss] [windowtitle] # used in header for window title. | [doctitle] # used at top of document.

|

[subtitle]
|
[doctitleend]
[bodystart] [menustart]
[menuend]
[menucategory] [menuitem] [currentmenuitem] [nomenu]
[css] [menulastbit]
[nomenulastbit] [bodyend] [infoblock]
[codeblock]
[blocktitle]
|
[infoblockcontent]
[codeblockcontent]
    
    [codeblockend]
    
[infoblockend]
[footerstart] [lastupdated] Last updated |, using jemdoc. """ b = '' for l in a.splitlines(True): if l.startswith(' '): b += l[4:] else: b += l return b class JandalError(Exception): pass def raisejandal(msg, line=0): if line == 0: s = "%s" % msg else: s = "line %d: %s" % (line, msg) raise JandalError(s) def readnoncomment(f): l = f.readline() if l == '': return l elif l[0] == '#': # jem: be a little more generous with the comments we accept? return readnoncomment(f) else: return l.rstrip() + '\n' # leave just one \n and no spaces etc. def parseconf(cns): syntax = {} warn = False # jem. make configurable? # manually add the defaults as a file handle. fs = [StringIO.StringIO(standardconf())] for sname in cns: fs.append(open(sname)) for f in fs: while pc(f) != '': l = readnoncomment(f) r = re.match(r'\[(.*)\]\n', l) if r: tag = r.group(1) s = '' l = readnoncomment(f) while l not in ('\n', ''): s += l l = readnoncomment(f) syntax[tag] = s f.close() return syntax def insertmenuitems(f, mname, current, prefix): m = open(mname) while pc(m) != '': l = readnoncomment(m) l = l.strip() if l == '': continue r = re.match(r'\s*(.*?)\s*\[(.*)\]', l) if r: # then we have a link. if r.group(2) == current: hb(f.outf, f.conf['currentmenuitem'], prefix + r.group(2), br(r.group(1))) else: hb(f.outf, f.conf['menuitem'], prefix + r.group(2), br(r.group(1))) else: # menu category. hb(f.outf, f.conf['menucategory'], br(l)) m.close() def out(f, s): f.write(s) def hb(f, tag, content1, content2=None): """Writes out a halfblock (hb).""" if content2 is None: out(f, re.sub(r'\|', content1, tag)) else: r = re.sub(r'\|1', content1, tag) r = re.sub(r'\|2', content2, r) out(f, r) def pc(f): """Peeks at next character in the file.""" # Should only be used to look at the first character f.outf a new line. c = f.read(1) if c: # only undo forward movement if we're not at the end. #if c == '#': # interpret comment lines as blank. # return '\n' if c in ' \t': return pc(f) f.seek(-1, 1) return c def nl(f, withcount=False, codemode=False): """Get input file line.""" s = f.inf.readline() f.linenum += 1 if not codemode: # remove any special characters - assume they were checked by pc() # before we got here. # remove any trailing comments. s = s.lstrip(' \t') s = re.sub(r'\s*(?$%\.~[\]-]""", r'\\\g<0>', s) else: return re.sub(r"""[\\*/+"'<>\.~[\]-]""", r'\\\g<0>', s) def replacequoted(b): """Quotes {{raw html}} sections. Insert a backslash right before the end with &bs;, an illegal html character.""" r = re.compile(r'\{\{(.*?)\}\}', re.M + re.S) m = r.search(b) while m: qb = quote(m.group(1), True) b = b[:m.start()] + qb + b[m.end():] m = r.search(b, m.start()) r = re.compile(r'(?' % " ".join(bits) + b[m.end():] m = r.search(b, m.start()) return b def replacelinks(b): # works with [link.html new link style]. r = re.compile(r'(?%s<\/a>' % (link, linkname) + b[m.end():] m = r.search(b, m.start()) return b def br(b): """Does simple text replacements on a block of text. ('block replacements')""" # Deal with literal backspaces. b = re.sub(r'\\\\', '&jemLITerl33talBS;', b) # Deal with {{html embedding}}. b = replacequoted(b) b = allreplace(b) # First do the URL thing. b = b.lstrip('-. \t') # remove leading spaces, tabs, dashes, dots. b = replacelinks(b) # Deal with /italics/ first because the '/' in other tags would otherwise # interfere. r = re.compile(r'(?\1', b) # Deal with *bold*. r = re.compile(r'(?\1', b) # Deal with +monospace+. r = re.compile(r'(?\1', b) # Deal with "double quotes". r = re.compile(r'(?', b) ## Deal with zero width \_. #r = re.compile(r"(?", re.M + re.S) b = re.sub(r, r'>', b) r = re.compile(r"(?\1', l) if l.startswith('>>>'): hb(f, '|\n', l) else: out(f, l + '\n') def py(f, l): # jem need to do much better here. l = l.rstrip() if l.startswith('>>>'): hb(f, '|\n', allreplace(l)) elif l.startswith('#'): hb(f, '|\n', allreplace(l)) else: out(f, allreplace(l) + '\n') def dashlist(f): level = 0 while pc(f.inf) == '-': (s, newlevel) = np(f, True) # first adjust list number as appropriate. if newlevel > level: for i in range(newlevel - level): if newlevel > 1: out(f.outf, '\n') out(f.outf, '\n
  • ') else: out(f.outf, '
  • \n
  • ') out(f.outf, br(s)) level = newlevel for i in range(level): out(f.outf, '
  • \n\n') def dotlist(f): level = 0 while pc(f.inf) == '.': (s, newlevel) = np(f, True) # first adjust list number as appropriate. if newlevel > level: for i in range(newlevel - level): if newlevel > 1: out(f.outf, '\n') out(f.outf, '
      \n
    1. ') elif newlevel < level: for i in range(level - newlevel): out(f.outf, '
    2. \n
    \n
  • ') else: out(f.outf, '
  • \n
  • ') out(f.outf, br(s)) level = newlevel for i in range(level): out(f.outf, '
  • \n\n') def colonlist(f): out(f.outf, '
    \n') while pc(f.inf) == ':': s = np(f) r = re.compile(r'\s*{(.*?)(?|\n', br(defpart)) hb(f.outf, '
    |
    \n', br(rest)) out(f.outf, '
    \n') def codeblock(f, g): out(f.outf, f.conf['codeblock']) if len(g[0]): hb(f.outf, f.conf['blocktitle'], g[0]) out(f.outf, f.conf['codeblockcontent']) if g[1] not in ('', 'pyint', 'py'): raise SyntaxError( \ "couldn't handle the jandal: unrecognised syntax " "highlighting on line %d" % linenum) # Now we are handling code. # Handle \~ and ~ differently. while 1: # wait for EOF. l = nl(f, codemode=True) if not l: break elif l.startswith('~'): break elif l.startswith('\\~'): l = l[1:] elif l.startswith('\\{'): l = l[1:] if g[1] == 'pyint': pyint(f.outf, l) elif g[1] == 'py': py(f.outf, l) else: out(f.outf, allreplace(l)) out(f.outf, f.conf['codeblockend']) def procfile(f): linenum = 0 menu = None footer = True nodefaultcss = False css = [] if pc(f.inf) == '#': l = f.inf.readline() linenum += 1 if l.startswith('# jemdoc: '): l = l[len('# jemdoc: '):] a = l.split(',') # jem only handle one argument for now. for b in a: b = b.strip() if b.startswith('menu'): sidemenu = True r = re.compile(r'(? 3 or len(g) < 2: raise SyntaxError('sidemenu error on line %d' % linenum) if len(g) == 2: menu = (f, g[0], g[1], '') else: menu = (f, g[0], g[1], g[2]) elif b.startswith('nodate'): footer = False elif b.startswith('nodefaultcss'): nodefaultcss = True elif b.startswith('addcss'): r = re.compile(r'(?|\n' % (c, c), br(s)) # look for comments. elif p == '#': nl(f) elif p == '\n': nl(f) # look for blocks. elif p == '~': nl(f) if infoblock: out(f.outf, f.conf['infoblockend']) infoblock = False nl(f) continue else: if pc(f.inf) == '{': l = br(nl(f)) r = re.compile(r'(?|

    \n', s) if footer: s = time.strftime('%F %R:%S %Z', time.localtime(time.time())) out(f.outf, f.conf['footerstart']) hb(f.outf, f.conf['lastupdated'], s) out(f.outf, f.conf['footerend']) if menu: out(f.outf, f.conf['menulastbit']) else: out(f.outf, f.conf['nomenulastbit']) out(f.outf, f.conf['bodyend']) if f.outf is not sys.stdout: f.outf.close() def main(): if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'): showhelp() raise SystemExit if sys.argv[1] == '--show-config': print standardconf() raise SystemExit outoverride = False confoverride = False outname = None confnames = [] for i in range(1, len(sys.argv), 2): if sys.argv[i] == '-o': if outoverride: raise RuntimeError("only one output file / directory, please") outname = sys.argv[i+1] outoverride = True elif sys.argv[i] == '-c': if confoverride: raise RuntimeError("only one config file, please") confnames.append(sys.argv[i+1]) confoverride = True elif sys.argv[i].startswith('-'): raise RuntimeError('unrecognised argument %s, try --help' % sys.argv[i]) else: break conf = parseconf(confnames) innames = [] for j in range(i, len(sys.argv)): # First, if not a file and no dot, try opening .jemdoc. Otherwise, fall back # to just doing exactly as asked. inname = sys.argv[j] if not os.path.isfile(inname) and '.' not in inname: inname += '.jemdoc' innames.append(inname) if outname is not None and not os.path.isdir(outname) and len(innames) > 1: raise RuntimeError('cannot handle one outfile with multiple infiles') for inname in innames: if outname is None: thisout = re.sub(r'.jemdoc$', '', inname) + '.html' elif os.path.isdir(outname): # if directory, prepend directory to automatically generated name. thisout = outname + re.sub(r'.jemdoc$', '', inname) + '.html' else: thisout = outname infile = open(inname) outfile = open(thisout, 'w') f = controlstruct(infile, outfile, conf) procfile(f) # if __name__ == '__main__': main()