Skip to content

Commit cbc32ff

Browse files
committed
Import the non-pandoc manpage generator from redo.
This makes it easier (possible?) to generate sshuttle.8 from sshuttle.md on MacOS. We also import the git-enhanced version numbering magic so the generated manpage can have a real version number.
1 parent 6698992 commit cbc32ff

20 files changed

+390
-5
lines changed

Documentation/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.8
2+
/md-to-man
3+
/*.md.tmp

Documentation/all.do

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/bin/ls *.md |
2+
sed 's/\.md/.8/' |
3+
xargs redo-ifchange
4+
5+
redo-always

Documentation/clean.do

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rm -f *~ .*~ *.8 t/*.8 md-to-man *.tmp t/*.tmp

Documentation/default.8.do

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
redo-ifchange md-to-man $2.md.tmp
2+
. ./md-to-man $1 $2 $3

Documentation/default.md.tmp.do

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
redo-ifchange ../version/vars $2.md
2+
. ../version/vars
3+
sed -e "s/%VERSION%/$TAG/" -e "s/%DATE%/$DATE/" $2.md

Documentation/md-to-man.do

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
redo-ifchange md2man.py
2+
if ./md2man.py </dev/null >/dev/null; then
3+
echo './md2man.py $2.md.tmp'
4+
else
5+
echo "Warning: md2man.py missing modules; can't generate manpages." >&2
6+
echo "Warning: try this: sudo easy_install markdown BeautifulSoup" >&2
7+
echo 'echo Skipping: $2.1 >&2'
8+
fi

Documentation/md2man.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
#!/usr/bin/env python
2+
import sys, os, markdown, re
3+
from BeautifulSoup import BeautifulSoup
4+
5+
def _split_lines(s):
6+
return re.findall(r'([^\n]*\n?)', s)
7+
8+
9+
class Writer:
10+
def __init__(self):
11+
self.started = False
12+
self.indent = 0
13+
self.last_wrote = '\n'
14+
15+
def _write(self, s):
16+
if s:
17+
self.last_wrote = s
18+
sys.stdout.write(s)
19+
20+
def writeln(self, s):
21+
if s:
22+
self.linebreak()
23+
self._write('%s\n' % s)
24+
25+
def write(self, s):
26+
if s:
27+
self.para()
28+
for line in _split_lines(s):
29+
if line.startswith('.'):
30+
self._write('\\&' + line)
31+
else:
32+
self._write(line)
33+
34+
def linebreak(self):
35+
if not self.last_wrote.endswith('\n'):
36+
self._write('\n')
37+
38+
def para(self, bullet=None):
39+
if not self.started:
40+
if not bullet:
41+
bullet = ' '
42+
if not self.indent:
43+
self.writeln(_macro('.PP'))
44+
else:
45+
assert(self.indent >= 2)
46+
prefix = ' '*(self.indent-2) + bullet + ' '
47+
self.writeln('.IP "%s" %d' % (prefix, self.indent))
48+
self.started = True
49+
50+
def end_para(self):
51+
self.linebreak()
52+
self.started = False
53+
54+
def start_bullet(self):
55+
self.indent += 3
56+
self.para(bullet='\\[bu]')
57+
58+
def end_bullet(self):
59+
self.indent -= 3
60+
self.end_para()
61+
62+
w = Writer()
63+
64+
65+
def _macro(name, *args):
66+
if not name.startswith('.'):
67+
raise ValueError('macro names must start with "."')
68+
fixargs = []
69+
for i in args:
70+
i = str(i)
71+
i = i.replace('\\', '')
72+
i = i.replace('"', "'")
73+
if (' ' in i) or not i:
74+
i = '"%s"' % i
75+
fixargs.append(i)
76+
return ' '.join([name] + list(fixargs))
77+
78+
79+
def macro(name, *args):
80+
w.writeln(_macro(name, *args))
81+
82+
83+
def _force_string(owner, tag):
84+
if tag.string:
85+
return tag.string
86+
else:
87+
out = ''
88+
for i in tag:
89+
if not (i.string or i.name in ['a', 'br']):
90+
raise ValueError('"%s" tags must contain only strings: '
91+
'got %r: %r' % (owner.name, tag.name, tag))
92+
out += _force_string(owner, i)
93+
return out
94+
95+
96+
def _clean(s):
97+
s = s.replace('\\', '\\\\')
98+
return s
99+
100+
101+
def _bitlist(tag):
102+
if getattr(tag, 'contents', None) == None:
103+
for i in _split_lines(str(tag)):
104+
yield None,_clean(i)
105+
else:
106+
for e in tag:
107+
name = getattr(e, 'name', None)
108+
if name in ['a', 'br']:
109+
name = None # just treat as simple text
110+
s = _force_string(tag, e)
111+
if name:
112+
yield name,_clean(s)
113+
else:
114+
for i in _split_lines(s):
115+
yield None,_clean(i)
116+
117+
118+
def _bitlist_simple(tag):
119+
for typ,text in _bitlist(tag):
120+
if typ and not typ in ['em', 'strong', 'code']:
121+
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
122+
yield text
123+
124+
125+
def _text(bitlist):
126+
out = ''
127+
for typ,text in bitlist:
128+
if not typ:
129+
out += text
130+
elif typ == 'em':
131+
out += '\\fI%s\\fR' % text
132+
elif typ in ['strong', 'code']:
133+
out += '\\fB%s\\fR' % text
134+
else:
135+
raise ValueError('unexpected tag %r inside %r' % (typ, tag.name))
136+
out = out.strip()
137+
out = re.sub(re.compile(r'^\s+', re.M), '', out)
138+
return out
139+
140+
141+
def text(tag):
142+
w.write(_text(_bitlist(tag)))
143+
144+
145+
# This is needed because .BI (and .BR, .RB, etc) are weird little state
146+
# machines that alternate between two fonts. So if someone says something
147+
# like foo<b>chicken</b><b>wicken</b>dicken we have to convert that to
148+
# .BI foo chickenwicken dicken
149+
def _boldline(l):
150+
out = ['']
151+
last_bold = False
152+
for typ,text in l:
153+
nonzero = not not typ
154+
if nonzero != last_bold:
155+
last_bold = not last_bold
156+
out.append('')
157+
out[-1] += re.sub(r'\s+', ' ', text)
158+
macro('.BI', *out)
159+
160+
161+
def do_definition(tag):
162+
w.end_para()
163+
macro('.TP')
164+
w.started = True
165+
split = 0
166+
pre = []
167+
post = []
168+
for typ,text in _bitlist(tag):
169+
if split:
170+
post.append((typ,text))
171+
elif text.lstrip().startswith(': '):
172+
split = 1
173+
post.append((typ,text.lstrip()[2:].lstrip()))
174+
else:
175+
pre.append((typ,text))
176+
_boldline(pre)
177+
w.write(_text(post))
178+
179+
180+
def do_list(tag):
181+
for i in tag:
182+
name = getattr(i, 'name', '').lower()
183+
if not name and not str(i).strip():
184+
pass
185+
elif name != 'li':
186+
raise ValueError('only <li> is allowed inside <ul>: got %r' % i)
187+
else:
188+
w.start_bullet()
189+
for xi in i:
190+
do(xi)
191+
w.end_para()
192+
w.end_bullet()
193+
194+
195+
def do(tag):
196+
name = getattr(tag, 'name', '').lower()
197+
if not name:
198+
text(tag)
199+
elif name == 'h1':
200+
macro('.SH', _force_string(tag, tag).upper())
201+
w.started = True
202+
elif name == 'h2':
203+
macro('.SS', _force_string(tag, tag))
204+
w.started = True
205+
elif name.startswith('h') and len(name)==2:
206+
raise ValueError('%r invalid - man page headers must be h1 or h2'
207+
% name)
208+
elif name == 'pre':
209+
t = _force_string(tag.code, tag.code)
210+
if t.strip():
211+
macro('.RS', '+4n')
212+
macro('.nf')
213+
w.write(_clean(t).rstrip())
214+
macro('.fi')
215+
macro('.RE')
216+
w.end_para()
217+
elif name == 'p' or name == 'br':
218+
g = re.match(re.compile(r'([^\n]*)\n +: +(.*)', re.S), str(tag))
219+
if g:
220+
# it's a definition list (which some versions of python-markdown
221+
# don't support, including the one in Debian-lenny, so we can't
222+
# enable that markdown extension). Fake it up.
223+
do_definition(tag)
224+
else:
225+
text(tag)
226+
w.end_para()
227+
elif name == 'ul':
228+
do_list(tag)
229+
else:
230+
raise ValueError('non-man-compatible html tag %r' % name)
231+
232+
233+
PROD='Untitled'
234+
VENDOR='Vendor Name'
235+
SECTION='9'
236+
GROUPNAME='User Commands'
237+
DATE=''
238+
AUTHOR=''
239+
240+
lines = []
241+
if len(sys.argv) > 1:
242+
for n in sys.argv[1:]:
243+
lines += open(n).read().decode('utf8').split('\n')
244+
else:
245+
lines += sys.stdin.read().decode('utf8').split('\n')
246+
247+
# parse pandoc-style document headers (not part of markdown)
248+
g = re.match(r'^%\s+(.*?)\((.*?)\)\s+(.*)$', lines[0])
249+
if g:
250+
PROD = g.group(1)
251+
SECTION = g.group(2)
252+
VENDOR = g.group(3)
253+
lines.pop(0)
254+
g = re.match(r'^%\s+(.*?)$', lines[0])
255+
if g:
256+
AUTHOR = g.group(1)
257+
lines.pop(0)
258+
g = re.match(r'^%\s+(.*?)$', lines[0])
259+
if g:
260+
DATE = g.group(1)
261+
lines.pop(0)
262+
g = re.match(r'^%\s+(.*?)$', lines[0])
263+
if g:
264+
GROUPNAME = g.group(1)
265+
lines.pop(0)
266+
267+
inp = '\n'.join(lines)
268+
if AUTHOR:
269+
inp += ('\n# AUTHOR\n\n%s\n' % AUTHOR).replace('<', '\\<')
270+
271+
html = markdown.markdown(inp)
272+
soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES)
273+
274+
macro('.TH', PROD.upper(), SECTION, DATE, VENDOR, GROUPNAME)
275+
macro('.ad', 'l') # left justified
276+
macro('.nh') # disable hyphenation
277+
for e in soup:
278+
do(e)

sshuttle.md renamed to Documentation/sshuttle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
% sshuttle(8) Sshuttle 0.46
1+
% sshuttle(8) Sshuttle %VERSION%
22
% Avery Pennarun <[email protected]>
3-
% 2011-01-25
3+
% %DATE%
44

55
# NAME
66

all.do

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
exec >&2
22
UI=
33
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all
4-
redo-ifchange sshuttle.8 $UI
4+
redo-ifchange Documentation/all $UI
55

66
echo
77
echo "What now?"
88
[ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app"
99
echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0"
1010
echo "- Read the README: less README.md"
11-
echo "- Read the man page: less sshuttle.md"
11+
echo "- Read the man page: less Documentation/sshuttle.md"

clean.do

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
redo ui-macos/clean
1+
redo ui-macos/clean Documentation/clean
22
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc

0 commit comments

Comments
 (0)