PDF-Make
view release on metacpan or search on metacpan
t/50-markup-parse.t view on Meta::CPAN
'mismatched close names both tags and the opening line');
# A missing </row> is discovered when </doc> arrives, and saying which
# element it actually closed is more use than "unclosed" on its own.
parse_err(qq{<doc>\n <row>\n <cell>x</cell>\n</doc>},
qr{</doc> closes <row> opened at line 2},
'a forgotten close is reported against the tag that found it');
parse_err(qq{<doc>\n <row>\n <cell>x</cell>},
qr/unclosed <row> opened at line 2/,
'and at end of input it is reported as unclosed');
parse_err('<doc><h1 class=big>x</h1></doc>',
qr/must be quoted/, 'unquoted attribute value');
parse_err('<doc><h1 hidden>x</h1></doc>',
qr/has no value/, 'valueless attribute, with the fix in the message');
parse_err('<doc><h1 a="1" a="2">x</h1></doc>',
qr/duplicate attribute 'a'.*first at line 1/,
'a repeated attribute is refused rather than silently resolved');
parse_err('<doc><img src="a<b" /></doc>',
qr/'<' inside the value of attribute/, 'a raw < inside an attribute value');
parse_err('<doc><h1 a="unterminated></doc>',
qr/opening quote is probably unclosed/,
'an unclosed quote leads with the likely cause, not the rare one');
parse_err('<doc><h1 a="unterminated',
qr/unterminated value/,
'and at end of input it is reported as unterminated');
};
subtest 'the root must be doc' => sub {
parse_err('<h1>x</h1>', qr/root element must be <doc>/, 'wrong root');
parse_err('', qr/empty document/, 'empty input');
parse_err(' ', qr/empty document/, 'whitespace only');
parse_err('hello', qr/text outside <doc>/, 'bare text');
parse_err('<doc></doc><doc></doc>', qr/content after <\/doc>/,
'a second root');
parse_err('<doc></doc> trailing', qr/content after <\/doc>/,
'trailing text');
};
subtest 'declarations and instructions are not part of the language' => sub {
parse_err('<?xml version="1.0"?><doc></doc>', qr/root element must be <doc>/,
'an XML declaration is not accepted');
parse_err('<doc><!DOCTYPE html></doc>', qr/not part of this markup/,
'nor a doctype');
};
subtest 'depth is bounded' => sub {
my $deep = '<doc>' . ('<box>' x 70) . 'x' . ('</box>' x 70) . '</doc>';
parse_err($deep, qr/nested deeper than 64/, 'deep nesting is refused');
my $ok = '<doc>' . ('<box>' x 40) . 'x' . ('</box>' x 40) . '</doc>';
parse_ok($ok, 'nesting within the limit is fine');
};
subtest 'a UTF-8 BOM is tolerated' => sub {
parse_ok("\xEF\xBB\xBF<doc><h1>x</h1></doc>", 'BOM skipped (bytes)');
parse_ok("\x{FEFF}<doc><h1>x</h1></doc>", 'BOM skipped (characters)');
};
subtest 'characters in, characters out, whichever way they arrive' => sub {
my $want = "caf\x{e9} \x{20ac}1,240 \x{4e2d}\x{6587}";
my $chars = "<doc><text>$want</text></doc>";
my $bytes = $chars;
utf8::encode($bytes);
for my $case (['character string', $chars], ['UTF-8 bytes', $bytes]) {
my ($name, $src) = @$case;
my $root = parse_ok($src, "parses a $name");
my $got = $root->{children}[0]{children}[0]{text};
is $got, $want, " round-trips through a $name";
ok utf8::is_utf8($got), ' and comes back as characters';
}
};
subtest 'invalid UTF-8 is refused with a position' => sub {
parse_err("<doc><text>bad \xff byte</text></doc>",
qr/invalid UTF-8: unexpected byte 0xFF/,
'a stray byte', line => 1, col => 16);
parse_err("<doc><text>caf\xC3",
qr/truncated sequence/, 'a sequence cut off by end of input');
parse_err("<doc><text>\xC3</text></doc>",
qr/not a continuation/,
'a lead byte followed by something else names that instead');
parse_err("<doc><text>\xC0\xAF</text></doc>",
qr/overlong encoding/, 'an overlong form');
parse_err("<doc><text>\xED\xA0\x80</text></doc>",
qr/encoded surrogate/, 'a surrogate encoded as UTF-8');
parse_err("<doc><text>\xE2\x28\xA1</text></doc>",
qr/not a continuation/, 'a bad continuation byte');
};
# ---- the tag table ----------------------------------------------------------
subtest 'the tag table comes from the parser' => sub {
my @tags = PDF::Make::Markup::Parse->tags;
ok scalar @tags >= 25, 'the set is published';
my %by = map { $_->{name} => $_ } @tags;
ok $by{doc}{container}, 'doc is a container';
ok $by{hr}{void}, 'hr is void';
ok $by{b}{inline}, 'b is inline';
ok !$by{h1}{container}, 'h1 is not a container';
for my $t (@tags) {
my $r = PDF::Make::Markup::Parse->check(
$t->{name} eq 'doc' ? '<doc></doc>'
: "<doc><$t->{name}/></doc>");
ok $r->{ok}, "<$t->{name}> is accepted by the parser it came from"
or diag $r->{error};
}
};
# ---- hostile input ----------------------------------------------------------
subtest 'malformed input never crashes, always explains' => sub {
my @seeds = (
qq{<doc size="A4">\n<h1>Invoice</h1>\n<table><tr><td>a</td></tr></table>\n</doc>},
'<doc><text>Total <b>due</b> & owing</text></doc>',
( run in 0.654 second using v1.01-cache-2.11-cpan-8dfa8b56332 )