aboutsummaryrefslogtreecommitdiff
path: root/bin/hedgedoc2quarto
blob: b2360d0a08ff86e7909461f50a15ca16ab66b520 (plain)
  1. #!/usr/bin/perl
  2. use v5.36;
  3. use utf8;
  4. =head1 NAME
  5. hedgedoc2quarto - convert HedgeDoc content to Quarto
  6. =head1 VERSION
  7. Version 0.0.1
  8. =head1 SYNOPSIS
  9. hedgedoc2quarto INFILE OUTFILE
  10. hedgedoc2quarto < INFILE > OUTFILE
  11. =head1 DESCRIPTION
  12. B<hedgedoc2quarto> reformats text content
  13. from HedgeDoc- to Quarto-flavored Markdown,
  14. and adapts embedded diagram code.
  15. Both HedgeDoc and Quarto uses Markdown,
  16. but different flavors,
  17. and whereas both handle (different subsets of) Mermaid diagrams,
  18. Quarto also (through plugins) handles PlantUML diagrams.
  19. =cut
  20. # slurp INFILE if passed as first argument, or else STDIN
  21. my ( $infile, $outfile, $bogus ) = @ARGV;
  22. die 'Too many arguments: expected INFILE and OUTFILE' if $bogus;
  23. @ARGV = ($infile) if $infile;
  24. my $content = do { local $/ = undef; <> };
  25. # TODO: integrate with subroutine handler below
  26. $content =~ s/^
  27. (?'fence'[``~]{3,})\s*
  28. \Kgraphviz\n
  29. (?'code'.*?\n)
  30. \k'fence'
  31. $/{dot}\n\/\/| fig-width: 100\%\n$+{code}$+{fence}/gsmx;
  32. $content =~ s/^
  33. (?'fence'[``~]{3,})\s*
  34. \Kmermaid\n
  35. (?'type'gantt)\n
  36. (?'code'.*?\n)
  37. \k'fence'
  38. $/
  39. # FIXME: implement option to choose output diagram language
  40. # "{mermaid}\n\%\%| fig-width: 100\%\n"
  41. # . &mmd2mmd( $+{type}, $+{code} )
  42. "{.plantuml}\n\%\%| fig-width: 100\%\n"
  43. . &mmd2puml( $+{type}, $+{code} )
  44. . $+{fence}
  45. /gsmex;
  46. if ($outfile) {
  47. open( FH, '>', $outfile ) or die $!;
  48. print FH $content;
  49. }
  50. else {
  51. print $content;
  52. }
  53. sub mmd2mmd ( $type, $code )
  54. {
  55. # strip special comment marker '%%QUARTO%%'
  56. $code =~ s/^\s*+\K%%QUARTO%%//gm;
  57. return "$type\n$code";
  58. }
  59. sub mmd2puml ( $type, $code )
  60. {
  61. my @newcode;
  62. # strip special comment marker '%%QUARTO%%'
  63. $code =~ s/^\s*+\K%%QUARTO%%//gm;
  64. open my $fh, '<', \$code or die $!;
  65. while (<$fh>) {
  66. /^\s*+$/
  67. and push @newcode, ''
  68. and next;
  69. /^(\s*+)%%PLANTUML%%\K.*/
  70. and push @newcode, "$1$&"
  71. and next;
  72. # convert comments markers
  73. /^(\s*+)(?:[%]{2,}(?'comment'\s*+))?+\K.*/;
  74. my $indent = defined( $+{comment} ) ? "$1'$2" : $1;
  75. $_ = $&;
  76. /^title\s/i
  77. and push @newcode, "${indent}$_"
  78. and next;
  79. /^excludes\s+weekends\b/i
  80. and push @newcode, "${indent}saturday are closed"
  81. and push @newcode, "${indent}sunday are closed"
  82. and next;
  83. /^weekday\s+\K(?:mon|tues|wednes|thurs|fri|satur|sun)day\b/i
  84. and push @newcode, "${indent}weeks start on $&"
  85. and next;
  86. /^(?:date|axis)Format\s/i
  87. and push @newcode, "${indent}'UNSUPPORTED: $_"
  88. and next;
  89. /^todayMarker\s+(off|on)\b/i
  90. and push @newcode, "${indent}'UNSUPPORTED' $_"
  91. and next;
  92. /^section\s+\K\S+(?:\s+\S+)*/i
  93. and push @newcode, "${indent}-- $& --"
  94. and next;
  95. if (/^tickInterval\s+(?'tickAmount'\d+)(?'tickUnit'millisecond|second|minute|hour|day|week|month)\s*$/i
  96. )
  97. {
  98. push @newcode, "${indent}projectscale daily"
  99. and next
  100. if $+{tickAmount} eq 1
  101. and $+{tickUnit} eq 'day';
  102. push @newcode, "${indent}projectscale weekly" and next
  103. if $+{tickAmount} eq 1 and $+{tickUnit} eq 'week'
  104. or $+{tickAmount} eq 7 and $+{tickUnit} eq 'day';
  105. push @newcode, "${indent}projectscale monthly"
  106. and next
  107. if $+{tickAmount} eq 1
  108. and $+{tickUnit} eq 'month';
  109. push @newcode, "${indent}projectscale quarterly"
  110. and next
  111. if $+{tickAmount} eq 3
  112. and $+{tickUnit} eq 'month';
  113. push @newcode, "${indent}projectscale yearly"
  114. and next
  115. if $+{tickAmount} eq 12
  116. and $+{tickUnit} eq 'month';
  117. push @newcode, "${indent}'UNSUPPORTED' $&"
  118. and next;
  119. }
  120. /^
  121. (?'title'[^:\n]+)
  122. \s*+:\s*+
  123. # optional tags
  124. (?:
  125. (?:
  126. (?'active'active)
  127. |
  128. (?'done'done)
  129. |
  130. (?'crit'crit)
  131. |
  132. (?'milestone'milestone)
  133. )\s*+
  134. ,\s*+
  135. )?+
  136. (?:
  137. # optional tertiary item
  138. (?:
  139. (?'taskID'(?&id))\s*+
  140. ,\s*+
  141. (?=.*,) # several items must follow
  142. )?+
  143. # optional secondary item
  144. (?:
  145. (?'startDate'(?&date))
  146. |
  147. after
  148. (?'afterTaskIDs'
  149. (?:\s+(?&id))++
  150. )
  151. )\s*+
  152. ,\s*+
  153. )?+
  154. # required main item
  155. (?:
  156. (?'endDate'(?&date))
  157. |
  158. until
  159. (?'untilTaskIDs'
  160. (?:\s+(?&id))++
  161. )
  162. |
  163. (?'duration'\d+)
  164. \s*+d
  165. )\s*+
  166. (?(DEFINE)
  167. (?'id'[^\s\d,][^\s,]*+) # assume digit as lead caracter is illegal
  168. (?'date'\d\d\d\d(?:-\d\d(?:-\d\d)?+)?+)
  169. )
  170. $/x
  171. or defined( $+{comment} )
  172. and push @newcode, "${indent}$_"
  173. and next
  174. or die "unhandled syntax on line $.: $_";
  175. defined( $+{active} )
  176. or defined( $+{done} )
  177. or defined( $+{crit} )
  178. and die "unhandled tag on line $.: $_";
  179. my $task = "${indent}\[$+{title}]";
  180. my $taskref = $task;
  181. # optional 3rd item
  182. if ( $+{taskID} ) {
  183. $task .= " as [$+{taskID}]";
  184. $taskref = "${indent}\[$+{taskID}]";
  185. }
  186. if ( defined( $+{afterTaskIDs} ) ) {
  187. my @reqs = split ' ', $+{afterTaskIDs};
  188. if ( $+{milestone} ) {
  189. push @newcode, "$task happens at [$_]'s end" for @reqs;
  190. }
  191. elsif ( $+{endDate} ) {
  192. push @newcode, "$task ends $+{endDate}";
  193. push( @newcode, "$taskref starts at [$_]'s end" ) for @reqs;
  194. }
  195. elsif ( defined( $+{untilTaskIDs} ) ) {
  196. my @reqsEnd = split ' ', $+{untilTaskIDs};
  197. push @newcode, "$task ends at [$_]'s end" for @reqsEnd;
  198. push( @newcode, "$taskref starts at [$_]'s end" ) for @reqs;
  199. }
  200. else {
  201. push @newcode, "$task requires $+{duration} days";
  202. push( @newcode, "$taskref starts at [$_]'s end" ) for @reqs;
  203. }
  204. }
  205. else {
  206. if ( $+{milestone} ) {
  207. push @newcode, "$task happens $+{startDate}";
  208. }
  209. elsif ( $+{endDate} ) {
  210. push @newcode,
  211. "$task starts $+{startDate} and ends $+{ednDate}";
  212. }
  213. elsif ( defined( $+{untilTaskIDs} ) ) {
  214. my @reqsEnd = split ' ', $+{untilTaskIDs};
  215. push @newcode, "$task starts $+{startDate}";
  216. push @newcode, "$task ends at [$_]'s end" for @reqsEnd;
  217. }
  218. else {
  219. push @newcode,
  220. "$task starts $+{startDate} and requires $+{duration} days";
  221. }
  222. }
  223. }
  224. $" = "\n";
  225. return "\@start$type\n@newcode\n\@end$type\n";
  226. }
  227. =encoding UTF-8
  228. =head1 AUTHOR
  229. Jonas Smedegaard C<< <dr@jones.dk> >>
  230. =head1 COPYRIGHT AND LICENSE
  231. Copyright © 2024 Jonas Smedegaard
  232. This program is free software:
  233. you can redistribute it and/or modify it
  234. under the terms of the GNU Affero General Public License
  235. as published by the Free Software Foundation,
  236. either version 3, or (at your option) any later version.
  237. This program is distributed in the hope that it will be useful,
  238. but WITHOUT ANY WARRANTY;
  239. without even the implied warranty
  240. of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  241. See the GNU Affero General Public License for more details.
  242. You should have received a copy
  243. of the GNU Affero General Public License along with this program.
  244. If not, see <https://www.gnu.org/licenses/>.
  245. =cut
  246. 1;