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> &amp; owing</text></doc>',



( run in 0.654 second using v1.01-cache-2.11-cpan-8dfa8b56332 )