mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-01-06 05:58:01 -05:00
358 lines
11 KiB
Python
Executable File
358 lines
11 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
# This script takes a manpage written in markdown and turns it into an html web
|
|
# page and a nroff man page. The input file must have the name of the program
|
|
# and the section in this format: NAME.NUM.md. The output files are written
|
|
# into the current directory named NAME.NUM.html and NAME.NUM. The input
|
|
# format has one extra extension: if a numbered list starts at 0, it is turned
|
|
# into a description list. The dl's dt tag is taken from the contents of the
|
|
# first tag inside the li, which is usually a p, code, or strong tag. The
|
|
# cmarkgfm or commonmark lib is used to transforms the input file into html.
|
|
# The html.parser is used as a state machine that both tweaks the html and
|
|
# outputs the nroff data based on the html tags.
|
|
#
|
|
# Copyright (C) 2020 Wayne Davison
|
|
#
|
|
# This program is freely redistributable.
|
|
|
|
import sys, os, re, argparse, subprocess, time
|
|
from html.parser import HTMLParser
|
|
|
|
CONSUMES_TXT = set('h1 h2 p li pre'.split())
|
|
|
|
HTML_START = """\
|
|
<html><head>
|
|
<title>%s</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto&family=Roboto+Mono&display=swap" rel="stylesheet">
|
|
<style>
|
|
body {
|
|
max-width: 50em;
|
|
margin: auto;
|
|
}
|
|
body, b, strong, u {
|
|
font-family: 'Roboto', sans-serif;
|
|
}
|
|
code {
|
|
font-family: 'Roboto Mono', monospace;
|
|
font-weight: bold;
|
|
}
|
|
pre code {
|
|
display: block;
|
|
font-weight: normal;
|
|
}
|
|
blockquote pre code {
|
|
background: #f1f1f1;
|
|
}
|
|
dd p:first-of-type {
|
|
margin-block-start: 0em;
|
|
}
|
|
</style>
|
|
</head><body>
|
|
"""
|
|
|
|
HTML_END = """\
|
|
<div style="float: right"><p><i>%s</i></p></div>
|
|
</body></html>
|
|
"""
|
|
|
|
MAN_START = r"""
|
|
.TH "%s" "%s" "%s" "%s" "User Commands"
|
|
""".lstrip()
|
|
|
|
MAN_END = """\
|
|
"""
|
|
|
|
NORM_FONT = ('\1', r"\fP")
|
|
BOLD_FONT = ('\2', r"\fB")
|
|
ULIN_FONT = ('\3', r"\fI")
|
|
|
|
md_parser = None
|
|
|
|
def main():
|
|
fi = re.match(r'^(?P<fn>(?P<srcdir>.+/)?(?P<name>(?P<prog>[^/]+)\.(?P<sect>\d+))\.md)$', args.mdfile)
|
|
if not fi:
|
|
die('Failed to parse NAME.NUM.md out of input file:', args.mdfile)
|
|
fi = argparse.Namespace(**fi.groupdict())
|
|
|
|
if not fi.srcdir:
|
|
fi.srcdir = './'
|
|
|
|
fi.title = fi.prog + '(' + fi.sect + ') man page'
|
|
fi.mtime = None
|
|
|
|
if os.path.lexists(fi.srcdir + '.git'):
|
|
fi.mtime = int(subprocess.check_output('git log -1 --format=%at'.split()))
|
|
|
|
chk_files = 'NEWS.md Makefile'.split()
|
|
for fn in chk_files:
|
|
try:
|
|
st = os.lstat(fi.srcdir + fn)
|
|
except:
|
|
die('Failed to find', fi.srcdir + fn)
|
|
if not fi.mtime:
|
|
fi.mtime = st.st_mtime
|
|
|
|
fi.date = time.strftime('%d %b %Y', time.localtime(fi.mtime))
|
|
|
|
env_subs = { 'prefix': os.environ.get('RSYNC_OVERRIDE_PREFIX', None) }
|
|
|
|
with open(fi.srcdir + 'Makefile', 'r', encoding='utf-8') as fh:
|
|
for line in fh:
|
|
m = re.match(r'^(\w+)=(.+)', line)
|
|
if not m:
|
|
continue
|
|
var, val = (m[1], m[2])
|
|
if var == 'prefix' and env_subs[var] is not None:
|
|
continue
|
|
while re.search(r'\$\{', val):
|
|
val = re.sub(r'\$\{(\w+)\}', lambda m: env_subs[m[1]], val)
|
|
env_subs[var] = val
|
|
if var == 'VERSION':
|
|
break
|
|
|
|
with open(fi.fn, 'r', encoding='utf-8') as fh:
|
|
txt = fh.read()
|
|
|
|
txt = re.sub(r'@VERSION@', env_subs['VERSION'], txt)
|
|
txt = re.sub(r'@LIBDIR@', env_subs['libdir'], txt)
|
|
fi.html_in = md_parser(txt)
|
|
txt = None
|
|
|
|
fi.man_headings = (fi.prog, fi.sect, fi.date, fi.prog + ' ' + env_subs['VERSION'])
|
|
|
|
HtmlToManPage(fi)
|
|
|
|
if args.test:
|
|
print("The test was successful.")
|
|
return
|
|
|
|
for fn, txt in ((fi.name + '.html', fi.html_out), (fi.name, fi.man_out)):
|
|
print("Wrote:", fn)
|
|
with open(fn, 'w', encoding='utf-8') as fh:
|
|
fh.write(txt)
|
|
|
|
|
|
def html_via_cmarkgfm(txt):
|
|
return cmarkgfm.markdown_to_html(txt)
|
|
|
|
|
|
def html_via_commonmark(txt):
|
|
return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt))
|
|
|
|
|
|
class HtmlToManPage(HTMLParser):
|
|
def __init__(self, fi):
|
|
HTMLParser.__init__(self, convert_charrefs=True)
|
|
|
|
st = self.state = argparse.Namespace(
|
|
list_state = [ ],
|
|
p_macro = ".P\n",
|
|
at_first_tag_in_li = False,
|
|
at_first_tag_in_dd = False,
|
|
dt_from = None,
|
|
in_pre = False,
|
|
html_out = [ HTML_START % fi.title ],
|
|
man_out = [ MAN_START % fi.man_headings ],
|
|
txt = '',
|
|
)
|
|
|
|
self.feed(fi.html_in)
|
|
fi.html_in = None
|
|
|
|
st.html_out.append(HTML_END % fi.date)
|
|
st.man_out.append(MAN_END)
|
|
|
|
fi.html_out = ''.join(st.html_out)
|
|
st.html_out = None
|
|
|
|
fi.man_out = ''.join(st.man_out)
|
|
st.man_out = None
|
|
|
|
|
|
def handle_starttag(self, tag, attrs_list):
|
|
st = self.state
|
|
if args.debug:
|
|
self.output_debug('START', (tag, attrs_list))
|
|
if st.at_first_tag_in_li:
|
|
if st.list_state[-1] == 'dl':
|
|
st.dt_from = tag
|
|
if tag == 'p':
|
|
tag = 'dt'
|
|
else:
|
|
st.html_out.append('<dt>')
|
|
st.at_first_tag_in_li = False
|
|
if tag == 'p':
|
|
if not st.at_first_tag_in_dd:
|
|
st.man_out.append(st.p_macro)
|
|
elif tag == 'li':
|
|
st.at_first_tag_in_li = True
|
|
lstate = st.list_state[-1]
|
|
if lstate == 'dl':
|
|
return
|
|
if lstate == 'o':
|
|
st.man_out.append(".IP o\n")
|
|
else:
|
|
st.man_out.append(".IP " + str(lstate) + ".\n")
|
|
st.list_state[-1] += 1
|
|
elif tag == 'blockquote':
|
|
st.man_out.append(".RS 4\n")
|
|
elif tag == 'pre':
|
|
st.in_pre = True
|
|
st.man_out.append(st.p_macro + ".nf\n")
|
|
elif tag == 'code' and not st.in_pre:
|
|
st.txt += BOLD_FONT[0]
|
|
elif tag == 'strong' or tag == 'b':
|
|
st.txt += BOLD_FONT[0]
|
|
elif tag == 'em' or tag == 'i':
|
|
tag = 'u' # Change it into underline to be more like the man page
|
|
st.txt += ULIN_FONT[0]
|
|
elif tag == 'ol':
|
|
start = 1
|
|
for var, val in attrs_list:
|
|
if var == 'start':
|
|
start = int(val) # We only support integers.
|
|
break
|
|
if st.list_state:
|
|
st.man_out.append(".RS\n")
|
|
if start == 0:
|
|
tag = 'dl'
|
|
attrs_list = [ ]
|
|
st.list_state.append('dl')
|
|
else:
|
|
st.list_state.append(start)
|
|
st.man_out.append(st.p_macro)
|
|
st.p_macro = ".IP\n"
|
|
elif tag == 'ul':
|
|
st.man_out.append(st.p_macro)
|
|
if st.list_state:
|
|
st.man_out.append(".RS\n")
|
|
st.p_macro = ".IP\n"
|
|
st.list_state.append('o')
|
|
st.html_out.append('<' + tag + ''.join(' ' + var + '="' + htmlify(val) + '"' for var, val in attrs_list) + '>')
|
|
st.at_first_tag_in_dd = False
|
|
|
|
|
|
def handle_endtag(self, tag):
|
|
st = self.state
|
|
if args.debug:
|
|
self.output_debug('END', (tag,))
|
|
if tag in CONSUMES_TXT or st.dt_from == tag:
|
|
txt = st.txt.strip()
|
|
st.txt = ''
|
|
else:
|
|
txt = None
|
|
add_to_txt = None
|
|
if tag == 'h1':
|
|
st.man_out.append(st.p_macro + '.SH "' + manify(txt) + '"\n')
|
|
elif tag == 'h2':
|
|
st.man_out.append(st.p_macro + '.SS "' + manify(txt) + '"\n')
|
|
elif tag == 'p':
|
|
if st.dt_from == 'p':
|
|
tag = 'dt'
|
|
st.man_out.append('.IP "' + manify(txt) + '"\n')
|
|
st.dt_from = None
|
|
elif txt != '':
|
|
st.man_out.append(manify(txt) + "\n")
|
|
elif tag == 'li':
|
|
if st.list_state[-1] == 'dl':
|
|
if st.at_first_tag_in_li:
|
|
die("Invalid 0. -> td translation")
|
|
tag = 'dd'
|
|
if txt != '':
|
|
st.man_out.append(manify(txt) + "\n")
|
|
st.at_first_tag_in_li = False
|
|
elif tag == 'blockquote':
|
|
st.man_out.append(".RE\n")
|
|
elif tag == 'pre':
|
|
st.in_pre = False
|
|
st.man_out.append(manify(txt) + "\n.fi\n")
|
|
elif (tag == 'code' and not st.in_pre) or tag == 'strong' or tag == 'b':
|
|
add_to_txt = NORM_FONT[0]
|
|
elif tag == 'em' or tag == 'i':
|
|
tag = 'u' # Change it into underline to be more like the man page
|
|
add_to_txt = NORM_FONT[0]
|
|
elif tag == 'ol' or tag == 'ul':
|
|
if st.list_state.pop() == 'dl':
|
|
tag = 'dl'
|
|
if st.list_state:
|
|
st.man_out.append(".RE\n")
|
|
else:
|
|
st.p_macro = ".P\n"
|
|
st.at_first_tag_in_dd = False
|
|
st.html_out.append('</' + tag + '>')
|
|
if add_to_txt:
|
|
if txt is None:
|
|
st.txt += add_to_txt
|
|
else:
|
|
txt += add_to_txt
|
|
if st.dt_from == tag:
|
|
st.man_out.append('.IP "' + manify(txt) + '"\n')
|
|
st.html_out.append('</dt><dd>')
|
|
st.at_first_tag_in_dd = True
|
|
st.dt_from = None
|
|
elif tag == 'dt':
|
|
st.html_out.append('<dd>')
|
|
st.at_first_tag_in_dd = True
|
|
|
|
|
|
def handle_data(self, data):
|
|
st = self.state
|
|
if args.debug:
|
|
self.output_debug('DATA', (data,))
|
|
st.html_out.append(htmlify(data))
|
|
st.txt += data
|
|
|
|
|
|
def output_debug(self, event, extra):
|
|
import pprint
|
|
st = self.state
|
|
if args.debug < 2:
|
|
st = argparse.Namespace(**vars(st))
|
|
if len(st.html_out) > 2:
|
|
st.html_out = ['...'] + st.html_out[-2:]
|
|
if len(st.man_out) > 2:
|
|
st.man_out = ['...'] + st.man_out[-2:]
|
|
print(event, extra)
|
|
pprint.PrettyPrinter(indent=2).pprint(vars(st))
|
|
|
|
|
|
def manify(txt):
|
|
return re.sub(r"^(['.])", r'\&\1', txt.replace('\\', '\\\\')
|
|
.replace(NORM_FONT[0], NORM_FONT[1])
|
|
.replace(BOLD_FONT[0], BOLD_FONT[1])
|
|
.replace(ULIN_FONT[0], ULIN_FONT[1]), flags=re.M)
|
|
|
|
|
|
def htmlify(txt):
|
|
return txt.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
|
|
|
|
def warn(*msg):
|
|
print(*msg, file=sys.stderr)
|
|
|
|
|
|
def die(*msg):
|
|
warn(*msg)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='Transform a NAME.NUM.md markdown file into a NAME.NUM.html web page & a NAME.NUM man page.', add_help=False)
|
|
parser.add_argument('--test', action='store_true', help='Test if we can parse the input w/o updating any files.')
|
|
parser.add_argument('--debug', '-D', action='count', default=0, help='Output copious info on the html parsing. Repeat for even more.')
|
|
parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
|
|
parser.add_argument('mdfile', help="The NAME.NUM.md file to parse.")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
import cmarkgfm
|
|
md_parser = html_via_cmarkgfm
|
|
except:
|
|
try:
|
|
import commonmark
|
|
md_parser = html_via_commonmark
|
|
except:
|
|
die("Failed to find cmarkgfm or commonmark for python3.")
|
|
|
|
main()
|