Annotation of embedaddon/rsync/md2man, revision 1.1

1.1     ! misho       1: #!/usr/bin/env python3
        !             2: 
        !             3: # This script takes a manpage written in markdown and turns it into an html web
        !             4: # page and a nroff man page.  The input file must have the name of the program
        !             5: # and the section in this format: NAME.NUM.md.  The output files are written
        !             6: # into the current directory named NAME.NUM.html and NAME.NUM.  The input
        !             7: # format has one extra extension: if a numbered list starts at 0, it is turned
        !             8: # into a description list. The dl's dt tag is taken from the contents of the
        !             9: # first tag inside the li, which is usually a p, code, or strong tag.  The
        !            10: # cmarkgfm or commonmark lib is used to transforms the input file into html.
        !            11: # The html.parser is used as a state machine that both tweaks the html and
        !            12: # outputs the nroff data based on the html tags.
        !            13: #
        !            14: # Copyright (C) 2020 Wayne Davison
        !            15: #
        !            16: # This program is freely redistributable.
        !            17: 
        !            18: import sys, os, re, argparse, subprocess, time
        !            19: from html.parser import HTMLParser
        !            20: 
        !            21: CONSUMES_TXT = set('h1 h2 p li pre'.split())
        !            22: 
        !            23: HTML_START = """\
        !            24: <html><head>
        !            25: <title>%s</title>
        !            26: <link href="https://fonts.googleapis.com/css2?family=Roboto&family=Roboto+Mono&display=swap" rel="stylesheet">
        !            27: <style>
        !            28: body {
        !            29:   max-width: 50em;
        !            30:   margin: auto;
        !            31: }
        !            32: body, b, strong, u {
        !            33:   font-family: 'Roboto', sans-serif;
        !            34: }
        !            35: code {
        !            36:   font-family: 'Roboto Mono', monospace;
        !            37:   font-weight: bold;
        !            38:   white-space: pre;
        !            39: }
        !            40: pre code {
        !            41:   display: block;
        !            42:   font-weight: normal;
        !            43: }
        !            44: blockquote pre code {
        !            45:   background: #f1f1f1;
        !            46: }
        !            47: dd p:first-of-type {
        !            48:   margin-block-start: 0em;
        !            49: }
        !            50: </style>
        !            51: </head><body>
        !            52: """
        !            53: 
        !            54: HTML_END = """\
        !            55: <div style="float: right"><p><i>%s</i></p></div>
        !            56: </body></html>
        !            57: """
        !            58: 
        !            59: MAN_START = r"""
        !            60: .TH "%s" "%s" "%s" "%s" "User Commands"
        !            61: """.lstrip()
        !            62: 
        !            63: MAN_END = """\
        !            64: """
        !            65: 
        !            66: NORM_FONT = ('\1', r"\fP")
        !            67: BOLD_FONT = ('\2', r"\fB")
        !            68: UNDR_FONT = ('\3', r"\fI")
        !            69: NBR_DASH = ('\4', r"\-")
        !            70: NBR_SPACE = ('\xa0', r"\ ")
        !            71: 
        !            72: md_parser = None
        !            73: 
        !            74: def main():
        !            75:     fi = re.match(r'^(?P<fn>(?P<srcdir>.+/)?(?P<name>(?P<prog>[^/]+)\.(?P<sect>\d+))\.md)$', args.mdfile)
        !            76:     if not fi:
        !            77:         die('Failed to parse NAME.NUM.md out of input file:', args.mdfile)
        !            78:     fi = argparse.Namespace(**fi.groupdict())
        !            79: 
        !            80:     if not fi.srcdir:
        !            81:         fi.srcdir = './'
        !            82: 
        !            83:     fi.title = fi.prog + '(' + fi.sect + ') man page'
        !            84:     fi.mtime = 0
        !            85: 
        !            86:     git_dir = fi.srcdir + '.git'
        !            87:     if os.path.lexists(git_dir):
        !            88:         fi.mtime = int(subprocess.check_output(['git', '--git-dir', git_dir, 'log', '-1', '--format=%at']))
        !            89: 
        !            90:     env_subs = { 'prefix': os.environ.get('RSYNC_OVERRIDE_PREFIX', None) }
        !            91: 
        !            92:     if args.test:
        !            93:         env_subs['VERSION'] = '1.0.0'
        !            94:         env_subs['libdir'] = '/usr'
        !            95:     else:
        !            96:         for fn in (fi.srcdir + 'version.h', 'Makefile'):
        !            97:             try:
        !            98:                 st = os.lstat(fn)
        !            99:             except:
        !           100:                 die('Failed to find', fi.srcdir + fn)
        !           101:             if not fi.mtime:
        !           102:                 fi.mtime = st.st_mtime
        !           103: 
        !           104:         with open(fi.srcdir + 'version.h', 'r', encoding='utf-8') as fh:
        !           105:             txt = fh.read()
        !           106:         m = re.search(r'"(.+?)"', txt)
        !           107:         env_subs['VERSION'] = m.group(1)
        !           108: 
        !           109:         with open('Makefile', 'r', encoding='utf-8') as fh:
        !           110:             for line in fh:
        !           111:                 m = re.match(r'^(\w+)=(.+)', line)
        !           112:                 if not m:
        !           113:                     continue
        !           114:                 var, val = (m.group(1), m.group(2))
        !           115:                 if var == 'prefix' and env_subs[var] is not None:
        !           116:                     continue
        !           117:                 while re.search(r'\$\{', val):
        !           118:                     val = re.sub(r'\$\{(\w+)\}', lambda m: env_subs[m.group(1)], val)
        !           119:                 env_subs[var] = val
        !           120:                 if var == 'srcdir':
        !           121:                     break
        !           122: 
        !           123:     with open(fi.fn, 'r', encoding='utf-8') as fh:
        !           124:         txt = fh.read()
        !           125: 
        !           126:     txt = re.sub(r'@VERSION@', env_subs['VERSION'], txt)
        !           127:     txt = re.sub(r'@LIBDIR@', env_subs['libdir'], txt)
        !           128: 
        !           129:     fi.html_in = md_parser(txt)
        !           130:     txt = None
        !           131: 
        !           132:     fi.date = time.strftime('%d %b %Y', time.localtime(fi.mtime))
        !           133:     fi.man_headings = (fi.prog, fi.sect, fi.date, fi.prog + ' ' + env_subs['VERSION'])
        !           134: 
        !           135:     HtmlToManPage(fi)
        !           136: 
        !           137:     if args.test:
        !           138:         print("The test was successful.")
        !           139:         return
        !           140: 
        !           141:     for fn, txt in ((fi.name + '.html', fi.html_out), (fi.name, fi.man_out)):
        !           142:         print("Wrote:", fn)
        !           143:         with open(fn, 'w', encoding='utf-8') as fh:
        !           144:             fh.write(txt)
        !           145: 
        !           146: 
        !           147: def html_via_commonmark(txt):
        !           148:     return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt))
        !           149: 
        !           150: 
        !           151: class HtmlToManPage(HTMLParser):
        !           152:     def __init__(self, fi):
        !           153:         HTMLParser.__init__(self, convert_charrefs=True)
        !           154: 
        !           155:         st = self.state = argparse.Namespace(
        !           156:                 list_state = [ ],
        !           157:                 p_macro = ".P\n",
        !           158:                 at_first_tag_in_li = False,
        !           159:                 at_first_tag_in_dd = False,
        !           160:                 dt_from = None,
        !           161:                 in_pre = False,
        !           162:                 in_code = False,
        !           163:                 html_out = [ HTML_START % fi.title ],
        !           164:                 man_out = [ MAN_START % fi.man_headings ],
        !           165:                 txt = '',
        !           166:                 )
        !           167: 
        !           168:         self.feed(fi.html_in)
        !           169:         fi.html_in = None
        !           170: 
        !           171:         st.html_out.append(HTML_END % fi.date)
        !           172:         st.man_out.append(MAN_END)
        !           173: 
        !           174:         fi.html_out = ''.join(st.html_out)
        !           175:         st.html_out = None
        !           176: 
        !           177:         fi.man_out = ''.join(st.man_out)
        !           178:         st.man_out = None
        !           179: 
        !           180: 
        !           181:     def handle_starttag(self, tag, attrs_list):
        !           182:         st = self.state
        !           183:         if args.debug:
        !           184:             self.output_debug('START', (tag, attrs_list))
        !           185:         if st.at_first_tag_in_li:
        !           186:             if st.list_state[-1] == 'dl':
        !           187:                 st.dt_from = tag
        !           188:                 if tag == 'p':
        !           189:                     tag = 'dt'
        !           190:                 else:
        !           191:                     st.html_out.append('<dt>')
        !           192:             elif tag == 'p':
        !           193:                 st.at_first_tag_in_dd = True # Kluge to suppress a .P at the start of an li.
        !           194:             st.at_first_tag_in_li = False
        !           195:         if tag == 'p':
        !           196:             if not st.at_first_tag_in_dd:
        !           197:                 st.man_out.append(st.p_macro)
        !           198:         elif tag == 'li':
        !           199:             st.at_first_tag_in_li = True
        !           200:             lstate = st.list_state[-1]
        !           201:             if lstate == 'dl':
        !           202:                 return
        !           203:             if lstate == 'o':
        !           204:                 st.man_out.append(".IP o\n")
        !           205:             else:
        !           206:                 st.man_out.append(".IP " + str(lstate) + ".\n")
        !           207:                 st.list_state[-1] += 1
        !           208:         elif tag == 'blockquote':
        !           209:             st.man_out.append(".RS 4\n")
        !           210:         elif tag == 'pre':
        !           211:             st.in_pre = True
        !           212:             st.man_out.append(st.p_macro + ".nf\n")
        !           213:         elif tag == 'code' and not st.in_pre:
        !           214:             st.in_code = True
        !           215:             st.txt += BOLD_FONT[0]
        !           216:         elif tag == 'strong' or tag == 'b':
        !           217:             st.txt += BOLD_FONT[0]
        !           218:         elif tag == 'em' or  tag == 'i':
        !           219:             tag = 'u' # Change it into underline to be more like the man page
        !           220:             st.txt += UNDR_FONT[0]
        !           221:         elif tag == 'ol':
        !           222:             start = 1
        !           223:             for var, val in attrs_list:
        !           224:                 if var == 'start':
        !           225:                     start = int(val) # We only support integers.
        !           226:                     break
        !           227:             if st.list_state:
        !           228:                 st.man_out.append(".RS\n")
        !           229:             if start == 0:
        !           230:                 tag = 'dl'
        !           231:                 attrs_list = [ ]
        !           232:                 st.list_state.append('dl')
        !           233:             else:
        !           234:                 st.list_state.append(start)
        !           235:             st.man_out.append(st.p_macro)
        !           236:             st.p_macro = ".IP\n"
        !           237:         elif tag == 'ul':
        !           238:             st.man_out.append(st.p_macro)
        !           239:             if st.list_state:
        !           240:                 st.man_out.append(".RS\n")
        !           241:                 st.p_macro = ".IP\n"
        !           242:             st.list_state.append('o')
        !           243:         st.html_out.append('<' + tag + ''.join(' ' + var + '="' + htmlify(val) + '"' for var, val in attrs_list) + '>')
        !           244:         st.at_first_tag_in_dd = False
        !           245: 
        !           246: 
        !           247:     def handle_endtag(self, tag):
        !           248:         st = self.state
        !           249:         if args.debug:
        !           250:             self.output_debug('END', (tag,))
        !           251:         if tag in CONSUMES_TXT or st.dt_from == tag:
        !           252:             txt = st.txt.strip()
        !           253:             st.txt = ''
        !           254:         else:
        !           255:             txt = None
        !           256:         add_to_txt = None
        !           257:         if tag == 'h1':
        !           258:             st.man_out.append(st.p_macro + '.SH "' + manify(txt) + '"\n')
        !           259:         elif tag == 'h2':
        !           260:             st.man_out.append(st.p_macro + '.SS "' + manify(txt) + '"\n')
        !           261:         elif tag == 'p':
        !           262:             if st.dt_from == 'p':
        !           263:                 tag = 'dt'
        !           264:                 st.man_out.append('.IP "' + manify(txt) + '"\n')
        !           265:                 st.dt_from = None
        !           266:             elif txt != '':
        !           267:                 st.man_out.append(manify(txt) + "\n")
        !           268:         elif tag == 'li':
        !           269:             if st.list_state[-1] == 'dl':
        !           270:                 if st.at_first_tag_in_li:
        !           271:                     die("Invalid 0. -> td translation")
        !           272:                 tag = 'dd'
        !           273:             if txt != '':
        !           274:                 st.man_out.append(manify(txt) + "\n")
        !           275:             st.at_first_tag_in_li = False
        !           276:         elif tag == 'blockquote':
        !           277:             st.man_out.append(".RE\n")
        !           278:         elif tag == 'pre':
        !           279:             st.in_pre = False
        !           280:             st.man_out.append(manify(txt) + "\n.fi\n")
        !           281:         elif (tag == 'code' and not st.in_pre):
        !           282:             st.in_code = False
        !           283:             add_to_txt = NORM_FONT[0]
        !           284:         elif tag == 'strong' or tag == 'b':
        !           285:             add_to_txt = NORM_FONT[0]
        !           286:         elif tag == 'em' or  tag == 'i':
        !           287:             tag = 'u' # Change it into underline to be more like the man page
        !           288:             add_to_txt = NORM_FONT[0]
        !           289:         elif tag == 'ol' or tag == 'ul':
        !           290:             if st.list_state.pop() == 'dl':
        !           291:                 tag = 'dl'
        !           292:             if st.list_state:
        !           293:                 st.man_out.append(".RE\n")
        !           294:             else:
        !           295:                 st.p_macro = ".P\n"
        !           296:             st.at_first_tag_in_dd = False
        !           297:         st.html_out.append('</' + tag + '>')
        !           298:         if add_to_txt:
        !           299:             if txt is None:
        !           300:                 st.txt += add_to_txt
        !           301:             else:
        !           302:                 txt += add_to_txt
        !           303:         if st.dt_from == tag:
        !           304:             st.man_out.append('.IP "' + manify(txt) + '"\n')
        !           305:             st.html_out.append('</dt><dd>')
        !           306:             st.at_first_tag_in_dd = True
        !           307:             st.dt_from = None
        !           308:         elif tag == 'dt':
        !           309:             st.html_out.append('<dd>')
        !           310:             st.at_first_tag_in_dd = True
        !           311: 
        !           312: 
        !           313:     def handle_data(self, txt):
        !           314:         st = self.state
        !           315:         if args.debug:
        !           316:             self.output_debug('DATA', (txt,))
        !           317:         if st.in_pre:
        !           318:             html = htmlify(txt)
        !           319:         else:
        !           320:             txt = re.sub(r'\s--(\s)', NBR_SPACE[0] + r'--\1', txt).replace('--', NBR_DASH[0]*2)
        !           321:             txt = re.sub(r'(^|\W)-', r'\1' + NBR_DASH[0], txt)
        !           322:             html = htmlify(txt)
        !           323:             if st.in_code:
        !           324:                 txt = re.sub(r'\s', NBR_SPACE[0], txt)
        !           325:                 html = html.replace(NBR_DASH[0], '-').replace(NBR_SPACE[0], ' ') # <code> is non-breaking in CSS
        !           326:         st.html_out.append(html.replace(NBR_SPACE[0], '&nbsp;').replace(NBR_DASH[0], '-&#8288;'))
        !           327:         st.txt += txt
        !           328: 
        !           329: 
        !           330:     def output_debug(self, event, extra):
        !           331:         import pprint
        !           332:         st = self.state
        !           333:         if args.debug < 2:
        !           334:             st = argparse.Namespace(**vars(st))
        !           335:             if len(st.html_out) > 2:
        !           336:                 st.html_out = ['...'] + st.html_out[-2:]
        !           337:             if len(st.man_out) > 2:
        !           338:                 st.man_out = ['...'] + st.man_out[-2:]
        !           339:         print(event, extra)
        !           340:         pprint.PrettyPrinter(indent=2).pprint(vars(st))
        !           341: 
        !           342: 
        !           343: def manify(txt):
        !           344:     return re.sub(r"^(['.])", r'\&\1', txt.replace('\\', '\\\\')
        !           345:             .replace(NBR_SPACE[0], NBR_SPACE[1])
        !           346:             .replace(NBR_DASH[0], NBR_DASH[1])
        !           347:             .replace(NORM_FONT[0], NORM_FONT[1])
        !           348:             .replace(BOLD_FONT[0], BOLD_FONT[1])
        !           349:             .replace(UNDR_FONT[0], UNDR_FONT[1]), flags=re.M)
        !           350: 
        !           351: 
        !           352: def htmlify(txt):
        !           353:     return txt.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
        !           354: 
        !           355: 
        !           356: def warn(*msg):
        !           357:     print(*msg, file=sys.stderr)
        !           358: 
        !           359: 
        !           360: def die(*msg):
        !           361:     warn(*msg)
        !           362:     sys.exit(1)
        !           363: 
        !           364: 
        !           365: if __name__ == '__main__':
        !           366:     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)
        !           367:     parser.add_argument('--test', action='store_true', help='Test if we can parse the input w/o updating any files.')
        !           368:     parser.add_argument('--debug', '-D', action='count', default=0, help='Output copious info on the html parsing. Repeat for even more.')
        !           369:     parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
        !           370:     parser.add_argument('mdfile', help="The NAME.NUM.md file to parse.")
        !           371:     args = parser.parse_args()
        !           372: 
        !           373:     try:
        !           374:         import cmarkgfm
        !           375:         md_parser = cmarkgfm.markdown_to_html
        !           376:     except:
        !           377:         try:
        !           378:             import commonmark
        !           379:             md_parser = html_via_commonmark
        !           380:         except:
        !           381:             die("Failed to find cmarkgfm or commonmark for python3.")
        !           382: 
        !           383:     main()

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>