123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 | #!/bin/env python3
# This script converts API YAML files into markdown documentation
# and prints it to standard out.
from datetime import datetime
import re
import yaml
def escape(text, spaces=False):
"""
Escapes text for safe inclusion in groff documents.
- Escapes backslashes as '\\e'
- Escapes periods at the start of a line as '\\&.'
"""
escaped_lines = []
for line in text.splitlines():
# Escape backslashes
line = line.replace("\\", r"\[rs]")
# Escape leading period or apostrophe (groff macro triggers)
if line.startswith('.'):
line = r'\&' + line
elif line.startswith("'"):
line = r'\&' + line
if spaces:
line = line.replace(' ', '\\ ')
escaped_lines.append(line)
return '\n'.join(escaped_lines)
def arg_signature(name, arg):
sig = name
if "type" in arg:
sig = sig + ": " + arg["type"]
if "default" in arg:
sig = sig + " = " + arg["default"]
return sig
def get_signature(name, info):
if "type" in info:
return ".BI " +escape(f'{name} : {info["type"]}', spaces=True)
sig = f'{name} : func('
if info["args"]:
sig += ", ".join(arg_signature(name,arg) for name,arg in info["args"].items())
sig += " -> " + info["return"].get("type", "Void")
else:
sig += "-> " + info["return"].get("type", "Void")
sig += ')'
return ".BI " + escape(sig, spaces=True)
template = ''''\\" t
.\\" Copyright (c) {year} Bruce Hill
.\\" All rights reserved.
.\\"
.TH {name} 3 {date} "Tomo man-pages"
.SH NAME
{name} \\- {short}
.SH LIBRARY
Tomo Standard Library
.SH SYNOPSIS
.nf
{signature}
.fi
.SH DESCRIPTION
{description}
'''
arg_prefix = '''
.TS
allbox;
lb lb lbx lb
l l l l.
Name Type Description Default'''
arg_prefix_no_default = '''
.TS
allbox;
lb lb lbx
l l l.
Name Type Description'''
type_template = ''''\\" t
.\\" Copyright (c) {year} Bruce Hill
.\\" All rights reserved.
.\\"
.TH {type} 3 {date} "Tomo man-pages"
.SH NAME
{type} \\- a Tomo type
.SH LIBRARY
Tomo Standard Library
.fi
.SH METHODS
{methods}
'''
def markdown_to_roff(text):
text = text.replace('\n', ' ')
text = re.sub(r'`([^`]*)`', '\\\\fB\\1\\\\fR', text)
return text
def write_method(path, name, info):
lines = []
year = datetime.now().strftime("%Y")
date = datetime.now().strftime("%Y-%m-%d")
signature = get_signature(name, info)
lines.append(template.format(year=year, date=date, name=name, short=info["short"], description=info["description"], signature=signature))
if "args" in info and info["args"]:
lines.append(".SH ARGUMENTS")
has_defaults = any('default' in a for a in info['args'].values())
lines.append(arg_prefix if has_defaults else arg_prefix_no_default)
for arg,arg_info in info["args"].items():
if has_defaults:
default = escape(arg_info['default'], spaces=True) if 'default' in arg_info else '-'
description = markdown_to_roff(arg_info['description'])
lines.append(f"{arg}\t{arg_info.get('type', '')}\t{description}\t{default}")
else:
description = markdown_to_roff(arg_info['description'])
lines.append(f"{arg}\t{arg_info.get('type', '')}\t{description}")
lines.append(".TE")
if "return" in info:
lines.append(".SH RETURN")
lines.append(info['return'].get('description', 'Nothing.'))
if "note" in info:
lines.append(".SH NOTES")
lines.append(info["note"])
if "errors" in info:
lines.append(".SH ERRORS")
lines.append(info["errors"])
if "example" in info:
lines.append(".SH EXAMPLES")
lines.append(".EX")
lines.append(escape(info['example']))
lines.append(".EE")
if "." in name:
type,_ = name.split(".")
lines.append(".SH SEE ALSO")
lines.append(f".BR Tomo-{type} (3)")
to_write = '\n'.join(lines) + '\n'
try:
with open(path, "r") as f:
existing = f.read()
if to_write.splitlines()[5:] == existing.splitlines()[5:]:
return
except FileNotFoundError:
pass
with open(path, "w") as f:
f.write(to_write)
print(f"Updated {path}")
fn_summary_template = '''
.TP
{signature}
{description}
For more, see:
.BR Tomo-{type}.{name} (3)
'''
def fn_summary(type, name, info) -> str:
signature = get_signature(type+"."+name, info)
return fn_summary_template.format(
type=type,
name=name,
signature=signature,
description=markdown_to_roff(info["description"]),
)
def write_type_manpage(path, type, methods):
year = datetime.now().strftime("%Y")
date = datetime.now().strftime("%Y-%m-%d")
method_summaries = [fn_summary(type, name, methods[name]) for name in sorted(methods.keys())]
type_manpage = type_template.format(
year=year,
date=date,
type=type,
methods='\n'.join(method_summaries),
)
try:
with open(path, "r") as f:
existing = f.read()
if type_manpage.splitlines()[5:] == existing.splitlines()[5:]:
return
except FileNotFoundError:
pass
with open(path, "w") as f:
f.write(type_manpage)
print(f"Updated {path}")
def convert_to_markdown(yaml_doc:str)->str:
data = yaml.safe_load(yaml_doc)
types = {}
for name,info in data.items():
write_method(f"man/man3/tomo-{name}.3", name, data[name])
if "." in name:
type,fn = name.split(".")
if type not in types:
types[type] = {}
types[type][fn] = info
for type,methods in types.items():
write_type_manpage(f"man/man3/tomo-{type}.3", type, methods)
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
all_files = ""
for filename in sys.argv[1:]:
with open(filename, "r") as f:
all_files += f.read()
convert_to_markdown(all_files)
else:
convert_to_markdown(sys.stdin.read())
|