no
warnings
'experimental::refaliasing'
;
our
$VERSION
=
'0.02'
;
Hide Show 6 lines of Pod
sub
new {
my
(
$class
,
$pmarkdown
,
$md
) =
@_
;
my
$this
=
bless
{
pmarkdown
=>
$pmarkdown
,
blocks
=> [],
blocks_stack
=> [],
paragraph
=> [],
last_line_is_blank
=> 0,
last_line_was_blank
=> 0,
skip_next_block_matching
=> 0,
is_lazy_continuation
=> 0,
md
=>
undef
,
last_pos
=> 0,
line_ending
=>
''
,
continuation_re
=>
qr//
,
linkrefs
=> {},
matched_prefix_size
=> 0,
},
$class
;
lock_keys_plus(%{
$this
},
qw(forced_line)
);
\
$this
->{md} =
$md
;
return
$this
;
}
my
$pkg
= __PACKAGE__;
sub
AUTOLOAD {
our
$AUTOLOAD
;
$AUTOLOAD
=~ s/${pkg}:://;
return
if
$AUTOLOAD
eq
'DESTROY'
;
confess
"Undefined method ${AUTOLOAD}"
unless
$AUTOLOAD
=~ m/^get_/;
my
$this
=
shift
@_
;
return
$this
->{pmarkdown}->
$AUTOLOAD
(
@_
);
}
my
$eol_re
=
qr/ \r\n | \n | \r /
x;
sub
next_line {
my
(
$this
) =
@_
;
return
delete
$this
->{forced_line}
if
exists
$this
->{forced_line};
return
if
pos
(
$this
->{md}) ==
length
(
$this
->{md});
$this
->{last_pos} =
pos
(
$this
->{md});
$this
->{md} =~ m/\G([^\n\r]*)(${eol_re})?/g or confess
'Should not happen'
;
my
(
$t
,
$e
) = ($1, $2);
if
($1 =~ /^[ \t]+$/) {
$this
->{line_ending} =
$t
.(
$e
//
''
)
if
$this
->get_preserve_white_lines;
return
''
;
}
else
{
$this
->{line_ending} =
$e
// (
$this
->get_force_final_new_line ?
"\n"
:
''
);
return
$t
;
}
}
sub
line_ending {
my
(
$this
) =
@_
;
return
$this
->{line_ending};
}
sub
set_pos {
my
(
$this
,
$pos
,
$last_pos
) =
@_
;
pos
(
$this
->{md}) =
$pos
;
$this
->{last_pos} =
$last_pos
if
defined
$last_pos
;
return
;
}
sub
get_pos {
my
(
$this
) =
@_
;
return
pos
(
$this
->{md});
}
sub
redo_line {
my
(
$this
) =
@_
;
confess
'Cannot push back more than one line'
unless
exists
$this
->{last_pos};
$this
->set_pos(
delete
$this
->{last_pos});
return
;
}
sub
process {
my
(
$this
) =
@_
;
pos
(
$this
->{md}) = 0;
$this
->_parse_yaml_metadata()
if
$this
->get_parse_file_metadata eq
'yaml'
;
while
(
defined
(
my
$l
=
$this
->next_line())) {
$this
->{is_lazy_continuation} = 0;
$this
->_parse_blocks(
$l
);
}
$this
->_finalize_paragraph();
while
(@{
$this
->{blocks_stack}}) {
$this
->_restore_parent_block();
}
return
delete
$this
->{linkrefs},
delete
$this
->{blocks};
}
sub
_finalize_paragraph {
my
(
$this
,
$force_loose_test
) =
@_
;
if
(@{
$this
->{paragraph}} ||
$force_loose_test
) {
if
(
$this
->{last_line_was_blank}) {
if
(@{
$this
->{blocks_stack}}
&&
$this
->{blocks_stack}[-1]{block}{type} eq
'list_item'
) {
$this
->{blocks_stack}[-1]{block}{loose} = 1;
}
}
}
return
unless
@{
$this
->{paragraph}};
push
@{
$this
->{blocks}}, {
type
=>
'paragraph'
,
content
=>
$this
->{paragraph}};
$this
->{paragraph} = [];
return
;
}
sub
_list_match {
my
(
$this
,
$item
) =
@_
;
return
0
unless
@{
$this
->{blocks}};
my
$list
=
$this
->{blocks}[-1];
return
$list
->{type} eq
'list'
&&
$list
->{style} eq
$item
->{style}
&&
$list
->{marker} eq
$item
->{marker};
}
sub
_add_block {
my
(
$this
,
$block
) =
@_
;
if
(
$block
->{type} eq
'list_item'
) {
$this
->_finalize_paragraph(0);
if
(
$this
->_list_match(
$block
)) {
push
@{
$this
->{blocks}[-1]{items}},
$block
;
$this
->{blocks}[-1]{loose} ||=
$block
->{loose};
}
else
{
my
$list
= {
type
=>
'list'
,
style
=>
$block
->{style},
marker
=>
$block
->{marker},
start_num
=>
$block
->{num},
items
=> [
$block
],
loose
=>
$block
->{loose}
};
push
@{
$this
->{blocks}},
$list
;
}
}
else
{
$this
->_finalize_paragraph(1);
push
@{
$this
->{blocks}},
$block
;
}
return
;
}
sub
_enter_child_block {
my
(
$this
,
$new_block
,
$cond
,
$prefix_re
,
$forced_next_line
) =
@_
;
$this
->_finalize_paragraph(1);
if
(
defined
$forced_next_line
) {
$this
->{forced_line} =
$forced_next_line
;
}
push
@{
$this
->{blocks_stack}}, {
cond
=>
$cond
,
block
=>
$new_block
,
parent_blocks
=>
$this
->{blocks},
continuation_re
=>
$this
->{continuation_re}
};
$this
->{continuation_re} =
qr/$this->{continuation_re} $prefix_re/
x;
$this
->{blocks} = [];
return
;
}
sub
_restore_parent_block {
my
(
$this
) =
@_
;
my
$last_block
=
pop
@{
$this
->{blocks_stack}};
my
$block
=
delete
$last_block
->{block};
$block
->{content} =
$this
->{blocks};
$this
->{blocks} =
delete
$last_block
->{parent_blocks};
$this
->{continuation_re} =
delete
$last_block
->{continuation_re};
$this
->_add_block(
$block
);
return
;
}
sub
_test_lazy_continuation {
my
(
$this
,
$l
) =
@_
;
return
unless
@{
$this
->{paragraph}};
my
$tester
= new(
ref
(
$this
),
$this
->{pmarkdown}, \
''
);
pos
(
$tester
->{md}) = 0;
$tester
->{paragraph} = [@{
$this
->{paragraph}} ? (
'foo'
) : ()];
$tester
->{is_lazy_continuation} = 1;
$tester
->_parse_blocks(
$l
);
if
(@{
$tester
->{paragraph}}) {
$this
->{is_lazy_continuation} = 1;
return
1;
}
return
0;
}
sub
_count_matching_blocks {
my
(
$this
,
$lr
) =
@_
;
$this
->{matched_prefix_size} = 0;
for
my
$i
(0 .. $
local
*::_ =
$lr
;
my
$r
=
$this
->{blocks_stack}[
$i
]{cond}();
$this
->{matched_prefix_size} +=
$r
if
defined
$r
&&
$r
> 0;
return
$i
unless
$r
;
}
return
@{
$this
->{blocks_stack}};
}
sub
_all_blocks_match {
my
(
$this
,
$lr
) =
@_
;
return
@{
$this
->{blocks_stack}} ==
$this
->_count_matching_blocks(
$lr
);
}
my
$thematic_break_re
=
qr/^\ {0,3} (?: (?:-[ \t]*){3,} | (_[ \t]*){3,} | (\*[ \t]*){3,} ) $/
x;
my
$block_quotes_re
=
qr/^ {0,3}>/
;
my
$indented_code_re
=
qr/^(?: {0,3}\t| {4})/
;
my
$list_item_marker_re
=
qr/ [-+*] | (?<digits>\d{1,9}) (?<symbol>[.)])/
x;
my
$list_item_re
=
qr/^ (?<indent>\ {0,3}) (?<marker>${list_item_marker_re}) (?<text>(?:[ \t].*)?) $/
x;
my
$supported_html_tags
=
join
(
'|'
,
qw(address article aside base basefont blockquote body caption center col colgroup dd details dialog dir div dl dt fieldset figcaption figure footer form frame frameset h1 h2 h3 h4 h5 h6 head header hr html iframe legend li link main menu menuitem nav noframes ol optgroup option p param search section summary table tbody td tfoot th thead title tr track ul)
);
my
$directive_name_re
=
qr/(?<name> [-\w]+ )?/
x;
my
$directive_content_re
=
qr/(?: \s* \[ (?<content> [^\]]+ ) \] )?/
x;
my
$directive_attribute_re
=
qr/(?: \s* \{ (?<attributes> .* ) \} )?/
x;
my
$directive_data_re
=
qr/${directive_name_re} ${directive_content_re} ${directive_attribute_re}/
x;
my
$directive_block_re
=
qr/^\ {0,3} (?<marker> :{3,} ) \s* ${directive_data_re} \s* :* \s* $/
x;
my
$html_tag_name_re
=
qr/[a-zA-Z][-a-zA-Z0-9]*/
;
my
$html_attribute_name_re
=
qr/[a-zA-Z_:][-a-zA-Z0-9_.:]*/
;
my
$html_space_re
=
qr/\n[ \t]*|[ \t][ \t]*\n?[ \t]*/
;
my
$opt_html_space_re
=
qr/[ \t]*\n?[ \t]*/
;
my
$html_attribute_value_re
=
qr/ [^ \t\n"'=<>`]+ | '[^']*' | "[^"]*" /
x;
my
$html_attribute_re
=
qr/ ${html_space_re} ${html_attribute_name_re} (?: ${opt_html_space_re} = ${opt_html_space_re} ${html_attribute_value_re} )? /
x;
my
$html_open_tag_re
=
qr/ < ${html_tag_name_re} ${html_attribute_re}* ${opt_html_space_re} \/
? > /x;
my
$html_close_tag_re
=
qr/ <\/
${html_tag_name_re} ${opt_html_space_re} > /x;
our
$l
;
sub
_parse_blocks {
my
$this
=
shift
@_
;
local
$l
=
shift
@_
;
if
(!
$this
->{skip_next_block_matching}) {
my
$matched_block
=
$this
->_count_matching_blocks(\
$l
);
if
(@{
$this
->{blocks_stack}} >
$matched_block
) {
$this
->_finalize_paragraph();
while
(@{
$this
->{blocks_stack}} >
$matched_block
) {
$this
->_restore_parent_block();
}
}
}
else
{
$this
->{skip_next_block_matching} = 0;
}
if
(
$this
->{last_line_is_blank}) {
if
(@{
$this
->{blocks_stack}}
&&
$this
->{blocks_stack}[-1]{block}{type} eq
'list_item'
) {
}
}
$this
->{last_line_was_blank} =
$this
->{last_line_is_blank};
$this
->{last_line_is_blank} = 0;
_do_atx_heading(
$this
)
|| (
$this
->get_use_setext_headings && _do_setext_heading(
$this
))
|| _do_thematic_break(
$this
)
|| _do_indented_code_block(
$this
)
|| (
$this
->get_use_fenced_code_blocks && _do_fenced_code_block(
$this
))
|| _do_html_block(
$this
)
|| _do_block_quotes(
$this
)
|| _do_list_item(
$this
)
|| _do_directive_block(
$this
)
|| _do_link_reference_definition(
$this
)
|| (
$this
->get_use_table_blocks && _do_table_block(
$this
))
|| _do_paragraph(
$this
)
|| croak
"Current line could not be parsed as anything: $l"
;
return
;
}
sub
_load_yaml_module {
my
(
$module_name
) =
@_
;
if
(!
eval
"require $module_name; 1"
) {
croak
"Cannot load module $module_name: ${EVAL_ERROR}"
;
}
return
;
}
sub
_call_yaml_parser {
my
(
$this
,
$yaml
) =
@_
;
my
$parser
=
$this
->get_yaml_parser;
my
$metadata
;
if
(
$parser
eq
'YAML::Tiny'
) {
return
eval
{ YAML::Tiny->read_string(
$yaml
)->[0] };
}
elsif
(
$parser
eq
'YAML::PP'
||
$parser
eq
'YAML::PP::LibYAML'
) {
_load_yaml_module(
$parser
);
return
eval
{ (
$parser
->new()->load_string(
$yaml
))[0] };
}
croak
"Unsupported YAML parser: $parser"
;
}
sub
_parse_yaml_metadata {
my
(
$this
) =
@_
;
my
$line_re
=
$this
->get_yaml_file_metadata_allows_empty_lines ?
qr/.*\n/
:
qr/.+\n/
;
if
(
$this
->{md} =~ m/ ^ ---\n (?<YAML> (?:
$line_re
)+? ) (?: --- | \.\.\. ) \n /gxc) {
my
$metadata
=
$this
->_call_yaml_parser($+{YAML});
if
(
$EVAL_ERROR
) {
pos
(
$this
->{md}) = 0;
carp
'YAML Metadata (Markdown frontmatter) is invalid'
if
$this
->get_warn_for_unused_input();
return
;
}
if
(
exists
(
$this
->{pmarkdown}{hooks}{yaml_metadata})) {
$this
->{pmarkdown}{hooks}{yaml_metadata}->(
$metadata
);
}
}
return
;
}
sub
_do_atx_heading {
my
(
$this
) =
@_
;
if
(
$l
=~ /^ \ {0,3} (\
$this
->_add_block({
type
=>
'heading'
,
level
=>
length
($1),
content
=> $2 //
''
,
debug
=>
'atx'
});
return
1;
}
return
;
}
sub
_do_setext_heading {
my
(
$this
) =
@_
;
return
unless
$l
=~ /^ {0,3}(-+|=+)[ \t]*$/;
if
( !@{
$this
->{paragraph}}
|| indent_size(
$this
->{paragraph}[0]) >= 4
||
$this
->{is_lazy_continuation}) {
return
;
}
my
$c
=
substr
$1, 0, 1;
my
$p
=
$this
->{paragraph};
my
$m
=
$this
->get_multi_lines_setext_headings;
if
(
$m
eq
'single_line'
&& @{
$p
} > 1) {
my
$last_line
=
pop
@{
$p
};
$this
->_finalize_paragraph();
$p
= [
$last_line
];
}
elsif
(
$m
eq
'break'
&&
$l
=~ m/${thematic_break_re}/) {
$this
->_finalize_paragraph();
$this
->_add_block({
type
=>
'break'
,
debug
=>
'setext_as_break'
});
return
1;
}
elsif
(
$m
eq
'ignore'
) {
push
@{
$this
->{paragraph}},
$l
;
return
1;
}
$this
->{paragraph} = [];
$this
->_add_block({
type
=>
'heading'
,
level
=> (
$c
eq
'='
? 1 : 2),
content
=>
$p
,
debug
=>
'setext'
});
return
1;
}
sub
_do_thematic_break {
my
(
$this
) =
@_
;
if
(
$l
!~ /${thematic_break_re}/) {
return
;
}
$this
->_add_block({
type
=>
'break'
,
debug
=>
'native_break'
});
return
1;
}
sub
_do_indented_code_block {
my
(
$this
) =
@_
;
if
(@{
$this
->{paragraph}} ||
$l
!~ m/${indented_code_re}/) {
return
;
}
my
$convert_tabs
=
$this
->get_code_blocks_convert_tabs_to_spaces;
tabs_to_space(
$l
,
$this
->{matched_prefix_size})
if
$convert_tabs
;
my
@code_lines
=
scalar
(remove_prefix_spaces(4,
$l
.
$this
->line_ending()));
my
$count
= 1;
my
$valid_count
= 1;
my
$valid_pos
=
$this
->get_pos();
while
(
defined
(
my
$nl
=
$this
->next_line())) {
if
(
$this
->_all_blocks_match(\
$nl
)) {
$count
++;
if
(
$nl
=~ m/${indented_code_re}/) {
$valid_pos
=
$this
->get_pos();
$valid_count
=
$count
;
tabs_to_space(
$nl
,
$this
->{matched_prefix_size})
if
$convert_tabs
;
push
@code_lines
,
scalar
(remove_prefix_spaces(4,
$nl
.
$this
->line_ending()));
}
elsif
(
$nl
eq
''
) {
push
@code_lines
,
scalar
(remove_prefix_spaces(4,
$nl
.
$this
->line_ending(), !
$convert_tabs
));
}
else
{
last
;
}
}
else
{
last
;
}
}
splice
@code_lines
,
$valid_count
;
$this
->set_pos(
$valid_pos
);
my
$code
=
join
(
''
,
@code_lines
);
$this
->_add_block({
type
=>
'code'
,
content
=>
$code
,
debug
=>
'indented'
});
return
1;
}
sub
_do_fenced_code_block {
my
(
$this
) =
@_
;
return
unless
$l
=~ /^ (?<indent>\ {0,3}) (?<fence>`{3,}|~{3,}) [ \t]* (?<info>.*?) [ \t]* $/x;
my
$f
=
substr
$+{fence}, 0, 1;
if
(
$f
eq
'`'
&&
index
($+{info},
'`'
) != -1) {
return
;
}
my
$fl
=
length
($+{fence});
my
$info
= $+{info};
my
$indent
=
length
($+{indent});
$info
=~ s/\\(\p{PosixPunct})/$1/g;
my
@code_lines
;
my
$end_fence_seen
= 0;
my
$start_pos
=
$this
->get_pos();
while
(
defined
(
my
$nl
=
$this
->next_line())) {
if
(
$this
->_all_blocks_match(\
$nl
)) {
if
(
$nl
=~ m/^ {0,3}${f}{
$fl
,}[ \t]*$/) {
$end_fence_seen
= 1;
last
;
}
else
{
push
@code_lines
,
scalar
(remove_prefix_spaces(
$indent
,
$nl
.
$this
->line_ending()));
}
}
else
{
$this
->redo_line()
if
!
$this
->get_fenced_code_blocks_must_be_closed;
last
;
}
}
if
(!
$end_fence_seen
&&
$this
->get_fenced_code_blocks_must_be_closed) {
$this
->set_pos(
$start_pos
);
return
;
}
my
$code
=
join
(
''
,
@code_lines
);
$this
->_add_block({
type
=>
'code'
,
content
=>
$code
,
info
=>
$info
,
debug
=>
'fenced'
});
return
1;
}
sub
_do_html_block {
my
(
$this
) =
@_
;
my
$html_end_condition
;
if
(
$l
=~ m/ ^\ {0,3} < (?:pre|script|style|textarea) (?:\ |\t|>|$) /x) {
$html_end_condition
=
qr/ <\/
(?:pre|script|style|textarea) > /x;
}
elsif
(
$l
=~ m/^ {0,3}<!--/) {
$html_end_condition
=
qr/-->/
;
}
elsif
(
$l
=~ m/^ {0,3}<\?/) {
$html_end_condition
=
qr/\?>/
;
}
elsif
(
$l
=~ m/^ {0,3}<![a-zA-Z]/) {
$html_end_condition
=
qr/=>/
;
}
elsif
(
$l
=~ m/^ {0,3}<!\[CDATA\[/) {
$html_end_condition
=
qr/]]>/
;
}
elsif
(
$l
=~ m/^\ {0,3} < \/? (?:${supported_html_tags}) (?:\ |\t|\/?>|$) /x) {
$html_end_condition
=
qr/^$/
;
}
elsif
(!@{
$this
->{paragraph}}
&&
$l
=~ m/^\ {0,3} (?: ${html_open_tag_re} | ${html_close_tag_re} ) [ \t]* $ /x) {
$html_end_condition
=
qr/^$/
;
}
if
(!
$html_end_condition
) {
return
;
}
my
@html_lines
=
$l
.
$this
->line_ending();
if
(
$l
!~ m/${html_end_condition}/) {
while
(
defined
(
my
$nl
=
$this
->next_line())) {
if
(
$this
->_all_blocks_match(\
$nl
)) {
if
(
$nl
!~ m/${html_end_condition}/) {
push
@html_lines
,
$nl
.
$this
->line_ending();
}
elsif
(
$nl
eq
''
) {
$this
->redo_line();
last
;
}
else
{
push
@html_lines
,
$nl
.
$this
->line_ending();
last
;
}
}
else
{
$this
->redo_line();
last
;
}
}
}
my
$html
=
join
(
''
,
@html_lines
);
remove_disallowed_tags(
$html
,
$this
->get_disallowed_html_tags);
$this
->_add_block({
type
=>
'html'
,
content
=>
$html
});
return
1;
}
sub
_do_block_quotes {
my
(
$this
) =
@_
;
return
unless
$l
=~ /${block_quotes_re}/;
my
$cond
=
sub
{
if
(s/(${block_quotes_re})/
' '
x
length
($1)/e) {
my
$m
;
(
$_
,
$m
) = remove_prefix_spaces(
length
($1) + 1,
$_
);
return
$m
;
}
return
$this
->_test_lazy_continuation(
$_
);
};
{
local
*::_ = \
$l
;
$this
->{matched_prefix_size} +=
$cond
->();
}
$this
->{skip_next_block_matching} = 1;
$this
->_enter_child_block({
type
=>
'quotes'
},
$cond
,
qr/ {0,3}(?:> ?)?/
,
$l
);
return
1;
}
sub
_do_list_item {
my
(
$this
) =
@_
;
return
unless
$l
=~ m/${list_item_re}/;
my
(
$indent_outside
,
$marker
,
$text
,
$digits
,
$symbol
) = @+{
qw(indent marker text digits symbol)
};
my
$indent_marker
=
length
(
$indent_outside
) +
length
(
$marker
);
my
$type
=
$marker
=~ m/[-+*]/ ?
'ul'
:
'ol'
;
my
$text_indent
= indent_size(
$text
,
$indent_marker
+
$this
->{matched_prefix_size});
my
$mode
=
$this
->get_lists_can_interrupt_paragraph;
if
(@{
$this
->{paragraph}}) {
return
if
$mode
eq
'never'
;
if
(
$mode
eq
'within_list'
&& !(@{
$this
->{blocks_stack}} &&
$this
->{blocks_stack}[-1]{block}{type} eq
'list_item'
)) {
return
;
}
if
(
$mode
eq
'strict'
&& (
$text
eq
''
|| (
$type
eq
'ol'
&&
$digits
!= 1))) {
return
;
}
}
return
if
$text
ne
''
&&
$text_indent
== 0;
my
$first_line_blank
=
$text
=~ m/^[ \t]*$/;
my
$discard_text_indent
=
$first_line_blank
|| indented(4 + 1,
$text
);
my
$indent_inside
=
$discard_text_indent
? 1 :
$text_indent
;
my
$indent
=
$indent_inside
+
$indent_marker
;
my
$cond
=
sub
{
if
(
$first_line_blank
&& m/^[ \t]*$/) {
return
0;
}
else
{
$first_line_blank
= 0;
}
if
(indent_size(
$_
) >=
$indent
) {
$_
= remove_prefix_spaces(
$indent
,
$_
);
return
$indent
;
}
return
(!m/${list_item_re}/ &&
$this
->_test_lazy_continuation(
$_
))
||
$_
eq
''
;
};
my
$forced_next_line
=
undef
;
if
(!
$first_line_blank
) {
$forced_next_line
= remove_prefix_spaces(
$indent
, (
' '
x
$indent_marker
).
$text
);
$this
->{matched_prefix_size} =
$indent
;
$this
->{skip_next_block_matching} = 1;
}
my
$item
= {
type
=>
'list_item'
,
style
=>
$type
,
marker
=>
$symbol
//
$marker
,
num
=>
int
(
$digits
// 1),
};
$item
->{loose} =
$this
->_list_match(
$item
) &&
$this
->{last_line_was_blank};
$this
->_enter_child_block(
$item
,
$cond
,
qr/ {0,${indent}}/
,
$forced_next_line
);
return
1;
}
sub
_do_directive_block {
my
(
$this
) =
@_
;
return
unless
$this
->get_use_directive_blocks();
return
unless
$l
=~ /${directive_block_re}/;
my
$lm
=
length
($+{marker});
my
$cond
=
sub
{
if
(m/^\ {0,3} :{
$lm
,} \s* $/x) {
$_
=
''
;
return
0;
}
return
-1;
};
$this
->_enter_child_block({
type
=>
'directive'
,
name
=> $+{name},
inline
=> $+{content},
attributes
=> $+{attributes}
},
$cond
,
qr/ {0,3}/
);
return
1;
}
sub
_do_link_reference_definition {
my
(
$this
) =
@_
;
return
if
@{
$this
->{paragraph}} ||
$l
!~ m/^ {0,3}\[/;
my
$last_pos
=
$this
->{last_pos};
my
$init_pos
=
$this
->get_pos();
$this
->redo_line();
my
$start_pos
=
$this
->get_pos();
my
$cont
=
$this
->{continuation_re};
confess
'Unexpected regex match failure'
unless
$this
->{md} =~ m/\G${cont}/g;
if
(
$this
->{md} =~ m/\G
\ {0,3} \[
(?>(?<LABEL>
(?:
[^\\\[\]]{0,100} (?:(?:\\\\)* \\ .)?
(
*COMMIT
) (?(?{
pos
() >
$start_pos
+ 1004 }) (
*FAIL
) )
)+
)) \]:
[ \t]* (?:\n ${cont})? [ \t]*
(?>(?<TARGET>
< (?: [^\n>]* (?<! \\) (?:\\\\)* )+ >
| [^< [:cntrl:]] [^ [:cntrl:]]*
))
(?:
(?> [ \t]* (?:\n ${cont}) [ \t]* | [ \t]+ )
(?<TITLE>
" (:?[^\n"
]* (?: (?<! \n) \n (?! \n) | (?<! \\) (?:\\\\)* \\
" )? )* "
|
' (:?[^\n'
]* (?: (?<! \n) \n (?! \n) | (?<! \\) (?:\\\\)* \\
' )? )* '
| \( (:?[^\n"()]* (?: (?<! \n) \n (?! \n) | (?<! \\) (?:\\\\)* \\ [()] )? )* \)
)
)?
[ \t]*(:?\r\n|\n|\r|$)
/gx
) {
my
(
$ref
,
$target
,
$title
) =
@LAST_PAREN_MATCH
{
qw(LABEL TARGET TITLE)
};
$ref
= normalize_label(
$ref
);
if
(
$ref
ne
''
) {
$this
->_finalize_paragraph(1);
if
(
exists
$this
->{linkrefs}{
$ref
}) {
carp
'Only the first appearance of a link reference definition is kept'
if
$this
->get_warn_for_unused_input();
return
1;
}
if
(
defined
$title
) {
$title
=~ s/^.(.*).$/$1/s;
_unescape_char(
$title
);
}
$target
=~ s/^<(.*)>$/$1/;
_unescape_char(
$target
);
$this
->{linkrefs}{
$ref
} = {
target
=>
$target
,
(
defined
$title
? (
'title'
=>
$title
) : ())
};
return
1;
}
}
$this
->set_pos(
$init_pos
,
$last_pos
);
return
;
}
sub
_do_table_block {
my
(
$this
) =
@_
;
my
$i
= !!@{
$this
->{paragraph}};
return
if
$i
&& !
$this
->get_table_blocks_can_interrupt_paragraph;
my
$m
=
$this
->get_table_blocks_pipes_requirements;
if
(
$m
eq
'strict'
|| (
$m
eq
'loose'
&&
$i
)) {
return
unless
$l
=~ m/^ {0,3}\|/;
}
else
{
return
unless
$l
=~ m/ (?<! \\) (?:\\\\)* \| /x;
}
my
$last_pos
=
$this
->{last_pos};
my
$init_pos
=
$this
->get_pos();
$this
->redo_line();
my
$table
=
$this
->_parse_table_structure();
if
(!
$table
) {
$this
->set_pos(
$init_pos
,
$last_pos
);
return
;
}
$this
->_add_block({
type
=>
'table'
,
content
=>
$table
});
return
1;
}
sub
_parse_table_structure {
my
(
$this
) =
@_
;
my
$m
=
$this
->get_table_blocks_pipes_requirements;
my
$i
= !!@{
$this
->{paragraph}};
my
$e
=
qr/(?<! \\) (?:\\\\)*/
x;
my
$cont
=
$this
->{continuation_re};
confess
'Unexpected regex match failure'
unless
$this
->{md} =~ m/\G${cont}/g;
pos
(
$this
->{md}) =
pos
(
$this
->{md});
confess
'Unexpected missing table markers'
unless
$this
->{md} =~ m/\G (\ {0,3}) (\|)?/gcx;
my
$n
=
length
($1) + 3;
my
$has_pipe
=
defined
$2;
my
@headers
=
$this
->{md} =~ m/\G [ \t]* (.*? [ \t]*
$e
) \| /gcx;
return
unless
@headers
;
return
unless
$this
->{md} =~ m/\G [ \t]* (.+)? [ \t]* ${eol_re} /gcx;
if
(
defined
$1) {
push
@headers
, $1;
$has_pipe
= 0;
}
return
if
(
$m
eq
'strict'
|| (
$m
eq
'loose'
&&
$i
) ||
@headers
== 1) && !
$has_pipe
;
return
unless
$this
->{md} =~ m/\G ${cont} \ {0,
$n
} (\|)? /gx;
$has_pipe
&&=
defined
$1;
my
@separators
=
$this
->{md} =~ m/\G [ \t]* ( :? -+ :? ) [ \t]* \| /gcx;
return
unless
$this
->{md} =~ m/\G [ \t]* (:? -+ :?)? [ \t]* (:? ${eol_re} | $ ) /gcx;
if
(
defined
$1) {
push
@separators
, $1;
$has_pipe
= 0;
}
return
unless
@separators
==
@headers
;
my
@align
=
map
{ s/^(:)?-+(:)?$/ $1 ? ($2 ?
'center'
:
'left'
) : ($2 ?
'right'
:
''
) /er }
@separators
;
return
if
(
$m
eq
'strict'
|| (
$m
eq
'loose'
&&
$i
) ||
@headers
== 1) && !
$has_pipe
;
return
if
$m
ne
'lax'
&&
@headers
== 1 && !
$has_pipe
;
return
if
$m
ne
'lax'
&& !
$has_pipe
&& min(
map
{
length
}
@separators
) < 2;
my
@table
;
while
(1) {
last
if
pos
(
$this
->{md}) ==
length
(
$this
->{md});
last
unless
$this
->{md} =~ m/\G ${cont} \ {0,
$n
} (\|)? /gcx;
$has_pipe
&&=
defined
$1;
last
if
!
defined
$1 &&
$this
->{md} =~ m/\G (?: [ ] | > | ${list_item_marker_re} )/x;
my
@cells
=
$this
->{md} =~ m/\G [ \t]* (.*? [ \t]*
$e
) \| /gcx;
pos
(
$this
->{md}) =
pos
(
$this
->{md});
confess
'Unexpected match failure'
unless
$this
->{md} =~ m/\G [ \t]* (.+)? [ \t]* (?: ${eol_re} | $ ) /gcx;
if
(
defined
$1) {
push
@cells
, $1;
$has_pipe
= 0;
}
last
unless
@cells
;
if
(
@cells
!=
@headers
) {
$#cells
=
@headers
- 1;
carp
'Excess cells in table row are ignored'
if
@cells
>
@headers
&&
$this
->get_warn_for_unused_input();
}
push
@table
, [
map
{
defined
? s/(
$e
)\\\|/${1}|/gr :
undef
}
@cells
];
}
return
{
headers
=> \
@headers
,
align
=> \
@align
,
table
=> \
@table
};
}
sub
_do_paragraph {
my
(
$this
) =
@_
;
if
(
$l
!~ m/^[ \t]*$/) {
push
@{
$this
->{paragraph}},
$l
;
return
1;
}
$this
->_finalize_paragraph();
$this
->{last_line_is_blank} =
!@{
$this
->{blocks_stack}} ||
$this
->{blocks_stack}[-1]{block}{type} ne
'quotes'
;
return
1;
}
sub
_unescape_char {
$_
[0] =~ s/\\(\p{PosixPunct})/$1/g;
return
;
}
1;