package Pandoc::Filter::ImagesFromCode; use strict; use warnings; use utf8; use Encode; use 5.010; our $VERSION = '0.36'; use Digest::MD5 'md5_hex'; use IPC::Run3; use File::Spec::Functions; use File::stat; use Pandoc::Elements; use Scalar::Util 'reftype'; use parent 'Pandoc::Filter', 'Exporter'; our @EXPORT_OK = qw(read_file write_file); sub new { my ($class, %opts) = @_; $opts{from} //= 'code'; $opts{dir} //= '.'; $opts{dir} =~ s!/$!!; $opts{name} //= sub { $_[0]->id =~ /^[a-z0-9_]+$/i ? $_[0]->id : md5_hex( encode( 'utf8', $_[0]->content ) ); }; die "missing option: to\n" unless $opts{to}; if ('ARRAY' ne reftype $opts{run} or !@{$opts{run}}) { die "missing or empty option: run\n"; } bless \%opts, $class; } sub to { my $to = $_[0]->{to}; my $format = $_[1]; if (ref $to) { return $to->($format); } elsif ($to) { return $to; } else { return 'png'; } } sub action { my $self = shift; sub { my ($e, $format, $m) = @_; return if $e->name ne 'CodeBlock'; my $code = $e->content; my $dir = $self->{dir}; my %args = ( name => $self->{name}->($e), from => $self->{from}, to => $self->to($format), ); $args{infile} = catfile($self->{dir}, "$args{name}.$args{from}"); $args{outfile} = catfile($self->{dir}, "$args{name}.$args{to}"); # TODO: document or remove this experimental code. If keep, expand args my $kv = $e->keyvals; my @options = $kv->get_all('option'); push @options, map { split /\s+/, $_ } $kv->get_all('options'); # TODO: print args in debug mode? # skip transformation if nothing has changed my $in = stat($args{infile}); my $out = stat($args{outfile}); if (!$self->{force} and $in and $out and $in->mtime <= $out->mtime) { if ($code eq read_file($args{infile}, ':utf8')) { # no need to rebuild the same outfile return build_image($e, $args{outfile}); } } write_file($args{infile}, $code, ':utf8'); my ($stderr, $stdout); my @command = map { my $s = $_; #if ($args{substr $s, 1, -1}) $s =~ s|\$([^\$]+)\$| $args{$1} // $1 |eg; $s } @{$self->{run}}; push @command, @options; run3 \@command, \undef, \$stdout, \$stderr, { binmode_stdin => ':utf8', binmode_stdout => ':raw', binmode_stderr => ':raw', }; if ($self->{capture}) { write_file($args{outfile}, $stdout, ':raw'); } # TODO: include $code or $stderr on error in debug mode # TODO: skip error if requested die $stderr if $stderr; return build_image($e, $args{outfile}); } } # build_image( $element [, $filename ] ) # # Maps an element to an L element with attributes # from the given element. The attribute C, if available, is transformed # into image caption. This utility function is useful for filters that transform # content to images. See graphviz, tikz, lilypond and similar filters in the # L. sub build_image { my $e = shift; my $filename = shift // ''; my $keyvals = $e->keyvals; my $title = $keyvals->get('title') // ''; my $img = Image attributes { id => $e->id, class => $e->class }, [], [$filename, $title]; my $caption = $keyvals->get('caption') // ''; if (defined $caption) { push @{$img->content}, Str($caption); } return Plain [ $img ]; } sub write_file { my ($file, $content, $encoding) = @_; open my $fh, ">$encoding", $file or die "failed to create file $file: $!\n"; print $fh $content; close $fh; } sub read_file { my ($file, $encoding) = @_; open my $fh, "<$encoding", $file or die "failed to open file: $file: $!\n"; my $content = do { local $/; <$fh> }; close $fh or die "failed to close file: $file: $!\n"; return $content; } 1; __END__ =head1 NAME Pandoc::Filter::ImagesFromCode - transform code blocks into images =head1 DESCRIPTION This L transforms L elements into L elements. Content of transformed code section and resulting image files are written to files. Attribute C is mapped to the image title and attribute C<caption> to an image caption, if available. =head1 CONFIGURATION =over =item from File extension of input files extracted from code blocks. Defaults to C<code>. =item to File extension of created image files. Can be a fixed string or a code reference that gets the document output format (for instance L<latex> or C<html>) as argument to produce different image formats depending on output format. =item name Code reference that maps the L<CodeBlock|Pandoc::Elements/CodeBlock> element to a filename (without directory and extension). By default the element's C<id> is used if it contains characters no other than C<a-zA-Z0-9_->. Otherwise the name is the MD5 hash of the element's content. =item dir Directory where to place input and output files, relative to the current directory. This directory (default C<.>) is prepended to all image references in the target document. =item run Command to transform input files to output files. Variable references C<$...$> can be used to refer to current values of C<from>, C<to>, C<name>, C<dir>, C<infile> and C<outfile>. Example: run => ['ditaa', '-o', '$infile$', '$outfile$'], =item capture Capture output of command and write it to C<outfile>. Disabled by default. =item force Apply transformation also if input and output file already exists unchanged. Disabled by default. =back =cut