From f6cb1e9d141d881ae6205027626d6643776e833c Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Sun, 15 Sep 2024 20:46:48 +0200
Subject: WIP requirements

---
 scripts/reqs2tex.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 83 insertions(+)
 create mode 100755 scripts/reqs2tex.py

(limited to 'scripts/reqs2tex.py')

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])
+
-- 
cgit v1.2.3


From dd2db2b7f62106e6c6c2abdaed73c5f608c690c6 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Mon, 16 Sep 2024 16:40:46 +0200
Subject: update reqs2tex and add comments to reqs.toml

---
 reqs.toml           | 25 +++++++++++++++++++++++++
 scripts/reqs2tex.py | 42 +++++++++++++++++++++++++++++++++++-------
 scripts/tex.py      |  3 +++
 3 files changed, 63 insertions(+), 7 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/reqs.toml b/reqs.toml
index 5e00dd9..c05cf71 100644
--- a/reqs.toml
+++ b/reqs.toml
@@ -1,10 +1,35 @@
+# 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'
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 9e71a48..c5ab3dd 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -9,7 +9,7 @@ def flatten(data):
     items = flatten(value)
     for item in items:
       if 'label' in item:
-        item['label'] = f"{key}:{item['label']}"
+        item['label'] = f"{key}.{item['label']}"
       else:
         item['label'] = f"{key}"
     out += items
@@ -24,16 +24,44 @@ def make_id(item):
     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'])}")
+
+  # logic checks
+  if item['type'] != 'user' and item['done'] is not None:
+    die("has definition of done but is not a user requirement")
+
+  # 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)
-  for index, item in enumerate(reqs):
+  index = 0
+  for item in 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)
+    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]
diff --git a/scripts/tex.py b/scripts/tex.py
index b044857..2fd51d8 100644
--- a/scripts/tex.py
+++ b/scripts/tex.py
@@ -41,3 +41,6 @@ def esc(plain):
 def tabrule(*cells):
   return "&".join(cells) + "\\\\"
 
+def label2ref(*labels):
+  return ",".join(["req:" + label.replace('.', ':') for label in labels])
+
-- 
cgit v1.2.3


From b31ebef3db3765eef8e0492897e870a9fa4cd32b Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Mon, 16 Sep 2024 18:48:23 +0200
Subject: fix cross-reference links to external file

---
 example.tex         |  4 ++--
 projdoc.cls         |  1 +
 readme.md           |  6 ++++++
 requirements.tex    |  1 -
 scripts/reqs2tex.py | 13 +++++--------
 5 files changed, 14 insertions(+), 11 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/example.tex b/example.tex
index ee1377a..e0d21c0 100644
--- a/example.tex
+++ b/example.tex
@@ -3,7 +3,7 @@
 % with the [draft] option. this replaces all images with placeholders.
 \input{meta.tex}
 
-\input{reqs.aux}
+\externaldocument{reqs}[./requirements.pdf]
 
 \title{Example Document}
 
@@ -174,7 +174,7 @@ 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}.
+e.g.~\cref{req:audio:handle,req:audio:async-api}.
 
 \end{document}
 
diff --git a/projdoc.cls b/projdoc.cls
index c11fe61..8a592e3 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}
diff --git a/readme.md b/readme.md
index 818d445..7b58cfd 100644
--- a/readme.md
+++ b/readme.md
@@ -18,6 +18,12 @@ 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].
 
+## TODO
+
+- 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
 [vimtex]: https://github.com/lervag/vimtex
diff --git a/requirements.tex b/requirements.tex
index 39e5831..1b51220 100644
--- a/requirements.tex
+++ b/requirements.tex
@@ -6,6 +6,5 @@
 
 \begin{document}
 
-
 \end{document}
 
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index c5ab3dd..667eeb6 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -69,18 +69,15 @@ def convert(data):
   return reqs
 
 def req2aux(req):
-  # TODO: this is a dead-end solution, newlabel only works for in-document anchors, not external links
+  ref = tex.label2ref(req['label'])
   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']}", '')),
+    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([tex.auxout(line) for line in out])
+  return "\n".join(out)
 
 def fmt_aux(data):
-  out = ""
-  out += tex.cmd('makeatletter')
-  out += "\n".join([req2aux(req) for req in data])
-  out += tex.cmd('makeatother')
+  out = "\n".join([req2aux(req) for req in data])
   return out
 
 def fmt_tex(data):
-- 
cgit v1.2.3


From 2e49e0e0db184295eb08e930a3ccdf10e80e40fe Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Mon, 16 Sep 2024 19:07:50 +0200
Subject: implement simple requirements dump

---
 glossary.bib        |  5 +++++
 projdoc.cls         | 25 +++++++++++++++----------
 requirements.tex    | 12 +++++++++++-
 scripts/reqs2tex.py | 32 +++++++++++++++++++-------------
 scripts/tex.py      |  8 +++++++-
 5 files changed, 57 insertions(+), 25 deletions(-)

(limited to 'scripts/reqs2tex.py')

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/projdoc.cls b/projdoc.cls
index 8a592e3..fe8c8bc 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -121,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
@@ -230,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][]{}%
diff --git a/requirements.tex b/requirements.tex
index 1b51220..dee529d 100644
--- a/requirements.tex
+++ b/requirements.tex
@@ -1,10 +1,20 @@
 \documentclass{projdoc}
 \input{meta.tex}
-\input{reqs.tex}
+
+\makeatletter
+\projdoc@description@leftmargin=2ex
+\projdoc@description@labelindent=0pt
+\projdoc@setdescriptionstyle
+\makeatother
 
 \title{Requirements}
 
 \begin{document}
 
+\section{Requirements}
+\begin{multicols}{2}
+\input{reqs.tex}
+\end{multicols}
+
 \end{document}
 
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 667eeb6..3bf0501 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -68,22 +68,28 @@ def convert(data):
 
   return reqs
 
