diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java b/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java index 055ea5e490..50b56d6b65 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/HTMLParser.java @@ -139,6 +139,8 @@ public static RuntimeList _alloc_pstate(RuntimeArray args, int ctx) { pstate.put("_eof", scalarFalse); pstate.put("_started", scalarFalse); pstate.put("_buf", new RuntimeScalar("")); + pstate.put("_literal_mode", new RuntimeScalar("")); + pstate.put("_pending_end_tag", new RuntimeScalar("")); pstate.put("_bool_attr_val", scalarUndef); pstate.put("_in_cdata", scalarFalse); @@ -227,12 +229,7 @@ public static RuntimeList parserEof(RuntimeArray args, int ctx) { pstate.put("_started", scalarTrue); fireEvent(self, selfHash, pstate, "start_document"); } - // Flush any remaining buffered text - String remaining = pstate.get("_buf").toString(); - if (!remaining.isEmpty()) { - pstate.put("_buf", new RuntimeScalar("")); - parseHtml(self, selfHash, pstate, remaining); - } + flushBufferedAtEof(self, selfHash, pstate); // Fire end_document event fireEvent(self, selfHash, pstate, "end_document"); pstate.put("_started", scalarFalse); @@ -448,6 +445,16 @@ private static RuntimeHash getPstate(RuntimeHash selfHash) { * @param eventArgs the event-specific arguments */ private static void fireEvent(RuntimeScalar self, RuntimeHash selfHash, RuntimeHash pstate, String eventName, RuntimeScalar... eventArgs) { + RuntimeScalar pendingEndTag = pstate.get("_pending_end_tag"); + if (pendingEndTag != null && pendingEndTag.getDefinedBoolean() + && !pendingEndTag.toString().isEmpty() + && !eventName.equals("text") && !eventName.equals("comment")) { + String tagName = pendingEndTag.toString(); + pstate.put("_pending_end_tag", new RuntimeScalar("")); + fireEvent(self, selfHash, pstate, "end", + new RuntimeScalar(tagName), new RuntimeScalar("")); + } + RuntimeHash handlers = pstate.get("_handlers").hashDeref(); RuntimeScalar cb = handlers.get(eventName + "_cb"); String handlerName = eventName; @@ -838,6 +845,33 @@ private static void parseHtml(RuntimeScalar self, RuntimeHash selfHash, RuntimeH int i = 0; int textStart = 0; + String literalMode = pstate.get("_literal_mode").toString(); + if (!literalMode.isEmpty()) { + int endIdx = findLiteralEnd(pstate, html, literalMode, 0); + if (endIdx < 0) { + pstate.put("_buf", new RuntimeScalar(html)); + return; + } + + int endTagEnd = html.indexOf('>', endIdx); + if (endTagEnd < 0) { + pstate.put("_buf", new RuntimeScalar(html)); + return; + } + + if (endIdx > 0) { + fireEvent(self, selfHash, pstate, "text", + new RuntimeScalar(html.substring(0, endIdx))); + } + endTagEnd++; + fireEvent(self, selfHash, pstate, "end", + new RuntimeScalar(literalMode), + new RuntimeScalar(html.substring(endIdx, endTagEnd))); + pstate.put("_literal_mode", new RuntimeScalar("")); + i = endTagEnd; + textStart = i; + } + while (i < len) { if (html.charAt(i) == '<') { // Flush pending text @@ -1093,17 +1127,7 @@ private static void parseHtml(RuntimeScalar self, RuntimeHash selfHash, RuntimeH || tagName.equals("xmp") || tagName.equals("listing") || tagName.equals("plaintext") || tagName.equals("textarea") || tagName.equals("title"))) { - String endTag = " when looking for end tag - endIdx = findEndTagSkippingCdata(html, endTag, i); - } else { - endIdx = findCaseInsensitive(html, endTag, i); - } + int endIdx = findLiteralEnd(pstate, html, tagName, i); if (endIdx >= 0) { // Emit raw content as text if (endIdx > i) { @@ -1124,9 +1148,10 @@ private static void parseHtml(RuntimeScalar self, RuntimeHash selfHash, RuntimeH return; } } else { - // No closing tag found - buffer everything for next parse() - pstate.put("_buf", new RuntimeScalar(html.substring(tagStart))); - // Re-emit the start tag on next parse when we have the full content + // The start event has already fired. Preserve literal mode and only + // buffer its content so a later chunk cannot emit the start twice. + pstate.put("_literal_mode", new RuntimeScalar(tagName)); + pstate.put("_buf", new RuntimeScalar(html.substring(i))); return; } } @@ -1145,6 +1170,51 @@ private static void parseHtml(RuntimeScalar self, RuntimeHash selfHash, RuntimeH } } + private static int findLiteralEnd(RuntimeHash pstate, String html, String tagName, int fromIndex) { + String endTag = " regardless of case. diff --git a/src/test/resources/unit/html_parser_literal_eof.t b/src/test/resources/unit/html_parser_literal_eof.t new file mode 100644 index 0000000000..3e719858bf --- /dev/null +++ b/src/test/resources/unit/html_parser_literal_eof.t @@ -0,0 +1,47 @@ +use strict; +use warnings; + +use HTML::Form; +use HTML::TokeParser; +use Test::More; + +my $html = <<'HTML'; + +Fixed document</head> +<form> +<table> +<tr><select name="foo"> +<option value="bar">Bar</option></select></td></tr> +</form> +</html> +HTML + +my $parser = HTML::TokeParser->new(\$html); +my @events; +while (my $token = $parser->get_token) { + my $type = $token->[0]; + if ($type eq 'S' || $type eq 'E') { + push @events, "$type:$token->[1]"; + } + elsif ($type eq 'T' && $token->[1] =~ /\S/) { + my $text = $token->[1]; + $text =~ s/^\s+|\s+$//g; + push @events, "T:$text"; + } +} + +is_deeply( + \@events, + [ + 'S:html', 'S:head', 'S:title', 'T:Fixed document', 'E:title', + 'E:head', 'S:form', 'S:table', 'S:tr', 'S:select', 'S:option', + 'T:Bar', 'E:option', 'E:select', 'E:td', 'E:tr', 'E:form', + 'E:html', + ], + 'unterminated title closes at EOF and the remaining markup is tokenized', +); + +my @forms = HTML::Form->parse($html, base => 'http://localhost/'); +is(scalar @forms, 1, 'form after an unterminated title is discovered'); + +done_testing;