#!/usr/bin/env python """ jemdoc: light markup, see http://jemdoc.jaboc.net/. version 0.3.4, 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=None, conf=None): 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.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] Page generated |, by 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(controlstruct(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(controlstruct(m)) != '': l = readnoncomment(m) l = l.strip() if l == '': continue r = re.match(r'\s*(.*?)\s*\[(.*)\]', l) if r: # then we have a link. link = r.group(2) # Don't use prefix if we have an absolute link. if '://' not in r.group(2): link = prefix + allreplace(link) if r.group(2) == current: hb(f.outf, f.conf['currentmenuitem'], link, br(r.group(1))) else: hb(f.outf, f.conf['menuitem'], link, 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, ditchcomments=True): """Peeks at next character in the file.""" # Should only be used to look at the first character of a new line. c = f.inf.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 ditchcomments and c == '#': nl(f) if c in ' \t': return pc(f) f.inf.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 = replaceimages(b) 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 putbsbs(l): for i in range(len(l)): l[i] = '\\b' + l[i] + '\\b' return l def gethl(lang): # disable comments by default, by choosing unlikely regex. d = {'statement':[], 'commentstart':'######%%%%%', 'operator':[], 'builtin':[], 'error':[]} if lang in ('py', 'python'): d['statement'] = putbsbs(['break', 'continue', 'del', 'except', 'exec', 'finally', 'pass', 'print', 'raise', 'return', 'try', 'with', 'global', 'assert', 'lambda', 'yield', 'def', 'class', 'for', 'while', 'if', 'elif', 'else', 'import', 'from', 'as']) d['operator'] = putbsbs(['and', 'in', 'is', 'not', 'or']) d['builtin'] = putbsbs(['True', 'False', 'set', 'open', 'frozenset', 'enumerate', 'object', 'hasattr', 'getattr', 'filter', 'eval', 'zip', 'vars', 'unicode', 'type', 'str', 'repr', 'round']) d['error'] = putbsbs(['\w*Error',]) d['commentstart'] = '#' elif lang == 'sh': d['statement'] = putbsbs(['cd', 'ls', 'sudo']) d['operator'] = ['>'] d['builtin'] = putbsbs(['curl', 'wget', '(?|\n', allreplace(l)) else: l = allreplace(l) # handle strings. r = re.compile(r'(".*?")') l = r.sub(r'\1', l) r = re.compile(r"('.*?')") l = r.sub(r'\1', l) # handle comments. r = re.compile(hl['commentstart']) l = r.sub(r'\1', l) if hl['statement']: r = re.compile('(' + '|'.join(hl['statement']) + ')') l = r.sub(r'\1', l) if hl['operator']: r = re.compile('(' + '|'.join(hl['operator']) + ')') l = r.sub(r'\1', l) if hl['builtin']: r = re.compile('(' + '|'.join(hl['builtin']) + ')') l = r.sub(r'\1', l) if hl['error']: r = re.compile('(' + '|'.join(hl['error']) + ')') l = r.sub(r'\1', l) l = re.sub('CLASS', 'class', l) out(f, l + '\n') def dashlist(f): level = 0 while pc(f) == '-': (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) == '.': (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) == ':': 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): if g[1] == 'raw': raw = True else: raw = False out(f.outf, f.conf['codeblock']) if g[0]: hb(f.outf, f.conf['blocktitle'], g[0]) out(f.outf, f.conf['codeblockcontent']) # 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:] # jem revise pyint out of the picture. if g[1] == 'pyint': pyint(f.outf, l) else: if raw: out(f.outf, l) else: language(f.outf, l, gethl(g[1])) if not raw: out(f.outf, f.conf['codeblockend']) def procfile(f): linenum = 0 menu = None footer = True nodefaultcss = False css = [] if pc(f, False) == '#': 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 elif imgblock: out(f.outf, '\n') imgblock = False nl(f) continue else: if pc(f) == '{': l = allreplace(nl(f)) r = re.compile(r'(?= 1: g[0] = br(g[0]) if len(g) in (0, 1): # info block. out(f.outf, f.conf['infoblock']) infoblock = True if len(g) == 1: # info block. hb(f.outf, f.conf['blocktitle'], g[0]) out(f.outf, f.conf['infoblockcontent']) elif len(g) == 2: codeblock(f, g) elif len(g) >= 4 and g[1] == 'img_left': # handles # {}{img_left}{source}{alttext}{width}{height}{linktarget}. g += ['']*(7 - len(g)) if g[4].isdigit(): g[4] += 'px' if g[5].isdigit(): g[5] += 'px' out(f.outf, '\n
    \n') if g[6]: out(f.outf, '' % g[6]) out(f.outf, '%s') if g[6]: out(f.outf, '') out(f.outf, ' ') imgblock = True else: raise JandalError("couldn't handle block", linenum) else: s = br(np(f)) if s: hb(f.outf, '

    |

    \n', s) if footer: s = time.strftime('%Y-%m-%d %H:%M:%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()