From 1c519543e2420ab80337b4717333ca0dc69b63f6 Mon Sep 17 00:00:00 2001
From: Jaro <jarorutjes07@gmail.com>
Date: Fri, 13 Sep 2024 15:51:54 +0200
Subject: updated time

---
 example.tex | 27 ++++++++++++++++++++++++++-
 1 file changed, 26 insertions(+), 1 deletion(-)

(limited to 'example.tex')

diff --git a/example.tex b/example.tex
index 8525973..ce45f6f 100644
--- a/example.tex
+++ b/example.tex
@@ -108,7 +108,7 @@ The \codeinline{blockcode} environment can be used to insert a code block:
 This is all included verbatim: \verb|asdf| \ $%!(*@#&)$
 \end{blockcode}
 
-\subsection{Lists}
+\subsection{Information dumps}
 
 Unordered (bullet) list:
 
@@ -136,9 +136,34 @@ Numbered list:
 
 (See \cref{item:enum-nest-ref})
 
+Description:
+
+\begin{description}
+	\item[Item one] description of item one
+	\item[Item two] description of item two
+	\item[Item three] ...
+\end{description}
+
+\subsection{Comparisons}
+
+\begin{comparison}
+	\pro{Good thing}
+	\con{Bad thing}
+	\pro{Good thing 2}
+	\con{Bad thing 2}
+\end{comparison}
+
 \subsection{Citations}
 
 Citations are inserted using the \codeinline{\autocite} command \autocite{rfc:3339}.
+The bibliography is automatically printed after \codeinline{\end{document}}.
+
+\subsection{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}}.
 
 \end{document}
 
-- 
cgit v1.2.3


From 4d085ec22a6d8560913f61096a83ebda4bbaa740 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Sat, 14 Sep 2024 11:10:00 +0200
Subject: clean up LaTeX build system

---
 .gitignore       |  2 --
 example.tex      | 10 ++++++----
 img/.gitignore   |  2 ++
 img/example.puml |  6 ++++++
 latexmkrc        | 61 ++++++++++++++++++++------------------------------------
 makefile         | 17 ----------------
 projdoc.cls      |  4 +++-
 7 files changed, 39 insertions(+), 63 deletions(-)
 create mode 100644 img/.gitignore
 create mode 100644 img/example.puml
 delete mode 100644 makefile

(limited to 'example.tex')

diff --git a/.gitignore b/.gitignore
index efae07a..dfaa9e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,8 +29,6 @@
 
 # output files
 *.pdf
