Replace pandoc-based md-to-man conversion with a custom python script.
Hopefully this will appease the people who can't install pandoc on MacOS.
This commit is contained in:
parent
3fdaa7cb0c
commit
e174b09d5f
6 changed files with 371 additions and 5 deletions
2
Documentation/.gitignore
vendored
2
Documentation/.gitignore
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
/*.1
|
*.1
|
||||||
/md-to-man
|
/md-to-man
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/bin/ls *.md |
|
/bin/ls *.md t/*.md |
|
||||||
sed 's/\.md/.1/' |
|
sed 's/\.md/.1/' |
|
||||||
xargs redo-ifchange
|
xargs redo-ifchange
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
rm -f *~ .*~ *.1 md-to-man *.tmp
|
rm -f *~ .*~ *.1 t/*.1 md-to-man *.tmp t/*.tmp
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
if pandoc </dev/null 2>/dev/null; then
|
redo-ifchange md2man.py
|
||||||
echo 'pandoc -s -r markdown -w man -o $3 $1.md.tmp'
|
if ./md2man.py </dev/null >/dev/null 2>/dev/null; then
|
||||||
|
echo './md2man.py $1.md.tmp'
|
||||||
else
|
else
|
||||||
(IFS=:; for DIR in $PATH; do redo-ifcreate "$DIR/pandoc"; done)
|
(IFS=:; for DIR in $PATH; do redo-ifcreate "$DIR/pandoc"; done)
|
||||||
echo "Warning: pandoc not installed; can't generate manpages." >&2
|
echo "Warning: pandoc not installed; can't generate manpages." >&2
|
||||||
|
|
|
||||||
278
Documentation/md2man.py
Executable file
278
Documentation/md2man.py
Executable file
|
|
@ -0,0 +1,278 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import sys, os, markdown, re
|
||||||
|
from BeautifulSoup import BeautifulSoup
|
||||||
|
|
||||||
|
def _split_lines(s):
|
||||||
|
return re.findall(r'([^\n]*\n?)', s)
|
||||||
|
|
||||||
|
|
||||||
|
class Writer:
|
||||||
|
def __init__(self):
|
||||||
|
self.started = False
|
||||||
|
self.indent = 0
|
||||||
|
self.last_wrote = '\n'
|
||||||
|
|
||||||
|
def _write(self, s):
|
||||||
|
if s:
|
||||||
|
self.last_wrote = s
|
||||||
|
sys.stdout.write(s)
|
||||||
|
|
||||||
|
def writeln(self, s):
|
||||||
|
if s:
|
||||||
|
self.linebreak()
|
||||||
|
self._write('%s\n' % s)
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
if s:
|
||||||
|
self.para()
|
||||||
|
for line in _split_lines(s):
|
||||||
|
if line.startswith('.'):
|
||||||
|
self._write('\\&' + line)
|
||||||
|
else:
|
||||||
|
self._write(line)
|
||||||
|
|
||||||
|
def linebreak(self):
|
||||||
|
if not self.last_wrote.endswith('\n'):
|
||||||
|
self._write('\n')
|
||||||
|
|
||||||
|
def para(self, bullet=None):
|
||||||
|
if not self.started:
|
||||||
|
if not bullet:
|
||||||
|
bullet = ' '
|
||||||
|
if not self.indent:
|
||||||
|
self.writeln(_macro('.PP'))
|
||||||
|
else:
|
||||||
|
assert(self.indent >= 2)
|
||||||
|
prefix = ' '*(self.indent-2) + bullet + ' '
|
||||||
|
self.writeln('.IP "%s" %d' % (prefix, self.indent))
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
def end_para(self):
|
||||||
|
self.linebreak()
|
||||||
|
self.started = False
|
||||||
|
|
||||||
|
def start_bullet(self):
|
||||||
|
self.indent += 3
|
||||||
|
self.para(bullet='\\[bu]')
|
||||||
|
|
||||||
|
def end_bullet(self):
|
||||||
|
self.indent -= 3
|
||||||
|
self.end_para()
|
||||||
|
|
||||||
|
w = Writer()
|
||||||
|
|
||||||
|
|
||||||
|
def _macro(name, *args):
|
||||||
|
if not name.startswith('.'):
|
||||||
|
raise ValueError('macro names must start with "."')
|
||||||
|
fixargs = []
|
||||||
|
for i in args:
|
||||||
|
i = str(i)
|
||||||
|
i = i.replace('\\', '')
|
||||||
|
i = i.replace('"', "'")
|
||||||
|
if (' ' in i) or not i:
|
||||||
|
i = '"%s"' % i
|
||||||
|
fixargs.append(i)
|
||||||
|
return ' '.join([name] + list(fixargs))
|
||||||
|
|
||||||
|
|
||||||
|
def macro(name, *args):
|
||||||
|
w.writeln(_macro(name, *args))
|
||||||
|
|
||||||
|
|
||||||
|
def _force_string(owner, tag):
|
||||||
|
if tag.string:
|
||||||
|
return tag.string
|
||||||
|
else:
|
||||||
|
out = ''
|
||||||
|
for i in tag:
|
||||||
|
if not (i.string or i.name in ['a', 'br']):
|
||||||
|
raise ValueError('"%s" tags must contain only strings: '
|
||||||
|
'got %r: %r' % (owner.name, tag.name, tag))
|
||||||
|
out += _force_string(owner, i)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(s):
|
||||||
|
s = s.replace('\\', '\\\\')
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _bitlist(tag):
|
||||||
|
if not getattr(tag, 'contents', None):
|
||||||
|
for i in _split_lines(str(tag)):
|
||||||
|
yield None,_clean(i)
|
||||||
|
else:
|
||||||
|
for e in tag:
|
||||||
|
name = getattr(e, 'name', None)
|
||||||
|
if name in ['a', 'br']:
|
||||||
|
name = None # just treat as simple text
|
||||||
|
s = _force_string(tag, e)
|
||||||
|
if name:
|
||||||
|
yield name,_clean(s)
|
||||||
|
else:
|
||||||
|
for i in _split_lines(s):
|
||||||
|
yield None,_clean(i)
|
||||||
|
|
||||||
|
|
||||||
|
def _bitlist_simple(tag):
|
||||||
|
for typ,text in _bitlist(tag):
|
||||||
|
if typ and not typ in ['em', 'strong', 'code']:
|
||||||
|
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
|
||||||
|
yield text
|
||||||
|
|
||||||
|
|
||||||
|
def _text(bitlist):
|
||||||
|
out = ''
|
||||||
|
for typ,text in bitlist:
|
||||||
|
if not typ:
|
||||||
|
out += text
|
||||||
|
elif typ == 'em':
|
||||||
|
out += '\\fI%s\\fR' % text
|
||||||
|
elif typ in ['strong', 'code']:
|
||||||
|
out += '\\fB%s\\fR' % text
|
||||||
|
else:
|
||||||
|
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
|
||||||
|
out = out.strip()
|
||||||
|
out = re.sub(re.compile(r'^\s+', re.M), '', out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def text(tag):
|
||||||
|
w.write(_text(_bitlist(tag)))
|
||||||
|
|
||||||
|
|
||||||
|
# This is needed because .BI (and .BR, .RB, etc) are weird little state
|
||||||
|
# machines that alternate between two fonts. So if someone says something
|
||||||
|
# like foo<b>chicken</b><b>wicken</b>dicken we have to convert that to
|
||||||
|
# .BI foo chickenwicken dicken
|
||||||
|
def _boldline(l):
|
||||||
|
out = ['']
|
||||||
|
last_bold = False
|
||||||
|
for typ,text in l:
|
||||||
|
nonzero = not not typ
|
||||||
|
if nonzero != last_bold:
|
||||||
|
last_bold = not last_bold
|
||||||
|
out.append('')
|
||||||
|
out[-1] += re.sub(r'\s+', ' ', text)
|
||||||
|
macro('.BI', *out)
|
||||||
|
|
||||||
|
|
||||||
|
def do_definition(tag):
|
||||||
|
w.end_para()
|
||||||
|
macro('.TP')
|
||||||
|
w.started = True
|
||||||
|
split = 0
|
||||||
|
pre = []
|
||||||
|
post = []
|
||||||
|
for typ,text in _bitlist(tag):
|
||||||
|
if split:
|
||||||
|
post.append((typ,text))
|
||||||
|
elif text.lstrip().startswith(': '):
|
||||||
|
split = 1
|
||||||
|
post.append((typ,text.lstrip()[2:].lstrip()))
|
||||||
|
else:
|
||||||
|
pre.append((typ,text))
|
||||||
|
_boldline(pre)
|
||||||
|
w.write(_text(post))
|
||||||
|
|
||||||
|
|
||||||
|
def do_list(tag):
|
||||||
|
for i in tag:
|
||||||
|
name = getattr(i, 'name', '').lower()
|
||||||
|
if not name and not str(i).strip():
|
||||||
|
pass
|
||||||
|
elif name != 'li':
|
||||||
|
raise ValueError('only <li> is allowed inside <ul>: got %r' % i)
|
||||||
|
else:
|
||||||
|
w.start_bullet()
|
||||||
|
for xi in i:
|
||||||
|
do(xi)
|
||||||
|
w.end_para()
|
||||||
|
w.end_bullet()
|
||||||
|
|
||||||
|
|
||||||
|
def do(tag):
|
||||||
|
name = getattr(tag, 'name', '').lower()
|
||||||
|
if not name:
|
||||||
|
text(tag)
|
||||||
|
elif name == 'h1':
|
||||||
|
macro('.SH', _force_string(tag, tag).upper())
|
||||||
|
w.started = True
|
||||||
|
elif name == 'h2':
|
||||||
|
macro('.SS', _force_string(tag, tag))
|
||||||
|
w.started = True
|
||||||
|
elif name.startswith('h') and len(name)==2:
|
||||||
|
raise ValueError('%r invalid - man page headers must be h1 or h2'
|
||||||
|
% name)
|
||||||
|
elif name == 'pre':
|
||||||
|
t = _force_string(tag.code, tag.code)
|
||||||
|
if t.strip():
|
||||||
|
macro('.RS', '+4n')
|
||||||
|
macro('.nf')
|
||||||
|
w.write(_clean(t).rstrip())
|
||||||
|
macro('.fi')
|
||||||
|
macro('.RE')
|
||||||
|
w.end_para()
|
||||||
|
elif name == 'p':
|
||||||
|
g = re.match(re.compile(r'([^\n]*)\n +: +(.*)', re.S), str(tag))
|
||||||
|
if g:
|
||||||
|
# it's a definition list (which some versions of python-markdown
|
||||||
|
# don't support, including the one in Debian-lenny, so we can't
|
||||||
|
# enable that markdown extension). Fake it up.
|
||||||
|
do_definition(tag)
|
||||||
|
else:
|
||||||
|
text(tag)
|
||||||
|
w.end_para()
|
||||||
|
elif name == 'ul':
|
||||||
|
do_list(tag)
|
||||||
|
else:
|
||||||
|
raise ValueError('non-man-compatible html tag %r' % name)
|
||||||
|
|
||||||
|
|
||||||
|
PROD='Untitled'
|
||||||
|
VENDOR='Vendor Name'
|
||||||
|
SECTION='9'
|
||||||
|
GROUPNAME='User Commands'
|
||||||
|
DATE=''
|
||||||
|
AUTHOR=''
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
for n in sys.argv[1:]:
|
||||||
|
lines += open(n).read().decode('utf8').split('\n')
|
||||||
|
else:
|
||||||
|
lines += sys.stdin.read().decode('utf8').split('\n')
|
||||||
|
|
||||||
|
# parse pandoc-style document headers (not part of markdown)
|
||||||
|
g = re.match(r'^%\s+(.*?)\((.*?)\)\s+(.*)$', lines[0])
|
||||||
|
if g:
|
||||||
|
PROD = g.group(1)
|
||||||
|
SECTION = g.group(2)
|
||||||
|
VENDOR = g.group(3)
|
||||||
|
lines.pop(0)
|
||||||
|
g = re.match(r'^%\s+(.*?)$', lines[0])
|
||||||
|
if g:
|
||||||
|
AUTHOR = g.group(1)
|
||||||
|
lines.pop(0)
|
||||||
|
g = re.match(r'^%\s+(.*?)$', lines[0])
|
||||||
|
if g:
|
||||||
|
DATE = g.group(1)
|
||||||
|
lines.pop(0)
|
||||||
|
g = re.match(r'^%\s+(.*?)$', lines[0])
|
||||||
|
if g:
|
||||||
|
GROUPNAME = g.group(1)
|
||||||
|
lines.pop(0)
|
||||||
|
|
||||||
|
inp = '\n'.join(lines)
|
||||||
|
if AUTHOR:
|
||||||
|
inp += ('\n# AUTHOR\n\n%s\n' % AUTHOR).replace('<', '\\<')
|
||||||
|
|
||||||
|
html = markdown.markdown(inp)
|
||||||
|
soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES)
|
||||||
|
|
||||||
|
macro('.TH', PROD.upper(), SECTION, DATE, VENDOR, GROUPNAME)
|
||||||
|
macro('.ad', 'l') # left justified
|
||||||
|
macro('.nh') # disable hyphenation
|
||||||
|
for e in soup:
|
||||||
|
do(e)
|
||||||
87
Documentation/t/testitem.md
Normal file
87
Documentation/t/testitem.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
% redo(1) Redo %VERSION%
|
||||||
|
% Avery Pennarun <apenwarr@gmail.com>
|
||||||
|
% %DATE%
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
redo-always - mark the current target as always needing to be rebuilt
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
redo-always
|
||||||
|
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
Normally redo-always is run from a .do file that has been
|
||||||
|
executed by `redo`(1). See `redo`(1) for more details.
|
||||||
|
|
||||||
|
This is a "quoted string."
|
||||||
|
|
||||||
|
"entirely quoted"
|
||||||
|
|
||||||
|
.starting with dot
|
||||||
|
|
||||||
|
I'm a \big \\backslasher!
|
||||||
|
|
||||||
|
This **line** has _multiple_ `formats`(interspersed) *with* each _other_
|
||||||
|
and *here is a multi
|
||||||
|
line italic.*
|
||||||
|
|
||||||
|
This line has an & and a < and a >.
|
||||||
|
|
||||||
|
- "line starting with quoted"
|
||||||
|
|
||||||
|
Here's some code
|
||||||
|
with indentation
|
||||||
|
yay! (a \backslash and <brackets>)
|
||||||
|
"foo"
|
||||||
|
|
||||||
|
skipped a line
|
||||||
|
indented
|
||||||
|
|
||||||
|
another skip
|
||||||
|
|
||||||
|
- -starting with dash
|
||||||
|
|
||||||
|
- .starting with dot
|
||||||
|
|
||||||
|
chicken
|
||||||
|
|
||||||
|
- list item
|
||||||
|
with more text
|
||||||
|
|
||||||
|
and even more
|
||||||
|
|
||||||
|
- second list
|
||||||
|
- third list
|
||||||
|
|
||||||
|
wicken
|
||||||
|
|
||||||
|
- list 1a
|
||||||
|
- list 1b
|
||||||
|
- list 2
|
||||||
|
- list 3
|
||||||
|
|
||||||
|
barf
|
||||||
|
|
||||||
|
First line
|
||||||
|
: definition list.
|
||||||
|
with
|
||||||
|
multiple
|
||||||
|
lines!
|
||||||
|
|
||||||
|
--item=*value*
|
||||||
|
: a description.
|
||||||
|
|
||||||
|
`-x`
|
||||||
|
: more stuff. if you had a lot of text, this is what it
|
||||||
|
would look like. It goes on and on and on.
|
||||||
|
|
||||||
|
a line with *altogether* "too much" stuff on it to realistically *make* it in a definition list
|
||||||
|
: and yet, here we are.
|
||||||
|
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
`redo`(1), `redo-ifcreate`(1), `redo-ifchange`(1), `redo-stamp`(1)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue