#!/usr/bin/env python
"""jemdoc version 0.5.3, 2008-03-29."""
# Copyright (C) 2007-2008 Jacob Mattingley (jacobm@stanford.edu).
#
# 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 .
#
# The LaTeX equation portions of this file were initially based on
# latexmath2png, by Kamil Kisiel (kamil@kamikisiel.net).
#
import sys
import os
import re
import time
import StringIO
from subprocess import *
import tempfile
def info():
print __doc__
print 'Platform: ' + sys.platform + '.'
print 'Python: %s, located at %s.' % (sys.version[:5], sys.executable)
print 'Equation support:',
(supported, message) = testeqsupport()
if supported:
print 'yes.'
else:
print 'no.'
print message
def testeqsupport():
supported = True
msg = ''
p = Popen('latex --version', shell=True, stdout=PIPE, stderr=PIPE)
rc = p.wait()
if rc != 0:
msg += ' latex: not found.\n'
supported = False
else:
msg += ' latex: ' + p.stdout.readlines()[0].rstrip() + '.\n'
p = Popen('dvipng --version', shell=True, stdout=PIPE, stderr=PIPE)
rc = p.wait()
if rc != 0:
msg += ' dvipng: not found.\n'
supported = False
else:
msg += ' dvipng: ' + p.stdout.readlines()[0].rstrip() + '.\n'
return (supported, msg[:-1])
class controlstruct(object):
def __init__(self, infile, outfile=None, conf=None, inname=None, eqs=True,
eqdir='eqs', eqdpi=130):
self.inname = inname
self.inf = infile
self.outf = outfile
self.conf = conf
self.linenum = 0
self.otherfiles = []
self.eqs = eqs
self.eqdir = eqdir
self.eqdpi = eqdpi
# Default to supporting equations until we know otherwise.
self.eqsupport = True
self.eqpackages = []
self.eqbd = {} # equation base depth.
self.baseline = None
def pushfile(self, newfile):
self.otherfiles.insert(0, self.inf)
self.inf = open(newfile)
def nextfile(self):
self.inf.close()
self.inf = self.otherfiles.pop(0)
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
will produce an index.html from index.jemdoc, using a default
configuration.
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
You can view version and installation details with
jemdoc --version
See http://jemdoc.jaboc.net/ for many 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.
|
[fwtitlestart]
[footerstart]
[lastupdated]
Page generated |, by jemdoc.
[sourcelink]
(source)
"""
b = ''
for l in a.splitlines(True):
if l.startswith(' '):
b += l[4:]
else:
b += l
return b
class JandalError(Exception):
pass
class NoEqSupport(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 menu item.
link = r.group(2)
# Don't use prefix if we have an absolute link.
if '://' not in r.group(2):
link = prefix + allreplace(link)
# replace spaces with nbsps.
# do do this, even though css would make it work - ie ignores.
menuitem = re.sub(r'(?&$%\.~[\]-]""", r'\\\g<0>', s)
def replacequoted(b):
"""Quotes {{raw html}} sections."""
r = re.compile(r'\{\{(.*?)\}\}', re.M + re.S)
m = r.search(b)
while m:
qb = quote(m.group(1))
b = b[:m.start()] + qb + b[m.end():]
m = r.search(b, m.start())
return b
def replacepercents(b):
# replace %sections% as +{{sections}}+. Do not replace if within a link.
r = re.compile(r'(?\n\n }}' % (fullfn, eqtext) + b[m.end():]
else:
b = b[:m.start()] + \
'{{}}' % (fullfn, eqtext, offset) + b[m.end():]
# jem: also clean out line breaks in the alttext?
m = r.search(b, m.start())
return replacequoted(b)
def replaceimages(b):
# works with [img{width}{height}{alttext} location caption].
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, f):
"""Does simple text replacements on a block of text. ('block replacements')"""
# Deal with literal backspaces.
if f.eqs and f.eqsupport:
b = replaceequations(b, f)
b = re.sub(r'\\\\', 'jemLITerl33talBS', b)
# Deal with {{html embedding}}.
b = replacequoted(b)
b = allreplace(b)
b = b.lstrip('-. \t') # remove leading spaces, tabs, dashes, dots.
b = replaceimages(b) # jem not sure if this is still used.
# Slightly nasty hackery in this next bit.
b = replacepercents(b)
b = replacelinks(b)
b = re.sub(r'BSNOTLINKLEFT12039XX', r'[', b)
b = re.sub(r'BSNOTLINKRIGHT12039XX', r']', b)
b = replacequoted(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 paragraph break. Caution! Should only use when we're already in
# a paragraph.
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':[], 'strings':False}
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'] = '#'
d['strings'] = True
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.
if hl['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 geneq(f, eq, dpi, wl, outname, packages=None):
# Open tex file.
tempdir = tempfile.gettempdir()
fd, texfile = tempfile.mkstemp('.tex', '', tempdir, True)
basefile = texfile[:-4]
g = os.fdopen(fd, 'w')
if packages is None:
packages = []
# Add the default list here.
packages += []
preamble = '\documentclass{article}\n'
for p in packages:
preamble += '\usepackage{%s}\n' % p
preamble += '\pagestyle{empty}\n\\begin{document}\n'
g.write(preamble)
# Write the equation itself.
if wl:
g.write('\\[%s\\]' % eq)
else:
g.write('$%s$' % eq)
# Finish off the tex file.
g.write('\n\\newpage\n\end{document}')
g.close()
try:
# Generate the DVI file
latexcmd = 'latex -file-line-error-style -interaction=nonstopmode ' + \
'-output-directory %s %s' % (tempdir, texfile)
p = Popen(latexcmd, shell=True, stdout=PIPE)
rc = p.wait()
if rc != 0:
for l in p.stdout.readlines():
print ' ' + l.rstrip()
raise Exception('latex error')
dvifile = basefile + '.dvi'
eqname = f.eqdir + '/' + outname + '.png'
dvicmd = 'dvipng --freetype0 -Q 8 -z 3 --depth -q -T tight -D %i -bg Transparent -o %s %s' % (dpi, eqname, dvifile)
# discard warnings, as well.
p = Popen(dvicmd, shell=True, stdout=PIPE, stderr=PIPE)
rc = p.wait()
if rc != 0:
print p.stderr.readlines()
raise Exception('dvipng error')
depth = int(p.stdout.readlines()[-1].split('=')[-1])
finally:
# Clean up.
exts = ['.tex', '.aux', '.dvi', '.log']
for ext in exts:
g = basefile + ext
if os.path.exists(g):
os.remove(g)
return (depth, eqname)
def dashlist(f, ordered=False):
level = 0
if ordered:
char = '.'
ul = 'ol'
else:
char = '-'
ul = 'ul'
while pc(f) == char:
(s, newlevel) = np(f, True, False)
# 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, '<%s>\n
' % ul)
elif newlevel < level:
out(f.outf, '\n
')
for i in range(level - newlevel):
#out(f.outf, '\n%s>\n
' % ul)
# demote means place '
' in the file.
out(f.outf, '%s>\n' % ul)
#out(f.outf, '\n
')
out(f.outf, '\n
')
else:
# same level, make a new list item.
out(f.outf, '\n
\n
')
out(f.outf, '
' + br(s, f) + '
')
level = newlevel
for i in range(level):
out(f.outf, '\n
\n%s>\n' % ul)
def colonlist(f):
out(f.outf, '
\n')
while pc(f) == ':':
s = np(f, eatblanks=False)
r = re.compile(r'\s*{(.*?)(?|\n', br(defpart, f))
hb(f.outf, '
|
\n', br(rest, f))
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])
if g[1] == 'jemdoc':
out(f.outf, f.conf['codeblockcontenttt'])
else:
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)
elif g[1] == 'jemdoc':
# doing this more nicely needs python 2.5.
for x in ('#', '~', '>>>', '\~', '{'):
if str(l).lstrip().startswith(x):
out(f.outf, '
')
out(f.outf, l + '
')
break
else:
for x in (':', '.', '-'):
if str(l).lstrip().startswith(x):
out(f.outf, ' ' + prependnbsps(l))
break
else:
if str(l).lstrip().startswith('='):
out(f.outf, prependnbsps(l) + ' ')
else:
out(f.outf, l)
else:
if l.startswith('\\#include{') or l.startswith('\\#includeraw{'):
out(f.outf, l[1:])
elif l.startswith('#') and doincludes(f, l[1:]):
continue
else:
language(f.outf, l, gethl(g[1]))
if not raw:
if g[1] == 'jemdoc':
out(f.outf, f.conf['codeblockendtt'])
else:
out(f.outf, f.conf['codeblockend'])
def prependnbsps(l):
g = re.search('(^ *)(.*)', l).groups()
return g[0].replace(' ', ' ') + g[1]
def inserttitle(f, t):
if t is not None:
hb(f.outf, f.conf['doctitle'], t)
# Look for a subtitle.
if pc(f) != '\n':
hb(f.outf, f.conf['subtitle'], br(np(f), f))
hb(f.outf, f.conf['doctitleend'], t)
def procfile(f):
f.linenum = 0
menu = None
# convert these to a dictionary.
showfooter = True
showsourcelink = False
showlastupdated = True
showlastupdatedtime = True
nodefaultcss = False
fwtitle = False
css = []
title = None
while pc(f, False) == '#':
l = f.inf.readline()
f.linenum += 1
if doincludes(f, l[1:]):
continue
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' % f.linenum)
if len(g) == 2:
menu = (f, g[0], g[1], '')
else:
menu = (f, g[0], g[1], g[2])
elif b.startswith('nofooter'):
showfooter = False
elif b.startswith('nodate'):
showlastupdated = False
elif b.startswith('notime'):
showlastupdatedtime = False
elif b.startswith('fwtitle'):
fwtitle = True
elif b.startswith('showsource'):
showsourcelink = True
elif b.startswith('nodefaultcss'):
nodefaultcss = True
elif b.startswith('addcss'):
r = re.compile(r'(?)|( ) *', ' ', t)
else:
t = None
#if title:
hb(f.outf, f.conf['windowtitle'], title)
out(f.outf, f.conf['bodystart'])
if fwtitle:
out(f.outf, f.conf['fwtitlestart'])
inserttitle(f, t)
out(f.outf, f.conf['fwtitleend'])
if menu:
out(f.outf, f.conf['menustart'])
insertmenuitems(*menu)
out(f.outf, f.conf['menuend'])
else:
out(f.outf, f.conf['nomenu'])
if not fwtitle:
inserttitle(f, t)
infoblock = False
imgblock = False
while 1: # wait for EOF.
p = pc(f)
if p == '':
break
# look for lists.
elif p == '-':
dashlist(f, False)
elif p == '.':
dashlist(f, True)
elif p == ':':
colonlist(f)
# look for titles.
elif p == '=':
(s, c) = nl(f, True)
# trim trailing \n.
s = s[:-1]
hb(f.outf, '|\n' % (c, c), br(s, f))
# look for comments.
elif p == '#':
l = 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], f)
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, '
')
imgblock = True
else:
raise JandalError("couldn't handle block", f.linenum)
else:
s = br(np(f), f)
if s:
hb(f.outf, '
|
\n', s)
if showfooter and (showlastupdated or showsourcelink):
out(f.outf, f.conf['footerstart'])
if showlastupdated:
if showlastupdatedtime:
ts = '%Y-%m-%d %H:%M:%S %Z'
else:
ts = '%Y-%m-%d'
s = time.strftime(ts, time.localtime(time.time()))
hb(f.outf, f.conf['lastupdated'], s)
if showsourcelink:
hb(f.outf, f.conf['sourcelink'], f.inname)
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
if sys.argv[1] == '--version':
info()
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, inname)
procfile(f)
#
if __name__ == '__main__':
main()