From 0fd0eaab3b8bfc154b83a3507c3caa9d9ab556c6 Mon Sep 17 00:00:00 2001
From: Jonas Smedegaard <dr@jones.dk>
Date: Mon, 1 Apr 2024 03:22:12 +0200
Subject: use PlantUML for Gantt diagrams

---
 _report.yml                      |   2 +
 _themes/doubleslash/README.md    |   3 +
 _themes/doubleslash/general.puml |  73 ++++++++++++++
 _themes/doubleslash/pgantt.puml  |  70 ++++++++++++++
 bin/hedgedoc2quarto              | 200 ++++++++++++++++++++++++++++++++++++++-
 5 files changed, 345 insertions(+), 3 deletions(-)
 create mode 100644 _themes/doubleslash/README.md
 create mode 100644 _themes/doubleslash/general.puml
 create mode 100644 _themes/doubleslash/pgantt.puml

diff --git a/_report.yml b/_report.yml
index 0117bfe..44be2ba 100644
--- a/_report.yml
+++ b/_report.yml
@@ -10,6 +10,8 @@ format:
 #    links-as-notes: true
     colorlinks: false
     pdf-engine: lualatex
+# permit preprocessing PlantUML SVG files with inkscape
+    pdf-engine-opt: --shell-escape
     documentclass: scrreprt
     classoption:
     - DIV=calc
diff --git a/_themes/doubleslash/README.md b/_themes/doubleslash/README.md
new file mode 100644
index 0000000..80d3431
--- /dev/null
+++ b/_themes/doubleslash/README.md
@@ -0,0 +1,3 @@
+# PlantUML Theme
+
+Origin: <https://github.com/doubleSlashde/umltheme>
diff --git a/_themes/doubleslash/general.puml b/_themes/doubleslash/general.puml
new file mode 100644
index 0000000..b8941ce
--- /dev/null
+++ b/_themes/doubleslash/general.puml
@@ -0,0 +1,73 @@
+''Define colors in doubleSlash corporate identity
+!define DS_BLUE           #00A5E1
+!define DS_LIGHTBLUE      #D7E9F4
+!define DS_ORANGE         #FF9F00
+!define DS_LIGHTGREY      #C6C6C6
+!define DS_GREY           #7A7A7A
+!define DS_DARKGREY       #515151
+
+!define SUPERLIGHTGREY    #F8F8F8
+!define ALICEBLUE         #F0F8FF
+!define STEELBLUE25       #374656
+!define STEELBLUE40       #8A9DB3
+!define BLACK50           #7F7F7F
+!define BLACK25           #404040
+!define LIGHTGREEN        #D5E8D4
+
+!define TIPBGCOLOR       #FFF6E7
+!define TIPBORDERCOLOR   #FFD999
+
+!define FONTNAME         "Arial"
+
+skinparam Shadowing false
+skinparam Padding 2
+skinparam Roundcorner 16
+skinparam BackgroundColor white
+skinparam DefaultFontName FONTNAME
+skinparam DefaultFontColor black
+skinparam DefaultTextAlignment center
+
+' Title
+'=================================================================
+skinparam TitleFontSize 28
+skinparam TitleFontName FONTNAME
+skinparam TitleFontColor black
+skinparam TitleFontStyle normal
+
+' Note 
+'=================================================================
+'skinparam NoteBackgroundColor TIPBGCOLOR
+'skinparam NoteBorderColor TIPBORDERCOLOR
+'skinparam NoteBorderThickness 0.3
+'skinparam NoteShadowing true
+skinparam NoteBackgroundColor transparent
+skinparam NoteBorderColor transparent
+skinparam NoteBorderThickness 0.0
+skinparam NoteFontColor black
+skinparam NoteFontSize 11
+skinparam NoteTextAlignment left
+
+' Rectangle
+'=================================================================
+skinparam RectangleBackgroundColor SUPERLIGHTGREY
+skinparam RectangleBorderColor DS_DARKGREY
+skinparam RectangleBorderColor DS_GREY
+skinparam RectangleBorderThickness 0.5
+skinparam RectangleFontColor black
+skinparam RectangleFontSize 14
+
+
+' Package
+'=================================================================
+skinparam PackageFontColor black
+skinparam PackageBackgroundColor SUPERLIGHTGREY
+skinparam PackageBorderColor DS_DARKGREY
+skinparam PackageFontSize 14
+
+' Arrow
+'=================================================================
+skinparam ArrowColor DS_GREY
+skinparam ArrowThickness 1.5
+skinparam ArrowFontName FONTNAME
+skinparam ArrowFontColor black
+skinparam ArrowFontStyle normal
diff --git a/_themes/doubleslash/pgantt.puml b/_themes/doubleslash/pgantt.puml
new file mode 100644
index 0000000..15a3e1d
--- /dev/null
+++ b/_themes/doubleslash/pgantt.puml
@@ -0,0 +1,70 @@
+!include general.puml
+
+'saturday are closed
+'sunday are closed
+'language de
+
+skinparam Roundcorner 6
+
+saturday are colored in SUPERLIGHTGREY
+sunday are colored in SUPERLIGHTGREY
+printscale weekly zoom 4
+
+<style>
+ganttDiagram {
+  task {
+    FontName FONTNAME
+    FontSize 12
+    FontColor black
+    FontStyle normal
+    BackGroundColor DS_LIGHTBLUE
+    LineColor DS_LIGHTBLUE
+    Padding 3
+   }
+
+  timeline {
+    FontName FONTNAME
+	 FontSize 12
+    BackgroundColor SUPERLIGHTGREY
+  }
+
+  milestone {
+    FontColor black
+    FontSize 12
+    FontStyle normal
+    BackGroundColor DS_BLUE
+    LineColor DS_BLUE
+    LineThickness 15.0
+    Margin 5
+  }
+
+  note {
+    FontColor black
+    FontSize 10
+    LineColor DS_LIGHTGREY
+  }
+	
+  closed {
+    BackgroundColor #ffdfd4
+    FontColor black
+  }
+
+  Arrow {
+    LineColor DS_GREY
+    LineStyle solid
+    LineThickness 1.5
+    FontName FONTNAME
+    FontColor black
+    FontSize 12
+  }
+
+  Separator {
+    LineColor DS_GREY
+    LineThickness 1.0
+    FontSize 12
+    FontColor black
+    Margin 5
+    Padding 5
+  }
+}
+</style>
diff --git a/bin/hedgedoc2quarto b/bin/hedgedoc2quarto
index c0bde3b..c81ddd5 100755
--- a/bin/hedgedoc2quarto
+++ b/bin/hedgedoc2quarto
@@ -26,7 +26,8 @@ and adapts embedded diagram code.
 
 Both HedgeDoc and Quarto uses Markdown,
 but different flavors,
