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
This commit is contained in:
parent
b848bc58c9
commit
fb0d94a2c0
88
build.pl
88
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{<img src="/assets/img/return.gif" />};
|
||||
my $return_link = qq{<img class="ret-link" src="/assets/img/return.gif" />};
|
||||
|
||||
# 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{<sup>[<a href="#fn:$id" id="fnref:$id" class="footnote">$footnote_counter</a>]</sup>};
|
||||
push(@used, $id);
|
||||
}
|
||||
$result;
|
||||
}xsge;
|
||||
|
||||
my $fn_block = qq{<div id="footnotes"><hr /><ol>};
|
||||
# finally, append the footnotes
|
||||
foreach my $id (@used) {
|
||||
my $footnote = $footnotes{$id};
|
||||
$fn_block .= qq{<li id="fn:$id">$footnote</li>};
|
||||
}
|
||||
$fn_block .= qq{</ol></div>};
|
||||
|
||||
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.";
|
||||
|
|
9
lib/Classy.pm
Normal file
9
lib/Classy.pm
Normal file
|
@ -0,0 +1,9 @@
|
|||
package Classy;
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
11
lib/Classy/Dialect.pm
Normal file
11
lib/Classy/Dialect.pm
Normal file
|
@ -0,0 +1,11 @@
|
|||
package Classy::Dialect;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use namespace::autoclean;
|
||||
|
||||
our $VERSION = '0.10';
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
140
lib/Classy/Dialect/BlockParser.pm
Normal file
140
lib/Classy/Dialect/BlockParser.pm
Normal file
|
@ -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<Markdent::Dialect::GitHub>, but adds parsing for
|
||||
custom "aside" contexts used by datagirl.xyz.
|
||||
|
||||
=head1 ROLES
|
||||
|
||||
This role does the L<Markdent::Role::Dialect::BlockParser> role.
|
||||
|
||||
=head1 BUGS
|
||||
|
||||
We'll find out!
|
||||
|
||||
=cut
|
144
lib/Classy/Dialect/SpanParser.pm
Normal file
144
lib/Classy/Dialect/SpanParser.pm
Normal file
|
@ -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;
|
||||
|
||||
# <sup>[<a href="#fn:id" title="fnref:id">idx</a>]</sup>
|
||||
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__
|
11
lib/Classy/Event.pm
Normal file
11
lib/Classy/Event.pm
Normal file
|
@ -0,0 +1,11 @@
|
|||
package Classy::Event;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use namespace::autoclean;
|
||||
|
||||
our $VERSION = '0.10';
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
56
lib/Classy/Event/AsideBlock.pm
Normal file
56
lib/Classy/Event/AsideBlock.pm
Normal file
|
@ -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<aside> or C<alert>, depending on the "severity" of the aside.
|
||||
|
||||
=head1 ROLES
|
||||
|
||||
This class does the L<Markdent::Role::Event> role.
|
||||
|
||||
=cut
|
11
lib/Classy/Handler.pm
Normal file
11
lib/Classy/Handler.pm
Normal file
|
@ -0,0 +1,11 @@
|
|||
package Classy::Handler;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use namespace::autoclean;
|
||||
|
||||
our $VERSION = '0.10';
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
64
lib/Classy/Handler/Fragment.pm
Normal file
64
lib/Classy/Handler/Fragment.pm
Normal file
|
@ -0,0 +1,64 @@
|
|||
package Classy::Handler::Fragment;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use namespace::autoclean;
|
||||
|
||||
our $VERSION = '0.10';
|
||||
|
||||
use Markdent::Role::HTMLStream;
|
||||
use List::Util qw(first);
|
||||
use Classy::Event::AsideBlock;
|
||||
use Markdent::Types;
|
||||
use Params::ValidationCompiler qw( validation_for );
|
||||
|
||||
use Moose;
|
||||
|
||||
with 'Markdent::Role::HTMLStream';
|
||||
|
||||
# We're a fragment, don't output start/end document
|
||||
sub start_document { }
|
||||
sub end_document { }
|
||||
|
||||
around '_stream_start_tag' => sub {
|
||||
my $orig = shift;
|
||||
my $self = shift;
|
||||
my $tag = shift;
|
||||
my $attr = shift;
|
||||
|
||||
if ($tag eq "img" && !exists $attr->{class}) {
|
||||
$attr->{class} = "as-post";
|
||||
}
|
||||
|
||||
$self->$orig($tag, $attr);
|
||||
};
|
||||
|
||||
{
|
||||
my $validator = validation_for(
|
||||
params => [
|
||||
inner => { type => t('Str') },
|
||||
class => {
|
||||
type => t('Str'),
|
||||
optional => 1,
|
||||
},
|
||||
],
|
||||
named_to_list => 1,
|
||||
);
|
||||
|
||||
sub aside_block {
|
||||
my $self = shift;
|
||||
my ( $inner, $class ) = $validator->(@_);
|
||||
my %attrs = ( class => "aside" );
|
||||
|
||||
if ( $class ) {
|
||||
$attrs{class} .= " $class";
|
||||
}
|
||||
$self->_stream_start_tag( 'div', \%attrs );
|
||||
|
||||
$self->_stream_text($inner);
|
||||
|
||||
$self->_stream_end_tag('div');
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
47
lib/Classy/Simple.pm
Normal file
47
lib/Classy/Simple.pm
Normal file
|
@ -0,0 +1,47 @@
|
|||
package Classy::Simple;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use namespace::autoclean;
|
||||
|
||||
our $VERSION = '0.10';
|
||||
|
||||
use Classy::Handler::Fragment;
|
||||
use Params::ValidationCompiler qw( validation_for );
|
||||
use Markdent::Parser;
|
||||
use Markdent::Types;
|
||||
|
||||
use Moose;
|
||||
|
||||
with 'Markdent::Role::Simple';
|
||||
|
||||
{
|
||||
# Validator from Markdent::Simple::Fragment
|
||||
my $validator = validation_for(
|
||||
params => [
|
||||
dialects => {
|
||||
type => t( 'ArrayRef', of => t('Str') ),
|
||||
default => sub { [] },
|
||||
},
|
||||
markdown => { type => t('Str') },
|
||||
],
|
||||
named_to_list => 1,
|
||||
);
|
||||
|
||||
sub markdown_to_html {
|
||||
my $self = shift;
|
||||
my ( $dialects, $markdown ) = $validator->(@_);
|
||||
|
||||
my $handler_class = 'Classy::Handler::Fragment';
|
||||
|
||||
return $self->_parse_markdown(
|
||||
$markdown,
|
||||
$dialects,
|
||||
$handler_class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
Loading…
Reference in a new issue