diff --git a/CLAUDE.md b/CLAUDE.md index 142584e..5487feb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ All publication writes are atomic: `_publish_template_to_file`/`_atomically_writ - **`Plerd::Post`** (`Post.pm`) — one post per Markdown source file. The real work happens in `_process_source_file`, fired by a Moose **trigger** the moment `source_file` is set (not lazily). That method: - parses the leading `key: value` metadata block (terminated by a blank line) into `attributes`, then the Markdown body; - - runs the body and title through `Text::MultiMarkdown::markdown` then `Plerd::SmartyPants`; + - runs the body and title through `Markdown::Perl` (a shared converter in GitHub mode, so fenced code blocks, tables, and the rest of GFM work) then `Plerd::SmartyPants`. The converter drops `"` from its HTML-escaped characters so SmartyPants can curl literal double quotes into typographic ones; quotes inside code are still escaped via `html_escaped_code_characters`; - **mutates the source file in place** — if `time`, `published_filename`, or `guid` are missing, it computes them and rewrites the file with the full metadata block. This write-back is core behavior, not a side effect to "fix." Date logic: explicit W3C `time:` wins; else a `YYYY-MM-DD` filename prefix sets midnight of that date (or now, if the date is today); else now. - resolves tags (comma-separated `tags:` header) into shared `Plerd::Tag` objects via `$plerd->tag_named`. - `publish` renders `post.tt`. `send_webmentions` walks the post's links via `Web::Mention`. diff --git a/cpanfile b/cpanfile index 367040a..8c58889 100644 --- a/cpanfile +++ b/cpanfile @@ -1,3 +1,6 @@ +# Markdown::Perl (our Markdown engine) requires Perl 5.26 or later. +requires 'perl', '5.026'; + requires 'Moose'; requires 'Template'; requires 'Path::Class'; @@ -7,7 +10,7 @@ requires 'JSON'; requires 'DateTime' => '1.54'; requires 'DateTime::Format::W3CDTF'; requires 'URI'; -requires 'Text::MultiMarkdown'; +requires 'Markdown::Perl'; requires 'YAML'; requires 'FindBin'; requires 'File::ChangeNotify'; @@ -21,3 +24,4 @@ requires 'Readonly'; requires 'Web::Mention' => '0.703'; requires 'MooseX::Types::URI'; requires 'Test::Warn'; +requires 'Capture::Tiny'; # Used by t/init.t. diff --git a/lib/Plerd/Post.pm b/lib/Plerd/Post.pm index 00657a4..edc238c 100644 --- a/lib/Plerd/Post.pm +++ b/lib/Plerd/Post.pm @@ -3,7 +3,7 @@ package Plerd::Post; use Moose; use DateTime; use DateTime::Format::W3CDTF; -use Text::MultiMarkdown qw( markdown ); +use Markdown::Perl; use URI; use HTML::Strip; use Data::GUID; @@ -18,6 +18,13 @@ use Web::Mention; use Readonly; Readonly my $WPM => 200; # The words-per-minute reading speed to assume +my $markdown = Markdown::Perl->new( + mode => 'github', + # Drop '"' so literal quotes survive for Plerd::SmartyPants to curl. + html_escaped_characters => '&<>', +); +sub markdown { $markdown->convert( $_[0] ) } + has 'plerd' => ( is => 'ro', required => 1, @@ -444,10 +451,11 @@ sub _process_source_file { } $self->body( $body ); - foreach ( qw( title body ) ) { - if ( defined( $self->$_ ) ) { - $self->$_( Plerd::SmartyPants::process( markdown( $self->$_ ) ) ); - } + foreach my $field ( qw( title body ) ) { + next unless defined $self->$field; + $self->$field( + Plerd::SmartyPants::process( markdown( $self->$field ) ) + ); } # Strip unnecessary
tags that the markdown processor just added to the title.
diff --git a/t/code_blocks.t b/t/code_blocks.t
new file mode 100644
index 0000000..fdaf57a
--- /dev/null
+++ b/t/code_blocks.t
@@ -0,0 +1,75 @@
+use warnings;
+use strict;
+use Test::More;
+use Path::Class::Dir;
+use Path::Class::File;
+use URI;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+
+use_ok( 'Plerd' );
+use Plerd::Init;
+
+# Regression test for GitHub issue #57: GitHub-style fenced code blocks.
+# Text::MultiMarkdown does not understand ``` or ~~~ fences -- it collapsed a
+# backtick-fenced block into a single inline span (so every line
+# wrapped together) and ignored ~~~ entirely. Plerd now pre-processes fences
+# into blocks before handing the body to Markdown.
+
+my $blog_dir = Path::Class::Dir->new( "$FindBin::Bin/code_blocks_blog" );
+$blog_dir->rmtree;
+Plerd::Init::initialize( $blog_dir->stringify, 0 );
+
+my $source = <<'END';
+title: Code
+
+Backtick fence with a language:
+
+```perl
+my $x = 1;
+if ( $x < 2 ) { say "ok"; }
+```
+
+Tilde fence:
+
+~~~
+plain text
+second line
+~~~
+
+Inline `code` is untouched.
+END
+
+Path::Class::File->new( $blog_dir, 'source', '2022-07-31-code.md' )->spew(
+ iomode => '>:encoding(utf8)', $source,
+);
+
+my $plerd = Plerd->new(
+ path => $blog_dir->stringify,
+ title => 'Test Blog',
+ author_name => 'Nobody',
+ author_email => 'nobody@example.com',
+ base_uri => URI->new( 'http://blog.example.com/' ),
+);
+
+$plerd->publish_all;
+my ( $post ) = @{ $plerd->posts };
+my $body = $post->body;
+
+like( $body, qr{},
+ 'Backtick fence becomes a block with a language class.' );
+like( $body, qr{my \$x = 1;\nif},
+ 'Code lines are preserved on separate lines, not wrapped together.' );
+like( $body, qr{\$x < 2},
+ 'Code special characters are HTML-escaped.' );
+like( $body, qr{\s*plain text\nsecond line},
+ 'Tilde fence also becomes a block.' );
+like( $body, qr{code},
+ 'Inline code spans still work.' );
+unlike( $body, qr{perl},
+ 'Fence is not collapsed into a single inline code span.' );
+
+$blog_dir->rmtree;
+
+done_testing();