aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--example.tex13
-rw-r--r--glossary.bib5
-rw-r--r--latexmkrc17
-rw-r--r--projdoc.cls34
-rw-r--r--readme.md13
-rw-r--r--reqs.toml56
-rw-r--r--requirements.tex22
-rw-r--r--scripts/.gitignore1
-rwxr-xr-xscripts/reqs2tex.py110
-rw-r--r--scripts/tex.py52
-rwxr-xr-xscripts/time2tex.py (renamed from time2tex.py)81
-rw-r--r--time.txt2
13 files changed, 351 insertions, 56 deletions
diff --git a/.gitignore b/.gitignore
index dfaa9e7..74caed0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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},
+}
+
diff --git a/latexmkrc b/latexmkrc
index 293cf09..880f859 100644
--- a/latexmkrc
+++ b/latexmkrc
@@ -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}
+
diff --git a/readme.md b/readme.md
index b63392b..7b58cfd 100644
--- a/readme.md
+++ b/readme.md
@@ -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])
diff --git a/time.txt b/time.txt
index 1863381..0db7b22 100644
--- a/time.txt
+++ b/time.txt
@@ -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