From fb0d94a2c084e5cd246160a820036accdb37c706 Mon Sep 17 00:00:00 2001 From: snow flurry Date: Sat, 5 Oct 2024 17:24:16 -0700 Subject: [PATCH] Add Markdent GFM extensions - Per-image classes, to restrict what images a CSS selector acts on (in-post vs out-of-post) - Fenced aside blocks (`!!!`-enclosed, like code blocks) - Footnotes are handled by the Markdown parser, instead of manually --- build.pl | 88 ++++-------------- lib/Classy.pm | 9 ++ lib/Classy/Dialect.pm | 11 +++ lib/Classy/Dialect/BlockParser.pm | 140 +++++++++++++++++++++++++++++ lib/Classy/Dialect/SpanParser.pm | 144 ++++++++++++++++++++++++++++++ lib/Classy/Event.pm | 11 +++ lib/Classy/Event/AsideBlock.pm | 56 ++++++++++++ lib/Classy/Handler.pm | 11 +++ lib/Classy/Handler/Fragment.pm | 64 +++++++++++++ lib/Classy/Simple.pm | 47 ++++++++++ 10 files changed, 511 insertions(+), 70 deletions(-) create mode 100644 lib/Classy.pm create mode 100644 lib/Classy/Dialect.pm create mode 100644 lib/Classy/Dialect/BlockParser.pm create mode 100644 lib/Classy/Dialect/SpanParser.pm create mode 100644 lib/Classy/Event.pm create mode 100644 lib/Classy/Event/AsideBlock.pm create mode 100644 lib/Classy/Handler.pm create mode 100644 lib/Classy/Handler/Fragment.pm create mode 100644 lib/Classy/Simple.pm diff --git a/build.pl b/build.pl index bd8a4b6..6c63856 100755 --- a/build.pl +++ b/build.pl @@ -9,7 +9,10 @@ use File::Slurp; use POSIX qw(strftime); use Text::FrontMatter::YAML; use Text::Template; -use Markdent::Simple::Fragment; +use FindBin; +use lib "$FindBin::RealBin/lib"; +use Classy::Simple; +use Classy::Dialect; use strict; use utf8; @@ -31,70 +34,18 @@ my $post_tmpl = Text::Template->new(SOURCE => "$tmpl_dir/post.html.tmpl"); my $assets_path = $cwd . ASSETS_PATH; my $out_path = $cwd . OUT_PATH; my $postout_path = $out_path . POSTS_PATH; -my $return_link = qq{}; +my $return_link = qq{}; # used across a couple functions -my $parser = Markdent::Simple::Fragment->new; +my $parser = Classy::Simple->new; sub make_fragment { - my $body = shift; $parser->markdown_to_html( - dialect => 'GitHub', - markdown => $body, + dialects => [ 'Classy::Dialect' ], + markdown => shift, ); } -sub strip_id { - my $id = shift; - $id =~ s/[ \t]/-/g; - $id =~ s/[^A-Za-z0-9_-]//g; - return $id; -} - -sub do_footnotes { - my $text = shift; - - my %footnotes; - my @used; - - my $footnote_counter = 0; - - # First grab the definitions - while ($text =~ s{ - \n\[\^([^\n]+?)\]\:[ \t]* - \n? - (.*?)(?:\n{1,2} # end at new paragraph - (?=\n[ ]{0,4}\S)|\Z) # Lookahead for non-space at line-start, or end of doc - }{\n}sx) { - my $id = strip_id($1); - $footnotes{$1} = make_fragment(qq{$2 [$return_link](#fnref:$id)}); - } - - # then replace the inline footnotes - $text =~ s{ - \[\^(.*?)\] - }{ - my $result = ''; - my $id = strip_id($1); - if (defined $footnotes{$id}) { - $footnote_counter++; - $result = qq{[$footnote_counter]}; - push(@used, $id); - } - $result; - }xsge; - - my $fn_block = qq{

    }; - # finally, append the footnotes - foreach my $id (@used) { - my $footnote = $footnotes{$id}; - $fn_block .= qq{
  1. $footnote
  2. }; - } - $fn_block .= qq{
}; - - return ($text, $fn_block); -} - # Converts a post file to a metadata hash. # # Takes one argument, the path to the post file. @@ -166,11 +117,7 @@ sub post_to_meta { $return; }egmxs; - my $fn_body; - ($body, $fn_body) = do_footnotes($body); - $fn_body = "" unless defined $fn_body; - - $metadata->{content} = make_fragment($body) . $fn_body; + $metadata->{content} = make_fragment($body); # . $fn_body; } # HACK: Stuffing the basename in the metadata because I don't want @@ -238,14 +185,21 @@ sub mkpath { } } +# subroutines used in templates +my %utils = ( + strftime => \&strftime, + include_tmpl => \&include_tmpl, +); sub include_tmpl { my $tmpl_name = shift; # TODO: do we want to cache snippet-type templates like this? my $tmpl = Text::Template->new(SOURCE => "$tmpl_dir/$tmpl_name.tmpl"); if (defined $tmpl) { - my $props = shift; - $tmpl->fill_in(HASH => { props => \$props }); + my %page_hash = %utils; + my $postdata = shift; + $page_hash{props} = $postdata; + $tmpl->fill_in(HASH => \%page_hash); } else { "error: $tmpl_dir/$tmpl_name.tmpl is missing" } @@ -254,12 +208,6 @@ sub include_tmpl { ## End subroutines -# subroutines used in templates -my %utils = ( - strftime => \&strftime, - include_tmpl => \&include_tmpl, -); - # make posts dir or die mkpath($postout_path) or die "Unable to create directory."; diff --git a/lib/Classy.pm b/lib/Classy.pm new file mode 100644 index 0000000..dbb29e4 --- /dev/null +++ b/lib/Classy.pm @@ -0,0 +1,9 @@ +package Classy; + +use 5.010; +use strict; +use warnings; + +1; + +__END__ diff --git a/lib/Classy/Dialect.pm b/lib/Classy/Dialect.pm new file mode 100644 index 0000000..1db0d25 --- /dev/null +++ b/lib/Classy/Dialect.pm @@ -0,0 +1,11 @@ +package Classy::Dialect; + +use strict; +use warnings; +use namespace::autoclean; + +our $VERSION = '0.10'; + +1; + +__END__ diff --git a/lib/Classy/Dialect/BlockParser.pm b/lib/Classy/Dialect/BlockParser.pm new file mode 100644 index 0000000..9b1fae1 --- /dev/null +++ b/lib/Classy/Dialect/BlockParser.pm @@ -0,0 +1,140 @@ +package Classy::Dialect::BlockParser; + +use Classy::Event::AsideBlock; +use Markdent::Regexes qw( :block ); + +use Markdent::Parser::BlockParser; + +use Moose::Role; + +with 'Markdent::Dialect::GitHub::BlockParser'; + +around _possible_block_matches => sub { + my $orig = shift; + my $self = shift; + + my @look_for = $self->$orig(); + unshift @look_for, 'aside_block'; + + return @look_for; +}; + +after parse_document => sub { + my $self = shift; + + return if $self->_span_parser->_fn_list_count eq 0; + + print('Dumping footnotes!') if $self->debug; + + $self->_send_event( + 'StartHTMLTag', + tag => 'div', + attributes => { + id => 'footnotes', + }, + ); + $self->_send_event('HorizontalRule'); + $self->_send_event('StartOrderedList'); + + for my $fn_id (@{ $self->_span_parser->_note_idx_map }) { + my $fndata = $self->_span_parser->_get_fn_by_id($fn_id); + print("fn-> $fn_id = $fndata\n"); + $self->_send_event( + 'StartHTMLTag', + tag => 'li', + attributes => { + id => "fn:$fn_id", + } + ); + $self->_span_parser->parse_block( "$fndata " ); + + $self->_send_event( + 'StartLink', + uri => "#fnref:$fn_id", + ); + # TODO: make this editable + $self->_send_event( + 'HTMLTag', + tag => 'img', + attributes => { + src => '/assets/img/return.gif', + class => 'backbtn', + }, + ); + + $self->_send_event('EndLink'); + $self->_send_event( + 'EndHTMLTag', + tag => 'li', + ); + } + + $self->_send_event('EndOrderedList'); + $self->_send_event( + 'EndHTMLTag', + tag => 'div', + ); +}; + +sub _match_aside_block { + my $self = shift; + my $text = shift; + + return unless ${$text} =~ / \G + $BlockStart + !!! + ([\w-]+)? # optional extra class name + \n + ( # alert block content + (?:.|\n)+? + \n # last newline required for _parse_text + ) + !!! + \n + /xmgc; + my $inner = $2; + my $classes = "aside" . (defined $1 ? " $1" : ""); + + $self->_debug_parse_result( + $inner, + 'aside block', + ) if $self->debug; + + $self->_send_event( + 'StartHTMLTag', + tag => 'div', + attributes => { + class => $classes, + } + ); + + $self->_parse_text( \$inner ); + + $self->_send_event( + 'EndHTMLTag', + tag => 'div', + ); + + return 1; +} + +1; + +__END__ + +=pod + +=head1 DESCRIPTION + +This role is similar to L, but adds parsing for +custom "aside" contexts used by datagirl.xyz. + +=head1 ROLES + +This role does the L role. + +=head1 BUGS + +We'll find out! + +=cut diff --git a/lib/Classy/Dialect/SpanParser.pm b/lib/Classy/Dialect/SpanParser.pm new file mode 100644 index 0000000..e45d029 --- /dev/null +++ b/lib/Classy/Dialect/SpanParser.pm @@ -0,0 +1,144 @@ +package Classy::Dialect::SpanParser; + +use strict; +use warnings; +use namespace::autoclean; + +our $VERSION = '0.10'; + +use Markdent::Event::StartHTMLTag; +use Markdent::Event::EndHTMLTag; +use Markdent::Event::HTMLTag; +use Markdent::Event::StartOrderedList; +use Markdent::Event::EndOrderedList; +use Markdent::Event::HorizontalRule; +use Markdent::Event::StartLink; +use Markdent::Event::EndLink; +use Markdent::Event::Text; +use Markdent::Types; + +use Moose::Role; + +with 'Markdent::Dialect::GitHub::SpanParser'; + +has _notes_by_id => ( + traits => ['Hash'], + is => 'ro', + isa => t( 'HashRef', of => t('Str') ), + default => sub { {} }, + init_arg => undef, + handles => { + _add_fn_by_id => 'set', + _get_fn_by_id => 'get', + _reset_footnotes => 'clear', + }, +); + +has _note_idx_map => ( + traits => ['Array'], + is => 'ro', + isa => t( 'ArrayRef', of => t('Str') ), + default => sub { [] }, + handles => { + _add_fn_to_map => 'push', + _get_fn_list => 'get', + _fn_list_count => 'count', + _reset_fn_map => 'clear', + }, +); + +# Footnotes are kinda like links, but IDs are prefixed with a caret (^) +around extract_link_ids => sub { + my $orig = shift; + my $self = shift; + my $text = shift; + + ${$text} =~ s/ ^ + \p{SpaceSeparator}{0,3} + \[ \^ ([^]]+) \] + : + \p{SpaceSeparator}* + \n? + \p{SpaceSeparator}* + ( + (.|\n[^\n])+ + ) + (?:\n\n|$) + / + $self->_process_id_for_fn( $1, $2 ); + '' + /egxm; + $self->$orig($text); +}; + +around _possible_span_matches => sub { + my $orig = shift; + my $self = shift; + my @look_for = $self->$orig(); + + unshift @look_for, 'footnote'; + + return @look_for; +}; + +sub _process_id_for_fn { + my $self = shift; + my $id = shift; + my $id_text = shift; + + $id_text =~ s/\s+$//; + $id_text =~ s/\n+/ /g; + $self->_debug_parse_result( + $id, + 'footnote', + [ text => $id_text ], + ) if $self->debug; + + $self->_add_fn_by_id( $id => $id_text ); +} + +# Matches the inline footnote +sub _match_footnote { + my $self = shift; + my $text = shift; + + my $pos = pos ${$text} || 0; + + return + unless ${$text} =~ / \G + \[ \^ ([^]]+) \] # footnote id + /xgc; + + my $fn_id = $1; + + $self->_add_fn_to_map($fn_id); + my $fn_idx = $self->_fn_list_count; + + # [idx] + my $start_sup = $self->_make_event( StartHTMLTag => { tag => "sup" }); + my $start_brack = '['; + my $end_brack = ']'; + my $end_sup = $self->_make_event( EndHTMLTag => { tag => "sup" }); + my $start_link = $self->_make_event( StartHTMLTag => { + tag => 'a', + attributes => { + href => "#fn:$fn_id", + id => "fnref:$fn_id", + }, + }); + my $end_link = $self->_make_event('EndLink'); + + $self->_markup_event($start_sup); + $self->_parse_text( \$start_brack ); + $self->_markup_event($start_link); + $self->_parse_text( \$fn_idx ); + $self->_markup_event($end_link); + $self->_parse_text( \$end_brack ); + $self->_markup_event($end_sup); + + return 1; +} + +1; + +__END__ diff --git a/lib/Classy/Event.pm b/lib/Classy/Event.pm new file mode 100644 index 0000000..4b2fdeb --- /dev/null +++ b/lib/Classy/Event.pm @@ -0,0 +1,11 @@ +package Classy::Event; + +use strict; +use warnings; +use namespace::autoclean; + +our $VERSION = '0.10'; + +1; + +__END__ diff --git a/lib/Classy/Event/AsideBlock.pm b/lib/Classy/Event/AsideBlock.pm new file mode 100644 index 0000000..1889828 --- /dev/null +++ b/lib/Classy/Event/AsideBlock.pm @@ -0,0 +1,56 @@ +package Classy::Event::AsideBlock; + +use strict; +use warnings; +use namespace::autoclean; + +our $VERSION = '0.10'; + +use Markdent::Types; + +use Moose; +use MooseX::StrictConstructor; + +has inner => ( + is => 'ro', + isa => t('Str'), + required => 1, +); + +has class => ( + is => 'ro', + isa => t('Str'), + predicate => 'has_class', +); + +with 'Markdent::Role::Event' => { event_class => __PACKAGE__ }; + +__PACKAGE__->meta->make_immutable; + +1; + +__END__ + +=pod + +=head1 DESCRIPTION + +This class represents an aside in the article. + +=head1 ATTRIBUTES + +This class has the following attributes: + +=head2 inner + +The inner markdown text of the block. + +=head2 type + +Either C