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 "%s %s?>" 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 "%s %s?>" 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 "%s>" 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