-def req2aux(req):
-  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_aux(data):
-  out = "\n".join([req2aux(req) for req in data])
-  return out
+  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):
-  return "\n".join([
-    tex.cmd('relax')
-  ])
+  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['type'] == 'user' else "")
+      )
+    )
+  return "\n\n".join(out)
 
 def main(input_file):
   data = {}
diff --git a/scripts/tex.py b/scripts/tex.py
index 2fd51d8..2509a87 100644
--- a/scripts/tex.py
+++ b/scripts/tex.py
@@ -1,7 +1,13 @@
 # utility function for converting latex code
 
 def group(*args):
-  return "".join("{" + arg + "}" for arg in 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
-- 
cgit v1.2.3


From 1de74261bef1a4c25250b7390b965093141c88f0 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Tue, 17 Sep 2024 10:12:20 +0200
Subject: remove user requirement dod check

---
 scripts/reqs2tex.py | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 3bf0501..6b7b77a 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -41,10 +41,6 @@ def sanitize(item, ids):
   if item['priority'] not in ['must', 'should', 'could', 'will not']:
     die(f"unknown or missing requirement priority {repr(item['type'])}")
 
-  # logic checks
-  if item['type'] != 'user' and item['done'] is not None:
-    die("has definition of done but is not a user requirement")
-
   # conversions
   if isinstance(item['done'], list):
     # safety check
@@ -86,7 +82,7 @@ def fmt_tex(data):
       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['type'] == 'user' else "")
+        (tex.cmd('item', ['Definition of done']) + req['done'] if req['done'] is not None else "")
       )
     )
   return "\n\n".join(out)
-- 
cgit v1.2.3


From 581044887a16d37c90116da544f5d9d600faa80c Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Tue, 17 Sep 2024 16:56:36 +0200
Subject: more fixes for reqs2tex

---
 scripts/reqs2tex.py | 103 ++++++++++++++++++++++++++++++++++------------------
 scripts/tex.py      |   3 --
 2 files changed, 68 insertions(+), 38 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 6b7b77a..db7e174 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -1,17 +1,47 @@
 #!/bin/python3
 import sys, tomllib, tex
-
+from enum import StrEnum
+
+def label2ref(*labels):
+  return ",".join(["req:" + label.replace('.', ':') for label in labels])
+
+class KEY(StrEnum):
+  LABEL = 'label'
+  TYPE = 'type'
+  ID = 'id'
+  INDEX = 'index'
+  DELETED = 'deleted'
+  DONE = 'done'
+  DESCRIPTION = 'description'
+  PRIORITY = 'priority'
+
+class REQ_TYPE(StrEnum):
+  SYSTEM = 'system'
+  USER = 'user'
+
+class REQ_PRIORITY(StrEnum):
+  MUST = 'must'
+  SHOULD = 'should'
+  COULD = 'could'
+  WONT = 'will not'
+
+# this doesn't work right
 def flatten(data):
-  if 'description' in data:
-    return [ data ]
   out = []
+  # this key is a requirement
+  if KEY.DESCRIPTION in data:
+    out.append(data)
+  # check for children
   for key, value in data.items():
+    # skip over reserved keys
+    if key in KEY: continue
+
     items = flatten(value)
     for item in items:
-      if 'label' in item:
-        item['label'] = f"{key}.{item['label']}"
+      if KEY.LABEL in item:
+        item[KEY.LABEL] = f"{key}.{item[KEY.LABEL]}"
       else:
-        item['label'] = f"{key}"
+        item[KEY.LABEL] = f"{key}"
     out += items
   return out
 
@@ -20,72 +50,75 @@ def make_id(item):
   global id_counter
   id_counter += 1
   return "{type_short}#{counter:03d}".format(
-    type_short = item['type'][0].upper(),
+    type_short = item[KEY.TYPE][0].upper(),
     counter = id_counter,
   )
 
 def sanitize(item, ids):
   def die(msg):
-    print(f"[{item['label']}]: {msg}")
+    print(f"[{item[KEY.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')
+  item[KEY.DESCRIPTION] = item.get(KEY.DESCRIPTION)
+  item[KEY.DONE] = item.get(KEY.DONE)
+  item[KEY.PRIORITY] = item.get(KEY.PRIORITY)
+  item[KEY.TYPE] = item.get(KEY.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'])}")
+  if item[KEY.TYPE] not in REQ_TYPE:
+    die(f"unknown or missing requirement type: {repr(item[KEY.TYPE])}")
+  if item[KEY.PRIORITY] not in REQ_PRIORITY:
+    die(f"unknown or missing requirement priority: {repr(item[KEY.PRIORITY])}")
 
   # conversions
-  if isinstance(item['done'], list):
+  if isinstance(item[KEY.DONE], list):
     # safety check
-    if not set(item['done']).issubset(ids):
+    if not set(item[KEY.DONE]).issubset(ids):
       die("definition of done includes unknown requirement(s)")
-    item['done'] = tex.cmd('Cref', tex.label2ref(*item['done']))
+    item[KEY.DONE] = tex.cmd('Cref', label2ref(*item[KEY.DONE]))
 
 def convert(data):
   reqs = flatten(data)
+  all_ids = [item[KEY.LABEL] for item in reqs]
   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
+    item[KEY.ID] = tex.esc(make_id(item))
+    item[KEY.DELETED] = item.get(KEY.DELETED, False)
+    if item[KEY.DELETED]: continue
+    item[KEY.INDEX] = index
     index += 1
-    sanitize(item, [req['label'] for req in reqs])
+    sanitize(item, all_ids)
 
   # skip deleted requirements (but process for make_id)
-  reqs = [item for item in reqs if item['deleted'] == False]
+  reqs = [item for item in reqs if item[KEY.DELETED] == False]
 
   return reqs
 
 def fmt_aux(data):
   out = []
-  for req in data:
-    ref = tex.label2ref(req['label'])
+  for item in data:
+    ref = label2ref(item[KEY.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')),
+      tex.cmd('newlabel', f"{ref}", tex.group(item[KEY.ID], item[KEY.ID], 'ggg', 'hhh', 'iii')),
+      tex.cmd('newlabel', f"{ref}@cref", tex.group(f"[requirement][aaa][bbb]{item[KEY.ID]}", '[ccc][ddd][eee]fff')),
     ]
   return "\n".join(out)
 
 def fmt_tex(data):
   out = []
-  for req in data:
+  for item in data:
     out.append(
-      tex.cmd('subsection', req['id']) + "\n\n" +\
+      tex.cmd('subsection', item[KEY.ID]) +\
+      tex.cmd('label', label2ref(item[KEY.LABEL])) +\
+      tex.cmd('par') +\
       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 "")
+        tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title() +\
+        tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION] +\
+        (tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else "")
       )
     )
