package App::WRT::Markup; use strict; use warnings; use feature "state"; use base qw(Exporter); our @EXPORT_OK = qw(line_parse image_markup eval_perl); use App::WRT::Image qw(image_size); use App::WRT::Util qw(file_get_contents); use Carp; use File::Basename; use Text::Textile; use Text::Markdown::Discount; # Some useful defaults: my %tags = ( retcon => q{div class="retcon"}, freeverse => 'p', list => "ul>\n 'div', freeverse => 'p', list => "li>\n "

\n\n

", list => "\n\n

  • " ); my %newlines = ( freeverse => "
    \n" ); my %dashes = ( freeverse => ' — ' ); =over =item eval_perl Evaluate embedded Perl in a string, replacing blocks enclosed with tags with whatever they return (well, evaluated in a scalar context). Returns the modified string. Also handles simple ${variables}, replacing them from the keys to $self. =cut sub eval_perl { my $self = shift; my ($text) = @_; while ($text =~ m{(.*?)}s) { my $block = $1; # Run the $block, and include anything returned: my $output = eval $block; if ($@) { # Errors - log and return an empty string: carp($@); $output = ''; } $text =~ s{\Q$block\E}{$output}s; } # Interpolate variables: $text =~ s{ \$\{ ([a-zA-Z_]+) \} }{ if (defined $self->{$1}) { $self->{$1}; } else { # TODO: Possibly this should be fatal. "UNDEFINED: $1"; } }gex; return $text; } =item line_parse Performs substitutions on lines called by fragment_slurp, at least. Calls include_process(), image_markup(), textile_process(), markdown_process(), eval_perl(). Applies before-parsing and after-parsing filters. Returns string. Parses some special markup. Specifically: print "hello world"; ${variable} interpolation from the WRT object path/to/file/from/project/root - Text::Textile to HTML - Text::Markdown::Discount to HTML filename.ext optional alt tag optional title text =cut sub line_parse { my $self = shift; my ($everything, $file) = (@_); # Eventually, this should probably only happen for templates: $everything = $self->eval_perl($everything); # Take care of , , , and tags: include_process($self, $everything); textile_process($everything); markdown_process($everything); $everything =~ s!(.*?)!$self->image_markup($file, $1)!seg; foreach my $key (keys %tags) { # Set some replacements, unless they've been explicitly set already: $end_tags{$key} ||= $tags{$key}; # Transform blocks: while ($everything =~ m| (<$key>\n?) (.*?) (\n?) |sx) { my $open = $1; my $block = $2; my $close = $3; # Save the bits between instances of the block: my (@interstices) = split /\Q$open$block$close\E/s, $everything; # Transform dashes, blank lines, and newlines: dashes($dashes{$key}, $block) if defined $dashes{$key}; $block =~ s/\n\n/$blank_lines{$key}/gs if defined $blank_lines{$key}; newlines($newlines{$key}, $block) if defined $newlines{$key}; # Slap it all back together as $everything, with start and end # tags: $block = "<$tags{$key}>$block"; $everything = join $block, @interstices; } } return $everything; } =item newlines($replacement, $block) Inline replace single newlines (i.e., line ends) within the block, except those preceded by a double-quote, which probably indicates a still-open tag. =cut sub newlines { $_[1] =~ s/(?<=[^"\n]) # not a double-quote or newline # don't capture \n # end-of-line (?=[^\n]) # not a newline # don't capture /$_[0]/xgs; } =item dashes($replacement, $block) Inline replace double dashes in a block - " -- " - with a given replacement. =cut sub dashes { $_[1] =~ s/(\s+) # whitespace - no capture \-{2} # two dashes (\n|\s+|$) # newline, whitespace, or eol /$1$_[0]$2/xgs; } =item include_process Inline replace filename tags, replacing them with the contents of files. =cut sub include_process { my $wrt = shift; $_[0] =~ s{ # start tag (.*?) # anything (non-greedy) # end tag }{ retrieve_include($wrt, $1); }xesg; } =item retrieve_include Get the contents of an included file. This probably needs a great deal more thought than I am presently giving it. =cut sub retrieve_include { my $wrt = shift; my ($file) = @_; # Trim leading and trailing spaces: $file =~ s/^\s+//; $file =~ s/\s+$//; if ($file =~ m{^ (/ | [.]/) }x) { # TODO: Leads with a slash or a ./ croak('Tried to open an include path with a leading / or ./ - not yet supported.'); } else { # Use the archive root as path. $file = $wrt->{root_dir} . '/' . $file; } if ($wrt->{cache_includes}) { if (defined $wrt->{include_cache}->{$file}) { return $wrt->{include_cache}->{$file}; } } unless (-e $file) { carp "No such file: $file"; return ''; } if (-d $file) { carp("Tried to open a directory as an include path: $file"); return ''; } if ($wrt->{cache_includes}) { $wrt->{include_cache}->{$file} = file_get_contents($file); return $wrt->{include_cache}->{$file}; } else { return file_get_contents($file); } } =item textile_process Inline replace markup in a string. =cut # This is exactly the kind of code that, even though it isn't doing anything # especially over the top, looks ghastly to people who don't read Perl, so I'll # try to explain a bit. sub textile_process { # First, there's a state variable here which can retain the Text::Textile # object between invocations of the function, saving us a bit of time on # subsequent calls. This should be equivalent to creating a closure around # the function and keeping a $textile variable there. state $textile; # Second, instead of unrolling the arguments to the function, we just act # directly on the first (0th) one. =~ more or less means "do a regexy # thing on this". It's followed by s, the substitution operator, which can # use curly braces as delimiters between pattern and replacement. $_[0] =~ s{ # Find tags... # start tag (.*?) # anything (non-greedy) # end tag }{ # ...and replace them with the result of evaluating this block. # //= means "defined-or-equals"; if the var hasn't been defined yet, # then make a new Textile object: $textile //= Text::Textile->new(); # Process the stuff we slurped out of our tags - this value will be # used to replace the entire match from above (in Perl, the last # expression evaluated is the return value of subs, evals, etc.): $textile->process($1); }xesg; # x: eXtended regexp - whitespace ignored by default, comments allowed # e: Execute the replacement as Perl code, and use its value # s: treat all lines of the search subject as a Single string # g: Globally replace all matches # For the genuinely concise version of this, see markdown_process(). } =item markdown_process Inline replace markup in a string. =cut sub markdown_process { state $markdown; my $flags = Text::Markdown::Discount::MKD_EXTRA_FOOTNOTE(); $_[0] =~ s{ (.*?) }{ $markdown //= Text::Markdown::Discount->new; $markdown->markdown($1, $flags); }xesg; } =item image_markup Parse out an image tag and return the appropriate html. Relies on image_size from App::WRT::Image. =cut sub image_markup { my $self = shift; my ($file, $block) = @_; # Get a basename and directory for the file (entry) referencing the image: my ($basename, $dir) = fileparse($file); # Truncated file date that just includes date + sub docs: my ($file_date) = $dir =~ m{ ( [0-9]{4}/ # year [0-9]{1,2}/ # month [0-9]{1,2}/ # day ([a-z]*/)* # sub-entries ) $ }x; # Process the contents of the tag: my ($image_url, $alt_text, $title_text) = split /\n/, $block; $alt_text ||= q{}; $title_text ||= $alt_text; # Resolve relative paths: my $image_file; if (-e "$dir/$image_url" ) { # The path is to an image file in the same directory as current entry: $image_file = "$dir/$image_url"; $image_url = "${file_date}${image_url}"; } elsif (-e $self->{entry_dir} . "/$image_url") { # The path is to an image file starting with the entry_dir, like # 2005/9/20/candles.jpg -> ./archives/2005/9/20/candles.jpg $image_file = $self->{entry_dir} . "/$image_url"; } # Get width & height in pixels for known filetypes: my ($width, $height) = image_size($self->{root_dir_abs} . '/' . $image_file); # This probably relies on mod_rewrite working: $image_url = $self->{image_url_root} . $image_url; return <<"IMG"; $alt_text IMG } =back 1;