diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81cd5af..4cef6d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,24 +8,15 @@ jobs: fail-fast: false matrix: ocaml: - - 4.13.1 - - 4.12.1 - - 4.11.2 - - 4.10.2 - - 4.09.1 - - 4.08.1 - - 4.07.1 - - 4.06.1 - - 4.05.0 - - 4.04.2 - - 4.03.0 + - 5.4.1 + - 4.14.2 steps: - - uses: actions/checkout@v2 - - uses: ocaml/setup-ocaml@v2 + - uses: actions/checkout@v4 + - uses: ocaml/setup-ocaml@v3 with: ocaml-compiler: ${{matrix.ocaml}} - - run: sudo apt-get install python-bs4 + - run: sudo apt-get install python3-bs4 - run: opam install --deps-only --with-test . --yes - run: opam install js_of_ocaml --yes @@ -34,7 +25,7 @@ jobs: - run: opam exec -- make dependency-test - run: opam lint - - if: ${{matrix.ocaml == '4.13.1'}} + - if: ${{matrix.ocaml == '5.4.1'}} env: COVERALLS_REPO_TOKEN: ${{secrets.GITHUB_TOKEN}} PULL_REQUEST_NUMBER: ${{github.event.number}} diff --git a/.gitignore b/.gitignore index cfd064a..9d11928 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ scratch/ _opam/ _build/ +_build-*/ +_fuzz/ +_tools/ bisect*.out _coverage *.install diff --git a/Makefile b/Makefile index 183a70b..e14fc68 100644 --- a/Makefile +++ b/Makefile @@ -2,22 +2,108 @@ build : dune build -p markup,markup-lwt +.PHONY : format +format : + dune build @src/lite/fmt @test/lite/fmt --auto-promote || dune build @src/lite/fmt + # This is not part of the ordinary build process. The output file, entities.ml, # is checked into git. .PHONY : entities entities : - dune exec src/translate_entities/translate_entities.exe \ - > src/entities.ml + dune exec src/entities/translate_entities/translate_entities.exe \ + > src/entities/entities.ml + +.PHONY : lite-ragel +lite-ragel : + cd src/lite && ragel-ocaml -L -F1 \ + -o ragel_html_tokenizer.ml ragel_html_tokenizer.ml.rl + python3 -c 'from pathlib import Path; p = Path("src/lite/ragel_html_tokenizer.ml"); p.write_text("\n".join(line.rstrip() for line in p.read_text().splitlines()) + "\n")' + rm -f src/lite/ragel_html_tokenizer.ri .PHONY : test test : dune runtest +LITE_TEST_EXE := _build/default/test/lite/lite_diff_corpus.exe +LITE_COUNT_TEST_EXE := _build/default/test/lite/lite_count_corpus.exe +LITE_PARSER_TEST_EXE := _build/default/test/lite/lite_parser_diff_corpus.exe +LITE_WRITER_TEST_EXE := _build/default/test/lite/lite_writer_diff_corpus.exe +LITE_TEST_CORPUS ?= big_tests + +.PHONY : test-lite +test-lite : + dune build --profile release test/lite/lite_diff_corpus.exe \ + test/lite/lite_count_corpus.exe \ + test/lite/lite_parser_diff_corpus.exe \ + test/lite/lite_writer_diff_corpus.exe + $(LITE_TEST_EXE) $(LITE_TEST_CORPUS) + $(LITE_COUNT_TEST_EXE) $(LITE_TEST_CORPUS) + $(LITE_PARSER_TEST_EXE) $(LITE_TEST_CORPUS) + $(LITE_WRITER_TEST_EXE) $(LITE_TEST_CORPUS) + +LITE_AFL_MODE ?= diff +ifeq ($(LITE_AFL_MODE),diff) +LITE_AFL_TARGET := test/fuzz/lite_diff_fuzz.exe +LITE_AFL_EXE := _build-afl/default/test/fuzz/lite_diff_fuzz.exe +LITE_AFL_DEFAULT_OUTPUT := _fuzz/lite +else ifeq ($(LITE_AFL_MODE),native) +LITE_AFL_TARGET := test/fuzz/lite_native_fuzz.exe +LITE_AFL_EXE := _build-afl/default/test/fuzz/lite_native_fuzz.exe +LITE_AFL_DEFAULT_OUTPUT := _fuzz/lite-native +else +$(error LITE_AFL_MODE must be diff or native) +endif +LITE_AFL_OUTPUT ?= $(LITE_AFL_DEFAULT_OUTPUT) +AFL_FUZZ ?= $(if $(wildcard _tools/AFLplusplus/afl-fuzz),_tools/AFLplusplus/afl-fuzz,afl-fuzz) +AFL_WHATSUP ?= $(if $(wildcard _tools/AFLplusplus/afl-whatsup),_tools/AFLplusplus/afl-whatsup,afl-whatsup) +J ?= 4 + +.PHONY : test-lite-afl +test-lite-afl : + @command -v $(AFL_FUZZ) >/dev/null || { \ + echo "$(AFL_FUZZ) not found; install AFL or set AFL_FUZZ" >&2; exit 1; } + dune build --build-dir _build-afl --profile afl $(LITE_AFL_TARGET) + @set -eu; \ + echo "fuzzing mode: $(LITE_AFL_MODE)"; \ + case "$(J)" in ''|*[!0-9]*|0) echo "J must be a positive integer" >&2; exit 2;; esac; \ + mkdir -p $(LITE_AFL_OUTPUT); \ + pids=''; \ + cleanup () { \ + trap - EXIT INT TERM; \ + if test -n "$$pids"; then kill $$pids 2>/dev/null || true; fi; \ + wait 2>/dev/null || true; \ + }; \ + trap cleanup EXIT INT TERM; \ + i=0; \ + while test $$i -lt $(J); do \ + id=$$(printf 'fuzzer%02d' $$i); \ + if test $$i -eq 0; then role=-M; else role=-S; fi; \ + input=test/fuzz/seeds; \ + if test -d $(LITE_AFL_OUTPUT)/$$id/queue; then input=-; fi; \ + echo "starting AFL worker $$id"; \ + AFL_NO_UI=1 AFL_NO_AFFINITY=1 AFL_SKIP_CPUFREQ=1 \ + AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \ + $(AFL_FUZZ) -i $$input -o $(LITE_AFL_OUTPUT) \ + -x test/fuzz/html.dict $$role $$id -- $(LITE_AFL_EXE) \ + >$(LITE_AFL_OUTPUT)/$$id.log 2>&1 & \ + pids="$$pids $$!"; \ + i=$$((i + 1)); \ + done; \ + wait + +.PHONY : test-lite-afl-native +test-lite-afl-native : + $(MAKE) test-lite-afl LITE_AFL_MODE=native + +.PHONY : test-lite-afl-report +test-lite-afl-report : + $(AFL_WHATSUP) -d $(LITE_AFL_OUTPUT) + .PHONY : coverage coverage : find . -name '*.coverage' | xargs rm -f dune runtest --instrument-with bisect_ppx --force - bisect-ppx-report html --expect src/ --do-not-expect src/translate_entities/ + bisect-ppx-report html --expect src/ --do-not-expect src/entities/translate_entities/ bisect-ppx-report summary @echo See _coverage/index.html diff --git a/dune b/dune new file mode 100644 index 0000000..f819bdc --- /dev/null +++ b/dune @@ -0,0 +1,4 @@ +(env + (afl + (flags + (:standard -warn-error -A)))) diff --git a/markup-lwt.opam b/markup-lwt.opam index 21e5ac2..94bdf7e 100644 --- a/markup-lwt.opam +++ b/markup-lwt.opam @@ -16,7 +16,7 @@ depends: [ "dune" {>= "2.7.0"} "lwt" "markup" - "ocaml" {>= "4.03.0"} + "ocaml" {>= "4.14.0"} ] build: [ diff --git a/markup.opam b/markup.opam index 073f164..dcf8236 100644 --- a/markup.opam +++ b/markup.opam @@ -13,11 +13,13 @@ dev-repo: "git+https://github.com/aantron/markup.ml.git" depends: [ "dune" {>= "2.7.0"} - "ocaml" {>= "4.03.0"} + "ocaml" {>= "4.14.0"} "uchar" "uutf" {>= "1.0.0"} "bisect_ppx" {dev & >= "2.5.0"} + "containers" {with-test} + "devkit" {with-test} "ounit2" {dev} ] # Markup.ml implicitly requires OCaml 4.02.3, as this is a contraint of Dune. diff --git a/src/.ocamlformat-ignore b/src/.ocamlformat-ignore new file mode 100644 index 0000000..812c264 --- /dev/null +++ b/src/.ocamlformat-ignore @@ -0,0 +1,6 @@ +*.ml +*.mli +common/** +entities/** +lwt/** +lwt_unix/** diff --git a/src/common.ml b/src/baseline/common.ml similarity index 63% rename from src/common.ml rename to src/baseline/common.ml index faa36f0..7fd90ff 100644 --- a/src/common.ml +++ b/src/baseline/common.ml @@ -4,21 +4,18 @@ type 'a cont = 'a -> unit type 'a cps = exn cont -> 'a cont -> unit -type location = int * int +type location = Markup_common.location -let compare_locations (line, column) (line', column') = - match line - line' with - | 0 -> column - column' - | order -> order +let compare_locations = Markup_common.compare_locations -type name = string * string +type name = Markup_common.name -let xml_ns = "http://www.w3.org/XML/1998/namespace" -let xmlns_ns = "http://www.w3.org/2000/xmlns/" -let xlink_ns = "http://www.w3.org/1999/xlink" -let html_ns = "http://www.w3.org/1999/xhtml" -let svg_ns = "http://www.w3.org/2000/svg" -let mathml_ns = "http://www.w3.org/1998/Math/MathML" +let xml_ns = Markup_common.Ns.xml +let xmlns_ns = Markup_common.Ns.xmlns +let xlink_ns = Markup_common.Ns.xlink +let html_ns = Markup_common.Ns.html +let svg_ns = Markup_common.Ns.svg +let mathml_ns = Markup_common.Ns.mathml module Token_tag = struct @@ -28,26 +25,21 @@ struct self_closing : bool} end -type xml_declaration = - {version : string; - encoding : string option; - standalone : bool option} - -type doctype = - {doctype_name : string option; - public_identifier : string option; - system_identifier : string option; - raw_text : string option; - force_quirks : bool} - -type signal = - [ `Start_element of name * (name * string) list - | `End_element - | `Text of string list - | `Xml of xml_declaration - | `Doctype of doctype - | `PI of string * string - | `Comment of string ] +type xml_declaration = Markup_common.xml_declaration = { + version : string; + encoding : string option; + standalone : bool option; +} + +type doctype = Markup_common.doctype = { + doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool; +} + +type signal = Markup_common.signal type general_token = [ `Xml of xml_declaration @@ -147,64 +139,7 @@ let is_valid_xml_char c = || is_in_range 0xE000 0xFFFD c || is_in_range 0x10000 0x10FFFF c -let signal_to_string = function - | `Comment s -> - Printf.sprintf "" s - - | `Doctype d -> - let text = - match d.doctype_name with - | None -> - begin match d.raw_text with - | None -> "" - | Some s -> " " ^ s - end - | Some name -> - match d.public_identifier, d.system_identifier with - | None, None -> " " ^ name - | Some p, None -> Printf.sprintf " %s PUBLIC \"%s\"" name p - | None, Some s -> Printf.sprintf " %s SYSTEM \"%s\"" name s - | Some p, Some s -> Printf.sprintf " %s PUBLIC \"%s\" \"%s\"" name p s - in - Printf.sprintf "" text - - | `Start_element (name, attributes) -> - let name_to_string = function - | "", local_name -> local_name - | ns, local_name -> ns ^ ":" ^ local_name - in - let attributes = - attributes - |> List.map (fun (name, value) -> - Printf.sprintf " %s=\"%s\"" (name_to_string name) value) - |> String.concat "" - in - Printf.sprintf "<%s%s>" (name_to_string name) attributes - - | `End_element -> - "" - - | `Text ss -> - String.concat "" ss - - | `Xml x -> - let s = Printf.sprintf "" x.version in - let s = - match x.encoding with - | None -> s - | Some encoding -> Printf.sprintf "%s encoding=\"%s\"" s encoding - in - let s = - match x.standalone with - | None -> s - | Some standalone -> - Printf.sprintf - "%s standalone=\"%s\"" s (if standalone then "yes" else "no") - in - s ^ "?>" - - | `PI (target, s) -> - Printf.sprintf "" target s +let signal_to_string = Markup_common.signal_to_string let token_to_string = function | `Xml x -> diff --git a/src/detect.ml b/src/baseline/detect.ml similarity index 100% rename from src/detect.ml rename to src/baseline/detect.ml diff --git a/src/detect.mli b/src/baseline/detect.mli similarity index 100% rename from src/detect.mli rename to src/baseline/detect.mli diff --git a/src/dune b/src/baseline/dune similarity index 79% rename from src/dune rename to src/baseline/dune index cb22a6a..7ea458c 100644 --- a/src/dune +++ b/src/baseline/dune @@ -4,7 +4,7 @@ (synopsis "Error-recovering functional HTML5 and XML parsers") (instrumentation (backend bisect_ppx)) - (libraries devkit uutf) + (libraries uutf markup.common markup.entities) (flags (:standard -w -9))) diff --git a/src/encoding.ml b/src/baseline/encoding.ml similarity index 100% rename from src/encoding.ml rename to src/baseline/encoding.ml diff --git a/src/baseline/entities.ml b/src/baseline/entities.ml new file mode 100644 index 0000000..d18a4a7 --- /dev/null +++ b/src/baseline/entities.ml @@ -0,0 +1,6 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* The entity table lives in the markup.entities library. *) + +include Markup_entities.Entities diff --git a/src/baseline/error.ml b/src/baseline/error.ml new file mode 100644 index 0000000..85d1068 --- /dev/null +++ b/src/baseline/error.ml @@ -0,0 +1,16 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +include Markup_common.Error + +open Common + +type 'a handler = 'a -> t -> unit cps +type parse_handler = location handler +type write_handler = (signal * int) handler + +let ignore_errors _ _ _ resume = resume () + +let report_if report condition location detail throw k = + if condition then report location (detail ()) throw k + else k () diff --git a/src/html_parser.ml b/src/baseline/html_parser.ml similarity index 99% rename from src/html_parser.ml rename to src/baseline/html_parser.ml index b7b3bbf..1758996 100644 --- a/src/html_parser.ml +++ b/src/baseline/html_parser.ml @@ -5,7 +5,9 @@ open Common open Token_tag open Kstream - +(* HTML space characters include U+000C, unlike XML whitespace. *) +let is_whitespace_only s = + String.for_all (fun c -> c = '\x0c' || is_whitespace (int_of_char c)) s (* Namespaces for pattern matching. *) type ns = [ `HTML | `MathML | `SVG | `Other of string ] @@ -726,6 +728,7 @@ sig type t val create : Stack.t -> t + val buffering : t -> bool val accumulate : t -> location -> signal -> bool @@ -746,6 +749,8 @@ struct enabled = false; position = Element.dummy} + let buffering subtree_buffer = subtree_buffer.enabled + let accumulate subtree_buffer l s = if not subtree_buffer.enabled then true else begin @@ -2472,6 +2477,7 @@ let parse ?depth_limit requested_context report (tokens, set_tokenizer_state, se close_cell l (fun () -> Active.clear_until_marker active_formatting_elements; push tokens v; + current_mode := in_row_mode; in_row_mode ()) | l, `End {name = @@ -2486,6 +2492,7 @@ let parse ?depth_limit requested_context report (tokens, set_tokenizer_state, se close_cell l (fun () -> Active.clear_until_marker active_formatting_elements; push tokens v; + current_mode := in_row_mode; in_row_mode ()) | l, `Start ({name = "select"} as t) -> @@ -2856,6 +2863,14 @@ let parse ?depth_limit requested_context report (tokens, set_tokenizer_state, se add_character l u_rep; mode ()) + | l, `String s when s <> "" && Subtree.buffering subtree_buffer -> + let decoded = String.get_utf_8_uchar s 0 in + let width = Uchar.utf_decode_length decoded in + if width < String.length s then + push tokens (l, `String (String.sub s width (String.length s - width))); + foreign_content mode force_html + (l, `Char (Uchar.to_int (Uchar.utf_decode_uchar decoded))) + | l, `String s -> add_string l s; if not @@ is_whitespace_only s then frameset_ok := false; diff --git a/src/html_parser.mli b/src/baseline/html_parser.mli similarity index 100% rename from src/html_parser.mli rename to src/baseline/html_parser.mli diff --git a/src/html_tokenizer.ml b/src/baseline/html_tokenizer.ml similarity index 96% rename from src/html_tokenizer.ml rename to src/baseline/html_tokenizer.ml index d89c7a0..b00efd3 100644 --- a/src/html_tokenizer.ml +++ b/src/baseline/html_tokenizer.ml @@ -1527,65 +1527,3 @@ let tokenize report (input, get_location) = let set_foreign = ( := ) foreign in (stream, set_state, set_foreign) - -module Ragel = struct - open Devkit - module HS = HtmlStream - - let decode raw = - let inner = HS.Raw.project raw in - try Web.htmldecode inner with _ -> inner - - let tokenize html = - let ctx = HS.init () in - let tokens = ref [] in - let tuck_with_location ~ctx token = - tuck tokens ((HS.get_lnum ctx, -1), token) - in - let call = function - | HS.Text raw -> tuck_with_location ~ctx (`String (decode raw)) - | Tag (name, attrs) -> - tuck_with_location ~ctx - (`Start - { - name; - attributes = List.rev_map (fun (k, v) -> (k, decode v)) attrs; - (* TODO: HS calls close tag immediately after open tag if tag is self closing; check if don't distinguish it changes anything *) - self_closing = false; - }) - | Close "br" -> - (* given
, HS genrates Tag ("br", ..) and Close "br", - but on the latter parser will detect unmatched end tag and insert opening, causing double
*) - () - | Close name -> - tuck_with_location ~ctx - (`End { name; attributes = []; self_closing = false }) - | Script (attrs, inner) -> - (* TODO: see if can remove these *) - tuck_with_location ~ctx - (`Start - { - name = "script"; - attributes = List.rev_map (fun (k, v) -> (k, decode v)) attrs; - self_closing = false; - }); - tuck_with_location ~ctx (`String (inner)); - tuck_with_location ~ctx - (`End { name = "script"; attributes = []; self_closing = false }) - | Style (attrs, inner) -> - tuck_with_location ~ctx - (`Start - { - name = "style"; - attributes = List.rev_map (fun (k, v) -> (k, decode v)) attrs; - self_closing = false; - }); - tuck_with_location ~ctx (`String (inner)); - tuck_with_location ~ctx - (`End { name = "style"; attributes = []; self_closing = false }) - in - - let () = HS.parse ~ctx call html in - tuck_with_location ~ctx `EOF; - Kstream.of_list (List.rev !tokens) -end diff --git a/src/html_tokenizer.mli b/src/baseline/html_tokenizer.mli similarity index 87% rename from src/html_tokenizer.mli rename to src/baseline/html_tokenizer.mli index 78e064e..7bf1c3f 100644 --- a/src/html_tokenizer.mli +++ b/src/baseline/html_tokenizer.mli @@ -20,8 +20,3 @@ val tokenize : (location * token) Kstream.t * (state -> unit) * ((unit -> bool) -> unit) - - -module Ragel : sig - val tokenize : string -> (location * token) Kstream.t -end \ No newline at end of file diff --git a/src/html_writer.ml b/src/baseline/html_writer.ml similarity index 100% rename from src/html_writer.ml rename to src/baseline/html_writer.ml diff --git a/src/html_writer.mli b/src/baseline/html_writer.mli similarity index 100% rename from src/html_writer.mli rename to src/baseline/html_writer.mli diff --git a/src/input.ml b/src/baseline/input.ml similarity index 86% rename from src/input.ml rename to src/baseline/input.ml index 136036e..7e9f17f 100644 --- a/src/input.ml +++ b/src/baseline/input.ml @@ -27,8 +27,11 @@ let preprocess is_valid_char report source = in let rec iterate () = - next source throw empty (function - | 0xFEFF when !first_char -> first_char := false; iterate () + next source throw empty (fun c -> + let was_first = !first_char in + first_char := false; + match c with + | 0xFEFF when was_first -> iterate () | 0x0D -> next source throw newline (function diff --git a/src/input.mli b/src/baseline/input.mli similarity index 100% rename from src/input.mli rename to src/baseline/input.mli diff --git a/src/baseline/kstream.ml b/src/baseline/kstream.ml new file mode 100644 index 0000000..6680a76 --- /dev/null +++ b/src/baseline/kstream.ml @@ -0,0 +1,6 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* See the comment in common.ml. *) + +include Markup_common.Kstream diff --git a/src/markup.ml b/src/baseline/markup.ml similarity index 64% rename from src/markup.ml rename to src/baseline/markup.ml index 15bc2ef..2bd34d4 100644 --- a/src/markup.ml +++ b/src/baseline/markup.ml @@ -24,13 +24,17 @@ module Synchronous : IO with type 'a t = 'a = struct let to_cps f throw k = match f () with v -> k v | exception exn -> throw exn end -type async = unit -type sync = unit -type ('data, 'sync) stream = 'data Kstream.t +type async = Markup_common.async +type sync = Markup_common.sync -let kstream s = s -let of_kstream s = s -let of_list = Kstream.of_list +(* [stream] is defined by the markup.common library, so that streams can be + exchanged with the other libraries built on it. [Kstream] is this library's + own stream implementation; both conversions below are the identity. *) +type ('data, 'sync) stream = ('data, 'sync) Markup_common.stream + +let kstream = Markup_common.Stream.Private.of_stream +let of_kstream = Markup_common.Stream.Private.to_stream +let of_list l = Kstream.of_list l |> of_kstream type location = Common.location @@ -60,10 +64,10 @@ let signal_to_string = Common.signal_to_string type 's parser = { mutable location : location; - mutable signals : (signal, 's) stream; + mutable signals : signal Kstream.t; } -let signals parser = parser.signals +let signals parser = of_kstream parser.signals let location parser = parser.location let stream_to_parser s = @@ -77,6 +81,7 @@ let stream_to_parser s = module Cps = struct let parse_xml report ?encoding namespace entity context source = + let source = kstream source in let with_encoding (encoding : Encoding.t) k = source |> encoding ~report |> Input.preprocess Common.is_valid_xml_char report @@ -96,14 +101,20 @@ module Cps = struct Kstream.construct constructor |> stream_to_parser let write_xml report prefix signals = - signals |> Xml_writer.write report prefix |> Utility.strings_to_bytes + signals |> kstream + |> Xml_writer.write report prefix + |> Utility.strings_to_bytes + |> of_kstream - let parse_html_ragel ?depth_limit report context source = - let tokens = Html_tokenizer.Ragel.tokenize source in - let signals = Html_parser.parse ?depth_limit context report (tokens, ignore, ignore) in + let parse_tokens ?depth_limit report context tokens = + let tokens = Kstream.of_list tokens in + let signals = + Html_parser.parse ?depth_limit context report (tokens, ignore, ignore) + in stream_to_parser signals let parse_html report ?depth_limit ?encoding context source = + let source = kstream source in let with_encoding (encoding : Encoding.t) k = source |> encoding ~report |> Input.preprocess Common.is_valid_html_char report @@ -119,26 +130,55 @@ module Cps = struct Detect.select_html source throw (fun encoding -> with_encoding encoding k) in - Kstream.construct constructor |> stream_to_parser let write_html ?escape_attribute ?escape_text signals = - signals + signals |> kstream |> Html_writer.write ?escape_attribute ?escape_text |> Utility.strings_to_bytes + |> of_kstream end -let string = Stream_io.string -let buffer = Stream_io.buffer -let channel = Stream_io.channel -let file = Stream_io.file -let to_channel c bytes = Stream_io.to_channel c bytes |> Synchronous.of_cps -let to_file f bytes = Stream_io.to_file f bytes |> Synchronous.of_cps +let string s = Stream_io.string s |> of_kstream +let buffer b = Stream_io.buffer b |> of_kstream +let channel c = Stream_io.channel c |> of_kstream -let preprocess_input_stream source = - Input.preprocess (fun _ -> true) Error.ignore_errors source +let file f = + let s, close = Stream_io.file f in + (of_kstream s, close) + +let to_channel c bytes = + Stream_io.to_channel c (kstream bytes) |> Synchronous.of_cps -include Utility +let to_file f bytes = Stream_io.to_file f (kstream bytes) |> Synchronous.of_cps + +let preprocess_input_stream source = + let signals, get_location = + Input.preprocess (fun _ -> true) Error.ignore_errors (kstream source) + in + (of_kstream signals, get_location) + +type 'a node = 'a Utility.node + +let content s = Utility.content (kstream s) |> of_kstream +let strings_to_bytes s = Utility.strings_to_bytes (kstream s) |> of_kstream +let text s = Utility.text (kstream s) |> of_kstream +let trim s = Utility.trim (kstream s) |> of_kstream +let normalize_text s = Utility.normalize_text (kstream s) |> of_kstream +let pretty_print s = Utility.pretty_print (kstream s) |> of_kstream +let html5 s = Utility.html5 (kstream s) |> of_kstream +let xhtml ?dtd s = Utility.xhtml ?dtd (kstream s) |> of_kstream +let xhtml_entity = Utility.xhtml_entity +let from_tree f v = Utility.from_tree f v |> of_kstream + +let trees ?text ?element ?comment ?pi ?xml ?doctype s = + Utility.trees ?text ?element ?comment ?pi ?xml ?doctype (kstream s) + |> of_kstream + +let elements f s = + Utility.elements f (kstream s) + |> Kstream.map (fun sub _ k -> k (of_kstream sub)) + |> of_kstream module Ns = struct let html = Common.html_ns @@ -231,7 +271,7 @@ module Asynchronous (IO : IO) = struct include Encoding let decode ?(report = fun _ _ -> IO.return ()) (f : Encoding.t) s = - f ~report:(wrap_report report) s + f ~report:(wrap_report report) (kstream s) |> of_kstream end let parse_xml ?(report = fun _ _ -> IO.return ()) ?encoding @@ -246,50 +286,74 @@ module Asynchronous (IO : IO) = struct ?depth_limit source = Cps.parse_html (wrap_report report) ?depth_limit ?encoding context source - let parse_html_ragel ?(report = fun _ _ -> IO.return ()) ?context ?depth_limit source = - Cps.parse_html_ragel ?depth_limit (wrap_report report) context source + (* callback into [TagStream.scan_ragel] *) + let parse_tokens ?(report = fun _ _ -> IO.return ()) ?context ?depth_limit + tokens = + Cps.parse_tokens ?depth_limit (wrap_report report) context tokens let write_html ?escape_attribute ?escape_text signals = Cps.write_html ?escape_attribute ?escape_text signals - let to_string bytes = Stream_io.to_string bytes |> IO.of_cps - let to_buffer bytes = Stream_io.to_buffer bytes |> IO.of_cps + let to_string bytes = Stream_io.to_string (kstream bytes) |> IO.of_cps + let to_buffer bytes = Stream_io.to_buffer (kstream bytes) |> IO.of_cps let stream f = let f = IO.to_cps f in (fun throw e k -> f throw (function None -> e () | Some v -> k v)) |> Kstream.make + |> of_kstream let fn = stream - let next s = Kstream.next_option s |> IO.of_cps - let peek s = Kstream.peek_option s |> IO.of_cps + let next s = Kstream.next_option (kstream s) |> IO.of_cps + let peek s = Kstream.peek_option (kstream s) |> IO.of_cps (* Without Flambda, thunks are repeatedly created and passed on IO.to_cps, resulting in a performance penalty. Flambda seems to optimize this away, however. *) let transform f v s = - Kstream.transform (fun v s -> IO.to_cps (fun () -> f v s)) v s + Kstream.transform (fun v s -> IO.to_cps (fun () -> f v s)) v (kstream s) + |> of_kstream let fold f v s = - Kstream.fold (fun v v' -> IO.to_cps (fun () -> f v v')) v s |> IO.of_cps + Kstream.fold (fun v v' -> IO.to_cps (fun () -> f v v')) v (kstream s) + |> IO.of_cps + + let map f s = + Kstream.map (fun v -> IO.to_cps (fun () -> f v)) (kstream s) |> of_kstream - let map f s = Kstream.map (fun v -> IO.to_cps (fun () -> f v)) s - let filter f s = Kstream.filter (fun v -> IO.to_cps (fun () -> f v)) s - let filter_map f s = Kstream.filter_map (fun v -> IO.to_cps (fun () -> f v)) s + let filter f s = + Kstream.filter (fun v -> IO.to_cps (fun () -> f v)) (kstream s) + |> of_kstream + + let filter_map f s = + Kstream.filter_map (fun v -> IO.to_cps (fun () -> f v)) (kstream s) + |> of_kstream let iter f s = - Kstream.iter (fun v -> IO.to_cps (fun () -> f v)) s |> IO.of_cps + Kstream.iter (fun v -> IO.to_cps (fun () -> f v)) (kstream s) |> IO.of_cps let drain s = iter (fun _ -> IO.return ()) s - let to_list s = Kstream.to_list s |> IO.of_cps + let to_list s = Kstream.to_list (kstream s) |> IO.of_cps let load s = - (fun throw k -> Kstream.to_list s throw (fun l -> k (Kstream.of_list l))) + (fun throw k -> + Kstream.to_list (kstream s) throw (fun l -> + k (of_kstream (Kstream.of_list l)))) |> IO.of_cps let tree ?text ?element ?comment ?pi ?xml ?doctype s = - Utility.tree ?text ?element ?comment ?pi ?xml ?doctype s |> IO.of_cps + Utility.tree ?text ?element ?comment ?pi ?xml ?doctype (kstream s) + |> IO.of_cps end include Asynchronous (Synchronous) + +module Internals = struct + include Common + module Token_tag = Common.Token_tag + + type token = Html_tokenizer.token + + let parse_tokens = parse_tokens +end diff --git a/src/markup.mli b/src/baseline/markup.mli similarity index 97% rename from src/markup.mli rename to src/baseline/markup.mli index c123404..9d0c928 100644 --- a/src/markup.mli +++ b/src/baseline/markup.mli @@ -86,14 +86,18 @@ val write_xml : signal stream -> char stream (** {2 Streams} *) -type async -type sync +type async = Markup_common.async +type sync = Markup_common.sync (** Phantom types for use with [('a, 's) stream] in place of ['s]. See explanation below. *) -type ('a, 's) stream +type ('a, 's) stream = ('a, 's) Markup_common.stream (** Streams of elements of type ['a]. + This is the same type as {!Markup_common.stream}, so that streams can be + exchanged with other libraries built on [markup.common], such as + [markup.tiny]. + In simple usage, when using only this module [Markup], the additional type parameter ['s] is always [sync], and there is no need to consider it further. @@ -115,7 +119,7 @@ type ('a, 's) stream debug parser output, use optional argument [?report] of the parsers, and look in module {!Error}. *) -type location = int * int +type location = Markup_common.location (** Line and column for parsing errors. Both numbers are one-based. *) (** Error type and [to_string] function. *) @@ -227,17 +231,17 @@ end (** {2 Signals} *) -type name = string * string +type name = Markup_common.name (** Expanded name: a namespace URI followed by a local name. *) -type xml_declaration = +type xml_declaration = Markup_common.xml_declaration = {version : string; encoding : string option; standalone : bool option} (** Representation of an XML declaration, i.e. []. *) -type doctype = +type doctype = Markup_common.doctype = {doctype_name : string option; public_identifier : string option; system_identifier : string option; @@ -369,12 +373,6 @@ val write_xml : (** {2 HTML} *) -val parse_html_ragel : - ?report:(location -> Error.t -> unit) -> - ?context:[< `Document | `Fragment of string ] -> - ?depth_limit:int -> - string -> 's parser - val parse_html : ?report:(location -> Error.t -> unit) -> ?encoding:Encoding.t -> @@ -975,3 +973,30 @@ val preprocess_input_stream : - HTML: [] tags found in the body do not have their attributes added to the [`Start_element "html"] signal emitted at the beginning of the document. *) + +(* Exposing some internal types and functions to allow sane integration *) +module Internals : sig + type location = Markup_common.location + + module Token_tag : sig + type t = + {name : string; + attributes : (string * string) list; + self_closing : bool} + end + + type token = + [ `Doctype of doctype + | `Start of Token_tag.t + | `End of Token_tag.t + | `Char of int + | `String of string + | `Comment of string + | `EOF ] + + val parse_tokens : + ?report:(location -> Error.t -> unit) -> + ?context:[< `Document | `Fragment of string ] -> + ?depth_limit:int -> + (location * token) list -> 's parser +end diff --git a/src/namespace.ml b/src/baseline/namespace.ml similarity index 100% rename from src/namespace.ml rename to src/baseline/namespace.ml diff --git a/src/namespace.mli b/src/baseline/namespace.mli similarity index 100% rename from src/namespace.mli rename to src/baseline/namespace.mli diff --git a/src/stream_io.ml b/src/baseline/stream_io.ml similarity index 100% rename from src/stream_io.ml rename to src/baseline/stream_io.ml diff --git a/src/text.ml b/src/baseline/text.ml similarity index 100% rename from src/text.ml rename to src/baseline/text.ml diff --git a/src/baseline/trie.ml b/src/baseline/trie.ml new file mode 100644 index 0000000..432cb4e --- /dev/null +++ b/src/baseline/trie.ml @@ -0,0 +1,6 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* See the comment in entities.ml. *) + +include Markup_entities.Trie diff --git a/src/utility.ml b/src/baseline/utility.ml similarity index 100% rename from src/utility.ml rename to src/baseline/utility.ml diff --git a/src/xml_parser.ml b/src/baseline/xml_parser.ml similarity index 100% rename from src/xml_parser.ml rename to src/baseline/xml_parser.ml diff --git a/src/xml_parser.mli b/src/baseline/xml_parser.mli similarity index 100% rename from src/xml_parser.mli rename to src/baseline/xml_parser.mli diff --git a/src/xml_tokenizer.ml b/src/baseline/xml_tokenizer.ml similarity index 100% rename from src/xml_tokenizer.ml rename to src/baseline/xml_tokenizer.ml diff --git a/src/xml_tokenizer.mli b/src/baseline/xml_tokenizer.mli similarity index 100% rename from src/xml_tokenizer.mli rename to src/baseline/xml_tokenizer.mli diff --git a/src/xml_writer.ml b/src/baseline/xml_writer.ml similarity index 100% rename from src/xml_writer.ml rename to src/baseline/xml_writer.ml diff --git a/src/xml_writer.mli b/src/baseline/xml_writer.mli similarity index 100% rename from src/xml_writer.mli rename to src/baseline/xml_writer.mli diff --git a/src/common/common.ml b/src/common/common.ml new file mode 100644 index 0000000..bd6cc33 --- /dev/null +++ b/src/common/common.ml @@ -0,0 +1,101 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +type 'a cont = 'a -> unit +type 'a cps = exn cont -> 'a cont -> unit + +type location = int * int + +let compare_locations (line, column) (line', column') = + match line - line' with + | 0 -> column - column' + | order -> order + +type name = string * string + +let xml_ns = "http://www.w3.org/XML/1998/namespace" +let xmlns_ns = "http://www.w3.org/2000/xmlns/" +let xlink_ns = "http://www.w3.org/1999/xlink" +let html_ns = "http://www.w3.org/1999/xhtml" +let svg_ns = "http://www.w3.org/2000/svg" +let mathml_ns = "http://www.w3.org/1998/Math/MathML" + +type xml_declaration = + {version : string; + encoding : string option; + standalone : bool option} + +type doctype = + {doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool} + +type signal = + [ `Start_element of name * (name * string) list + | `End_element + | `Text of string list + | `Xml of xml_declaration + | `Doctype of doctype + | `PI of string * string + | `Comment of string ] + +let signal_to_string = function + | `Comment s -> + Printf.sprintf "" s + + | `Doctype d -> + let text = + match d.doctype_name with + | None -> + begin match d.raw_text with + | None -> "" + | Some s -> " " ^ s + end + | Some name -> + match d.public_identifier, d.system_identifier with + | None, None -> " " ^ name + | Some p, None -> Printf.sprintf " %s PUBLIC \"%s\"" name p + | None, Some s -> Printf.sprintf " %s SYSTEM \"%s\"" name s + | Some p, Some s -> Printf.sprintf " %s PUBLIC \"%s\" \"%s\"" name p s + in + Printf.sprintf "" text + + | `Start_element (name, attributes) -> + let name_to_string = function + | "", local_name -> local_name + | ns, local_name -> ns ^ ":" ^ local_name + in + let attributes = + attributes + |> List.map (fun (name, value) -> + Printf.sprintf " %s=\"%s\"" (name_to_string name) value) + |> String.concat "" + in + Printf.sprintf "<%s%s>" (name_to_string name) attributes + + | `End_element -> + "" + + | `Text ss -> + String.concat "" ss + + | `Xml x -> + let s = Printf.sprintf "" x.version in + let s = + match x.encoding with + | None -> s + | Some encoding -> Printf.sprintf "%s encoding=\"%s\"" s encoding + in + let s = + match x.standalone with + | None -> s + | Some standalone -> + Printf.sprintf + "%s standalone=\"%s\"" s (if standalone then "yes" else "no") + in + s ^ "?>" + + | `PI (target, s) -> + Printf.sprintf "" target s diff --git a/src/common/dune b/src/common/dune new file mode 100644 index 0000000..4939517 --- /dev/null +++ b/src/common/dune @@ -0,0 +1,9 @@ +(library + (name markup_common) + (public_name markup.common) + (synopsis "Shared signal, stream and error types for Markup.ml") + (instrumentation + (backend bisect_ppx)) + (private_modules common) + (flags + (:standard -w -9))) diff --git a/src/error.ml b/src/common/error.ml similarity index 86% rename from src/error.ml rename to src/common/error.ml index e29fd12..d03f02b 100644 --- a/src/error.ml +++ b/src/common/error.ml @@ -1,8 +1,6 @@ (* This file is part of Markup.ml, released under the MIT license. See LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) -open Common - type t = [ `Decoding_error of string * string | `Bad_token of string * string * string @@ -69,13 +67,3 @@ let to_string ?location error = match location with | None -> message | Some (line, column) -> fmt "line %i, column %i: %s" line column message - -type 'a handler = 'a -> t -> unit cps -type parse_handler = location handler -type write_handler = (signal * int) handler - -let ignore_errors _ _ _ resume = resume () - -let report_if report condition location detail throw k = - if condition then report location (detail ()) throw k - else k () diff --git a/src/common/error.mli b/src/common/error.mli new file mode 100644 index 0000000..887e422 --- /dev/null +++ b/src/common/error.mli @@ -0,0 +1,15 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +type t = + [ `Decoding_error of string * string + | `Bad_token of string * string * string + | `Unexpected_eoi of string + | `Bad_document of string + | `Unmatched_start_tag of string + | `Unmatched_end_tag of string + | `Bad_namespace of string + | `Misnested_tag of string * string * (string * string) list + | `Bad_content of string ] + +val to_string : ?location:int * int -> t -> string diff --git a/src/kstream.ml b/src/common/kstream.ml similarity index 98% rename from src/kstream.ml rename to src/common/kstream.ml index aa61f82..2cf4bb9 100644 --- a/src/kstream.ml +++ b/src/common/kstream.ml @@ -1,7 +1,8 @@ (* This file is part of Markup.ml, released under the MIT license. See LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) -open Common +type 'a cont = 'a Common.cont +type 'a cps = 'a Common.cps type 'a t = {mutable f : exn cont -> unit cont -> 'a cont -> unit} diff --git a/src/kstream.mli b/src/common/kstream.mli similarity index 96% rename from src/kstream.mli rename to src/common/kstream.mli index 19e52f6..44db9eb 100644 --- a/src/kstream.mli +++ b/src/common/kstream.mli @@ -15,7 +15,8 @@ interface of Markup.ml, and the internal code should be calling them only when it is statically provable that the functions will succeed. *) -open Common +type 'a cont = 'a -> unit +type 'a cps = exn cont -> 'a cont -> unit type 'a t diff --git a/src/common/markup_common.ml b/src/common/markup_common.ml new file mode 100644 index 0000000..decf81f --- /dev/null +++ b/src/common/markup_common.ml @@ -0,0 +1,43 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +module Kstream = Kstream +module Error = Error +module Stream = Stream + +type async = unit +type sync = unit +type ('a, 's) stream = ('a, 's) Stream.t + +type location = Common.location + +let compare_locations = Common.compare_locations + +type name = Common.name + +type xml_declaration = Common.xml_declaration = { + version : string; + encoding : string option; + standalone : bool option; +} + +type doctype = Common.doctype = { + doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool; +} + +type signal = Common.signal + +let signal_to_string = Common.signal_to_string + +module Ns = struct + let html = Common.html_ns + let svg = Common.svg_ns + let mathml = Common.mathml_ns + let xml = Common.xml_ns + let xmlns = Common.xmlns_ns + let xlink = Common.xlink_ns +end diff --git a/src/common/markup_common.mli b/src/common/markup_common.mli new file mode 100644 index 0000000..397cc8b --- /dev/null +++ b/src/common/markup_common.mli @@ -0,0 +1,74 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(** Types shared by the [markup] and [markup.tiny] libraries. + + Both libraries alias the types below, so that streams produced by one are + accepted directly by consumers written against the other. Nothing here is + intended to be used directly by applications; use [Markup] or + [Markup_tiny]. *) + +(** Internal stream implementation, exposed so that libraries built on top of + this one can construct the shared stream type. Not a stable interface. *) +module Kstream = Kstream +module Stream = Stream + +module Error = Error + +(** {2 Streams} *) + +type async +type sync +(** Phantom types for use with [('a, 's) stream] in place of ['s]. They are + distinct and abstract, so that a [sync] stream cannot be passed where an + [async] stream is expected, and vice versa. *) + +type ('a, 's) stream = ('a, 's) Stream.t +(** Streams of elements of type ['a]. The operations on streams are + {!Kstream}'s; see {!Stream.Private} for converting between a [Kstream.t] + and a [stream] at no cost. *) + +(** {2 Errors} *) + +type location = int * int + +val compare_locations : location -> location -> int + +(** {2 Signals} *) + +type name = string * string + +type xml_declaration = { + version : string; + encoding : string option; + standalone : bool option; +} + +type doctype = { + doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool; +} + +type signal = + [ `Start_element of name * (name * string) list + | `End_element + | `Text of string list + | `Xml of xml_declaration + | `Doctype of doctype + | `PI of string * string + | `Comment of string ] + +val signal_to_string : [< signal ] -> string + +(** Common namespace URIs. *) +module Ns : sig + val html : string + val svg : string + val mathml : string + val xml : string + val xmlns : string + val xlink : string +end diff --git a/src/common/stream.ml b/src/common/stream.ml new file mode 100644 index 0000000..94789fa --- /dev/null +++ b/src/common/stream.ml @@ -0,0 +1,11 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* The phantom parameter is deliberately absent from the representation. *) +type ('a, 's) t = 'a Kstream.t + +module Private = +struct + let to_stream s = s + let of_stream s = s +end diff --git a/src/common/stream.mli b/src/common/stream.mli new file mode 100644 index 0000000..4f5293b --- /dev/null +++ b/src/common/stream.mli @@ -0,0 +1,24 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* The stream type shared by the libraries built on markup.common. This module + is only the type; the operations on streams (in [Kstream]) remain private + to each library. + + [t] is abstract here, which is what makes the phantom parameter meaningful: + outside this library, [('a, sync) t] and [('a, async) t] cannot be shown + equal, so a synchronous stream cannot be passed where an asynchronous one is + expected, and vice versa. That abstraction is the only thing keeping the two + distinct outside this library. *) + +type ('a, 's) t + +module Private : +sig + (* The two identity conversions between a [Kstream.t] and a phantom-tagged + [Stream.t], exposed so that a library implementing streams on top of + [Kstream] can exchange streams with other such libraries at no cost. + Not a stable interface. *) + val to_stream : 'a Kstream.t -> ('a, 's) t + val of_stream : ('a, 's) t -> 'a Kstream.t +end diff --git a/src/entities/dune b/src/entities/dune new file mode 100644 index 0000000..d2cd155 --- /dev/null +++ b/src/entities/dune @@ -0,0 +1,8 @@ +(library + (name markup_entities) + (public_name markup.entities) + (synopsis "HTML character entity table and lookup trie for Markup.ml") + (instrumentation + (backend bisect_ppx)) + (flags + (:standard -w -9))) diff --git a/src/entities.json b/src/entities/entities.json similarity index 100% rename from src/entities.json rename to src/entities/entities.json diff --git a/src/entities.ml b/src/entities/entities.ml similarity index 100% rename from src/entities.ml rename to src/entities/entities.ml diff --git a/src/entities/markup_entities.ml b/src/entities/markup_entities.ml new file mode 100644 index 0000000..e1066a7 --- /dev/null +++ b/src/entities/markup_entities.ml @@ -0,0 +1,9 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* Interface module of the [markup.entities] library. It only re-exports the + library's modules so that they are reachable as [Markup_entities.Entities] + and [Markup_entities.Trie]. *) + +module Entities = Entities +module Trie = Trie diff --git a/src/translate_entities/dune b/src/entities/translate_entities/dune similarity index 100% rename from src/translate_entities/dune rename to src/entities/translate_entities/dune diff --git a/src/translate_entities/translate_entities.ml b/src/entities/translate_entities/translate_entities.ml similarity index 96% rename from src/translate_entities/translate_entities.ml rename to src/entities/translate_entities/translate_entities.ml index 117e13f..99c33aa 100644 --- a/src/translate_entities/translate_entities.ml +++ b/src/entities/translate_entities/translate_entities.ml @@ -22,7 +22,7 @@ let () = print_string "(string * [ `One of int | `Two of int * int ]) array"; print_string " = [|\n "; - Yojson.Basic.from_file "src/entities.json" + Yojson.Basic.from_file "src/entities/entities.json" |> to_assoc |> List.map (fun (k, v) -> let k = String.sub k 1 (String.length k - 2) in diff --git a/src/trie.ml b/src/entities/trie.ml similarity index 100% rename from src/trie.ml rename to src/entities/trie.ml diff --git a/src/lite/common.ml b/src/lite/common.ml new file mode 100644 index 0000000..c8ec639 --- /dev/null +++ b/src/lite/common.ml @@ -0,0 +1,184 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +type 'a cont = 'a -> unit +type 'a cps = exn cont -> 'a cont -> unit +type location = Markup_common.location + +let compare_locations = Markup_common.compare_locations + +type name = Markup_common.name + +let xml_ns = Markup_common.Ns.xml +let xmlns_ns = Markup_common.Ns.xmlns +let xlink_ns = Markup_common.Ns.xlink +let html_ns = Markup_common.Ns.html +let svg_ns = Markup_common.Ns.svg +let mathml_ns = Markup_common.Ns.mathml + +module Token_tag = struct + type t = { + name : string; + attributes : (string * string) list; + self_closing : bool; + } +end + +type xml_declaration = Markup_common.xml_declaration = { + version : string; + encoding : string option; + standalone : bool option; +} + +type doctype = Markup_common.doctype = { + doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool; +} + +type signal = Markup_common.signal + +type general_token = + [ `Xml of xml_declaration + | `Doctype of doctype + | `Start of Token_tag.t + | `End of Token_tag.t + | `Chars of string list + | `Char of int + | `PI of string * string + | `Comment of string + | `EOF ] + +let u_rep = Uchar.to_int Uutf.u_rep +let add_utf_8 buffer c = Uutf.Buffer.add_utf_8 buffer (Uchar.unsafe_of_int c) +let format_char = Printf.sprintf "U+%04X" + +(* Type constraints are necessary to avoid polymorphic comparison, which would + greatly reduce performance: https://github.com/aantron/markup.ml/pull/15. *) +let is_in_range (lower : int) (upper : int) c = c >= lower && c <= upper + +(* HTML 8.2.2.5. *) +let is_control_character = function + | 0x000B -> true + | c when is_in_range 0x0001 0x0008 c -> true + | c when is_in_range 0x000E 0x001F c -> true + | c when is_in_range 0x007F 0x009F c -> true + | _ -> false + +(* HTML 8.2.2.5. *) +let is_non_character = function + | c when is_in_range 0xFDD0 0xFDEF c -> true + | c when c land 0xFFFF = 0xFFFF || c land 0xFFFF = 0xFFFE -> true + | _ -> false + +let is_digit = is_in_range 0x0030 0x0039 + +let is_hex_digit = function + | c when is_digit c -> true + | c when is_in_range 0x0041 0x0046 c -> true + | c when is_in_range 0x0061 0x0066 c -> true + | _ -> false + +let is_scalar = function + | c when c >= 0x10FFFF || (c >= 0xD800 && c <= 0xDFFF) -> false + | _ -> true + +let is_uppercase = is_in_range 0x0041 0x005A +let is_lowercase = is_in_range 0x0061 0x007A + +let is_alphabetic = function + | c when is_uppercase c -> true + | c when is_lowercase c -> true + | _ -> false + +let is_alphanumeric = function + | c when is_alphabetic c -> true + | c when is_digit c -> true + | _ -> false + +let is_whitespace c = c = 0x0020 || c = 0x000A || c = 0x0009 || c = 0x000D + +let is_whitespace_only s = + try + s + |> String.iter (fun c -> + let c = int_of_char c in + if c = 0x000C || is_whitespace c then () else raise Exit); + true + with Exit -> false + +let to_lowercase = function c when is_uppercase c -> c + 0x20 | c -> c +let is_printable = is_in_range 0x0020 0x007E + +let char c = + if is_printable c then begin + let buffer = Buffer.create 4 in + add_utf_8 buffer c; + Buffer.contents buffer + end + else format_char c + +let is_valid_html_char c = not (is_control_character c || is_non_character c) + +let is_valid_xml_char c = + is_in_range 0x0020 0xD7FF c + || c = 0x0009 || c = 0x000A || c = 0x000D + || is_in_range 0xE000 0xFFFD c + || is_in_range 0x10000 0x10FFFF c + +let signal_to_string = Markup_common.signal_to_string + +let token_to_string = function + | `Xml x -> signal_to_string (`Xml x) + | `Doctype d -> signal_to_string (`Doctype d) + | `Start t -> + let name = ("", t.Token_tag.name) in + let attributes = + t.Token_tag.attributes |> List.map (fun (n, v) -> (("", n), v)) + in + let s = signal_to_string (`Start_element (name, attributes)) in + if not t.Token_tag.self_closing then s + else String.sub s 0 (String.length s - 1) ^ "/>" + | `End t -> Printf.sprintf "" t.Token_tag.name + | `Chars ss -> String.concat "" ss + | `Char i -> char i + | `String s -> s + | `PI v -> signal_to_string (`PI v) + | `Comment s -> signal_to_string (`Comment s) + | `EOF -> "EOF" + +let whitespace_chars = " \t\n\r" + +let whitespace_prefix_length s = + let rec loop index = + if index = String.length s then index + else if String.contains whitespace_chars s.[index] then loop (index + 1) + else index + in + loop 0 + +let whitespace_suffix_length s = + let rec loop rindex = + if rindex = String.length s then rindex + else if String.contains whitespace_chars s.[String.length s - rindex - 1] + then loop (rindex + 1) + else rindex + in + loop 0 + +let trim_string_left s = + let prefix_length = whitespace_prefix_length s in + String.sub s prefix_length (String.length s - prefix_length) + +let trim_string_right s = + let suffix_length = whitespace_suffix_length s in + String.sub s 0 (String.length s - suffix_length) + +(* String.trim not available for OCaml < 4.00. *) +let trim_string s = s |> trim_string_left |> trim_string_right + +(* Specialization of List.mem at string list, to avoid polymorphic + comparison. *) +let list_mem_string (s : string) l = List.exists (fun s' -> s' = s) l diff --git a/src/lite/dune b/src/lite/dune new file mode 100644 index 0000000..b6500d5 --- /dev/null +++ b/src/lite/dune @@ -0,0 +1,18 @@ +(env + (afl + (ocamlopt_flags + (:standard -afl-instrument -afl-inst-ratio 20)))) + +(library + (name markup_lite) + (public_name markup.lite) + (synopsis "Small fast synchronous HTML parser") + (private_modules common encoding error html_entity_decoder html_parser + html_tokenizer html_writer kstream markup_declaration namespace + ragel_html_tokenizer raw_text string_helpers text token_source) + (libraries markup.common markup.entities uutf) + (foreign_stubs + (language c) + (names string_helpers)) + (flags + (:standard -w -9))) diff --git a/src/lite/encoding.ml b/src/lite/encoding.ml new file mode 100644 index 0000000..062e503 --- /dev/null +++ b/src/lite/encoding.ml @@ -0,0 +1,427 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +let ascii_lower = Char.lowercase_ascii +let is_space = function ' ' | '\t' | '\n' | '\r' | '\x0C' -> true | _ -> false +let is_letter = function 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false + +type cursor = { input : string; limit : int; mutable position : int } + +let next cursor = + assert (cursor.position < cursor.limit); + let byte = cursor.input.[cursor.position] in + cursor.position <- cursor.position + 1; + byte + +let skip_spaces cursor = + while + cursor.position < cursor.limit && is_space cursor.input.[cursor.position] + do + cursor.position <- cursor.position + 1 + done + +(* [text] must contain only lowercase ASCII. *) +let starts_with_ci cursor text = + let length = String.length text in + let rec matches index = + index = length + || ascii_lower cursor.input.[cursor.position + index] = text.[index] + && matches (index + 1) + in + cursor.position + length <= cursor.limit && matches 0 + +let read_while cursor predicate = + let start = cursor.position in + while + cursor.position < cursor.limit && predicate cursor.input.[cursor.position] + do + cursor.position <- cursor.position + 1 + done; + String.sub cursor.input start (cursor.position - start) + +let read_quoted_value cursor quote = + let start = cursor.position in + while + cursor.position < cursor.limit && cursor.input.[cursor.position] <> quote + do + cursor.position <- cursor.position + 1 + done; + if cursor.position >= cursor.limit then "" + else + let value = String.sub cursor.input start (cursor.position - start) in + cursor.position <- cursor.position + 1; + String.lowercase_ascii value + +let read_unquoted_value cursor terminator = + read_while cursor (fun byte -> not (is_space byte || byte = terminator)) + |> String.lowercase_ascii + +let read_value cursor = + skip_spaces cursor; + match next cursor with + | ('\'' | '"') as quote -> read_quoted_value cursor quote + | _ -> + cursor.position <- cursor.position - 1; + read_unquoted_value cursor '>' + +let read_attribute_name cursor = + let start = cursor.position in + while + cursor.position < cursor.limit + && + let byte = cursor.input.[cursor.position] in + not + (is_space byte || byte = '/' || byte = '>' + || (byte = '=' && cursor.position > start)) + do + cursor.position <- cursor.position + 1 + done; + String.sub cursor.input start (cursor.position - start) + |> String.lowercase_ascii + +let read_attribute cursor = + skip_spaces cursor; + while + cursor.position < cursor.limit && cursor.input.[cursor.position] = '/' + do + cursor.position <- cursor.position + 1; + skip_spaces cursor + done; + if cursor.position >= cursor.limit || cursor.input.[cursor.position] = '>' + then None + else + let name = read_attribute_name cursor in + skip_spaces cursor; + let value = + if cursor.position < cursor.limit && cursor.input.[cursor.position] = '=' + then begin + cursor.position <- cursor.position + 1; + if cursor.position < cursor.limit then read_value cursor else "" + end + else "" + in + Some (name, value) + +let extract_encoding value : string option = + let cursor = { input = value; limit = String.length value; position = 0 } in + let rec search () = + if cursor.position >= cursor.limit then None + else if starts_with_ci cursor "charset" then begin + cursor.position <- cursor.position + 7; + skip_spaces cursor; + if cursor.position >= cursor.limit then None + else if next cursor <> '=' then begin + cursor.position <- cursor.position - 1; + search () + end + else begin + skip_spaces cursor; + if cursor.position >= cursor.limit then None + else + let value = + match next cursor with + | ('\'' | '"') as quote -> read_quoted_value cursor quote + | _ -> + cursor.position <- cursor.position - 1; + read_unquoted_value cursor ';' + in + if value = "" then search () else Some value + end + end + else begin + cursor.position <- cursor.position + 1; + search () + end + in + search () + +let normalize_declared_encoding value = + match String.lowercase_ascii (Common.trim_string value) with + | "unicode-1-1-utf-8" | "utf-8" | "utf8" -> "utf-8" + | "utf-16" | "utf-16be" | "utf-16le" -> "utf-8" + | "cp1251" | "windows-1251" | "x-cp1251" -> "windows-1251" + | "ansi_x3.4-1968" | "ascii" | "cp1252" | "cp819" | "csisolatin1" | "ibm819" + | "iso-8859-1" | "iso-ir-100" | "iso8859-1" | "iso88591" | "iso_8859-1" + | "iso_8859-1:1987" | "l1" | "latin1" | "us-ascii" | "windows-1252" + | "x-cp1252" -> + "windows-1252" + | value -> value + +(** Parse encoding inside a [meta] tag *) +let read_meta_encoding cursor = + let rec attributes names got_pragma need_pragma charset = + match read_attribute cursor with + | None -> + if need_pragma = Some true && not got_pragma then None + else Option.map normalize_declared_encoding charset + | Some (name, _) when Common.list_mem_string name names -> + attributes names got_pragma need_pragma charset + | Some (name, value) -> + let names = name :: names in + begin match name with + | "http-equiv" -> + attributes names + (got_pragma || value = "content-type") + need_pragma charset + | "content" when charset = None -> + begin match extract_encoding value with + | Some value -> attributes names got_pragma (Some true) (Some value) + | None -> attributes names got_pragma need_pragma charset + end + | "charset" when value <> "" -> + attributes names got_pragma (Some false) (Some value) + | _ -> attributes names got_pragma need_pragma charset + end + in + attributes [] false None None + +let skip_tag_like cursor = + while cursor.position < cursor.limit && next cursor <> '>' do + () + done + +let skip_tag cursor = + while + cursor.position < cursor.limit + && (not (is_space cursor.input.[cursor.position])) + && cursor.input.[cursor.position] <> '>' + do + cursor.position <- cursor.position + 1 + done; + while cursor.position < cursor.limit && read_attribute cursor <> None do + () + done; + if cursor.position < cursor.limit then cursor.position <- cursor.position + 1 + +let skip_comment cursor = + while + cursor.position + 2 < cursor.limit + && (cursor.input.[cursor.position] <> '-' + || cursor.input.[cursor.position + 1] <> '-' + || cursor.input.[cursor.position + 2] <> '>') + do + cursor.position <- cursor.position + 1 + done; + cursor.position <- min cursor.limit (cursor.position + 3) + +let declared_encoding input : string option = + let cursor = + { input; limit = min 1024 (String.length input); position = 0 } + in + let rec scan () = + while cursor.position < cursor.limit && next cursor <> '<' do + () + done; + if cursor.position >= cursor.limit then None + else if starts_with_ci cursor "!--" then begin + cursor.position <- cursor.position + 3; + skip_comment cursor; + scan () + end + else if starts_with_ci cursor "meta" then begin + cursor.position <- cursor.position + 4; + if + cursor.position < cursor.limit + && (is_space cursor.input.[cursor.position] + || cursor.input.[cursor.position] = '/') + then ( + match read_meta_encoding cursor with + | Some _ as encoding -> encoding + | None -> + if cursor.position < cursor.limit then + cursor.position <- cursor.position + 1; + scan ()) + else begin + skip_tag cursor; + scan () + end + end + else if cursor.position < cursor.limit then begin + begin match cursor.input.[cursor.position] with + | '!' | '?' -> skip_tag_like cursor + | '/' + when cursor.position + 1 < cursor.limit + && is_letter cursor.input.[cursor.position + 1] -> + skip_tag cursor + | '/' -> skip_tag_like cursor + | byte when is_letter byte -> skip_tag cursor + | _ -> () + end; + scan () + end + else None + in + scan () + +let transcode scalar input = + let output = Buffer.create (String.length input + 32) in + String.iter + (fun byte -> + Uutf.Buffer.add_utf_8 output (Uchar.of_int (scalar (Char.code byte)))) + input; + Buffer.contents output + +module Windows_1252 = struct + let high = + [| + 0x20AC; + 0x0081; + 0x201A; + 0x0192; + 0x201E; + 0x2026; + 0x2020; + 0x2021; + 0x02C6; + 0x2030; + 0x0160; + 0x2039; + 0x0152; + 0x008D; + 0x017D; + 0x008F; + 0x0090; + 0x2018; + 0x2019; + 0x201C; + 0x201D; + 0x2022; + 0x2013; + 0x2014; + 0x02DC; + 0x2122; + 0x0161; + 0x203A; + 0x0153; + 0x009D; + 0x017E; + 0x0178; + |] + + let decode input = + transcode + (fun byte -> + if byte < 0x80 || byte >= 0xA0 then byte else high.(byte - 0x80)) + input +end + +let windows_1251_high = + [| + 0x0402; + 0x0403; + 0x201A; + 0x0453; + 0x201E; + 0x2026; + 0x2020; + 0x2021; + 0x20AC; + 0x2030; + 0x0409; + 0x2039; + 0x040A; + 0x040C; + 0x040B; + 0x040F; + 0x0452; + 0x2018; + 0x2019; + 0x201C; + 0x201D; + 0x2022; + 0x2013; + 0x2014; + 0xFFFD; + 0x2122; + 0x0459; + 0x203A; + 0x045A; + 0x045C; + 0x045B; + 0x045F; + 0x00A0; + 0x040E; + 0x045E; + 0x0408; + 0x00A4; + 0x0490; + 0x00A6; + 0x00A7; + 0x0401; + 0x00A9; + 0x0404; + 0x00AB; + 0x00AC; + 0x00AD; + 0x00AE; + 0x0407; + 0x00B0; + 0x00B1; + 0x0406; + 0x0456; + 0x0491; + 0x00B5; + 0x00B6; + 0x00B7; + 0x0451; + 0x2116; + 0x0454; + 0x00BB; + 0x0458; + 0x0405; + 0x0455; + 0x0457; + |] + +let windows_1251 input = + transcode + (fun byte -> + if byte < 0x80 then byte + else if byte < 0xC0 then windows_1251_high.(byte - 0x80) + (* Match the baseline's long-standing Windows-1251 table exactly. *) + else if byte = 0xD0 then 0x0410 + else 0x0410 + byte - 0xC0) + input + +let transcode_utf_16 fold input = + let output = Buffer.create (String.length input) in + fold + (fun () _ -> function + | `Uchar uchar -> Uutf.Buffer.add_utf_8 output uchar + | `Malformed _ -> Uutf.Buffer.add_utf_8 output Uutf.u_rep) + () input; + Buffer.contents output + +let utf_16be input = + transcode_utf_16 + (fun folder state input -> Uutf.String.fold_utf_16be folder state input) + input + +let utf_16le input = + transcode_utf_16 + (fun folder state input -> Uutf.String.fold_utf_16le folder state input) + input + +let bom input = + let length = String.length input in + if length >= 2 && input.[0] = '\xFE' && input.[1] = '\xFF' then Some `UTF_16BE + else if length >= 2 && input.[0] = '\xFF' && input.[1] = '\xFE' then + Some `UTF_16LE + else if + length >= 3 + && input.[0] = '\xEF' + && input.[1] = '\xBB' + && input.[2] = '\xBF' + then Some `UTF_8 + else None + +let decode_html input : string = + match bom input with + | Some `UTF_16BE -> utf_16be input + | Some `UTF_16LE -> utf_16le input + | Some `UTF_8 -> input + | None -> ( + match declared_encoding input with + | Some "windows-1251" -> windows_1251 input + | Some "windows-1252" -> Windows_1252.decode input + | _ -> input) diff --git a/src/lite/encoding.mli b/src/lite/encoding.mli new file mode 100644 index 0000000..14f3623 --- /dev/null +++ b/src/lite/encoding.mli @@ -0,0 +1,8 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +val decode_html : string -> string +(** Detect encoding, and decode to utf8 if necessary *) + +val utf_16be : string -> string +val utf_16le : string -> string diff --git a/src/lite/error.ml b/src/lite/error.ml new file mode 100644 index 0000000..f69362b --- /dev/null +++ b/src/lite/error.ml @@ -0,0 +1,14 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +include Markup_common.Error +open Common + +type 'a handler = 'a -> t -> unit cps +type parse_handler = location handler +type write_handler = (signal * int) handler + +let ignore_errors _ _ _ resume = resume () + +let report_if report condition location detail throw k = + if condition then report location (detail ()) throw k else k () diff --git a/src/lite/html_entity_decoder.ml b/src/lite/html_entity_decoder.ml new file mode 100644 index 0000000..fa025f2 --- /dev/null +++ b/src/lite/html_entity_decoder.ml @@ -0,0 +1,132 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common +module Trie = Markup_entities.Trie + +let named_entity_trie = + lazy + (Array.fold_left + (fun trie (name, characters) -> Trie.add name characters trie) + (Trie.create ()) Markup_entities.Entities.entities) + +let replace_windows_1252_entity = function + | 0x80 -> 0x20AC + | 0x82 -> 0x201A + | 0x83 -> 0x0192 + | 0x84 -> 0x201E + | 0x85 -> 0x2026 + | 0x86 -> 0x2020 + | 0x87 -> 0x2021 + | 0x88 -> 0x02C6 + | 0x89 -> 0x2030 + | 0x8A -> 0x0160 + | 0x8B -> 0x2039 + | 0x8C -> 0x0152 + | 0x8E -> 0x017D + | 0x91 -> 0x2018 + | 0x92 -> 0x2019 + | 0x93 -> 0x201C + | 0x94 -> 0x201D + | 0x95 -> 0x2022 + | 0x96 -> 0x2013 + | 0x97 -> 0x2014 + | 0x98 -> 0x02DC + | 0x99 -> 0x2122 + | 0x9A -> 0x0161 + | 0x9B -> 0x203A + | 0x9C -> 0x0153 + | 0x9E -> 0x017E + | 0x9F -> 0x0178 + | c -> c + +let[@inline] is_decimal c = is_digit (Char.code c) +let[@inline] is_hexadecimal c = is_hex_digit (Char.code c) +let[@inline] is_alphanumeric_char c = is_alphanumeric (Char.code c) + +let decode_references in_attribute text = + let length = String.length text in + let buffer = Buffer.create length in + let numeric_value ~hexadecimal digits finish = + let digits = String.sub text digits (finish - digits) in + let value = + int_of_string_opt (if hexadecimal then "0x" ^ digits else digits) + in + match value with + | None -> u_rep + | Some value -> + let value = replace_windows_1252_entity value in + if value = 0 || not (is_scalar value) then u_rep else value + in + let rec search copied index = + if index >= length then + Buffer.add_substring buffer text copied (length - copied) + else if text.[index] <> '&' then search copied (index + 1) + else + match reference (index + 1) with + | None -> search copied (index + 1) + | Some (after, value) -> + Buffer.add_substring buffer text copied (index - copied); + begin match value with + | `One codepoint -> add_utf_8 buffer codepoint + | `Two (first, second) -> + add_utf_8 buffer first; + add_utf_8 buffer second + end; + search after after + and reference start = + if start >= length then None + else + match text.[start] with + | '\t' | '\n' | '\x0C' | ' ' | '<' | '&' -> None + | '#' -> numeric_reference (start + 1) + | _ -> named_reference start + and numeric_reference start = + if start < length && (text.[start] = 'x' || text.[start] = 'X') then + let digits = start + 1 in + let finish = consume_while digits is_hexadecimal in + if finish = digits then None + else + Some (terminate finish (numeric_value ~hexadecimal:true digits finish)) + else + let finish = consume_while start is_decimal in + if finish = start then None + else + Some (terminate finish (numeric_value ~hexadecimal:false start finish)) + and terminate finish value = + if finish < length && text.[finish] = ';' then (finish + 1, `One value) + else (finish, `One value) + and named_reference start = + let rec walk best index trie = + if index >= length then best + else + let trie = Trie.advance (Char.code text.[index]) trie in + match Trie.matches trie with + | Trie.No -> best + | Trie.Prefix -> walk best (index + 1) trie + | Trie.Multiple value -> walk (Some (index + 1, value)) (index + 1) trie + | Trie.Yes value -> Some (index + 1, value) + in + match walk None start (Lazy.force named_entity_trie) with + | None -> None + | Some (name_end, value) -> + if name_end < length && text.[name_end] = ';' then + Some (name_end + 1, value) + else if + in_attribute && name_end < length + && (is_alphanumeric_char text.[name_end] || text.[name_end] = '=') + then None + else Some (name_end, value) + and consume_while index predicate = + if index < length && predicate text.[index] then + consume_while (index + 1) predicate + else index + in + search 0 0; + Buffer.contents buffer + +let decode text = + if String.contains text '&' then decode_references false text else text + +let decode_attribute text = + if String.contains text '&' then decode_references true text else text diff --git a/src/lite/html_parser.ml b/src/lite/html_parser.ml new file mode 100644 index 0000000..d8b0295 --- /dev/null +++ b/src/lite/html_parser.ml @@ -0,0 +1,2758 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common +open Token_tag +open Html_tokenizer +open Kstream + +(* Namespaces for pattern matching. *) +type ns = HTML | MathML | SVG | Other of string [@@warning "-37"] +type qname = ns * string + +module Ns : sig + val to_string : ns -> string +end = struct + let to_string = function + | HTML -> html_ns + | MathML -> mathml_ns + | SVG -> svg_ns + | Other s -> s +end + +(* Elements. *) +type element = { + element_name : qname; + location : location; + is_html_integration_point : bool; + suppress : bool; + mutable buffering : bool; + mutable is_open : bool; + mutable attributes : (name * string) list; + mutable end_location : location; + mutable children : annotated_node list; + mutable parent : element; +} + +and node = + | Element of element + | Text of string list + | PI of string * string + | Comment of string + +and annotated_node = location * node + +(* Element helpers. *) +module Element : sig + val create : + ?is_html_integration_point:bool -> + ?suppress:bool -> + qname -> + location -> + element + + val dummy : element + val is_special : qname -> bool + val is_not_hidden : Token_tag.t -> bool +end = struct + let rec dummy = + { + element_name = (HTML, "dummy"); + location = (1, 1); + is_html_integration_point = false; + suppress = true; + buffering = false; + is_open = false; + attributes = []; + end_location = (1, 1); + children = []; + parent = dummy; + } + + let create ?(is_html_integration_point = false) ?(suppress = false) name + location = + { + element_name = name; + location; + is_html_integration_point; + suppress; + buffering = false; + is_open = true; + attributes = []; + end_location = (1, 1); + children = []; + parent = dummy; + } + + let is_special = function + | ( HTML, + ( "address" | "applet" | "area" | "article" | "aside" | "base" + | "basefont" | "bgsound" | "blockquote" | "body" | "br" | "button" + | "caption" | "center" | "col" | "colgroup" | "dd" | "details" | "dir" + | "div" | "dl" | "dt" | "embed" | "fieldset" | "figcaption" | "figure" + | "footer" | "form" | "frame" | "frameset" | "h1" | "h2" | "h3" | "h4" + | "h5" | "h6" | "head" | "header" | "hgroup" | "hr" | "html" | "iframe" + | "img" | "input" | "isindex" | "li" | "link" | "listing" | "main" + | "marquee" | "meta" | "nav" | "noembed" | "noframes" | "noscript" + | "object" | "ol" | "p" | "param" | "plaintext" | "pre" | "script" + | "section" | "select" | "source" | "style" | "summary" | "table" + | "tbody" | "td" | "template" | "textarea" | "tfoot" | "th" | "thead" + | "title" | "tr" | "track" | "ul" | "wbr" | "xmp" ) ) -> + true + | MathML, ("mi" | "mo" | "mn" | "ms" | "mtext" | "annotation-xml") -> true + | SVG, ("foreignObject" | "desc" | "title") -> true + | _ -> false + + let is_not_hidden tag = + tag.Token_tag.attributes + |> List.exists (fun (name, value) -> name = "type" && value <> "hidden") +end + +(* Context detection. *) +type simple_context = [ `Document | `Fragment of string ] +type context = Document | Fragment of qname + +module Context : sig + type t + + val uninitialized : unit -> t + val initialize : [< simple_context ] -> t -> unit cps + val the_context : t -> context + val element : t -> element option + val token : t -> string option +end = struct + let[@warning "-32"] detect tokens throw k = + let tokens, restore = checkpoint tokens in + + let last_name = ref None in + let next_token k = + next_expected tokens throw (fun token -> + begin match token with + | _, Start { name } -> last_name := Some name + | _ -> () + end; + k token) + in + + let k context = + restore (); + k (context, !last_name) + in + + let rec scan () = + next_token begin function + | _, Doctype _ -> k `Document + | _, String s when not @@ is_whitespace_only s -> k (`Fragment "body") + | _, String _ -> scan () + | _, Char c when not @@ is_whitespace c -> k (`Fragment "body") + | _, Char _ -> scan () + | _, EOF -> k (`Fragment "body") + | _, Start { name = "html" } -> k `Document + | _, Start { name = "head" | "body" | "frameset" } -> + k (`Fragment "html") + | ( _, + Start + { + name = + ( "base" | "basefont" | "bgsound" | "link" | "meta" + | "noframes" | "style" | "template" | "title" ); + } ) -> + k (`Fragment "head") + | _, Start { name = "frame" } -> k (`Fragment "frameset") + | _, Start { name = "li" } -> k (`Fragment "ul") + | ( _, + Start + { + name = + "caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead"; + } ) -> + k (`Fragment "table") + | _, Start { name = "tr" } -> k (`Fragment "tbody") + | _, Start { name = "td" | "th" } -> k (`Fragment "tr") + | _, Start { name = "optgroup" | "option" } -> k (`Fragment "select") + | ( _, + Start + { + name = + ( "altglyph" | "altglyphdef" | "altglyphitem" | "animate" + | "animatecolor" | "animatemotion" | "animatetransform" + | "circle" | "clippath" | "color-profile" | "cursor" + | "defs" | "desc" | "ellipse" | "feblend" | "fecolormatrix" + | "fecomponenttransfer" | "fecomposite" + | "fediffuselighting" | "fedisplacementmap" + | "fedistantlight" | "feflood" | "fefunca" | "fefuncb" + | "fefuncg" | "fefuncr" | "fegaussianblur" | "feimage" + | "femerge" | "femergenode" | "femorphology" | "feoffset" + | "fepointlight" | "fespecularlighting" | "fespotlight" + | "fetile" | "feturbulence" | "filter" | "font-face" + | "font-face-format" | "font-face-name" | "font-face-src" + | "font-face-uri" | "foreignobject" | "g" | "glyph" + | "glyphref" | "hkern" | "image" | "line" | "lineargradient" + | "marker" | "mask" | "metadata" | "missing-glyph" | "mpath" + | "path" | "pattern" | "polygon" | "polyline" + | "radialgradient" | "rect" | "set" | "stop" | "switch" + | "symbol" | "text" | "textpath" | "tref" | "tspan" | "use" + ); + } ) -> + k (`Fragment "svg") + | ( _, + Start + { + name = + ( "maction" | "maligngroup" | "malignmark" | "menclose" + | "merror" | "mfenced" | "mfrac" | "mglyph" | "mi" + | "mlabeledtr" | "mlongdiv" | "mmultiscripts" | "mn" | "mo" + | "mover" | "mpadded" | "mphantom" | "mroot" | "mrow" | "ms" + | "mscarries" | "mscarry" | "msgroup" | "msline" | "mspace" + | "msqrt" | "msrow" | "mstack" | "mstyle" | "msub" | "msup" + | "msubsup" | "mtable" | "mtd" | "mtext" | "mtr" | "munder" + | "munderover" | "semantics" | "annotation" + | "annotation-xml" ); + } ) -> + k (`Fragment "math") + | _, Start _ -> k (`Fragment "body") + | _, (End _ | Comment _) -> scan () + end + in + + scan () + + type t = (context * element option * string option) ref + + let uninitialized () = ref (Document, None, None) + + let initialize requested_context state _throw k = + let context = + match requested_context with + | `Fragment element -> ( + (* HTML element names are case-insensitive, even in foreign content. + Lowercase the element name given by the user before analysis by the + parser, to match this convention. [String.lowercase] is acceptable + here because the API assumes the string [element] is in UTF-8. *) + match String.lowercase_ascii element with + | "math" -> Fragment (MathML, "math") + | "svg" -> Fragment (SVG, "svg") + | element -> Fragment (HTML, element)) + | `Document -> Document + in + let context_element = + match context with + | Document -> None + | Fragment name -> + let is_html_integration_point = + match name with + | SVG, ("foreignObject" | "desc" | "title") -> true + | _ -> false + in + Some + (Element.create ~is_html_integration_point ~suppress:true name (1, 1)) + in + state := (context, context_element, None); + k () + + let the_context { contents = c, _, _ } = c + let element { contents = _, e, _ } = e + let token { contents = _, _, t } = t +end + +(* Heplers for foreign content. *) +module Foreign : sig + val is_mathml_text_integration_point : qname -> bool + val is_html_integration_point : ns -> string -> (string * string) list -> bool + + val adjust_mathml_attributes : + ((string * string) * string) list -> ((string * string) * string) list + + val adjust_svg_attributes : + ((string * string) * string) list -> ((string * string) * string) list + + val adjust_svg_tag_name : string -> string +end = struct + let is_mathml_text_integration_point = function + | MathML, ("mi" | "mo" | "mn" | "ms" | "mtext") -> true + | _ -> false + + let is_html_integration_point namespace tag_name attributes = + match namespace with + | HTML | Other _ -> false + | MathML -> + tag_name = "annotation-xml" + && attributes + |> List.exists (function + | "encoding", "text/html" -> true + | "encoding", "application/xhtml+xml" -> true + | _ -> false) + | SVG -> list_mem_string tag_name [ "foreignObject"; "desc"; "title" ] + + let adjust_mathml_attributes attributes = + attributes + |> List.map (fun ((ns, name), value) -> + let name = + if ns = mathml_ns && name = "definitionurl" then "definitionURL" + else name + in + ((ns, name), value)) + + let adjust_svg_attributes attributes = + attributes + |> List.map (fun ((ns, name), value) -> + let name = + match name with + | "attributename" -> "attributeName" + | "attributetype" -> "attributeType" + | "basefrequency" -> "baseFrequency" + | "baseprofile" -> "baseProfile" + | "calcmode" -> "calcMode" + | "clippathunits" -> "clipPathUnits" + | "contentscripttype" -> "contentScriptType" + | "contentstyletype" -> "contentStyleType" + | "diffuseconstant" -> "diffuseConstant" + | "edgemode" -> "edgeMode" + | "externalresourcesrequired" -> "externalResourcesRequired" + | "filterres" -> "filterRes" + | "filterunits" -> "filterUnits" + | "glyphref" -> "glyphRef" + | "gradienttransform" -> "gradientTransform" + | "gradientunits" -> "gradientUnits" + | "kernelmatrix" -> "kernelMatrix" + | "kernelunitlength" -> "kernelUnitLength" + | "keypoints" -> "keyPoints" + | "keysplines" -> "keySplines" + | "keytimes" -> "keyTimes" + | "lengthadjust" -> "lengthAdjust" + | "limitingconeangle" -> "limitingConeAngle" + | "markerheight" -> "markerHeight" + | "markerunits" -> "markerUnits" + | "markerwidth" -> "markerWidth" + | "maskcontentunits" -> "maskContentUnits" + | "maskunits" -> "maskUnits" + | "numoctaves" -> "numOctaves" + | "pathlength" -> "pathLength" + | "patterncontentunits" -> "patternContentUnits" + | "patterntransform" -> "patternTransform" + | "patternunits" -> "patternUnits" + | "pointsatx" -> "pointsAtX" + | "pointsaty" -> "pointsAtY" + | "pointsatz" -> "pointsAtZ" + | "preservealpha" -> "preserveAlpha" + | "preserveaspectratio" -> "preserveAspectRatio" + | "primitiveunits" -> "primitiveUnits" + | "refx" -> "refX" + | "refy" -> "refY" + | "repeatcount" -> "repeatCount" + | "repeatdur" -> "repeatDur" + | "requiredextensions" -> "requiredExtensions" + | "requiredfeatures" -> "requiredFeatures" + | "specularconstant" -> "specularConstant" + | "specularexponent" -> "specularExponent" + | "spreadmethod" -> "spreadMethod" + | "startoffset" -> "startOffset" + | "stddeviation" -> "stdDeviation" + | "stitchtiles" -> "stitchTiles" + | "surfacescale" -> "surfaceScale" + | "systemlanguage" -> "systemLanguage" + | "tablevalues" -> "tableValues" + | "targetx" -> "targetX" + | "targety" -> "targetY" + | "textlength" -> "textLength" + | "viewbox" -> "viewBox" + | "viewtarget" -> "viewTarget" + | "xchannelselector" -> "xChannelSelector" + | "ychannelselector" -> "yChannelSelector" + | "zoomandpan" -> "zoomAndPan" + | _ -> name + in + ((ns, name), value)) + + let adjust_svg_tag_name = function + | "altglyph" -> "altGlyph" + | "altglyphdef" -> "altGlyphDef" + | "altglyphitem" -> "altGlyphItem" + | "animatecolor" -> "animateColor" + | "animatemotion" -> "animateMotion" + | "animatetransform" -> "animateTransform" + | "clippath" -> "clipPath" + | "feblend" -> "feBlend" + | "fecolormatrix" -> "feColorMatrix" + | "fecomponenttransfer" -> "feComponentTransfer" + | "fecomposite" -> "feComposite" + | "feconvolvematrix" -> "feConvolveMatrix" + | "fediffuselighting" -> "feDiffuseLighting" + | "fedisplacementmap" -> "feDisplacementMap" + | "fedistantlight" -> "feDistantLight" + | "fedropshadow" -> "feDropShadow" + | "feflood" -> "feFlood" + | "fefunca" -> "feFuncA" + | "fefuncb" -> "feFuncB" + | "fefuncg" -> "feFuncG" + | "fefuncr" -> "feFuncR" + | "fegaussianblur" -> "feGaussianBlur" + | "feimage" -> "feImage" + | "femerge" -> "feMerge" + | "femergenode" -> "feMergeNode" + | "femorphology" -> "feMorphology" + | "feoffset" -> "feOffset" + | "fepointlight" -> "fePointLight" + | "fespecularlighting" -> "feSpecularLighting" + | "fespotlight" -> "feSpotLight" + | "fetile" -> "feTile" + | "feturbulence" -> "feTurbulence" + | "foreignobject" -> "foreignObject" + | "glyphref" -> "glyphRef" + | "lineargradient" -> "linearGradient" + | "radialgradient" -> "radialGradient" + | "textpath" -> "textPath" + | s -> s +end + +(* Stack of open elements. *) +module Stack : sig + type t = element list ref * int + + val create : ?limit:int -> unit -> t + val elements : t -> element list ref + val current_element : t -> element option + val require_current_element : t -> element + val adjusted_current_element : Context.t -> t -> element option + val current_element_is : t -> string list -> bool + val current_element_is_foreign : Context.t -> t -> bool + val has : t -> string -> bool + val in_scope : t -> string -> bool + val in_button_scope : t -> string -> bool + val in_list_item_scope : t -> string -> bool + val in_table_scope : t -> string -> bool + val in_select_scope : t -> string -> bool + val one_in_scope : t -> string list -> bool + val one_in_table_scope : t -> string list -> bool + val target_in_scope : t -> element -> bool + val remove : t -> element -> unit + val replace : t -> old:element -> new_:element -> unit + val insert_below : t -> anchor:element -> new_:element -> unit +end = struct + type t = element list ref * int + (** The Adoption Agency algorithm sometimes push too many elements when trying + to recover from malformed HTMLs. Most of the time such HTML *) + + let create ?(limit = Int.max_int) () = (ref [], limit) + let elements (el, _) = el + + let current_element (open_elements, _) = + match !open_elements with [] -> None | element :: _ -> Some element + + let require_current_element t = + match current_element t with + | None -> failwith "require_current_element: None" + | Some element -> element + + let adjusted_current_element context (open_elements, _) = + match (!open_elements, Context.element context) with + | [ _ ], Some element -> Some element + | [], _ -> None + | element :: _, _ -> Some element + + let current_element_is (open_elements, _) names = + match !open_elements with + | { element_name = HTML, name } :: _ -> list_mem_string name names + | _ -> false + + let current_element_is_foreign context t = + match adjusted_current_element context t with + | Some { element_name = ns, _ } when ns <> HTML -> true + | _ -> false + + let has (open_elements, _) name = + List.exists + (fun { element_name = ns, name' } -> ns = HTML && name' = name) + !open_elements + + let in_scope_general is_delimiter (open_elements, depth_limit) name' = + let rec scan depth = function + | [] -> false + | _ when depth = 0 -> failwith "in_scope_general: depth limit reached" + | { element_name = (ns, name'') as name } :: more -> + if ns = HTML && name'' = name' then true + else if is_delimiter name then false + else scan (depth - 1) more + in + scan depth_limit !open_elements + + let is_scope_delimiter = function + | ( HTML, + ( "applet" | "caption" | "html" | "table" | "td" | "th" | "marquee" + | "object" | "template" ) ) -> + true + | MathML, ("mi" | "mo" | "mn" | "ms" | "mtext" | "annotation-xml") -> true + | SVG, ("foreignObject" | "desc" | "title") -> true + | _ -> false + + let is_button_scope_delimiter = function + | HTML, "button" -> true + | name -> is_scope_delimiter name + + let is_list_item_scope_delimiter = function + | HTML, ("ol" | "ul") -> true + | name -> is_scope_delimiter name + + let is_table_scope_delimiter = function + | HTML, ("html" | "table" | "template") -> true + | _ -> false + + let in_scope = in_scope_general is_scope_delimiter + let in_button_scope = in_scope_general is_button_scope_delimiter + let in_list_item_scope = in_scope_general is_list_item_scope_delimiter + let in_table_scope = in_scope_general is_table_scope_delimiter + + let in_select_scope (open_elements, depth_limit) name = + let rec scan depth = function + | [] -> false + | _ when depth = 0 -> failwith "in_select_scope: depth limit reached" + | { element_name = ns, name' } :: more -> + if ns <> HTML then false + else if name' = name then true + else if name' = "optgroup" || name' = "option" then + scan (depth - 1) more + else false + in + scan depth_limit !open_elements + + let one_in_scope (open_elements, depth_limit) names = + let rec scan depth = function + | [] -> false + | _ when depth = 0 -> failwith "one_in_scope: depth limit reached" + | { element_name = (ns, name') as name } :: more -> + if ns = HTML && list_mem_string name' names then true + else if is_scope_delimiter name then false + else scan (depth - 1) more + in + scan depth_limit !open_elements + + let one_in_table_scope (open_elements, depth_limit) names = + let rec scan depth = function + | [] -> false + | _ when depth = 0 -> failwith "one_in_table_scope: depth limit reached" + | { element_name = (ns, name') as name } :: more -> + if ns = HTML && list_mem_string name' names then true + else if is_table_scope_delimiter name then false + else scan (depth - 1) more + in + scan depth_limit !open_elements + + let target_in_scope (open_elements, depth_limit) node = + let rec scan depth = function + | [] -> false + | _ when depth = 0 -> failwith "target_in_scope: depth limit reached" + | e :: more -> + if e == node then true + else if is_scope_delimiter node.element_name then false + else scan (depth - 1) more + in + scan depth_limit !open_elements + + let remove (open_elements, _) element = + open_elements := List.filter (( != ) element) !open_elements; + element.is_open <- false + + let replace (open_elements, _) ~old ~new_ = + open_elements := + List.map + (fun e -> + if e == old then ( + e.is_open <- false; + new_) + else e) + !open_elements + + let insert_below (open_elements, _) ~anchor ~new_ = + let rec insert prefix = function + | [] -> List.rev prefix + | e :: more when e == anchor -> + List.rev_append prefix @@ (new_ :: e :: more) + | e :: more -> insert (e :: prefix) more + in + open_elements := insert [] !open_elements +end + +(* List of active formatting elements. *) +module Active : sig + type entry = Marker | Element_ of element * location * Token_tag.t + type t = entry list ref + + val create : unit -> t + val add_marker : t -> unit + val clear_until_marker : t -> unit + val has : t -> element -> bool + val remove : t -> element -> unit + val replace : t -> old:element -> new_:element -> unit + val insert_after : t -> anchor:element -> new_:element -> unit + val has_before_marker : t -> string -> element option +end = struct + type entry = Marker | Element_ of element * location * Token_tag.t + type t = entry list ref + + let create () = ref [] + + let add_marker active_formatting_elements = + active_formatting_elements := Marker :: !active_formatting_elements + + let clear_until_marker active_formatting_elements = + let rec iterate = function + | Marker :: rest -> rest + | Element_ _ :: rest -> iterate rest + | [] -> [] + in + active_formatting_elements := iterate !active_formatting_elements + + let has active_formatting_elements element = + !active_formatting_elements + |> List.exists (function + | Element_ (e, _, _) when e == element -> true + | _ -> false) + + let remove active_formatting_elements element = + active_formatting_elements := + !active_formatting_elements + |> List.filter (function + | Element_ (e, _, _) when e == element -> false + | _ -> true) + + let replace active_formatting_elements ~old ~new_ = + active_formatting_elements := + !active_formatting_elements + |> List.map (function + | Element_ (e, l, t) when e == old -> Element_ (new_, l, t) + | e -> e) + + let insert_after active_formatting_elements ~anchor ~new_ = + let rec insert prefix = function + | [] -> List.rev prefix + | (Element_ (e, l, t) as v) :: more when e == anchor -> + let new_entry = Element_ (new_, l, t) in + List.rev_append prefix (v :: new_entry :: more) + | v :: more -> insert (v :: prefix) more + in + active_formatting_elements := insert [] !active_formatting_elements + + let has_before_marker active_formatting_elements name = + let rec scan = function + | [] | Marker :: _ -> None + | Element_ (n, _, _) :: _ when n.element_name = (HTML, name) -> Some n + | _ :: more -> scan more + in + scan !active_formatting_elements +end + +type mode = unit -> unit + +(* Stack of template insertion modes. *) +module Template : sig + type t = mode list ref + + val create : unit -> t + val push : t -> mode -> unit + val pop : t -> unit +end = struct + type t = (unit -> unit) list ref + + let create () = ref [] + + let push template_insertion_modes mode = + template_insertion_modes := mode :: !template_insertion_modes + + let pop template_insertion_modes = + match !template_insertion_modes with + | [] -> () + | _ :: rest -> template_insertion_modes := rest +end + +(* Subtree buffers. HTML specifies the "adoption agency algorithm" for + recovering from certain kinds of errors. This algorithm is (apparently) + incompatible with streaming parsers that do not maintain a DOM - such as + Markup.ml. So, when the Markup.ml parser encounters a situation in which it + may be necessary to later run the adoption agency algorithm, it buffers its + signal output. Instead of being emitted, the signals are used to construct a + DOM subtree. If the algorithm is run, it is run on this subtree. Whenever the + parser can "prove" that the subtree can no longer be involved in the adoption + agency algorithm, it serializes the subtree into the signal stream. In + practice, this means that buffering begins when a formatting element is + encountered, and ends when the parent of the formatting element is popped off + the open element stack. *) +module Subtree : sig + type t + + val create : Stack.t -> t + val accumulate : t -> location -> signal -> bool + val enable : t -> unit + val disable : t -> (location * signal) list + + val adoption_agency_algorithm : + t -> Active.t -> location -> string -> bool * (location * Error.t) list + + val buffering : t -> bool +end = struct + type t = { + open_elements : Stack.t; + mutable enabled : bool; + mutable position : element; + } + + let create open_elements = + { open_elements; enabled = false; position = Element.dummy } + + let accumulate subtree_buffer l s = + if not subtree_buffer.enabled then true + else begin + begin match s with + | `Start_element (_, attributes) -> + let parent = subtree_buffer.position in + let child = + Stack.require_current_element subtree_buffer.open_elements + in + + child.attributes <- attributes; + child.parent <- parent; + parent.children <- (l, Element child) :: parent.children; + + subtree_buffer.position <- child + | `End_element -> + subtree_buffer.position.end_location <- l; + subtree_buffer.position <- + Stack.require_current_element subtree_buffer.open_elements + | `Text ss -> + subtree_buffer.position.children <- + (l, Text ss) :: subtree_buffer.position.children + | `PI (t, s) -> + subtree_buffer.position.children <- + (l, PI (t, s)) :: subtree_buffer.position.children + | `Comment s -> + subtree_buffer.position.children <- + (l, Comment s) :: subtree_buffer.position.children + | `Xml _ | `Doctype _ -> () + end; + + false + end + + let enable subtree_buffer = + if subtree_buffer.enabled then () + else + match Stack.current_element subtree_buffer.open_elements with + | None -> () + | Some element -> + element.buffering <- true; + subtree_buffer.position <- element; + subtree_buffer.enabled <- true + + let buffering subtree_buffer = subtree_buffer.enabled + + let disable subtree_buffer = + let _, depth_limit = subtree_buffer.open_elements in + let rec traverse depth acc = function + | _ when depth = 0 -> failwith "Subtree.disable: depth limit reached" + | l, Element { element_name; attributes; end_location; children } -> + let name = (Ns.to_string (fst element_name), snd element_name) in + let start_signal = (l, `Start_element (name, attributes)) in + let end_signal = (end_location, `End_element) in + start_signal + :: List.fold_left (traverse (depth - 1)) (end_signal :: acc) children + | l, Text ss -> + begin match acc with + | (_, `Text ss') :: rest -> (l, `Text (ss @ ss')) :: rest + | _ -> (l, `Text ss) :: acc + end + | l, PI (t, s) -> (l, `PI (t, s)) :: acc + | l, Comment s -> (l, `Comment s) :: acc + in + + let result = + List.fold_left (traverse depth_limit) [] + (Stack.require_current_element subtree_buffer.open_elements).children + in + + subtree_buffer.enabled <- false; + + result + + (* Part of 8.2.5.4.7. *) + let adoption_agency_algorithm subtree_buffer active_formatting_elements l + subject = + let ((open_elements, _) as stack) = subtree_buffer.open_elements in + + let above_removed_nodes = ref [] in + + let rec above_in_stack node = function + | e :: e' :: _ when e == node -> e' + | _ :: more -> above_in_stack node more + | [] -> failwith "above_in_stack: not found" + in + + let above_node node = + if node.is_open then above_in_stack node !open_elements + else + try List.find (fun (e, _) -> e == node) !above_removed_nodes |> snd + with Not_found -> failwith "above_node: not found" + in + + let remove_node node = + above_removed_nodes := + (node, above_in_stack node !open_elements) :: !above_removed_nodes; + Stack.remove stack node + in + + let reparent node new_parent = + let old_parent = node.parent in + + let entry, filtered_children = + let rec remove prefix = function + | ((_, Element e) as entry) :: rest when e == node -> + (entry, List.rev_append prefix rest) + | e :: rest -> remove (e :: prefix) rest + | [] -> ((node.location, Element node), old_parent.children) + in + remove [] old_parent.children + in + + old_parent.children <- filtered_children; + new_parent.children <- entry :: new_parent.children; + node.parent <- new_parent + in + + let inner_loop formatting_element furthest_block = + let rec repeat inner_loop_counter node last_node bookmark = + let node = above_node node in + + if node == formatting_element then (last_node, bookmark) + else begin + if inner_loop_counter > 3 then + Active.remove active_formatting_elements node; + + if not @@ Active.has active_formatting_elements node then begin + remove_node node; + repeat (inner_loop_counter + 1) node last_node bookmark + end + else begin + let new_node = + { + node with + is_open = true; + children = []; + parent = Element.dummy; + } + in + + node.end_location <- l; + + Stack.replace stack ~old:node ~new_:new_node; + Active.replace active_formatting_elements ~old:node ~new_:new_node; + + reparent last_node new_node; + + repeat (inner_loop_counter + 1) new_node new_node + (if last_node == furthest_block then Some new_node else bookmark) + end + end + in + repeat 1 furthest_block furthest_block None + in + + let find_formatting_element () = + let rec scan = function + | [] -> None + | Active.Marker :: _ -> None + | Active.Element_ (({ element_name = HTML, n } as e), _, _) :: _ + when n = subject -> + Some e + | _ :: rest -> scan rest + in + scan !active_formatting_elements + in + + let find_furthest_block formatting_element = + let rec scan furthest = function + | [] -> furthest + | e :: _ when e == formatting_element -> furthest + | e :: more when Element.is_special e.element_name -> scan (Some e) more + | _ :: more -> scan furthest more + in + scan None !open_elements + in + + let pop_to_formatting_element formatting_element = + let rec pop () = + match !open_elements with + | [] -> () + | e :: more -> + open_elements := more; + e.is_open <- false; + e.end_location <- l; + if e != formatting_element then pop () + in + pop (); + subtree_buffer.position <- Stack.require_current_element stack + in + + let rec outer_loop outer_loop_counter errors = + let outer_loop_counter = outer_loop_counter + 1 in + + if outer_loop_counter >= 8 then (true, List.rev errors) + else + begin match find_formatting_element () with + | None -> (false, List.rev errors) + | Some formatting_element -> + if not formatting_element.is_open then begin + Active.remove active_formatting_elements formatting_element; + (true, List.rev ((l, `Unmatched_end_tag subject) :: errors)) + end + else + begin if not @@ Stack.target_in_scope stack formatting_element + then begin + (true, List.rev ((l, `Unmatched_end_tag subject) :: errors)) + end + else begin + let errors = + if Stack.require_current_element stack == formatting_element + then errors + else (l, `Unmatched_end_tag subject) :: errors + in + + match find_furthest_block formatting_element with + | None -> + pop_to_formatting_element formatting_element; + Active.remove active_formatting_elements formatting_element; + (true, List.rev errors) + | Some furthest_block -> + formatting_element.end_location <- l; + + let common_ancestor = + above_in_stack formatting_element !open_elements + in + + let last_node, bookmark = + inner_loop formatting_element furthest_block + in + + reparent last_node common_ancestor; + + let new_node = + { + formatting_element with + is_open = true; + children = []; + parent = Element.dummy; + } + in + + new_node.children <- furthest_block.children; + furthest_block.children <- []; + new_node.children + |> List.iter (function + | _, Element child -> child.parent <- new_node + | _ -> ()); + + reparent new_node furthest_block; + + begin match bookmark with + | None -> + Active.replace active_formatting_elements + ~old:formatting_element ~new_:new_node + | Some node -> + Active.remove active_formatting_elements + formatting_element; + Active.insert_after active_formatting_elements + ~anchor:node ~new_:new_node + end; + + Stack.remove stack formatting_element; + Stack.insert_below stack ~anchor:furthest_block + ~new_:new_node; + + outer_loop outer_loop_counter errors + end + end + end + in + + let current_node = Stack.require_current_element stack in + if current_node.element_name = (HTML, subject) then begin + open_elements := List.tl !open_elements; + current_node.is_open <- false; + current_node.end_location <- l; + begin match Stack.current_element stack with + | Some element -> subtree_buffer.position <- element + | None -> () + end; + Active.remove active_formatting_elements current_node; + (true, []) + end + else outer_loop 0 [] +end + +let parse ?depth_limit requested_context report tokens = + let context = Context.uninitialized () in + let tokenizer_state = ref Data in + let tokenizer_drop_candidate = ref false in + let token_location = Token_source.location () in + let next_token tokens = + let state = !tokenizer_state in + if state <> Data then tokenizer_state := Data; + let drop_candidate = !tokenizer_drop_candidate in + if drop_candidate then tokenizer_drop_candidate := false; + let token = Token_source.next tokens state ~drop_candidate token_location in + ((token_location.line, token_location.column), token) + in + let push = Token_source.push in + let set_tokenizer_state state = tokenizer_state := state in + + let throw = ref (fun _ -> ()) in + let ended = ref (fun _ -> ()) in + let output = ref (fun _ -> ()) in + + let remove_nulls s = + if String.contains s '\x00' then + String.concat "" (String.split_on_char '\x00' s) + else s + in + let replace_nulls s = + if String.contains s '\x00' then begin + let buffer = Buffer.create (String.length s + 8) in + String.iter + (fun byte -> + if byte = '\x00' then Buffer.add_string buffer "\xEF\xBF\xBD" + else Buffer.add_char buffer byte) + s; + Buffer.contents buffer + end + else s + in + let report_if = Error.report_if report in + let unmatched_end_tag l name k = + report l (`Unmatched_end_tag name) !throw k + in + let misnested_tag l t context_name k = + report l + (`Misnested_tag (t.name, context_name, t.Token_tag.attributes)) + !throw k + in + + let open_elements = Stack.create ?limit:depth_limit () in + let active_formatting_elements = Active.create () in + let subtree_buffer = Subtree.create open_elements in + let text = Text.prepare () in + let template_insertion_modes = Template.create () in + let frameset_ok = ref true in + let head_seen = ref false in + let form_element_pointer = ref None in + + let add_character = Text.add text in + let add_string = Text.add_string text in + + Token_source.set_foreign tokens (fun () -> + Stack.current_element_is_foreign context open_elements); + + let report_if_stack_has_other_than names k = + let rec iterate = function + | [] -> k () + | { element_name = ns, name; location } :: more -> + report_if + (not (ns = HTML && list_mem_string name names)) + location + (fun () -> `Unmatched_start_tag name) + !throw + (fun () -> iterate more) + in + iterate !(Stack.elements open_elements) + in + + let rec current_mode = ref initial_mode + and constructor throw_ k = + Context.initialize requested_context context throw_ (fun () -> + let initial_tokenizer_state = + match Context.the_context context with + | Fragment (HTML, ("title" | "textarea")) -> RCDATA + | Fragment + (HTML, ("style" | "xmp" | "iframe" | "noembed" | "noframes")) -> + RAWTEXT + | Fragment (HTML, "script") -> Script_data + | Fragment (HTML, "plaintext") -> PLAINTEXT + | _ -> Data + in + + set_tokenizer_state initial_tokenizer_state; + + begin match Context.the_context context with + | Document -> () + | Fragment _ -> + let notional_root = + Element.create ~suppress:true (HTML, "html") (1, 1) + in + Stack.elements open_elements := [ notional_root ] + end; + + begin match Context.the_context context with + | Fragment (HTML, "template") -> + Template.push template_insertion_modes in_template_mode + | _ -> () + end; + + (* The following is a deviation from conformance. The goal is to avoid + insertion of a element into a fragment beginning with a or + element. *) + begin match Context.token context with + | Some ("body" | "frameset") -> head_seen := true + | _ -> () + end; + + current_mode := + begin match Context.the_context context with + | Fragment _ -> reset_mode () + | Document -> initial_mode + end; + + (fun throw_ e k -> + throw := throw_; + ended := e; + output := k; + !current_mode ()) + |> make |> k) + (* 8.2.3.1. *) + and reset_mode () = + let rec iterate last = function + | [ e ] when (not last) && Context.the_context context <> Document -> + begin match Context.the_context context with + | Document -> assert false + | Fragment name -> iterate true [ { e with element_name = name } ] + end + | { element_name = _, "select" } :: ancestors -> + let rec iterate' = function + | [] -> in_select_mode + | { element_name = _, "template" } :: _ -> in_select_mode + | { element_name = _, "table" } :: _ -> in_select_in_table_mode + | _ :: ancestors -> iterate' ancestors + in + iterate' ancestors + | { element_name = _, ("tr" | "th") } :: _ :: _ -> in_cell_mode + | { element_name = _, "tr" } :: _ -> in_row_mode + | { element_name = _, ("tbody" | "thead" | "tfoot") } :: _ -> + in_table_body_mode + | { element_name = _, "caption" } :: _ -> in_caption_mode + | { element_name = _, "colgroup" } :: _ -> in_column_group_mode + | { element_name = _, "table" } :: _ -> in_table_mode + | { element_name = _, "template" } :: _ -> + begin match !template_insertion_modes with + | [] -> initial_mode (* This is an internal error, actually. *) + | mode :: _ -> mode + end + (* The next case corresponds to item 12 of "Resetting the insertion mode + appropriately." It is commented out as deliberate deviation from the + specification, because that makes parsing of fragments intended for + elements more intuitive. For conformance, the pattern in the + following case would have to end with ::_::_, not ::_. *) + (* | [{element_name = _, "head"}] -> in_body_mode *) + | { element_name = _, "head" } :: _ -> in_head_mode + | { element_name = _, "body" } :: _ -> in_body_mode + | { element_name = _, "frameset" } :: _ -> in_frameset_mode + | { element_name = _, "html" } :: _ -> + if !head_seen then after_head_mode else before_head_mode + | _ :: rest -> iterate last rest + | [] -> in_body_mode + in + iterate false !(Stack.elements open_elements) + and emit' l s m = + if Subtree.accumulate subtree_buffer l s then begin + current_mode := m; + !output (l, s) + end + else m () + and emit_list ss m = + match ss with + | [] -> m () + | (l, s) :: more -> emit' l s (fun () -> emit_list more m) + and emit_text m = + match Text.emit text with + | None -> m () + | Some (l', strings) -> emit' l' (`Text strings) m + and emit l s m = emit_text (fun () -> emit' l s m) + and push_and_emit ?(formatting = false) ?(acknowledge = false) + ?(namespace = HTML) ?(set_form_element_pointer = false) location + ({ Token_tag.name; attributes; self_closing } as tag) mode = + report_if + (self_closing && not acknowledge) + location + (fun () -> `Bad_token ("/>", "tag", "should not be self-closing")) + !throw + (fun () -> + let namespace_string = Ns.to_string namespace in + + let tag_name = + match namespace with + | SVG -> Foreign.adjust_svg_tag_name name + | _ -> name + in + + let is_html_integration_point = + Foreign.is_html_integration_point namespace tag_name attributes + in + + let attributes = + List.map (fun (n, v) -> (Namespace.Parsing.parse n, v)) attributes + in + let attributes = + match namespace with + | HTML | Other _ -> attributes + | MathML -> Foreign.adjust_mathml_attributes attributes + | SVG -> Foreign.adjust_svg_attributes attributes + in + + let element_entry = + Element.create ~is_html_integration_point (namespace, name) location + in + let elements_ref = Stack.elements open_elements in + elements_ref := element_entry :: !elements_ref; + + if set_form_element_pointer then + form_element_pointer := Some element_entry; + + if formatting then + active_formatting_elements := + Active.Element_ (element_entry, location, tag) + :: !active_formatting_elements; + + emit location + (`Start_element ((namespace_string, tag_name), attributes)) + mode) + and push_implicit location name mode = + push_and_emit location + { Token_tag.name; attributes = []; self_closing = false } + mode + and pop location mode = + match !(Stack.elements open_elements) with + | [] -> mode () + | element :: more -> + emit_text (fun () -> + (fun k -> + if not element.buffering then k () + else emit_list (Subtree.disable subtree_buffer) k) (fun () -> + Stack.elements open_elements := more; + element.is_open <- false; + if element.suppress then mode () + else emit' location `End_element mode)) + and pop_until condition location mode = + let rec iterate () = + match !(Stack.elements open_elements) with + | [] -> mode () + | element :: _ -> + if condition element then mode () else pop location iterate + in + iterate () + and close_element ?(ns = HTML) l name mode = + pop_until + (fun { element_name = ns', name' } -> ns' = ns && name' = name) + l + (fun () -> pop l mode) + and pop_until_and_raise_errors names location mode = + let rec iterate () = + match !(Stack.elements open_elements) with + | [] -> mode () + | { element_name = ns, name } :: _ -> + if ns = HTML && list_mem_string name names then pop location mode + else + report location (`Unmatched_start_tag name) !throw (fun () -> + pop location iterate) + in + iterate () + and pop_implied ?(except = "") location mode = + pop_until + (fun { element_name = _, name } -> + name = except + || not + @@ list_mem_string name + [ + "dd"; + "dt"; + "li"; + "option"; + "optgroup"; + "p"; + "rb"; + "rp"; + "rt"; + "rtc"; + ]) + location mode + and pop_to_table_context location mode = + pop_until + (function + | { element_name = HTML, ("table" | "template" | "html") } -> true + | _ -> false) + location mode + and pop_to_table_body_context location mode = + pop_until + (function + | { + element_name = + HTML, ("tbody" | "thead" | "tfoot" | "template" | "html"); + } -> + true + | _ -> false) + location mode + and pop_to_table_row_context location mode = + pop_until + (function + | { element_name = HTML, ("tr" | "template" | "html") } -> true + | _ -> false) + location mode + and close_element_with_implied name location mode = + pop_implied ~except:name location (fun () -> + let check_element k = + match Stack.current_element open_elements with + | Some { element_name = HTML, name' } when name' = name -> k () + | Some { element_name = _, name; location } -> + report location (`Unmatched_start_tag name) !throw k + | None -> unmatched_end_tag location name k + in + check_element (fun () -> close_element location name mode)) + and close_cell location mode = + pop_implied location (fun () -> + (fun mode -> + match Stack.current_element open_elements with + | Some { element_name = HTML, ("td" | "th") } -> mode () + | Some { element_name = _, name } -> + unmatched_end_tag location name mode + | None -> unmatched_end_tag location "" mode) + @@ fun () -> + pop_until + (function + | { element_name = HTML, ("td" | "th") } -> true | _ -> false) + location + (fun () -> pop location mode)) + and close_current_p_element l mode = + if Stack.in_button_scope open_elements "p" then + close_element_with_implied "p" l mode + else mode () + and close_preceding_tag names l mode = + let rec scan = function + | [] -> mode () + | { element_name = (ns, name) as name' } :: more -> + if ns = HTML && list_mem_string name names then + close_element_with_implied name l mode + else if + Element.is_special name' + && + match name' with + | HTML, ("address" | "div" | "p") -> false + | _ -> true + then mode () + else scan more + in + scan !(Stack.elements open_elements) + and emit_end l = + pop_until (fun _ -> false) l (fun () -> emit_text (fun () -> !ended ())) + and reconstruct_active_formatting_elements mode = + let rec get_prefix prefix = function + | [] -> (prefix, []) + | Active.Marker :: _ as l -> (prefix, l) + | Active.Element_ ({ is_open = true }, _, _) :: _ as l -> (prefix, l) + | Active.Element_ ({ is_open = false }, l, tag) :: more -> + get_prefix ((l, tag) :: prefix) more + in + let to_reopen, remainder = get_prefix [] !active_formatting_elements in + active_formatting_elements := remainder; + + begin match to_reopen with + | [] -> () + | _ :: _ -> Subtree.enable subtree_buffer + end; + + let rec reopen = function + | [] -> mode () + | (l, tag) :: more -> + push_and_emit ~formatting:true l tag (fun () -> reopen more) + in + reopen to_reopen + (* 8.2.5. *) + and dispatch tokens rules = + match next_token tokens with + | (_, t) as v -> + let foreign = + match (Stack.adjusted_current_element context open_elements, t) with + | None, _ -> false + | Some { element_name = HTML, _ }, _ -> false + | Some { element_name }, Start { name } + when Foreign.is_mathml_text_integration_point element_name + && name <> "mglyph" && name <> "malignmark" -> + false + | ( Some { element_name = MathML, "annotation-xml" }, + Start { name = "svg" } ) -> + false + | Some { is_html_integration_point = true }, Start _ -> false + | Some { is_html_integration_point = true }, Char _ -> false + | Some { is_html_integration_point = true }, String _ -> false + | _, EOF -> false + | _ -> true + in + + if not foreign then rules v + else foreign_content !current_mode (fun () -> rules v) v + | exception exn -> !throw exn + (* 8.2.5.4.1. *) + and initial_mode () = + dispatch tokens begin function + | _, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) -> + initial_mode () + | _, String s when is_whitespace_only s -> initial_mode () + | l, Comment s -> emit l (`Comment s) initial_mode + | l, Doctype d -> emit l (`Doctype d) before_html_mode + | v -> + push tokens v; + before_html_mode () + end + (* 8.2.5.4.2. *) + and before_html_mode () = + dispatch tokens begin function + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + before_html_mode + | l, Comment s -> emit l (`Comment s) before_html_mode + | _, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) -> + before_html_mode () + | _, String s when is_whitespace_only s -> before_html_mode () + | l, Start ({ name = "html" } as t) -> + push_and_emit l t before_head_mode + | l, End { name } + when not @@ list_mem_string name [ "head"; "body"; "html"; "br" ] -> + unmatched_end_tag l name before_html_mode + | (l, _) as v -> + push tokens v; + push_implicit l "html" before_head_mode + end + (* 8.2.5.4.3. *) + and before_head_mode () = + dispatch tokens begin function + | _, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) -> + before_head_mode () + | _, String s when is_whitespace_only s -> before_head_mode () + | l, Comment s -> emit l (`Comment s) before_head_mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + before_head_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "html" before_head_mode v + | l, Start ({ name = "head" } as t) -> + head_seen := true; + push_and_emit l t in_head_mode + | l, End { name } + when not @@ list_mem_string name [ "head"; "body"; "html"; "br" ] -> + report l (`Unmatched_end_tag name) !throw before_head_mode + | (l, _) as v -> + head_seen := true; + push tokens v; + push_implicit l "head" in_head_mode + end + (* 8.2.5.4.4. *) + and in_head_mode () = + dispatch tokens (fun v -> in_head_mode_rules in_head_mode v) + (* 8.2.5.4.4. *) + and in_head_mode_rules mode = function + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + add_character l c; + mode () + | l, String s when is_whitespace_only s -> + add_string l s; + mode () + | l, Comment s -> emit l (`Comment s) mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "head" in_head_mode v + | ( l, + Start + ({ name = "base" | "basefont" | "bgsound" | "link" | "meta" } as t) ) + -> + push_and_emit ~acknowledge:true l t (fun () -> pop l mode) + | l, Start ({ name = "title" } as t) -> + push_and_emit l t (fun () -> parse_rcdata mode) + | l, Start ({ name = "noframes" | "style" } as t) -> + push_and_emit l t (fun () -> parse_rawtext mode) + | l, Start ({ name = "noscript" } as t) -> + push_and_emit l t in_head_noscript_mode + | l, Start ({ name = "script" } as t) -> + push_and_emit l t (fun () -> + set_tokenizer_state Script_data; + set_drop_candidate true; + text_mode mode) + | l, End { name = "head" } -> pop l after_head_mode + | l, Start ({ name = "template" } as t) -> + Active.add_marker active_formatting_elements; + frameset_ok := false; + Template.push template_insertion_modes in_template_mode; + push_and_emit l t in_template_mode + | l, End { name = "template" } -> + if not @@ Stack.has open_elements "template" then + report l (`Unmatched_end_tag "template") !throw mode + else begin + Active.clear_until_marker active_formatting_elements; + Template.pop template_insertion_modes; + close_element_with_implied "template" l (fun () -> reset_mode () ()) + end + | l, Start ({ name = "head" } as t) -> misnested_tag l t "head" mode + | l, End { name } when not @@ list_mem_string name [ "body"; "html"; "br" ] + -> + report l (`Unmatched_end_tag name) !throw mode + | (l, _) as v -> + push tokens v; + pop l after_head_mode + (* 8.2.5.4.5. *) + and in_head_noscript_mode () = + dispatch tokens begin function + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + in_head_noscript_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "noscript" in_head_noscript_mode v + | l, End { name = "noscript" } -> pop l in_head_mode + | (_, String s) as v when is_whitespace_only s -> + in_head_mode_rules in_head_noscript_mode v + | ( _, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) + | _, Comment _ + | ( _, + Start + { + name = + ( "basefont" | "bgsound" | "link" | "meta" | "noframes" + | "style" ); + } ) ) as v -> + in_head_mode_rules in_head_noscript_mode v + | l, Start ({ name = "head" | "noscript" } as t) -> + misnested_tag l t "noscript" in_head_noscript_mode + | l, End { name } when name <> "br" -> + report l (`Unmatched_end_tag name) !throw in_head_noscript_mode + | (l, _) as v -> + report l (`Bad_content "noscript") !throw (fun () -> + push tokens v; + pop l in_head_mode) + end + (* 8.2.5.4.6. *) + and after_head_mode () = + dispatch tokens begin function + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + add_character l c; + after_head_mode () + | l, String s when is_whitespace_only s -> + add_string l s; + after_head_mode () + | l, Comment s -> emit l (`Comment s) after_head_mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + after_head_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "html" after_head_mode v + | l, Start ({ name = "body" } as t) -> + frameset_ok := false; + push_and_emit l t in_body_mode + | l, Start ({ name = "frameset" } as t) -> + push_and_emit l t in_frameset_mode + | ( l, + Start + ({ + name = + ( "base" | "basefont" | "bgsound" | "link" | "meta" + | "noframes" | "script" | "style" | "template" | "title" ); + } as t) ) as v -> + misnested_tag l t "html" (fun () -> + in_head_mode_rules after_head_mode v) + | (_, End { name = "template" }) as v -> + in_head_mode_rules after_head_mode v + | l, Start { name = "head" } -> + report l (`Bad_document "duplicate head element") !throw + after_head_mode + | l, End { name } + when not @@ list_mem_string name [ "body"; "html"; "br" ] -> + report l (`Unmatched_end_tag name) !throw after_head_mode + (* This case is not found in the specification. It is a deliberate + deviation from conformance, so that fragments "..." don't + get an implicit element generated after the element. *) + | l, EOF + when Context.the_context context = Fragment (HTML, "html") + || Context.the_context context = Fragment (HTML, "head") -> + emit_end l + | (l, _) as t -> + push tokens t; + push_implicit l "body" in_body_mode + end + (* 8.2.5.4.7. *) + and in_body_mode () = + dispatch tokens (fun v -> in_body_mode_rules "body" in_body_mode v) + (* 8.2.5.4.7. *) + and in_body_mode_rules context_name mode = function + | l, Char 0 -> report l (`Bad_token ("U+0000", "body", "null")) !throw mode + | l, String s -> + let text = remove_nulls s in + if text = "" && s <> "" then mode () + else + reconstruct_active_formatting_elements (fun () -> + add_string l text; + if not @@ is_whitespace_only text then frameset_ok := false; + mode ()) + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + reconstruct_active_formatting_elements (fun () -> + add_character l c; + mode ()) + | l, Char c -> + frameset_ok := false; + reconstruct_active_formatting_elements (fun () -> + add_character l c; + mode ()) + | l, Comment s -> emit l (`Comment s) mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw mode + | l, Start ({ name = "html" } as t) -> misnested_tag l t context_name mode + | ( ( _, + Start + { + name = + ( "base" | "basefont" | "bgsound" | "link" | "meta" | "noframes" + | "script" | "style" | "template" | "title" ); + } ) + | _, End { name = "template" } ) as v -> + in_head_mode_rules mode v + | l, Start ({ name = "body" } as t) -> misnested_tag l t context_name mode + | l, Start ({ name = "frameset" } as t) -> + misnested_tag l t context_name (fun () -> + match !(Stack.elements open_elements) with + | [ _ ] -> mode () + | _ -> + let rec second_is_body = function + | [ { element_name = HTML, "body" }; _ ] -> true + | [] -> false + | _ :: more -> second_is_body more + in + if not @@ second_is_body !(Stack.elements open_elements) then + mode () + else if not !frameset_ok then mode () + else + (* There is a deviation here due to the nature of the parser: if a + body element has been emitted, it can't be suppressed. *) + pop_until + (fun _ -> + match !(Stack.elements open_elements) with + | [ _ ] -> true + | _ -> false) + l + (fun () -> push_and_emit l t in_frameset_mode)) + | (l, EOF) as v -> + report_if_stack_has_other_than + [ + "dd"; + "dt"; + "li"; + "p"; + "tbody"; + "td"; + "tfoot"; + "th"; + "thead"; + "tr"; + "body"; + "html"; + ] (fun () -> + match !template_insertion_modes with + | [] -> emit_end l + | _ -> in_template_mode_rules mode v) + | l, End { name = "body" } -> + if not @@ Stack.in_scope open_elements "body" then + report l (`Unmatched_end_tag "body") !throw mode + else + report_if_stack_has_other_than + [ + "dd"; + "dt"; + "li"; + "optgroup"; + "option"; + "p"; + "rb"; + "rp"; + "rt"; + "rtc"; + "tbody"; + "td"; + "tfoot"; + "th"; + "thead"; + "tr"; + "body"; + "html"; + ] (fun () -> after_body_mode ()) + | (l, End { name = "html" }) as v -> + if not @@ Stack.in_scope open_elements "body" then + report l (`Unmatched_end_tag "html") !throw mode + else + report_if_stack_has_other_than + [ + "dd"; + "dt"; + "li"; + "optgroup"; + "option"; + "p"; + "rb"; + "rp"; + "rt"; + "rtc"; + "tbody"; + "td"; + "tfoot"; + "th"; + "thead"; + "tr"; + "body"; + "html"; + ] (fun () -> + push tokens v; + after_body_mode ()) + | ( l, + Start + ({ + name = + ( "address" | "article" | "aside" | "blockquote" | "center" + | "details" | "dialog" | "dir" | "div" | "dl" | "fieldset" + | "figcaption" | "figure" | "footer" | "header" | "hgroup" + | "main" | "nav" | "ol" | "p" | "section" | "summary" | "ul" ); + } as t) ) -> + close_current_p_element l (fun () -> push_and_emit l t mode) + | l, Start ({ name = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" } as t) -> + close_current_p_element l (fun () -> + (fun mode' -> + match Stack.current_element open_elements with + | Some + { + element_name = + HTML, (("h1" | "h2" | "h3" | "h4" | "h5" | "h6") as name'); + } -> + misnested_tag l t name' (fun () -> pop l mode') + | _ -> mode' ()) (fun () -> push_and_emit l t mode)) + | l, Start ({ name = "pre" | "listing" } as t) -> + frameset_ok := false; + close_current_p_element l (fun () -> + push_and_emit l t (fun () -> + (* https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element *) + (* In the HTML syntax, a leading newline character immediately following the pre element start tag is stripped. *) + match next_token tokens with + | _, Char 0x000A -> mode () + | loc, String s when String.starts_with ~prefix:"\n" s -> + push tokens + (loc, String (String.sub s 1 (String.length s - 1))); + mode () + | v -> + push tokens v; + mode () + | exception exn -> !throw exn)) + | l, Start ({ name = "form" } as t) -> + if + !form_element_pointer <> None + && (not @@ Stack.has open_elements "template") + then misnested_tag l t "form" mode + else begin + close_current_p_element l (fun () -> + let in_template = Stack.has open_elements "template" in + push_and_emit ~set_form_element_pointer:(not in_template) l t mode) + end + | l, Start ({ name = "li" } as t) -> + frameset_ok := false; + close_preceding_tag [ "li" ] l (fun () -> + close_current_p_element l (fun () -> push_and_emit l t mode)) + | l, Start ({ name = "dd" | "dt" } as t) -> + frameset_ok := false; + close_preceding_tag [ "dd"; "dt" ] l (fun () -> + close_current_p_element l (fun () -> push_and_emit l t mode)) + | l, Start ({ name = "plaintext" } as t) -> + close_current_p_element l (fun () -> + set_tokenizer_state PLAINTEXT; + push_and_emit l t mode) + | l, Start ({ name = "button" } as t) -> + (fun mode' -> + if Stack.in_scope open_elements "button" then + misnested_tag l t "button" (fun () -> + close_element_with_implied "button" l mode') + else mode' ()) (fun () -> + frameset_ok := false; + reconstruct_active_formatting_elements (fun () -> + push_and_emit l t mode)) + | ( l, + End + { + name = + ( "address" | "article" | "aside" | "blockquote" | "button" + | "center" | "details" | "dialog" | "dir" | "div" | "dl" + | "fieldset" | "figcaption" | "figure" | "footer" | "header" + | "hgroup" | "listing" | "main" | "nav" | "ol" | "pre" | "section" + | "summary" | "ul" ) as name; + } ) -> + if not @@ Stack.in_scope open_elements name then + report l (`Unmatched_end_tag name) !throw mode + else close_element_with_implied name l mode + | l, End { name = "form" } -> + if not @@ Stack.has open_elements "template" then begin + let form_element = !form_element_pointer in + form_element_pointer := None; + match form_element with + | Some element when Stack.target_in_scope open_elements element -> + pop_implied l (fun () -> + match Stack.current_element open_elements with + | Some element' when element' == element -> pop l mode + | _ -> + report element.location (`Unmatched_start_tag "form") + !throw (fun () -> + pop_until + (fun element' -> element' == element) + l + (fun () -> pop l mode))) + | _ -> report l (`Unmatched_end_tag "form") !throw mode + end + else if not @@ Stack.in_scope open_elements "form" then + report l (`Unmatched_end_tag "form") !throw mode + else close_element_with_implied "form" l mode + | l, End { name = "p" } -> + (fun mode' -> + if not @@ Stack.in_button_scope open_elements "p" then + report l (`Unmatched_end_tag "p") !throw (fun () -> + push_implicit l "p" mode') + else mode' ()) (fun () -> close_element_with_implied "p" l mode) + | l, End { name = "li" } -> + if not @@ Stack.in_list_item_scope open_elements "li" then + report l (`Unmatched_end_tag "li") !throw mode + else close_element_with_implied "li" l mode + | l, End { name = ("dd" | "dt") as name } -> + if not @@ Stack.in_scope open_elements name then + report l (`Unmatched_end_tag name) !throw mode + else close_element_with_implied name l mode + | l, End { name = ("h1" | "h2" | "h3" | "h4" | "h5" | "h6") as name } -> + if + not + @@ Stack.one_in_scope open_elements + [ "h1"; "h2"; "h3"; "h4"; "h5"; "h6" ] + then report l (`Unmatched_end_tag name) !throw mode + else + pop_implied l (fun () -> + (fun next -> + match Stack.current_element open_elements with + | Some { element_name = HTML, name' } + when list_mem_string name' + [ "h1"; "h2"; "h3"; "h4"; "h5"; "h6" ] -> + next () + | _ -> report l (`Unmatched_end_tag name) !throw next) + @@ fun () -> + pop_until_and_raise_errors + [ "h1"; "h2"; "h3"; "h4"; "h5"; "h6" ] + l mode) + | l, Start ({ name = "a" } as t) -> + (fun k -> + match Active.has_before_marker active_formatting_elements "a" with + | None -> k () + | Some existing -> + misnested_tag l t "a" (fun () -> + adoption_agency_algorithm l "a" (fun () -> + Stack.remove open_elements existing; + Active.remove active_formatting_elements existing; + k ()))) (fun () -> + Subtree.enable subtree_buffer; + reconstruct_active_formatting_elements (fun () -> + push_and_emit ~formatting:true l t mode)) + | ( l, + Start + ({ + name = + ( "b" | "big" | "code" | "em" | "font" | "i" | "s" | "small" + | "strike" | "strong" | "tt" | "u" ); + } as t) ) -> + Subtree.enable subtree_buffer; + reconstruct_active_formatting_elements (fun () -> + push_and_emit ~formatting:true l t mode) + | l, Start ({ name = "nobr" } as t) -> + Subtree.enable subtree_buffer; + reconstruct_active_formatting_elements (fun () -> + (fun k -> + if not @@ Stack.in_scope open_elements "nobr" then k () + else + misnested_tag l t "nobr" (fun () -> + adoption_agency_algorithm l "nobr" (fun () -> + reconstruct_active_formatting_elements k))) (fun () -> + push_and_emit ~formatting:true l t mode)) + | ( l, + End + { + name = + ( "a" | "b" | "big" | "code" | "em" | "font" | "i" | "nobr" | "s" + | "small" | "strike" | "strong" | "tt" | "u" ) as name; + } ) -> + adoption_agency_algorithm l name mode + | l, Start ({ name = "applet" | "marquee" | "object" } as t) -> + frameset_ok := false; + reconstruct_active_formatting_elements (fun () -> + Active.add_marker active_formatting_elements; + push_and_emit l t mode) + | l, End { name = ("applet" | "marquee" | "object") as name } -> + if not @@ Stack.in_scope open_elements name then + report l (`Unmatched_end_tag name) !throw mode + else begin + Active.clear_until_marker active_formatting_elements; + close_element_with_implied name l mode + end + | l, Start ({ name = "table" } as t) -> + frameset_ok := false; + close_current_p_element l (fun () -> push_and_emit l t in_table_mode) + | l, End { name = "br" } -> + report l (`Unmatched_end_tag "br") !throw (fun () -> + in_body_mode_rules context_name mode + ( l, + Start + { + Token_tag.name = "br"; + attributes = []; + self_closing = false; + } )) + | ( l, + Start + ({ name = "area" | "br" | "embed" | "img" | "keygen" | "wbr" } as t) ) + -> + frameset_ok := false; + reconstruct_active_formatting_elements (fun () -> + push_and_emit ~acknowledge:true l t (fun () -> pop l mode)) + | l, Start ({ name = "input" } as t) -> + if Element.is_not_hidden t then frameset_ok := false; + reconstruct_active_formatting_elements (fun () -> + push_and_emit ~acknowledge:true l t (fun () -> pop l mode)) + | l, Start ({ name = "param" | "source" | "track" } as t) -> + push_and_emit ~acknowledge:true l t (fun () -> pop l mode) + | l, Start ({ name = "hr" } as t) -> + frameset_ok := false; + close_current_p_element l (fun () -> + push_and_emit ~acknowledge:true l t (fun () -> pop l mode)) + | l, Start ({ name = "image" } as t) -> + report l + (`Bad_token ("image", "tag", "should be 'img'")) + !throw + (fun () -> + push tokens (l, Start { t with name = "img" }); + mode ()) + | l, Start ({ name = "textarea" } as t) -> + frameset_ok := false; + push_and_emit l t (fun () -> + set_tokenizer_state RCDATA; + set_drop_candidate true; + match next_token tokens with + | _, Char 0x000A -> text_mode mode + | loc, String s when String.starts_with ~prefix:"\n" s -> + push tokens (loc, String (String.sub s 1 (String.length s - 1))); + text_mode mode + | v -> + push tokens v; + text_mode mode + | exception exn -> !throw exn) + | l, Start { name = "xmp" } -> + frameset_ok := false; + close_current_p_element l (fun () -> + reconstruct_active_formatting_elements (fun () -> + parse_rawtext ~emitted:false mode)) + | l, Start ({ name = "iframe" } as t) -> + frameset_ok := false; + push_and_emit l t (fun () -> parse_rawtext mode) + | l, Start ({ name = "noembed" } as t) -> + push_and_emit l t (fun () -> parse_rawtext mode) + | l, Start ({ name = "select" } as t) -> + frameset_ok := false; + select_in_body l t in_select_mode + | l, Start ({ name = "optgroup" | "option" } as t) -> + (fun mode' -> + if Stack.current_element_is open_elements [ "option" ] then + pop l mode' + else mode' ()) (fun () -> + reconstruct_active_formatting_elements (fun () -> + push_and_emit l t mode)) + | l, Start ({ name = "rb" | "rtc" } as t) -> + (fun mode' -> + let finish () = + if Stack.current_element_is open_elements [ "ruby" ] then mode' () + else misnested_tag l t context_name mode' + in + if Stack.in_scope open_elements "ruby" then pop_implied l finish + else finish ()) (fun () -> push_and_emit l t mode) + | l, Start ({ name = "rp" | "rt" } as t) -> + (fun mode' -> + let finish () = + if Stack.current_element_is open_elements [ "ruby"; "rtc" ] then + mode' () + else misnested_tag l t context_name mode' + in + if Stack.in_scope open_elements "ruby" then + pop_implied ~except:"rtc" l finish + else finish ()) (fun () -> push_and_emit l t mode) + | l, Start ({ name = "math" } as t) -> + reconstruct_active_formatting_elements (fun () -> + push_and_emit ~acknowledge:true ~namespace:MathML l t (fun () -> + if t.self_closing then pop l mode else mode ())) + | l, Start ({ name = "svg" } as t) -> + reconstruct_active_formatting_elements (fun () -> + push_and_emit ~acknowledge:true ~namespace:SVG l t (fun () -> + if t.self_closing then pop l mode else mode ())) + | ( l, + Start + ({ + name = + ( "caption" | "col" | "colgroup" | "frame" | "head" | "tbody" + | "td" | "tfoot" | "th" | "thead" | "tr" ); + } as t) ) -> + misnested_tag l t context_name mode + | l, Start t -> + reconstruct_active_formatting_elements (fun () -> + push_and_emit l t mode) + | l, End { name } -> any_other_end_tag_in_body l name mode + (* Part of 8.2.5.4.7. *) + and any_other_end_tag_in_body l name mode = + let rec close = function + | [] -> mode () + | { element_name = (ns, name') as name'' } :: rest -> + if ns = HTML && name' = name then + pop_implied ~except:name l (fun () -> pop l mode) + else if Element.is_special name'' then + report l (`Unmatched_end_tag name) !throw mode + else close rest + in + close !(Stack.elements open_elements) + (* Part of 8.2.5.4.7. *) + and adoption_agency_algorithm l name mode = + Subtree.enable subtree_buffer; + emit_text (fun () -> + let handled, errors = + Subtree.adoption_agency_algorithm subtree_buffer + active_formatting_elements l name + in + let rec report_all errors k = + match errors with + | [] -> k () + | (l, error) :: more -> + report l error !throw (fun () -> report_all more k) + in + report_all errors (fun () -> + if not handled then any_other_end_tag_in_body l name mode + else mode ())) + (* Part of 8.2.5.4.7. *) + and select_in_body l t next_mode = + frameset_ok := false; + reconstruct_active_formatting_elements (fun () -> + push_and_emit l t next_mode) + (* 8.2.5.4.8. *) + and text_mode original_mode = + dispatch tokens begin function + | l, Char c -> + add_character l c; + text_mode original_mode + | l, String s -> + add_string l s; + text_mode original_mode + | (l, EOF) as v -> + report l (`Unexpected_eoi "content") !throw (fun () -> + push tokens v; + pop l original_mode) + | l, End _ -> pop l original_mode + | _ -> text_mode original_mode + end + (* 8.2.5.2. *) + and parse_rcdata ?(emitted = true) original_mode = + set_tokenizer_state RCDATA; + set_drop_candidate emitted; + text_mode original_mode + (* 8.2.5.2. *) + and parse_rawtext ?(emitted = true) original_mode = + set_tokenizer_state RAWTEXT; + set_drop_candidate emitted; + text_mode original_mode + (* Baseline resets the tokenizer state per character while its stale + [current_mode] is the closure entering a text state and characters + dispatch to foreign content, discarding a pending end-tag candidate + after its '<'. That configuration is decided here: the closure is + stale-reachable only when the start tag's emission updated + [current_mode]. *) + and set_drop_candidate emitted = + tokenizer_drop_candidate := + emitted && not (Subtree.buffering subtree_buffer) + and anything_else_in_table mode ((l, _) as v) = + report l (`Bad_content "table") !throw (fun () -> + in_body_mode_rules "table" mode v) + (* 8.2.5.4.9. *) + and in_table_mode () = + dispatch tokens (fun v -> in_table_mode_rules in_table_mode v) + and in_table_mode_rules mode = function + | (_, Char _ | _, String _) as v + when Stack.current_element_is open_elements + [ "table"; "tbody"; "tfoot"; "thead"; "tr" ] -> + push tokens v; + in_table_text_mode true [] mode + | l, Comment s -> emit l (`Comment s) mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw mode + | l, Start ({ name = "caption" } as t) -> + pop_to_table_context l (fun () -> + Active.add_marker active_formatting_elements; + push_and_emit l t in_caption_mode) + | l, Start ({ name = "colgroup" } as t) -> + pop_to_table_context l (fun () -> + push_and_emit l t in_column_group_mode) + | (l, Start { name = "col" }) as v -> + pop_to_table_context l (fun () -> + push tokens v; + push_implicit l "colgroup" in_column_group_mode) + | l, Start ({ name = "tbody" | "tfoot" | "thead" } as t) -> + pop_to_table_context l (fun () -> push_and_emit l t in_table_body_mode) + | (l, Start { name = "td" | "th" | "tr" }) as v -> + pop_to_table_context l (fun () -> + push tokens v; + push_implicit l "tbody" in_table_body_mode) + | (l, Start ({ name = "table" } as t)) as v -> + misnested_tag l t "table" (fun () -> + if not @@ Stack.has open_elements "table" then mode () + else begin + push tokens v; + close_element l "table" (fun () -> reset_mode () ()) + end) + | l, End { name = "table" } -> + if not @@ Stack.in_table_scope open_elements "table" then + report l (`Unmatched_end_tag "table") !throw mode + else close_element l "table" (fun () -> reset_mode () ()) + | ( l, + End + { + name = + ( "body" | "caption" | "col" | "colgroup" | "html" | "tbody" + | "td" | "tfoot" | "th" | "thead" | "tr" ) as name; + } ) -> + report l (`Unmatched_end_tag name) !throw mode + | ( _, Start { name = "style" | "script" | "template" } + | _, End { name = "template" } ) as v -> + in_head_mode_rules mode v + | l, Start ({ name = "input" } as t) when Element.is_not_hidden t -> + misnested_tag l t "table" (fun () -> + push_and_emit ~acknowledge:true l t (fun () -> pop l mode)) + | l, Start ({ name = "form" } as t) -> + misnested_tag l t "table" (fun () -> + push_and_emit l t (fun () -> pop l mode)) + | (_, EOF) as v -> in_body_mode_rules "table" mode v + | v -> anything_else_in_table mode v + (* 8.2.5.4.10. *) + and in_table_text_mode only_space cs mode = + dispatch tokens begin function + | l, Char 0 -> + report l + (`Bad_token ("U+0000", "table", "null")) + !throw + (fun () -> in_table_text_mode only_space cs mode) + | (_, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020)) as v -> + in_table_text_mode only_space (v :: cs) mode + | (_, Char _) as v -> in_table_text_mode false (v :: cs) mode + | l, String s when String.contains s '\x00' -> + let s = remove_nulls s in + let v = (l, String s) in + if is_whitespace_only s then + in_table_text_mode only_space (v :: cs) mode + else in_table_text_mode false (v :: cs) mode + | (_, String s) as v when is_whitespace_only s -> + in_table_text_mode only_space (v :: cs) mode + | (_, String _) as v -> in_table_text_mode false (v :: cs) mode + | v -> + push tokens v; + if not only_space then + let rec reprocess = function + | [] -> mode () + | v :: more -> + anything_else_in_table (fun () -> reprocess more) v + in + reprocess (List.rev cs) + else begin + List.rev cs + |> List.iter (function + | l, Char c -> add_character l c + | l, String s when Token_source.native_text_runs tokens -> + add_string l s + | _ -> ()); + mode () + end + end + (* 8.2.5.4.11. *) + and in_caption_mode () = + dispatch tokens begin function + | l, End { name = "caption" } -> + if not @@ Stack.in_table_scope open_elements "caption" then + report l (`Unmatched_end_tag "caption") !throw in_caption_mode + else begin + Active.clear_until_marker active_formatting_elements; + close_element_with_implied "caption" l in_table_mode + end + | ( l, + Start + ({ + name = + ( "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" + | "th" | "thead" | "tr" ); + } as t) ) as v -> + misnested_tag l t "caption" (fun () -> + if not @@ Stack.in_table_scope open_elements "caption" then + in_caption_mode () + else begin + Active.clear_until_marker active_formatting_elements; + push tokens v; + close_element l "caption" in_table_mode + end) + | (l, End { name = "table" }) as v -> + report l (`Unmatched_end_tag "table") !throw (fun () -> + if not @@ Stack.in_table_scope open_elements "caption" then + in_caption_mode () + else begin + Active.clear_until_marker active_formatting_elements; + push tokens v; + close_element l "caption" in_table_mode + end) + | ( l, + End + { + name = + ( "body" | "col" | "colgroup" | "html" | "tbody" | "td" + | "tfoot" | "th" | "thead" | "tr" ) as name; + } ) -> + report l (`Unmatched_end_tag name) !throw in_caption_mode + | l, Start ({ name = "select" } as t) -> + select_in_body l t in_select_in_table_mode + | v -> in_body_mode_rules "caption" in_caption_mode v + end + (* 8.2.5.4.12. *) + and in_column_group_mode () = + dispatch tokens begin function + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + add_character l c; + in_column_group_mode () + | l, String s when is_whitespace_only s -> + add_string l s; + in_column_group_mode () + | l, Comment s -> emit l (`Comment s) in_column_group_mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + in_column_group_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "colgroup" in_column_group_mode v + | l, Start ({ name = "col" } as t) -> + push_and_emit ~acknowledge:true l t (fun () -> + pop l in_column_group_mode) + | l, End { name = "colgroup" } -> + if not @@ Stack.current_element_is open_elements [ "colgroup" ] then + report l (`Unmatched_end_tag "colgroup") !throw + in_column_group_mode + else pop l in_table_mode + | l, End { name = "col" } -> + report l (`Unmatched_end_tag "col") !throw in_column_group_mode + | (_, Start { name = "template" } | _, End { name = "template" }) as v + -> + in_head_mode_rules in_column_group_mode v + | (_, EOF) as v -> in_body_mode_rules "colgroup" in_column_group_mode v + | (l, _) as v -> + if not @@ Stack.current_element_is open_elements [ "colgroup" ] then + report l (`Bad_content "colgroup") !throw in_table_mode + else begin + push tokens v; + pop l in_table_mode + end + end + (* 8.2.5.4.13. *) + and in_table_body_mode () = + dispatch tokens begin function + | l, Start ({ name = "tr" } as t) -> + pop_to_table_body_context l (fun () -> + push_and_emit l t in_row_mode) + | (l, Start ({ name = "th" | "td" } as t)) as v -> + misnested_tag l t "table" (fun () -> + pop_to_table_body_context l (fun () -> + push tokens v; + push_implicit l "tr" in_row_mode)) + | l, End { name = ("tbody" | "tfoot" | "thead") as name } -> + if not @@ Stack.in_table_scope open_elements name then + report l (`Unmatched_end_tag name) !throw in_table_body_mode + else pop_to_table_body_context l (fun () -> pop l in_table_mode) + | ( l, + Start + ({ + name = + "caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead"; + } as t) ) as v -> + if + not + @@ Stack.one_in_table_scope open_elements + [ "tbody"; "thead"; "tfoot" ] + then misnested_tag l t "table" in_table_body_mode + else begin + push tokens v; + pop_to_table_body_context l (fun () -> pop l in_table_mode) + end + | (l, End { name = "table" as name }) as v -> + if + not + @@ Stack.one_in_table_scope open_elements + [ "tbody"; "thead"; "tfoot" ] + then report l (`Unmatched_end_tag name) !throw in_table_body_mode + else begin + push tokens v; + pop_to_table_body_context l (fun () -> pop l in_table_mode) + end + | ( l, + End + { + name = + ( "body" | "caption" | "col" | "colgroup" | "html" | "td" + | "th" | "tr" ) as name; + } ) -> + report l (`Unmatched_end_tag name) !throw in_table_body_mode + | v -> in_table_mode_rules in_table_body_mode v + end + (* 8.2.5.4.14. *) + and in_row_mode () = + dispatch tokens begin function + | l, Start ({ name = "th" | "td" } as t) -> + Active.add_marker active_formatting_elements; + pop_to_table_row_context l (fun () -> + push_and_emit l t in_cell_mode) + | l, End { name = "tr" } -> + if not @@ Stack.in_table_scope open_elements "tr" then + report l (`Unmatched_end_tag "tr") !throw in_row_mode + else pop_to_table_row_context l (fun () -> pop l in_table_body_mode) + | ( ( l, + Start + { + name = + ( "caption" | "col" | "colgroup" | "tbody" | "tfoot" + | "thead" | "tr" ); + } ) + | l, End { name = "table" } ) as v -> + if not @@ Stack.in_table_scope open_elements "tr" then + match snd v with + | Start t -> misnested_tag l t "tr" in_row_mode + | End { name } -> + report l (`Unmatched_end_tag name) !throw in_row_mode + | _ -> assert false + else + pop_to_table_row_context l (fun () -> + push tokens v; + pop l in_table_body_mode) + | (l, End { name = ("tbody" | "tfoot" | "thead") as name }) as v -> + if not @@ Stack.in_table_scope open_elements name then + report l (`Unmatched_end_tag name) !throw in_row_mode + else if not @@ Stack.in_table_scope open_elements "tr" then + in_row_mode () + else + pop_to_table_row_context l (fun () -> + push tokens v; + pop l in_table_body_mode) + | ( l, + End + { + name = + ( "body" | "caption" | "col" | "colgroup" | "html" | "td" + | "th" ) as name; + } ) -> + report l (`Unmatched_end_tag name) !throw in_row_mode + | v -> in_table_mode_rules in_row_mode v + end + (* 8.2.5.4.15. *) + and in_cell_mode () = + dispatch tokens begin function + | l, End { name = ("td" | "th") as name } -> + if not @@ Stack.in_table_scope open_elements name then + report l (`Unmatched_end_tag name) !throw in_cell_mode + else + close_element_with_implied name l (fun () -> + Active.clear_until_marker active_formatting_elements; + in_row_mode ()) + | ( l, + Start + ({ + name = + ( "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" + | "th" | "thead" | "tr" ); + } as t) ) as v -> + if not @@ Stack.one_in_table_scope open_elements [ "td"; "th" ] then + misnested_tag l t "td/th" in_cell_mode + else + close_cell l (fun () -> + Active.clear_until_marker active_formatting_elements; + push tokens v; + current_mode := in_row_mode; + in_row_mode ()) + | ( l, + End + { + name = + ("body" | "caption" | "col" | "colgroup" | "html") as name; + } ) -> + report l (`Unmatched_end_tag name) !throw in_cell_mode + | ( l, + End + { name = ("table" | "tbody" | "tfoot" | "thead" | "tr") as name } + ) as v -> + if not @@ Stack.in_table_scope open_elements name then + report l (`Unmatched_end_tag name) !throw in_cell_mode + else + close_cell l (fun () -> + Active.clear_until_marker active_formatting_elements; + push tokens v; + current_mode := in_row_mode; + in_row_mode ()) + | l, Start ({ name = "select" } as t) -> + select_in_body l t in_select_in_table_mode + | v -> in_body_mode_rules "td" in_cell_mode v + end + (* 8.2.5.4.16. *) + and in_select_mode () = + dispatch tokens (fun v -> in_select_mode_rules in_select_mode v) + and in_select_mode_rules mode = function + | l, Char 0 -> + report l (`Bad_token ("U+0000", "select", "null")) !throw mode + | l, Char c -> + add_character l c; + mode () + | l, String s -> + add_string l (remove_nulls s); + mode () + | l, Comment s -> emit l (`Comment s) mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw mode + | (_, Start { name = "html" }) as v -> in_body_mode_rules "select" mode v + | l, Start ({ name = "option" } as t) -> + (fun mode' -> + if Stack.current_element_is open_elements [ "option" ] then + pop l mode' + else mode' ()) (fun () -> push_and_emit l t mode) + | l, Start ({ name = "optgroup" } as t) -> + (fun mode' -> + if Stack.current_element_is open_elements [ "option" ] then + pop l mode' + else mode' ()) + @@ (fun mode' () -> + if Stack.current_element_is open_elements [ "optgroup" ] then + pop l mode' + else mode' ()) + @@ fun () -> push_and_emit l t mode + | l, End { name = "optgroup" } -> + (fun mode' -> + match !(Stack.elements open_elements) with + | { element_name = HTML, "option" } + :: { element_name = HTML, "optgroup" } + :: _ -> + pop l mode' + | _ -> mode' ()) (fun () -> + if Stack.current_element_is open_elements [ "optgroup" ] then + pop l mode + else report l (`Unmatched_end_tag "optgroup") !throw mode) + | l, End { name = "option" } -> + if Stack.current_element_is open_elements [ "option" ] then pop l mode + else report l (`Unmatched_end_tag "option") !throw mode + | l, End { name = "select" } -> + if not @@ Stack.in_select_scope open_elements "select" then + report l (`Unmatched_end_tag "select") !throw mode + else close_element l "select" (fun () -> reset_mode () ()) + | l, Start ({ name = "select" } as t) -> + misnested_tag l t "select" (fun () -> + close_element l "select" (fun () -> reset_mode () ())) + | (l, Start ({ name = "input" | "keygen" | "textarea" } as t)) as v -> + misnested_tag l t "select" (fun () -> + if not @@ Stack.in_select_scope open_elements "select" then mode () + else begin + push tokens v; + close_element l "select" (fun () -> reset_mode () ()) + end) + | (_, (Start { name = "script" | "template" } | End { name = "template" })) + as v -> + in_head_mode_rules mode v + | (_, EOF) as v -> in_body_mode_rules "select" mode v + | l, _ -> report l (`Bad_content "select") !throw mode + (* 8.2.5.4.17. *) + and in_select_in_table_mode () = + dispatch tokens begin function + | ( l, + Start + ({ + name = + ( "caption" | "table" | "tbody" | "tfoot" | "thead" | "tr" + | "td" | "th" ); + } as t) ) as v -> + misnested_tag l t "table" (fun () -> + push tokens v; + close_element l "select" (fun () -> reset_mode () ())) + | ( l, + End + { + name = + ( "caption" | "table" | "tbody" | "tfoot" | "thead" | "tr" + | "td" | "th" ) as name; + } ) as v -> + report l (`Unmatched_end_tag "name") !throw (fun () -> + if not @@ Stack.in_table_scope open_elements name then + in_select_in_table_mode () + else begin + push tokens v; + close_element l "select" (fun () -> reset_mode () ()) + end) + | v -> in_select_mode_rules in_select_in_table_mode v + end + (* 8.2.5.4.18. *) + and in_template_mode () = + dispatch tokens (fun v -> in_table_mode_rules in_template_mode v) + (* 8.2.5.4.18. *) + and in_template_mode_rules mode = function + | (_, (Char _ | Comment _ | Doctype _ | String _)) as v -> + in_body_mode_rules "template" mode v + | ( ( _, + Start + { + name = + ( "base" | "basefont" | "bgsound" | "link" | "meta" | "noframes" + | "script" | "style" | "template" | "title" ); + } ) + | _, End { name = "template" } ) as v -> + in_head_mode_rules mode v + | (_, Start { name = "caption" | "colgroup" | "tbody" | "tfoot" | "thead" }) + as v -> + Template.pop template_insertion_modes; + Template.push template_insertion_modes in_table_mode; + push tokens v; + in_table_mode () + | (_, Start { name = "col" }) as v -> + Template.pop template_insertion_modes; + Template.push template_insertion_modes in_column_group_mode; + push tokens v; + in_column_group_mode () + | (_, Start { name = "tr" }) as v -> + Template.pop template_insertion_modes; + Template.push template_insertion_modes in_table_body_mode; + push tokens v; + in_table_body_mode () + | (_, Start { name = "td" | "th" }) as v -> + Template.pop template_insertion_modes; + Template.push template_insertion_modes in_row_mode; + push tokens v; + in_row_mode () + | (_, Start _) as v -> + Template.pop template_insertion_modes; + Template.push template_insertion_modes in_body_mode; + push tokens v; + in_body_mode () + | l, End { name } -> report l (`Unmatched_end_tag name) !throw mode + | (l, EOF) as v -> + if not @@ Stack.has open_elements "template" then emit_end l + else begin + report l (`Unmatched_end_tag "template") !throw (fun () -> + Active.clear_until_marker active_formatting_elements; + Template.pop template_insertion_modes; + push tokens v; + close_element l "template" (fun () -> reset_mode () ())) + end + (* 8.2.5.4.19. *) + and after_body_mode () = + dispatch tokens begin function + | (_, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020)) as v -> + in_body_mode_rules "html" after_body_mode v + | (_, String s) as v when is_whitespace_only s -> + in_body_mode_rules "html" after_body_mode v + | l, Comment s -> emit l (`Comment s) after_body_mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + after_body_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "html" after_body_mode v + | _, End { name = "html" } -> after_after_body_mode () + | l, EOF -> emit_end l + | (l, _) as v -> + report l (`Bad_document "content after body") !throw (fun () -> + push tokens v; + in_body_mode ()) + end + (* 8.2.5.4.20. *) + and in_frameset_mode () = + dispatch tokens begin function + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + add_character l c; + in_frameset_mode () + | l, String s when is_whitespace_only s -> + add_string l s; + in_frameset_mode () + | l, Comment s -> emit l (`Comment s) in_frameset_mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + in_frameset_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "frameset" in_frameset_mode v + | l, Start ({ name = "frameset" } as t) -> + push_and_emit l t in_frameset_mode + | l, End { name = "frameset" } -> + (fun mode' -> + if Stack.current_element_is open_elements [ "html" ] then + report l (`Unmatched_end_tag "frameset") !throw mode' + else pop l mode') (fun () -> + if Stack.current_element_is open_elements [ "frameset" ] then + in_frameset_mode () + else after_frameset_mode ()) + | l, Start ({ name = "frame" } as t) -> + push_and_emit ~acknowledge:true l t (fun () -> + pop l in_frameset_mode) + | (_, Start { name = "noframes" }) as v -> + in_head_mode_rules in_frameset_mode v + | l, EOF -> + (fun mode' -> + if not @@ Stack.current_element_is open_elements [ "html" ] then + report l (`Unexpected_eoi "frameset") !throw mode' + else mode' ()) (fun () -> emit_end l) + | l, _ -> report l (`Bad_content "frameset") !throw in_frameset_mode + end + (* 8.2.5.4.21. *) + and after_frameset_mode () = + dispatch tokens begin function + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + add_character l c; + after_frameset_mode () + | l, String s when is_whitespace_only s -> + add_string l s; + after_frameset_mode () + | l, Comment s -> emit l (`Comment s) after_frameset_mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw + after_frameset_mode + | (_, Start { name = "html" }) as v -> + in_body_mode_rules "html" after_frameset_mode v + | l, End { name = "html" } -> + close_element l "html" after_after_frameset_mode + | (_, Start { name = "noframes" }) as v -> + in_head_mode_rules after_frameset_mode v + | l, EOF -> emit_end l + | l, _ -> report l (`Bad_content "html") !throw after_frameset_mode + end + (* 8.2.5.4.22. *) + and after_after_body_mode () = + dispatch tokens begin function + | l, Comment s -> emit l (`Comment s) after_after_body_mode + | ( _, Doctype _ + | _, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) + | _, Start { name = "html" } ) as v -> + in_body_mode_rules "html" after_after_body_mode v + | (_, String s) as v when is_whitespace_only s -> + in_body_mode_rules "html" after_after_body_mode v + | l, EOF -> emit_end l + | (l, _) as v -> + push tokens v; + report l (`Bad_content "html") !throw in_body_mode + end + (* 8.2.5.4.23. *) + and after_after_frameset_mode () = + dispatch tokens begin function + | l, Comment s -> emit l (`Comment s) after_after_frameset_mode + | ( _, Doctype _ + | _, Char (0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) + | _, Start { name = "html" } ) as v -> + in_body_mode_rules "html" after_after_frameset_mode v + | (_, String s) as v when is_whitespace_only s -> + in_body_mode_rules "html" after_after_frameset_mode v + | l, EOF -> emit_end l + | (_, Start { name = "noframes" }) as v -> + in_head_mode_rules after_after_frameset_mode v + | l, _ -> + report l (`Bad_content "html") !throw after_after_frameset_mode + end + (* 8.2.5.5. *) + and foreign_start_tag mode l tag = + let namespace = + match Stack.adjusted_current_element context open_elements with + | None -> HTML + | Some { element_name = ns, _ } -> ns + in + + push_and_emit ~acknowledge:true ~namespace l tag (fun () -> + if tag.self_closing then pop l mode else mode ()) + and is_html_font_tag tag = + tag.Token_tag.attributes + |> List.exists (function + | ("color" | "face" | "size"), _ -> true + | _ -> false) + and foreign_content mode force_html v = + match v with + | l, Char 0 -> + report l + (`Bad_token ("U+0000", "foreign content", "null")) + !throw + (fun () -> + add_character l u_rep; + mode ()) + | l, String s when s <> "" && Subtree.buffering subtree_buffer -> + let decoded = String.get_utf_8_uchar s 0 in + let width = Uchar.utf_decode_length decoded in + if width < String.length s then + push tokens (l, String (String.sub s width (String.length s - width))); + foreign_content mode force_html + (l, Char (Uchar.to_int (Uchar.utf_decode_uchar decoded))) + | l, String s -> + add_string l (replace_nulls s); + if not @@ is_whitespace_only (remove_nulls s) then frameset_ok := false; + mode () + | l, Char ((0x0009 | 0x000A | 0x000C | 0x000D | 0x0020) as c) -> + add_character l c; + mode () + | l, Char c -> + frameset_ok := false; + add_character l c; + mode () + | l, Comment s -> emit l (`Comment s) mode + | l, Doctype _ -> + report l (`Bad_document "doctype should be first") !throw mode + | ( l, + Start + ({ + name = + ( "b" | "big" | "blockquote" | "body" | "br" | "center" | "code" + | "dd" | "div" | "dl" | "dt" | "em" | "embed" | "font" | "h1" + | "h2" | "h3" | "h4" | "h5" | "h6" | "head" | "hr" | "i" | "img" + | "li" | "listing" | "main" | "meta" | "nobr" | "ol" | "p" + | "pre" | "ruby" | "s" | "small" | "span" | "strong" | "strike" + | "sub" | "sup" | "table" | "tt" | "u" | "ul" | "var" ) as name; + } as t) ) as v -> + if name = "font" && (not @@ is_html_font_tag t) then + foreign_start_tag mode l t + else + misnested_tag l t "xml tag" (fun () -> + push tokens v; + pop l (fun () -> + pop_until + (function + | { element_name = HTML, _ } -> true + | { is_html_integration_point = true } -> true + | { element_name } -> + Foreign.is_mathml_text_integration_point element_name) + l mode)) + | l, Start t -> foreign_start_tag mode l t + | l, End { name = "script" } + when match Stack.current_element open_elements with + | Some { element_name = SVG, "script" } -> true + | _ -> false -> + pop l mode + | l, End { name } -> + (fun mode' -> + match Stack.current_element open_elements with + | Some { element_name = _, name' } + when String.lowercase_ascii name' = name -> + mode' () + | _ -> report l (`Unmatched_end_tag name) !throw (fun () -> mode' ())) + (fun () -> + let rec scan = function + | [] -> mode () + | { element_name = ns, name' } :: _ + when String.lowercase_ascii name' = name -> + close_element ~ns l name mode + | { element_name = HTML, _ } :: _ -> force_html () + | _ :: rest -> scan rest + in + scan !(Stack.elements open_elements)) + | _, EOF -> force_html () + in + + construct constructor diff --git a/src/lite/html_parser.mli b/src/lite/html_parser.mli new file mode 100644 index 0000000..087012a --- /dev/null +++ b/src/lite/html_parser.mli @@ -0,0 +1,11 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +val parse : + ?depth_limit:int -> + [< `Document | `Fragment of string ] -> + Error.parse_handler -> + Token_source.t -> + (location * signal) Kstream.t diff --git a/src/lite/html_tokenizer.ml b/src/lite/html_tokenizer.ml new file mode 100644 index 0000000..a226f23 --- /dev/null +++ b/src/lite/html_tokenizer.ml @@ -0,0 +1,15 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +type token = + | Doctype of doctype + | Start of Token_tag.t + | End of Token_tag.t + | Char of int + | String of string + | Comment of string + | EOF + +type state = Data | RCDATA | RAWTEXT | Script_data | PLAINTEXT diff --git a/src/lite/html_tokenizer.mli b/src/lite/html_tokenizer.mli new file mode 100644 index 0000000..a226f23 --- /dev/null +++ b/src/lite/html_tokenizer.mli @@ -0,0 +1,15 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +type token = + | Doctype of doctype + | Start of Token_tag.t + | End of Token_tag.t + | Char of int + | String of string + | Comment of string + | EOF + +type state = Data | RCDATA | RAWTEXT | Script_data | PLAINTEXT diff --git a/src/lite/html_writer.ml b/src/lite/html_writer.ml new file mode 100644 index 0000000..1342f92 --- /dev/null +++ b/src/lite/html_writer.ml @@ -0,0 +1,204 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +let rec ascii_attribute_safe s index = + if index = String.length s then true + else + match s.[index] with + | '&' | '"' | '\x80' .. '\xFF' -> false + | _ -> ascii_attribute_safe s (index + 1) + +let escape_attribute_slow ~into:buffer s = + Uutf.String.fold_utf_8 + (fun () _ -> function + | `Malformed _ -> () + | `Uchar c -> ( + match Uchar.to_int c with + | 0x0026 -> Buffer.add_string buffer "&" + | 0x00A0 -> Buffer.add_string buffer " " + | 0x0022 -> Buffer.add_string buffer """ + | c -> add_utf_8 buffer c)) + () s + +let escape_attribute buffer s = + if ascii_attribute_safe s 0 then Buffer.add_string buffer s + else escape_attribute_slow ~into:buffer s + +let rec ascii_text_safe s index = + if index = String.length s then true + else + match s.[index] with + | '&' | '<' | '>' | '\x80' .. '\xFF' -> false + | _ -> ascii_text_safe s (index + 1) + +let escape_text_slow ~into:buffer s = + Uutf.String.fold_utf_8 + (fun () _ -> function + | `Malformed _ -> () + | `Uchar c -> ( + match Uchar.to_int c with + | 0x0026 -> Buffer.add_string buffer "&" + | 0x00A0 -> Buffer.add_string buffer " " + | 0x003C -> Buffer.add_string buffer "<" + | 0x003E -> Buffer.add_string buffer ">" + | c -> add_utf_8 buffer c)) + () s + +let escape_text buffer s = + if ascii_text_safe s 0 then Buffer.add_string buffer s + else escape_text_slow ~into:buffer s + +let void_elements = + [ + "area"; + "base"; + "basefont"; + "bgsound"; + "br"; + "col"; + "embed"; + "frame"; + "hr"; + "img"; + "input"; + "keygen"; + "link"; + "meta"; + "param"; + "source"; + "track"; + "wbr"; + ] + +let prepend_newline_for = [ "pre"; "textarea"; "listing" ] + +let rec starts_with_newline = function + | [] -> false + | s :: more -> + if String.length s = 0 then starts_with_newline more else s.[0] = '\x0A' + +let literal_text_elements = + [ "style"; "script"; "xmp"; "iframe"; "noembed"; "noframes"; "plaintext" ] + +let element_name = function + | ns, local_name when list_mem_string ns [ html_ns; svg_ns; mathml_ns ] -> + local_name + | ns, local_name when ns = xml_ns -> "xml:" ^ local_name + | ns, local_name when ns = xmlns_ns -> "xmlns:" ^ local_name + | ns, local_name when ns = xlink_ns -> "xlink:" ^ local_name + | _, local_name -> local_name + +let attribute_name = function + | "", local_name -> local_name + | ns, local_name when ns = xml_ns -> "xml:" ^ local_name + | ns, "xmlns" when ns = xmlns_ns -> "xmlns" + | ns, local_name when ns = xmlns_ns -> "xmlns:" ^ local_name + | ns, local_name when ns = xlink_ns -> "xlink:" ^ local_name + | _, local_name -> local_name + +let write ?(escape_attribute = escape_attribute) ?(escape_text = escape_text) + buffer stream = + let signals = Markup_common.Stream.Private.of_stream stream in + let open_elements = ref [] in + let pending = ref None in + + let in_literal_text_element () = + match !open_elements with + | element :: _ -> list_mem_string element literal_text_elements + | [] -> false + in + + let rec next throw ended k = + match !pending with + | Some signal -> + pending := None; + k signal + | None -> Kstream.next signals throw ended k + and peek throw ended k = + next throw ended (fun signal -> + pending := Some signal; + k signal) + and loop throw ended = + next throw ended (fun signal -> + match signal with + | `Start_element (((ns, local_name) as name), attributes) -> + let tag_name = element_name name in + Buffer.add_char buffer '<'; + Buffer.add_string buffer tag_name; + List.iter + (fun (name, value) -> + Buffer.add_char buffer ' '; + Buffer.add_string buffer (attribute_name name); + Buffer.add_string buffer "=\""; + escape_attribute buffer value; + Buffer.add_char buffer '"') + attributes; + Buffer.add_char buffer '>'; + + if ns = html_ns && list_mem_string local_name void_elements then + peek throw + (fun () -> ended ()) + (function + | `End_element -> + next throw + (fun () -> assert false) + (fun _ -> loop throw ended) + | `Start_element _ | `Text _ | `Comment _ | `PI _ | `Xml _ + | `Doctype _ -> + open_elements := tag_name :: !open_elements; + loop throw ended) + else begin + open_elements := tag_name :: !open_elements; + if ns = html_ns && list_mem_string local_name prepend_newline_for + then + peek throw + (fun () -> ended ()) + (function + | `Text strings when starts_with_newline strings -> + Buffer.add_char buffer '\n'; + loop throw ended + | `Text _ | `Start_element _ | `End_element | `Comment _ + | `PI _ | `Doctype _ | `Xml _ -> + loop throw ended) + else loop throw ended + end + | `End_element -> + begin match !open_elements with + | [] -> loop throw ended + | name :: rest -> + open_elements := rest; + Buffer.add_string buffer "'; + loop throw ended + end + | `Text strings -> + if List.for_all (fun s -> String.length s = 0) strings then + loop throw ended + else begin + if in_literal_text_element () then + List.iter (Buffer.add_string buffer) strings + else List.iter (escape_text buffer) strings; + loop throw ended + end + | `Comment s -> + Buffer.add_string buffer ""; + loop throw ended + | `PI (target, s) -> + Buffer.add_string buffer "'; + loop throw ended + | `Doctype _ as doctype -> + Buffer.add_string buffer (signal_to_string doctype); + loop throw ended + | `Xml _ -> loop throw ended) + in + + loop raise (fun () -> ()) diff --git a/src/lite/kstream.ml b/src/lite/kstream.ml new file mode 100644 index 0000000..6680a76 --- /dev/null +++ b/src/lite/kstream.ml @@ -0,0 +1,6 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* See the comment in common.ml. *) + +include Markup_common.Kstream diff --git a/src/lite/markup_declaration.ml b/src/lite/markup_declaration.ml new file mode 100644 index 0000000..c55d9a3 --- /dev/null +++ b/src/lite/markup_declaration.ml @@ -0,0 +1,403 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* Port of the comment, bogus comment, and doctype tokenizer states of + src/baseline/html_tokenizer.ml, operating on the normalized input string. + [scan] is given the index of the '!' or '?' that follows '<' and returns + the token together with the index of the first unconsumed byte. *) + +open Common + +type result = { + token : Html_tokenizer.token; + next : int; + (* Byte count after [next] that the caller must ASCII-lowercase in the + input, mirroring baseline's lowercased pushback of the six-codepoint + PUBLIC/SYSTEM lookahead. *) + lowercase : int; +} + +let u_rep_utf_8 = "\xEF\xBF\xBD" + +let add buffer byte = + if byte = '\x00' then Buffer.add_string buffer u_rep_utf_8 + else Buffer.add_char buffer byte + +let is_whitespace = function '\t' | '\n' | '\x0C' | ' ' -> true | _ -> false + +let matches data index keyword = + index + String.length keyword <= String.length data + && begin + let rec check offset = + offset >= String.length keyword + || (data.[index + offset] = keyword.[offset] && check (offset + 1)) + in + check 0 + end + +let matches_lowercase data index keyword = + index + String.length keyword <= String.length data + && begin + let rec check offset = + offset >= String.length keyword + || Char.lowercase_ascii data.[index + offset] = keyword.[offset] + && check (offset + 1) + in + check 0 + end + +let bogus_comment data start = + let length = String.length data in + let buffer = Buffer.create 32 in + let rec consume index = + if index >= length then + { + token = Html_tokenizer.Comment (Buffer.contents buffer); + next = index; + lowercase = 0; + } + else + match data.[index] with + | '>' -> + { + token = Html_tokenizer.Comment (Buffer.contents buffer); + next = index + 1; + lowercase = 0; + } + | byte -> + add buffer byte; + consume (index + 1) + in + consume start + +let comment data start = + let length = String.length data in + let buffer = Buffer.create 64 in + let finish index = + { + token = Html_tokenizer.Comment (Buffer.contents buffer); + next = index; + lowercase = 0; + } + in + let rec comment_start index = + if index >= length then finish index + else + match data.[index] with + | '-' -> comment_start_dash (index + 1) + | '>' -> finish (index + 1) + | byte -> + add buffer byte; + comment_text (index + 1) + and comment_start_dash index = + if index >= length then finish index + else + match data.[index] with + | '-' -> comment_end (index + 1) + | '>' -> finish (index + 1) + | byte -> + Buffer.add_char buffer '-'; + add buffer byte; + comment_text (index + 1) + and comment_text index = + if index >= length then finish index + else + match data.[index] with + | '-' -> comment_end_dash (index + 1) + | byte -> + add buffer byte; + comment_text (index + 1) + and comment_end_dash index = + if index >= length then finish index + else + match data.[index] with + | '-' -> comment_end (index + 1) + | byte -> + Buffer.add_char buffer '-'; + add buffer byte; + comment_text (index + 1) + and comment_end index = + if index >= length then finish index + else + match data.[index] with + | '>' -> finish (index + 1) + | '!' -> comment_end_bang (index + 1) + | '-' -> + Buffer.add_char buffer '-'; + comment_end (index + 1) + | byte -> + Buffer.add_string buffer "--"; + add buffer byte; + comment_text (index + 1) + and comment_end_bang index = + if index >= length then finish index + else + match data.[index] with + | '-' -> + Buffer.add_string buffer "--!"; + comment_end_dash (index + 1) + | '>' -> finish (index + 1) + | byte -> + Buffer.add_string buffer "--!"; + add buffer byte; + comment_text (index + 1) + in + comment_start start + +let doctype data start = + let length = String.length data in + let name = ref None in + let public_identifier = ref None in + let system_identifier = ref None in + let quirks = ref false in + let lowercase = ref 0 in + let add_to field byte = + let buffer = + match !field with + | Some buffer -> buffer + | None -> + let buffer = Buffer.create 32 in + field := Some buffer; + buffer + in + add buffer byte + in + let finish ?(force_quirks = false) index = + if force_quirks then quirks := true; + let contents field = + match !field with + | None -> None + | Some buffer -> Some (Buffer.contents buffer) + in + { + token = + Html_tokenizer.Doctype + { + doctype_name = contents name; + public_identifier = contents public_identifier; + system_identifier = contents system_identifier; + raw_text = None; + force_quirks = !quirks; + }; + next = index; + lowercase = !lowercase; + } + in + let rec doctype_start index = + if index >= length then finish ~force_quirks:true index + else if is_whitespace data.[index] then before_name (index + 1) + else before_name index + and before_name index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then before_name (index + 1) + else if byte = '>' then finish ~force_quirks:true (index + 1) + else begin + add_to name (Char.lowercase_ascii byte); + name_state (index + 1) + end + and name_state index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then after_name (index + 1) + else if byte = '>' then finish (index + 1) + else begin + add_to name (Char.lowercase_ascii byte); + name_state (index + 1) + end + and after_name index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then after_name (index + 1) + else if byte = '>' then finish (index + 1) + else if matches_lowercase data index "public" then + after_public_keyword (index + 6) + else if matches_lowercase data index "system" then + after_system_keyword (index + 6) + else begin + quirks := true; + (* Baseline reads the keyword lookahead as six codepoints and pushes + it back lowercased (after_doctype_name_state), so window bytes + past a terminating '>' come back lowercased. *) + let rec window_end count index = + if count = 0 || index >= length then index + else + let width = + if data.[index] < '\x80' then 1 + else if data.[index] < '\xE0' then 2 + else if data.[index] < '\xF0' then 3 + else 4 + in + window_end (count - 1) (min length (index + width)) + in + let window_end = window_end 6 index in + let rec find_gt index = + if index >= window_end then None + else if data.[index] = '>' then Some index + else find_gt (index + 1) + in + match find_gt index with + | None -> bogus window_end + | Some gt -> + let rec has_upper index = + index < window_end + && (('A' <= data.[index] && data.[index] <= 'Z') + || has_upper (index + 1)) + in + if has_upper (gt + 1) then lowercase := window_end - (gt + 1); + finish (gt + 1) + end + and after_public_keyword index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then before_public_identifier (index + 1) + else if byte = '"' || byte = '\'' then begin + public_identifier := Some (Buffer.create 32); + identifier_quoted public_identifier byte after_public_identifier + (index + 1) + end + else if byte = '>' then finish ~force_quirks:true (index + 1) + else begin + quirks := true; + bogus (index + 1) + end + and before_public_identifier index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then before_public_identifier (index + 1) + else if byte = '"' || byte = '\'' then begin + public_identifier := Some (Buffer.create 32); + identifier_quoted public_identifier byte after_public_identifier + (index + 1) + end + else if byte = '>' then finish ~force_quirks:true (index + 1) + else begin + quirks := true; + bogus (index + 1) + end + and identifier_quoted field quote next_state index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if byte = quote then next_state (index + 1) + else if byte = '>' then finish ~force_quirks:true (index + 1) + else begin + add_to field byte; + identifier_quoted field quote next_state (index + 1) + end + and after_public_identifier index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then between_identifiers (index + 1) + else if byte = '>' then finish (index + 1) + else if byte = '"' || byte = '\'' then begin + system_identifier := Some (Buffer.create 32); + identifier_quoted system_identifier byte after_system_identifier + (index + 1) + end + else begin + quirks := true; + bogus (index + 1) + end + and between_identifiers index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then between_identifiers (index + 1) + else if byte = '>' then finish (index + 1) + else if byte = '"' || byte = '\'' then begin + system_identifier := Some (Buffer.create 32); + identifier_quoted system_identifier byte after_system_identifier + (index + 1) + end + else begin + quirks := true; + bogus (index + 1) + end + and after_system_keyword index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then before_system_identifier (index + 1) + else if byte = '"' || byte = '\'' then begin + system_identifier := Some (Buffer.create 32); + identifier_quoted system_identifier byte after_system_identifier + (index + 1) + end + else if byte = '>' then finish ~force_quirks:true (index + 1) + else begin + quirks := true; + bogus (index + 1) + end + and before_system_identifier index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then before_system_identifier (index + 1) + else if byte = '"' || byte = '\'' then begin + system_identifier := Some (Buffer.create 32); + identifier_quoted system_identifier byte after_system_identifier + (index + 1) + end + else if byte = '>' then finish ~force_quirks:true (index + 1) + else begin + quirks := true; + bogus (index + 1) + end + and after_system_identifier index = + if index >= length then finish ~force_quirks:true index + else + let byte = data.[index] in + if is_whitespace byte then after_system_identifier (index + 1) + else if byte = '>' then finish (index + 1) + else bogus (index + 1) + and bogus index = + if index >= length then finish index + else if data.[index] = '>' then finish (index + 1) + else bogus (index + 1) + in + doctype_start start + +let cdata data start = + let length = String.length data in + let buffer = Buffer.create 64 in + let finish next = + { + token = Html_tokenizer.String (Buffer.contents buffer); + next; + lowercase = 0; + } + in + let rec consume index = + if index >= length then finish index + else if + index + 3 <= length + && data.[index] = ']' + && data.[index + 1] = ']' + && data.[index + 2] = '>' + then finish (index + 3) + else begin + add buffer data.[index]; + consume (index + 1) + end + in + consume start + +let scan ~foreign data index = + if data.[index] = '?' then bogus_comment data (index + 1) + else if + index + 3 <= String.length data + && data.[index + 1] = '-' + && data.[index + 2] = '-' + then comment data (index + 3) + else if matches_lowercase data (index + 1) "doctype" then + doctype data (index + 8) + else if foreign && matches data (index + 1) "[CDATA[" then + cdata data (index + 8) + else bogus_comment data (index + 1) diff --git a/src/lite/markup_lite.ml b/src/lite/markup_lite.ml new file mode 100644 index 0000000..3a82407 --- /dev/null +++ b/src/lite/markup_lite.ml @@ -0,0 +1,98 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +type async = Markup_common.async +type sync = Markup_common.sync +type ('data, 'sync) stream = ('data, 'sync) Markup_common.stream +type location = Markup_common.location +type name = Markup_common.name + +type xml_declaration = Markup_common.xml_declaration = { + version : string; + encoding : string option; + standalone : bool option; +} + +type doctype = Markup_common.doctype = { + doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool; +} + +type signal = Markup_common.signal +type encoding = [ `Auto | `UTF_8 | `UTF_16BE | `UTF_16LE ] + +module Token_tag = Common.Token_tag + +type token = + [ `Doctype of doctype + | `Start of Token_tag.t + | `End of Token_tag.t + | `Char of int + | `String of string + | `Comment of string + | `EOF ] + +module Error = Markup_common.Error +module Ns = Markup_common.Ns + +let signal_to_string = Markup_common.signal_to_string + +let wrap_report report location error throw resume = + match report location error with + | () -> resume () + | exception exn -> throw exn + +let parse_source report context depth_limit tokens = + Html_parser.parse ?depth_limit context (wrap_report report) tokens + |> Kstream.map (fun (_, signal) _ continue -> continue signal) + |> Markup_common.Stream.Private.to_stream + |> fun stream -> (stream : (signal, sync) stream) + +let parse_html ?(report = fun _ _ -> ()) ?(encoding = `Auto) + ?(context : [ `Document | `Fragment of string ] = `Document) ?depth_limit + html = + let source = + match encoding with + | `Auto -> Token_source.create html + | `UTF_8 -> Token_source.create_utf_8 html + | `UTF_16BE -> Encoding.utf_16be html |> Token_source.create_utf_8 + | `UTF_16LE -> Encoding.utf_16le html |> Token_source.create_utf_8 + in + parse_source report context depth_limit source + +let parse_tokens ?(report = fun _ _ -> ()) + ?(context : [ `Document | `Fragment of string ] = `Document) ?depth_limit + tokens = + let adapt (location, token) = + let token = + match token with + | `Doctype d -> Html_tokenizer.Doctype d + | `Start t -> Html_tokenizer.Start t + | `End t -> Html_tokenizer.End t + | `Char c -> Html_tokenizer.Char c + | `String s -> Html_tokenizer.String s + | `Comment s -> Html_tokenizer.Comment s + | `EOF -> Html_tokenizer.EOF + in + (location, token) + in + tokens |> List.map adapt |> Token_source.of_tokens + |> parse_source report context depth_limit + +let iter f stream = + stream |> Markup_common.Stream.Private.of_stream + |> Kstream.iter (fun value _ continue -> + f value; + continue ()) + |> fun iterate -> iterate raise ignore + +let write_html ?escape_attribute ?escape_text buffer signals = + Html_writer.write ?escape_attribute ?escape_text buffer signals + +let to_html_string ?escape_attribute ?escape_text signals = + let buffer = Buffer.create 512 in + write_html ?escape_attribute ?escape_text buffer signals; + Buffer.contents buffer diff --git a/src/lite/markup_lite.mli b/src/lite/markup_lite.mli new file mode 100644 index 0000000..cc54ed6 --- /dev/null +++ b/src/lite/markup_lite.mli @@ -0,0 +1,79 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(** Small synchronous HTML parser using types shared with {!Markup}. *) + +type async = Markup_common.async +type sync = Markup_common.sync +type ('data, 'sync) stream = ('data, 'sync) Markup_common.stream +type location = Markup_common.location +type name = Markup_common.name + +type xml_declaration = Markup_common.xml_declaration = { + version : string; + encoding : string option; + standalone : bool option; +} + +type doctype = Markup_common.doctype = { + doctype_name : string option; + public_identifier : string option; + system_identifier : string option; + raw_text : string option; + force_quirks : bool; +} + +type signal = Markup_common.signal +type encoding = [ `Auto | `UTF_8 | `UTF_16BE | `UTF_16LE ] + +module Token_tag : sig + type t = { + name : string; + attributes : (string * string) list; + self_closing : bool; + } +end + +type token = + [ `Doctype of doctype + | `Start of Token_tag.t + | `End of Token_tag.t + | `Char of int + | `String of string + | `Comment of string + | `EOF ] + +module Error = Markup_common.Error +module Ns = Markup_common.Ns + +val signal_to_string : [< signal ] -> string + +val parse_html : + ?report:(location -> Error.t -> unit) -> + ?encoding:encoding -> + ?context:[ `Document | `Fragment of string ] -> + ?depth_limit:int -> + string -> + (signal, sync) stream + +val parse_tokens : + ?report:(location -> Error.t -> unit) -> + ?context:[ `Document | `Fragment of string ] -> + ?depth_limit:int -> + (location * token) list -> + (signal, sync) stream + +val iter : ('a -> unit) -> ('a, sync) stream -> unit + +val write_html : + ?escape_attribute:(Buffer.t -> string -> unit) -> + ?escape_text:(Buffer.t -> string -> unit) -> + Buffer.t -> + (signal, sync) stream -> + unit + +val to_html_string : + ?escape_attribute:(Buffer.t -> string -> unit) -> + ?escape_text:(Buffer.t -> string -> unit) -> + (signal, sync) stream -> + string diff --git a/src/lite/namespace.ml b/src/lite/namespace.ml new file mode 100644 index 0000000..404fad0 --- /dev/null +++ b/src/lite/namespace.ml @@ -0,0 +1,203 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +let list_map_cps : ('a -> 'b cps) -> 'a list -> 'b list cps = + fun f l throw k -> + let rec loop accumulator = function + | [] -> k (List.rev accumulator) + | x :: l -> f x throw (fun x' -> loop (x' :: accumulator) l) + in + loop [] l + +module Parsing = struct + type context_entry = { f : string -> string option; previous : context_entry } + type context = context_entry ref + + let parse qualified_name = + try + let colon_index = String.index qualified_name ':' in + if colon_index = 0 then raise Not_found; + let prefix = String.sub qualified_name 0 colon_index in + let suffix = + String.sub qualified_name (colon_index + 1) + (String.length qualified_name - colon_index - 1) + in + (prefix, suffix) + with Not_found -> ("", qualified_name) + + let init top_level = + let f = function + | "xml" -> Some xml_ns + | "xmlns" -> Some xmlns_ns + | s -> top_level s + in + let rec entry = { f; previous = entry } in + ref entry + + let expand_element report context raw_element_name throw k = + let ns, name = parse raw_element_name in + match !context.f ns with + | Some uri -> k (uri, name) + | None -> ( + match ns with + | "" -> k ("", name) + | prefix -> + report () (`Bad_namespace prefix) throw (fun () -> k (prefix, name)) + ) + + let push report context raw_element_name raw_attributes throw k = + let parsed_attributes = + raw_attributes |> List.map (fun (name, value) -> (parse name, value)) + in + + let f = + parsed_attributes + |> List.fold_left + (fun f -> function + | ("xmlns", prefix), uri -> + fun p -> if p = prefix then Some uri else f p + | ("", "xmlns"), uri -> fun p -> if p = "" then Some uri else f p + | _ -> f) + !context.f + in + + let entry = { f; previous = !context } in + context := entry; + + expand_element report context raw_element_name throw + (fun expanded_element_name -> + list_map_cps + begin fun (name, value) _ k -> + match name with + | "", "xmlns" -> k ((xmlns_ns, "xmlns"), value) + | "", name -> k (("", name), value) + | ns, name -> ( + match f ns with + | Some uri -> k ((uri, name), value) + | None -> + report () (`Bad_namespace ns) throw (fun () -> + k ((ns, name), value))) + end + parsed_attributes throw + (fun expanded_attributes -> + k (expanded_element_name, expanded_attributes))) + + let pop ({ contents = { previous } } as context) = context := previous +end + +module StringMap = Map.Make (String) + +module Writing = struct + type context_entry = { + namespace_to_prefix : string list StringMap.t; + prefix_to_namespace : string StringMap.t; + previous : context_entry; + } + + type context = context_entry ref * (string -> string option) + + let init top_level = + let namespace_to_prefix = + StringMap.empty |> StringMap.add "" [ "" ] + |> StringMap.add xml_ns [ "xml" ] + |> StringMap.add xmlns_ns [ "xmlns" ] + in + + let prefix_to_namespace = + StringMap.empty |> StringMap.add "" "" |> StringMap.add "xml" xml_ns + |> StringMap.add "xmlns" xmlns_ns + in + + let rec entry = + { namespace_to_prefix; prefix_to_namespace; previous = entry } + in + + (ref entry, top_level) + + let lookup report allow_default context namespace throw k = + let candidate_prefixes = + try StringMap.find namespace !(fst context).namespace_to_prefix + with Not_found -> [] + in + + let prefix = + try + Some + (candidate_prefixes + |> List.find (fun prefix -> + (allow_default || prefix <> "") + && begin try + StringMap.find prefix !(fst context).prefix_to_namespace + = namespace + with Not_found -> false + end)) + with Not_found -> None + in + + let prefix = + match prefix with + | Some _ -> prefix + | None -> ( + match snd context namespace with + | None -> None + | Some prefix -> + if + ((not allow_default) && prefix = "") + || StringMap.mem prefix !(fst context).prefix_to_namespace + then None + else Some prefix) + in + + match prefix with + | None -> report () (`Bad_namespace namespace) throw (fun () -> k "") + | Some prefix -> k prefix + + let format prefix name = + match prefix with "" -> name | prefix -> prefix ^ ":" ^ name + + let unexpand_element report context (namespace, name) throw k = + lookup report true context namespace throw (fun prefix -> + k (format prefix name)) + + let unexpand_attribute report context ((namespace, name), value) throw k = + match namespace with + | "" -> k (name, value) + | uri -> + if uri = xmlns_ns && name = "xmlns" then k ("xmlns", value) + else + lookup report false context namespace throw (fun prefix -> + k (format prefix name, value)) + + let extend k v map = + let vs = try StringMap.find k map with Not_found -> [] in + StringMap.add k (v :: vs) map + + let push report context element_name attributes throw k = + let namespace_to_prefix, prefix_to_namespace = + attributes + |> List.fold_left + (fun (ns_to_prefix, prefix_to_ns) -> function + | (ns, "xmlns"), uri when ns = xmlns_ns -> + (extend uri "" ns_to_prefix, StringMap.add "" uri prefix_to_ns) + | (ns, prefix), uri when ns = xmlns_ns -> + ( extend uri prefix ns_to_prefix, + StringMap.add prefix uri prefix_to_ns ) + | _ -> (ns_to_prefix, prefix_to_ns)) + ( !(fst context).namespace_to_prefix, + !(fst context).prefix_to_namespace ) + in + + let entry = + { namespace_to_prefix; prefix_to_namespace; previous = !(fst context) } + in + fst context := entry; + + unexpand_element report context element_name throw (fun element_name -> + list_map_cps (unexpand_attribute report context) attributes throw + (fun attributes -> k (element_name, attributes))) + + let pop (({ contents = { previous } }, _) as context) = + fst context := previous +end diff --git a/src/lite/ragel_html_tokenizer.ml b/src/lite/ragel_html_tokenizer.ml new file mode 100644 index 0000000..ed7d4f9 --- /dev/null +++ b/src/lite/ragel_html_tokenizer.ml @@ -0,0 +1,1024 @@ +(* Derived from Devkit htmlStream_ragel.ml.rl. + Devkit is distributed under LGPL-2.1-only with the OCaml linking exception. + The original source is available from https://github.com/ygrek/ocaml-webstack. *) + +[@@@ocaml.warning "-38-32"] + +open Common +open Html_tokenizer + +type location_out = { mutable line : int; mutable column : int } + +type t = { + mutable data : string; + (* Whether [data] is a private copy that may be mutated in place. *) + mutable data_owned : bool; + cs : int ref; + p : int ref; + pe : int ref; + eof : int ref; + mark : int ref; + tag : string ref; + mutable last_start_tag : string; + mutable declaration : int; + mutable bogus : int; + mutable tag_scan : int; + mutable end_scan : int; + mutable line : int; + tokens : Html_tokenizer.token array; + lines : int array; + mutable read : int; + mutable write : int; + mutable finished : bool; +} + +let decode = Html_entity_decoder.decode + +(* The first occurrence of a name wins, like src/baseline. *) +let attributes attrs = + let rec dedupe seen = function + | [] -> [] + | (name, value) :: rest -> + if List.mem name seen then dedupe seen rest + else + (name, Html_entity_decoder.decode_attribute value) + :: dedupe (name :: seen) rest + in + dedupe [] attrs + +let make_tag ?(self_closing = false) name attributes = + { Token_tag.name; attributes; self_closing } + +let normalize_name text = + let text = String.lowercase_ascii text in + if not (String.contains text '\x00') then text + else begin + let buffer = Buffer.create (String.length text + 8) in + String.iter + (fun byte -> + if byte = '\x00' then Buffer.add_string buffer "\xEF\xBF\xBD" + else Buffer.add_char buffer byte) + text; + Buffer.contents buffer + end + +let buffer_capacity = 128 +let maximum_transition_output = 3 + +let emit scanner token = + scanner.tokens.(scanner.write) <- token; + scanner.lines.(scanner.write) <- scanner.line; + scanner.write <- scanner.write + 1 + +(* The tree builder treats a leading whitespace run differently from the rest + of a text run in several insertion modes; src/baseline gets this for free + from per-character tokens. *) +let emit_text scanner text = + let length = String.length text in + let rec whitespace_end index = + if index < length then + match text.[index] with + | '\t' | '\n' | '\x0C' | '\r' | ' ' -> whitespace_end (index + 1) + | _ -> index + else index + in + let boundary = whitespace_end 0 in + if boundary = 0 || boundary = length then emit scanner (String text) + else begin + emit scanner (String (String.sub text 0 boundary)); + emit scanner (String (String.sub text boundary (length - boundary))) + end + +let _htmlstream_trans_keys : int array = + Array.concat [ [| 10; 60; 10; 60; 10; 122; 10; 122; 9; 62; 9; 62; 0 |] ] + +let _htmlstream_key_spans : int array = + Array.concat [ [| 51; 51; 113; 113; 54; 54 |] ] + +let _htmlstream_index_offsets : int array = + Array.concat [ [| 0; 52; 104; 218; 332; 387 |] ] + +let _htmlstream_indicies : int array = + Array.concat + [ + [| + 1; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 0; + 2; + 0; + 4; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 3; + 5; + 3; + 7; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 8; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 9; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 6; + 8; + 6; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 6; + 6; + 6; + 6; + 6; + 6; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 10; + 6; + 12; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 11; + 13; + 11; + 11; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 11; + 11; + 11; + 11; + 11; + 11; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 14; + 11; + 16; + 17; + 15; + 16; + 16; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 16; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 16; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 15; + 16; + 15; + 19; + 20; + 18; + 19; + 19; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 19; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 19; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 18; + 19; + 18; + 0; + |]; + ] + +let _htmlstream_trans_targs : int array = + Array.concat + [ [| 1; 1; 2; 1; 1; 2; 0; 0; 0; 3; 5; 0; 0; 0; 4; 4; 1; 1; 5; 1; 1 |] ] + +let _htmlstream_trans_actions : int array = + Array.concat + [ + [| 1; 2; 0; 0; 4; 3; 6; 7; 8; 0; 1; 10; 11; 0; 1; 0; 13; 14; 0; 16; 17 |]; + ] + +let _htmlstream_eof_actions : int array = + Array.concat [ [| 0; 3; 5; 9; 12; 15 |] ] + +let htmlstream_start : int = 0 +let htmlstream_first_final : int = 0 +let htmlstream_error : int = -1 +let htmlstream_en_main : int = 0 + +type _htmlstream_state = { mutable keys : int; mutable trans : int } + +exception Goto_match_htmlstream +exception Goto_again_htmlstream +exception Goto_eof_trans_htmlstream + +let create data = + let cs = ref 0 in + + begin + cs.contents <- htmlstream_start + end; + + let length = String.length data in + { + data; + data_owned = false; + cs; + p = ref 0; + pe = ref length; + eof = ref length; + mark = ref (-1); + tag = ref ""; + last_start_tag = ""; + declaration = -1; + bogus = -1; + tag_scan = -1; + end_scan = -1; + line = 1; + tokens = Array.make buffer_capacity EOF; + lines = Array.make buffer_capacity 1; + read = 0; + write = 0; + finished = false; + } + +let scan_data_text scanner = + let start = !(scanner.p) in + let limit = !(scanner.eof) in + if + !(scanner.cs) <> htmlstream_en_main + || !(scanner.mark) >= 0 || start >= limit + || scanner.data.[start] = '<' + then false + else begin + let stop = String_helpers.find scanner.data start '<' in + scanner.line <- + scanner.line + String_helpers.count scanner.data start stop '\n'; + scanner.p := stop; + emit_text scanner (decode (String.sub scanner.data start (stop - start))); + if stop >= limit then scanner.finished <- true; + true + end + +let run scanner foreign = + let data = scanner.data in + let cs = scanner.cs in + let p = scanner.p in + let pe = scanner.pe in + let eof = scanner.eof in + let mark = scanner.mark in + let tag = scanner.tag in + pe := !eof; + let pause () = + if scanner.write >= buffer_capacity - maximum_transition_output && !p < !eof + then pe := !p + 1 + in + let sub () = + assert (!mark >= 0); + let text = if !p <= !mark then "" else String.sub data !mark (!p - !mark) in + mark := -1; + text + in + if scanner.tag_scan >= 0 then begin + let start = scanner.tag_scan in + scanner.tag_scan <- -1; + let name = !tag in + let result = Tag_attributes.scan data start in + let next = + if not result.Tag_attributes.ok then !eof + else begin + let attrs = attributes result.Tag_attributes.attributes in + let self_closing = result.Tag_attributes.self_closing in + scanner.last_start_tag <- name; + emit scanner (Start (make_tag ~self_closing name attrs)); + result.Tag_attributes.next + end + in + for index = start + 1 to next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + p := next; + cs := htmlstream_en_main; + (* clear so [scan_data_text] has a chance to fire again *) + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scanner.end_scan >= 0 then begin + let start = scanner.end_scan in + scanner.end_scan <- -1; + let name = !tag in + let result = Tag_attributes.scan data start in + if result.Tag_attributes.ok then emit scanner (End (make_tag name [])); + let next = + if result.Tag_attributes.ok then result.Tag_attributes.next else !eof + in + for index = start + 1 to next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + p := next; + cs := htmlstream_en_main; + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scanner.bogus >= 0 then begin + let start = scanner.bogus in + scanner.bogus <- -1; + (* The consumed character is a codepoint, not a byte. *) + let width = + if data.[start] < '\x80' then 1 + else if data.[start] < '\xE0' then 2 + else if data.[start] < '\xF0' then 3 + else 4 + in + let start = min (start + width) !eof in + let result = Markup_declaration.bogus_comment data start in + emit scanner result.Markup_declaration.token; + for index = start to result.Markup_declaration.next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + p := result.Markup_declaration.next; + cs := htmlstream_en_main; + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scanner.declaration >= 0 then begin + let start = scanner.declaration in + scanner.declaration <- -1; + let result = Markup_declaration.scan ~foreign data start in + emit scanner result.Markup_declaration.token; + for index = start to result.Markup_declaration.next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + if result.Markup_declaration.lowercase > 0 then begin + let bytes = + if scanner.data_owned then Bytes.unsafe_of_string scanner.data + else begin + let copy = Bytes.of_string scanner.data in + scanner.data <- Bytes.unsafe_to_string copy; + scanner.data_owned <- true; + copy + end + in + let next = result.Markup_declaration.next in + for index = next to next + result.Markup_declaration.lowercase - 1 do + Bytes.set bytes index (Char.lowercase_ascii (Bytes.get bytes index)) + done + end; + p := result.Markup_declaration.next; + cs := htmlstream_en_main; + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scan_data_text scanner then () + else begin + begin + let state = { keys = 0; trans = 0 } in + let rec do_start () = + if p.contents = pe.contents then do_test_eof () else do_resume () + and do_resume () = + begin try + let keys = cs.contents lsl 1 in + let inds = _htmlstream_index_offsets.(cs.contents) in + + let slen = _htmlstream_key_spans.(cs.contents) in + state.trans <- + _htmlstream_indicies.(inds + + + if + slen > 0 + && _htmlstream_trans_keys.(keys) + <= Char.code data.[p.contents] + && Char.code data.[p.contents] + <= _htmlstream_trans_keys.(keys + 1) + then + Char.code data.[p.contents] + - _htmlstream_trans_keys.(keys) + else slen) + with Goto_match_htmlstream -> () + end; + do_eof_trans () + and do_eof_trans () = + cs.contents <- _htmlstream_trans_targs.(state.trans); + + begin try + if _htmlstream_trans_actions.(state.trans) = 0 then + raise_notrace Goto_again_htmlstream; + + match _htmlstream_trans_actions.(state.trans) with + | 1 -> + begin + mark := !p + end; + () + | 3 -> + begin + emit_text scanner (decode (sub ())); + pause () + end; + () + | 8 -> + begin + scanner.declaration <- !p; + pe := !p + 1 + end; + () + | 10 -> + begin + scanner.bogus <- !p; + pe := !p + 1 + end; + () + | 6 -> + begin + emit scanner (String "<"); + pause (); + p.contents <- p.contents - 1; + begin + cs.contents <- 0; + if true then raise_notrace Goto_again_htmlstream + end + end; + () + | 4 -> + begin + scanner.line <- scanner.line + 1 + end; + () + | 2 -> + begin + mark := !p + end; + begin + scanner.line <- scanner.line + 1 + end; + () + | 13 -> + begin + tag := normalize_name @@ sub (); + scanner.end_scan <- !p; + p.contents <- p.contents - 1; + pe := !p + 1 + end; + begin + mark := !p + end; + () + | 16 -> + begin + tag := normalize_name @@ sub (); + scanner.tag_scan <- !p; + p.contents <- p.contents - 1; + pe := !p + 1 + end; + begin + mark := !p + end; + () + | 11 -> + begin + scanner.bogus <- !p; + pe := !p + 1 + end; + begin + scanner.line <- scanner.line + 1 + end; + () + | 7 -> + begin + emit scanner (String "<"); + pause (); + p.contents <- p.contents - 1; + begin + cs.contents <- 0; + if true then raise_notrace Goto_again_htmlstream + end + end; + begin + scanner.line <- scanner.line + 1 + end; + () + | 14 -> + begin + tag := normalize_name @@ sub (); + scanner.end_scan <- !p; + p.contents <- p.contents - 1; + pe := !p + 1 + end; + begin + mark := !p + end; + begin + scanner.line <- scanner.line + 1 + end; + () + | 17 -> + begin + tag := normalize_name @@ sub (); + scanner.tag_scan <- !p; + p.contents <- p.contents - 1; + pe := !p + 1 + end; + begin + mark := !p + end; + begin + scanner.line <- scanner.line + 1 + end; + () + | _ -> () + with Goto_again_htmlstream -> () + end; + + do_again () + and do_again () = + p.contents <- p.contents + 1; + if p.contents <> pe.contents then do_resume () else do_test_eof () + and do_test_eof () = + if p.contents = eof.contents then + begin try + begin match _htmlstream_eof_actions.(cs.contents) with + | 12 -> + begin + tag := normalize_name @@ sub (); + scanner.end_scan <- !p; + p.contents <- p.contents - 1; + pe := !p + 1 + end; + () + | 3 -> + begin + emit_text scanner (decode (sub ())); + pause () + end; + () + | 15 -> + begin + tag := normalize_name @@ sub (); + scanner.tag_scan <- !p; + p.contents <- p.contents - 1; + pe := !p + 1 + end; + () + | 5 -> + begin + emit scanner (String "<") + end; + () + | 9 -> + begin + emit scanner (String "<"); + emit scanner (String "/") + end; + () + | _ -> () + end + with + | Goto_again_htmlstream -> do_again () + | Goto_eof_trans_htmlstream -> do_eof_trans () + end + in + do_start () + end; + + if + scanner.declaration >= 0 || scanner.bogus >= 0 || scanner.tag_scan >= 0 + || scanner.end_scan >= 0 + then () + else if !p >= !eof then scanner.finished <- true + else if scanner.write = 0 then scanner.finished <- true + end + +(* The tree builder requested a non-Data state for the next scan; the last + start tag emitted is the appropriate end tag. In fragment parsing no start + tag has been seen, so no end tag ever matches. *) +let scan_raw_state scanner state drop_candidate = + let data = scanner.data in + let start = !(scanner.p) in + if start >= !(scanner.eof) then scanner.finished <- true + else begin + let next_index = + match (state : Html_tokenizer.state) with + | PLAINTEXT -> + let body = Raw_text.plaintext data start in + emit scanner (String body.Raw_text.text); + body.Raw_text.next + | _ -> + let name = scanner.last_start_tag in + if name = "" then begin + let body = Raw_text.plaintext data start in + let text = + match (state : Html_tokenizer.state) with + | RCDATA -> decode body.Raw_text.text + | _ -> body.Raw_text.text + in + emit scanner (String text); + body.Raw_text.next + end + else begin + let body = + Raw_text.scan ~drop_end_tag_candidate:drop_candidate data start + name + in + emit scanner (String body.Raw_text.text); + if body.Raw_text.had_end_tag then + emit scanner (End (make_tag name [])); + body.Raw_text.next + end + in + for index = start to next_index - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + scanner.p := next_index; + scanner.cs := htmlstream_en_main; + scanner.mark := -1; + if next_index >= !(scanner.eof) then scanner.finished <- true + end + +let rec next scanner (state : Html_tokenizer.state) foreign ~drop_candidate + (location : location_out) = + if scanner.read < scanner.write then begin + let index = scanner.read in + let token = scanner.tokens.(index) in + location.line <- scanner.lines.(index); + location.column <- -1; + scanner.tokens.(index) <- EOF; + scanner.read <- index + 1; + token + end + else if scanner.finished then begin + location.line <- scanner.line; + location.column <- -1; + EOF + end + else if state <> Data then begin + scanner.read <- 0; + scanner.write <- 0; + scan_raw_state scanner state (foreign && drop_candidate); + next scanner Data foreign ~drop_candidate location + end + else begin + scanner.read <- 0; + scanner.write <- 0; + run scanner foreign; + next scanner state foreign ~drop_candidate location + end diff --git a/src/lite/ragel_html_tokenizer.ml.rl b/src/lite/ragel_html_tokenizer.ml.rl new file mode 100644 index 0000000..0cbc7dd --- /dev/null +++ b/src/lite/ragel_html_tokenizer.ml.rl @@ -0,0 +1,390 @@ +(* Derived from Devkit htmlStream_ragel.ml.rl. + Devkit is distributed under LGPL-2.1-only with the OCaml linking exception. + The original source is available from https://github.com/ygrek/ocaml-webstack. *) + +[@@@ocaml.warning "-38-32"] + +open Common +open Html_tokenizer + +type location_out = { + mutable line : int; + mutable column : int; +} + +type t = { + mutable data : string; + (* Whether [data] is a private copy that may be mutated in place. *) + mutable data_owned : bool; + cs : int ref; + p : int ref; + pe : int ref; + eof : int ref; + mark : int ref; + tag : string ref; + mutable last_start_tag : string; + mutable declaration : int; + mutable bogus : int; + mutable tag_scan : int; + mutable end_scan : int; + mutable line : int; + tokens : Html_tokenizer.token array; + lines : int array; + mutable read : int; + mutable write : int; + mutable finished : bool; +} + +let decode = Html_entity_decoder.decode + +(* The first occurrence of a name wins, like src/baseline. *) +let attributes attrs = + let rec dedupe seen = function + | [] -> [] + | (name, value) :: rest -> + if List.mem name seen then dedupe seen rest + else + (name, Html_entity_decoder.decode_attribute value) + :: dedupe (name :: seen) rest + in + dedupe [] attrs + +let make_tag ?(self_closing = false) name attributes = + {Token_tag.name; attributes; self_closing} + +let normalize_name text = + let text = String.lowercase_ascii text in + if not (String.contains text '\x00') then text + else begin + let buffer = Buffer.create (String.length text + 8) in + String.iter + (fun byte -> + if byte = '\x00' then Buffer.add_string buffer "\xEF\xBF\xBD" + else Buffer.add_char buffer byte) + text; + Buffer.contents buffer + end + +let buffer_capacity = 128 +let maximum_transition_output = 3 + +let emit scanner token = + scanner.tokens.(scanner.write) <- token; + scanner.lines.(scanner.write) <- scanner.line; + scanner.write <- scanner.write + 1 + +(* The tree builder treats a leading whitespace run differently from the rest + of a text run in several insertion modes; src/baseline gets this for free + from per-character tokens. *) +let emit_text scanner text = + let length = String.length text in + let rec whitespace_end index = + if index < length then + match text.[index] with + | '\t' | '\n' | '\x0C' | '\r' | ' ' -> whitespace_end (index + 1) + | _ -> index + else index + in + let boundary = whitespace_end 0 in + if boundary = 0 || boundary = length then emit scanner (String text) + else begin + emit scanner (String (String.sub text 0 boundary)); + emit scanner (String (String.sub text boundary (length - boundary))) + end + +%%{ + machine htmlstream; + + action mark { mark := !p } + action close_tag { + tag := normalize_name @@ sub (); + scanner.end_scan <- !p; + fhold; + pe := !p + 1; + } + action text { + emit_text scanner (decode (sub ())); + pause (); + } + action tag_start { + tag := normalize_name @@ sub (); + scanner.tag_scan <- !p; + fhold; + pe := !p + 1; + } + action markup_declaration { scanner.declaration <- !p; pe := !p + 1; } + action bogus_close { scanner.bogus <- !p; pe := !p + 1; } + action lt_text { + emit scanner (String "<"); + pause (); + fhold; + fgoto main; + } + action eof_lt { emit scanner (String "<") } + action eof_lt_slash { + emit scanner (String "<"); + emit scanner (String "/") + } + + count_newlines = ('\n' >{ scanner.line <- scanner.line + 1 } | ^'\n'+)**; + + html_ws = 0x09 | 0x0A | 0x0C | 0x0D | 0x20; + tag_name = alpha ( any - ( html_ws | '/' | '>' ) )*; + + close_tag = tag_name >mark %close_tag; + open_tag = tag_name >mark %tag_start; + declaration = ('!'|'?') @markup_declaration; + tag = ('<' %/eof_lt) ( + ('/' %/eof_lt_slash) + ( close_tag + | '>' + | ( any - ( alpha | '>' ) ) @bogus_close ) + | open_tag + | declaration + | ( any - ( alpha | '/' | '!' | '?' ) ) @lt_text ); + main := (((tag | ^'<' >mark ^'<'* %text ) )** | count_newlines); + + write data; +}%% + +let create data = + let cs = ref 0 in + %%write init; + let length = String.length data in + {data; + data_owned = false; + cs; + p = ref 0; + pe = ref length; + eof = ref length; + mark = ref (-1); + tag = ref ""; + last_start_tag = ""; + declaration = (-1); + bogus = (-1); + tag_scan = (-1); + end_scan = (-1); + line = 1; + tokens = Array.make buffer_capacity EOF; + lines = Array.make buffer_capacity 1; + read = 0; + write = 0; + finished = false} + +let scan_data_text scanner = + let start = !(scanner.p) in + let limit = !(scanner.eof) in + if + !(scanner.cs) <> htmlstream_en_main + || !(scanner.mark) >= 0 + || start >= limit + || scanner.data.[start] = '<' + then false + else begin + let stop = String_helpers.find scanner.data start '<' in + scanner.line <- + scanner.line + String_helpers.count scanner.data start stop '\n'; + scanner.p := stop; + emit_text scanner + (decode (String.sub scanner.data start (stop - start))); + if stop >= limit then scanner.finished <- true; + true + end + +let run scanner foreign = + let data = scanner.data in + let cs = scanner.cs in + let p = scanner.p in + let pe = scanner.pe in + let eof = scanner.eof in + let mark = scanner.mark in + let tag = scanner.tag in + pe := !eof; + let pause () = + if scanner.write >= buffer_capacity - maximum_transition_output && + !p < !eof then + pe := !p + 1 + in + let sub () = + assert (!mark >= 0); + let text = + if !p <= !mark then "" else String.sub data !mark (!p - !mark) + in + mark := -1; + text + in + if scanner.tag_scan >= 0 then begin + let start = scanner.tag_scan in + scanner.tag_scan <- (-1); + let name = !tag in + let result = Tag_attributes.scan data start in + let next = + if not result.Tag_attributes.ok then !eof + else begin + let attrs = attributes result.Tag_attributes.attributes in + let self_closing = result.Tag_attributes.self_closing in + scanner.last_start_tag <- name; + emit scanner (Start (make_tag ~self_closing name attrs)); + result.Tag_attributes.next + end + in + for index = start + 1 to next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + p := next; + cs := htmlstream_en_main; + (* clear so [scan_data_text] has a chance to fire again *) + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scanner.end_scan >= 0 then begin + let start = scanner.end_scan in + scanner.end_scan <- (-1); + let name = !tag in + let result = Tag_attributes.scan data start in + if result.Tag_attributes.ok then + emit scanner (End (make_tag name [])); + let next = + if result.Tag_attributes.ok then result.Tag_attributes.next else !eof + in + for index = start + 1 to next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + p := next; + cs := htmlstream_en_main; + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scanner.bogus >= 0 then begin + let start = scanner.bogus in + scanner.bogus <- (-1); + (* The consumed character is a codepoint, not a byte. *) + let width = + if data.[start] < '\x80' then 1 + else if data.[start] < '\xE0' then 2 + else if data.[start] < '\xF0' then 3 + else 4 + in + let start = min (start + width) !eof in + let result = Markup_declaration.bogus_comment data start in + emit scanner result.Markup_declaration.token; + for index = start to result.Markup_declaration.next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + p := result.Markup_declaration.next; + cs := htmlstream_en_main; + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scanner.declaration >= 0 then begin + let start = scanner.declaration in + scanner.declaration <- (-1); + let result = Markup_declaration.scan ~foreign data start in + emit scanner result.Markup_declaration.token; + for index = start to result.Markup_declaration.next - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + if result.Markup_declaration.lowercase > 0 then begin + let bytes = + if scanner.data_owned then Bytes.unsafe_of_string scanner.data + else begin + let copy = Bytes.of_string scanner.data in + scanner.data <- Bytes.unsafe_to_string copy; + scanner.data_owned <- true; + copy + end + in + let next = result.Markup_declaration.next in + for index = next to next + result.Markup_declaration.lowercase - 1 do + Bytes.set bytes index (Char.lowercase_ascii (Bytes.get bytes index)) + done + end; + p := result.Markup_declaration.next; + cs := htmlstream_en_main; + mark := -1; + if !p >= !eof then scanner.finished <- true + end + else if scan_data_text scanner then () + else begin + %%write exec; + if + scanner.declaration >= 0 || scanner.bogus >= 0 || scanner.tag_scan >= 0 + || scanner.end_scan >= 0 + then () + else if !p >= !eof then scanner.finished <- true + else if scanner.write = 0 then scanner.finished <- true + end + +(* The tree builder requested a non-Data state for the next scan; the last + start tag emitted is the appropriate end tag. In fragment parsing no start + tag has been seen, so no end tag ever matches. *) +let scan_raw_state scanner state drop_candidate = + let data = scanner.data in + let start = !(scanner.p) in + if start >= !(scanner.eof) then scanner.finished <- true + else begin + let next_index = + match (state : Html_tokenizer.state) with + | PLAINTEXT -> + let body = Raw_text.plaintext data start in + emit scanner (String body.Raw_text.text); + body.Raw_text.next + | _ -> + let name = scanner.last_start_tag in + if name = "" then begin + let body = Raw_text.plaintext data start in + let text = + match (state : Html_tokenizer.state) with + | RCDATA -> decode body.Raw_text.text + | _ -> body.Raw_text.text + in + emit scanner (String text); + body.Raw_text.next + end + else begin + let body = + Raw_text.scan ~drop_end_tag_candidate:drop_candidate data start name + in + emit scanner (String body.Raw_text.text); + if body.Raw_text.had_end_tag then + emit scanner (End (make_tag name [])); + body.Raw_text.next + end + in + for index = start to next_index - 1 do + if data.[index] = '\n' then scanner.line <- scanner.line + 1 + done; + scanner.p := next_index; + scanner.cs := htmlstream_en_main; + scanner.mark := -1; + if next_index >= !(scanner.eof) then scanner.finished <- true + end + +let rec next scanner (state : Html_tokenizer.state) foreign ~drop_candidate + (location : location_out) = + if scanner.read < scanner.write then begin + let index = scanner.read in + let token = scanner.tokens.(index) in + location.line <- scanner.lines.(index); + location.column <- -1; + scanner.tokens.(index) <- EOF; + scanner.read <- index + 1; + token + end + else if scanner.finished then begin + location.line <- scanner.line; + location.column <- -1; + EOF + end + else if state <> Data then begin + scanner.read <- 0; + scanner.write <- 0; + scan_raw_state scanner state (foreign && drop_candidate); + next scanner Data foreign ~drop_candidate location + end + else begin + scanner.read <- 0; + scanner.write <- 0; + run scanner foreign; + next scanner state foreign ~drop_candidate location + end diff --git a/src/lite/ragel_html_tokenizer.mli b/src/lite/ragel_html_tokenizer.mli new file mode 100644 index 0000000..62316e5 --- /dev/null +++ b/src/lite/ragel_html_tokenizer.mli @@ -0,0 +1,15 @@ +(* Derived from Devkit's htmlStream_ragel.ml.rl. + Devkit is distributed under LGPL-2.1-only with the OCaml linking exception. *) + +type location_out = { mutable line : int; mutable column : int } +type t + +val create : string -> t + +val next : + t -> + Html_tokenizer.state -> + bool -> + drop_candidate:bool -> + location_out -> + Html_tokenizer.token diff --git a/src/lite/raw_text.ml b/src/lite/raw_text.ml new file mode 100644 index 0000000..6e8fe95 --- /dev/null +++ b/src/lite/raw_text.ml @@ -0,0 +1,311 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* Port of the RCDATA, RAWTEXT, and script data tokenizer states of + src/baseline/html_tokenizer.ml (8.2.4.3, 8.2.4.5, 8.2.4.6, and + 8.2.4.11-8.2.4.43), operating on the normalized input string. [scan] is + given the index just after the '>' of the opening tag and the lowercase + element name, and consumes the element's raw content up to and including + its end tag, if any. *) + +type result = { text : string; had_end_tag : bool; next : int } + +let u_rep_utf_8 = "\xEF\xBF\xBD" + +let add buffer byte = + if byte = '\x00' then Buffer.add_string buffer u_rep_utf_8 + else Buffer.add_char buffer byte + +let is_whitespace = function '\t' | '\n' | '\x0C' | ' ' -> true | _ -> false +let is_letter = function 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false + +let plaintext data start = + let length = String.length data in + let buffer = Buffer.create (length - start) in + for index = start to length - 1 do + add buffer data.[index] + done; + { text = Buffer.contents buffer; had_end_tag = false; next = length } + +let scan ?(drop_end_tag_candidate = false) data start tag = + let length = String.length data in + let decode = match tag with "title" | "textarea" -> true | _ -> false in + let buffer = Buffer.create 256 in + let finish had_end_tag next = + let text = Buffer.contents buffer in + let text = if decode then Html_entity_decoder.decode text else text in + { text; had_end_tag; next } + in + let word_is first after word = + after - first = String.length word + && begin + let rec check offset = + offset >= String.length word + || Char.lowercase_ascii data.[first + offset] = word.[offset] + && check (offset + 1) + in + check 0 + end + in + (* End tag attributes are parsed only to find the terminating '>'; names + and values are discarded. *) + let rec before_attribute_name index = + if index >= length then None + else + match data.[index] with + | byte when is_whitespace byte -> before_attribute_name (index + 1) + | '/' -> self_closing_start_tag (index + 1) + | '>' -> Some (index + 1) + | _ -> attribute_name (index + 1) + and attribute_name index = + if index >= length then None + else + match data.[index] with + | byte when is_whitespace byte -> after_attribute_name (index + 1) + | '/' -> self_closing_start_tag (index + 1) + | '=' -> before_attribute_value (index + 1) + | '>' -> Some (index + 1) + | _ -> attribute_name (index + 1) + and after_attribute_name index = + if index >= length then None + else + match data.[index] with + | byte when is_whitespace byte -> after_attribute_name (index + 1) + | '/' -> self_closing_start_tag (index + 1) + | '=' -> before_attribute_value (index + 1) + | '>' -> Some (index + 1) + | _ -> attribute_name (index + 1) + and before_attribute_value index = + if index >= length then None + else + match data.[index] with + | byte when is_whitespace byte -> before_attribute_value (index + 1) + | ('"' | '\'') as quote -> attribute_value_quoted quote (index + 1) + | '>' -> Some (index + 1) + | _ -> attribute_value_unquoted (index + 1) + and attribute_value_quoted quote index = + if index >= length then None + else if data.[index] = quote then after_attribute_value_quoted (index + 1) + else attribute_value_quoted quote (index + 1) + and after_attribute_value_quoted index = + if index >= length then None + else + match data.[index] with + | byte when is_whitespace byte -> before_attribute_name (index + 1) + | '/' -> self_closing_start_tag (index + 1) + | '>' -> Some (index + 1) + | _ -> before_attribute_name index + and attribute_value_unquoted index = + if index >= length then None + else + match data.[index] with + | byte when is_whitespace byte -> before_attribute_name (index + 1) + | '>' -> Some (index + 1) + | _ -> attribute_value_unquoted (index + 1) + and self_closing_start_tag index = + if index >= length then None + else if data.[index] = '>' then Some (index + 1) + else before_attribute_name index + in + let finish_tag = function + | Some next -> finish true next + | None -> finish false length + in + let rec text index = + if index >= length then finish false index + else + match data.[index] with + | '<' -> text_less_than_sign index (index + 1) + | byte -> + add buffer byte; + text (index + 1) + and text_less_than_sign lt index = + if index < length && data.[index] = '/' then end_tag_open text lt (index + 1) + else begin + Buffer.add_char buffer '<'; + text index + end + and end_tag_open state lt index = + if index < length && is_letter data.[index] then + end_tag_name state lt (index + 1) + else begin + Buffer.add_string buffer + (if index >= length && drop_end_tag_candidate then "<" else "= length then dump () + else + match data.[index] with + | byte when is_whitespace byte && appropriate () -> + finish_tag (before_attribute_name (index + 1)) + | '/' when appropriate () -> + finish_tag (self_closing_start_tag (index + 1)) + | '>' when appropriate () -> finish true (index + 1) + | byte when is_letter byte -> end_tag_name state lt (index + 1) + | _ -> dump () + and script index = + if index >= length then finish false index + else + match data.[index] with + | '<' -> script_less_than_sign index (index + 1) + | byte -> + add buffer byte; + script (index + 1) + and script_less_than_sign lt index = + if index >= length then begin + Buffer.add_char buffer '<'; + script index + end + else + match data.[index] with + | '/' -> end_tag_open script lt (index + 1) + | '!' -> + Buffer.add_string buffer " + Buffer.add_char buffer '<'; + script index + and escape_start index = + if index < length && data.[index] = '-' then begin + Buffer.add_char buffer '-'; + escape_start_dash (index + 1) + end + else script index + and escape_start_dash index = + if index < length && data.[index] = '-' then begin + Buffer.add_char buffer '-'; + escaped_dash_dash (index + 1) + end + else script index + and escaped index = + if index >= length then finish false index + else + match data.[index] with + | '-' -> + Buffer.add_char buffer '-'; + escaped_dash (index + 1) + | '<' -> escaped_less_than_sign index (index + 1) + | byte -> + add buffer byte; + escaped (index + 1) + and escaped_dash index = + if index >= length then finish false index + else + match data.[index] with + | '-' -> + Buffer.add_char buffer '-'; + escaped_dash_dash (index + 1) + | '<' -> escaped_less_than_sign index (index + 1) + | byte -> + add buffer byte; + escaped (index + 1) + and escaped_dash_dash index = + if index >= length then finish false index + else + match data.[index] with + | '-' -> + Buffer.add_char buffer '-'; + escaped_dash_dash (index + 1) + | '<' -> escaped_less_than_sign index (index + 1) + | '>' -> + Buffer.add_char buffer '>'; + script (index + 1) + | byte -> + add buffer byte; + escaped (index + 1) + and escaped_less_than_sign lt index = + if index >= length then begin + Buffer.add_char buffer '<'; + escaped index + end + else + match data.[index] with + | '/' -> end_tag_open escaped lt (index + 1) + | byte when is_letter byte -> + Buffer.add_char buffer '<'; + Buffer.add_char buffer byte; + double_escape_start index (index + 1) + | _ -> + Buffer.add_char buffer '<'; + escaped index + and double_escape_start first index = + if index >= length then escaped index + else + match data.[index] with + | ('\t' | '\n' | '\x0C' | ' ' | '/' | '>') as byte -> + Buffer.add_char buffer byte; + if word_is first index "script" then double_escaped (index + 1) + else escaped (index + 1) + | byte when is_letter byte -> + Buffer.add_char buffer byte; + double_escape_start first (index + 1) + | _ -> escaped index + and double_escaped index = + if index >= length then finish false index + else + match data.[index] with + | '-' -> + Buffer.add_char buffer '-'; + double_escaped_dash (index + 1) + | '<' -> + Buffer.add_char buffer '<'; + double_escaped_less_than_sign (index + 1) + | byte -> + add buffer byte; + double_escaped (index + 1) + and double_escaped_dash index = + if index >= length then finish false index + else + match data.[index] with + | '-' -> + Buffer.add_char buffer '-'; + double_escaped_dash_dash (index + 1) + | '<' -> + Buffer.add_char buffer '<'; + double_escaped_less_than_sign (index + 1) + | byte -> + add buffer byte; + double_escaped (index + 1) + and double_escaped_dash_dash index = + if index >= length then finish false index + else + match data.[index] with + | '-' -> + Buffer.add_char buffer '-'; + double_escaped_dash_dash (index + 1) + | '<' -> + Buffer.add_char buffer '<'; + double_escaped_less_than_sign (index + 1) + | '>' -> + Buffer.add_char buffer '>'; + script (index + 1) + | byte -> + add buffer byte; + double_escaped (index + 1) + and double_escaped_less_than_sign index = + if index < length && data.[index] = '/' then begin + Buffer.add_char buffer '/'; + double_escape_end (index + 1) (index + 1) + end + else double_escaped index + and double_escape_end first index = + if index >= length then double_escaped index + else + match data.[index] with + | ('\t' | '\n' | '\x0C' | ' ' | '/' | '>') as byte -> + Buffer.add_char buffer byte; + if word_is first index "script" then escaped (index + 1) + else double_escaped (index + 1) + | byte when is_letter byte -> + Buffer.add_char buffer byte; + double_escape_end first (index + 1) + | _ -> double_escaped index + in + match tag with "script" -> script start | _ -> text start diff --git a/src/lite/string_helpers.c b/src/lite/string_helpers.c new file mode 100644 index 0000000..f51b11a --- /dev/null +++ b/src/lite/string_helpers.c @@ -0,0 +1,31 @@ +/* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. */ + +#include +#include + +CAMLprim value markup_lite_string_helpers_find( + value string, value start, value character) +{ + const char *data = String_val(string); + const mlsize_t length = caml_string_length(string); + const mlsize_t offset = Long_val(start); + const char *found = + memchr(data + offset, Int_val(character), length - offset); + return Val_long(found == NULL ? length : (mlsize_t)(found - data)); +} + +CAMLprim value markup_lite_string_helpers_count( + value string, value start, value stop, value character) +{ + const unsigned char *data = (const unsigned char *)String_val(string); + const mlsize_t first = Long_val(start); + const mlsize_t last = Long_val(stop); + const unsigned char byte = Int_val(character); + mlsize_t count = 0; + + for (mlsize_t index = first; index < last; ++index) + count += data[index] == byte; + + return Val_long(count); +} diff --git a/src/lite/string_helpers.ml b/src/lite/string_helpers.ml new file mode 100644 index 0000000..64031ba --- /dev/null +++ b/src/lite/string_helpers.ml @@ -0,0 +1,20 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +external unsafe_find : string -> int -> char -> int + = "markup_lite_string_helpers_find" +[@@noalloc] + +external unsafe_count : string -> int -> int -> char -> int + = "markup_lite_string_helpers_count" +[@@noalloc] + +let find string start character = + if start < 0 || start > String.length string then + invalid_arg "String_helpers.find"; + unsafe_find string start character + +let count string start stop character = + if start < 0 || stop < start || stop > String.length string then + invalid_arg "String_helpers.count"; + unsafe_count string start stop character diff --git a/src/lite/tag_attributes.ml b/src/lite/tag_attributes.ml new file mode 100644 index 0000000..3ec0a87 --- /dev/null +++ b/src/lite/tag_attributes.ml @@ -0,0 +1,139 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +(* Port of the attribute tokenizer states of src/baseline/html_tokenizer.ml + (8.2.4.34-43 plus the self-closing state). [scan] is given the index of + the first byte after a start tag's name and returns the attributes in + source order with raw (undecoded) values, whether the tag is + self-closing, and the index after the closing '>'. [ok] is false when + the input ends inside the tag, in which case no token is emitted. *) + +type result = { + attributes : (string * string) list; + self_closing : bool; + next : int; + ok : bool; +} + +let u_rep_utf_8 = "\xEF\xBF\xBD" + +let add buffer byte = + if byte = '\x00' then Buffer.add_string buffer u_rep_utf_8 + else Buffer.add_char buffer byte + +let add_lowercase buffer byte = + if byte = '\x00' then Buffer.add_string buffer u_rep_utf_8 + else Buffer.add_char buffer (Char.lowercase_ascii byte) + +let is_whitespace = function '\t' | '\n' | '\x0C' | ' ' -> true | _ -> false + +let scan data start = + let length = String.length data in + let attributes = ref [] in + let name = Buffer.create 16 in + let value = Buffer.create 32 in + let commit () = + if Buffer.length name > 0 then + attributes := (Buffer.contents name, Buffer.contents value) :: !attributes; + Buffer.clear name; + Buffer.clear value + in + let finish ?(self_closing = false) index = + commit (); + { attributes = List.rev !attributes; self_closing; next = index; ok = true } + in + let eof index = + { attributes = []; self_closing = false; next = index; ok = false } + in + let rec before_name index = + if index >= length then eof index + else + let byte = data.[index] in + if is_whitespace byte then before_name (index + 1) + else if byte = '/' then self_closing_start (index + 1) + else if byte = '>' then finish (index + 1) + else begin + add_lowercase name byte; + in_name (index + 1) + end + and in_name index = + if index >= length then eof index + else + let byte = data.[index] in + if is_whitespace byte then after_name (index + 1) + else if byte = '/' then begin + commit (); + self_closing_start (index + 1) + end + else if byte = '=' then before_value (index + 1) + else if byte = '>' then finish (index + 1) + else begin + add_lowercase name byte; + in_name (index + 1) + end + and after_name index = + if index >= length then eof index + else + let byte = data.[index] in + if is_whitespace byte then after_name (index + 1) + else if byte = '/' then begin + commit (); + self_closing_start (index + 1) + end + else if byte = '=' then before_value (index + 1) + else if byte = '>' then finish (index + 1) + else begin + commit (); + add_lowercase name byte; + in_name (index + 1) + end + and before_value index = + if index >= length then eof index + else + let byte = data.[index] in + if is_whitespace byte then before_value (index + 1) + else if byte = '"' || byte = '\'' then quoted byte (index + 1) + else if byte = '>' then finish (index + 1) + else begin + add value byte; + unquoted (index + 1) + end + and quoted quote index = + if index >= length then eof index + else + let byte = data.[index] in + if byte = quote then begin + commit (); + after_quoted (index + 1) + end + else begin + add value byte; + quoted quote (index + 1) + end + and unquoted index = + if index >= length then eof index + else + let byte = data.[index] in + if is_whitespace byte then begin + commit (); + before_name (index + 1) + end + else if byte = '>' then finish (index + 1) + else begin + add value byte; + unquoted (index + 1) + end + and after_quoted index = + if index >= length then eof index + else + let byte = data.[index] in + if is_whitespace byte then before_name (index + 1) + else if byte = '/' then self_closing_start (index + 1) + else if byte = '>' then finish (index + 1) + else before_name index + and self_closing_start index = + if index >= length then eof index + else if data.[index] = '>' then finish ~self_closing:true (index + 1) + else before_name index + in + before_name start diff --git a/src/lite/text.ml b/src/lite/text.ml new file mode 100644 index 0000000..cc14cd5 --- /dev/null +++ b/src/lite/text.ml @@ -0,0 +1,51 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +type t = { + mutable strings : string list; + buffer : Buffer.t; + mutable location : location option; +} + +(* This is changed for unit testing. *) +let length_limit = ref (Sys.max_string_length / 2) +let prepare () = { strings = []; buffer = Buffer.create 256; location = None } + +let note_location text location = + begin match text.location with + | None -> text.location <- Some location + | Some _ -> () + end + +let adding text location = + note_location text location; + + if Buffer.length text.buffer >= !length_limit then begin + text.strings <- Buffer.contents text.buffer :: text.strings; + Buffer.clear text.buffer + end + +let add text location c = + adding text location; + add_utf_8 text.buffer c + +(* This is only used for strings that are expected to be very small, at the + moment. *) +let add_string text location s = + adding text location; + Buffer.add_string text.buffer s + +let emit text = + match text.location with + | None -> None + | Some location -> + text.location <- None; + if Buffer.length text.buffer = 0 then None + else begin + let strings = Buffer.contents text.buffer :: text.strings |> List.rev in + text.strings <- []; + Buffer.clear text.buffer; + Some (location, strings) + end diff --git a/src/lite/token_source.ml b/src/lite/token_source.ml new file mode 100644 index 0000000..d478905 --- /dev/null +++ b/src/lite/token_source.ml @@ -0,0 +1,104 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +type location_out = Ragel_html_tokenizer.location_out = { + mutable line : int; + mutable column : int; +} + +type pushed_token = { token : Html_tokenizer.token; line : int; column : int } + +type t = { + scanner : Ragel_html_tokenizer.t; + mutable pushed : pushed_token list; + mutable foreign : unit -> bool; + native_text_runs : bool; +} + +let valid_utf_8 = String.is_valid_utf_8 + +let replace_malformed html = + let buffer = Buffer.create (String.length html + 16) in + Uutf.String.fold_utf_8 + (fun () _ -> function + | `Uchar uchar -> Uutf.Buffer.add_utf_8 buffer uchar + | `Malformed _ -> Uutf.Buffer.add_utf_8 buffer Uutf.u_rep) + () html; + Buffer.contents buffer + +let normalize_newlines html = + if not (String.contains html '\r') then html + else begin + let length = String.length html in + let buffer = Buffer.create length in + let rec copy index = + if index < length then + match html.[index] with + | '\r' -> + Buffer.add_char buffer '\n'; + if index + 1 < length && html.[index + 1] = '\n' then + copy (index + 2) + else copy (index + 1) + | c -> + Buffer.add_char buffer c; + copy (index + 1) + in + copy 0; + Buffer.contents buffer + end + +(* The baseline UTF-8 decoder consumes one leading BOM, and its input + preprocessor consumes one more leading U+FEFF. *) +let strip_leading_bom html = + let length = String.length html in + let has_bom index = + index + 3 <= length + && html.[index] = '\xEF' + && html.[index + 1] = '\xBB' + && html.[index + 2] = '\xBF' + in + let start = if not (has_bom 0) then 0 else if has_bom 3 then 6 else 3 in + if start = 0 then html else String.sub html start (length - start) + +let create_utf_8 html = + let html = if valid_utf_8 html then html else replace_malformed html in + let html = strip_leading_bom html in + let html = normalize_newlines html in + { + scanner = Ragel_html_tokenizer.create html; + pushed = []; + foreign = (fun () -> false); + native_text_runs = true; + } + +let create html = Encoding.decode_html html |> create_utf_8 + +let of_tokens tokens = + let pushed = + List.map (fun ((line, column), token) -> { token; line; column }) tokens + in + { + scanner = Ragel_html_tokenizer.create ""; + pushed; + foreign = (fun () -> false); + native_text_runs = false; + } + +let native_text_runs source = source.native_text_runs +let location () = { line = 1; column = -1 } + +let next source state ~drop_candidate (out : location_out) = + match source.pushed with + | { token; line; column } :: rest -> + source.pushed <- rest; + out.line <- line; + out.column <- column; + token + | [] -> + Ragel_html_tokenizer.next source.scanner state (source.foreign ()) + ~drop_candidate out + +let set_foreign source foreign = source.foreign <- foreign + +let push source ((line, column), token) = + source.pushed <- { token; line; column } :: source.pushed diff --git a/src/lite/token_source.mli b/src/lite/token_source.mli new file mode 100644 index 0000000..d5d5f4d --- /dev/null +++ b/src/lite/token_source.mli @@ -0,0 +1,23 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Common + +type location_out = { mutable line : int; mutable column : int } +type t + +val create : string -> t +val create_utf_8 : string -> t +val of_tokens : (location * Html_tokenizer.token) list -> t +val native_text_runs : t -> bool +val location : unit -> location_out + +val next : + t -> + Html_tokenizer.state -> + drop_candidate:bool -> + location_out -> + Html_tokenizer.token + +val set_foreign : t -> (unit -> bool) -> unit +val push : t -> location * Html_tokenizer.token -> unit diff --git a/test/dune b/test/dune index 69f30da..029607e 100644 --- a/test/dune +++ b/test/dune @@ -1,6 +1,6 @@ (executable (name test) - (libraries markup ounit2 test_support)) + (libraries markup devkit ounit2 test_support)) (rule (alias runtest) diff --git a/test/fuzz/dune b/test/fuzz/dune new file mode 100644 index 0000000..fa231a4 --- /dev/null +++ b/test/fuzz/dune @@ -0,0 +1,12 @@ +(env + (afl + (ocamlopt_flags + (:standard -afl-instrument -afl-inst-ratio 20)))) + +(executable + (name lite_diff_fuzz) + (libraries lite_test_oracle markup markup.common markup.lite unix)) + +(executable + (name lite_native_fuzz) + (libraries markup.lite unix)) diff --git a/test/fuzz/html.dict b/test/fuzz/html.dict new file mode 100644 index 0000000..a726eb1 --- /dev/null +++ b/test/fuzz/html.dict @@ -0,0 +1,25 @@ +open="<" +close="" +self_close="/>" +comment_open="" +script_open="" +style_open="" +table_open="" +table_close="
" +template_open="" +svg_open="" +math_open="" +amp="&" +decimal="&#" +hex="&#x" +quote="\"" +apostrophe="'" +equals="=" +fragment_document="FRAGMENT " +fragment_td="FRAGMENT td\x0a" +newline="\x0a" diff --git a/test/fuzz/lite_diff_fuzz.ml b/test/fuzz/lite_diff_fuzz.ml new file mode 100644 index 0000000..b9fbc96 --- /dev/null +++ b/test/fuzz/lite_diff_fuzz.ml @@ -0,0 +1,127 @@ +let maximum_input_length = 1024 * 1024 +let depth_limit = 60 + +let read_input channel = + let buffer = Buffer.create 4096 in + let bytes = Bytes.create 65536 in + let rec read total = + let count = input channel bytes 0 (Bytes.length bytes) in + if count = 0 then Some (Buffer.contents buffer) + else + let total = total + count in + if total > maximum_input_length then None + else begin + Buffer.add_subbytes buffer bytes 0 count; + read total + end + in + read 0 + +(* "FRAGMENT \n" parses in fragment context ; + anything else parses the whole input as a document. *) +let context_of_input input : [ `Document | `Fragment of string ] * string = + let prefix = "FRAGMENT " in + let prefix_length = String.length prefix in + if + String.length input >= prefix_length + && String.sub input 0 prefix_length = prefix + then + match String.index_from_opt input prefix_length '\n' with + | Some newline -> + ( `Fragment (String.sub input prefix_length (newline - prefix_length)), + String.sub input (newline + 1) (String.length input - newline - 1) ) + | None -> + ( `Fragment + (String.sub input prefix_length + (String.length input - prefix_length)), + "" ) + else (`Document, input) + +let collect iter stream = + let values = ref [] in + iter (fun value -> values := value :: !values) stream; + List.rev !values + +type errors = (Markup_common.location * Markup_common.Error.t) list + +type outcome = + | Signals of Markup_common.signal list * errors + | Raised of string * errors + +let run parse collect_signals = + let errors = ref [] in + let report location error = errors := (location, error) :: !errors in + try + let signals = collect_signals (parse report) in + Signals (signals, List.rev !errors) + with exn -> Raised (Printexc.to_string exn, List.rev !errors) + +let oracle context tokens = + run + (fun report -> Oracle.parse_adapted ~depth_limit ~context report tokens) + (collect Markup.iter) + +let lite context tokens = + run + (fun report -> + Oracle.parse_lite_adapted ~depth_limit ~context report tokens) + (collect Markup_lite.iter) + +let truncate string = + let maximum = 240 in + if String.length string <= maximum then string + else String.sub string 0 maximum ^ "..." + +let signal = function + | None -> "" + | Some signal -> + Markup_common.signal_to_string signal |> truncate |> Printf.sprintf "%S" + +let error = function + | None -> "" + | Some ((line, column), error) -> + Printf.sprintf "(%d,%d) %s" line column + (truncate (Markup_common.Error.to_string error)) + +let first = function [] -> None | value :: _ -> Some value + +let crash format = + Printf.ksprintf + (fun message -> + Printf.eprintf "markup.lite differential mismatch: %s\n%!" message; + Unix.kill (Unix.getpid ()) Sys.sigabrt; + exit 2) + format + +let compare_lists what to_string expected actual = + let rec compare index expected actual = + match (expected, actual) with + | [], [] -> () + | expected :: expected_rest, actual :: actual_rest when expected = actual -> + compare (index + 1) expected_rest actual_rest + | expected, actual -> + crash "%s %d: oracle=%s lite=%s" what index + (to_string (first expected)) + (to_string (first actual)) + in + compare 0 expected actual + +let compare_signals = compare_lists "signal" signal +let compare_errors = compare_lists "error" error + +let compare_with_oracle expected expected_errors = function + | Signals (actual, actual_errors) -> + compare_signals expected actual; + compare_errors expected_errors actual_errors + | Raised (exception_, _) -> crash "Lite raised: %S" exception_ + +let check input = + let context, body = context_of_input input in + (* HtmlStream adaptation is intentionally performed once. *) + let tokens = Oracle.adapt body in + match oracle context tokens with + | Raised _ -> () + | Signals (expected, expected_errors) -> + compare_with_oracle expected expected_errors (lite context tokens) + +let () = match read_input stdin with Some input -> check input | None -> () diff --git a/test/fuzz/lite_native_fuzz.ml b/test/fuzz/lite_native_fuzz.ml new file mode 100644 index 0000000..ef7c989 --- /dev/null +++ b/test/fuzz/lite_native_fuzz.ml @@ -0,0 +1,52 @@ +let maximum_input_length = 1024 * 1024 + +let read_input channel = + let buffer = Buffer.create 4096 in + let bytes = Bytes.create 65536 in + let rec read total = + let count = input channel bytes 0 (Bytes.length bytes) in + if count = 0 then Some (Buffer.contents buffer) + else + let total = total + count in + if total > maximum_input_length then None + else begin + Buffer.add_subbytes buffer bytes 0 count; + read total + end + in + read 0 + +(* "FRAGMENT \n" parses in fragment context ; + anything else parses the whole input as a document. *) +let context_of_input input : [ `Document | `Fragment of string ] * string = + let prefix = "FRAGMENT " in + let prefix_length = String.length prefix in + if + String.length input >= prefix_length + && String.sub input 0 prefix_length = prefix + then + match String.index_from_opt input prefix_length '\n' with + | Some newline -> + ( `Fragment (String.sub input prefix_length (newline - prefix_length)), + String.sub input (newline + 1) (String.length input - newline - 1) ) + | None -> + ( `Fragment + (String.sub input prefix_length + (String.length input - prefix_length)), + "" ) + else (`Document, input) + +let crash exception_ = + Printf.eprintf "markup.lite native parser raised: %s\n%!" + (Printexc.to_string exception_); + Unix.kill (Unix.getpid ()) Sys.sigabrt; + exit 2 + +let check input = + let context, body = context_of_input input in + try + Markup_lite.parse_html ~context ~report:(fun _ _ -> ()) body + |> Markup_lite.iter (fun _ -> ()) + with exn -> crash exn + +let () = match read_input stdin with Some input -> check input | None -> () diff --git a/test/fuzz/seeds/empty.html b/test/fuzz/seeds/empty.html new file mode 100644 index 0000000..e69de29 diff --git a/test/fuzz/seeds/foreign.html b/test/fuzz/seeds/foreign.html new file mode 100644 index 0000000..2ee3284 --- /dev/null +++ b/test/fuzz/seeds/foreign.html @@ -0,0 +1 @@ +

xy diff --git a/test/fuzz/seeds/fragment.html b/test/fuzz/seeds/fragment.html new file mode 100644 index 0000000..1823c11 --- /dev/null +++ b/test/fuzz/seeds/fragment.html @@ -0,0 +1,2 @@ +FRAGMENT td +ab \ No newline at end of file diff --git a/test/fuzz/seeds/malformed.html b/test/fuzz/seeds/malformed.html new file mode 100644 index 0000000..aa7323c --- /dev/null +++ b/test/fuzz/seeds/malformed.html @@ -0,0 +1 @@ +

x

"; + agrees "empty" "ab"; + ]; + "doctype" + >::: [ + agrees "lowercase" "

x

"; + agrees "uppercase" "text"; + ]; + "br end tag" + >::: [ agrees "between text" "x
y"; agrees "alone" "
" ]; + "self-closing" + >::: [ agrees "div" "
x"; agrees "span and b" "xy" ]; + "rcdata" + >::: [ + agrees "textarea start tag" ""; + agrees "textarea end tag" "x"; + ]; + "script escaping" + >::: [ + agrees "escaped close tag" + " -->"; + agrees "double escaped" + "y"; + ]; + "lenient entities" + >::: [ + agrees "named without semicolon" "a&b"; + agrees "numeric without semicolon" "x&y"; + agrees "attribute named without semicolon" + "

x

"; + ]; + "invalid utf8" + >::: [ + agrees "leading byte" "\xff

x

"; + agrees "byte in text" "

a\xffb

"; + agrees "continuation bytes" "\xb5\x95"; + ]; + "declared encoding" + >::: [ + agrees_utf_8 "explicit UTF-8 ignores declaration" + "

\xC3\xA9"; + agrees "windows-1251" + "

\xCF\xF0\xE8\xE2\xE5\xF2"; + agrees "baseline windows-1251 D0 mapping" + "

\xD0"; + agrees "HTML iso-8859-1 is windows-1252" + "

caf\xE9"; + agrees "bare charset text is ignored" + "

charset=windows-1252 \xC3\xA9"; + agrees "charset in a comment is ignored" + "

\xC3\xA9"; + agrees "charset in a quoted attribute is ignored" + "

\xC3\xA9"; + agrees "content pragma" + "

\xE9"; + agrees "pragma after content" + "

\xE9"; + agrees "content without pragma is ignored" + "

\xC3\xA9"; + agrees "duplicate charset attribute keeps the first" + "

\xCF"; + agrees "UTF-8 BOM takes precedence" + "\xEF\xBB\xBF

caf\xC3\xA9"; + ]; + "crlf" + >::: [ + agrees "crlf in text" "

a\r\nb

"; agrees "lone cr" "a\rb"; + ]; + "bom" + >::: [ + agrees "leading bom" "\xEF\xBB\xBFa"; + agrees_with_text "internal bom is preserved" "a\xEF\xBB\xBFb" + "a\xEF\xBB\xBFb"; + agrees "two leading boms" "\xEF\xBB\xBF\xEF\xBB\xBFa"; + agrees "UTF-16LE bom" + "\xFF\xFE\x3C\x00\x70\x00\x3E\x00\xE9\x00\x3C\x00\x2F\x00\x70\x00\x3E\x00"; + agrees "UTF-16BE bom" + "\xFE\xFF\x00\x3C\x00\x70\x00\x3E\x00\xE9\x00\x3C\x00\x2F\x00\x70\x00\x3E"; + agrees_encoding "explicit UTF-16LE" Markup.Encoding.utf_16le + `UTF_16LE + "\x3C\x00\x70\x00\x3E\x00\x78\x00\x3C\x00\x2F\x00\x70\x00\x3E\x00"; + ]; + "table whitespace" + >::: [ + agrees "before colgroup" "\n\t
"; + agrees "between rows" + "
a
"; + ]; + "attributes" + >::: [ + agrees "source order" "

x"; + agrees "duplicates" "

x"; + ]; + "garbage tags" + >::: [ + agrees "at sign in name" "z"; + agrees "with attribute and end tag" + "t"; + ]; + "tag open text" + >::: [ + agrees "empty tag" "<>x"; + agrees "comparison operators" "text < 5 and > 3"; + agrees "space before name" "< div>x

"; + ]; + "attribute names" + >::: [ + agrees "quote as name" "
x"; + agrees "equals as name" "

x"; + agrees "control character in name" "

x"; + ]; + "rawtext elements" + >::: [ + agrees "noembed" "<span>xy"; + agrees "xmp" "

a<b>cd"; + ]; + "plaintext" + >::: [ + agrees "swallows rest" "

a<b>"; + agrees "end tag is text" "<plaintext>x</plaintext>y"; + ]; + "fragment foreign" + >::: [ + agrees ~context:(`Fragment "svg") "td in svg" "<td>x"; + agrees ~context:(`Fragment "svg") "div span in svg" + "<div><span></div>"; + ]; + "parse_tokens" + >::: [ + ( "document" >:: fun _ -> + let tokens = + [ + ((1, 1), `Start (token_tag "p")); + ((1, 4), `String "x"); + ((1, 5), `End (token_tag "p")); + ((1, 9), `EOF); + ] + in + assert_equal ~printer:print_signals + (lite `Document "<p>x</p>") + (lite_tokens `Document tokens) ); + ( "fragment" >:: fun _ -> + let tokens = [ ((1, 1), `String "<b>"); ((1, 4), `EOF) ] in + assert_equal ~printer:print_signals + (lite (`Fragment "textarea") "&lt;b>") + (lite_tokens (`Fragment "textarea") tokens) ); + ( "report location" >:: fun _ -> + let reports = ref [] in + let tokens = + [ ((7, 11), `End (token_tag "p")); ((7, 15), `EOF) ] + in + ignore + (lite_tokens + ~report:(fun location error -> + reports := (location, error) :: !reports) + `Document tokens); + assert_bool "token location was not reported" + (List.exists + (fun (location, _) -> location = (7, 11)) + !reports) ); + ]; + "foreign breakout reentry" + >::: [ + agrees "svg b svg text" "<svg><b><svg>ab"; + agrees "math b math text" "<math><b><math>xy"; + agrees "svg s svg digits" "<svg><s><svg>00"; + ]; + "cdata in foreign content" + >::: [ + agrees "svg" "<svg><![CDATA[a]]></svg>"; + agrees "math" "<math><![CDATA[1]]></math>"; + ]; + "form feed whitespace" + >::: [ + agrees "alone" "\x0C"; + agrees "after col" "<table><col>\x0C"; + agrees "after template" "<template></template>\x0C"; + ]; + "nul does not reconstruct formatting" + >::: [ + agrees "p b p" "<p><b><p>\x00"; + agrees "li s li" "<li><s><li>\x00<p"; + ]; + "fragment breakout rawtext eof" + >::: [ + agrees ~context:(`Fragment "svg") "style slash" + "<p></p><style></"; + agrees ~context:(`Fragment "svg") "script candidate" + "<p></p><script></x"; + agrees ~context:(`Fragment "math") "style candidate" + "<p></p><style></x"; + agrees "document candidate" "<style></x"; + agrees ~context:(`Fragment "svg") "complete candidate" + "<p></p><style></x>"; + ]; + "termination" >::: [ terminates; bounded_agreement ]; + ]) diff --git a/test/lite/lite_count_corpus.ml b/test/lite/lite_count_corpus.ml new file mode 100644 index 0000000..c65ca40 --- /dev/null +++ b/test/lite/lite_count_corpus.ml @@ -0,0 +1,230 @@ +type parser_selection = Both | Oracle | Lite + +let arguments () = + let selection = ref Both in + let directories = ref [] in + let parser = function + | "both" -> selection := Both + | "oracle" -> selection := Oracle + | "lite" -> selection := Lite + | _ -> assert false + in + let usage = + Printf.sprintf "Usage: %s [--parser both|oracle|lite] DIRECTORY" + Sys.argv.(0) + in + let specs = + [ + ( "--parser", + Arg.Symbol ([ "both"; "oracle"; "lite" ], parser), + " Select which parser to run (default: both)" ); + ] + in + Arg.parse specs + (fun directory -> directories := directory :: !directories) + usage; + match !directories with + | [ directory ] -> (!selection, directory) + | _ -> + Arg.usage specs usage; + exit 2 + +let html_files directory = + let files = CCIO.File.read_dir ~recurse:true (CCIO.File.make directory) in + let rec collect acc = + match files () with + | None -> List.sort String.compare acc + | Some path -> + let path = CCIO.File.to_string path in + collect + (if Filename.check_suffix path ".html" then path :: acc else acc) + in + collect [] + +type stats = { + mutable wall_seconds : float; + mutable user_seconds : float; + mutable system_seconds : float; + mutable minor_words : float; + mutable major_words : float; + mutable promoted_words : float; + mutable allocated_words : float; + mutable minor_collections : int; + mutable major_collections : int; + mutable compactions : int; +} + +let empty_stats () = + { + wall_seconds = 0.; + user_seconds = 0.; + system_seconds = 0.; + minor_words = 0.; + major_words = 0.; + promoted_words = 0.; + allocated_words = 0.; + minor_collections = 0; + major_collections = 0; + compactions = 0; + } + +let measure stats f = + let gc_before = Gc.quick_stat () in + let allocated_before = Gc.allocated_bytes () in + let cpu_before = Unix.times () in + let wall_before = Unix.gettimeofday () in + let result = f () in + let wall_after = Unix.gettimeofday () in + let cpu_after = Unix.times () in + let gc_after = Gc.quick_stat () in + let allocated_after = Gc.allocated_bytes () in + stats.wall_seconds <- stats.wall_seconds +. wall_after -. wall_before; + stats.user_seconds <- + stats.user_seconds +. cpu_after.tms_utime -. cpu_before.tms_utime; + stats.system_seconds <- + stats.system_seconds +. cpu_after.tms_stime -. cpu_before.tms_stime; + stats.minor_words <- + stats.minor_words +. gc_after.minor_words -. gc_before.minor_words; + stats.major_words <- + stats.major_words +. gc_after.major_words -. gc_before.major_words; + stats.promoted_words <- + stats.promoted_words +. gc_after.promoted_words -. gc_before.promoted_words; + stats.allocated_words <- + stats.allocated_words + +. ((allocated_after -. allocated_before) /. (float Sys.word_size /. 8.)); + stats.minor_collections <- + stats.minor_collections + gc_after.minor_collections + - gc_before.minor_collections; + stats.major_collections <- + stats.major_collections + gc_after.major_collections + - gc_before.major_collections; + stats.compactions <- + stats.compactions + gc_after.compactions - gc_before.compactions; + result + +type outcome = Count of int | Raised of string + +let count iter stream = + let count = ref 0 in + iter (fun _ -> incr count) stream; + !count + +let run f = try Count (f ()) with exn -> Raised (Printexc.to_string exn) + +let count_oracle html = + run (fun () -> + Oracle.parse ~context:`Document (fun _ _ -> ()) html |> count Markup.iter) + +let count_lite html = + run (fun () -> + Markup_lite.parse_html ~context:`Document html |> count Markup_lite.iter) + +let compare path oracle lite = + match (oracle, lite) with + | Count oracle, Count lite when oracle = lite -> Some oracle + | Raised oracle, Raised lite when oracle = lite -> Some 0 + | Count oracle, Count lite -> + Printf.eprintf "%s: signal count differs: oracle=%d lite=%d\n" path oracle + lite; + None + | Raised oracle, Raised lite -> + Printf.eprintf "%s: exception differs:\n oracle: %s\n lite: %s\n" path + oracle lite; + None + | Raised exception_, Count count -> + Printf.eprintf "%s: oracle raised but Lite produced %d signals: %s\n" path + count exception_; + None + | Count count, Raised exception_ -> + Printf.eprintf "%s: Lite raised but oracle produced %d signals: %s\n" path + count exception_; + None + +let print_stats name bytes stats = + let mib = float bytes /. 1048576. in + Printf.printf + "%s: wall_seconds=%.6f user_seconds=%.6f system_seconds=%.6f \ + throughput_mib_s=%.2f allocated_words=%.0f minor_words=%.0f \ + major_words=%.0f promoted_words=%.0f minor_collections=%d \ + major_collections=%d compactions=%d\n" + name stats.wall_seconds stats.user_seconds stats.system_seconds + (mib /. stats.wall_seconds) + stats.allocated_words stats.minor_words stats.major_words + stats.promoted_words stats.minor_collections stats.major_collections + stats.compactions + +let run_one name count_parser files = + let stats = empty_stats () in + let bytes = ref 0 in + let signals = ref 0 in + let exceptions = ref 0 in + List.iteri + (fun index path -> + let html = CCIO.File.read_exn (CCIO.File.make path) in + bytes := !bytes + String.length html; + begin match measure stats (fun () -> count_parser html) with + | Count count -> signals := !signals + count + | Raised _ -> incr exceptions + end; + if (index + 1) mod 100 = 0 then + Printf.eprintf "checked %d/%d\r%!" (index + 1) (List.length files)) + files; + Printf.eprintf "checked %d/%d\n%!" (List.length files) (List.length files); + Printf.printf "files=%d bytes=%d signals=%d exceptions=%d\n" + (List.length files) !bytes !signals !exceptions; + print_stats (name ^ " count") !bytes stats; + Printf.printf "OK: %d HTML files consumed with %s\n%!" (List.length files) + name + +let run_both files = + let failures = ref 0 in + let bytes = ref 0 in + let signals = ref 0 in + let oracle_stats = empty_stats () in + let lite_stats = empty_stats () in + List.iteri + (fun index path -> + let html = CCIO.File.read_exn (CCIO.File.make path) in + bytes := !bytes + String.length html; + let oracle, lite = + if index mod 2 = 0 then begin + let oracle = measure oracle_stats (fun () -> count_oracle html) in + let lite = measure lite_stats (fun () -> count_lite html) in + (oracle, lite) + end + else begin + let lite = measure lite_stats (fun () -> count_lite html) in + let oracle = measure oracle_stats (fun () -> count_oracle html) in + (oracle, lite) + end + in + begin match compare path oracle lite with + | Some count -> signals := !signals + count + | None -> incr failures + end; + if (index + 1) mod 100 = 0 then + Printf.eprintf "checked %d/%d\r%!" (index + 1) (List.length files)) + files; + Printf.eprintf "checked %d/%d\n%!" (List.length files) (List.length files); + Printf.printf "files=%d bytes=%d signals=%d\n" (List.length files) !bytes + !signals; + print_stats "oracle count" !bytes oracle_stats; + print_stats "lite count" !bytes lite_stats; + if !failures <> 0 then begin + Printf.eprintf "FAILED: %d files differed\n" !failures; + exit 1 + end; + Printf.printf "OK: %d HTML files had equal signal counts\n%!" + (List.length files) + +let () = + let selection, directory = arguments () in + let files = html_files directory in + if files = [] then begin + Printf.eprintf "No .html files found under: %s\n" directory; + exit 2 + end; + match selection with + | Both -> run_both files + | Oracle -> run_one "oracle" count_oracle files + | Lite -> run_one "lite" count_lite files diff --git a/test/lite/lite_diff_corpus.ml b/test/lite/lite_diff_corpus.ml new file mode 100644 index 0000000..efaf3f3 --- /dev/null +++ b/test/lite/lite_diff_corpus.ml @@ -0,0 +1,181 @@ +let usage program = + Printf.eprintf "Usage: %s DIRECTORY\n" program; + exit 2 + +let html_files directory = + let files = CCIO.File.read_dir ~recurse:true (CCIO.File.make directory) in + let rec collect acc = + match files () with + | None -> List.sort String.compare acc + | Some path -> + let path = CCIO.File.to_string path in + collect + (if Filename.check_suffix path ".html" then path :: acc else acc) + in + collect [] + +let collect iter stream = + let values = ref [] in + iter (fun value -> values := value :: !values) stream; + List.rev !values + +let equal_lists equal left right = + let rec loop left right = + match (left, right) with + | [], [] -> true + | l :: ls, r :: rs when equal l r -> loop ls rs + | _ -> false + in + loop left right + +let first_difference to_string left right = + let rec loop index left right = + match (left, right) with + | [], [] -> "no difference" + | [], _ :: _ -> Printf.sprintf "left ended at signal %d" index + | _ :: _, [] -> Printf.sprintf "right ended at signal %d" index + | l :: ls, r :: rs -> + if l = r then loop (index + 1) ls rs + else + Printf.sprintf "signal %d:\n oracle: %s\n lite: %s" index + (to_string l) (to_string r) + in + loop 0 left right + +type result = + | Parsed of + Markup_common.signal list + * (Markup_common.location * Markup_common.Error.t) list + | Raised of string * (Markup_common.location * Markup_common.Error.t) list + +type stats = { mutable wall_seconds : float; mutable minor_words : float } + +let empty_stats () = { wall_seconds = 0.; minor_words = 0. } + +let measure stats f = + let minor_words_before = (Gc.quick_stat ()).minor_words in + let wall_before = Unix.gettimeofday () in + let result = f () in + stats.wall_seconds <- + stats.wall_seconds +. Unix.gettimeofday () -. wall_before; + stats.minor_words <- + stats.minor_words +. (Gc.quick_stat ()).minor_words -. minor_words_before; + result + +let run parse collect_signals html = + let errors = ref [] in + let report location error = errors := (location, error) :: !errors in + try Parsed (collect_signals (parse report html), List.rev !errors) + with exn -> Raised (Printexc.to_string exn, List.rev !errors) + +let compare path oracle lite = + match (oracle, lite) with + | Parsed (oracle_signals, oracle_errors), Parsed (lite_signals, lite_errors) + -> + if not (equal_lists ( = ) oracle_signals lite_signals) then begin + Printf.eprintf "%s: signal mismatch: %s\n" path + (first_difference Markup_common.signal_to_string oracle_signals + lite_signals); + false + end + else if not (equal_lists ( = ) oracle_errors lite_errors) then begin + Printf.eprintf "%s: error/location mismatch\n" path; + false + end + else true + | Raised (oracle_exn, oracle_errors), Raised (lite_exn, lite_errors) -> + if oracle_exn = lite_exn && equal_lists ( = ) oracle_errors lite_errors + then true + else begin + Printf.eprintf "%s: exception mismatch:\n oracle: %s\n lite: %s\n" + path oracle_exn lite_exn; + false + end + | Raised (exn, _), Parsed _ -> + Printf.eprintf "%s: oracle raised but Lite did not: %s\n" path exn; + false + | Parsed _, Raised (exn, _) -> + Printf.eprintf "%s: Lite raised but oracle did not: %s\n" path exn; + false + +let check_case name html = + let oracle = run Oracle.parse (collect Markup.iter) html in + let lite = + run + (fun report html -> Markup_lite.parse_html ~report html) + (collect Markup_lite.iter) html + in + if not (compare name oracle lite) then exit 1 + +let check_decoder_cases () = + [ + ("decoder empty", ""); + ("decoder ASCII without ampersand", "<p>plain ASCII text</p>"); + ("decoder UTF-8 without ampersand", "<p title=\"été 東京\">naïve ✓</p>"); + ( "decoder attributes without ampersand", + "<div data-value=\"${value}\">x</div>" ); + ("decoder complete references", "<p title=\"a&amp;b\">&lt;&#62;&#x3e;</p>"); + ("decoder incomplete references", "<p>a& b&amp b&#12 b&#x2a</p>"); + ] + |> List.iter (fun (name, html) -> check_case name html) + +let check_depth_limit () = + let html = "<div><span></div>" in + let oracle = run (Oracle.parse ~depth_limit:1) (collect Markup.iter) html in + let lite = + run + (fun report html -> Markup_lite.parse_html ~report ~depth_limit:1 html) + (collect Markup_lite.iter) html + in + match (oracle, lite) with + | Raised _, Raised _ -> + if not (compare "depth-limit check" oracle lite) then exit 1 + | _ -> + Printf.eprintf "depth-limit check: expected both parsers to raise\n"; + exit 1 + +let () = + check_decoder_cases (); + check_depth_limit (); + let directory = + match Array.to_list Sys.argv with + | [ _; directory ] -> directory + | _ -> usage Sys.argv.(0) + in + let files = html_files directory in + if files = [] then begin + Printf.eprintf "No .html files found under: %s\n" directory; + exit 2 + end; + let failures = ref 0 in + let oracle_stats = empty_stats () in + let lite_stats = empty_stats () in + List.iteri + (fun index path -> + let html = CCIO.File.read_exn (CCIO.File.make path) in + let oracle = + measure oracle_stats (fun () -> + run Oracle.parse (collect Markup.iter) html) + in + let lite = + measure lite_stats (fun () -> + run + (fun report html -> Markup_lite.parse_html ~report html) + (collect Markup_lite.iter) html) + in + if not (compare path oracle lite) then incr failures; + if (index + 1) mod 100 = 0 then + Printf.eprintf "checked %d/%d\r%!" (index + 1) (List.length files)) + files; + Printf.eprintf "checked %d/%d\n%!" (List.length files) (List.length files); + Printf.printf + "oracle: wall_seconds=%.6f minor_words=%.0f\n\ + lite: wall_seconds=%.6f minor_words=%.0f\n\ + %!" + oracle_stats.wall_seconds oracle_stats.minor_words lite_stats.wall_seconds + lite_stats.minor_words; + if !failures <> 0 then begin + Printf.eprintf "FAILED: %d files differed\n" !failures; + exit 1 + end; + Printf.printf "OK: %d HTML files matched exactly\n%!" (List.length files) diff --git a/test/lite/lite_dump_signals.ml b/test/lite/lite_dump_signals.ml new file mode 100644 index 0000000..cb8bf86 --- /dev/null +++ b/test/lite/lite_dump_signals.ml @@ -0,0 +1,59 @@ +let collect iter stream = + let values = ref [] in + iter (fun value -> values := value :: !values) stream; + List.rev !values + +type outcome = Signals of Markup_common.signal list | Raised of string + +let run parse collect_signals input = + try Signals (collect_signals (parse input)) + with exn -> Raised (Printexc.to_string exn) + +let context_of_input input : [ `Document | `Fragment of string ] * string = + let prefix = "FRAGMENT " in + let plen = String.length prefix in + if String.length input >= plen && String.sub input 0 plen = prefix then + match String.index_from_opt input plen '\n' with + | Some nl -> + ( `Fragment (String.sub input plen (nl - plen)), + String.sub input (nl + 1) (String.length input - nl - 1) ) + | None -> (`Document, input) + else (`Document, input) + +let oracle context input = + run + (fun input -> Oracle.parse ~context (fun _ _ -> ()) input) + (collect Markup.iter) input + +let lite context input = + run + (fun input -> Markup_lite.parse_html ~context input) + (collect Markup_lite.iter) input + +let print label = function + | Raised exn -> Printf.printf "%s: RAISED %S\n" label exn + | Signals signals -> + Printf.printf "%s: %d signals\n" label (List.length signals); + List.iteri + (fun i signal -> + Printf.printf " %d: %S\n" i (Markup_common.signal_to_string signal)) + signals + +let read_all channel = + let buffer = Buffer.create 4096 in + (try + while true do + Buffer.add_channel buffer channel 1 + done + with End_of_file -> ()); + Buffer.contents buffer + +let () = + let input = read_all stdin in + let context, input = context_of_input input in + Printf.printf "input: %S\n%!" input; + Printf.eprintf "oracle...\n%!"; + print "oracle" (oracle context input); + Printf.eprintf "lite...\n%!"; + print "lite" (lite context input); + Printf.eprintf "done\n%!" diff --git a/test/lite/lite_fuzz_regression.ml b/test/lite/lite_fuzz_regression.ml new file mode 100644 index 0000000..f19667b --- /dev/null +++ b/test/lite/lite_fuzz_regression.ml @@ -0,0 +1,201 @@ +(* Regression cases minimized from the 2026-08-27 Lite differential fuzzing + corpus. The devkit-backed parser is the oracle for all of these cases. *) + +open OUnit2 + +let collect iter stream = + let signals = ref [] in + iter (fun signal -> signals := signal :: !signals) stream; + List.rev !signals + +let oracle html = collect Markup.iter (Oracle.parse (fun _ _ -> ()) html) + +let lite html = + collect Markup_lite.iter (Markup_lite.parse_html ~report:(fun _ _ -> ()) html) + +let print_signals signals = + signals + |> List.map Markup_common.signal_to_string + |> String.concat "\n " |> Printf.sprintf "\n %s" + +let agrees name html = + name >:: fun _ -> + assert_equal ~printer:print_signals (oracle html) (lite html) + +let adapted_oracle html = + let tokens = Oracle.adapt html in + collect Markup.iter + (Oracle.parse_adapted ~context:`Document (fun _ _ -> ()) tokens) + +let adapted_lite html = + let tokens = Oracle.adapt html in + collect Markup_lite.iter + (Oracle.parse_lite_adapted ~context:`Document (fun _ _ -> ()) tokens) + +let adapted_agrees name html = + name >:: fun _ -> + assert_equal ~printer:print_signals (adapted_oracle html) (adapted_lite html) + +(* [adapted_agrees] compares signals only. These two also need the error + stream: the 2026-09-03 divergences below were error-only or error-visible. *) +let adapted_outcome parse html = + let tokens = Oracle.adapt html in + let errors = ref [] in + let report location error = errors := (location, error) :: !errors in + let signals = parse report tokens in + ( List.map Markup_common.signal_to_string signals, + List.rev_map + (fun ((line, column), error) -> + Printf.sprintf "(%d,%d) %s" line column + (Markup_common.Error.to_string error)) + !errors ) + +let adapted_oracle_outcome = + adapted_outcome (fun report tokens -> + collect Markup.iter + (Oracle.parse_adapted ~context:`Document report tokens)) + +let adapted_lite_outcome = + adapted_outcome (fun report tokens -> + collect Markup_lite.iter + (Oracle.parse_lite_adapted ~context:`Document report tokens)) + +let print_outcome (signals, errors) = + Printf.sprintf "\n %s" (String.concat "\n " (signals @ errors)) + +let adapted_agrees_with_errors name html = + name >:: fun _ -> + assert_equal ~printer:print_outcome + (adapted_oracle_outcome html) + (adapted_lite_outcome html) + +let lite_parses name html = name >:: fun _ -> ignore (lite html) + +let rawtext_failures = + [ + ("garbage script at EOF", "<script/;>"); + ("garbage script with content at EOF", "<script//>x"); + ("garbage script with close tag", "<script/x>alert(1)</script>done"); + ("garbage title", "<title/x>a rest</title>done"); + ("garbage style", "<style/x>p{}</style>done"); + ("garbage in attribute position (/=)", "<script /=>x</script>y"); + ("garbage in attribute position (=)", "<script =>x</script>y"); + ("garbage after retained attribute", "<script a/b>x</script>y"); + ("less-than inside rawtext start tag", "ex<script\001more<b>"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let rawtext_guards = + [ + ("non-rawtext slash garbage", "<b/x>text"); + ("non-rawtext less-than garbage", "<div foo<bar>text"); + ("rawtext garbage after valued attribute", "<script foo=1//>x"); + ("valid self-closing script", "<script />x"); + ("normal rawtext EOF", "<script>x"); + ("control character is tag whitespace", "<scripT\001more>x"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let entity_failures = + [ + ("text surrogate numeric reference", "x&#xdeee;y"); + ("text first surrogate numeric reference", "x&#xd800;"); + ("text U+FFFF numeric reference", "&#xFFFF;"); + ("text literal U+FFFE", "\239\191\190&amp;"); + ("text literal U+FFFF with unknown entity", "\239\191\191&arp;"); + ("text literal U+FFFF leaves whole chunk raw", "\239\191\191&a&amp;"); + ("attribute surrogate numeric reference", "<b c=\"&#xd800;\">x</b>"); + ("attribute literal U+FFFF", "<b c=\"\239\191\191&amp;\">x</b>"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let entity_guards = + [ + ("unknown entity", "&arp;"); + ("zero numeric reference", "x&#0;y"); + ("out-of-range numeric reference", "x&#x110000;y"); + ("ordinary named entity", "a&amp;b"); + ("other BMP noncharacter", "x&#xFDD0;y"); + ("other supplementary noncharacter", "x&#x1FFFE;y"); + ("literal replacement character", "\239\191\189&amp;"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let tree_builder_failures = + [ + adapted_agrees "foreign buffered text run" "<math><b><svg>eP"; + agrees "Lite require_current_element" + "<template><td><svg></td><tbody/><title></title><></U>"; + lite_parses "empty stack after formatting element" + "<table><script></script><><s></script><td><svg></td><tr/><s/>"; + agrees "Lite above_in_stack" + "<table><td><b><table><td><svg></td><script></script><><tr/><tr><td><svg></td></tr>M<P></b>"; + ] + +let candidate_recovery_failures = + [ + ("dropped xmp empty candidate", "<math><mo><Xmp></"); + ("dropped xmp named candidate", "<math><mo><xmp></x"); + ("dropped xmp mid-stream candidate", "<math><mo><xmp></b>c"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let candidate_recovery_guards = + [ + ("emitted rawtext element in foreign", "<math><mo><style></x"); + ("emitted rawtext element in html", "<div><style></x"); + ("emitted rcdata element in foreign", "<math><mo><title></x"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let doctype_lookahead_failures = + [ + ("keyword mismatch tail lowercased", "<!doctype a b>XYZw"); + ("fuzzer case", "<!doctype hte html>L~tmlml><stml><he"); + ("multibyte window", "<!doctype a X\xc3\xa9>ABCD"); + ("entity started in window", "<!doctype a b>&LT;a"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let doctype_lookahead_guards = + [ + ("public keyword", "<!doctype a public 'x'>YZ"); + ("gt outside window", "<!doctype a bcdefgh>XY"); + ("eof inside window", "<!doctype a b>X"); + ("lowercase tail", "<!doctype a b>xyz<B>T"); + ] + |> List.map (fun (name, html) -> agrees name html) + +let pre_newline_pushback = + [ + (* [<a;>] produces no token of its own; it only splits the character run + into [String "a\n"; String "\n"]. Lite used to consume the second token + inside the [pre] handler and drop it, keeping one newline the oracle + dropped. Needs pre/listing + an open formatting element + foreign + content: the formatting element keeps the subtree buffer on, so + [current_mode] stays pinned to the [pre] continuation and every character + of foreign text re-enters it. *) + adapted_agrees_with_errors + "split foreign text run under pre and a formatting element" + "<pre><a><svg>a\n<a;>\n"; + (* Error-stream only. The empty remainder has to be re-dispatched so that + "in table" reports a second [bad content in 'table']. *) + adapted_agrees_with_errors + "pre as a direct table child with a leading newline" "<table><pre>\n"; + ] + +let () = + run_test_tt_main + ("Lite fuzz regressions" + >::: [ + "rawtext garbage recovery" >::: rawtext_failures; + "rawtext guards" >::: rawtext_guards; + "entity chunk fallback" >::: entity_failures; + "entity guards" >::: entity_guards; + "tree-builder invariants" >::: tree_builder_failures; + "doctype keyword lookahead" >::: doctype_lookahead_failures; + "doctype lookahead guards" >::: doctype_lookahead_guards; + "end-tag candidate recovery" >::: candidate_recovery_failures; + "end-tag candidate guards" >::: candidate_recovery_guards; + "pre newline push-back" >::: pre_newline_pushback; + ]) diff --git a/test/lite/lite_main_entity_regression.ml b/test/lite/lite_main_entity_regression.ml new file mode 100644 index 0000000..fe98653 --- /dev/null +++ b/test/lite/lite_main_entity_regression.ml @@ -0,0 +1,31 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +let html = "<p>&sup2;</p>" + +let main_signals () = + html |> Markup.string + |> Markup.parse_html ~context:`Document + |> Markup.signals |> Markup.to_list + +let lite_signals () = + let signals = ref [] in + Markup_lite.parse_html html + |> Markup_lite.iter (fun signal -> signals := signal :: !signals); + List.rev !signals + +let print_signals label signals = + Printf.eprintf "%s:\n" label; + List.iter + (fun signal -> Printf.eprintf " %s\n" (Markup.signal_to_string signal)) + signals + +let () = + let main = main_signals () in + let lite = lite_signals () in + if main <> lite then begin + Printf.eprintf "Main and Lite differ for %S\n" html; + print_signals "Main" main; + print_signals "Lite" lite; + exit 1 + end diff --git a/test/lite/lite_mismatch_regression.ml b/test/lite/lite_mismatch_regression.ml new file mode 100644 index 0000000..5565467 --- /dev/null +++ b/test/lite/lite_mismatch_regression.ml @@ -0,0 +1,50 @@ +(* Representative non-NUL mismatch families from some production mismatches. + The production path first adapts HtmlStream tokens, so these tests use the + same adapter rather than parsing independently tokenized HTML. *) + +open OUnit2 + +let collect iter stream = + let signals = ref [] in + iter (fun signal -> signals := signal :: !signals) stream; + List.rev !signals + +let baseline html = + let tokens = Oracle.adapt html in + collect Markup.iter + (Oracle.parse_adapted ~context:`Document (fun _ _ -> ()) tokens) + +let lite html = + let tokens = Oracle.adapt html in + collect Markup_lite.iter + (Oracle.parse_lite_adapted ~context:`Document (fun _ _ -> ()) tokens) + +let print_signals signals = + signals + |> List.map Markup_common.signal_to_string + |> String.concat "\n " |> Printf.sprintf "\n %s" + +let agrees name html = + name >:: fun _ -> + assert_equal ~printer:print_signals (baseline html) (lite html) + +let () = + run_test_tt_main + ("production mismatch regressions" + >::: [ + (* Representative of the first-diff-965 gachon.ac.kr pages. *) + agrees "paragraph reconstruction across an active anchor" + "<p><a href='https://example.test/'>one<p>two"; + (* Representative of the first-diff-884/885 gachon.ac.kr pages. *) + agrees "nested bold reconstruction across a block" + "<div><b><b><div>text"; + (* Representative of the first-diff-992/998 toyless pages. *) + agrees "font reconstruction inside a nested div" + "<div class='options'><font color='#138f9a'><div id='option'>text"; + (* Minimized from http://koganeijinja.com/: the trailing newline is + required. HtmlStream presents it as a String token. *) + agrees "formatting reconstruction after stripped pre newline" + "<p><font><pre>\n"; + agrees "formatting reconstruction after stripped listing newline" + "<p><font><listing>\n"; + ]) diff --git a/test/lite/lite_parser_count_corpus.ml b/test/lite/lite_parser_count_corpus.ml new file mode 100644 index 0000000..9421b83 --- /dev/null +++ b/test/lite/lite_parser_count_corpus.ml @@ -0,0 +1,115 @@ +let usage program = + Printf.eprintf "Usage: %s DIRECTORY\n" program; + exit 2 + +let html_files directory = + let files = CCIO.File.read_dir ~recurse:true (CCIO.File.make directory) in + let rec collect acc = + match files () with + | None -> List.sort String.compare acc + | Some path -> + let path = CCIO.File.to_string path in + collect + (if Filename.check_suffix path ".html" then path :: acc else acc) + in + collect [] + +let count iter stream = + let count = ref 0 in + iter (fun _ -> incr count) stream; + !count + +type stats = { + mutable wall : float; + mutable user : float; + mutable system : float; + mutable words : float; + mutable minor_collections : int; + mutable major_collections : int; +} + +let stats () = + { + wall = 0.; + user = 0.; + system = 0.; + words = 0.; + minor_collections = 0; + major_collections = 0; + } + +let measure stats f = + let gc_before = Gc.quick_stat () in + let allocated_before = Gc.allocated_bytes () in + let cpu_before = Unix.times () in + let wall_before = Unix.gettimeofday () in + let result = f () in + let wall_after = Unix.gettimeofday () in + let cpu_after = Unix.times () in + let gc_after = Gc.quick_stat () in + let allocated_after = Gc.allocated_bytes () in + stats.wall <- stats.wall +. wall_after -. wall_before; + stats.user <- stats.user +. cpu_after.tms_utime -. cpu_before.tms_utime; + stats.system <- stats.system +. cpu_after.tms_stime -. cpu_before.tms_stime; + stats.words <- + stats.words + +. ((allocated_after -. allocated_before) /. (float Sys.word_size /. 8.)); + stats.minor_collections <- + stats.minor_collections + gc_after.minor_collections + - gc_before.minor_collections; + stats.major_collections <- + stats.major_collections + gc_after.major_collections + - gc_before.major_collections; + result + +let print name bytes stats = + Printf.printf + "%s: wall_seconds=%.6f user_seconds=%.6f system_seconds=%.6f \ + throughput_mib_s=%.2f allocated_words=%.0f minor_collections=%d \ + major_collections=%d\n" + name stats.wall stats.user stats.system + (float bytes /. 1048576. /. stats.wall) + stats.words stats.minor_collections stats.major_collections + +let () = + let directory = + match Array.to_list Sys.argv with + | [ _; directory ] -> directory + | _ -> usage Sys.argv.(0) + in + let files = html_files directory in + if files = [] then usage Sys.argv.(0); + let baseline_stats = stats () in + let lite_stats = stats () in + let bytes = ref 0 in + let failures = ref 0 in + List.iteri + (fun index path -> + let html = CCIO.File.read_exn (CCIO.File.make path) in + bytes := !bytes + String.length html; + (* HtmlStream production and adaptation are outside both timed regions. *) + let tokens = Oracle.adapt html in + let baseline () = + Oracle.parse_adapted ~context:`Document (fun _ _ -> ()) tokens + |> count Markup.iter + in + let lite () = + Oracle.parse_lite_adapted ~context:`Document (fun _ _ -> ()) tokens + |> count Markup_lite.iter + in + let baseline_count, lite_count = + if index mod 2 = 0 then + (measure baseline_stats baseline, measure lite_stats lite) + else + let lite_count = measure lite_stats lite in + (measure baseline_stats baseline, lite_count) + in + if baseline_count <> lite_count then begin + incr failures; + Printf.eprintf "%s: signal count differs: %d vs %d\n" path + baseline_count lite_count + end) + files; + print "baseline parser-only" !bytes baseline_stats; + print "Lite parser-only" !bytes lite_stats; + if !failures <> 0 then exit 1 diff --git a/test/lite/lite_parser_diff_corpus.ml b/test/lite/lite_parser_diff_corpus.ml new file mode 100644 index 0000000..340599e --- /dev/null +++ b/test/lite/lite_parser_diff_corpus.ml @@ -0,0 +1,146 @@ +let usage program = + Printf.eprintf "Usage: %s DIRECTORY\n" program; + exit 2 + +let html_files directory = + let files = CCIO.File.read_dir ~recurse:true (CCIO.File.make directory) in + let rec collect acc = + match files () with + | None -> List.sort String.compare acc + | Some path -> + let path = CCIO.File.to_string path in + collect + (if Filename.check_suffix path ".html" then path :: acc else acc) + in + collect [] + +let collect iter stream = + let values = ref [] in + iter (fun value -> values := value :: !values) stream; + List.rev !values + +exception Timeout + +type result = + | Parsed of + Markup_common.signal list + * (Markup_common.location * Markup_common.Error.t) list + | Raised of string * (Markup_common.location * Markup_common.Error.t) list + +let with_timeout f = + let previous = + Sys.signal Sys.sigalrm (Sys.Signal_handle (fun _ -> raise Timeout)) + in + ignore (Unix.alarm 2); + Fun.protect + ~finally:(fun () -> + ignore (Unix.alarm 0); + Sys.set_signal Sys.sigalrm previous) + f + +let run parse collect_signals = + let errors = ref [] in + let report location error = errors := (location, error) :: !errors in + try + Parsed + (with_timeout (fun () -> collect_signals (parse report)), List.rev !errors) + with exn -> Raised (Printexc.to_string exn, List.rev !errors) + +let first_difference to_string left right = + let rec loop index left right = + match (left, right) with + | [], [] -> "no difference" + | [], _ :: _ -> Printf.sprintf "baseline ended at %d" index + | _ :: _, [] -> Printf.sprintf "Lite ended at %d" index + | x :: xs, y :: ys -> + if x = y then loop (index + 1) xs ys + else + Printf.sprintf "item %d: baseline=%s Lite=%s" index (to_string x) + (to_string y) + in + loop 0 left right + +let error_to_string ((line, column), error) = + Printf.sprintf "(%d,%d) %s" line column (Markup_common.Error.to_string error) + +let compare name baseline lite = + if baseline = lite then true + else begin + begin match (baseline, lite) with + | Parsed (signals, errors), Parsed (lite_signals, lite_errors) -> + if signals <> lite_signals then + Printf.eprintf "%s: signal mismatch: %s\n" name + (first_difference Markup_common.signal_to_string signals + lite_signals) + else + Printf.eprintf "%s: error mismatch: %s\n" name + (first_difference error_to_string errors lite_errors) + | Raised (exn, _), Raised (lite_exn, _) -> + Printf.eprintf "%s: exception mismatch: baseline=%S Lite=%S\n" name exn + lite_exn + | Raised (exn, _), Parsed _ -> + Printf.eprintf "%s: baseline raised %S but Lite parsed\n" name exn + | Parsed _, Raised (exn, _) -> + Printf.eprintf "%s: Lite raised %S but baseline parsed\n" name exn + end; + false + end + +let contexts = + [ + ("document", `Document); + ("div-fragment", `Fragment "div"); + ("table-fragment", `Fragment "table"); + ("svg-fragment", `Fragment "svg"); + ("math-fragment", `Fragment "math"); + ] + +let check path html = + (* Adapt exactly once. Both parsers receive values derived from this list. *) + let tokens = Oracle.adapt html in + List.for_all + (fun (context_name, context) -> + List.for_all + (fun depth_limit -> + let suffix = + match depth_limit with + | None -> context_name + | Some n -> Printf.sprintf "%s/depth-%d" context_name n + in + let baseline = + run + (fun report -> + Oracle.parse_adapted ?depth_limit ~context report tokens) + (collect Markup.iter) + in + let lite = + run + (fun report -> + Oracle.parse_lite_adapted ?depth_limit ~context report tokens) + (collect Markup_lite.iter) + in + compare (path ^ ":" ^ suffix) baseline lite) + [ None; Some 1; Some 8 ]) + contexts + +let () = + let directory = + match Array.to_list Sys.argv with + | [ _; directory ] -> directory + | _ -> usage Sys.argv.(0) + in + let files = html_files directory in + if files = [] then usage Sys.argv.(0); + let failures = ref 0 in + List.iteri + (fun index path -> + if not (check path (CCIO.File.read_exn (CCIO.File.make path))) then + incr failures; + if (index + 1) mod 100 = 0 then + Printf.eprintf "checked %d/%d\r%!" (index + 1) (List.length files)) + files; + Printf.eprintf "checked %d/%d\n%!" (List.length files) (List.length files); + if !failures <> 0 then begin + Printf.eprintf "FAILED: %d files differed\n" !failures; + exit 1 + end diff --git a/test/lite/lite_writer_diff_corpus.ml b/test/lite/lite_writer_diff_corpus.ml new file mode 100644 index 0000000..5be8d25 --- /dev/null +++ b/test/lite/lite_writer_diff_corpus.ml @@ -0,0 +1,187 @@ +let usage program = + Printf.eprintf "Usage: %s DIRECTORY\n" program; + exit 2 + +let html_files directory = + let files = CCIO.File.read_dir ~recurse:true (CCIO.File.make directory) in + let rec collect acc = + match files () with + | None -> List.sort String.compare acc + | Some path -> + let path = CCIO.File.to_string path in + collect + (if Filename.check_suffix path ".html" then path :: acc else acc) + in + collect [] + +let collect stream = + let signals = ref [] in + Markup_lite.iter (fun signal -> signals := signal :: !signals) stream; + List.rev !signals + +type stats = { + mutable wall_seconds : float; + mutable minor_words : float; + mutable major_words : float; +} + +let empty_stats () = { wall_seconds = 0.; minor_words = 0.; major_words = 0. } + +let measure stats f = + let gc_before = Gc.quick_stat () in + let wall_before = Unix.gettimeofday () in + let result = f () in + let wall_after = Unix.gettimeofday () in + let gc_after = Gc.quick_stat () in + stats.wall_seconds <- stats.wall_seconds +. wall_after -. wall_before; + stats.minor_words <- + stats.minor_words +. gc_after.minor_words -. gc_before.minor_words; + stats.major_words <- + stats.major_words +. gc_after.major_words -. gc_before.major_words; + result + +let write_with_markup signals = + signals |> Markup.of_list |> Markup.write_html |> Markup.to_string + +let write_with_lite signals = + let buffer = Buffer.create 4096 in + Markup_lite.write_html buffer (Markup.of_list signals); + Buffer.contents buffer + +let check_escape_regressions () = + let cases = + [ + ("ASCII-safe", [ `Text [ "plain text" ] ]); + ("ASCII escapes", [ `Text [ "<&>" ] ]); + ( "attribute escapes", + [ + `Start_element ((Markup.Ns.html, "p"), [ (("", "title"), "a&\"b") ]); + `End_element; + ] ); + ("UTF-8 and non-breaking space", [ `Text [ "été\xC2\xA0東京" ] ]); + ("malformed UTF-8", [ `Text [ "before\xFFafter" ] ]); + ] + in + List.iter + (fun (name, signals) -> + let markup = write_with_markup signals in + let lite = write_with_lite signals in + if markup <> lite then begin + Printf.eprintf "%s escape regression:\n Markup: %S\n Lite: %S\n" + name markup lite; + exit 1 + end) + cases + +let check_buffer_api () = + let wrap prefix buffer text = + Buffer.add_string buffer prefix; + Buffer.add_char buffer '('; + Buffer.add_string buffer text; + Buffer.add_char buffer ')' + in + let signals = + [ + `Start_element ((Markup.Ns.html, "p"), [ (("", "id"), "x") ]); + `Text [ "y" ]; + `End_element; + ] + in + let buffer = Buffer.create 64 in + Buffer.add_string buffer "prefix:"; + Markup_lite.write_html ~escape_attribute:(wrap "A") ~escape_text:(wrap "T") + buffer (Markup.of_list signals); + Markup_lite.write_html buffer (Markup.of_list [ `Text [ "<&" ] ]); + let expected = "prefix:<p id=\"A(x)\">T(y)</p>&lt;&amp;" in + if Buffer.contents buffer <> expected then begin + Printf.eprintf "buffer API check failed:\n expected: %S\n actual: %S\n" + expected (Buffer.contents buffer); + exit 1 + end + +let difference left right = + let left_length = String.length left in + let right_length = String.length right in + let limit = min left_length right_length in + let rec find index = + if index = limit then index + else if left.[index] = right.[index] then find (index + 1) + else index + in + let index = find 0 in + let context string = + let start = max 0 (index - 24) in + let length = min (String.length string - start) 64 in + String.sub string start length + in + if index = limit then + Printf.sprintf + "output lengths differ at byte %d (Markup: %d, Lite: %d)\n\ + \ Markup: %S\n\ + \ Lite: %S" + index left_length right_length (context left) (context right) + else + Printf.sprintf + "output differs at byte %d (Markup: 0x%02X, Lite: 0x%02X)\n\ + \ Markup: %S\n\ + \ Lite: %S" + index + (Char.code left.[index]) + (Char.code right.[index]) + (context left) (context right) + +let () = + check_escape_regressions (); + check_buffer_api (); + let directory = + match Array.to_list Sys.argv with + | [ _; directory ] -> directory + | _ -> usage Sys.argv.(0) + in + let files = html_files directory in + if files = [] then begin + Printf.eprintf "No .html files found under: %s\n" directory; + exit 2 + end; + let failures = ref 0 in + let markup_stats = empty_stats () in + let lite_stats = empty_stats () in + List.iteri + (fun index path -> + let html = CCIO.File.read_exn (CCIO.File.make path) in + let signals = collect (Markup_lite.parse_html html) in + let markup, lite = + if index mod 2 = 0 then begin + let markup = + measure markup_stats (fun () -> write_with_markup signals) + in + let lite = measure lite_stats (fun () -> write_with_lite signals) in + (markup, lite) + end + else begin + let lite = measure lite_stats (fun () -> write_with_lite signals) in + let markup = + measure markup_stats (fun () -> write_with_markup signals) + in + (markup, lite) + end + in + if markup <> lite then begin + incr failures; + Printf.eprintf "%s: %s\n" path (difference markup lite) + end; + if (index + 1) mod 100 = 0 then + Printf.eprintf "checked %d/%d\r%!" (index + 1) (List.length files)) + files; + Printf.eprintf "checked %d/%d\n%!" (List.length files) (List.length files); + Printf.printf + "markup writer: wall_seconds=%.6f minor_words=%.0f major_words=%.0f\n\ + lite writer: wall_seconds=%.6f minor_words=%.0f major_words=%.0f\n\ + %!" + markup_stats.wall_seconds markup_stats.minor_words markup_stats.major_words + lite_stats.wall_seconds lite_stats.minor_words lite_stats.major_words; + if !failures <> 0 then begin + Printf.eprintf "FAILED: %d files differed\n" !failures; + exit 1 + end; + Printf.printf "OK: %d HTML files serialized exactly\n%!" (List.length files) diff --git a/test/lite/oracle.ml b/test/lite/oracle.ml new file mode 100644 index 0000000..affb885 --- /dev/null +++ b/test/lite/oracle.ml @@ -0,0 +1,95 @@ +module HS = Devkit.HtmlStream + +(* Raw-text tokenizer states substitute U+FFFD for U+0000. *) +let raw_text text = + if String.contains text '\x00' then + String.concat "\xEF\xBF\xBD" (String.split_on_char '\x00' text) + else text + +let decode raw = + let inner = HS.Raw.project raw in + try Devkit.Web.htmldecode inner with _ -> inner + +(* See [htmlstream-adapter.md] for the common adaptation policy and its + test-only suitability assessment. HtmlStream does not expose comments, + self-closing syntax, raw-text token boundaries, or columns. The strict + parser oracle therefore omits comments, defaults [self_closing] to false, + expands Script/Style into three tokens, and uses [(line, -1)] for both + parsers. *) +let adapt html : (Markup.location * Markup.Internals.token) list = + let ctx = HS.init () in + let tokens = ref [] in + let emit token = tokens := ((HS.get_lnum ctx, -1), token) :: !tokens in + let attributes attrs = + List.rev_map + (fun (name, value) -> (raw_text name, raw_text (decode value))) + attrs + in + let tag name attributes : Markup.Internals.Token_tag.t = + { name; attributes; self_closing = false } + in + (* Both tree builders assume tokenizers never leave U+0000 inside a + [`String]; split NULs out as [`Char 0] to preserve that invariant. *) + let emit_text text = + String.split_on_char '\x00' text + |> List.iteri (fun i part -> + if i > 0 then emit (`Char 0); + if part <> "" then emit (`String part)) + in + let step = function + | HS.Text raw -> emit_text (decode raw) + | HS.Tag (name, attrs) -> emit (`Start (tag name (attributes attrs))) + | HS.Close "br" -> () + | HS.Close name -> emit (`End (tag name [])) + | HS.Script (attrs, text) -> + emit (`Start (tag "script" (attributes attrs))); + emit (`String (raw_text text)); + emit (`End (tag "script" [])) + | HS.Style (attrs, text) -> + emit (`Start (tag "style" (attributes attrs))); + emit (`String (raw_text text)); + emit (`End (tag "style" [])) + in + HS.parse ~ctx step html; + emit `EOF; + List.rev !tokens + +let lite_tokens tokens : (Markup_lite.location * Markup_lite.token) list = + let tag (tag : Markup.Internals.Token_tag.t) : Markup_lite.Token_tag.t = + { + name = tag.name; + attributes = tag.attributes; + self_closing = tag.self_closing; + } + in + List.map + (fun (location, token) -> + let token : Markup_lite.token = + match token with + | `Doctype d -> `Doctype d + | `Start t -> `Start (tag t) + | `End t -> `End (tag t) + | `Char c -> `Char c + | `String s -> `String s + | `Comment s -> `Comment s + | `EOF -> `EOF + in + (location, token)) + tokens + +let parse_adapted ?depth_limit + ?(context : [ `Document | `Fragment of string ] = `Document) report tokens = + tokens + |> Markup.Internals.parse_tokens ?depth_limit ~report ~context + |> Markup.signals + +let parse_lite_adapted ?depth_limit + ?(context : [ `Document | `Fragment of string ] = `Document) report tokens = + tokens |> lite_tokens + |> Markup_lite.parse_tokens ?depth_limit ~report ~context + +let parse ?depth_limit + ?(context : [ `Document | `Fragment of string ] = `Document) report html = + Markup.string html + |> Markup.parse_html ~report ~context ?depth_limit + |> Markup.signals diff --git a/test/ragel_html_tokenizer.ml b/test/ragel_html_tokenizer.ml new file mode 100644 index 0000000..d2ef737 --- /dev/null +++ b/test/ragel_html_tokenizer.ml @@ -0,0 +1,44 @@ +(* This file is part of Markup.ml, released under the MIT license. See + LICENSE.md for details, or visit https://github.com/aantron/markup.ml. *) + +open Markup__Common + +module HS = Devkit.HtmlStream + +let decode raw = + let inner = HS.Raw.project raw in + try Devkit.Web.htmldecode inner with _ -> inner + +let tokenize html : (location * Markup__Html_tokenizer.token) list = + let ctx = HS.init () in + let tokens = ref [] in + let emit token = tokens := ((HS.get_lnum ctx, -1), token) :: !tokens in + let attributes attrs = + List.rev_map (fun (name, value) -> name, decode value) attrs + in + let tag name attributes = + {Token_tag.name; attributes; self_closing = false} + in + let step = function + | HS.Text raw -> + emit (`String (decode raw)) + | HS.Tag (name, attrs) -> + emit (`Start (tag name (attributes attrs))) + | HS.Close "br" -> + (* [HtmlStream] emits both [Tag "br"] and [Close "br"] for [<br/>]. + Feeding the close token to the tree builder creates a second [br]. *) + () + | HS.Close name -> + emit (`End (tag name [])) + | HS.Script (attrs, text) -> + emit (`Start (tag "script" (attributes attrs))); + emit (`String text); + emit (`End (tag "script" [])) + | HS.Style (attrs, text) -> + emit (`Start (tag "style" (attributes attrs))); + emit (`String text); + emit (`End (tag "style" [])) + in + HS.parse ~ctx step html; + emit `EOF; + List.rev !tokens diff --git a/test/test.ml b/test/test.ml index 8cace4f..7e20574 100644 --- a/test/test.ml +++ b/test/test.ml @@ -21,8 +21,7 @@ let suite = Test_detect.tests; Test_utility.tests; Test_integration.tests; - (* Test_ragel_tokenizer.tests; *) - (* Test_ragel_parser.tests; *) + Test_ragel_tokenizer.tests; ] let () = diff --git a/test/test_ragel_parser.ml b/test/test_ragel_parser.ml deleted file mode 100644 index b6438ef..0000000 --- a/test/test_ragel_parser.ml +++ /dev/null @@ -1,1716 +0,0 @@ -open OUnit2 -open Test_support -open Markup__Common -module Error = Markup__Error -module Kstream = Markup__Kstream - -let print_token_stream stream = - let print_token (_, token) _throw k = - print_endline @@ String.escaped @@ token_to_string token; - k () - in - print_endline "tokens:"; - Kstream.iter print_token stream (wrong_k "failed") ignore - -let print_signal_stream stream = - let print_token (_, token) _throw k = - print_endline @@ String.escaped @@ signal_to_string token; - k () - in - print_endline "tokens:"; - Kstream.iter print_token stream (wrong_k "failed") ignore - -let doctype = - `Doctype - { - doctype_name = Some "html"; - public_identifier = None; - system_identifier = None; - raw_text = None; - force_quirks = false; - } - -let start_element name = `Start_element ((html_ns, name), []) - -let expect ?prefix ?(context = Some `Document) text signals = - (* ragel parser doesn't emit bad token Error, filter them out *) - let signals = - List.filter (function - | _, _, E (`Bad_token _) | _, _, E (`Bad_document "doctype should be first") - -> false - | _ -> true) signals - in - - let report, iterate, ended = - expect_no_location_signals ?prefix signal_to_string text signals - in - - let token_stream = Markup__Html_tokenizer.Ragel.tokenize text in - - let signal_stream = - Markup__Html_parser.parse context report (token_stream, ignore, ignore) - in - (* print_signal_stream signal_stream; *) - iter iterate signal_stream; - ended () - -let tests = - [ - ( "html.parser.basic" >:: fun _ -> - expect "<!DOCTYPE html><html><head></head><body></body></html>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 22, S (start_element "head")); - (1, 28, S `End_element); - (1, 35, S (start_element "body")); - (1, 55, S `End_element); - (1, 55, S `End_element); - ]; - - expect ~prefix:true " <!--foo--> <!DOCTYPE html>" - [ (1, 2, S (`Comment "foo")); (1, 13, S doctype) ]; - - expect ~prefix:true "<!DOCTYPE html> <!--foo--> <html></html>" - [ - (1, 1, S doctype); - (1, 17, S (`Comment "foo")); - (1, 28, S (start_element "html")); - ]; - - expect ~prefix:true "<html> <!--foo--> <head></head></html>" - [ - (1, 1, S (start_element "html")); - (1, 8, S (`Comment "foo")); - (1, 19, S (start_element "head")); - ] ); - ( "html.parser.implicit-top-level" >:: fun _ -> - expect "<!DOCTYPE html>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 16, S (start_element "head")); - (1, 16, S `End_element); - (1, 16, S (start_element "body")); - (1, 16, S `End_element); - (1, 16, S `End_element); - ]; - - expect "<!DOCTYPE html><html></html>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 22, S (start_element "head")); - (1, 22, S `End_element); - (1, 22, S (start_element "body")); - (1, 29, S `End_element); - (1, 29, S `End_element); - ]; - - expect "<!DOCTYPE html><head></head>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 16, S (start_element "head")); - (1, 22, S `End_element); - (1, 29, S (start_element "body")); - (1, 29, S `End_element); - (1, 29, S `End_element); - ]; - - expect "<!DOCTYPE html><body></body>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 16, S (start_element "head")); - (1, 16, S `End_element); - (1, 16, S (start_element "body")); - (1, 29, S `End_element); - (1, 29, S `End_element); - ]; - - expect "<!DOCTYPE html><p></p>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 16, S (start_element "head")); - (1, 16, S `End_element); - (1, 16, S (start_element "body")); - (1, 16, S (start_element "p")); - (1, 19, S `End_element); - (1, 23, S `End_element); - (1, 23, S `End_element); - ]; - - expect "<!DOCTYPE html><title></title>" - [ - (1, 1, S doctype); - (1, 16, S (start_element "html")); - (1, 16, S (start_element "head")); - (1, 16, S (start_element "title")); - (1, 23, S `End_element); - (1, 31, S `End_element); - (1, 31, S (start_element "body")); - (1, 31, S `End_element); - (1, 31, S `End_element); - ] ); - ( "html.parser.no-doctype" >:: fun _ -> - expect ~prefix:true "<title>foo</title>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S (start_element "title")); - (1, 8, S (`Text [ "foo" ])); - ] ); - ( "html.parser.double-doctype" >:: fun _ -> - expect ~prefix:true "<!DOCTYPE html><!DOCTYPE html><html></html>" - [ - (1, 1, S doctype); - (1, 16, E (`Bad_document "doctype should be first")); - (1, 31, S (start_element "html")); - ] ); - ( "html.parser.end-before-html" >:: fun _ -> - expect ~prefix:true "</p><html></html>" - [ (1, 1, E (`Unmatched_end_tag "p")); (1, 5, S (start_element "html")) ] - ); - ( "html.parser.junk-before-head" >:: fun _ -> - expect ~prefix:true "<html><!DOCTYPE html><html></p><head></head></html>" - [ - (1, 1, S (start_element "html")); - (1, 7, E (`Bad_document "doctype should be first")); - (1, 22, E (`Misnested_tag ("html", "html", []))); - (1, 28, E (`Unmatched_end_tag "p")); - (1, 32, S (start_element "head")); - ] ); - ( "html.parser.head" >:: fun _ -> - expect ~prefix:true "<head> <!--foo--><link><link/><meta><meta/></head>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 7, S (`Text [ " " ])); - (1, 8, S (`Comment "foo")); - (1, 18, S (start_element "link")); - (1, 18, S `End_element); - (1, 24, S (start_element "link")); - (1, 24, S `End_element); - (1, 31, S (start_element "meta")); - (1, 31, S `End_element); - (1, 37, S (start_element "meta")); - (1, 37, S `End_element); - (1, 44, S `End_element); - (1, 51, S (start_element "body")); - ] ); - ( "html.parser.style" >:: fun _ -> - expect ~prefix:true "<head><style>foo</head>&lt;</style></head>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 7, S (start_element "style")); - (1, 14, S (`Text [ "foo</head>&lt;" ])); - (1, 28, S `End_element); - (1, 36, S `End_element); - (1, 43, S (start_element "body")); - ] ); - ( "html.parser.title" >:: fun _ -> - expect ~prefix:true "<head><title>foo</head>&lt;</title></head>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 7, S (start_element "title")); - (1, 14, S (`Text [ "foo</head><" ])); - (1, 28, S `End_element); - (1, 36, S `End_element); - (1, 43, S (start_element "body")); - ] ); - ( "html.parser.script" >:: fun _ -> - expect ~prefix:true "<head><script><!--foo</head>&lt;bar</script></head>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 7, S (start_element "script")); - (1, 15, S (`Text [ "<!--foo</head>&lt;bar" ])); - (1, 36, S `End_element); - (1, 45, S `End_element); - (1, 52, S (start_element "body")); - ] ); - ( "html.parser.junk-in-head" >:: fun _ -> - expect ~prefix:true "<head><!DOCTYPE html><html><head></p></head>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 7, E (`Bad_document "doctype should be first")); - (1, 22, E (`Misnested_tag ("html", "head", []))); - (1, 28, E (`Misnested_tag ("head", "head", []))); - (1, 34, E (`Unmatched_end_tag "p")); - (1, 38, S `End_element); - (1, 45, S (start_element "body")); - ] ); - ( "html.parser.junk-after-head" >:: fun _ -> - expect ~prefix:true - "<head></head> <!--foo--><!DOCTYPE html><html><meta><head></p><body>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 7, S `End_element); - (1, 14, S (`Text [ " " ])); - (1, 15, S (`Comment "foo")); - (1, 25, E (`Bad_document "doctype should be first")); - (1, 40, E (`Misnested_tag ("html", "html", []))); - (1, 46, E (`Misnested_tag ("meta", "html", []))); - (1, 46, S (start_element "meta")); - (1, 46, S `End_element); - (1, 52, E (`Bad_document "duplicate head element")); - (1, 58, E (`Unmatched_end_tag "p")); - (1, 62, S (start_element "body")); - ] ); - ( "html.parser.whitespace-after-head" >:: fun _ -> - expect "<html><head></head> </html>" - [ - (1, 1, S (start_element "html")); - (1, 7, S (start_element "head")); - (1, 13, S `End_element); - (1, 20, S (`Text [ " " ])); - (1, 21, S (start_element "body")); - (1, 28, S `End_element); - (1, 28, S `End_element); - ]; - - expect "<html><head><title>foo</title></head> </html>" - [ - (1, 1, S (start_element "html")); - (1, 7, S (start_element "head")); - (1, 13, S (start_element "title")); - (1, 20, S (`Text [ "foo" ])); - (1, 23, S `End_element); - (1, 31, S `End_element); - (1, 38, S (`Text [ " " ])); - (1, 39, S (start_element "body")); - (1, 46, S `End_element); - (1, 46, S `End_element); - ] ); - ( "html.parser.body-content" >:: fun _ -> - expect "<body><!--foo--> bar</body>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 7, S (`Comment "foo")); - (1, 17, S (`Text [ " bar" ])); - (1, 28, S `End_element); - (1, 28, S `End_element); - ] ); - ( "html.parser.body.whitespace" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - " \n\r\t\x0c&#x0d;" - [ (1, 1, S (`Text [ " \n\n\t\x0c\r" ])) ] ); - ( "html.parser.paragraphs" >:: fun _ -> - expect "<p>foo</p>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S (`Text [ "foo" ])); - (1, 7, S `End_element); - (1, 11, S `End_element); - (1, 11, S `End_element); - ]; - - expect "<p>foo<p>bar<div>baz</div>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S (`Text [ "foo" ])); - (1, 7, S `End_element); - (1, 7, S (start_element "p")); - (1, 10, S (`Text [ "bar" ])); - (1, 13, S `End_element); - (1, 13, S (start_element "div")); - (1, 18, S (`Text [ "baz" ])); - (1, 21, S `End_element); - (1, 27, S `End_element); - (1, 27, S `End_element); - ] ); - ( "html.parser.p.autoclose" >:: fun _ -> - expect - ("<p><address></address><p><article></article><p><aside></aside>\n" - ^ "<p><blockquote></blockquote><p><center></center>\n" - ^ "<p><details></details><p><dialog></dialog><p><dir></dir>\n" - ^ "<p><div></div><p><dl></dl><p><fieldset></fieldset>\n" - ^ "<p><figcaption></figcaption><p><figure></figure>\n" - ^ "<p><footer></footer><p><header></header><p><hgroup></hgroup>\n" - ^ "<p><main></main><p><nav></nav><p><ol></ol><p><p></p>\n" - ^ "<p><section></section><p><summary></summary><p><ul></ul>\n" - ^ "<p><h1></h1><p><h2></h2><p><h3></h3><p><h4></h4><p><h5></h5>\n" - ^ "<p><h6></h6>") - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "address")); - (1, 13, S `End_element); - (1, 23, S (start_element "p")); - (1, 26, S `End_element); - (1, 26, S (start_element "article")); - (1, 35, S `End_element); - (1, 45, S (start_element "p")); - (1, 48, S `End_element); - (1, 48, S (start_element "aside")); - (1, 55, S `End_element); - (1, 63, S (`Text [ "\n" ])); - (2, 1, S (start_element "p")); - (2, 4, S `End_element); - (2, 4, S (start_element "blockquote")); - (2, 16, S `End_element); - (2, 29, S (start_element "p")); - (2, 32, S `End_element); - (2, 32, S (start_element "center")); - (2, 40, S `End_element); - (2, 49, S (`Text [ "\n" ])); - (3, 1, S (start_element "p")); - (3, 4, S `End_element); - (3, 4, S (start_element "details")); - (3, 13, S `End_element); - (3, 23, S (start_element "p")); - (3, 26, S `End_element); - (3, 26, S (start_element "dialog")); - (3, 34, S `End_element); - (3, 43, S (start_element "p")); - (3, 46, S `End_element); - (3, 46, S (start_element "dir")); - (3, 51, S `End_element); - (3, 57, S (`Text [ "\n" ])); - (4, 1, S (start_element "p")); - (4, 4, S `End_element); - (4, 4, S (start_element "div")); - (4, 9, S `End_element); - (4, 15, S (start_element "p")); - (4, 18, S `End_element); - (4, 18, S (start_element "dl")); - (4, 22, S `End_element); - (4, 27, S (start_element "p")); - (4, 30, S `End_element); - (4, 30, S (start_element "fieldset")); - (4, 40, S `End_element); - (4, 51, S (`Text [ "\n" ])); - (5, 1, S (start_element "p")); - (5, 4, S `End_element); - (5, 4, S (start_element "figcaption")); - (5, 16, S `End_element); - (5, 29, S (start_element "p")); - (5, 32, S `End_element); - (5, 32, S (start_element "figure")); - (5, 40, S `End_element); - (5, 49, S (`Text [ "\n" ])); - (6, 1, S (start_element "p")); - (6, 4, S `End_element); - (6, 4, S (start_element "footer")); - (6, 12, S `End_element); - (6, 21, S (start_element "p")); - (6, 24, S `End_element); - (6, 24, S (start_element "header")); - (6, 32, S `End_element); - (6, 41, S (start_element "p")); - (6, 44, S `End_element); - (6, 44, S (start_element "hgroup")); - (6, 52, S `End_element); - (6, 61, S (`Text [ "\n" ])); - (7, 1, S (start_element "p")); - (7, 4, S `End_element); - (7, 4, S (start_element "main")); - (7, 10, S `End_element); - (7, 17, S (start_element "p")); - (7, 20, S `End_element); - (7, 20, S (start_element "nav")); - (7, 25, S `End_element); - (7, 31, S (start_element "p")); - (7, 34, S `End_element); - (7, 34, S (start_element "ol")); - (7, 38, S `End_element); - (7, 43, S (start_element "p")); - (7, 46, S `End_element); - (7, 46, S (start_element "p")); - (7, 49, S `End_element); - (7, 53, S (`Text [ "\n" ])); - (8, 1, S (start_element "p")); - (8, 4, S `End_element); - (8, 4, S (start_element "section")); - (8, 13, S `End_element); - (8, 23, S (start_element "p")); - (8, 26, S `End_element); - (8, 26, S (start_element "summary")); - (8, 35, S `End_element); - (8, 45, S (start_element "p")); - (8, 48, S `End_element); - (8, 48, S (start_element "ul")); - (8, 52, S `End_element); - (8, 57, S (`Text [ "\n" ])); - (9, 1, S (start_element "p")); - (9, 4, S `End_element); - (9, 4, S (start_element "h1")); - (9, 8, S `End_element); - (9, 13, S (start_element "p")); - (9, 16, S `End_element); - (9, 16, S (start_element "h2")); - (9, 20, S `End_element); - (9, 25, S (start_element "p")); - (9, 28, S `End_element); - (9, 28, S (start_element "h3")); - (9, 32, S `End_element); - (9, 37, S (start_element "p")); - (9, 40, S `End_element); - (9, 40, S (start_element "h4")); - (9, 44, S `End_element); - (9, 49, S (start_element "p")); - (9, 52, S `End_element); - (9, 52, S (start_element "h5")); - (9, 56, S `End_element); - (9, 61, S (`Text [ "\n" ])); - (10, 1, S (start_element "p")); - (10, 4, S `End_element); - (10, 4, S (start_element "h6")); - (10, 8, S `End_element); - (10, 13, S `End_element); - (10, 13, S `End_element); - ] ); - ( "html.parser.attributes" >:: fun _ -> - expect "<div :class='foo'></div>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - ( 1, - 1, - S (`Start_element ((html_ns, "div"), [ (("", ":class"), "foo") ])) - ); - (1, 19, S `End_element); - (1, 25, S `End_element); - (1, 25, S `End_element); - ] ); - ( "html.parser.links" >:: fun _ -> - expect {|<a href="foo.com?bar=on&acte=123">foo</a>|} - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - ( 1, - 1, - S - (`Start_element - ((html_ns, "a"), [ (("", "href"), "foo.com?bar=on&acte=123") ])) - ); - (1, 35, S (`Text [ "foo" ])); - (1, 38, S `End_element); - (1, 42, S `End_element); - (1, 42, S `End_element); - ]; - - expect {|<a href="foo.com?bar=on&image=on">foo</a>|} - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - ( 1, - 1, - S - (`Start_element - ((html_ns, "a"), [ (("", "href"), "foo.com?bar=on&image=on") ])) - ); - (1, 35, S (`Text [ "foo" ])); - (1, 38, S `End_element); - (1, 42, S `End_element); - (1, 42, S `End_element); - ]; - - expect {|<a href="foo.com?bar=on&image;">foo</a>|} - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - ( 1, - 1, - S - (`Start_element - ((html_ns, "a"), [ (("", "href"), "foo.com?bar=onℑ") ])) ); - (1, 33, S (`Text [ "foo" ])); - (1, 36, S `End_element); - (1, 40, S `End_element); - (1, 40, S `End_element); - ] ); - ( "html.parser.headings" >:: fun _ -> - expect "<h1><h2><h3><h4><h5><h6><h1>foo</h1>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "h1")); - (1, 5, E (`Misnested_tag ("h2", "h1", []))); - (1, 5, S `End_element); - (1, 5, S (start_element "h2")); - (1, 9, E (`Misnested_tag ("h3", "h2", []))); - (1, 9, S `End_element); - (1, 9, S (start_element "h3")); - (1, 13, E (`Misnested_tag ("h4", "h3", []))); - (1, 13, S `End_element); - (1, 13, S (start_element "h4")); - (1, 17, E (`Misnested_tag ("h5", "h4", []))); - (1, 17, S `End_element); - (1, 17, S (start_element "h5")); - (1, 21, E (`Misnested_tag ("h6", "h5", []))); - (1, 21, S `End_element); - (1, 21, S (start_element "h6")); - (1, 25, E (`Misnested_tag ("h1", "h6", []))); - (1, 25, S `End_element); - (1, 25, S (start_element "h1")); - (1, 29, S (`Text [ "foo" ])); - (1, 32, S `End_element); - (1, 37, S `End_element); - (1, 37, S `End_element); - ] ); - ( "html.parser.pre" >:: fun _ -> - expect "<p><pre>foo</pre>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "pre")); - (1, 9, S (`Text [ "foo" ])); - (1, 12, S `End_element); - (1, 18, S `End_element); - (1, 18, S `End_element); - ]; - - expect "<p><pre>\n\nfoo</pre>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "pre")); - (2, 1, S (`Text [ "\nfoo" ])); - (3, 4, S `End_element); - (3, 10, S `End_element); - (3, 10, S `End_element); - ] ); - ( "html.parser.listing.leading-newline" >:: fun _ -> - expect "<listing>\n\nfoo</listing>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "listing")); - (2, 1, S (`Text [ "\nfoo" ])); - (3, 4, S `End_element); - (3, 14, S `End_element); - (3, 14, S `End_element); - ] ); - ( "html.parser.textarea" >:: fun _ -> - expect "<textarea>foo</p></textarea>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "textarea")); - (1, 11, S (`Text [ "foo</p>" ])); - (1, 18, S `End_element); - (1, 29, S `End_element); - (1, 29, S `End_element); - ]; - - expect "<textarea>\n\nfoo</p></textarea>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "textarea")); - (2, 1, S (`Text [ "\nfoo</p>" ])); - (3, 8, S `End_element); - (3, 19, S `End_element); - (3, 19, S `End_element); - ]; - - expect - ~context:(Some (`Fragment "body")) - "<textarea></textarea><p>foo</p>" - [ - (1, 1, S (start_element "textarea")); - (1, 11, S `End_element); - (1, 22, S (start_element "p")); - (1, 25, S (`Text [ "foo" ])); - (1, 28, S `End_element); - ] ); - ( "html.parser.list" >:: fun _ -> - expect "<ul><li>foo<li>bar</ul>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "ul")); - (1, 5, S (start_element "li")); - (1, 9, S (`Text [ "foo" ])); - (1, 12, S `End_element); - (1, 12, S (start_element "li")); - (1, 16, S (`Text [ "bar" ])); - (1, 19, S `End_element); - (1, 19, S `End_element); - (1, 24, S `End_element); - (1, 24, S `End_element); - ] ); - ( "html.parser.definition" >:: fun _ -> - expect "<p><dt>foo<dd>bar" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "dt")); - (1, 8, S (`Text [ "foo" ])); - (1, 11, S `End_element); - (1, 11, S (start_element "dd")); - (1, 15, S (`Text [ "bar" ])); - (1, 18, S `End_element); - (1, 18, S `End_element); - (1, 18, S `End_element); - ] ); - ( "html.parser.plaintext" >:: fun _ -> - expect "<p><plaintext>foo</plaintext></p>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "plaintext")); - (1, 4, E (`Unmatched_start_tag "plaintext")); - (1, 15, S (`Text [ "foo</plaintext></p>" ])); - (1, 34, S `End_element); - (1, 34, S `End_element); - (1, 34, S `End_element); - ] ); - ( "html.parser.table" >:: fun _ -> - expect "<p><table><tr><td>foo</td><td>bar</td></tr></table>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "table")); - (1, 11, S (start_element "tbody")); - (1, 11, S (start_element "tr")); - (1, 15, S (start_element "td")); - (1, 19, S (`Text [ "foo" ])); - (1, 22, S `End_element); - (1, 27, S (start_element "td")); - (1, 31, S (`Text [ "bar" ])); - (1, 34, S `End_element); - (1, 39, S `End_element); - (1, 44, S `End_element); - (1, 44, S `End_element); - (1, 52, S `End_element); - (1, 52, S `End_element); - ] ); - ( "html.parser.select" >:: fun _ -> - expect "<select><option>foo<option>bar</select>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "select")); - (1, 9, S (start_element "option")); - (1, 17, S (`Text [ "foo" ])); - (1, 20, S `End_element); - (1, 20, S (start_element "option")); - (1, 28, S (`Text [ "bar" ])); - (1, 31, S `End_element); - (1, 31, S `End_element); - (1, 40, S `End_element); - (1, 40, S `End_element); - ] ); - ( "html.parser.datalist" >:: fun _ -> - expect "<datalist><option><option></datalist>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "datalist")); - (1, 11, S (start_element "option")); - (1, 19, S `End_element); - (1, 19, S (start_element "option")); - (1, 27, S `End_element); - (1, 27, S `End_element); - (1, 38, S `End_element); - (1, 38, S `End_element); - ] ); - ( "html.parser.datalist.whitespace" >:: fun _ -> - expect "<datalist>\n<option>\n<option>\n</datalist>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "datalist")); - (1, 11, S (`Text [ "\n" ])); - (2, 1, S (start_element "option")); - (2, 9, S (`Text [ "\n" ])); - (3, 1, S `End_element); - (3, 1, S (start_element "option")); - (3, 9, S (`Text [ "\n" ])); - (4, 1, S `End_element); - (4, 1, S `End_element); - (4, 12, S `End_element); - (4, 12, S `End_element); - ] ); - ( "html.parser.ruby" >:: fun _ -> - expect "<rb>a<rt>b" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, E (`Misnested_tag ("rb", "body", []))); - (1, 1, S (start_element "rb")); - (1, 6, E (`Misnested_tag ("rt", "body", []))); - (1, 5, S (`Text [ "a" ])); - (1, 6, S (start_element "rt")); - (1, 6, E (`Unmatched_start_tag "rt")); - (1, 1, E (`Unmatched_start_tag "rb")); - (1, 10, S (`Text [ "b" ])); - (1, 11, S `End_element); - (1, 11, S `End_element); - (1, 11, S `End_element); - (1, 11, S `End_element); - ] ); - ( "html.parser.truncated-body" >:: fun _ -> - expect "<body>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 7, S `End_element); - (1, 7, S `End_element); - ]; - - expect "<body></html>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 14, S `End_element); - (1, 14, S `End_element); - ] ); - ( "html.parser.junk-in-body" >:: fun _ -> - expect "<body>\x00<!DOCTYPE html><html><meta><body attr='value'></body>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 7, E (`Bad_token ("U+0000", "body", "null"))); - (1, 8, E (`Bad_document "doctype should be first")); - (1, 23, E (`Misnested_tag ("html", "body", []))); - (1, 29, S (start_element "meta")); - (1, 29, S `End_element); - (1, 35, E (`Misnested_tag ("body", "body", [ ("attr", "value") ]))); - (1, 61, S `End_element); - (1, 61, S `End_element); - ] ); - ( "html.parser.nested-html-in-body" >:: fun _ -> - expect "<div><html></html>foo</div><div>bar</div>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "div")); - (1, 6, E (`Misnested_tag ("html", "body", []))); - (1, 1, E (`Unmatched_start_tag "div")); - (1, 19, E (`Bad_content "html")); - (1, 19, S (`Text [ "foo" ])); - (1, 22, S `End_element); - (1, 28, S (start_element "div")); - (1, 33, S (`Text [ "bar" ])); - (1, 36, S `End_element); - (1, 42, S `End_element); - (1, 42, S `End_element); - ] ); - ( "html.parser.nested-html-with-body-in-body" >:: fun _ -> - expect "<p><html><body><p></body><br><p>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, E (`Misnested_tag ("html", "body", []))); - (1, 10, E (`Misnested_tag ("body", "body", []))); - (1, 16, S `End_element); - (1, 16, S (start_element "p")); - (1, 26, E (`Bad_document "content after body")); - (1, 26, S (start_element "br")); - (1, 26, S `End_element); - (1, 30, S `End_element); - (1, 30, S (start_element "p")); - (1, 33, S `End_element); - (1, 33, S `End_element); - (1, 33, S `End_element); - ] ); - ( "html.parser.whitespace-at-end" >:: fun _ -> - expect "<html><body></body></html> " - [ - (1, 1, S (start_element "html")); - (1, 7, S (start_element "head")); - (1, 7, S `End_element); - (1, 7, S (start_element "body")); - (1, 27, S (`Text [ " " ])); - (1, 28, S `End_element); - (1, 28, S `End_element); - ] ); - ( "html.parser.foreign" >:: fun _ -> - expect "<body><svg><g/></svg></body>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 7, S (`Start_element ((svg_ns, "svg"), []))); - (1, 12, S (`Start_element ((svg_ns, "g"), []))); - (1, 12, S `End_element); - (1, 16, S `End_element); - (1, 29, S `End_element); - (1, 29, S `End_element); - ] ); - ( "html.parser.foreign.attribute" >:: fun _ -> - expect "<body><svg refX=\"\"><g/></svg></body>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 7, S (`Start_element ((svg_ns, "svg"), [ (("", "refX"), "") ]))); - (1, 20, S (`Start_element ((svg_ns, "g"), []))); - (1, 20, S `End_element); - (1, 24, S `End_element); - (1, 37, S `End_element); - (1, 37, S `End_element); - ] ); - ( "html.parser.foreign.svg-followed-by-html" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<svg><feTile></feTile></svg><b></b>" - [ - (1, 1, S (`Start_element ((svg_ns, "svg"), []))); - (1, 6, S (`Start_element ((svg_ns, "feTile"), []))); - (1, 14, S `End_element); - (1, 23, S `End_element); - (1, 29, S (start_element "b")); - (1, 32, S `End_element); - ] ); - ( "html.parser.reconstruct-active-formatting-elements" >:: fun _ -> - expect "<p><em><strong>foo<p>bar" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 8, E (`Unmatched_start_tag "strong")); - (1, 4, S (start_element "em")); - (1, 8, S (start_element "strong")); - (1, 16, S (`Text [ "foo" ])); - (1, 19, S `End_element); - (1, 19, S `End_element); - (1, 19, S `End_element); - (1, 19, S (start_element "p")); - (1, 8, E (`Unmatched_start_tag "strong")); - (1, 4, E (`Unmatched_start_tag "em")); - (1, 4, S (start_element "em")); - (1, 8, S (start_element "strong")); - (1, 22, S (`Text [ "bar" ])); - (1, 25, S `End_element); - (1, 25, S `End_element); - (1, 25, S `End_element); - (1, 25, S `End_element); - (1, 25, S `End_element); - ] ); - ( "html.parser.close-formatting-elements" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<a>fo</a>o" - [ - (1, 1, S (start_element "a")); - (1, 4, S (`Text [ "fo" ])); - (1, 6, S `End_element); - (1, 10, S (`Text [ "o" ])); - ] ); - ( "html.parser.reset-mode" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<table></table><table></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S `End_element); - (1, 16, S (start_element "table")); - (1, 23, S `End_element); - ] ); - ( "html.parser.fragment" >:: fun _ -> - expect - ~context:(Some (`Fragment "title")) - "</p>" - [ (1, 1, S (`Text [ "</p>" ])) ]; - - expect - ~context:(Some (`Fragment "textarea")) - "</p>" - [ (1, 1, S (`Text [ "</p>" ])) ]; - - expect - ~context:(Some (`Fragment "body")) - "</p>" - [ - (1, 1, E (`Unmatched_end_tag "p")); - (1, 1, S (start_element "p")); - (1, 1, S `End_element); - ]; - - expect - ~context:(Some (`Fragment "body")) - "<!DOCTYPE html>" - [ (1, 1, E (`Bad_document "doctype should be first")) ] ); - ( "html.parser.fragment.rawtext" >:: fun _ -> - expect - ~context:(Some (`Fragment "style")) - "&nbsp;</p>" - [ (1, 1, S (`Text [ "&nbsp;</p>" ])) ] ); - ( "html.parser.fragment.script" >:: fun _ -> - expect - ~context:(Some (`Fragment "script")) - "&nbsp;</p>" - [ (1, 1, S (`Text [ "&nbsp;</p>" ])) ] ); - ( "html.parser.fragment.plaintext" >:: fun _ -> - expect - ~context:(Some (`Fragment "plaintext")) - "&nbsp;</p></plaintext>" - [ (1, 1, S (`Text [ "&nbsp;</p></plaintext>" ])) ] ); - ( "html.parser.context-detection" >:: fun _ -> - expect ~context:None "<p>foo</p>" - [ - (1, 1, S (start_element "p")); - (1, 4, S (`Text [ "foo" ])); - (1, 7, S `End_element); - ]; - - expect ~context:None "<html></html>" - [ - (1, 1, S (start_element "html")); - (1, 7, S (start_element "head")); - (1, 7, S `End_element); - (1, 7, S (start_element "body")); - (1, 14, S `End_element); - (1, 14, S `End_element); - ] ); - ( "html.parser.foreign-context" >:: fun _ -> - expect ~context:None "<g/>" - [ - (1, 1, S (`Start_element ((svg_ns, "g"), []))); (1, 1, S `End_element); - ] ); - ( "html.parser.context-disambiguation" >:: fun _ -> - expect - ~context:(Some (`Fragment "svg")) - "<a></a>" - [ - (1, 1, S (`Start_element ((svg_ns, "a"), []))); (1, 4, S `End_element); - ] ); - ( "html.parser.context-case-insensitivity" >:: fun _ -> - expect - ~context:(Some (`Fragment "SVG")) - "<a></a>" - [ - (1, 1, S (`Start_element ((svg_ns, "a"), []))); (1, 4, S `End_element); - ] ); - ( "html.parser.bad-self-closing-tag" >:: fun _ -> - expect "<p/>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, E (`Bad_token ("/>", "tag", "should not be self-closing"))); - (1, 1, S (start_element "p")); - (1, 5, S `End_element); - (1, 5, S `End_element); - (1, 5, S `End_element); - ] ); - ( "html.parser.image-tag" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<image/>" - [ - (1, 1, E (`Bad_token ("image", "tag", "should be 'img'"))); - (1, 1, S (start_element "img")); - (1, 1, S `End_element); - ] ); - ( "html.parser.nulls" >:: fun _ -> - expect - ~context:(Some (`Fragment "svg")) - "\x00foo" - [ - (1, 1, E (`Bad_token ("U+0000", "foreign content", "null"))); - (1, 1, S (`Text [ "\xef\xbf\xbdfoo" ])); - ]; - - expect - ~context:(Some (`Fragment "body")) - "<table>\x00foo</table>" - [ - (1, 1, S (start_element "table")); - (1, 8, E (`Bad_token ("U+0000", "table", "null"))); - (1, 9, E (`Bad_content "table")); - (1, 10, E (`Bad_content "table")); - (1, 11, E (`Bad_content "table")); - (1, 9, S (`Text [ "foo" ])); - (1, 12, S `End_element); - ]; - - expect - ~context:(Some (`Fragment "select")) - "\x00foo" - [ - (1, 1, E (`Bad_token ("U+0000", "select", "null"))); - (1, 2, S (`Text [ "foo" ])); - ] ); - ( "html.parser.foreign.cdata" >:: fun _ -> - expect ~context:None "<svg><![CDATA[foo]]></svg>" - [ - (1, 1, S (`Start_element ((svg_ns, "svg"), []))); - (1, 15, S (`Text [ "foo" ])); - (1, 21, S `End_element); - ] ); - ( "html.parser.large-text" >:: fun _ -> - with_text_limit 8 (fun () -> - expect ~context:None "foobar" [ (1, 1, S (`Text [ "foobar" ])) ]; - expect ~context:None "foobarbaz" - [ (1, 1, S (`Text [ "foobarba"; "z" ])) ]) ); - ( "html.parser.adoption-agency.simple" >:: fun _ -> - expect ~context:None "foo<b>bar</b>baz" - [ - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "bar" ])); - (1, 10, S `End_element); - (1, 14, S (`Text [ "baz" ])); - ] ); - ( "html.parser.adoption-agency.stray" >:: fun _ -> - expect ~context:None "foo</b>bar" - [ - (1, 4, E (`Unmatched_end_tag "b")); (1, 1, S (`Text [ "foo"; "bar" ])); - ] ); - ( "html.parser.adoption-agency.nested" >:: fun _ -> - expect ~context:None "foo<b>bar<em>baz</em>quux</b>lulz" - [ - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "bar" ])); - (1, 10, S (start_element "em")); - (1, 14, S (`Text [ "baz" ])); - (1, 17, S `End_element); - (1, 22, S (`Text [ "quux" ])); - (1, 26, S `End_element); - (1, 30, S (`Text [ "lulz" ])); - ] ); - ( "html.parser.adoption-agency.nested.stray" >:: fun _ -> - expect ~context:None "foo<b>bar</em>baz</b>quux" - [ - (1, 10, E (`Unmatched_end_tag "em")); - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "bar"; "baz" ])); - (1, 18, S `End_element); - (1, 22, S (`Text [ "quux" ])); - ] ); - ( "html.parser.adoption-agency.interleaved" >:: fun _ -> - expect ~context:None "foo<b>bar<em>baz</b>quux</em>" - [ - (1, 17, E (`Unmatched_end_tag "b")); - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "bar" ])); - (1, 10, S (start_element "em")); - (1, 14, S (`Text [ "baz" ])); - (1, 17, S `End_element); - (1, 17, S `End_element); - (1, 10, S (start_element "em")); - (1, 21, S (`Text [ "quux" ])); - (1, 25, S `End_element); - ] ); - ( "html.parser.adoption-agency.block" >:: fun _ -> - expect ~context:None "foo<b>bar<p>baz</b>quux" - [ - (1, 16, E (`Unmatched_end_tag "b")); - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "bar" ])); - (1, 16, S `End_element); - (1, 10, S (start_element "p")); - (1, 4, S (start_element "b")); - (1, 13, S (`Text [ "baz" ])); - (1, 16, S `End_element); - (1, 20, S (`Text [ "quux" ])); - (1, 24, S `End_element); - ] ); - ( "html.parser.adoption-agency.block.nested" >:: fun _ -> - expect ~context:None "foo<b>bar<em>baz<strong>quux<p>blah</b>lulz" - [ - (1, 36, E (`Unmatched_end_tag "b")); - (1, 17, E (`Unmatched_start_tag "strong")); - (1, 10, E (`Unmatched_start_tag "em")); - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "bar" ])); - (1, 10, S (start_element "em")); - (1, 14, S (`Text [ "baz" ])); - (1, 17, S (start_element "strong")); - (1, 25, S (`Text [ "quux" ])); - (1, 36, S `End_element); - (1, 36, S `End_element); - (1, 36, S `End_element); - (1, 10, S (start_element "em")); - (1, 17, S (start_element "strong")); - (1, 29, S (start_element "p")); - (1, 4, S (start_element "b")); - (1, 32, S (`Text [ "blah" ])); - (1, 36, S `End_element); - (1, 40, S (`Text [ "lulz" ])); - (1, 44, S `End_element); - (1, 44, S `End_element); - (1, 44, S `End_element); - ] ); - ( "html.parser.adoption-agency.reconstructed" >:: fun _ -> - expect ~context:None "<p><b>foo<p>bar<em>baz</b>quux" - [ - (1, 1, S (start_element "p")); - (1, 4, E (`Unmatched_start_tag "b")); - (1, 4, S (start_element "b")); - (1, 7, S (`Text [ "foo" ])); - (1, 10, S `End_element); - (1, 10, S `End_element); - (1, 10, S (start_element "p")); - (1, 23, E (`Unmatched_end_tag "b")); - (1, 16, E (`Unmatched_start_tag "em")); - (1, 4, S (start_element "b")); - (1, 13, S (`Text [ "bar" ])); - (1, 16, S (start_element "em")); - (1, 20, S (`Text [ "baz" ])); - (1, 23, S `End_element); - (1, 23, S `End_element); - (1, 16, S (start_element "em")); - (1, 27, S (`Text [ "quux" ])); - (1, 31, S `End_element); - (1, 31, S `End_element); - ] ); - ( "html.parser.noscript" >:: fun _ -> - expect ~context:None "<head><noscript><meta></noscript></head>" - [ - (1, 1, S (start_element "head")); - (1, 7, S (start_element "noscript")); - (1, 17, S (start_element "meta")); - (1, 17, S `End_element); - (1, 23, S `End_element); - (1, 34, S `End_element); - ] ); - ( "html.parser.noscript.bad" >:: fun _ -> - expect ~context:None - "<head><noscript><!DOCTYPE \ - html><html><head><noscript></head></noscript>" - [ - (1, 1, S (start_element "head")); - (1, 7, S (start_element "noscript")); - (1, 17, E (`Bad_document "doctype should be first")); - (1, 32, E (`Misnested_tag ("html", "noscript", []))); - (1, 38, E (`Misnested_tag ("head", "noscript", []))); - (1, 44, E (`Misnested_tag ("noscript", "noscript", []))); - (1, 54, E (`Unmatched_end_tag "head")); - (1, 61, S `End_element); - (1, 72, S `End_element); - ] ); - ( "html.parser.noscript.head.content" >:: fun _ -> - expect - ~context:(Some (`Fragment "head")) - "<noscript> \t\n<!--foo--> foo</noscript>" - [ - (1, 1, S (start_element "noscript")); - (1, 11, S (`Text [ " \t\n" ])); - (2, 1, S (`Comment "foo")); - (2, 12, E (`Bad_content "noscript")); - (2, 11, S (`Text [ " " ])); - (2, 12, S `End_element); - (2, 12, S (start_element "body")); - (2, 15, E (`Unmatched_end_tag "noscript")); - (2, 12, S (`Text [ "foo" ])); - (2, 26, S `End_element); - ] ); - ( "html.parser.noscript.inferred.content" >:: fun _ -> - expect ~context:None "<noscript> \t\n<!--foo--> foo</noscript>" - [ - (1, 1, S (start_element "noscript")); - (1, 11, S (`Text [ " \t\n" ])); - (2, 1, S (`Comment "foo")); - (2, 11, S (`Text [ " foo" ])); - (2, 15, S `End_element); - ] ); - ( "html.parser.noscript-script.inferred.content" >:: fun _ -> - expect ~context:None "<script>foo</script><noscript>bar</noscript>" - [ - (1, 1, S (start_element "script")); - (1, 9, S (`Text [ "foo" ])); - (1, 12, S `End_element); - (1, 21, S (start_element "noscript")); - (1, 31, S (`Text [ "bar" ])); - (1, 34, S `End_element); - ] ); - ( "html.parser.head.fragment" >:: fun _ -> - expect ~context:None "<base>" - [ (1, 1, S (start_element "base")); (1, 1, S `End_element) ]; - - expect ~context:None "<basefont>" - [ (1, 1, S (start_element "basefont")); (1, 1, S `End_element) ]; - - expect ~context:None "<bgsound>" - [ (1, 1, S (start_element "bgsound")); (1, 1, S `End_element) ]; - - expect ~context:None "<link>" - [ (1, 1, S (start_element "link")); (1, 1, S `End_element) ]; - - expect ~context:None "<meta>" - [ (1, 1, S (start_element "meta")); (1, 1, S `End_element) ]; - - expect ~context:None "<noframes></noframes>" - [ (1, 1, S (start_element "noframes")); (1, 11, S `End_element) ]; - - expect ~context:None "<style></style>" - [ (1, 1, S (start_element "style")); (1, 8, S `End_element) ] ); - ( "html.parser.body.fragment" >:: fun _ -> - expect ~context:None "<body></body>" - [ (1, 1, S (start_element "body")); (1, 14, S `End_element) ] ); - ( "html.parser.body.content-truncated" >:: fun _ -> - expect ~context:None "<p></body></html>foo" - [ - (1, 1, S (start_element "p")); - (1, 4, E (`Unmatched_end_tag "body")); - (1, 11, E (`Unmatched_end_tag "html")); - (1, 18, S (`Text [ "foo" ])); - (1, 21, S `End_element); - ] ); - ( "html.parser.nested-button" >:: fun _ -> - expect ~context:None "<button><button>submit</button></button>" - [ - (1, 1, S (start_element "button")); - (1, 9, E (`Misnested_tag ("button", "button", []))); - (1, 9, S `End_element); - (1, 9, S (start_element "button")); - (1, 17, S (`Text [ "submit" ])); - (1, 23, S `End_element); - (1, 32, E (`Unmatched_end_tag "button")); - ] ); - ( "html.parser.nested-list" >:: fun _ -> - expect ~context:None "<ul><li><ul></li></ul></li></ul>" - [ - (1, 1, S (start_element "ul")); - (1, 5, S (start_element "li")); - (1, 9, S (start_element "ul")); - (1, 13, E (`Unmatched_end_tag "li")); - (1, 18, S `End_element); - (1, 23, S `End_element); - (1, 28, S `End_element); - ] ); - ( "html.parser.definitions" >:: fun _ -> - expect ~context:None "</dd><dd></dd>" - [ - (1, 1, E (`Unmatched_end_tag "dd")); - (1, 6, S (start_element "dd")); - (1, 10, S `End_element); - ] ); - ( "html.parser.nested-achor" >:: fun _ -> - expect ~context:None "<a><a></a></a>" - [ - (1, 4, E (`Misnested_tag ("a", "a", []))); - (1, 11, E (`Unmatched_end_tag "a")); - (1, 1, S (start_element "a")); - (1, 4, S `End_element); - (1, 4, S (start_element "a")); - (1, 7, S `End_element); - ] ); - ( "html.parser.nested-anchor.reconstruct" >:: fun _ -> - expect ~context:None "<p><a>foo<a>bar<p>baz" - [ - (1, 1, S (start_element "p")); - (1, 10, E (`Misnested_tag ("a", "a", []))); - (1, 10, E (`Unmatched_start_tag "a")); - (1, 4, S (start_element "a")); - (1, 7, S (`Text [ "foo" ])); - (1, 10, S `End_element); - (1, 10, S (start_element "a")); - (1, 13, S (`Text [ "bar" ])); - (1, 16, S `End_element); - (1, 16, S `End_element); - (1, 16, S (start_element "p")); - (1, 10, E (`Unmatched_start_tag "a")); - (1, 10, S (start_element "a")); - (1, 19, S (`Text [ "baz" ])); - (1, 22, S `End_element); - (1, 22, S `End_element); - ] ); - ( "html.parser.nested-nobr" >:: fun _ -> - expect ~context:None "foo<nobr>bar<nobr>baz</nobr>quux</nobr>blah" - [ - (1, 13, E (`Misnested_tag ("nobr", "nobr", []))); - (1, 33, E (`Unmatched_end_tag "nobr")); - (1, 1, S (`Text [ "foo" ])); - (1, 4, S (start_element "nobr")); - (1, 10, S (`Text [ "bar" ])); - (1, 13, S `End_element); - (1, 13, S (start_element "nobr")); - (1, 19, S (`Text [ "baz" ])); - (1, 22, S `End_element); - (1, 29, S (`Text [ "quux"; "blah" ])); - ] ); - ( "html.parser.end-br" >:: fun _ -> - expect ~context:None "<br></br>" - [ - (1, 1, S (start_element "br")); - (1, 1, S `End_element); - (1, 5, E (`Unmatched_end_tag "br")); - (1, 5, S (start_element "br")); - (1, 5, S `End_element); - ] ); - ( "html.parser.hr" >:: fun _ -> - expect ~context:None "<p><hr>" - [ - (1, 1, S (start_element "p")); - (1, 4, S `End_element); - (1, 4, S (start_element "hr")); - (1, 4, S `End_element); - ] ); - ( "html.parser.input" >:: fun _ -> - expect ~context:None "<input type='text'>" - [ - ( 1, - 1, - S (`Start_element ((html_ns, "input"), [ (("", "type"), "text") ])) - ); - (1, 1, S `End_element); - ] ); - ( "html.parser.iframe" >:: fun _ -> - expect ~context:None "<iframe><p>foo&amp;</p></iframe>" - [ - (1, 1, S (start_element "iframe")); - (1, 9, S (`Text [ "<p>foo&amp;</p>" ])); - (1, 24, S `End_element); - ] ); - ( "html.parser.noembed" >:: fun _ -> - expect ~context:None "<noembed><p>foo&amp;</p></noembed>" - [ - (1, 1, S (start_element "noembed")); - (1, 10, S (`Text [ "<p>foo&amp;</p>" ])); - (1, 25, S `End_element); - ] ); - ( "html.parser.generic-tag" >:: fun _ -> - expect ~context:None "<foo></foo>" - [ (1, 1, S (start_element "foo")); (1, 6, S `End_element) ] ); - ( "html.parser.option.body" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<option><optgroup></optgroup>" - [ - (1, 1, S (start_element "option")); - (1, 9, S `End_element); - (1, 9, S (start_element "optgroup")); - (1, 19, S `End_element); - ] ); - ( "html.parser.table-content-in-body" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<caption><col><colgroup><tbody><td><tfoot><th><thead><tr>" - [ - (1, 1, E (`Misnested_tag ("caption", "body", []))); - (1, 10, E (`Misnested_tag ("col", "body", []))); - (1, 15, E (`Misnested_tag ("colgroup", "body", []))); - (1, 25, E (`Misnested_tag ("tbody", "body", []))); - (1, 32, E (`Misnested_tag ("td", "body", []))); - (1, 36, E (`Misnested_tag ("tfoot", "body", []))); - (1, 43, E (`Misnested_tag ("th", "body", []))); - (1, 47, E (`Misnested_tag ("thead", "body", []))); - (1, 54, E (`Misnested_tag ("tr", "body", []))); - ] ); - ( "html.parser.caption" >:: fun _ -> - expect ~context:None "<table><caption>foo<p>bar</caption></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "caption")); - (1, 17, S (`Text [ "foo" ])); - (1, 20, S (start_element "p")); - (1, 23, S (`Text [ "bar" ])); - (1, 26, S `End_element); - (1, 26, S `End_element); - (1, 36, S `End_element); - ] ); - ( "html.parser.colgroup" >:: fun _ -> - expect ~context:None "<table><colgroup><col></colgroup></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "colgroup")); - (1, 18, S (start_element "col")); - (1, 18, S `End_element); - (1, 23, S `End_element); - (1, 34, S `End_element); - ] ); - ( "html.parser.colgroup.implicit" >:: fun _ -> - expect ~context:None "<table><col></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "colgroup")); - (1, 8, S (start_element "col")); - (1, 8, S `End_element); - (1, 13, S `End_element); - (1, 13, S `End_element); - ] ); - ( "html.parser.td.direct" >:: fun _ -> - expect ~context:None "<table><td></td></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "tbody")); - (1, 8, E (`Misnested_tag ("td", "table", []))); - (1, 8, S (start_element "tr")); - (1, 8, S (start_element "td")); - (1, 12, S `End_element); - (1, 17, S `End_element); - (1, 17, S `End_element); - (1, 17, S `End_element); - ] ); - ( "html.parser.tbody" >:: fun _ -> - expect ~context:None "<table><tbody></tbody></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "tbody")); - (1, 15, S `End_element); - (1, 23, S `End_element); - ] ); - ( "html.parser.nested-table" >:: fun _ -> - expect ~context:None "<table><table></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, E (`Misnested_tag ("table", "table", []))); - (1, 8, S `End_element); - (1, 8, S (start_element "table")); - (1, 15, S `End_element); - ] ); - ( "html.parser.nested-caption" >:: fun _ -> - expect ~context:None "<table><caption><caption></caption></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "caption")); - (1, 17, E (`Misnested_tag ("caption", "caption", []))); - (1, 17, S `End_element); - (1, 17, S (start_element "caption")); - (1, 26, S `End_element); - (1, 36, S `End_element); - ] ); - ( "html.parser.truncated-caption" >:: fun _ -> - expect ~context:None "<table><caption></table>" - [ - (1, 1, S (start_element "table")); - (1, 8, S (start_element "caption")); - (1, 17, E (`Unmatched_end_tag "table")); - (1, 17, S `End_element); - (1, 17, S `End_element); - ] ); - ( "html.parser.nested-tbody" >:: fun _ -> - expect ~context:None "<tbody><tbody></tbody>" - [ - (1, 1, S (start_element "tbody")); - (1, 8, S `End_element); - (1, 8, S (start_element "tbody")); - (1, 15, S `End_element); - ] ); - ( "html.parser.option" >:: fun _ -> - expect ~context:None "<option></option>" - [ (1, 1, S (start_element "option")); (1, 9, S `End_element) ] ); - ( "html.parser.optgroup" >:: fun _ -> - expect ~context:None - "<select><optgroup><option><optgroup><option></optgroup></select>" - [ - (1, 1, S (start_element "select")); - (1, 9, S (start_element "optgroup")); - (1, 19, S (start_element "option")); - (1, 27, S `End_element); - (1, 27, S `End_element); - (1, 27, S (start_element "optgroup")); - (1, 37, S (start_element "option")); - (1, 45, S `End_element); - (1, 45, S `End_element); - (1, 56, S `End_element); - ] ); - ( "html.parser.form" >:: fun _ -> - expect ~context:None "<form></form>" - [ (1, 1, S (start_element "form")); (1, 7, S `End_element) ] ); - ( "html.parser.form.nested" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<form><form></form>" - [ - (1, 1, S (start_element "form")); - (1, 7, E (`Misnested_tag ("form", "form", []))); - (1, 13, S `End_element); - ] ); - ( "html.parser.form.unopened" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "</form>" - [ (1, 1, E (`Unmatched_end_tag "form")) ] ); - ( "html.parser.noframes" >:: fun _ -> - expect ~context:None "<noframes>foo&amp;bar</a></noframes>" - [ - (1, 1, S (start_element "noframes")); - (1, 11, S (`Text [ "foo&amp;bar</a>" ])); - (1, 26, S `End_element); - ] ); - ( "html.parser.frameset" >:: fun _ -> - expect "<frameset><frame></frameset>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "frameset")); - (1, 11, S (start_element "frame")); - (1, 11, S `End_element); - (1, 18, S `End_element); - (1, 29, S `End_element); - ] ); - ( "html.parser.frameset.fragment" >:: fun _ -> - expect ~context:None "<frameset></frameset>" - [ (1, 1, S (start_element "frameset")); (1, 11, S `End_element) ]; - - expect ~context:None "<frame>" - [ (1, 1, S (start_element "frame")); (1, 1, S `End_element) ] ); - ( "html.parser.frameset.content" >:: fun _ -> - expect ~context:None - ("<frameset> \t\n<!--foo--><noframes></noframes><frameset></frameset>" - ^ "</frameset>") - [ - (1, 1, S (start_element "frameset")); - (1, 11, S (`Text [ " \t\n" ])); - (2, 1, S (`Comment "foo")); - (2, 11, S (start_element "noframes")); - (2, 21, S `End_element); - (2, 32, S (start_element "frameset")); - (2, 42, S `End_element); - (2, 53, S `End_element); - ] ); - ( "html.parser.frameset.bad" >:: fun _ -> - expect ~context:None "<frameset><!DOCTYPE html><html>f" - [ - (1, 1, S (start_element "frameset")); - (1, 11, E (`Bad_document "doctype should be first")); - (1, 26, E (`Misnested_tag ("html", "frameset", []))); - (1, 32, E (`Bad_content "frameset")); - (1, 33, E (`Unexpected_eoi "frameset")); - (1, 33, S `End_element); - ] ); - ( "html.parser.after-frameset.content" >:: fun _ -> - expect ~context:None - "<frameset></frameset> \t\n<!--foo--><noframes></noframes></html>" - [ - (1, 1, S (start_element "frameset")); - (1, 11, S `End_element); - (1, 22, S (`Text [ " \t\n" ])); - (2, 1, S (`Comment "foo")); - (2, 11, S (start_element "noframes")); - (2, 21, S `End_element); - ] ); - ( "html.parser.after-frameset.bad" >:: fun _ -> - expect ~context:None "<frameset></frameset><!DOCTYPE html><html>f" - [ - (1, 1, S (start_element "frameset")); - (1, 11, S `End_element); - (1, 22, E (`Bad_document "doctype should be first")); - (1, 37, E (`Misnested_tag ("html", "html", []))); - (1, 43, E (`Bad_content "html")); - ] ); - ( "html.parser.frameset-in-body" >:: fun _ -> - expect - ~context:(Some (`Fragment "body")) - "<frameset><p>" - [ - (1, 1, E (`Misnested_tag ("frameset", "body", []))); - (1, 11, S (start_element "p")); - (1, 14, S `End_element); - ]; - - expect ~context:None "<body><p><frameset><p></body>" - [ - (1, 1, S (start_element "body")); - (1, 7, S (start_element "p")); - (1, 10, E (`Misnested_tag ("frameset", "body", []))); - (1, 20, S `End_element); - (1, 20, S (start_element "p")); - (1, 30, S `End_element); - (1, 30, S `End_element); - ]; - - expect ~context:None "<p><frameset><p>" - [ - (1, 1, S (start_element "p")); - (1, 4, E (`Misnested_tag ("frameset", "body", []))); - (1, 14, S `End_element); - (1, 14, S (start_element "p")); - (1, 17, S `End_element); - ]; - - expect "<p><frameset><frame></frameset>" - [ - (1, 1, S (start_element "html")); - (1, 1, S (start_element "head")); - (1, 1, S `End_element); - (1, 1, S (start_element "body")); - (1, 1, S (start_element "p")); - (1, 4, E (`Misnested_tag ("frameset", "body", []))); - (1, 4, S `End_element); - (1, 4, S `End_element); - (1, 4, S (start_element "frameset")); - (1, 14, S (start_element "frame")); - (1, 14, S `End_element); - (1, 21, S `End_element); - (1, 32, S `End_element); - ] ); - ] diff --git a/test/test_ragel_tokenizer.ml b/test/test_ragel_tokenizer.ml new file mode 100644 index 0000000..e50d999 --- /dev/null +++ b/test/test_ragel_tokenizer.ml @@ -0,0 +1,35 @@ +open OUnit2 + +let tag name attributes : Markup__Common.Token_tag.t = + {name; attributes; self_closing = false} + +let tokens_without_locations html = + html |> Ragel_html_tokenizer.tokenize |> List.map snd + +let tests = + [ + ( "private.html-tokenize.tokens" >:: fun _ -> + let actual = + tokens_without_locations + "<br/><p a='&amp;'>&lt;</p><script>&amp;</script><style>x</style>" + in + let expected : Markup__Html_tokenizer.token list = + [ + `Start (tag "br" []); + `Start (tag "p" ["a", "&"]); + `String "<"; + `End (tag "p" []); + `Start (tag "script" []); + `String "&amp;"; + `End (tag "script" []); + `Start (tag "style" []); + `String "x"; + `End (tag "style" []); + `EOF; + ] + in + assert_equal expected actual ); + ( "private.html-tokenize.locations" >:: fun _ -> + let actual = Ragel_html_tokenizer.tokenize "one\ntwo" in + assert_equal [((2, -1), `String "one\ntwo"); ((2, -1), `EOF)] actual ); + ]