Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
d76dd77
Add callback adaptor for external parser
tysg Aug 22, 2024
dc3725f
Remove Ragel-specific code and expose internals
tysg Aug 29, 2024
f9e55e2
create markup.common
Aug 25, 2026
e4dc083
salvage ragel tokenizer from git history for testing purposes
Aug 25, 2026
50a002a
copy HTML parser for markup.lite
Aug 25, 2026
92bf488
bootstrap markup.lite with Ragel HTML parsing
Aug 25, 2026
ff44c55
add markup.lite corpus test target
Aug 25, 2026
b26d49f
report timing and allocations in Lite corpus test
Aug 25, 2026
853f783
switch Lite parser to pull token source
Aug 25, 2026
4e80120
make Lite Ragel tokenizer resumable
Aug 25, 2026
ac5dce1
buffer Lite Ragel tokens
Aug 26, 2026
ad5ed18
vibecoded html decoder to replace devkit's
Aug 26, 2026
92dd7f9
html writer in markup.lite, directly to Buffer.t
Aug 26, 2026
1fe18f2
writer test for markup.lite
Aug 26, 2026
4a8a94b
format code in src/lite
Aug 26, 2026
b9c53e7
`make format` target
Aug 26, 2026
14d0e01
ocamlformat ignore src/
Aug 26, 2026
07b2167
handle depth_limit in markup.lite
Aug 26, 2026
d3515aa
format lite
Aug 26, 2026
7d3de56
add afl fuzz
Aug 26, 2026
d276639
other differential test that should be more sensitive to perf diffs
Aug 26, 2026
671359b
perf: fast path in html entity decoder
Aug 26, 2026
ef31253
perf: html writer: more fast path in escaping
Aug 26, 2026
f219ea1
more tests
Aug 26, 2026
762d2f1
improve test-lite runner to run only oracle or lite on demand
Aug 26, 2026
fff37ac
perf: pull Lite parser tokens directly
Aug 26, 2026
0437057
format
Aug 26, 2026
e30c7b9
perf: html parser: replace assoc lists with pattern matching
Aug 27, 2026
de217c5
perf: use a regular variant
Aug 27, 2026
852ddd7
perf: use regular variant for token type
Aug 27, 2026
9f425b5
use regular variants in more places in src/lite
Aug 27, 2026
73119fb
format
Aug 27, 2026
b7961a1
move `markup` library to src/baseline
Aug 27, 2026
dbfe29b
regression test for entity parsing discrepancy
Aug 27, 2026
4eb33af
fix: entity decoder must accept alphanum
Aug 27, 2026
2cdcefb
perf: use a Str_tbl for entity decoder; also remove poly variants
Aug 27, 2026
c2c8364
format test/lite too
Aug 27, 2026
a5d53c2
regression tests for bugs found via fuzzing
Aug 27, 2026
9b4c285
fix discrepancy in recovery when meeting garbage tags
Aug 27, 2026
46d5473
fix: handle undecodable chunk of text the same as baseline
Aug 27, 2026
7af748d
fix: handle corner case when element stack is empty
Aug 27, 2026
a291fa2
test
Aug 27, 2026
2590397
fuzzing: change what's tested
Aug 27, 2026
94b680e
add new regression tests from fuzzing changes
Aug 27, 2026
66ffebb
fix: replace invalid utf8 in lite input with U+FFFD like src/baseline
Aug 27, 2026
d00798c
use uutf to validate utf8 input
Aug 27, 2026
aeb55b9
fix: emit </br> end tags like src/baseline
Aug 27, 2026
15e0788
fix: parse tag names with non-ident characters like src/baseline
Aug 27, 2026
9a44e20
fix: decode character references exactly like src/baseline
Aug 27, 2026
da3c46d
fix: normalize CR and CRLF to LF like src/baseline
Aug 27, 2026
23f14a1
fix: strip BOM like src/baseline
Aug 27, 2026
7b143b5
fix: emit comments, bogus comments, and doctypes like src/baseline
Aug 27, 2026
05bf31a
fix: attribute source order and duplicate handling like src/baseline
Aug 27, 2026
b48255c
fix: self-closing start tags carry the flag like src/baseline
Aug 27, 2026
e379f81
fix: svg and math fragment contexts are foreign like src/baseline
Aug 27, 2026
83056bd
fix: scan script, style, title, and textarea bodies like src/baseline
Aug 27, 2026
c269f45
fix: keep whitespace-only table text from String tokens like src/base…
Aug 27, 2026
068b9e8
fix: parse start tag attributes with src/baseline tokenizer states
Aug 27, 2026
7307132
fix: honor tree-builder tokenizer state feedback like src/baseline
Aug 27, 2026
2d6586a
test: wire up lite_dump_signals with fragment support
Aug 27, 2026
382cf9d
test: run lite baseline regressions in dune runtest
Aug 27, 2026
8385918
Raise the minimum OCaml version to 4.14
Aug 27, 2026
2928e17
Use String.is_valid_utf_8 for Lite input validation
Aug 27, 2026
c6b2c1a
Preserve internal BOM characters during HTML preprocessing
Aug 27, 2026
0085f0b
Add BOM preprocessing regression tests
Aug 27, 2026
809abdd
Treat form feed as whitespace in Lite text tokens
Aug 27, 2026
4c41008
Skip formatting reconstruction for discarded NUL text
Aug 27, 2026
96e744c
Parse CDATA sections in foreign content
Aug 27, 2026
6bf36a9
Match foreign fragment rawtext candidate recovery
Aug 27, 2026
1d70542
Lowercase the doctype keyword lookahead pushback like baseline
Aug 27, 2026
1d61a91
Drop rawtext end-tag candidates only when baseline's state reset fires
Aug 27, 2026
f07f8ab
Add Lite parser token entry point
Aug 28, 2026
7b66269
Add strict HtmlStream parser comparison
Aug 28, 2026
2f2b125
Fuzz strict adapted parser streams
Aug 28, 2026
f9956b0
Restore stable generated Lite scanner
Aug 28, 2026
a1a6152
Use baseline native parser for regressions
Aug 28, 2026
7a1a4d7
Separate native and explicit text run semantics
Aug 28, 2026
b2bffb6
Improve strict parser mismatch diagnostics
Aug 28, 2026
e9c51df
Report CPU throughput and collections in benchmarks
Aug 28, 2026
7036135
Add parser-only adapted token benchmark
Aug 28, 2026
2da6a34
Measure residual allocations in corpus benchmarks
Aug 28, 2026
9f5f2e5
Format Lite tokenizer support
Aug 28, 2026
2443b31
Honor declared legacy HTML encodings
Aug 28, 2026
f35dc11
fix is_whitespace_only in baseline html parser
Aug 28, 2026
9ceab7d
format
Aug 28, 2026
0802567
fuzz oracle: sanitize `\x00` in tokenizer
Aug 28, 2026
c75791a
Run strict parser corpus checks in test-lite
Aug 28, 2026
473be3d
Fix foreign reentry and row recovery
Aug 28, 2026
226daf0
Document the HtmlStream adapter policy
Aug 28, 2026
b6a0548
Fast-path native data text scanning in tokenizer
Aug 28, 2026
1f8ce8d
Record misnested SVG parser loop
Aug 28, 2026
9608330
fix (baseline): prevent infinite loop
Aug 28, 2026
cb628ec
Format Lite oracle
Aug 28, 2026
902ddd2
Fix baseline buffered foreign text runs
Aug 28, 2026
2339d6f
Skip known baseline invariant failures in the fuzzer
Aug 28, 2026
18d7f61
Use successful baseline parses as fuzz oracles
Aug 28, 2026
6e29dd6
Add selectable native Lite fuzzing
Aug 28, 2026
9e0040a
Collect fuzz parser errors after signals
Aug 28, 2026
6e37297
add explicit UTF-8 mode to Markup_lite
Aug 28, 2026
47993c7
Remove dead test_ragel_parser.ml
Sep 1, 2026
cac28c3
perf: reset mark to allow fast path to run again
Sep 1, 2026
8f8de5d
support BOM-detected UTF-16 in lite
Sep 1, 2026
f1d1a19
fix: CI: install missing opam deps
Sep 1, 2026
52250f8
better encoding detection, with BOM and meta+charset
c-cube Sep 2, 2026
3a79c76
add Markup_lite.to_html_string
c-cube Sep 2, 2026
0f15523
fix handling of empty lines
c-cube Sep 2, 2026
39ed8a2
more regression tests found by shadowing in prod
c-cube Sep 2, 2026
a8ffdff
fix a diff between baseline and lite
c-cube Sep 3, 2026
584d967
regression tests for fuzzer-found differences
c-cube Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
scratch/
_opam/
_build/
_build-*/
_fuzz/
_tools/
bisect*.out
_coverage
*.install
Expand Down
92 changes: 89 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(env
(afl
(flags
(:standard -warn-error -A))))
2 changes: 1 addition & 1 deletion markup-lwt.opam
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ depends: [
"dune" {>= "2.7.0"}
"lwt"
"markup"
"ocaml" {>= "4.03.0"}
"ocaml" {>= "4.14.0"}
]

build: [
Expand Down
4 changes: 3 additions & 1 deletion markup.opam
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/.ocamlformat-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ml
*.mli
common/**
entities/**
lwt/**
lwt_unix/**
115 changes: 25 additions & 90 deletions src/common.ml → src/baseline/common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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-->" 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 "<!DOCTYPE%s>" 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 "<?xml version=\"%s\">" 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 ->
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/dune → src/baseline/dune
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

File renamed without changes.
6 changes: 6 additions & 0 deletions src/baseline/entities.ml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions src/baseline/error.ml
Original file line number Diff line number Diff line change
@@ -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 ()
Loading
Loading