refactor(xml): consume lexmount fork instead of vendored parser

Consume lexmount/html5ever at 4574311c63e79bba7d4ddb98cb0562d8dad499ee and remove the XML vendor tree and obsolete ignore exceptions. All 11 production XML source files match the former vendor byte-for-byte; HTML, XML and Stylo still share registry markup5ever 0.40.0 and web_atoms 0.3.0.

Rebased the style stack onto origin/main at ebae4ca6ca.

Validation: Release; cargo fmt --all; strict workspace/all-target/all-feature Clippy; cargo nextest run --no-fail-fast (20413 passed, 18 skipped); 312 paired XML WPT executions and 840 Chromium overflow comparisons with no differences. Full WPT and nine status/subtest-change replays found no new stable regression.
This commit is contained in:
ldm0
2026-10-21 17:55:02 +08:00
committed by Donough Liu
parent a4a188d503
commit fa293e83e2
25 changed files with 4 additions and 4461 deletions
-2
View File
@@ -12,8 +12,6 @@
!/vendor/v8-152.2.0/**
!/vendor/deno_v8-0.3.0/
!/vendor/deno_v8-0.3.0/**
!/vendor/xml5ever-0.39.0/
!/vendor/xml5ever-0.39.0/**
/.moli
__pycache__
*.pyc
Generated
+3 -2
View File
@@ -3159,7 +3159,7 @@ dependencies = [
"moli-script",
"moli-stylesheet-blocking",
"url",
"xml5ever 0.40.0",
"xml5ever 0.40.1",
"xmlparser",
]
@@ -6863,7 +6863,8 @@ dependencies = [
[[package]]
name = "xml5ever"
version = "0.40.0"
version = "0.40.1"
source = "git+https://github.com/lexmount/html5ever?rev=4574311c63e79bba7d4ddb98cb0562d8dad499ee#4574311c63e79bba7d4ddb98cb0562d8dad499ee"
dependencies = [
"log",
"markup5ever 0.40.0",
+1 -2
View File
@@ -92,7 +92,6 @@ members = [
"moli-http-cache",
"moli-wpt-compat"
]
exclude = ["vendor/xml5ever-0.40.0"]
[workspace.package]
license = "MIT OR Apache-2.0"
@@ -103,7 +102,7 @@ fontique = { git = "https://github.com/lexmount/parley", rev = "a18cfc3ba6638d44
# Share fontations between shaping, raster rendering and PDF font subsetting.
subsetter = { git = "https://github.com/lexmount/subsetter", rev = "8d3815245835af309559c3b4a872d6a0a54913a9" }
url = { git = "https://github.com/lexmount/rust-url", rev = "ca9fcad2b99f20c30c28a618ad78410f6fd31161" }
xml5ever = { path = "vendor/xml5ever-0.40.0" }
xml5ever = { git = "https://github.com/lexmount/html5ever", rev = "4574311c63e79bba7d4ddb98cb0562d8dad499ee" }
curl = { git = "https://github.com/lexmount/curl-rust", rev = "9d40ca90d8f19ccfd11afe7dd49bc32aab79435c" }
curl-sys = { git = "https://github.com/lexmount/curl-rust", rev = "9d40ca90d8f19ccfd11afe7dd49bc32aab79435c" }
cookie = { git = "https://github.com/ldm0/cookie-rs", branch = "priority" }
-70
View File
@@ -1,70 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.85"
name = "xml5ever"
version = "0.40.0"
authors = ["The xml5ever project developers"]
build = false
exclude = ["xml5lib-tests/*"]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Push based streaming parser for XML."
homepage = "https://github.com/servo/html5ever/blob/main/xml5ever/README.md"
documentation = "https://docs.rs/xml5ever"
readme = "README.md"
keywords = [
"xml",
"xml5",
"parser",
"parsing",
]
categories = [
"parser-implementations",
"web-programming",
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/servo/html5ever"
[features]
serde = ["markup5ever/serde"]
trace_tokenizer = []
[lib]
name = "xml5ever"
path = "src/lib.rs"
[[example]]
name = "simple_xml_tokenizer"
path = "examples/simple_xml_tokenizer.rs"
[[example]]
name = "xml_tokenizer"
path = "examples/xml_tokenizer.rs"
[[bench]]
name = "xml5ever"
path = "benches/xml5ever.rs"
harness = false
[dependencies.log]
version = "0.4"
[dependencies.markup5ever]
version = "0.40"
[dev-dependencies.criterion]
version = "0.8"
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-25
View File
@@ -1,25 +0,0 @@
Copyright (c) 2014 The html5ever Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
-14
View File
@@ -1,14 +0,0 @@
# xml5ever 0.40.0
This directory contains the source of the crates.io `xml5ever` 0.40.0 release.
It is patched locally because Moli needs the tokenizer to preserve CDATA as a
distinct token, the XML tree builder to surface CDATA nodes to its sink, and
namespace declarations to remain available as DOM attributes after binding.
Numeric character references use the XML 1.0 Char production, preserving
referenced carriage returns and C1 values without HTML's Windows-1252 mapping.
The upstream source is licensed under MIT or Apache-2.0; see `LICENSE-MIT` and
`LICENSE-APACHE` in this directory.
Updated with the crates.io 0.40.0 release. The CDATA, namespace attribute,
and XML character-reference patches above are retained on the new source.
-72
View File
@@ -1,72 +0,0 @@
# xml5ever
![http://www.apache.org/licenses/LICENSE-2.0](https://img.shields.io/badge/license-Apache-blue.svg) ![https://opensource.org/licenses/MIT](https://img.shields.io/badge/license-MIT-blue.svg)
[![Docs.rs](https://docs.rs/xml5ever/badge.svg)](https://docs.rs/xml5ever)
[![](https://img.shields.io/crates/v/xml5ever.svg)](https://crates.io/crates/xml5ever)
[API documentation](https://docs.rs/xml5ever)
**Warning:** This library is alpha quality, so no guarantees are given.
This crate provides a push based XML parser library that trades well-formedness for error recovery.
xml5ever is based largely on [html5ever](https://github.com/servo/html5ever) parser, so if you have experience with html5ever you will be familiar with xml5ever.
The library is dual licensed under MIT and Apache license.
# Why you should use xml5ever
Main use case for this library is when XML is badly formatted, usually from bad XML
templates. XML5 tries to handle most common errors, in a manner similar to HTML5.
## When you should use it?
- You aren't interested in well-formed documents.
- You need to get some info from your data even if it has errors (although not all possible errors are handled).
- You want to features like character references or XML namespaces.
## When you shouldn't use it
- You need to have your document validated.
- You require DTD support.
- You require an easy to use parser, with lots of extensions (e.g. XPath, XQuery).
- You require a battle tested, industry proven solution.
# Installation
Add xml5ever as a dependency:
```bash
cargo add xml5ever
```
# Getting started
Here is a very simple RcDom backed parser:
```rust
let input = "<xml></xml>".to_tendril();
// To parse XML into a tree form, we need a TreeSink
// luckily xml5ever comes with a static RC backed tree represetation.
let dom: RcDom = parse(std::iter::once(input), Default::default());
// Do something with dom
```
The thing that does actual parsing is the `parse` function. It expects an iterator that can be converted into `StrTendril`, so you can use `std::iter::once(input)` or `Some(input).into_iter()` (where `input` is `StrTendril` like structure).
# Working on xml5ever
To build examples and tests you need to do something along the lines of:
```rust
git submodule update --init # to fetch xml5lib-tests
cargo build
cargo test
```
This will fetch tests from outside repository and it will invoke cargo to
build and test the crate. If you need docs checkout either [API docs](https://docs.rs/xml5ever) or run `cargo docs`
to generate documentation.
-77
View File
@@ -1,77 +0,0 @@
#[macro_use]
extern crate criterion;
extern crate markup5ever;
extern crate xml5ever;
use std::fs;
use std::path::PathBuf;
use criterion::Criterion;
use markup5ever::buffer_queue::BufferQueue;
use xml5ever::tendril::*;
use xml5ever::tokenizer::{ProcessResult, Token, TokenSink, XmlTokenizer};
struct Sink;
impl TokenSink for Sink {
type Handle = ();
fn process_token(&self, token: Token) -> ProcessResult<()> {
// Don't use the token, but make sure we don't get
// optimized out entirely.
std::hint::black_box(token);
ProcessResult::Continue
}
}
fn run_bench(c: &mut Criterion, name: &str) {
let mut path = PathBuf::from("./");
path.push("data/bench/");
path.push(name);
let mut file = fs::File::open(&path).expect("can't open file");
// Read the file and treat it as an infinitely repeating sequence of characters.
let mut file_input = ByteTendril::new();
file.read_to_tendril(&mut file_input)
.expect("can't read file");
let file_input: StrTendril = file_input.try_reinterpret().unwrap();
let size = file_input.len();
let mut stream = file_input.chars().cycle();
// Break the input into chunks of 1024 chars (= a few kB).
// This simulates reading from the network.
let mut input = vec![];
let mut total = 0usize;
while total < size {
// The by_ref() call is important, otherwise we get wrong results!
// See rust-lang/rust#18045.
let sz = std::cmp::min(1024, size - total);
input.push(stream.by_ref().take(sz).collect::<String>().to_tendril());
total += sz;
}
let test_name = format!("xml tokenizing {name}");
c.bench_function(&test_name, move |b| {
b.iter(|| {
let tok = XmlTokenizer::new(Sink, Default::default());
let buffer = BufferQueue::default();
// We are doing clone inside the bench function, this is not ideal, but possibly
// necessary since our iterator consumes the underlying buffer.
for buf in input.clone().into_iter() {
buffer.push_back(buf);
let _ = tok.feed(&buffer);
}
let _ = tok.feed(&buffer);
tok.end();
})
});
}
fn xml5ever_benchmarks(c: &mut Criterion) {
run_bench(c, "strong.xml");
}
criterion_group!(benches, xml5ever_benchmarks);
criterion_main!(benches);
-1
View File
@@ -1 +0,0 @@
<strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong><strong>
-223
View File
@@ -1,223 +0,0 @@
# Examples
The examples have been designed with [`cargo-script`](https://github.com/DanielKeep/cargo-script) in mind.
Here I'll just give broad overview how to install [`cargo script`] for Rust 1.5. For more details, check out [cargo-script repository](https://github.com/DanielKeep/cargo-script).
cargo install cargo-script
# Token printer
The basis of xml5ever is its tokenizer and tree builder. Roughly speaking tokenizer
takes input and returns a set of tokens like comment, processing instruction, start
tag, end tag, etc.
First let's define our dependencies:
```toml
[dependencies]
xml5ever = "0.2.0"
tendril = "0.1.3"
```
With dependencies declared, we can now make a simple tokenizer sink. First step is to
define a [`TokenSink`](https://docs.rs/xml5ever/latest/xml5ever/tokenizer/trait.TokenSink.html). [`TokenSink`](https://docs.rs/xml5ever/latest/xml5ever/tokenizer/trait.TokenSink.html) are traits that received stream of [`Tokens`](https://docs.rs/xml5ever/latest/xml5ever/tokenizer/enum.Token.html).
In our case we'll define a unit struct (i.e. a struct without any fields).
```rust
struct SimpleTokenPrinter;
```
To make `SimpleTokenPrinter` a [`TokenSink`](https://docs.rs/xml5ever/latest/xml5ever/tokenizer/trait.TokenSink.html), we need to implement [process_token](https://docs.rs/xml5ever/latest/xml5ever/tokenizer/trait.TokenSink.html#tymethod.process_token) method.
```rust
impl TokenSink for SimpleTokenPrinter {
fn process_token(&mut self, token: Token) {
match token {
CharacterTokens(b) => {
println!("TEXT: {}", &*b);
},
NullCharacterToken => print!("NULL"),
TagToken(tag) => {
println!("{:?} {} ", tag.kind, &*tag.name.local);
},
ParseError(err) => {
println!("ERROR: {}", err);
},
PIToken(Pi{ref target, ref data}) => {
println!("PI : <?{} {}?>", &*target, &*data);
},
CommentToken(ref comment) => {
println!("<!--{:?}-->", &*comment);
},
EOFToken => {
println!("EOF");
},
DoctypeToken(Doctype{ref name, ref public_id, ..}) => {
println!("<!DOCTYPE {:?} {:?}>", &*name, &*public_id);
}
}
}
}
```
Now, we need some input to process. For input we'll use `stdin`. However, xml5ever `tokenize_to` method only takes `StrTendril`. So we need to construct a
[`ByteTendril`](https://docs.rs/tendril/latest/tendril/type.ByteTendril.html) using `ByteTendril::new()`, then read the `stdin` using [`read_to_tendril`](https://docs.rs/tendril/latest/tendril/trait.ReadExt.html#tymethod.read_to_tendril) extension.
Once that is set, to make `SimpleTokenPrinter` parse the input, call,
`tokenize_to` with it as the first parameter, input wrapped in Option for second parameter and XmlToke.
```rust
fn main() {
let sink = SimpleTokenPrinter;
// We need a ByteTendril to read a file
let mut input = ByteTendril::new();
// Using SliceExt.read_to_tendril we read stdin
io::stdin().read_to_tendril(&mut input).unwrap();
// For xml5ever we need StrTendril, so we reinterpret it
// into StrTendril.
//
// You might wonder, how does `try_reinterpret` know we
// need StrTendril and the answer is type inference based
// on `tokenize_xml_to` signature.
let input = input.try_reinterpret().unwrap();
// Here we create and run tokenizer
let mut tok = XmlTokenizer::new(sink, Default::default());
// We pass input to parsed.
tok.feed(input);
// tok.end must be invoked for final bytes to be processed.
tok.end();
}
```
NOTE: `unwrap` causes panic, it's only OK to use in simple examples.
For full source code check out: [`examples/simple_xml_tokenizer.rs`](https://github.com/servo/html5ever/blob/main/xml5ever/examples/simple_xml_tokenizer.rs)
Once we have successfully compiled the example we run the example with inline
xml
```bash
cargo script simple_xml_tokenizer.rs <<< "<xml>Text with <b>bold words</b>!</xml>"
```
or by sending an [`examples/example.xml`](https://github.com/servo/html5ever/blob/main/xml5ever/examples/example.xml) located in same folder as examples.
```bash
cargo script simple_xml_tokenizer.rs < example.xml
```
# Tree printer
To actually get an XML document tree from the xml5ever, you need to use a `TreeSink`.
`TreeSink` is in many way similar to the TokenSink. Basically, TokenSink takes data
and returns list of tokens, while TreeSink takes tokens and returns a tree of parsed
XML document. Do note, that this is a simplified explanation and consult
documentation for more info.
Ok, with that in mind, let's build us a TreePrinter. For example if we get an XML
file like:
```xml
<student>
<first-name>Bobby</first-name>
<last-name>Tables</last-name>
</student>
```
We'd want a structure similar to this:
```
#document
student
first-name
#text Bobby
last-name
#text Tables
```
We won't print anything other than element names and text fields. So comments,
doctypes and other such elements are ignored.
First part is similar to making SimpleTokenPrinter:
```rust
// We need to allocate an input tendril for xml5ever
let mut input = ByteTendril::new();
// Using SliceExt.read_to_tendril functions we can read stdin
io::stdin().read_to_tendril(&mut input).unwrap();
let input = input.try_reinterpret().unwrap();
```
This time, we need an implementation of [`TreeSink`](https://docs.rs/xml5ever/latest/xml5ever/tree_builder/trait.TreeSink.html). xml5ever comes with a
built-in `TreeSink` implementation called [`RcDom`](https://docs.rs/markup5ever_rcdom/latest/markup5ever_rcdom/struct.RcDom.html). To process input into
a `TreeSink` we use the following line:
```rust
let dom: RcDom = parse(one_input(input), Default::default());
```
Let's analyze it a bit. First there is `let dom: RcDom`. We need this part,
because the type inferencer can't infer which TreeSink implementation we mean
in this scenario.
Function [`one_input`](https://ygg01.github.io/docs/xml5ever/xml5ever/fn.one_input.html) is a convenience function that turns any value into an iterator. In this case
it converts a StrTendril into an Iterator over itself.
Ok, so now that we parsed our tree what with it? Well, for that we might need some
kind of function that will help us traverse it. We shall call that function `walk`.
```rust
fn walk(prefix: &str, handle: Handle) {
let node = handle.borrow();
// We print out the prefix before we start
print!("{}", prefix);
// We are only interested in following nodes:
// Document, Text and Element, so our match
// reflects that.
match node.node {
Document
=> println!("#document"),
Text(ref text) => {
println!("#text {}", text.escape_default())
},
Element(ref name, _) => {
println!("{}", name.local);
},
_ => {},
}
// We increase indent in child nodes
let new_indent = {
let mut temp = String::new();
temp.push_str(prefix);
temp.push_str(" ");
temp
};
for child in node.children.iter()
// In order to avoid weird indentation, we filter
// only Text/Element nodes.
// We don't need to filter Document since its guaranteed
// child elements don't contain documents
.filter(|child| match child.borrow().node {
Text(_) | Element (_, _) => true,
_ => false,
}
) {
// Recursion - Yay!
walk(&new_indent, child.clone());
}
}
```
For full source code check out: [`examples/xml_tree_printer.rs`](https://github.com/servo/html5ever/blob/main/rcdom/examples/xml_tree_printer.rs)
-3
View File
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml PUBLIC "http://www.wc3.org/">
<student><first-name>Bobby</first-name><last-name>Tables</last-name></student>
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env run-cargo-script
//! This is a regular crate doc comment, but it also contains a partial
//! Cargo manifest. Note the use of a *fenced* code block, and the
//! `cargo` "language".
//!
//! ```cargo
//! [dependencies]
//! xml5ever = "0.1.1"
//! tendril = "0.1.3"
//! markup5ever = "0.7.4"
//! ```
extern crate markup5ever;
extern crate xml5ever;
use std::io;
use markup5ever::buffer_queue::BufferQueue;
use xml5ever::tendril::{ByteTendril, ReadExt};
use xml5ever::tokenizer::{Doctype, Pi, ProcessResult, Token, TokenSink, XmlTokenizer};
struct SimpleTokenPrinter;
impl TokenSink for SimpleTokenPrinter {
type Handle = ();
fn process_token(&self, token: Token) -> ProcessResult<()> {
match token {
Token::Characters(b) => {
println!("TEXT: {}", &*b);
},
Token::Cdata(b) => {
println!("CDATA: {}", &*b);
},
Token::NullCharacter => print!("NULL"),
Token::Tag(tag) => {
println!("{:?} {} ", tag.kind, &*tag.name.local);
},
Token::ParseError(err) => {
println!("ERROR: {err}");
},
Token::ProcessingInstruction(Pi {
ref target,
ref data,
}) => {
println!("PI : <?{target} {data}?>");
},
Token::Comment(ref comment) => {
println!("<!--{comment:?}-->");
},
Token::EndOfFile => {
println!("EOF");
},
Token::Doctype(Doctype {
ref name,
ref public_id,
..
}) => {
println!("<!DOCTYPE {name:?} {public_id:?}>");
},
};
ProcessResult::Continue
}
}
fn main() {
// Our implementation of TokenSink
let sink = SimpleTokenPrinter;
// We need a ByteTendril to read a file
let mut input = ByteTendril::new();
// Using SliceExt.read_to_tendril we can read stdin
io::stdin().read_to_tendril(&mut input).unwrap();
// For xml5ever we need StrTendril, so we reinterpret it
// into StrTendril.
// Load input into BufferQueue
let input_buffer = BufferQueue::default();
input_buffer.push_back(input.try_reinterpret().unwrap());
// Here we create and run tokenizer
let tok = XmlTokenizer::new(sink, Default::default());
let _ = tok.feed(&input_buffer);
tok.end();
}
-119
View File
@@ -1,119 +0,0 @@
#!/usr/bin/env run-cargo-script
//! This is a regular crate doc comment, but it also contains a partial
//! Cargo manifest. Note the use of a *fenced* code block, and the
//! `cargo` "language".
//!
//! ```cargo
//! [dependencies]
//! xml5ever = "0.2.0"
//! tendril = "0.1.3"
//! markup5ever = "0.7.4"
//! ```
extern crate markup5ever;
extern crate xml5ever;
use std::cell::Cell;
use std::io;
use markup5ever::buffer_queue::BufferQueue;
use xml5ever::tendril::{ByteTendril, ReadExt};
use xml5ever::tokenizer::{
EmptyTag, EndTag, Pi, ProcessResult, ShortTag, StartTag, Token, TokenSink, XmlTokenizer,
XmlTokenizerOpts,
};
#[derive(Clone)]
struct TokenPrinter {
in_char_run: Cell<bool>,
}
impl TokenPrinter {
fn is_char(&self, is_char: bool) {
match (self.in_char_run.get(), is_char) {
(false, true) => print!("CHAR : \""),
(true, false) => println!("\""),
_ => (),
}
self.in_char_run.set(is_char);
}
fn do_char(&self, c: char) {
self.is_char(true);
print!("{}", c.escape_default().collect::<String>());
}
}
impl TokenSink for TokenPrinter {
type Handle = ();
fn process_token(&self, token: Token) -> ProcessResult<()> {
match token {
Token::Characters(b) => {
for c in b.chars() {
self.do_char(c);
}
},
Token::Cdata(b) => {
self.is_char(false);
println!("CDATA: {}", &*b);
},
Token::NullCharacter => self.do_char('\0'),
Token::Tag(tag) => {
self.is_char(false);
// This is not proper HTML serialization, of course.
match tag.kind {
StartTag => print!("TAG : <\x1b[32m{}\x1b[0m", tag.name.local),
EndTag => print!("END TAG : <\x1b[31m/{}\x1b[0m", tag.name.local),
ShortTag => print!("Short TAG : <\x1b[31m/{}\x1b[0m", tag.name.local),
EmptyTag => print!("Empty TAG : <\x1b[31m{}\x1b[0m", tag.name.local),
}
for attr in tag.attrs.iter() {
print!(
" \x1b[36m{}\x1b[0m='\x1b[34m{}\x1b[0m'",
attr.name.local, attr.value
);
}
if tag.kind == EmptyTag {
print!("/");
}
println!(">");
},
Token::ParseError(err) => {
self.is_char(false);
println!("ERROR: {err}");
},
Token::ProcessingInstruction(Pi { target, data }) => {
self.is_char(false);
println!("PI : <?{target:?} {data:?}?>");
},
_ => {
self.is_char(false);
println!("OTHER: {token:?}");
},
};
ProcessResult::Continue
}
}
fn main() {
let sink = TokenPrinter {
in_char_run: Cell::new(false),
};
let mut input = ByteTendril::new();
io::stdin().read_to_tendril(&mut input).unwrap();
let input_buffer = BufferQueue::default();
input_buffer.push_back(input.try_reinterpret().unwrap());
let tok = XmlTokenizer::new(
sink,
XmlTokenizerOpts {
profile: true,
exact_errors: true,
..Default::default()
},
);
let _ = tok.feed(&input_buffer);
tok.end();
tok.sink.is_char(false);
}
-91
View File
@@ -1,91 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::tokenizer::{XmlTokenizer, XmlTokenizerOpts};
use crate::tree_builder::{XmlTreeBuilder, XmlTreeBuilderOpts, XmlTreeSink};
use std::borrow::Cow;
use crate::tendril;
use crate::tendril::stream::{TendrilSink, Utf8LossyDecoder};
use crate::tendril::StrTendril;
use markup5ever::buffer_queue::BufferQueue;
use markup5ever::TokenizerResult;
/// All-encompasing parser setting structure.
#[derive(Clone, Default)]
pub struct XmlParseOpts {
/// Xml tokenizer options.
pub tokenizer: XmlTokenizerOpts,
/// Xml tree builder .
pub tree_builder: XmlTreeBuilderOpts,
}
/// Parse and send results to a `TreeSink`.
///
/// ## Example
///
/// ```ignore
/// let mut sink = MySink;
/// parse_document(&mut sink, iter::once(my_str), Default::default());
/// ```
pub fn parse_document<Sink>(sink: Sink, opts: XmlParseOpts) -> XmlParser<Sink>
where
Sink: XmlTreeSink,
{
let tb = XmlTreeBuilder::new(sink, opts.tree_builder);
let tok = XmlTokenizer::new(tb, opts.tokenizer);
XmlParser {
tokenizer: tok,
input_buffer: BufferQueue::default(),
}
}
/// An XML parser,
/// ready to receive Unicode input through the `tendril::TendrilSink` trait’s methods.
pub struct XmlParser<Sink>
where
Sink: XmlTreeSink,
{
/// Tokenizer used by XmlParser.
pub tokenizer: XmlTokenizer<XmlTreeBuilder<Sink::Handle, Sink>>,
/// Input used by XmlParser.
pub input_buffer: BufferQueue,
}
impl<Sink: XmlTreeSink> TendrilSink<tendril::fmt::UTF8> for XmlParser<Sink> {
type Output = Sink::Output;
fn process(&mut self, t: StrTendril) {
self.input_buffer.push_back(t);
// FIXME: Properly support </script> somehow.
while let TokenizerResult::Script(_) = self.tokenizer.feed(&self.input_buffer) {}
}
// FIXME: Is it too noisy to report every character decoding error?
fn error(&mut self, desc: Cow<'static, str>) {
self.tokenizer.sink.sink.parse_error(desc)
}
fn finish(self) -> Self::Output {
self.tokenizer.end();
self.tokenizer.sink.sink.finish()
}
}
impl<Sink: XmlTreeSink> XmlParser<Sink> {
/// Wrap this parser into a `TendrilSink` that accepts UTF-8 bytes.
///
/// Use this when your input is bytes that are known to be in the UTF-8 encoding.
/// Decoding is lossy, like `String::from_utf8_lossy`.
#[allow(clippy::wrong_self_convention)]
pub fn from_utf8(self) -> Utf8LossyDecoder<Self> {
Utf8LossyDecoder::new(self)
}
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! This crate provides a push based XML parser library that
//! adheres to XML5 specification. In other words this library
//! trades well-formedness for error recovery.
//!
//! The idea behind this, was to minimize number of errors from
//! tools that generate XML (e.g. `&#83` won't just return `&#83`
//! as text, but will parse it into `S` ).
//! You can check out full specification [here](https://ygg01.github.io/xml5_draft/).
//!
//! What this library provides is a solid XML parser that can:
//!
//! * Parse somewhat erroneous XML input
//! * Provide support for [Numeric character references](https://en.wikipedia.org/wiki/Numeric_character_reference).
//! * Provide partial [XML namespace](http://www.w3.org/TR/xml-names11/) support.
//! * Provide full set of SVG/MathML entities
//!
//! What isn't in scope for this library:
//!
//! * Document Type Definition parsing - this is pretty hard to do right and nowadays, its used
//!
#![crate_name = "xml5ever"]
#![crate_type = "dylib"]
#![allow(unexpected_cfgs)]
#![deny(missing_docs)]
pub use markup5ever::*;
pub(crate) mod macros;
/// Driver
pub mod driver;
/// Serializer for XML5.
pub mod serialize;
/// XML5 tokenizer - converts input into tokens
pub mod tokenizer;
/// XML5 tree builder - converts tokens into a tree like structure
pub mod tree_builder;
-18
View File
@@ -1,18 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
macro_rules! time {
($e:expr) => {{
let t0 = ::std::time::Instant::now();
let result = $e;
let dt = t0.elapsed().as_nanos() as u64;
(result, dt)
}};
}
pub(crate) use time;
-216
View File
@@ -1,216 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::tree_builder::NamespaceMap;
use crate::QualName;
pub use markup5ever::serialize::{AttrRef, Serialize, Serializer, TraversalScope};
use std::io::{self, Write};
#[derive(Clone)]
/// Struct for setting serializer options.
pub struct SerializeOpts {
/// Serialize the root node? Default: ChildrenOnly
pub traversal_scope: TraversalScope,
}
impl Default for SerializeOpts {
fn default() -> SerializeOpts {
SerializeOpts {
traversal_scope: TraversalScope::ChildrenOnly(None),
}
}
}
/// Method for serializing generic node to a given writer.
pub fn serialize<Wr, T>(writer: Wr, node: &T, opts: SerializeOpts) -> io::Result<()>
where
Wr: Write,
T: Serialize,
{
let mut ser = XmlSerializer::new(writer);
node.serialize(&mut ser, opts.traversal_scope)
}
/// Struct used for serializing nodes into a text that other XML
/// parses can read.
///
/// Serializer contains a set of functions (start_elem, end_elem...)
/// that make parsing nodes easier.
pub struct XmlSerializer<Wr> {
writer: Wr,
namespace_stack: NamespaceMapStack,
}
#[derive(Debug)]
struct NamespaceMapStack(Vec<NamespaceMap>);
impl NamespaceMapStack {
fn new() -> NamespaceMapStack {
NamespaceMapStack(vec![])
}
fn push(&mut self, namespace: NamespaceMap) {
self.0.push(namespace);
}
fn pop(&mut self) {
self.0.pop();
}
}
/// Writes given text into the Serializer, escaping it,
/// depending on where the text is written inside the tag or attribute value.
///
/// For example
///```text
/// <tag>'&-quotes'</tag> becomes <tag>'&amp;-quotes'</tag>
/// <tag = "'&-quotes'"> becomes <tag = "&apos;&amp;-quotes&apos;"
///```
fn write_to_buf_escaped<W: Write>(writer: &mut W, text: &str, attr_mode: bool) -> io::Result<()> {
for c in text.chars() {
match c {
'&' => writer.write_all(b"&amp;"),
'\'' if attr_mode => writer.write_all(b"&apos;"),
'"' if attr_mode => writer.write_all(b"&quot;"),
'<' if !attr_mode => writer.write_all(b"&lt;"),
'>' if !attr_mode => writer.write_all(b"&gt;"),
c => writer.write_fmt(format_args!("{c}")),
}?;
}
Ok(())
}
#[inline]
fn write_qual_name<W: Write>(writer: &mut W, name: &QualName) -> io::Result<()> {
if let Some(ref prefix) = name.prefix {
writer.write_all(prefix.as_bytes())?;
writer.write_all(b":")?;
}
writer.write_all(name.local.as_bytes())?;
Ok(())
}
impl<Wr: Write> XmlSerializer<Wr> {
/// Creates a new Serializier from a writer and given serialization options.
pub fn new(writer: Wr) -> Self {
XmlSerializer {
writer,
namespace_stack: NamespaceMapStack::new(),
}
}
#[inline(always)]
fn qual_name(&mut self, name: &QualName) -> io::Result<()> {
self.find_or_insert_ns(name);
write_qual_name(&mut self.writer, name)
}
#[inline(always)]
fn qual_attr_name(&mut self, name: &QualName) -> io::Result<()> {
self.find_or_insert_ns(name);
write_qual_name(&mut self.writer, name)
}
fn find_uri(&self, name: &QualName) -> bool {
let mut found = false;
for stack in self.namespace_stack.0.iter().rev() {
if let Some(Some(el)) = stack.get(&name.prefix) {
found = *el == name.ns;
break;
}
}
found
}
fn find_or_insert_ns(&mut self, name: &QualName) {
if (name.prefix.is_some() || !name.ns.is_empty()) && !self.find_uri(name) {
if let Some(last_ns) = self.namespace_stack.0.last_mut() {
last_ns.insert(name);
}
}
}
}
impl<Wr: Write> Serializer for XmlSerializer<Wr> {
/// Serializes given start element into text. Start element contains
/// qualified name and an attributes iterator.
fn start_elem<'a, AttrIter>(&mut self, name: QualName, attrs: AttrIter) -> io::Result<()>
where
AttrIter: Iterator<Item = AttrRef<'a>>,
{
self.namespace_stack.push(NamespaceMap::empty());
self.writer.write_all(b"<")?;
self.qual_name(&name)?;
if let Some(current_namespace) = self.namespace_stack.0.last() {
for (prefix, url_opt) in current_namespace.get_scope_iter() {
self.writer.write_all(b" xmlns")?;
if let Some(ref prefix) = *prefix {
self.writer.write_all(b":")?;
self.writer.write_all(prefix.as_bytes())?;
}
self.writer.write_all(b"=\"")?;
let url = if let Some(ref a) = *url_opt {
a.as_bytes()
} else {
b""
};
self.writer.write_all(url)?;
self.writer.write_all(b"\"")?;
}
}
for (name, value) in attrs {
self.writer.write_all(b" ")?;
self.qual_attr_name(name)?;
self.writer.write_all(b"=\"")?;
write_to_buf_escaped(&mut self.writer, value, true)?;
self.writer.write_all(b"\"")?;
}
self.writer.write_all(b">")?;
Ok(())
}
/// Serializes given end element into text.
fn end_elem(&mut self, name: QualName) -> io::Result<()> {
self.namespace_stack.pop();
self.writer.write_all(b"</")?;
self.qual_name(&name)?;
self.writer.write_all(b">")
}
/// Serializes comment into text.
fn write_comment(&mut self, text: &str) -> io::Result<()> {
self.writer.write_all(b"<!--")?;
self.writer.write_all(text.as_bytes())?;
self.writer.write_all(b"-->")
}
/// Serializes given doctype
fn write_doctype(&mut self, name: &str) -> io::Result<()> {
self.writer.write_all(b"<!DOCTYPE ")?;
self.writer.write_all(name.as_bytes())?;
self.writer.write_all(b">")
}
/// Serializes text for a node or an attributes.
fn write_text(&mut self, text: &str) -> io::Result<()> {
write_to_buf_escaped(&mut self.writer, text, false)
}
/// Serializes given processing instruction.
fn write_processing_instruction(&mut self, target: &str, data: &str) -> io::Result<()> {
self.writer.write_all(b"<?")?;
self.writer.write_all(target.as_bytes())?;
self.writer.write_all(b" ")?;
self.writer.write_all(data.as_bytes())?;
self.writer.write_all(b"?>")
}
}
-451
View File
@@ -1,451 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use super::{TokenSink, XmlTokenizer};
use crate::data;
use crate::tendril::StrTendril;
use log::debug;
use markup5ever::buffer_queue::BufferQueue;
use std::borrow::Cow::{self, Borrowed};
use std::char::from_u32;
use self::State::*;
pub use self::Status::*;
//§ tokenizing-character-references
pub struct CharRef {
/// The resulting character(s)
pub chars: [char; 2],
/// How many slots in `chars` are valid?
pub num_chars: u8,
}
pub enum Status {
Stuck,
Progress,
Done,
}
#[derive(Debug)]
enum State {
Begin,
Octothorpe,
Numeric(u32), // base
NumericSemicolon,
Named,
BogusName,
}
pub struct CharRefTokenizer {
state: State,
addnl_allowed: Option<char>,
result: Option<CharRef>,
num: u32,
num_too_big: bool,
seen_digit: bool,
hex_marker: Option<char>,
name_buf_opt: Option<StrTendril>,
name_match: Option<(u32, u32)>,
name_len: usize,
}
impl CharRefTokenizer {
// NB: We assume that we have an additional allowed character iff we're
// tokenizing in an attribute value.
pub fn new(addnl_allowed: Option<char>) -> CharRefTokenizer {
CharRefTokenizer {
state: Begin,
addnl_allowed,
result: None,
num: 0,
num_too_big: false,
seen_digit: false,
hex_marker: None,
name_buf_opt: None,
name_match: None,
name_len: 0,
}
}
// A CharRefTokenizer can only tokenize one character reference,
// so this method consumes the tokenizer.
pub fn get_result(self) -> CharRef {
self.result.expect("get_result called before done")
}
fn name_buf(&self) -> &StrTendril {
self.name_buf_opt
.as_ref()
.expect("name_buf missing in named character reference")
}
fn name_buf_mut(&mut self) -> &mut StrTendril {
self.name_buf_opt
.as_mut()
.expect("name_buf missing in named character reference")
}
fn finish_none(&mut self) -> Status {
self.result = Some(CharRef {
chars: ['\0', '\0'],
num_chars: 0,
});
Done
}
fn finish_one(&mut self, c: char) -> Status {
self.result = Some(CharRef {
chars: [c, '\0'],
num_chars: 1,
});
Done
}
}
impl CharRefTokenizer {
pub fn step<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
if self.result.is_some() {
return Done;
}
debug!("char ref tokenizer stepping in state {:?}", self.state);
match self.state {
Begin => self.do_begin(tokenizer, input),
Octothorpe => self.do_octothorpe(tokenizer, input),
Numeric(base) => self.do_numeric(tokenizer, base, input),
NumericSemicolon => self.do_numeric_semicolon(tokenizer, input),
Named => self.do_named(tokenizer, input),
BogusName => self.do_bogus_name(tokenizer, input),
}
}
fn do_begin<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
match tokenizer.peek(input) {
Some('\t' | '\n' | '\x0C' | ' ' | '<' | '&') => self.finish_none(),
Some(c) if Some(c) == self.addnl_allowed => self.finish_none(),
Some('#') => {
tokenizer.discard_char(input);
self.state = Octothorpe;
Progress
},
Some(_) => {
self.state = Named;
self.name_buf_opt = Some(StrTendril::new());
Progress
},
None => Stuck,
}
}
fn do_octothorpe<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
match tokenizer.peek(input) {
Some(c @ ('x' | 'X')) => {
tokenizer.discard_char(input);
self.hex_marker = Some(c);
self.state = Numeric(16);
},
Some(_) => {
self.hex_marker = None;
self.state = Numeric(10);
},
None => return Stuck,
}
Progress
}
fn do_numeric<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
base: u32,
input: &BufferQueue,
) -> Status {
let Some(c) = tokenizer.peek(input) else {
return Stuck;
};
match c.to_digit(base) {
Some(n) => {
tokenizer.discard_char(input);
self.num = self.num.wrapping_mul(base);
if self.num > 0x10FFFF {
// We might overflow, and the character is definitely invalid.
// We still parse digits and semicolon, but don't use the result.
self.num_too_big = true;
}
self.num = self.num.wrapping_add(n);
self.seen_digit = true;
Progress
},
None if !self.seen_digit => self.unconsume_numeric(tokenizer, input),
None => {
self.state = NumericSemicolon;
Progress
},
}
}
fn do_numeric_semicolon<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
match tokenizer.peek(input) {
Some(';') => tokenizer.discard_char(input),
Some(_) => tokenizer.emit_error(Borrowed(
"Semicolon missing after numeric character reference",
)),
None => return Stuck,
};
self.finish_numeric(tokenizer)
}
fn unconsume_numeric<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
let mut unconsume = StrTendril::from_char('#');
if let Some(c) = self.hex_marker {
unconsume.push_char(c);
}
tokenizer.unconsume(input, unconsume);
tokenizer.emit_error(Borrowed("Numeric character reference without digits"));
self.finish_none()
}
fn finish_numeric<Sink: TokenSink>(&mut self, tokenizer: &XmlTokenizer<Sink>) -> Status {
let (c, error) = match self.num {
n if !self.num_too_big
&& matches!(
n,
0x9 | 0xa | 0xd | 0x20..=0xd7ff | 0xe000..=0xfffd | 0x10000..=0x10ffff
) =>
{
(
from_u32(n).expect("the XML Char production contains only Unicode scalars"),
false,
)
},
_ => ('\u{fffd}', true),
};
if error {
let msg = if tokenizer.opts.exact_errors {
Cow::from(format!(
"Invalid numeric character reference value 0x{:06X}",
self.num
))
} else {
Cow::from("Invalid numeric character reference")
};
tokenizer.emit_error(msg);
}
self.finish_one(c)
}
fn do_named<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
let Some(c) = tokenizer.get_char(input) else {
return Stuck;
};
self.name_buf_mut().push_char(c);
match data::NAMED_ENTITIES.get(&self.name_buf()[..]) {
// We have either a full match or a prefix of one.
Some(&m) => {
if m.0 != 0 {
// We have a full match, but there might be a longer one to come.
self.name_match = Some(m);
self.name_len = self.name_buf().len();
}
// Otherwise we just have a prefix match.
Progress
},
// Can't continue the match.
None => self.finish_named(tokenizer, Some(c), input),
}
}
fn emit_name_error<Sink: TokenSink>(&mut self, tokenizer: &XmlTokenizer<Sink>) {
let msg = if tokenizer.opts.exact_errors {
Cow::from(format!("Invalid character reference &{}", self.name_buf()))
} else {
Cow::from("Invalid character reference")
};
tokenizer.emit_error(msg);
}
fn unconsume_name<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) {
tokenizer.unconsume(input, self.name_buf_opt.take().unwrap());
}
fn finish_named<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
end_char: Option<char>,
input: &BufferQueue,
) -> Status {
match self.name_match {
None => {
match end_char {
Some(c) if c.is_ascii_alphanumeric() => {
// Keep looking for a semicolon, to determine whether
// we emit a parse error.
self.state = BogusName;
return Progress;
},
// Check length because &; is not a parse error.
Some(';') if self.name_buf().len() > 1 => self.emit_name_error(tokenizer),
_ => (),
}
self.unconsume_name(tokenizer, input);
self.finish_none()
},
Some((c1, c2)) => {
// We have a complete match, but we may have consumed
// additional characters into self.name_buf. Usually
// at least one, but several in cases like
//
// &not => match for U+00AC
// &noti => valid prefix for &notin
// &notit => can't continue match
let name_len = self.name_len;
assert!(name_len > 0);
let last_matched = self.name_buf()[name_len - 1..].chars().next().unwrap();
// There might not be a next character after the match, if
// we had a full match and then hit EOF.
let next_after = if name_len == self.name_buf().len() {
None
} else {
Some(self.name_buf()[name_len..].chars().next().unwrap())
};
// "If the character reference is being consumed as part of an
// attribute, and the last character matched is not a U+003B
// SEMICOLON character (;), and the next character is either a
// U+003D EQUALS SIGN character (=) or an alphanumeric ASCII
// character, then, for historical reasons, all the characters
// that were matched after the U+0026 AMPERSAND character (&)
// must be unconsumed, and nothing is returned. However, if
// this next character is in fact a U+003D EQUALS SIGN
// character (=), then this is a parse error"
let unconsume_all = match (self.addnl_allowed, last_matched, next_after) {
(_, ';', _) => false,
(Some(_), _, Some('=')) => {
tokenizer.emit_error(Borrowed(
"Equals sign after character reference in attribute",
));
true
},
(Some(_), _, Some(c)) if c.is_ascii_alphanumeric() => true,
_ => {
tokenizer.emit_error(Borrowed(
"Character reference does not end with semicolon",
));
false
},
};
if unconsume_all {
self.unconsume_name(tokenizer, input);
self.finish_none()
} else {
tokenizer
.unconsume(input, StrTendril::from_slice(&self.name_buf()[name_len..]));
self.result = Some(CharRef {
chars: [from_u32(c1).unwrap(), from_u32(c2).unwrap()],
num_chars: if c2 == 0 { 1 } else { 2 },
});
Done
}
},
}
}
fn do_bogus_name<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) -> Status {
let Some(c) = tokenizer.get_char(input) else {
return Stuck;
};
self.name_buf_mut().push_char(c);
match c {
_ if c.is_ascii_alphanumeric() => return Progress,
';' => self.emit_name_error(tokenizer),
_ => (),
}
self.unconsume_name(tokenizer, input);
self.finish_none()
}
pub fn end_of_file<Sink: TokenSink>(
&mut self,
tokenizer: &XmlTokenizer<Sink>,
input: &BufferQueue,
) {
while self.result.is_none() {
match self.state {
Begin => drop(self.finish_none()),
Numeric(_) if !self.seen_digit => drop(self.unconsume_numeric(tokenizer, input)),
Numeric(_) | NumericSemicolon => {
tokenizer.emit_error(Borrowed("EOF in numeric character reference"));
self.finish_numeric(tokenizer);
},
Named => drop(self.finish_named(tokenizer, None, input)),
BogusName => {
self.unconsume_name(tokenizer, input);
self.finish_none();
},
Octothorpe => {
tokenizer.unconsume(input, StrTendril::from_slice("#"));
tokenizer.emit_error(Borrowed("EOF after '#' in character reference"));
self.finish_none();
},
}
}
}
}
-118
View File
@@ -1,118 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::borrow::Cow;
use crate::tendril::StrTendril;
use crate::tokenizer::ProcessResult;
use crate::{Attribute, QualName};
pub use self::TagKind::{EmptyTag, EndTag, ShortTag, StartTag};
/// Tag kind denotes which kind of tag did we encounter.
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
pub enum TagKind {
/// Beginning of a tag (e.g. `<a>`).
StartTag,
/// End of a tag (e.g. `</a>`).
EndTag,
/// Empty tag (e.g. `<a/>`).
EmptyTag,
/// Short tag (e.g. `</>`).
ShortTag,
}
/// XML 5 Tag Token
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Tag {
/// Token kind denotes which type of token was encountered.
/// E.g. if parser parsed `</a>` the token kind would be `EndTag`.
pub kind: TagKind,
/// Qualified name of the tag.
pub name: QualName,
/// List of attributes attached to this tag.
/// Only valid in start and empty tag.
pub attrs: Vec<Attribute>,
}
impl Tag {
/// Sorts attributes in a tag.
pub fn equiv_modulo_attr_order(&self, other: &Tag) -> bool {
if (self.kind != other.kind) || (self.name != other.name) {
return false;
}
let mut self_attrs = self.attrs.clone();
let mut other_attrs = other.attrs.clone();
self_attrs.sort();
other_attrs.sort();
self_attrs == other_attrs
}
}
/// A `DOCTYPE` token.
/// Doctype token in XML5 is rather limited for reasons, such as:
/// security and simplicity. XML5 only supports declaring DTD with
/// name, public identifier and system identifier
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct Doctype {
/// Name of DOCTYPE declared
pub name: Option<StrTendril>,
/// Public identifier of this DOCTYPE.
pub public_id: Option<StrTendril>,
/// System identifier of this DOCTYPE.
pub system_id: Option<StrTendril>,
}
/// A ProcessingInstruction token.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Pi {
/// What is the name of processing instruction.
pub target: StrTendril,
/// Text of processing instruction.
pub data: StrTendril,
}
/// Describes tokens encountered during parsing of input.
#[derive(PartialEq, Eq, Debug)]
pub enum Token {
/// Doctype token
Doctype(Doctype),
/// Token tag founds. This token applies to all
/// possible kinds of tags (like start, end, empty tag, etc.).
Tag(Tag),
/// Processing Instruction token
ProcessingInstruction(Pi),
/// Comment token.
Comment(StrTendril),
/// Token that represents a series of characters.
Characters(StrTendril),
/// Token that represents a CDATA section.
Cdata(StrTendril),
/// End of File found.
EndOfFile,
/// NullCharacter encountered.
NullCharacter,
/// Error happened
ParseError(Cow<'static, str>),
}
/// Types which can receive tokens from the tokenizer.
pub trait TokenSink {
/// Handle to a DOM script element
type Handle;
/// Process a token.
fn process_token(&self, token: Token) -> ProcessResult<Self::Handle>;
/// Signal to the sink that parsing has ended.
fn end(&self) {}
}
File diff suppressed because it is too large Load Diff
-84
View File
@@ -1,84 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
enum QualNameState {
BeforeName,
InName,
AfterColon,
}
pub struct QualNameTokenizer<'a> {
state: QualNameState,
slice: &'a [u8],
valid_index: Option<u32>,
curr_ind: usize,
}
impl QualNameTokenizer<'_> {
pub fn new(tag: &[u8]) -> QualNameTokenizer<'_> {
QualNameTokenizer {
state: QualNameState::BeforeName,
slice: tag,
valid_index: None,
curr_ind: 0,
}
}
pub fn run(&mut self) -> Option<u32> {
if !self.slice.is_empty() {
loop {
if !self.step() {
break;
}
}
}
self.valid_index
}
fn incr(&mut self) -> bool {
if self.curr_ind + 1 < self.slice.len() {
self.curr_ind += 1;
return true;
}
false
}
fn step(&mut self) -> bool {
match self.state {
QualNameState::BeforeName => self.do_before_name(),
QualNameState::InName => self.do_in_name(),
QualNameState::AfterColon => self.do_after_colon(),
}
}
fn do_before_name(&mut self) -> bool {
if self.slice[self.curr_ind] == b':' {
false
} else {
self.state = QualNameState::InName;
self.incr()
}
}
fn do_in_name(&mut self) -> bool {
if self.slice[self.curr_ind] == b':' && self.curr_ind + 1 < self.slice.len() {
self.valid_index = Some(self.curr_ind as u32);
self.state = QualNameState::AfterColon;
}
self.incr()
}
fn do_after_colon(&mut self) -> bool {
if self.slice[self.curr_ind] == b':' {
self.valid_index = None;
return false;
}
self.incr()
}
}
-167
View File
@@ -1,167 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Tokenizer states.
/// Specifies either the public or system identifier from a [Document Type Declaration] (DTD).
///
/// [Document Type Declaration]: https://en.wikipedia.org/wiki/Document_type_declaration
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, Debug)]
pub enum DoctypeKind {
/// The public identifier.
Public,
/// The system identifier.
System,
}
/// Specifies the different states a XML tokenizer will assume during parsing.
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, Debug)]
pub enum XmlState {
/// The initial state of the parser.
///
/// It is equivalent to the [`Data`](https://html.spec.whatwg.org/#data-state) state of the html parser,
/// except null codepoints do not cause errors.
Data,
/// Indicates that the parser has found a `<` character and will try to parse a tag.
TagState,
/// Indicates that the parser has consumed the `/` of a closing tag, like `</foo>`.
EndTagState,
/// Indicates that the parser is currently parsing the name of a closing tag, like the `foo` of `</foo>`.
EndTagName,
/// Indicates that the parser has finished parsing the name of a closing tag and expects a `>` to follow.
EndTagNameAfter,
/// Indicates that the parser has started parsing a [processing instruction] (PI).
///
/// This state is reached after the initial `?` character has been consumed.
///
/// [processing instruction]: https://en.wikipedia.org/wiki/Processing_Instruction
Pi,
/// Indicates that the parser is currently parsing the target of a [processing instruction].
///
/// For example, the target of `<?xml-stylesheet type="text/xsl" href="style.xsl"?>` is `xml-stylesheet`.
///
/// [processing instruction]: https://en.wikipedia.org/wiki/Processing_Instruction
PiTarget,
/// Indicates that the parser has finished parsing the target of a [processing instruction].
///
/// [processing instruction]: https://en.wikipedia.org/wiki/Processing_Instruction
PiTargetAfter,
/// Indicates that the parser is currently parsing the data of a [processing instruction].
///
/// The "data" refers to everything between the target and the closing `?` character.
///
/// [processing instruction]: https://en.wikipedia.org/wiki/Processing_Instruction
PiData,
/// Indicates that the parser has parsed the closing `?` of a [processing instruction].
///
/// [processing instruction]: https://en.wikipedia.org/wiki/Processing_Instruction
PiAfter,
/// Indicates that the parser has parsed the initial `!` of a markup declaration.
///
/// Examples of such declarations include `<!ENTITY chap1 SYSTEM "chap1.xml">` or `<!-- Comment -->`.
MarkupDecl,
/// Indicates that the parser has parsed the start of a comment (`<!--`).
CommentStart,
/// Indicates that the parser has parsed the start of a comment and a `-` directly after it.
CommentStartDash,
/// Indicates that the parser is currently parsing the data within a comment.
Comment,
/// Indicates that the parser has parsed a `<` character within a comment.
CommentLessThan,
/// Indicates that the parser has parsed `<!` within a comment.
CommentLessThanBang,
/// Indicates that the parser has parsed `<!-` within a comment.
CommentLessThanBangDash,
/// Indicates that the parser has parsed `<!--` within a comment.
CommentLessThanBangDashDash,
/// Indicates that the parser has parsed two `-` characters within a comment which may or may not
/// be the beginning of the comment end (`-->`).
CommentEnd,
/// Indicates that the parser has parsed a `-` character within a comment which may or may not
/// be the beginning of the comment end (`-->`).
CommentEndDash,
/// Indicates that the parser has parsed `--!` within a comment which may or may not be part of the
/// end of the comment. Comments in XML can be closed with `--!>`.
CommentEndBang,
/// Indicates that the parser has parsed the beginning of a CDATA section (`<![CDATA[`).
Cdata,
/// Indicates that the parser has parsed a `]` character within a CDATA section, which may be part of
/// the end of the section (`]]>`).
CdataBracket,
/// Indicates that the parser has parsed two `]` characters within a CDATA section, which may be part of
/// the end of the section (`]]>`).
CdataEnd,
/// Indicates that the parser is currently parsing the name of a tag, such as `foo` in `<foo>`.
TagName,
/// Indicates that the parser has parsed the `/` of a self-closing tag, such as `<foo/>`.
TagEmpty,
/// Indicates that the parser has finished parsing the name of a tag and is now expecting either attributes or
/// a `>`.
TagAttrNameBefore,
/// Indicates that the parser is currently parsing the name of an attribute within a tag, such as
/// `bar` in `<foo bar=baz>`.
TagAttrName,
/// Indicates that the parser has finished parsing the name of an attribute.
TagAttrNameAfter,
/// Indicates that the parser is about to parse the value of an attribute.
TagAttrValueBefore,
/// Indicates that the parser is currently parsing the value of an attribute, such as `baz` in
/// `<foo bar=baz>`.
///
/// Includes information about how the value is quoted, because the quotes before and after the attribute
/// value need to match.
TagAttrValue(AttrValueKind),
/// Indicates that the parser has parsed the beginning of a document type definition (`<!DOCTYPE`).
Doctype,
/// Indicates that the parser expects to parse the name of the document type definition next.
BeforeDoctypeName,
/// Indicates that the parser is currently parsing the name of a document type definition, such as
/// `html` in `<!DOCTYPE html>`.
DoctypeName,
/// Indicates that the parser has finished parsing the name of the document type definition and now optionally
/// expects either a public or a system identifier.
AfterDoctypeName,
/// Indicates that the parser has parsed a keyword for either a public or system identifier (`PUBLIC` or `SYSTEM`).
AfterDoctypeKeyword(DoctypeKind),
/// Indicates that the parser is about to parse the value of a public or system identifier within
/// a document type definition, such as `foo` in
/// `<!DOCTYPE html PUBLIC "foo" "bar">`.
BeforeDoctypeIdentifier(DoctypeKind),
/// Indicates that the parser is currently parsing the value of a public or system identifier
/// that is surrounded by double quotes , such as `foo` in
/// `<!DOCTYPE html PUBLIC "foo" "bar">`.
DoctypeIdentifierDoubleQuoted(DoctypeKind),
/// Indicates that the parser is currently parsing the value of a public or system identifier
/// that is surrounded by single quotes , such as `foo` in
/// `<!DOCTYPE html PUBLIC 'foo' 'bar'>`.
DoctypeIdentifierSingleQuoted(DoctypeKind),
/// Indicates that the parser has finished parsing either a public or system identifier within a
/// document type definition.
AfterDoctypeIdentifier(DoctypeKind),
/// Indicates that the parser has finished parsing a public identifier and now expects
/// a system identifier.
BetweenDoctypePublicAndSystemIdentifiers,
/// Indicates that the parser is currently parsing an ill-formed document type defintion, such as
/// `<!DOCTYPE html what-is-this>`.
BogusDoctype,
/// Indicates that the parser is currently parsing an ill-formed comment, such as
/// `<? this is not what a comment should look like! >`.
BogusComment,
}
/// Specifies how an attribute value is quoted, if at all.
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, Debug)]
pub enum AttrValueKind {
/// A attribute value that is not surrounded by quotes, like `bar` in `foo=bar`.
Unquoted,
/// A attribute value that is not surrounded by quotes, like `bar` in `foo='bar'`.
SingleQuoted,
/// A attribute value that is not surrounded by quotes, like `bar` in `foo="bar"`.
DoubleQuoted,
}
-800
View File
@@ -1,800 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
mod types;
use log::{debug, warn};
use markup5ever::{local_name, namespace_prefix, ns};
use std::borrow::Cow;
use std::borrow::Cow::Borrowed;
use std::cell::{Cell, Ref, RefCell};
use std::collections::btree_map::Iter;
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::fmt::{Debug, Error, Formatter};
use std::mem;
pub use self::interface::{ElemName, NodeOrText, Tracer, TreeSink};
use self::types::*;
use crate::interface::{self, create_element, AppendNode, Attribute, QualName};
use crate::interface::{AppendText, ExpandedName};
use crate::tokenizer::{self, EndTag, ProcessResult, StartTag, Tag, TokenSink};
use crate::tokenizer::{Doctype, EmptyTag, Pi, ShortTag};
use crate::{LocalName, Namespace, Prefix};
use crate::tendril::{StrTendril, Tendril};
/// XML-specific tree sink operations that are not part of markup5ever's
/// HTML-oriented [`TreeSink`] interface.
pub trait XmlTreeSink: TreeSink {
/// Create a CDATA section node.
fn create_cdata(&self, text: StrTendril) -> Self::Handle;
}
static XML_URI: &str = "http://www.w3.org/XML/1998/namespace";
static XMLNS_URI: &str = "http://www.w3.org/2000/xmlns/";
type InsResult = Result<(), Cow<'static, str>>;
#[derive(Debug)]
struct NamespaceMapStack(Vec<NamespaceMap>);
impl NamespaceMapStack {
fn new() -> NamespaceMapStack {
NamespaceMapStack(vec![NamespaceMap::default()])
}
fn push(&mut self, map: NamespaceMap) {
self.0.push(map);
}
fn pop(&mut self) {
self.0.pop();
}
}
pub(crate) struct NamespaceMap {
// Map that maps prefixes to URI.
//
// Key denotes namespace prefix, and value denotes
// URI it maps to.
//
// If value of value is None, that means the namespace
// denoted by key has been undeclared.
scope: BTreeMap<Option<Prefix>, Option<Namespace>>,
}
impl Debug for NamespaceMap {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "\nNamespaceMap[")?;
for (key, value) in &self.scope {
writeln!(f, " {key:?} : {value:?}")?;
}
write!(f, "]")
}
}
impl NamespaceMap {
// Returns an empty namespace.
pub(crate) fn empty() -> NamespaceMap {
NamespaceMap {
scope: BTreeMap::new(),
}
}
fn default() -> NamespaceMap {
NamespaceMap {
scope: {
let mut map = BTreeMap::new();
map.insert(None, None);
map.insert(Some(namespace_prefix!("xml")), Some(ns!(xml)));
map.insert(Some(namespace_prefix!("xmlns")), Some(ns!(xmlns)));
map
},
}
}
pub(crate) fn get(&self, prefix: &Option<Prefix>) -> Option<&Option<Namespace>> {
self.scope.get(prefix)
}
pub(crate) fn get_scope_iter(&self) -> Iter<'_, Option<Prefix>, Option<Namespace>> {
self.scope.iter()
}
pub(crate) fn insert(&mut self, name: &QualName) {
let prefix = name.prefix.as_ref().cloned();
let namespace = Some(Namespace::from(&*name.ns));
self.scope.insert(prefix, namespace);
}
fn insert_ns(&mut self, attr: &Attribute) -> InsResult {
if &*attr.value == XMLNS_URI {
return Err(Borrowed("Can't declare XMLNS URI"));
};
let opt_uri = if attr.value.is_empty() {
None
} else {
Some(Namespace::from(&*attr.value))
};
let result = match (&attr.name.prefix, &*attr.name.local) {
(&Some(namespace_prefix!("xmlns")), "xml") => {
if &*attr.value != XML_URI {
Err(Borrowed("XML namespace can't be redeclared"))
} else {
Ok(())
}
},
(&Some(namespace_prefix!("xmlns")), "xmlns") => {
Err(Borrowed("XMLNS namespaces can't be changed"))
},
(&Some(namespace_prefix!("xmlns")), _) | (&None, "xmlns") => {
// We can have two cases of properly defined xmlns
// First with default namespace e.g.
//
// <a xmlns = "www.uri.org" />
let ns_prefix = if &*attr.name.local == "xmlns" {
None
// Second is with named namespace e.g.
//
// <a xmlns:a = "www.uri.org" />
} else {
Some(Prefix::from(&*attr.name.local))
};
if opt_uri.is_some() && self.scope.contains_key(&ns_prefix) {
Err(Borrowed("Namespace already defined"))
} else {
self.scope.insert(ns_prefix, opt_uri);
Ok(())
}
},
(_, _) => Err(Borrowed("Invalid namespace declaration.")),
};
result
}
}
/// Tree builder options, with an impl for Default.
#[derive(Copy, Clone, Default)]
pub struct XmlTreeBuilderOpts {}
/// The XML tree builder.
pub struct XmlTreeBuilder<Handle, Sink> {
/// Configuration options for XmlTreeBuilder
_opts: XmlTreeBuilderOpts,
/// Consumer of tree modifications.
pub sink: Sink,
/// The document node, which is created by the sink.
doc_handle: Handle,
/// Stack of open elements, most recently added at end.
open_elems: RefCell<Vec<Handle>>,
/// Current element pointer.
curr_elem: RefCell<Option<Handle>>,
/// Stack of namespace identifiers and namespaces.
namespace_stack: RefCell<NamespaceMapStack>,
/// Current namespace identifier
current_namespace: RefCell<NamespaceMap>,
/// Current tree builder phase.
phase: Cell<XmlPhase>,
}
impl<Handle, Sink> XmlTreeBuilder<Handle, Sink>
where
Handle: Clone,
Sink: XmlTreeSink<Handle = Handle>,
{
/// Create a new tree builder which sends tree modifications to a particular `TreeSink`.
///
/// The tree builder is also a `TokenSink`.
pub fn new(sink: Sink, opts: XmlTreeBuilderOpts) -> XmlTreeBuilder<Handle, Sink> {
let doc_handle = sink.get_document();
XmlTreeBuilder {
_opts: opts,
sink,
doc_handle,
open_elems: RefCell::new(vec![]),
curr_elem: RefCell::new(None),
namespace_stack: RefCell::new(NamespaceMapStack::new()),
current_namespace: RefCell::new(NamespaceMap::empty()),
phase: Cell::new(XmlPhase::Start),
}
}
/// Call the `Tracer`'s `trace_handle` method on every `Handle` in the tree builder's
/// internal state. This is intended to support garbage-collected DOMs.
pub fn trace_handles(&self, tracer: &dyn Tracer<Handle = Handle>) {
tracer.trace_handle(&self.doc_handle);
for e in self.open_elems.borrow().iter() {
tracer.trace_handle(e);
}
if let Some(h) = self.curr_elem.borrow().as_ref() {
tracer.trace_handle(h);
}
}
// Debug helper
#[cfg(not(for_c))]
#[allow(dead_code)]
fn dump_state(&self, label: String) {
debug!("dump_state on {label}");
debug!(" open_elems:");
for node in self.open_elems.borrow().iter() {
debug!(" {:?}", self.sink.elem_name(node));
}
debug!("");
}
#[cfg(for_c)]
fn debug_step(&self, _mode: XmlPhase, _token: &Token) {}
#[cfg(not(for_c))]
fn debug_step(&self, mode: XmlPhase, token: &Token) {
debug!(
"processing {:?} in insertion mode {:?}",
format!("{:?}", token),
mode
);
}
fn declare_ns(&self, attr: &mut Attribute) {
if let Err(msg) = self.current_namespace.borrow_mut().insert_ns(attr) {
self.sink.parse_error(msg);
}
// Namespace declarations are real DOM attributes. Keep their XMLNS
// namespace identity even when the declaration itself is invalid.
attr.name.ns = ns!(xmlns);
}
fn find_uri(&self, prefix: &Option<Prefix>) -> Result<Option<Namespace>, Cow<'static, str>> {
let mut uri = Err(Borrowed("No appropriate namespace found"));
let current_namespace = self.current_namespace.borrow();
for ns in self
.namespace_stack
.borrow()
.0
.iter()
.chain(Some(&*current_namespace))
.rev()
{
if let Some(el) = ns.get(prefix) {
uri = Ok(el.clone());
break;
}
}
uri
}
fn bind_qname(&self, name: &mut QualName) {
match self.find_uri(&name.prefix) {
Ok(uri) => {
let ns_uri = match uri {
Some(e) => e,
None => ns!(),
};
name.ns = ns_uri;
},
Err(msg) => {
self.sink.parse_error(msg);
},
}
}
// This method takes in name qualified name and binds it to the
// existing namespace context.
//
// Returns false if the attribute is a duplicate, returns true otherwise.
fn bind_attr_qname(
&self,
present_attrs: &mut HashSet<(Namespace, LocalName)>,
name: &mut QualName,
) -> bool {
// Attributes don't have default namespace
let mut not_duplicate = true;
if name.prefix.is_some() {
self.bind_qname(name);
not_duplicate = Self::check_duplicate_attr(present_attrs, name);
}
not_duplicate
}
fn check_duplicate_attr(
present_attrs: &mut HashSet<(Namespace, LocalName)>,
name: &QualName,
) -> bool {
let pair = (name.ns.clone(), name.local.clone());
if present_attrs.contains(&pair) {
return false;
}
present_attrs.insert(pair);
true
}
fn process_namespaces(&self, tag: &mut Tag) {
// List of already present namespace local name attribute pairs.
let mut present_attrs: HashSet<(Namespace, LocalName)> = Default::default();
let mut new_attr = vec![];
// First we extract all namespace declarations
for attr in tag.attrs.iter_mut().filter(|attr| {
attr.name.prefix == Some(namespace_prefix!("xmlns"))
|| attr.name.local == local_name!("xmlns")
}) {
self.declare_ns(attr);
}
// Bind ordinary attributes and retain namespace declarations in their
// original order. Tree sinks need the declarations as Attr nodes even
// though the namespace resolver has already consumed their values.
for attr in tag.attrs.iter_mut() {
let is_namespace_declaration = attr.name.ns == ns!(xmlns);
let keep = if is_namespace_declaration {
Self::check_duplicate_attr(&mut present_attrs, &attr.name)
} else {
self.bind_attr_qname(&mut present_attrs, &mut attr.name)
};
if keep {
new_attr.push(attr.clone());
}
}
tag.attrs = new_attr;
// Then we bind the tags namespace.
self.bind_qname(&mut tag.name);
// Finally, we dump current namespace if its unneeded.
let x = mem::replace(
&mut *self.current_namespace.borrow_mut(),
NamespaceMap::empty(),
);
// Only start tag doesn't dump current namespace. However, <script /> is treated
// differently than every other empty tag, so it needs to retain the current
// namespace as well.
if tag.kind == StartTag || (tag.kind == EmptyTag && tag.name.local == local_name!("script"))
{
self.namespace_stack.borrow_mut().push(x);
}
}
fn process_to_completion(
&self,
mut token: Token,
) -> ProcessResult<<Self as TokenSink>::Handle> {
// Queue of additional tokens yet to be processed.
// This stays empty in the common case where we don't split whitespace.
let mut more_tokens = VecDeque::new();
loop {
let phase = self.phase.get();
#[allow(clippy::unused_unit)]
match self.step(phase, token) {
XmlProcessResult::Done => {
let Some(popped_token) = more_tokens.pop_front() else {
return ProcessResult::Continue;
};
token = popped_token;
},
XmlProcessResult::Reprocess(m, t) => {
self.phase.set(m);
token = t;
},
XmlProcessResult::Script(node) => {
assert!(more_tokens.is_empty());
return ProcessResult::Script(node);
},
}
}
}
}
impl<Handle, Sink> TokenSink for XmlTreeBuilder<Handle, Sink>
where
Handle: Clone,
Sink: XmlTreeSink<Handle = Handle>,
{
type Handle = Handle;
fn process_token(&self, token: tokenizer::Token) -> ProcessResult<Self::Handle> {
// Handle `ParseError` and `DoctypeToken`; convert everything else to the local `Token` type.
let token = match token {
tokenizer::Token::ParseError(e) => {
self.sink.parse_error(e);
return ProcessResult::Done;
},
tokenizer::Token::Doctype(d) => Token::Doctype(d),
tokenizer::Token::ProcessingInstruction(instruction) => Token::Pi(instruction),
tokenizer::Token::Tag(x) => Token::Tag(x),
tokenizer::Token::Comment(x) => Token::Comment(x),
tokenizer::Token::NullCharacter => Token::NullCharacter,
tokenizer::Token::EndOfFile => Token::Eof,
tokenizer::Token::Characters(x) => Token::Characters(x),
tokenizer::Token::Cdata(x) => Token::Cdata(x),
};
self.process_to_completion(token)
}
fn end(&self) {
let mut open_elems = self.open_elems.borrow_mut();
if !open_elems.is_empty() {
self.sink
.parse_error(Borrowed("Unexpected EOF with an unclosed XML element"));
}
for node in open_elems.drain(..).rev() {
self.sink.pop(&node);
}
}
}
fn current_node<Handle>(open_elems: &[Handle]) -> &Handle {
open_elems.last().expect("no current element")
}
impl<Handle, Sink> XmlTreeBuilder<Handle, Sink>
where
Handle: Clone,
Sink: XmlTreeSink<Handle = Handle>,
{
fn current_node(&self) -> Ref<'_, Handle> {
Ref::map(self.open_elems.borrow(), |elems| {
elems.last().expect("no current element")
})
}
fn insert_appropriately(&self, child: NodeOrText<Handle>) {
let open_elems = self.open_elems.borrow();
let target = current_node(&open_elems);
self.sink.append(target, child);
}
fn insert_tag(&self, tag: Tag) -> XmlProcessResult<Handle> {
let child = create_element(&self.sink, tag.name, tag.attrs);
self.insert_appropriately(AppendNode(child.clone()));
self.add_to_open_elems(child)
}
fn append_tag(&self, tag: Tag) -> XmlProcessResult<Handle> {
let child = create_element(&self.sink, tag.name, tag.attrs);
self.insert_appropriately(AppendNode(child.clone()));
self.sink.pop(&child);
XmlProcessResult::Done
}
fn append_tag_to_doc(&self, tag: Tag) -> Handle {
let child = create_element(&self.sink, tag.name, tag.attrs);
self.sink
.append(&self.doc_handle, AppendNode(child.clone()));
child
}
fn add_to_open_elems(&self, el: Handle) -> XmlProcessResult<Handle> {
self.open_elems.borrow_mut().push(el);
XmlProcessResult::Done
}
fn append_comment_to_doc(&self, text: StrTendril) -> XmlProcessResult<Handle> {
let comment = self.sink.create_comment(text);
self.sink.append(&self.doc_handle, AppendNode(comment));
XmlProcessResult::Done
}
fn append_comment_to_tag(&self, text: StrTendril) -> XmlProcessResult<Handle> {
let open_elems = self.open_elems.borrow();
let target = current_node(&open_elems);
let comment = self.sink.create_comment(text);
self.sink.append(target, AppendNode(comment));
XmlProcessResult::Done
}
fn append_doctype_to_doc(&self, doctype: Doctype) -> XmlProcessResult<Handle> {
fn get_tendril(opt: Option<StrTendril>) -> StrTendril {
match opt {
Some(expr) => expr,
None => Tendril::new(),
}
}
self.sink.append_doctype_to_document(
get_tendril(doctype.name),
get_tendril(doctype.public_id),
get_tendril(doctype.system_id),
);
XmlProcessResult::Done
}
fn append_pi_to_doc(&self, pi: Pi) -> XmlProcessResult<Handle> {
let pi = self.sink.create_pi(pi.target, pi.data);
self.sink.append(&self.doc_handle, AppendNode(pi));
XmlProcessResult::Done
}
fn append_pi_to_tag(&self, pi: Pi) -> XmlProcessResult<Handle> {
let open_elems = self.open_elems.borrow();
let target = current_node(&open_elems);
let pi = self.sink.create_pi(pi.target, pi.data);
self.sink.append(target, AppendNode(pi));
XmlProcessResult::Done
}
fn append_text(&self, chars: StrTendril) -> XmlProcessResult<Handle> {
self.insert_appropriately(AppendText(chars));
XmlProcessResult::Done
}
fn append_cdata(&self, text: StrTendril) -> XmlProcessResult<Handle> {
let cdata = self.sink.create_cdata(text);
self.insert_appropriately(AppendNode(cdata));
XmlProcessResult::Done
}
fn tag_in_open_elems(&self, tag: &Tag) -> bool {
self.open_elems
.borrow()
.iter()
.any(|a| self.sink.elem_name(a).expanded() == tag.name.expanded())
}
// Pop elements until an element from the set has been popped.
fn pop_until<P>(&self, pred: P)
where
P: Fn(ExpandedName) -> bool,
{
loop {
if self.current_node_in(&pred) {
break;
}
self.pop();
}
}
fn current_node_in<TagSet>(&self, set: TagSet) -> bool
where
TagSet: Fn(ExpandedName) -> bool,
{
// FIXME: take namespace into consideration:
set(self.sink.elem_name(&self.current_node()).expanded())
}
fn close_tag(&self, tag: Tag) -> XmlProcessResult<Handle> {
debug!(
"Close tag: current_node.name {:?} \n Current tag {:?}",
self.sink.elem_name(&self.current_node()),
tag.name
);
if *self.sink.elem_name(&self.current_node()).local_name() != tag.name.local {
self.sink
.parse_error(Borrowed("Current node doesn't match tag"));
}
let is_closed = self.tag_in_open_elems(&tag);
if is_closed {
self.pop_until(|p| p == tag.name.expanded());
self.pop();
}
XmlProcessResult::Done
}
fn no_open_elems(&self) -> bool {
self.open_elems.borrow().is_empty()
}
fn pop(&self) -> Handle {
self.namespace_stack.borrow_mut().pop();
let node = self
.open_elems
.borrow_mut()
.pop()
.expect("no current element");
self.sink.pop(&node);
node
}
fn stop_parsing(&self) -> XmlProcessResult<Handle> {
warn!("stop_parsing for XML5 not implemented, full speed ahead!");
XmlProcessResult::Done
}
}
fn any_not_whitespace(x: &StrTendril) -> bool {
!x.bytes()
.all(|b| matches!(b, b'\t' | b'\r' | b'\n' | b'\x0C' | b' '))
}
impl<Handle, Sink> XmlTreeBuilder<Handle, Sink>
where
Handle: Clone,
Sink: XmlTreeSink<Handle = Handle>,
{
fn step(&self, mode: XmlPhase, token: Token) -> XmlProcessResult<<Self as TokenSink>::Handle> {
self.debug_step(mode, &token);
match mode {
XmlPhase::Start => match token {
Token::Tag(Tag {
kind: StartTag,
name,
attrs,
}) => {
let tag = {
let mut tag = Tag {
kind: StartTag,
name,
attrs,
};
self.process_namespaces(&mut tag);
tag
};
self.phase.set(XmlPhase::Main);
let handle = self.append_tag_to_doc(tag);
self.add_to_open_elems(handle)
},
Token::Tag(Tag {
kind: EmptyTag,
name,
attrs,
}) => {
let tag = {
let mut tag = Tag {
kind: EmptyTag,
name,
attrs,
};
self.process_namespaces(&mut tag);
tag
};
self.phase.set(XmlPhase::End);
let handle = self.append_tag_to_doc(tag);
self.sink.pop(&handle);
XmlProcessResult::Done
},
Token::Comment(comment) => self.append_comment_to_doc(comment),
Token::Pi(pi) => self.append_pi_to_doc(pi),
Token::Characters(ref chars) if !any_not_whitespace(chars) => {
XmlProcessResult::Done
},
Token::Eof => {
self.sink
.parse_error(Borrowed("Unexpected EOF in start phase"));
XmlProcessResult::Reprocess(XmlPhase::End, Token::Eof)
},
Token::Doctype(d) => {
self.append_doctype_to_doc(d);
XmlProcessResult::Done
},
_ => {
self.sink
.parse_error(Borrowed("Unexpected element in start phase"));
XmlProcessResult::Done
},
},
XmlPhase::Main => match token {
Token::Characters(chs) => self.append_text(chs),
Token::Cdata(text) => self.append_cdata(text),
Token::Tag(Tag {
kind: StartTag,
name,
attrs,
}) => {
let tag = {
let mut tag = Tag {
kind: StartTag,
name,
attrs,
};
self.process_namespaces(&mut tag);
tag
};
self.insert_tag(tag)
},
Token::Tag(Tag {
kind: EmptyTag,
name,
attrs,
}) => {
let tag = {
let mut tag = Tag {
kind: EmptyTag,
name,
attrs,
};
self.process_namespaces(&mut tag);
tag
};
if tag.name.local == local_name!("script") {
self.insert_tag(tag.clone());
let script = current_node(&self.open_elems.borrow()).clone();
self.close_tag(tag);
XmlProcessResult::Script(script)
} else {
self.append_tag(tag)
}
},
Token::Tag(Tag {
kind: EndTag,
name,
attrs,
}) => {
let tag = {
let mut tag = Tag {
kind: EndTag,
name,
attrs,
};
self.process_namespaces(&mut tag);
tag
};
if tag.name.local == local_name!("script") {
let script = current_node(&self.open_elems.borrow()).clone();
self.close_tag(tag);
if self.no_open_elems() {
self.phase.set(XmlPhase::End);
}
return XmlProcessResult::Script(script);
}
let retval = self.close_tag(tag);
if self.no_open_elems() {
self.phase.set(XmlPhase::End);
}
retval
},
Token::Tag(Tag { kind: ShortTag, .. }) => {
self.pop();
if self.no_open_elems() {
self.phase.set(XmlPhase::End);
}
XmlProcessResult::Done
},
Token::Comment(comment) => self.append_comment_to_tag(comment),
Token::Pi(pi) => self.append_pi_to_tag(pi),
Token::Eof | Token::NullCharacter => {
XmlProcessResult::Reprocess(XmlPhase::End, Token::Eof)
},
Token::Doctype(_) => {
self.sink
.parse_error(Borrowed("Unexpected element in main phase"));
XmlProcessResult::Done
},
},
XmlPhase::End => match token {
Token::Comment(comment) => self.append_comment_to_doc(comment),
Token::Pi(pi) => self.append_pi_to_doc(pi),
Token::Characters(ref chars) if !any_not_whitespace(chars) => {
XmlProcessResult::Done
},
Token::Eof => self.stop_parsing(),
_ => {
self.sink
.parse_error(Borrowed("Unexpected element in end phase"));
XmlProcessResult::Done
},
},
}
}
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2014-2017 The html5ever Project Developers. See the
// COPYRIGHT file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::tendril::StrTendril;
use crate::tokenizer::{Doctype, Pi, Tag};
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum XmlPhase {
Start,
Main,
End,
}
/// A subset/refinement of `tokenizer::Token`. Everything else is handled
/// specially at the beginning of `process_token`.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Token {
Tag(Tag),
Doctype(Doctype),
Comment(StrTendril),
Characters(StrTendril),
Cdata(StrTendril),
Pi(Pi),
NullCharacter,
Eof,
}
pub enum XmlProcessResult<Handle> {
Done,
Reprocess(XmlPhase, Token),
Script(Handle),
}