From 0d607a8834f804e780cbba32a2802d0dcefecec3 Mon Sep 17 00:00:00 2001 From: Jason McIntosh Date: Sun, 21 Jun 2026 19:59:07 -0400 Subject: [PATCH 1/2] Accept CRLF source files and normalize them to LF (#25) CRLF ("Windows") source files historically broke publication with a misleading "not in W3C format" error, because the old metadata regex captured the trailing carriage return into attribute values. That regex was tightened in 920dbdf, which incidentally cured the crash, but the in-place source rewrite still left CRLF newlines in the body, producing a source file with mixed line endings. Normalize CRLF (and bare CR) to LF as each line is read, in both the metadata and body loops of _process_source_file, so parsing is robust by construction and the rewritten source comes out uniformly LF. Adds t/crlf.t covering publication and clean rewriting of a CRLF source. Co-Authored-By: Claude Opus 4.8 --- lib/Plerd/Post.pm | 6 +++-- t/crlf.t | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 t/crlf.t diff --git a/lib/Plerd/Post.pm b/lib/Plerd/Post.pm index 00657a4..cedbc9e 100644 --- a/lib/Plerd/Post.pm +++ b/lib/Plerd/Post.pm @@ -413,6 +413,7 @@ sub _process_source_file { my %attributes; my @ordered_attribute_names = qw( title time published_filename guid tags); while ( my $line = <$fh> ) { + $line =~ s/\r\n?/\n/g; # Normalize CRLF (or bare-CR) newlines to LF. chomp $line; last unless $line =~ /\S/; my ($key, $value) = $line =~ /^\s*(\w+?)\s*:\s*(.*?)\s*$/; @@ -428,8 +429,9 @@ sub _process_source_file { $self->attributes( \%attributes ); my $body; - while ( <$fh> ) { - $body .= $_; + while ( my $line = <$fh> ) { + $line =~ s/\r\n?/\n/g; # Normalize CRLF (or bare-CR) newlines to LF. + $body .= $line; } close $fh; diff --git a/t/crlf.t b/t/crlf.t new file mode 100644 index 0000000..3b249c0 --- /dev/null +++ b/t/crlf.t @@ -0,0 +1,65 @@ +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 #25: source files that use CRLF ("Windows") +# newlines used to break publication with a misleading "not in W3C format" +# error. Plerd should accept CRLF source files, publish them cleanly, and +# normalize the rewritten source file to LF newlines. + +my $blog_dir = Path::Class::Dir->new( "$FindBin::Bin/crlf_testblog" ); +$blog_dir->rmtree; + +Plerd::Init::initialize( $blog_dir->stringify, 0 ); + +my $source_file = Path::Class::File->new( $blog_dir, 'source', 'crlf-post.md' ); +{ + open my $fh, '>:raw', $source_file->stringify + or die "Can't write $source_file: $!"; + print $fh "title: CRLF Post\r\n"; + print $fh "time: 2018-03-04T12:00:00\r\n"; + print $fh "tags: foo, bar\r\n"; + print $fh "\r\n"; + print $fh "This is the **body** of the post.\r\n"; + print $fh "\r\n"; + print $fh "Second paragraph here.\r\n"; + close $fh; +} + +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/' ), +); + +eval { $plerd->publish_all; }; +is( $@, '', 'A CRLF source file publishes without error.' ); + +my $published = Path::Class::File->new( + $blog_dir, 'docroot', '2018-03-04-crlf-post.html' +); +ok( -e $published, 'The CRLF post produced its HTML file.' ); + +# The source file is rewritten in place during publication; it should no +# longer contain any carriage returns. +my $rewritten = do { + open my $fh, '<:raw', $source_file->stringify or die $!; + local $/; + <$fh>; +}; +unlike( $rewritten, qr/\r/, 'Rewritten source file is free of carriage returns.' ); + +$blog_dir->rmtree; + +done_testing(); From bd2c50f410a09ed124cbfad4ee972bd9a47c675f Mon Sep 17 00:00:00 2001 From: Jason McIntosh Date: Mon, 22 Jun 2026 10:22:38 -0400 Subject: [PATCH 2/2] Extract newline normalization into a helper (#25) Address review feedback: rather than repeating the normalization regex in both the metadata and body read loops, factor it into a single _normalize_newlines subroutine. Co-Authored-By: Claude Opus 4.8 --- lib/Plerd/Post.pm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/Plerd/Post.pm b/lib/Plerd/Post.pm index cedbc9e..a142e36 100644 --- a/lib/Plerd/Post.pm +++ b/lib/Plerd/Post.pm @@ -398,6 +398,15 @@ sub _build_social_meta_tags { } +# Normalize CRLF ("Windows") and bare-CR ("classic Mac") newlines to LF, so +# source files publish cleanly and get rewritten with consistent line endings +# regardless of the editor that produced them. +sub _normalize_newlines { + my ( $text ) = @_; + $text =~ s/\r\n?/\n/g; + return $text; +} + # This next internal method does a bunch of stuff. # It's called via Moose-trigger when the object's source_file attribute is set. # * Read and store the file's data (body) and metadata @@ -413,7 +422,7 @@ sub _process_source_file { my %attributes; my @ordered_attribute_names = qw( title time published_filename guid tags); while ( my $line = <$fh> ) { - $line =~ s/\r\n?/\n/g; # Normalize CRLF (or bare-CR) newlines to LF. + $line = _normalize_newlines( $line ); chomp $line; last unless $line =~ /\S/; my ($key, $value) = $line =~ /^\s*(\w+?)\s*:\s*(.*?)\s*$/; @@ -430,8 +439,7 @@ sub _process_source_file { my $body; while ( my $line = <$fh> ) { - $line =~ s/\r\n?/\n/g; # Normalize CRLF (or bare-CR) newlines to LF. - $body .= $line; + $body .= _normalize_newlines( $line ); } close $fh;