#!/usr/bin/perl use v5.36; use utf8; =head1 NAME hedgedoc2quarto - convert HedgeDoc content to Quarto =head1 VERSION Version 0.0.1 =head1 SYNOPSIS hedgedoc2quarto INFILE OUTFILE hedgedoc2quarto < INFILE > OUTFILE =head1 DESCRIPTION B reformats text content from HedgeDoc- to Quarto-flavored Markdown, and adapts embedded diagram code. Both HedgeDoc and Quarto uses Markdown, but different flavors, and whereas both handle (different subsets of) Mermaid diagrams, Quarto also (through plugins) handles PlantUML diagrams. =cut # slurp INFILE if passed as first argument, or else STDIN my ( $infile, $outfile, $bogus ) = @ARGV; die 'Too many arguments: expected INFILE and OUTFILE' if $bogus; @ARGV = ($infile) if $infile; my $content = do { local $/ = undef; <> }; # TODO: integrate with subroutine handler below $content =~ s/^ (?'fence'[``~]{3,})\s* \Kgraphviz\n (?'code'.*?\n) \k'fence' $/{dot}\n\/\/| fig-width: 100\%\n$+{code}$+{fence}/gsmx; $content =~ s/^ (?'fence'[``~]{3,})\s* \Kmermaid\n (?'type'gantt)\n (?'code'.*?\n) \k'fence' $/ # 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; if ($outfile) { open( FH, '>', $outfile ) or die $!; print FH $content; } else { print $content; } sub mmd2mmd ( $type, $code ) { # strip special comment marker '%%QUARTO%%' $code =~ s/^\s*+\K%%QUARTO%%//gm; 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 Jonas Smedegaard C<< >> =head1 COPYRIGHT AND LICENSE Copyright © 2024 Jonas Smedegaard This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . =cut 1;