-  return "\n\n".join(out)
+  return "".join(out)
 
 def main(input_file):
   data = {}
diff --git a/scripts/tex.py b/scripts/tex.py
index 2509a87..59c6895 100644
--- a/scripts/tex.py
+++ b/scripts/tex.py
@@ -47,6 +47,3 @@ def esc(plain):
 def tabrule(*cells):
   return "&".join(cells) + "\\\\"
 
-def label2ref(*labels):
-  return ",".join(["req:" + label.replace('.', ':') for label in labels])
-
-- 
cgit v1.2.3


From ca5fb75953ae2a73d2d41ceff59e2688b11cbf2b Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Tue, 17 Sep 2024 17:06:04 +0200
Subject: fix flatten function

---
 scripts/reqs2tex.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index db7e174..8c2236a 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -25,18 +25,19 @@ class REQ_PRIORITY(StrEnum):
   COULD = 'could'
   WONT = 'will not'
 
-# this doesn't work right
 def flatten(data):
   out = []
-  # this key is a requirement
-  if KEY.DESCRIPTION in data:
-    out.append(data)
-  # check for children
   for key, value in data.items():
+    # this item is a requirement
+    if key == KEY.DESCRIPTION:
+      out.append(data)
+
     # skip over reserved keys
     if key in KEY: continue
 
+    # recursively flatten other requirements
     items = flatten(value)
+    # and prefix them with the current key
     for item in items:
       if KEY.LABEL in item:
         item[KEY.LABEL] = f"{key}.{item[KEY.LABEL]}"
-- 
cgit v1.2.3


From 1df61d671706436c17e23bc9dcdc3bbd0f14a167 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Tue, 17 Sep 2024 17:35:19 +0200
Subject: labels/refs working inside requirements.tex

---
 scripts/reqs2tex.py | 28 +++++++++++++++++-----------
 scripts/tex.py      | 17 +++++++++++++++--
 2 files changed, 32 insertions(+), 13 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 8c2236a..700d05f 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -107,19 +107,25 @@ def fmt_aux(data):
   return "\n".join(out)
 
 def fmt_tex(data):
-  out = []
+  out = ""
   for item in data:
-    out.append(
-      tex.cmd('subsection', item[KEY.ID]) +\
-      tex.cmd('label', label2ref(item[KEY.LABEL])) +\
-      tex.cmd('par') +\
-      tex.env('description',
-        tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title() +\
-        tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION] +\
-        (tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else "")
-      )
+    out += tex.join(
+      tex.cmd('subsection', item[KEY.ID]),
+      tex.withatletter(
+        tex.cmd('cref@constructprefix', 'requirement', r'\cref@result'),
+        tex.pedef('@currentlabel', item[KEY.ID]),
+        tex.pedef('@currentlabelname', item[KEY.ID]),
+        tex.pedef('cref@currentlabel', tex.group(['requirement'], [''], [r'\cref@result']) + item[KEY.ID]),
+      ),
+      tex.cmd('label', ['requirement'], label2ref(item[KEY.LABEL])),
+      tex.cmd('par'),
+      tex.env('description', tex.join(
+        tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title(),
+        tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION],
+        (tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else ""),
+      ))
     )
-  return "".join(out)
+  return out
 
 def main(input_file):
   data = {}
diff --git a/scripts/tex.py b/scripts/tex.py
index 59c6895..eebf8ec 100644
--- a/scripts/tex.py
+++ b/scripts/tex.py
@@ -9,6 +9,9 @@ def group(*args):
       out += "{" + arg + "}"
   return out
 
+def join(*things):
+  return "".join(things)
+
 def string(content):
   return r"\string" + content
 
@@ -18,11 +21,14 @@ def cmd(*args):
   if len(args) == 0: args = [""]
   return f"\\{name}" + group(*args)
 
+def pedef(*args):
+  return r"\protected@edef" + cmd(*args)
+
 def csdef(*args):
   return r"\def" + cmd(*args)
 
-def auxout(content):
-  return r"\write\@auxout" + group(content)
+def auxout(*content):
+  return r"\write\@auxout" + group(join(*content))
 
 def scmd(*args):
   return string(cmd(*args))
@@ -47,3 +53,10 @@ def esc(plain):
 def tabrule(*cells):
   return "&".join(cells) + "\\\\"
 
+def withatletter(*content):
+  return join(
+    cmd('makeatletter'),
+    *content,
+    cmd('makeatother'),
+  )
+
-- 
cgit v1.2.3


From 6b034454f35819999cc26cfe472d537bf1eb3fbf Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Wed, 18 Sep 2024 13:40:39 +0200
Subject: fix requirement cross-references

