diff options
author | Loek Le Blansch <loek@pipeframe.xyz> | 2024-09-15 20:46:48 +0200 |
---|---|---|
committer | Loek Le Blansch <loek@pipeframe.xyz> | 2024-09-15 20:46:48 +0200 |
commit | f6cb1e9d141d881ae6205027626d6643776e833c (patch) | |
tree | c9dc4872c7306b1f038bb4f62bc5c114e8378d86 | |
parent | c9d4ba22a8b6d0d5fb1062701eda8b1af554d422 (diff) |
WIP requirements
-rw-r--r-- | example.tex | 13 | ||||
-rw-r--r-- | latexmkrc | 5 | ||||
-rw-r--r-- | projdoc.cls | 8 | ||||
-rwxr-xr-x | reqs2tex.py | 27 | ||||
-rw-r--r-- | scripts/.gitignore | 1 | ||||
-rwxr-xr-x | scripts/reqs2tex.py | 83 | ||||
-rw-r--r-- | scripts/tex.py | 43 | ||||
-rwxr-xr-x | scripts/time2tex.py (renamed from time2tex.py) | 51 |
8 files changed, 179 insertions, 52 deletions
diff --git a/example.tex b/example.tex index 4ae4d95..ee1377a 100644 --- a/example.tex +++ b/example.tex @@ -3,6 +3,8 @@ % with the [draft] option. this replaces all images with placeholders. \input{meta.tex} +\input{reqs.aux} + \title{Example Document} \begin{document} @@ -155,17 +157,24 @@ Description: \con{Bad thing 2} \end{comparison} -\subsection{Citations} +\subsection{References} + +\subsubsection{Citations} Citations are inserted using the \codeinline{\autocite} command \autocite{rfc:3339}. The bibliography is automatically printed after \codeinline{\end{document}}. -\subsection{Glossary} +\subsubsection{Glossary} Glossary entries can be inserted using the \codeinline{\gls} commands. Example: ``\Gls{sdl2} handles \glspl{hid} as well!''. In following occurrences of acronyms, only their short form is printed: `\gls{sdl2}' and `\gls{hid}'. All of these link to the glossary that is automatically printed after \codeinline{\end{document}}. +\subsubsection{Requirements} + +Requirements are referenced like \codeinline{\label}s: +e.g.~\cref{req:audio:handle:id,req:audio:async-api:id}. + \end{document} @@ -26,11 +26,12 @@ sub plantuml { add_cus_dep('txt', 'tex', 0, 'time2tex'); sub time2tex { - return system "python3 time2tex.py '$_[0].txt'"; + return system "python3 scripts/time2tex.py '$_[0].txt'"; } add_cus_dep('toml', 'tex', 0, 'reqs2tex'); +add_cus_dep('toml', 'aux', 0, 'reqs2tex'); sub reqs2tex { - return system "python3 reqs2tex.py '$_[0].toml'"; + return system "python3 scripts/reqs2tex.py '$_[0].toml'"; } diff --git a/projdoc.cls b/projdoc.cls index 0d27a1f..c11fe61 100644 --- a/projdoc.cls +++ b/projdoc.cls @@ -307,3 +307,11 @@ \newcommand\noparbreak{\par\nobreak\@afterheading} \makeatother +% cleveref extra types +\crefname{paragraph}{paragraph}{paragraphs} +\Crefname{paragraph}{Paragraph}{Paragraphs} +\crefname{requirement}{requirement}{requirements} +\Crefname{requirement}{Requirement}{Requirements} +\crefname{test}{test}{tests} +\Crefname{test}{Test}{Tests} + diff --git a/reqs2tex.py b/reqs2tex.py deleted file mode 100755 index 68c8f40..0000000 --- a/reqs2tex.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/python3 -import sys, tomllib - -def fmt(data): - print(data) - return f""" -\\makeatletter -\\makeatother -""" - -def main(input_file): - data = {} - with open(input_file, "rb") as file: - data = tomllib.load(file) - - output = fmt(data) - - output_file = input_file.removesuffix(".toml") + ".tex" - with open(output_file, "w+") as file: - file.write(output) - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("usage: reqs2tex.py reqs.toml") - exit(1) - main(sys.argv[1]) - diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py new file mode 100755 index 0000000..9e71a48 --- /dev/null +++ b/scripts/reqs2tex.py @@ -0,0 +1,83 @@ +#!/bin/python3 +import sys, tomllib, tex + +def flatten(data): + if 'description' in data: + return [ data ] + out = [] + for key, value in data.items(): + items = flatten(value) + for item in items: + if 'label' in item: + item['label'] = f"{key}:{item['label']}" + else: + item['label'] = f"{key}" + out += items + return out + +id_counter = 0 +def make_id(item): + global id_counter + id_counter += 1 + return "{type_short}#{counter:03d}".format( + type_short = item['type'][0].upper(), + counter = id_counter, + ) + +def convert(data): + reqs = flatten(data) + for index, item in enumerate(reqs): + item['id'] = tex.esc(make_id(item)) + item['index'] = index + item['description'] = item.get('description', '???') + item['done'] = item.get('done', None) + item['priority'] = item.get('priority', 'must') + item['type'] = item.get('type', 'system') + item['deleted'] = item.get('deleted', False) + + # skip deleted requirements (but process for make_id) + reqs = [item for item in reqs if item['deleted'] == False] + + return reqs + +def req2aux(req): + # TODO: this is a dead-end solution, newlabel only works for in-document anchors, not external links + out = [ + tex.scmd('newlabel', f"req:{req['label']}:id", tex.group(req['id'], req['id'], '', './requirements.pdf', '')), + tex.scmd('newlabel', f"req:{req['label']}:id@cref", tex.group(f"[requirement][][]{req['id']}", '')), + ] + return "\n".join([tex.auxout(line) for line in out]) + +def fmt_aux(data): + out = "" + out += tex.cmd('makeatletter') + out += "\n".join([req2aux(req) for req in data]) + out += tex.cmd('makeatother') + return out + +def fmt_tex(data): + return "\n".join([ + tex.cmd('relax') + ]) + +def main(input_file): + data = {} + with open(input_file, "rb") as file: + data = tomllib.load(file) + + requirements = convert(data) + + output_aux = input_file.removesuffix(".toml") + ".aux" + with open(output_aux, "w+") as file: + file.write(fmt_aux(requirements)) + + output_tex = input_file.removesuffix(".toml") + ".tex" + with open(output_tex, "w+") as file: + file.write(fmt_tex(requirements)) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: reqs2tex.py reqs.toml") + exit(1) + main(sys.argv[1]) + diff --git a/scripts/tex.py b/scripts/tex.py new file mode 100644 index 0000000..b044857 --- /dev/null +++ b/scripts/tex.py @@ -0,0 +1,43 @@ +# utility function for converting latex code + +def group(*args): + return "".join("{" + arg + "}" for arg in args) + +def string(content): + return r"\string" + content + +def cmd(*args): + name = args[0] + args = args[1:] + if len(args) == 0: args = [""] + return f"\\{name}" + group(*args) + +def csdef(*args): + return r"\def" + cmd(*args) + +def auxout(content): + return r"\write\@auxout" + group(content) + +def scmd(*args): + return string(cmd(*args)) + +def env(name, *args): + content = args[-1] + args = args[0:-1] + out = f"\\begin{{{name}}}" + if len(args) > 0: + out += group(*args) + out += content + out += f"\\end{{{name}}}" + return out + +def esc(plain): + plain = plain.replace("\\", "\\string\\") + plain = plain.replace("#", "\\#") + plain = plain.replace("$", "\\$") + plain = plain.replace("%", "\\%") + return plain + +def tabrule(*cells): + return "&".join(cells) + "\\\\" + diff --git a/time2tex.py b/scripts/time2tex.py index 6e3de9c..17661d5 100755 --- a/time2tex.py +++ b/scripts/time2tex.py @@ -1,5 +1,5 @@ #!/bin/python3 -import sys +import sys, tex from datetime import datetime, timedelta def fmt_duration(sec): @@ -19,11 +19,14 @@ def fmt_duration(sec): return "\\,".join(out) def fmt_percentage(fac): - return f"{{\\footnotesize\\itshape({round(fac * 100)}\\%)}}" + return tex.sgroup( + tex.cmd('footnotesize') +\ + tex.cmd('itshape') +\ + tex.esc(f"({round(fac * 100)}%)") + ) def fmt_member_overview(times): # calculations - out = "" tracked = {} total_time = 0 for time in times: @@ -32,24 +35,29 @@ def fmt_member_overview(times): tracked[time["name"]] += time["duration"] total_time += time["duration"] - # begin table - out += r"\begin{table}\centering" - out += r"\begin{tabular}{lr@{~}l}\toprule" - out += r"\textbf{Member} & \textbf{Tracked} &\\\midrule{}" + out = "" + + # header + out += tex.cmd('toprule') + out += tex.tabrule(tex.cmd('textbf', 'Member'), tex.cmd('textbf', 'Tracked'), '') + out += tex.cmd('midrule') # member overview members = sorted(list(set(time["name"] for time in times))) for name in members: - out += f"{name} & {fmt_duration(tracked[name])} & {fmt_percentage(tracked[name] / total_time)}\\\\" - out += r"\midrule{}" + out += tex.tabrule(name, fmt_duration(tracked[name]), fmt_percentage(tracked[name] / total_time)) + out += tex.cmd('midrule') # sum - out += f"&{fmt_duration(total_time)}&\\\\" + out += tex.tabrule('', fmt_duration(total_time), '') + out += tex.cmd('bottomrule') - # end table - out += r"\bottomrule\end{tabular}" - out += r"\caption{Tracked time per group member}\label{tab:time-member}" - out += r"\end{table}" + out = tex.env('tabular', 'lr@{~}l', out) + out = tex.cmd('centering') +\ + out +\ + tex.cmd('caption', 'Tracked time per group member') +\ + tex.cmd('label', 'tab:time-member') + out = tex.env('table', out) return out @@ -85,6 +93,7 @@ def fmt_weekly_overview(times): for member in members: member_totals[member] = sum(time["duration"] for time in times if time["name"] == member) + # TODO: refactor # begin table out += r"\begin{table}\centering" out += r"\fitimg{" @@ -173,13 +182,13 @@ def parse(content): return out def fmt(times): - return f""" -\\section{{Overviews}}\n -\\subsection{{Members}}\n -{fmt_member_overview(times)} -\\subsection{{Weekly}}\n -{fmt_weekly_overview(times)} -""" + return "\n\n".join([ + tex.cmd('section', 'Overviews'), + tex.cmd('subsection', 'Members'), + fmt_member_overview(times), + tex.cmd('subsection', 'Weekly'), + fmt_weekly_overview(times), + ]) def main(input_file): content = "" |