-!img/*.pdf
-img/*.puml.pdf
 
 # generated files
 time.tex
diff --git a/example.tex b/example.tex
index ce45f6f..4ae4d95 100644
--- a/example.tex
+++ b/example.tex
@@ -35,7 +35,7 @@ These don't show up in the table of contents by default
 
 \Cref{fig:example-a} definitely shows something. Referencing multiple things at once
 is neatly handled by cleveref:
-\cref{fig:example-a,fig:example-b,fig:example-c,tab:gate-xor}.
+\cref{fig:example-a,fig:example-b,fig:example-puml,tab:gate-xor}.
 
 \begin{figure}
 	\centering
@@ -46,15 +46,17 @@ is neatly handled by cleveref:
 
 \begin{figure}
 	\begin{subfigure}{0.45\textwidth}%
+		\centering
 		\fitimg{\includegraphics{example-image-b}}
 		\caption{Example image B}
 		\label{fig:example-b}
 	\end{subfigure}%
 	\hfill
 	\begin{subfigure}{0.45\textwidth}%
-		\fitimg{\includegraphics{example-image-c}}
-		\caption{Example image C}
-		\label{fig:example-c}
+		\centering
+		\includepumldiag{img/example.puml}
+		\caption{Example PlantUML diagram}
+		\label{fig:example-puml}
 	\end{subfigure}%
 	\caption{Subfigures}
 \end{figure}
diff --git a/img/.gitignore b/img/.gitignore
new file mode 100644
index 0000000..60a51f5
--- /dev/null
+++ b/img/.gitignore
@@ -0,0 +1,2 @@
+!*.pdf
+*.eps
diff --git a/img/example.puml b/img/example.puml
new file mode 100644
index 0000000..942dce2
--- /dev/null
+++ b/img/example.puml
@@ -0,0 +1,6 @@
+@startuml
+!include theme.ipuml
+
+Bob -> Alice : hello
+
+@enduml
diff --git a/latexmkrc b/latexmkrc
index 66ed0d1..293cf09 100644
--- a/latexmkrc
+++ b/latexmkrc
@@ -1,49 +1,32 @@
-$pdflatex = "xelatex %O %S";
+# https://nl.mirrors.cicku.me/ctan/support/latexmk/latexmk.pdf
+
+$pdflatex = "xelatex --interaction=nonstopmode %O %S";
 $pdf_mode = 1;
 $dvi_mode = 0;
 $postscript_mode = 0;
+$clean_ext .= ' %R.ist %R.xdy bbl run.xml';
+@default_files = (
+	'example',
+	'plan',
+	'research',
+	'timerep',
+);
 
-# https://tex.stackexchange.com/questions/400325/latexmkrc-for-bib2gls
-add_cus_dep('glo', 'gls', 0, 'run_makeglossaries');
-add_cus_dep('acn', 'acr', 0, 'run_makeglossaries');
-add_cus_dep('aux', 'glstex', 0, 'run_bib2gls');
+push @file_not_found, '^Package .* No file `([^\\\']*)\\\'';
+push @generated_exts, 'glo', 'gls', 'glg';
 
-sub run_makeglossaries {
-  if ( $silent ) {
-    system "makeglossaries -q '$_[0]'";
-  } else {
-    system "makeglossaries '$_[0]'";
-  };
+add_cus_dep('aux', 'glstex', 0, 'bib2gls');
+sub bib2gls {
+	return system "bib2gls '$_[0]'";
 }
 
-sub run_bib2gls {
-  if ( $silent ) {
-    my $ret = system "bib2gls --silent --group '$_[0]'";
-  } else {
-    my $ret = system "bib2gls --group '$_[0]'";
-  };
-  my ($base, $path) = fileparse( $_[0] );
-  if ($path && -e "$base.glstex") {
-    rename "$base.glstex", "$path$base.glstex";
-  }
-  # Analyze log file.
-  local *LOG;
-  $LOG = "$_[0].glg";
-  if (!$ret && -e $LOG) {
-    open LOG, "<$LOG";
-    while (<LOG>) {
-      if (/^Reading (.*\.bib)\s$/) {
-        rdb_ensure_file( $rule, $1 );
-      }
-    }
-    close LOG;
-  }
-  return $ret;
+add_cus_dep('puml', 'eps', 0, 'plantuml');
+sub plantuml {
+  return system "plantuml -teps '$_[0].puml'";
 }
 
-push @file_not_found, '^Package .* No file `([^\\\']*)\\\'';
-push @generated_exts, 'glo', 'gls', 'glg';
-push @generated_exts, 'acn', 'acr', 'alg';
-$clean_ext .= ' %R.ist %R.xdy';
-$clean_ext .= ' bbl run.xml';
+add_cus_dep('txt', 'tex', 0, 'time2tex');
+sub time2tex {
+	return system "./time2tex.py '$_[0].txt' > '$_[0].tex'";
+}
 
diff --git a/makefile b/makefile
deleted file mode 100644
index 15eae95..0000000
--- a/makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-all: plan.pdf
-all: research.pdf
-
-LATEXMKFLAGS += -cd
-LATEXMKFLAGS += -interaction=nonstopmode
-%.pdf: %.tex
-	-latexmk $(LATEXMKFLAGS) $<
-
-%.puml.pdf: %.puml
-	plantuml -tpdf $<
-	mv $*.pdf $@
-
-%.tex: %.txt
-	./time2tex.py $< > $@
-
-timerep.pdf: time.tex
-
diff --git a/projdoc.cls b/projdoc.cls
index c0257ca..0d27a1f 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -58,6 +58,7 @@
 \RequirePackage{subcaption}
 \RequirePackage{multicol}
 \RequirePackage{comparison} % ./comparison.sty
+\RequirePackage{xstring}
 
 % font style
 \setmainfont{TeX Gyre Schola}
@@ -297,7 +298,8 @@
 
 % adjust scale for puml diagrams
 \newcommand{\includepumldiag}[1]{%
-	\fitimg{\includegraphics[scale=0.6]{#1}}%
+	\StrSubstitute{#1}{.puml}{.eps}[\filename]%
+	\fitimg{\includegraphics[scale=0.6]{\filename}}%
 }
 
 % prevent page break between two paragraphs
-- 
cgit v1.2.3


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

---
 example.tex         |  13 +++-
 latexmkrc           |   5 +-
 projdoc.cls         |   8 ++
 reqs2tex.py         |  27 -------
 scripts/.gitignore  |   1 +
 scripts/reqs2tex.py |  83 ++++++++++++++++++++
 scripts/tex.py      |  43 +++++++++++
 scripts/time2tex.py | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 time2tex.py         | 204 -------------------------------------------------
 9 files changed, 362 insertions(+), 235 deletions(-)
 delete mode 100755 reqs2tex.py
 create mode 100644 scripts/.gitignore
 create mode 100755 scripts/reqs2tex.py
 create mode 100644 scripts/tex.py
 create mode 100755 scripts/time2tex.py
 delete mode 100755 time2tex.py

(limited to 'example.tex')

diff --git a/example.tex b/example.tex
index 4ae4d95..ee1377a 100644
--- a/example.tex
+++ b/example.tex
@@ -3,6 +3,8 @@
 % with the [draft] option. this replaces all images with placeholders.
 \input{meta.tex}
 
+\input{reqs.aux}
+
 \title{Example Document}
 
 \begin{document}
@@ -155,17 +157,24 @@ Description:
 	\con{Bad thing 2}
 \end{comparison}
 
-\subsection{Citations}
+\subsection{References}
+
+\subsubsection{Citations}
 
 Citations are inserted using the \codeinline{\autocite} command \autocite{rfc:3339}.
 The bibliography is automatically printed after \codeinline{\end{document}}.
 
-\subsection{Glossary}
+\subsubsection{Glossary}
 
 Glossary entries can be inserted using the \codeinline{\gls} commands. Example:
 ``\Gls{sdl2} handles \glspl{hid} as well!''. In following occurrences of acronyms,
 only their short form is printed: `\gls{sdl2}' and `\gls{hid}'. All of these link to
 the glossary that is automatically printed after \codeinline{\end{document}}.
 
+\subsubsection{Requirements}
+
+Requirements are referenced like \codeinline{\label}s:
+e.g.~\cref{req:audio:handle:id,req:audio:async-api:id}.
+
 \end{document}
 
diff --git a/latexmkrc b/latexmkrc
index 2ce20fa..5a49064 100644
--- a/latexmkrc
+++ b/latexmkrc
@@ -26,11 +26,12 @@ sub plantuml {
 
 add_cus_dep('txt', 'tex', 0, 'time2tex');
 sub time2tex {
-	return system "python3 time2tex.py '$_[0].txt'";
+	return system "python3 scripts/time2tex.py '$_[0].txt'";
 }
 
 add_cus_dep('toml', 'tex', 0, 'reqs2tex');
+add_cus_dep('toml', 'aux', 0, 'reqs2tex');
 sub reqs2tex {
-	return system "python3 reqs2tex.py '$_[0].toml'";
+	return system "python3 scripts/reqs2tex.py '$_[0].toml'";
 }
 
diff --git a/projdoc.cls b/projdoc.cls
index 0d27a1f..c11fe61 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -307,3 +307,11 @@
 \newcommand\noparbreak{\par\nobreak\@afterheading}
 \makeatother
 
+% cleveref extra types
+\crefname{paragraph}{paragraph}{paragraphs}
+\Crefname{paragraph}{Paragraph}{Paragraphs}
+\crefname{requirement}{requirement}{requirements}
+\Crefname{requirement}{Requirement}{Requirements}
+\crefname{test}{test}{tests}
+\Crefname{test}{Test}{Tests}
+
diff --git a/reqs2tex.py b/reqs2tex.py
deleted file mode 100755
index 68c8f40..0000000
--- a/reqs2tex.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/python3
-import sys, tomllib
-
-def fmt(data):
-  print(data)
-  return f"""
-\\makeatletter
-\\makeatother
-"""
-
-def main(input_file):
-  data = {}
-  with open(input_file, "rb") as file:
-    data = tomllib.load(file)
-
-  output = fmt(data)
-
-  output_file = input_file.removesuffix(".toml") + ".tex"
-  with open(output_file, "w+") as file:
-    file.write(output)
-
-if __name__ == "__main__":
-  if len(sys.argv) != 2:
-    print("usage: reqs2tex.py reqs.toml")
-    exit(1)
-  main(sys.argv[1])
-
diff --git a/scripts/.gitignore b/scripts/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/scripts/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
new file mode 100755
index 0000000..9e71a48
--- /dev/null
+++ b/scripts/reqs2tex.py
@@ -0,0 +1,83 @@
+#!/bin/python3
+import sys, tomllib, tex
+
+def flatten(data):
+  if 'description' in data:
+    return [ data ]
+  out = []
+  for key, value in data.items():
+    items = flatten(value)
+    for item in items:
+      if 'label' in item:
+        item['label'] = f"{key}:{item['label']}"
+      else:
+        item['label'] = f"{key}"
+    out += items
+  return out
+
+id_counter = 0
+def make_id(item):
+  global id_counter
+  id_counter += 1
+  return "{type_short}#{counter:03d}".format(
+    type_short = item['type'][0].upper(),
+    counter = id_counter,
+  )
+
+def convert(data):
+  reqs = flatten(data)
+  for index, item in enumerate(reqs):
+    item['id'] = tex.esc(make_id(item))
+    item['index'] = index
+    item['description'] = item.get('description', '???')
+    item['done'] = item.get('done', None)
+    item['priority'] = item.get('priority', 'must')
+    item['type'] = item.get('type', 'system')
+    item['deleted'] = item.get('deleted', False)
+
+  # skip deleted requirements (but process for make_id)
+  reqs = [item for item in reqs if item['deleted'] == False]
+
+  return reqs
+
+def req2aux(req):
+  # TODO: this is a dead-end solution, newlabel only works for in-document anchors, not external links
+  out = [
+    tex.scmd('newlabel', f"req:{req['label']}:id", tex.group(req['id'], req['id'], '', './requirements.pdf', '')),
+    tex.scmd('newlabel', f"req:{req['label']}:id@cref", tex.group(f"[requirement][][]{req['id']}", '')),
+  ]
+  return "\n".join([tex.auxout(line) for line in out])
+
+def fmt_aux(data):
+  out = ""
+  out += tex.cmd('makeatletter')
+  out += "\n".join([req2aux(req) for req in data])
+  out += tex.cmd('makeatother')
+  return out
+
+def fmt_tex(data):
+  return "\n".join([
+    tex.cmd('relax')
+  ])
+
+def main(input_file):
+  data = {}
+  with open(input_file, "rb") as file:
+    data = tomllib.load(file)
+
+  requirements = convert(data)
+
+  output_aux = input_file.removesuffix(".toml") + ".aux"
+  with open(output_aux, "w+") as file:
+    file.write(fmt_aux(requirements))
+
+  output_tex = input_file.removesuffix(".toml") + ".tex"
+  with open(output_tex, "w+") as file:
+    file.write(fmt_tex(requirements))
+
+if __name__ == "__main__":
+  if len(sys.argv) != 2:
+    print("usage: reqs2tex.py reqs.toml")
+    exit(1)
+  main(sys.argv[1])
+
diff --git a/scripts/tex.py b/scripts/tex.py
new file mode 100644
index 0000000..b044857
--- /dev/null
+++ b/scripts/tex.py
@@ -0,0 +1,43 @@
+# utility function for converting latex code
+
+def group(*args):
+  return "".join("{" + arg + "}" for arg in args)
+
+def string(content):
+  return r"\string" + content
+
+def cmd(*args):
+  name = args[0]
+  args = args[1:]
+  if len(args) == 0: args = [""]
+  return f"\\{name}" + group(*args)
+
+def csdef(*args):
+  return r"\def" + cmd(*args)
+
+def auxout(content):
+  return r"\write\@auxout" + group(content)
+
+def scmd(*args):
+  return string(cmd(*args))
+
+def env(name, *args):
+  content = args[-1]
+  args = args[0:-1]
+  out = f"\\begin{{{name}}}"
+  if len(args) > 0:
+    out += group(*args)
+  out += content
+  out += f"\\end{{{name}}}"
+  return out
+
+def esc(plain):
+  plain = plain.replace("\\", "\\string\\")
+  plain = plain.replace("#", "\\#")
+  plain = plain.replace("$", "\\$")
+  plain = plain.replace("%", "\\%")
+  return plain
+
+def tabrule(*cells):
+  return "&".join(cells) + "\\\\"
+
diff --git a/scripts/time2tex.py b/scripts/time2tex.py
new file mode 100755
index 0000000..17661d5
--- /dev/null
+++ b/scripts/time2tex.py
@@ -0,0 +1,213 @@
+#!/bin/python3
+import sys, tex
+from datetime import datetime, timedelta
+
+def fmt_duration(sec):
+  mins = (sec + 59) // 60 # integer divide, round up
+  out = []
+
+  if mins == 0:
+    return "--"
+
+  hour = mins // 60
+  if hour > 0:
+    out.append("%02dh" % (hour, ))
+    mins = mins % 60
+
+  out.append("%02dm" % (mins, ))
+
+  return "\\,".join(out)
+
+def fmt_percentage(fac):
+  return tex.sgroup(
+    tex.cmd('footnotesize') +\
+    tex.cmd('itshape') +\
+    tex.esc(f"({round(fac * 100)}%)")
+  )
+
+def fmt_member_overview(times):
+  # calculations
+  tracked = {}
+  total_time = 0
+  for time in times:
+    if not time["name"] in tracked:
+      tracked[time["name"]] = 0
+    tracked[time["name"]] += time["duration"]
+    total_time += time["duration"]
+
+  out = ""
+
+  # header
+  out += tex.cmd('toprule')
+  out += tex.tabrule(tex.cmd('textbf', 'Member'), tex.cmd('textbf', 'Tracked'), '')
+  out += tex.cmd('midrule')
+
+  # member overview
+  members = sorted(list(set(time["name"] for time in times)))
+  for name in members:
+    out += tex.tabrule(name, fmt_duration(tracked[name]), fmt_percentage(tracked[name] / total_time))
+  out += tex.cmd('midrule')
+
+  # sum
+  out += tex.tabrule('', fmt_duration(total_time), '')
+  out += tex.cmd('bottomrule')
+
+  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
+
+def fmt_weekly_overview(times):
+  # calculations
+  out = ""
+  weeks = []
+  member_totals = {}
+  total_time = sum(time["duration"] 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
+  week_end = time_end + timedelta(days=7-time_end.weekday())
+
+  week = week_start
+  week_num = 1
+  while week < week_end:
+    week_times = [time for time in times if time["date"] >= week and time["date"] < (week + timedelta(days=7))]
+
+    week_entry = {
+      "num": week_num,
+      "members": {},
+      "total": sum(time["duration"] for time in week_times)
+    }
+
+    for member in members:
+      week_entry["members"][member] = sum(time["duration"] for time in week_times if time["name"] == member)
+
+    weeks.append(week_entry)
+    week_num += 1
+    week += timedelta(days=7)
+  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:
+    out += f"&\\textbf{{{member}}}&"
+  out += r"&\textbf{Subtotal}\\\midrule{}"
+
+  for entry in weeks:
+    out += f"{entry['num']}"
+    for member in members:
+      out += f"&{fmt_duration(entry['members'][member])}&{fmt_percentage(entry['members'][member] / entry['total'])}"
+    out += f"&{fmt_duration(entry['total'])}\\\\"
+
+  out += r"\midrule{}"
+  for member in members:
+    out += f"&{fmt_duration(member_totals[member])}&{fmt_percentage(member_totals[member] / total_time)}"
+  out += f"&{fmt_duration(total_time)}\\\\"
+
+  # 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}"
+
+  return out
+
+def duration2secs(duration):
+  out = 0 # output (seconds)
+  cur = 0 # current figure (unknown)
+  for c in duration:
+    if c.isdigit():
+      cur = cur * 10 + int(c)
+      continue
+    if c == "h":
+      out += cur * 3600
+      cur = 0
+      continue
+    if c == "m":
+      out += cur * 60
+      cur = 0
+      continue
+    if c == "s":
+      out += cur * 1
+      cur = 0
+      continue
+
+    raise Exception("invalid duration format")
+  if cur != 0: raise Exception("invalid duration format")
+  return out
+
+def line2data(line):
+  # parse fields from input string
+  data = {}
+  next = line.find(':')
+  data["name"] = line[0:next].strip()
+  line = line[next+1:].strip()
+  next = line.find(' ')
+  data["date"] = line[0:next].strip()
+  line = line[next+1:].strip()
+  next = line.find(' ')
+  data["duration"] = line[0:next].strip()
+  line = line[next+1:].strip()
+  data["description"] = line
+
+  # deserialize parsed fields
+  data["name"] = data["name"].title()
+  data["date"] = datetime.strptime(data["date"], '%Y-%m-%d')
+  data["duration"] = duration2secs(data["duration"])
+  data["description"] = [el.strip() for el in data["description"].split("::")]
+
+  return data
+
+def parse(content):
+  # split content at newlines
+  lines = content.split("\n")
+  out = []
+  for i, line in enumerate(lines):
+    line = line.strip()
+    if line.startswith("#"): continue
+    if len(line) == 0: continue
+
+    try: out.append(line2data(line))
+    except Exception as e: raise Exception(f"line {i+1}: {e}")
+  return out
+
+def fmt(times):
+  return "\n\n".join([
+    tex.cmd('section', 'Overviews'),
+    tex.cmd('subsection', 'Members'),
+    fmt_member_overview(times),
+    tex.cmd('subsection', 'Weekly'),
+    fmt_weekly_overview(times),
+  ])
+
+def main(input_file):
+  content = ""
+  with open(input_file, "r") as file:
+    content = file.read()
+
+  try: parsed = parse(content)
+  except Exception as e:
+    print(f"{input_file}: {e}")
+    exit(1)
+  output = 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.py time.txt")
+    exit(1)
+  main(sys.argv[1])
+
diff --git a/time2tex.py b/time2tex.py
deleted file mode 100755
index 6e3de9c..0000000
--- a/time2tex.py
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/bin/python3
-import sys
-from datetime import datetime, timedelta
-
-def fmt_duration(sec):
-  mins = (sec + 59) // 60 # integer divide, round up
-  out = []
-
-  if mins == 0:
-    return "--"
-
-  hour = mins // 60
-  if hour > 0:
-    out.append("%02dh" % (hour, ))
-    mins = mins % 60
-
-  out.append("%02dm" % (mins, ))
-
-  return "\\,".join(out)
-
-def fmt_percentage(fac):
-  return f"{{\\footnotesize\\itshape({round(fac * 100)}\\%)}}"
-
-def fmt_member_overview(times):
-  # calculations
-  out = ""
-  tracked = {}
-  total_time = 0
-  for time in times:
-    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{}"
-
-  # member overview
-  members = sorted(list(set(time["name"] for time in times)))
-  for name in members:
-    out += f"{name} & {fmt_duration(tracked[name])} & {fmt_percentage(tracked[name] / total_time)}\\\\"
-  out += r"\midrule{}"
-
-  # sum
-  out += f"&{fmt_duration(total_time)}&\\\\"
-
-  # end table
-  out += r"\bottomrule\end{tabular}"
-  out += r"\caption{Tracked time per group member}\label{tab:time-member}"
-  out += r"\end{table}"
-
-  return out
-
-def fmt_weekly_overview(times):
-  # calculations
-  out = ""
-  weeks = []
-  member_totals = {}
-  total_time = sum(time["duration"] 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
-  week_end = time_end + timedelta(days=7-time_end.weekday())
-
-  week = week_start
-  week_num = 1
-  while week < week_end:
-    week_times = [time for time in times if time["date"] >= week and time["date"] < (week + timedelta(days=7))]
-
-    week_entry = {
-      "num": week_num,
-      "members": {},
-      "total": sum(time["duration"] for time in week_times)
-    }
-
-    for member in members:
-      week_entry["members"][member] = sum(time["duration"] for time in week_times if time["name"] == member)
-
-    weeks.append(week_entry)
-    week_num += 1
-    week += timedelta(days=7)
-  for member in members:
-    member_totals[member] = sum(time["duration"] for time in times if time["name"] == member)
-
-  # 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:
-    out += f"&\\textbf{{{member}}}&"
-  out += r"&\textbf{Subtotal}\\\midrule{}"
-
-  for entry in weeks:
-    out += f"{entry['num']}"
-    for member in members:
-      out += f"&{fmt_duration(entry['members'][member])}&{fmt_percentage(entry['members'][member] / entry['total'])}"
-    out += f"&{fmt_duration(entry['total'])}\\\\"
-
-  out += r"\midrule{}"
-  for member in members:
-    out += f"&{fmt_duration(member_totals[member])}&{fmt_percentage(member_totals[member] / total_time)}"
-  out += f"&{fmt_duration(total_time)}\\\\"
-
-  # 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}"
-
-  return out
-
-def duration2secs(duration):
-  out = 0 # output (seconds)
-  cur = 0 # current figure (unknown)
-  for c in duration:
-    if c.isdigit():
-      cur = cur * 10 + int(c)
-      continue
-    if c == "h":
-      out += cur * 3600
-      cur = 0
-      continue
-    if c == "m":
-      out += cur * 60
-      cur = 0
-      continue
-    if c == "s":
-      out += cur * 1
-      cur = 0
-      continue
-
-    raise Exception("invalid duration format")
-  if cur != 0: raise Exception("invalid duration format")
-  return out
-
-def line2data(line):
-  # parse fields from input string
-  data = {}
-  next = line.find(':')
-  data["name"] = line[0:next].strip()
-  line = line[next+1:].strip()
-  next = line.find(' ')
-  data["date"] = line[0:next].strip()
-  line = line[next+1:].strip()
-  next = line.find(' ')
-  data["duration"] = line[0:next].strip()
-  line = line[next+1:].strip()
-  data["description"] = line
-
-  # deserialize parsed fields
-  data["name"] = data["name"].title()
-  data["date"] = datetime.strptime(data["date"], '%Y-%m-%d')
-  data["duration"] = duration2secs(data["duration"])
-  data["description"] = [el.strip() for el in data["description"].split("::")]
-
-  return data
-
-def parse(content):
-  # split content at newlines
-  lines = content.split("\n")
-  out = []
-  for i, line in enumerate(lines):
-    line = line.strip()
-    if line.startswith("#"): continue
-    if len(line) == 0: continue
-
-    try: out.append(line2data(line))
-    except Exception as e: raise Exception(f"line {i+1}: {e}")
-  return out
-
-def fmt(times):
-  return f"""
-\\section{{Overviews}}\n
-\\subsection{{Members}}\n
-{fmt_member_overview(times)}
-\\subsection{{Weekly}}\n
-{fmt_weekly_overview(times)}
-"""
-
-def main(input_file):
-  content = ""
-  with open(input_file, "r") as file:
-    content = file.read()
-
-  try: parsed = parse(content)
-  except Exception as e:
-    print(f"{input_file}: {e}")
-    exit(1)
-  output = 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.py time.txt")
-    exit(1)
-  main(sys.argv[1])
-
-- 
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 'example.tex')

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 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 'example.tex')

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 6329755a372d90985bda1cd164fae8d91d4a60c6 Mon Sep 17 00:00:00 2001
From: Loek Le Blansch <loek@pipeframe.xyz>
Date: Thu, 26 Sep 2024 15:50:58 +0200
Subject: remove meta.tex and just set defaults directly in projdoc.cls

---
 example.tex      |  1 -
 meta.tex         |  9 ---------
 plan.tex         |  1 -
 projdoc.cls      | 15 +++++++++++++--
 requirements.tex |  1 -
 research.tex     |  1 -
 timerep.tex      |  1 -
 7 files changed, 13 insertions(+), 16 deletions(-)
 delete mode 100644 meta.tex

(limited to 'example.tex')

diff --git a/example.tex b/example.tex
index 24c525b..9f36f59 100644
--- a/example.tex
+++ b/example.tex
@@ -1,7 +1,6 @@
 \documentclass{projdoc}
 % if the document compiles too slow (likely due to many/large images), try compiling
 % with the [draft] option. this replaces all images with placeholders.
-\input{meta.tex}
 
 \title{Example Document}
 
diff --git a/meta.tex b/meta.tex
deleted file mode 100644
index db7c7b1..0000000
--- a/meta.tex
+++ /dev/null
@@ -1,9 +0,0 @@
-\organization{Avans University of Applied Sciences}
-\project{Project cr\^epe}
-\author{%
-	Loek Le Blansch\and%
-	Wouter Boerenkamps\and%
-	Jaro Rutjes\and%
-	Max Smits\and%
-	Niels Stunnebrink%
-}
diff --git a/plan.tex b/plan.tex
index c439aa7..a67598e 100644
--- a/plan.tex
+++ b/plan.tex
@@ -1,5 +1,4 @@
 \documentclass{projdoc}
-\input{meta.tex}
 
 \title{Project Plan}
 
diff --git a/projdoc.cls b/projdoc.cls
index 16b1790..8a2c05d 100644
--- a/projdoc.cls
+++ b/projdoc.cls
@@ -1,6 +1,19 @@
 \NeedsTeXFormat{LaTeX2e}
 \ProvidesClass{projdoc}[2024-09-03 class projdoc]
 
+% project defaults
+\makeatletter
+\def\@project{Project cr\^epe}
+\def\@organization{Avans University of Applied Sciences}
+\def\@author{%
+	Loek Le Blansch\and%
+	Wouter Boerenkamps\and%
+	Jaro Rutjes\and%
+	Max Smits\and%
+	Niels Stunnebrink%
+}
+\makeatother
+
 % based on article
 \LoadClass{article}
 
@@ -160,9 +173,7 @@
 
 % \maketitle format
 \makeatletter
-\let\@project\relax
 \def\project#1{\def\@project{#1}}
-\let\@organization\relax
 \def\organization#1{\def\@organization{#1}}
 \def\@maketitle{%
 	\centering%
diff --git a/requirements.tex b/requirements.tex
index be0e103..6b4a87a 100644
--- a/requirements.tex
+++ b/requirements.tex
@@ -1,5 +1,4 @@
 \documentclass{projdoc}
-\input{meta.tex}
 
 \makeatletter
 \projdoc@description@leftmargin=2ex
diff --git a/research.tex b/research.tex
index 228f3ac..5aa7c4f 100644
--- a/research.tex
+++ b/research.tex
@@ -1,5 +1,4 @@
 \documentclass{projdoc}
-\input{meta.tex}
 
 \title{Research document}
 
diff --git a/timerep.tex b/timerep.tex
index 7590217..34a30ea 100644
--- a/timerep.tex
+++ b/timerep.tex
@@ -1,5 +1,4 @@
 \documentclass{projdoc}
-\input{meta.tex}
 
 \title{Time Report}
 
-- 
cgit v1.2.3