-and they handle different subsets of Mermaid diagrams.
+and whereas both handle (different subsets of) Mermaid diagrams,
+Quarto also (through plugins) handles PlantUML diagrams.
 
 =cut
 
@@ -43,8 +44,12 @@ $content =~ s/^
 		(?'code'.*?\n)
 		\k'fence'
 	$/
-	"{mermaid}\n\%\%| fig-width: 100\%\n"
-	. &mmd2mmd( $+{type}, $+{code} )
+
+# FIXME: implement option to choose output diagram language
+#	"{mermaid}\n\%\%| fig-width: 100\%\n"
+#	. &mmd2mmd( $+{type}, $+{code} )
+	"{.plantuml}\n\%\%| fig-width: 100\%\n"
+	. &mmd2puml( $+{type}, $+{code} )
 	. $+{fence}
 	/gsmex;
 
@@ -65,6 +70,195 @@ sub mmd2mmd ( $type, $code )
 	return "$type\n$code";
 }
 
+sub mmd2puml ( $type, $code )
+{
+	my @newcode;
+
+	# strip special comment marker '%%QUARTO%%'
+	$code =~ s/^\s*+\K%%QUARTO%%//gm;
+
+	open my $fh, '<', \$code or die $!;
+
+	while (<$fh>) {
+
+		/^\s*+$/
+			and push @newcode, ''
+			and next;
+
+		/^(\s*+)%%PLANTUML%%\K.*/
+			and push @newcode, "$1$&"
+			and next;
+
+		# convert comments markers
+		/^(\s*+)(?:[%]{2,}(?'comment'\s*+))?+\K.*/;
+		my $indent = defined( $+{comment} ) ? "$1'$2" : $1;
+		$_ = $&;
+
+		/^title\s/i
+			and push @newcode, "${indent}$_"
+			and next;
+
+		/^excludes\s+weekends\b/i
+			and push @newcode, "${indent}saturday are closed"
+			and push @newcode, "${indent}sunday are closed"
+			and next;
+		/^weekday\s+\K(?:mon|tues|wednes|thurs|fri|satur|sun)day\b/i
+			and push @newcode, "${indent}weeks start on $&"
+			and next;
+		/^(?:date|axis)Format\s/i
+			and push @newcode, "${indent}'UNSUPPORTED: $_"
+			and next;
+		/^todayMarker\s+(off|on)\b/i
+			and push @newcode, "${indent}'UNSUPPORTED' $_"
+			and next;
+		/^section\s+\K\S+(?:\s+\S+)*/i
+			and push @newcode, "${indent}-- $& --"
+			and next;
+
+		if (/^tickInterval\s+(?'tickAmount'\d+)(?'tickUnit'millisecond|second|minute|hour|day|week|month)\s*$/i
+			)
+		{
+			push @newcode, "${indent}projectscale daily"
+				and next
+				if $+{tickAmount} eq 1
+				and $+{tickUnit} eq 'day';
+			push @newcode, "${indent}projectscale weekly" and next
+				if $+{tickAmount} eq 1 and $+{tickUnit} eq 'week'
+				or $+{tickAmount} eq 7 and $+{tickUnit} eq 'day';
+			push @newcode, "${indent}projectscale monthly"
+				and next
+				if $+{tickAmount} eq 1
+				and $+{tickUnit} eq 'month';
+			push @newcode, "${indent}projectscale quarterly"
+				and next
+				if $+{tickAmount} eq 3
+				and $+{tickUnit} eq 'month';
+			push @newcode, "${indent}projectscale yearly"
+				and next
+				if $+{tickAmount} eq 12
+				and $+{tickUnit} eq 'month';
+			push @newcode, "${indent}'UNSUPPORTED' $&"
+				and next;
+		}
+
+		/^
+			(?'title'[^:\n]+)
+			\s*+:\s*+
+
+			# optional tags
+			(?:
+				(?:
+					(?'active'active)
+				|
+					(?'done'done)
+				|
+					(?'crit'crit)
+				|
+					(?'milestone'milestone)
+				)\s*+
+				,\s*+
+			)?+
+
+			(?:
+				# optional tertiary item
+				(?:
+					(?'taskID'(?&id))\s*+
+					,\s*+
+					(?=.*,) # several items must follow
+				)?+
+
+				# optional secondary item
+				(?:
+					(?'startDate'(?&date))
+				|
+					after
+					(?'afterTaskIDs'
+						(?:\s+(?&id))++
+					)
+				)\s*+
+				,\s*+
+			)?+
+
+			# required main item
+			(?:
+				(?'endDate'(?&date))
+			|
+				until
+				(?'untilTaskIDs'
+					(?:\s+(?&id))++
+				)
+			|
+				(?'duration'\d+)
+				\s*+d
+			)\s*+
+		(?(DEFINE)
+			(?'id'[^\s\d,][^\s,]*+) # assume digit as lead caracter is illegal
+			(?'date'\d\d\d\d(?:-\d\d(?:-\d\d)?+)?+)
+		)
+		$/x
+			or defined( $+{comment} )
+			and push @newcode, "${indent}$_"
+			and next
+			or die "unhandled syntax on line $.: $_";
+
+		defined( $+{active} )
+			or defined( $+{done} )
+			or defined( $+{crit} )
+			and die "unhandled tag on line $.: $_";
+
+		my $task    = "${indent}\[$+{title}]";
+		my $taskref = $task;
+
+		# optional 3rd item
+		if ( $+{taskID} ) {
+			$task .= " as [$+{taskID}]";
+			$taskref = "${indent}\[$+{taskID}]";
+		}
+
+		if ( defined( $+{afterTaskIDs} ) ) {
+			my @reqs = split ' ', $+{afterTaskIDs};
+
+			if ( $+{milestone} ) {
+				push @newcode, "$task happens at [$_]'s end" for @reqs;
+			}
+			elsif ( $+{endDate} ) {
+				push @newcode, "$task ends $+{endDate}";
+				push( @newcode, "$taskref starts at [$_]'s end" ) for @reqs;
+			}
+			elsif ( defined( $+{untilTaskIDs} ) ) {
+				my @reqsEnd = split ' ', $+{untilTaskIDs};
+				push @newcode, "$task ends at [$_]'s end" for @reqsEnd;
+				push( @newcode, "$taskref starts at [$_]'s end" ) for @reqs;
+			}
+			else {
+				push @newcode, "$task requires $+{duration} days";
+				push( @newcode, "$taskref starts at [$_]'s end" ) for @reqs;
+			}
+		}
+		else {
+			if ( $+{milestone} ) {
+				push @newcode, "$task happens $+{startDate}";
+			}
+			elsif ( $+{endDate} ) {
+				push @newcode,
+					"$task starts $+{startDate} and ends $+{ednDate}";
+			}
+			elsif ( defined( $+{untilTaskIDs} ) ) {
+				my @reqsEnd = split ' ', $+{untilTaskIDs};
+				push @newcode, "$task starts $+{startDate}";
+				push @newcode, "$task ends at [$_]'s end" for @reqsEnd;
+			}
+			else {
+				push @newcode,
+					"$task starts $+{startDate} and requires $+{duration} days";
+			}
+		}
+	}
+
+	$" = "\n";
+	return "\@start$type\n@newcode\n\@end$type\n";
+}
+
 =encoding UTF-8
 
 =head1 AUTHOR
-- 
cgit v1.2.3