---
 example.tex         |  2 --
 projdoc.cls         | 83 +++++++++++++++++++++++++++++++++++++++++++++++------
 readme.md           |  6 ----
 scripts/reqs2tex.py | 16 +++++++++--
 4 files changed, 88 insertions(+), 19 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/example.tex b/example.tex
index e0d21c0..24c525b 100644
--- a/example.tex
+++ b/example.tex
@@ -3,8 +3,6 @@
 % with the [draft] option. this replaces all images with placeholders.
 \input{meta.tex}
 
-\externaldocument{reqs}[./requirements.pdf]
-
 \title{Example Document}
 
 \begin{document}
diff --git a/projdoc.cls b/projdoc.cls
index fe8c8bc..23f3ea9 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -145,13 +145,13 @@
 % \cref{<refname>} is used
 \makeatletter
 \NewDocumentCommand{\customlabel}{omm}{%
-  \begingroup
-  \cref@constructprefix{#1}{\cref@result}%
-  \protected@edef\@currentlabel{#3}%
-  \protected@edef\@currentlabelname{#3}%
-  \protected@edef\cref@currentlabel{[#1][][\cref@result]#3}
-  \label[#1]{#2}%
-  \endgroup
+	\begingroup
+	\cref@constructprefix{#1}{\cref@result}%
+	\protected@edef\@currentlabel{#3}%
+	\protected@edef\@currentlabelname{#3}%
+	\protected@edef\cref@currentlabel{[#1][][\cref@result]#3}
+	\label[#1]{#2}%
+	\endgroup
 }
 \makeatother
 
@@ -215,6 +215,9 @@
 	selection={recorded and deps and see},
 ]
 
+% requirements
+\externaldocument{reqs}[./requirements.pdf]
+
 % default document header/trailer
 \makeatletter
 \def\projdoc@header{
@@ -293,8 +296,8 @@
 	\def\UrlRight{\hbox{\,}}%
 }
 \DefineVerbatimEnvironment{blockcode}{Verbatim}{
-  tabsize=2,
-  obeytabs,
+	tabsize=2,
+	obeytabs,
 }
 
 % scale down image if it exceeds page margins
@@ -321,3 +324,65 @@
 \crefname{test}{test}{tests}
 \Crefname{test}{Test}{Tests}
 
+% fix cleveref showing filename to external cross-reference
+% see <https://tex.stackexchange.com/a/708445/211562>
+% edited from cleveref source
+\makeatletter
+\def\cref@getref#1#2{%
+	\expandafter\let\expandafter#2\csname r@#1@cref\endcsname%
+	\expandafter\expandafter\expandafter\def%
+		\expandafter\expandafter\expandafter#2%
+		\expandafter\expandafter\expandafter{%
+			\expandafter\@firstoffive#2}}%
+\def\cpageref@getref#1#2{%
+	\expandafter\let\expandafter#2\csname r@#1@cref\endcsname%
+	\expandafter\expandafter\expandafter\def%
+		\expandafter\expandafter\expandafter#2%
+		\expandafter\expandafter\expandafter{%
+			\expandafter\@secondoffive#2}}%
+\AtBeginDocument{%
+	\def\label@noarg#1{%
+		\cref@old@label{#1}%
+		\@bsphack%
+		\edef\@tempa{{page}{\the\c@page}}%
+		\setcounter{page}{1}%
+		\edef\@tempb{\thepage}%
+		\expandafter\setcounter\@tempa%
+		\cref@constructprefix{page}{\cref@result}%
+		\protected@write\@auxout{}{%
+			\string\newlabel{#1@cref}{%
+				{\cref@currentlabel}%
+				{[\@tempb][\arabic{page}][\cref@result]\thepage}%
+				{}%
+				{}%
+				{}%
+			}%
+		}%
+		\@esphack%
+	}%
+	\def\label@optarg[#1]#2{%
+		\cref@old@label{#2}%
+		\@bsphack%
+		\edef\@tempa{{page}{\the\c@page}}%
+		\setcounter{page}{1}%
+		\edef\@tempb{\thepage}%
+		\expandafter\setcounter\@tempa%
+		\cref@constructprefix{page}{\cref@result}%
+		\protected@edef\cref@currentlabel{%
+			\expandafter\cref@override@label@type%
+			\cref@currentlabel\@nil{#1}%
+		}%
+		\protected@write\@auxout{}{%
+			\string\newlabel{#2@cref}{%
+				{\cref@currentlabel}%
+				{[\@tempb][\arabic{page}][\cref@result]\thepage}%
+				{}%
+				{}%
+				{}%
+			}%
+		}%
+		\@esphack%
+	}%
+}
+\makeatother
+
diff --git a/readme.md b/readme.md
index 7b58cfd..818d445 100644
--- a/readme.md
+++ b/readme.md
@@ -18,12 +18,6 @@ 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].
 
-## TODO
-
-- 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
 [vimtex]: https://github.com/lervag/vimtex
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 700d05f..8a6976a 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -101,8 +101,20 @@ def fmt_aux(data):
   for item in data:
     ref = label2ref(item[KEY.LABEL])
     out += [
-      tex.cmd('newlabel', f"{ref}", tex.group(item[KEY.ID], item[KEY.ID], 'ggg', 'hhh', 'iii')),
-      tex.cmd('newlabel', f"{ref}@cref", tex.group(f"[requirement][aaa][bbb]{item[KEY.ID]}", '[ccc][ddd][eee]fff')),
+      tex.cmd('newlabel', f"{ref}", tex.group(
+        item[KEY.ID],
+        '',
+        '',
+        ref,
+        '',
+      )),
+      tex.cmd('newlabel', f"{ref}@cref", tex.group(
+        f"[requirement][][]{item[KEY.ID]}",
+        '[][][]',
+        '',
+        '',
+        './requirements.pdf',
+      )),
     ]
   return "\n".join(out)
 
-- 
cgit v1.2.3


From f2e1083970107994a031a394198fde039bdf3b77 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Wed, 18 Sep 2024 14:10:59 +0200
Subject: cleanup

---
 projdoc.cls         | 2 +-
 scripts/reqs2tex.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/projdoc.cls b/projdoc.cls
index 23f3ea9..7420d38 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -216,7 +216,7 @@
 ]
 
 % requirements
-\externaldocument{reqs}[./requirements.pdf]
+\externaldocument{reqs}[requirements.pdf]
 
 % default document header/trailer
 \makeatletter
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 8a6976a..82b0aae 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -113,7 +113,7 @@ def fmt_aux(data):
         '[][][]',
         '',
         '',
-        './requirements.pdf',
+        '',
       )),
     ]
   return "\n".join(out)
-- 
cgit v1.2.3


From 0027f5df316892f121bb9f4b5b6b641646273ff0 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Wed, 18 Sep 2024 14:54:48 +0200
Subject: add requirements + improve generated reqs.tex

---
 projdoc.cls         |  7 +++++--
 reqs.toml           | 39 ++++++++++++++++++++++++++++++---------
 requirements.tex    |  1 +
 scripts/reqs2tex.py | 15 ++++++++-------
 scripts/tex.py      | 10 ++++++++++
 5 files changed, 54 insertions(+), 18 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/projdoc.cls b/projdoc.cls
index 7420d38..fccf8c1 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -215,8 +215,11 @@
 	selection={recorded and deps and see},
 ]
 
-% requirements
-\externaldocument{reqs}[requirements.pdf]
+% allow cross-references to requirements.pdf from all documents except
+% requirements.pdf itself
+\IfEq*{\jobname}{requirements}{}{
+	\externaldocument{reqs}[requirements.pdf]
+}
 
 % default document header/trailer
 \makeatletter
diff --git a/reqs.toml b/reqs.toml
index c05cf71..6645ea4 100644
--- a/reqs.toml
+++ b/reqs.toml
@@ -10,33 +10,46 @@
 # (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]
+[audio]
 # Requirement type ('system' | 'user')
-type = 'system'
+type = 'user'
 # 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).
+The engine allows the game programmer to easily start, pause and stop
+background music, while simultaniously playing sound effects.
 '''
-# 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.
+# Definition of done. If 'done' is a string, it is treated as LaTeX code, if it
+# is a list of strings, each item is treated as the ID of another requirement,
+# and the references are checked before LaTeX runs.
+done = [
+	'audio.async-api',
+	'audio.handle',
+	'audio.stream-mix',
+	'audio.volume',
+]
 #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.async-api]
+type = 'system'
+priority = 'must'
+description = '''
+The public audio \gls{api} supports starting audio samples asynchronously
+(i.e.~fire and forget).
+'''
+
 [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.
+pause, resume and stop) audio samples after they are created/initialized.
 '''
 
 [audio.stream-mix]
@@ -46,6 +59,14 @@ description = '''
 The audio system supports playing multiple audio streams simultaniously.
 '''
 
+[audio.volume]
+type = 'system'
+priority = 'must'
+description = '''
+The public audio \gls{api} allows the game programmer to control the volume of
+audio samples.
+'''
+
 [aux.license]
 type = 'system'
 priority = 'must'
diff --git a/requirements.tex b/requirements.tex
index 2936272..cbaba81 100644
--- a/requirements.tex
+++ b/requirements.tex
@@ -6,6 +6,7 @@
 \projdoc@description@labelindent=0pt
 \projdoc@setdescriptionstyle
 \makeatother
+\setcounter{secnumdepth}{1}
 
 \title{Requirements}
 
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 82b0aae..ff9f3bb 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -122,7 +122,7 @@ def fmt_tex(data):
   out = ""
   for item in data:
     out += tex.join(
-      tex.cmd('subsection', item[KEY.ID]),
+      tex.cmd('subsection', f"{item[KEY.ID]}: {item[KEY.LABEL]}".upper()),
       tex.withatletter(
         tex.cmd('cref@constructprefix', 'requirement', r'\cref@result'),
         tex.pedef('@currentlabel', item[KEY.ID]),
@@ -130,12 +130,13 @@ def fmt_tex(data):
         tex.pedef('cref@currentlabel', tex.group(['requirement'], [''], [r'\cref@result']) + item[KEY.ID]),
       ),
       tex.cmd('label', ['requirement'], label2ref(item[KEY.LABEL])),
-      tex.cmd('par'),
-      tex.env('description', tex.join(
-        tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title(),
-        tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION],
-        (tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else ""),
-      ))
+      tex.cmd('parbox', tex.cmd('linewidth'),
+        tex.env('description', tex.join(
+          tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title(),
+          tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION],
+          (tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else ""),
+        )),
+      )
     )
   return out
 
diff --git a/scripts/tex.py b/scripts/tex.py
index e8fc65b..07d275a 100644
--- a/scripts/tex.py
+++ b/scripts/tex.py
@@ -69,3 +69,13 @@ def explist(*items):
       out += explist(*item)
   return out
 
+def sec(level, heading):
+  level = max(min(3, level), 0)
+  section = [
+    'section',
+    'subsection',
+    'subsubsection',
+    'paragraph',
+  ][level]
+  return cmd(section, heading)
+
-- 
cgit v1.2.3


From abab5051391b7f8a212539400433eb1db0bb4f06 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Wed, 18 Sep 2024 15:06:45 +0200
Subject: abbreviate requirement field labels after first occurrence on every
 page

---
 glossary.bib        | 13 +++++++++++++
 requirements.tex    | 21 +++++++++++++++++++++
 scripts/reqs2tex.py | 11 ++++++++---
 3 files changed, 42 insertions(+), 3 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/glossary.bib b/glossary.bib
index 437db86..8bf48ac 100644
--- a/glossary.bib
+++ b/glossary.bib
@@ -33,3 +33,16 @@
 	long = {Application Programming Interface},
 }
 
+@acronym{reqlabel-priority,
+	short = {pri},
+	long = {priority},
+}
+@acronym{reqlabel-description,
+	short = {req},
+	long = {requirement},
+}
+@acronym{reqlabel-done,
+	short = {DoD},
+	long = {definition of done},
+}
+
diff --git a/requirements.tex b/requirements.tex
index cbaba81..78496e8 100644
--- a/requirements.tex
+++ b/requirements.tex
@@ -8,6 +8,27 @@
 \makeatother
 \setcounter{secnumdepth}{1}
 
+\usepackage{bophook}
+\makeatletter
+\def\reqlabel#1{\csuse{reqlabel@#1}}
+\def\reqlabelreset{%
+	\global\def\reqlabel@priority{%
+		\Glsdesc{reqlabel-priority}%
+		\global\def\reqlabel@priority{\Glstext{reqlabel-priority}}%
+	}%
+	\global\def\reqlabel@description{%
+		\Glsdesc{reqlabel-description}%
+		\global\def\reqlabel@description{\Glstext{reqlabel-description}}%
+	}%
+	\global\def\reqlabel@done{%
+		\Glsdesc{reqlabel-done}%
+		\global\def\reqlabel@done{\Glstext{reqlabel-done}}%
+	}%
+}
+% Abbreviate requirement field labels after first occurrence on every page
+\AtBeginPage{\reqlabelreset}
+\makeatother
+
 \title{Requirements}
 
 \begin{document}
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index ff9f3bb..e5f063d 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -132,9 +132,14 @@ def fmt_tex(data):
       tex.cmd('label', ['requirement'], label2ref(item[KEY.LABEL])),
       tex.cmd('parbox', tex.cmd('linewidth'),
         tex.env('description', tex.join(
-          tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title(),
-          tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION],
-          (tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else ""),
+          tex.cmd('item', [tex.cmd('reqlabel', 'priority')]),
+          item[KEY.PRIORITY].title(),
+          tex.cmd('item', [tex.cmd('reqlabel', 'description')]),
+          item[KEY.DESCRIPTION],
+          *([
+            tex.cmd('item', [tex.cmd('reqlabel', 'done')]),
+            item[KEY.DONE]
+          ] if item[KEY.DONE] is not None else []),
         )),
       )
     )
-- 
cgit v1.2.3


From c37f21e48f17abe9fb6bf1549f680e6f730aed8c Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Wed, 18 Sep 2024 16:52:40 +0200
Subject: WIP research

---
 projdoc.cls         |  3 +++
 reqs.toml           | 16 ++++++++++-
 research.tex        | 34 ++++++++++-------------
 scripts/reqs2tex.py |  3 +++
 sources.bib         | 77 +++++++++++++++++++++++++++++++++++++++--------------
 5 files changed, 92 insertions(+), 41 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/projdoc.cls b/projdoc.cls
index fccf8c1..05e401c 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -389,3 +389,6 @@
 }
 \makeatother
 
+% missing reference marker
+\def\mref{\textsuperscript{\textit{\,ref?}}}
+
diff --git a/reqs.toml b/reqs.toml
index 6645ea4..2da91ac 100644
--- a/reqs.toml
+++ b/reqs.toml
@@ -67,7 +67,9 @@ The public audio \gls{api} allows the game programmer to control the volume of
 audio samples.
 '''
 
-[aux.license]
+# TODO: audio encoding support?
+
+[lib.license]
 type = 'system'
 priority = 'must'
 description = '''
@@ -75,3 +77,15 @@ External libraries must have a license that is MIT-compatible, or one that
 allows linking against MIT code.
 '''
 
+[lib.platform]
+type = 'system'
+priority = 'must'
+description = '''
+External libraries must have cross-platform support for at least Linux and
+Windows.
+'''
+
+# TODO: library popularity as quality factor?
+# TODO: library documentation as quality factor?
+# TODO: modularity over less libraries? (i.e. why don't we just SDL2 everything?)
+
diff --git a/research.tex b/research.tex
index ad33d10..abfde3b 100644
--- a/research.tex
+++ b/research.tex
@@ -201,44 +201,38 @@ for audio some options could be: FMOD, Wwise, or iirKlang
 
 \subsection{Conclusion}
 
-% TODO: this entire section
 \section{Audio}
 
-% should audio research be scoped down to SDL2 (if that's what we're going with) or
-% standalone libraries only (for modularity?).
-
-The game engine is required to have an audio system with support for playing multiple
-audio streams (i.e.~tracks or samples) simultaniously. Since writing a custom live
-audio mixing engine is outside the scope of this project, this section compares
+The game engine is required to have an audio system
+\autocite[\ref{req:audio}]{crepe:requirements}. Since writing a custom real-time
+audio mixing engine is outside the scope of this project\mref, this section compares
 various standalone audio engines that could be used in the engine.
 
-% TODO: requirements first!
+\subsection{Libraries}
+
+\Cref{tab:audio-engines} compares several standalone audio engine libraries that fit
+\cref{req:audio,req:lib:license}.
 
-% REQ ~ is cross-platform
-% REQ ~ supports multiple audio formats (TODO: which)
-% REQ ~ supports simultanious playback / mixing
-% REQ ~ has an open-source license
 \begin{table}
 	\centering
 	\begin{tabular}{llc}
 		\toprule
 		\textbf{Library} & \textbf{License} & \textbf{API}\\
 		\midrule
-		miniaudio & MIT-0 & C\\
-		YSE & EPL & C++\\
-		SoLoud & Zlip/LibPng & C++\\
+		miniaudio \autocite{lib:miniaudio} & MIT-0 & C\\
+		YSE \autocite{lib:yse} & EPL & C++\\
+		SoLoud \autocite{lib:soloud} & Zlip/LibPng & C++\\
 		\bottomrule
 	\end{tabular}
 	\caption{Audio engine library comparison}
 	\label{tab:audio-engines}
 \end{table}
-% TODO: ref https://miniaud.io/
-% TODO: ref https://www.attr-x.net/yse/
 
-Not considered further:
+Other popular libraries that were researched but are unsuitable for this project
+include:\noparbreak
 \begin{description}
-	\item[FMOD] is proprietary
-	\item[PortAudio] requires manual mixing
+	\item[FMOD \autocite{lib:fmod}] Is proprietary (violates \cref{req:lib:license})
+	\item[PortAudio \autocite{lib:portaudio}] Does not handle mixing
 \end{description}
 
 \section{Physics}
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index e5f063d..6984466 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -94,6 +94,9 @@ def convert(data):
   # skip deleted requirements (but process for make_id)
   reqs = [item for item in reqs if item[KEY.DELETED] == False]
 
+  # sort by label
+  reqs = sorted(reqs, key=lambda item: item[KEY.LABEL])
+
   return reqs
 
 def fmt_aux(data):
diff --git a/sources.bib b/sources.bib
index 50f5ead..6f5ce0c 100644
--- a/sources.bib
+++ b/sources.bib
@@ -13,38 +13,75 @@
 }
 
 @misc{miro:scrum-board,
-  author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
-  title = {Scrum Board on Miro},
-  url = {https://miro.com/app/board/uXjVKjtdM64=/?share_link_id=303851465474},
-  date = {2024-09-10},
+	author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
+	title = {Scrum Board on Miro},
+	url = {https://miro.com/app/board/uXjVKjtdM64=/?share_link_id=303851465474},
+	date = {2024-09-10},
 }
 
 @misc{crepe:code-repo,
-  author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
-  title = {Crepe Code Repository},
-  url = {https://github.com/lonkaars/crepe},
-  date = {2024-09-10},
+	author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
+	title = {Crepe Code Repository},
+	url = {https://github.com/lonkaars/crepe},
+	date = {2024-09-10},
 }
 
 @misc{crepe:docs-repo,
-  author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
-  title = {Crepe Documentation Repository},
-  url = {https://github.com/lonkaars},
-  date = {2024-09-10},
+	author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
+	title = {Crepe Documentation Repository},
+	url = {https://github.com/lonkaars},
+	date = {2024-09-10},
 }
 
 @misc{crepe:docs-standard,
-  author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
-  title = {Crepe Documentation Standard},
-  url = {https://github.com/lonkaars/crepe-docs/blob/master/contributing.md},
-  date = {2024-09-10},
+	author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
+	title = {Crepe Documentation Standard},
+	url = {https://github.com/lonkaars/crepe-docs/blob/master/contributing.md},
+	date = {2024-09-10},
 }
 
 @misc{crepe:code-standard,
-  author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
-  title = {Crepe Code Standard},
-  url = {https://github.com/lonkaars/crepe/blob/master/contributing.md},
-  date = {2024-09-10},
+	author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
+	title = {Crepe Code Standard},
+	url = {https://github.com/lonkaars/crepe/blob/master/contributing.md},
+	date = {2024-09-10},
 }
 
+@report{crepe:requirements,
+	author = {Loek Le Blansch and Wouter Boerenkamps and Jaro Rutjes and Max Smits and Niels Stunnebrink},
+	title = {Requirements},
+	date = {2024-09-18},
+}
+
+@online{lib:miniaudio,
+	title = {miniaudio - A single file audio playback and capture library.},
+	% author = {David Reid},
+	url = {https://miniaud.io},
+	urldate = {2024-09-18},
+}
+
+@online{lib:yse,
+	title = {YSE - cross-platform sound engine},
+	url = {https://www.attr-x.net/yse},
+	urldate = {2024-09-18},
+}
+
+@online{lib:soloud,
+	title = {SoLoud},
+	% author = {Jari Komppa},
+	url = {https://solhsa.com/soloud},
+	urldate = {2024-09-18},
+}
+
+@online{lib:fmod,
+	title = {FMOD},
+	url = {https://www.fmod.com},
+	urldate = {2024-09-18},
+}
+
+@online{lib:portaudio,
+	title = {PortAudio - an Open-Source Cross-Platform Audio API},
+	url = {https://www.portaudio.com},
+	urldate = {2024-09-18},
+}
 
-- 
cgit v1.2.3


From f63057474a461fe814458c66515e281700f296df Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Thu, 19 Sep 2024 09:52:35 +0200
Subject: fix requirement ordering in reqs2tex

---
 reqs.toml           | 27 +++++++++-------------
 scripts/reqs2tex.py | 65 ++++++++++++++++++++++++++++-------------------------
 2 files changed, 45 insertions(+), 47 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/reqs.toml b/reqs.toml
index 6645ea4..35c7a96 100644
--- a/reqs.toml
+++ b/reqs.toml
@@ -1,15 +1,10 @@
 # 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}).
+# (cross-)referenced from LaTeX by prefixing this ID with `req:` (i.e. this
+# requirement is referenced as \cref{req:audio}).
 [audio]
 # Requirement type ('system' | 'user')
 type = 'user'
@@ -25,10 +20,10 @@ background music, while simultaniously playing sound effects.
 # is a list of strings, each item is treated as the ID of another requirement,
 # and the references are checked before LaTeX runs.
 done = [
-	'audio.async-api',
-	'audio.handle',
-	'audio.stream-mix',
-	'audio.volume',
+	'audio:async-api',
+	'audio:handle',
+	'audio:stream-mix',
+	'audio:volume',
 ]
 #done = 'When I feel like it'
 # Requirements that are no longer applicable should set `deleted` to `true`.
@@ -36,7 +31,7 @@ done = [
 # different document revisions.
 #deleted = true
 
-[audio.async-api]
+[audio:async-api]
 type = 'system'
 priority = 'must'
 description = '''
@@ -44,7 +39,7 @@ The public audio \gls{api} supports starting audio samples asynchronously
 (i.e.~fire and forget).
 '''
 
-[audio.handle]
+[audio:handle]
 type = 'system'
 priority = 'must'
 description = '''
@@ -52,14 +47,14 @@ The public audio \gls{api} allows the game programmer to control (i.e.~play,
 pause, resume and stop) audio samples after they are created/initialized.
 '''
 
-[audio.stream-mix]
+[audio:stream-mix]
 type = 'system'
 priority = 'must'
 description = '''
 The audio system supports playing multiple audio streams simultaniously.
 '''
 
-[audio.volume]
+[audio:volume]
 type = 'system'
 priority = 'must'
 description = '''
@@ -67,7 +62,7 @@ The public audio \gls{api} allows the game programmer to control the volume of
 audio samples.
 '''
 
-[aux.license]
+[aux:license]
 type = 'system'
 priority = 'must'
 description = '''
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index e5f063d..1863b0d 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -1,9 +1,9 @@
 #!/bin/python3
-import sys, tomllib, tex
+import sys, tomllib, tex, re
 from enum import StrEnum
 
 def label2ref(*labels):
-  return ",".join(["req:" + label.replace('.', ':') for label in labels])
+  return ",".join(["req:" + label for label in labels])
 
 class KEY(StrEnum):
   LABEL = 'label'
@@ -25,27 +25,6 @@ class REQ_PRIORITY(StrEnum):
   COULD = 'could'
   WONT = 'will not'
 
-def flatten(data):
-  out = []
-  for key, value in data.items():
-    # this item is a requirement
-    if key == KEY.DESCRIPTION:
-      out.append(data)
-
-    # skip over reserved keys
-    if key in KEY: continue
-
-    # recursively flatten other requirements
-    items = flatten(value)
-    # and prefix them with the current key
-    for item in items:
-      if KEY.LABEL in item:
-        item[KEY.LABEL] = f"{key}.{item[KEY.LABEL]}"
-      else:
-        item[KEY.LABEL] = f"{key}"
-    out += items
-  return out
-
 id_counter = 0
 def make_id(item):
   global id_counter
@@ -79,8 +58,7 @@ def sanitize(item, ids):
       die("definition of done includes unknown requirement(s)")
     item[KEY.DONE] = tex.cmd('Cref', label2ref(*item[KEY.DONE]))
 
-def convert(data):
-  reqs = flatten(data)
+def convert(reqs):
   all_ids = [item[KEY.LABEL] for item in reqs]
   index = 0
   for item in reqs:
@@ -94,6 +72,9 @@ def convert(data):
   # skip deleted requirements (but process for make_id)
   reqs = [item for item in reqs if item[KEY.DELETED] == False]
 
+  # sort by label
+  reqs = sorted(reqs, key=lambda req: req[KEY.LABEL])
+
   return reqs
 
 def fmt_aux(data):
@@ -145,20 +126,42 @@ def fmt_tex(data):
     )
   return out
 
+def tomlload(content):
+  # replace requirement labels with temp value
+  label_map = dict()
+  label_idx = 0
+  lines = content.split("\n")
+  for index, line in enumerate(lines):
+    match = re.search(r"^\s*\[(.+)\]", line)
+    if match is None: continue
+    lines[index] = f"[{label_idx}]"
+    label_map[str(label_idx)] = match.group(1)
+    label_idx += 1
+  content = "\n".join(lines)
+
+  # load TOML and replace temporary labels with real labels
+  data_dict = tomllib.loads(content)
+  data_list = []
+  for key, value in data_dict.items():
+    value[KEY.LABEL] = label_map[key]
+    data_list.append(value)
+
+  return data_list
+
 def main(input_file):
-  data = {}
-  with open(input_file, "rb") as file:
-    data = tomllib.load(file)
+  data = []
+  with open(input_file, "r") as file:
+    data = tomlload(file.read())
 
-  requirements = convert(data)
+  items = convert(data)
 
   output_aux = input_file.removesuffix(".toml") + ".aux"
   with open(output_aux, "w+") as file:
-    file.write(fmt_aux(requirements))
+    file.write(fmt_aux(items))
 
   output_tex = input_file.removesuffix(".toml") + ".tex"
   with open(output_tex, "w+") as file:
-    file.write(fmt_tex(requirements))
+    file.write(fmt_tex(items))
 
 if __name__ == "__main__":
   if len(sys.argv) != 2:
-- 
cgit v1.2.3


From 17b996ebd1c01402fb49332e45bcedb5a8706d36 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Fri, 20 Sep 2024 10:17:30 +0200
Subject: fix DeprecationWarning for python <3.12

---
 scripts/reqs2tex.py | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

(limited to 'scripts/reqs2tex.py')

diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
index 1863b0d..31303ff 100755
--- a/scripts/reqs2tex.py
+++ b/scripts/reqs2tex.py
@@ -15,15 +15,17 @@ class KEY(StrEnum):
   DESCRIPTION = 'description'
   PRIORITY = 'priority'
 
-class REQ_TYPE(StrEnum):
-  SYSTEM = 'system'
-  USER = 'user'
-
-class REQ_PRIORITY(StrEnum):
-  MUST = 'must'
-  SHOULD = 'should'
-  COULD = 'could'
-  WONT = 'will not'
+REQ_TYPE = [
+  'system',
+  'user',
+]
+
+REQ_PRIORITY = [
+  'must',
+  'should',
+  'could',
+  'will not',
+]
 
 id_counter = 0
 def make_id(item):
-- 
cgit v1.2.3