diff options
| author | Max-001 <80035972+Max-001@users.noreply.github.com> | 2024-09-17 17:11:50 +0200 | 
|---|---|---|
| committer | Max-001 <80035972+Max-001@users.noreply.github.com> | 2024-09-17 17:11:50 +0200 | 
| commit | a76cbcc5def5a6a09fb72b8aad5724c1de80b2a7 (patch) | |
| tree | 5788b2ba519f74060db15f1683db963f75532bcb | |
| parent | e007d0bcd7484a364d8f1ca425ef58b13d0bef93 (diff) | |
| parent | 5c0649ac31b030cbb3c8c5e0684ee9419fe50054 (diff) | |
Merge remote-tracking branch 'origin/master' into max/time
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | example.tex | 13 | ||||
| -rw-r--r-- | glossary.bib | 5 | ||||
| -rw-r--r-- | latexmkrc | 17 | ||||
| -rw-r--r-- | projdoc.cls | 34 | ||||
| -rw-r--r-- | readme.md | 13 | ||||
| -rw-r--r-- | reqs.toml | 56 | ||||
| -rw-r--r-- | requirements.tex | 22 | ||||
| -rw-r--r-- | scripts/.gitignore | 1 | ||||
| -rwxr-xr-x | scripts/reqs2tex.py | 110 | ||||
| -rw-r--r-- | scripts/tex.py | 52 | ||||
| -rwxr-xr-x | scripts/time2tex.py (renamed from time2tex.py) | 81 | ||||
| -rw-r--r-- | time.txt | 2 | 
13 files changed, 351 insertions, 56 deletions
@@ -32,3 +32,4 @@  # generated files  time.tex +reqs.tex diff --git a/example.tex b/example.tex index 4ae4d95..e0d21c0 100644 --- a/example.tex +++ b/example.tex @@ -3,6 +3,8 @@  % with the [draft] option. this replaces all images with placeholders.  \input{meta.tex} +\externaldocument{reqs}[./requirements.pdf] +  \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,req:audio:async-api}. +  \end{document} diff --git a/glossary.bib b/glossary.bib index 4d02e4a..437db86 100644 --- a/glossary.bib +++ b/glossary.bib @@ -28,3 +28,8 @@  	description = {Graphics library developed by \hbox{Microsoft}},  } +@acronym{api, +	short = {API}, +	long = {Application Programming Interface}, +} + @@ -1,5 +1,7 @@  # https://nl.mirrors.cicku.me/ctan/support/latexmk/latexmk.pdf +use File::Spec::Functions; +  $pdflatex = "xelatex --interaction=nonstopmode %O %S";  $pdf_mode = 1;  $dvi_mode = 0; @@ -14,19 +16,26 @@ $clean_ext .= ' %R.ist %R.xdy bbl run.xml';  push @file_not_found, '^Package .* No file `([^\\\']*)\\\'';  push @generated_exts, 'glo', 'gls', 'glg'; -  add_cus_dep('aux', 'glstex', 0, 'bib2gls');  sub bib2gls { -	return system "bib2gls '$_[0]'"; +	return Run_msg("bib2gls $_[0]");  }  add_cus_dep('puml', 'eps', 0, 'plantuml');  sub plantuml { -  return system "plantuml -teps '$_[0].puml'"; +  return Run_msg("plantuml -teps $_[0].puml");  }  add_cus_dep('txt', 'tex', 0, 'time2tex');  sub time2tex { -	return system "./time2tex.py '$_[0].txt' > '$_[0].tex'"; +	return Run_msg("python3 @{[catfile('scripts', 'time2tex.py')]} $_[0].txt");  } +add_cus_dep('toml', 'tex', 0, 'reqs2tex'); +add_cus_dep('toml', 'aux', 0, 'reqs2tex'); +sub reqs2tex { +	return Run_msg("python3 @{[catfile('scripts', 'reqs2tex.py')]} $_[0].toml"); +} + +# vim:ft=perl + diff --git a/projdoc.cls b/projdoc.cls index 0d27a1f..fe8c8bc 100644 --- a/projdoc.cls +++ b/projdoc.cls @@ -40,6 +40,7 @@  \RequirePackage{tabularx}  \RequirePackage{booktabs}  \RequirePackage{needspace} +\RequirePackage{xr-hyper}  \RequirePackage{hyperref}  \RequirePackage{microtype}  \RequirePackage{xcolor} @@ -120,16 +121,23 @@  	itemsep=\dimexpr\style@itemsep-\style@parsep\relax,  	parsep=\style@parsep,  } -\def\projdoc@setdescriptionstyle{% +\def\projdoc@description@before{%  	\renewcommand\makelabel[1]{%  		{\bfseries ##1}:%  	}%  } -\setdescription{ -	before={\projdoc@setdescriptionstyle}, -	leftmargin=3em, -	labelindent=3ex, +\newlength\projdoc@description@leftmargin% +\projdoc@description@leftmargin=3em% +\newlength\projdoc@description@labelindent% +\projdoc@description@labelindent=3ex% +\def\projdoc@setdescriptionstyle{% +	\setdescription{ +		before={\projdoc@description@before}, +		leftmargin=\projdoc@description@leftmargin, +		labelindent=\projdoc@description@labelindent, +	}%  } +\projdoc@setdescriptionstyle%  \makeatother  % create a label using \customlabel[<creftype>]{<refname>}{<reftext>} that displays @@ -229,11 +237,9 @@  	}{}%  	% glossary  	\ifbool{projdoc@used@gls}{% -		\setdescription{ -			before={\projdoc@setdescriptionstyle}, -			leftmargin=2ex, -			labelindent=0pt, -		}% +		\projdoc@description@leftmargin=2ex% +		\projdoc@description@labelindent=0pt% +		\projdoc@setdescriptionstyle%  		\section*{Glossary}%  		\begin{multicols}{2}%  			\renewcommand{\glossarysection}[2][]{}% @@ -307,3 +313,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} + @@ -9,19 +9,20 @@ Please see [style.md](./style.md) for writing style and  Requirements: -- A LaTeX distribution that includes the XeLaTeX compiler +- A LaTeX distribution that includes the XeLaTeX compiler and latexmk  - PlantUML -- Python 3 (timerep only) +- Python 3  - Fonts (see see [style.md](./style.md) for download links) -A `latexmkrc` file is provided for copmilation with `latexmk`. The documents +A `latexmkrc` file is provided for copmilation with latexmk. The documents  should also compile under [Visual Studio Code][vscode] using the [LaTeX  Workshop extension][latexworkshop], as well as [VimTeX][vimtex]. -## Requirements +## TODO -TODO: how to store + cross-reference requirements w/o extra latex compilation -runs +- Requirement cross-references are broken (they print both the label and the +  path to the other document, should be label only). Interesting: +  `\creflabelformat` and `\@templabel` (inside #2 of `\creflabelformat`).  [vscode]: https://code.visualstudio.com  [latexworkshop]: https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop diff --git a/reqs.toml b/reqs.toml new file mode 100644 index 0000000..c05cf71 --- /dev/null +++ b/reqs.toml @@ -0,0 +1,56 @@ +# This is a TOML file containing all project requirements. The reqs2tex script +# can be used to generate the files necessary to compile requirements.tex and +# cross-reference the requirements from other documents. +# +# Note that TOML has a hash table structure, so keys used for defining +# requirement properties cannot be used for requirement IDs. The properties are: +# label, type, id, index, deleted, done, description, priority + +# This is the requirement cross-reference ID. Requirements can be +# (cross-)referenced from LaTeX by prefixing this ID with `req:` and +# substituting dots for colons (i.e. this requirement is referenced as +# \cref{req:audio:async-api}). +[audio.async-api] +# Requirement type ('system' | 'user') +type = 'system' +# MoSCoW priority ('must' | 'should' | 'could' | 'will not') +priority = 'must' +# Requirement body. Supports LaTeX formatting. (tip: use single quotes so +# backslash doesn't act as an escape character) +description = ''' +The public audio \gls{api} supports starting audio samples asynchronously +(i.e.~fire and forget). +''' +# Definition of done (user requirements only). If 'done' is a string, it is +# treated as LaTeX code (like description), if it is a list of strings, each +# item is treated as the ID of another requirement. +#done = 'When I feel like it' +#done = [ 'audio.handle', 'audio.stream-mix' ] +# Requirements that are no longer applicable should set `deleted` to `true`. +# This will make sure the requirements are numbered consistently across +# different document revisions. +#deleted = true + +[audio.handle] +type = 'system' +priority = 'must' +description = ''' +The public audio \gls{api} allows the game programmer to control (i.e.~play, +pause and stop) audio samples after they are created/initialized. +''' + +[audio.stream-mix] +type = 'system' +priority = 'must' +description = ''' +The audio system supports playing multiple audio streams simultaniously. +''' + +[aux.license] +type = 'system' +priority = 'must' +description = ''' +External libraries must have a license that is MIT-compatible, or one that +allows linking against MIT code. +''' + diff --git a/requirements.tex b/requirements.tex new file mode 100644 index 0000000..2936272 --- /dev/null +++ b/requirements.tex @@ -0,0 +1,22 @@ +\documentclass{projdoc} +\input{meta.tex} + +\makeatletter +\projdoc@description@leftmargin=2ex +\projdoc@description@labelindent=0pt +\projdoc@setdescriptionstyle +\makeatother + +\title{Requirements} + +\begin{document} +\tablestables +\newpage + +\section{Requirements} +\begin{multicols}{2} +\input{reqs.tex} +\end{multicols} + +\end{document} + 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..6b7b77a --- /dev/null +++ b/scripts/reqs2tex.py @@ -0,0 +1,110 @@ +#!/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 sanitize(item, ids): +  def die(msg): +    print(f"[{item['label']}]: {msg}") +    exit(1) + +  # ensure properties +  item['description'] = item.get('description') +  item['done'] = item.get('done') +  item['priority'] = item.get('priority') +  item['type'] = item.get('type') + +  # type checks +  if item['type'] not in ['system', 'user']: +    die(f"unknown or missing requirement type {repr(item['type'])}") +  if item['priority'] not in ['must', 'should', 'could', 'will not']: +    die(f"unknown or missing requirement priority {repr(item['type'])}") + +  # conversions +  if isinstance(item['done'], list): +    # safety check +    if not set(item['done']).issubset(ids): +      die("definition of done includes unknown requirement(s)") +    item['done'] = tex.cmd('Cref', tex.label2ref(*item['done'])) + +def convert(data): +  reqs = flatten(data) +  index = 0 +  for item in reqs: +    item['id'] = tex.esc(make_id(item)) +    item['deleted'] = item.get('deleted', False) +    if item['deleted']: continue +    item['index'] = index +    index += 1 +    sanitize(item, [req['label'] for req in reqs]) + +  # skip deleted requirements (but process for make_id) +  reqs = [item for item in reqs if item['deleted'] == False] + +  return reqs + +def fmt_aux(data): +  out = [] +  for req in data: +    ref = tex.label2ref(req['label']) +    out += [ +      tex.cmd('newlabel', f"{ref}", tex.group(req['id'], req['id'], 'ggg', 'hhh', 'iii')), +      tex.cmd('newlabel', f"{ref}@cref", tex.group(f"[requirement][aaa][bbb]{req['id']}", '[ccc][ddd][eee]fff')), +    ] +  return "\n".join(out) + +def fmt_tex(data): +  out = [] +  for req in data: +    out.append( +      tex.cmd('subsection', req['id']) + "\n\n" +\ +      tex.env('description', +        tex.cmd('item', ['Priority']) + req['priority'].title() +\ +        tex.cmd('item', ['Requirement']) + req['description'] +\ +        (tex.cmd('item', ['Definition of done']) + req['done'] if req['done'] is not None else "") +      ) +    ) +  return "\n\n".join(out) + +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..2509a87 --- /dev/null +++ b/scripts/tex.py @@ -0,0 +1,52 @@ +# utility function for converting latex code + +def group(*args): +  out = "" +  for arg in args: +    if isinstance(arg, list): +      out += "[" + arg[0] + "]" +    if isinstance(arg, str): +      out += "{" + arg + "}" +  return out + +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) + "\\\\" + +def label2ref(*labels): +  return ",".join(["req:" + label.replace('.', ':') for label in labels]) + diff --git a/time2tex.py b/scripts/time2tex.py index fe3091f..8c3dd9b 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,36 +19,45 @@ def fmt_duration(sec):    return "\\,".join(out)  def fmt_percentage(fac): -  return f"{{\\footnotesize\\itshape({round(fac * 100)}\\%)}}" +  return tex.group( +    tex.cmd('footnotesize') +\ +    tex.cmd('itshape') +\ +    tex.esc(f"({round(fac * 100)}%)") +  )  def fmt_member_overview(times):    # calculations -  out = "" -  members = {} +  tracked = {}    total_time = 0    for time in times: -    if not time["name"] in members: -      members[time["name"]] = 0 -    members[time["name"]] += time["duration"] +    if not time["name"] in tracked: +      tracked[time["name"]] = 0 +    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 -  for name, tracked in members.items(): -    out += f"{name} & {fmt_duration(tracked)} & {fmt_percentage(tracked / total_time)}\\\\" -  out += r"\midrule{}" +  members = sorted(list(set(time["name"] for time in times))) +  for name in members: +    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 @@ -58,7 +67,7 @@ def fmt_weekly_overview(times):    weeks = []    member_totals = {}    total_time = sum(time["duration"] for time in times) -  members = list(set(time["name"] for time in times)) +  members = sorted(list(set(time["name"] for time in times)))    time_start = min(time["date"] for time in times)    time_end = max(time["date"] for time in times)    week_start = time_start - timedelta(days=time_start.weekday()) # round down to nearest monday @@ -84,8 +93,10 @@ 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{"    out += f"\\begin{{tabular}}{{l{'r@{~}l' * len(members)}@{{\\qquad}}r}}\\toprule"    out += r"\textbf{\#}"    for member in members: @@ -105,6 +116,7 @@ def fmt_weekly_overview(times):    # end table    out += r"\bottomrule\end{tabular}" +  out += r"}" # \fitimg    out += r"\caption{Tracked time per week}\label{tab:time-weekly}"    out += r"\end{table}" @@ -170,17 +182,15 @@ def parse(content):    return out  def fmt(times): -  # TODO: Task overview -  print(f""" -\\section{{Overviews}}\n -\\subsection{{Members}}\n -{fmt_member_overview(times)} -\\subsection{{Weekly}}\n -{fmt_weekly_overview(times)} -""") - -def main(): -  input_file = sys.argv[1] +  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 = ""    with open(input_file, "r") as file:      content = file.read() @@ -189,12 +199,15 @@ def main():    except Exception as e:      print(f"{input_file}: {e}")      exit(1) +  output = fmt(parsed) -  fmt(parsed) +  output_file = input_file.removesuffix(".txt") + ".tex" +  with open(output_file, "w+") as file: +    file.write(output)  if __name__ == "__main__":    if len(sys.argv) != 2: -    print("usage: time2tex <input>") +    print("usage: time2tex.py time.txt")      exit(1) -  main() +  main(sys.argv[1]) @@ -28,6 +28,8 @@ loek: 2024-09-14 45m refactoring :: clean up LaTeX build system  loek: 2024-09-14 1h10m docs :: requirements  loek: 2024-09-15 2h55m docs :: requirements  loek: 2024-09-16 2h30m docs :: requirements +loek: 2024-09-17 1h20m project meeting +loek: 2024-09-17 55m bugs :: cross-platform latexmk filespec  max: 2024-09-02 1h project kickoff  max: 2024-09-02 45m first project meeting  |