makiri 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -0
- data/README.md +6 -0
- data/ext/makiri/rust/build.rs +1 -7
- data/ext/makiri/rust/src/bridge/doc.rs +30 -8
- data/ext/makiri/rust/src/bridge/dom_error.rs +47 -0
- data/ext/makiri/rust/src/bridge/fragment.rs +1 -2
- data/ext/makiri/rust/src/bridge/gvl.rs +1 -2
- data/ext/makiri/rust/src/bridge/html.rs +50 -53
- data/ext/makiri/rust/src/bridge/mod.rs +3 -0
- data/ext/makiri/rust/src/bridge/ruby.rs +8 -2
- data/ext/makiri/rust/src/bridge/string.rs +36 -10
- data/ext/makiri/rust/src/bridge/wrapper.rs +171 -49
- data/ext/makiri/rust/src/bridge/xml.rs +125 -55
- data/ext/makiri/rust/src/bridge/xml_decode.rs +49 -69
- data/ext/makiri/rust/src/bridge/xpath/context_object.rs +24 -39
- data/ext/makiri/rust/src/bridge/xpath/handler.rs +11 -15
- data/ext/makiri/rust/src/bridge/xpath/mod.rs +74 -16
- data/ext/makiri/rust/src/dom_rules.rs +18 -0
- data/ext/makiri/rust/src/glue/html_doc.rs +20 -0
- data/ext/makiri/rust/src/glue/html_node/css.rs +3 -0
- data/ext/makiri/rust/src/glue/html_node/mutate.rs +43 -40
- data/ext/makiri/rust/src/glue/node.rs +43 -5
- data/ext/makiri/rust/src/glue/query.rs +2 -21
- data/ext/makiri/rust/src/glue/xml_doc.rs +11 -0
- data/ext/makiri/rust/src/glue/xml_node/css.rs +3 -3
- data/ext/makiri/rust/src/glue/xml_node/mod.rs +1 -0
- data/ext/makiri/rust/src/glue/xml_node/mutate.rs +88 -21
- data/ext/makiri/rust/src/glue/xml_node/serialize.rs +6 -0
- data/ext/makiri/rust/src/lexbor/abi.rs +11 -18
- data/ext/makiri/rust/src/lexbor/adapter/cross_import.rs +56 -14
- data/ext/makiri/rust/src/lexbor/adapter/html/attrs.rs +15 -7
- data/ext/makiri/rust/src/lexbor/adapter/html/build.rs +12 -4
- data/ext/makiri/rust/src/lexbor/adapter/html/mod.rs +54 -10
- data/ext/makiri/rust/src/lexbor/adapter/html/mutate.rs +16 -13
- data/ext/makiri/rust/src/lexbor/adapter/post_parse.rs +31 -17
- data/ext/makiri/rust/src/lexbor/adapter/text_index.rs +9 -9
- data/ext/makiri/rust/src/lexbor/adapter/tree_guard.rs +31 -24
- data/ext/makiri/rust/src/lexbor/css_match/compile.rs +71 -28
- data/ext/makiri/rust/src/lexbor/css_match/mod.rs +6 -0
- data/ext/makiri/rust/src/lexbor/css_match/query.rs +27 -43
- data/ext/makiri/rust/src/lexbor/css_match/scratch.rs +1 -1
- data/ext/makiri/rust/src/lexbor/css_match/simple.rs +4 -4
- data/ext/makiri/rust/src/lexbor/fragment.rs +22 -24
- data/ext/makiri/rust/src/lexbor/stylesheet.rs +50 -23
- data/ext/makiri/rust/src/lexbor/tests.rs +21 -9
- data/ext/makiri/rust/src/xml/arena.rs +107 -13
- data/ext/makiri/rust/src/xml/attr_key.rs +17 -4
- data/ext/makiri/rust/src/xml/chars/expand.rs +126 -25
- data/ext/makiri/rust/src/xml/chars/mod.rs +1 -1
- data/ext/makiri/rust/src/xml/dom_name.rs +4 -3
- data/ext/makiri/rust/src/xml/mod.rs +1 -0
- data/ext/makiri/rust/src/xml/model.rs +37 -30
- data/ext/makiri/rust/src/xml/mutate/attr.rs +58 -31
- data/ext/makiri/rust/src/xml/mutate/copy.rs +219 -58
- data/ext/makiri/rust/src/xml/mutate/edit.rs +2 -0
- data/ext/makiri/rust/src/xml/mutate/factory.rs +70 -16
- data/ext/makiri/rust/src/xml/mutate/insert.rs +15 -14
- data/ext/makiri/rust/src/xml/mutate/mod.rs +8 -5
- data/ext/makiri/rust/src/xml/mutate/ns.rs +87 -138
- data/ext/makiri/rust/src/xml/ns_scope.rs +104 -0
- data/ext/makiri/rust/src/xml/qname.rs +66 -1
- data/ext/makiri/rust/src/xml/selftest.rs +20 -6
- data/ext/makiri/rust/src/xml/serialize/c14n.rs +22 -12
- data/ext/makiri/rust/src/xml/serialize/mod.rs +20 -15
- data/ext/makiri/rust/src/xml/serialize/xml.rs +23 -13
- data/ext/makiri/rust/src/xml/tree/dtd.rs +15 -33
- data/ext/makiri/rust/src/xml/tree/mod.rs +30 -37
- data/ext/makiri/rust/src/xml/xpath.rs +9 -8
- data/ext/makiri/rust/src/xpath/order.rs +73 -4
- data/ext/makiri/rust/src/xpath/verify.rs +8 -53
- data/lib/makiri/html/document.rb +45 -21
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/document.rb +10 -16
- data/script/check_unsafe_boundaries.rb +69 -41
- data/suppressions/ruby.supp +14 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: baaa765e2bd6208137962299994628a55a36ab68e29f9de9921e686679c69bf2
|
|
4
|
+
data.tar.gz: cd74ab123b1f8384d7aa14c448f1f12ee78371335061b60728c06424ccc5973b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ce8aa721e0e6848e2f7015f924f510599064f309c590181abef990e9b81121a1675aade8917515fdc35577bb65bb5edcf0d72ce4582df686eb0156d461d5e7b
|
|
7
|
+
data.tar.gz: 692a2a400e1be9ecc12fcd67cf317f5d7e51eef38c2846f25ab3bc23dd6626acdf02d860309e4444bf1b2b646461e8a32934804ebc8d1cc3f9a8593d729a79d8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0] - 2026-10-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
* `Document#attribute_version` (HTML and XML): an Integer that increases
|
|
8
|
+
whenever an attribute is added, removed or set (to the same value too),
|
|
9
|
+
including through an Attr node's `content=` or `remove`. Child-list and
|
|
10
|
+
text edits leave it as it is, so it can key a cache of attributes.
|
|
11
|
+
* `Makiri::XML::Document#create_element_ns(namespace_uri, qualified_name)`,
|
|
12
|
+
like the HTML Document's and the DOM's `createElementNS`. The element keeps
|
|
13
|
+
its namespace wherever it is inserted; a name that is not an XML QName
|
|
14
|
+
(`f}oo`) makes an element that `to_xml` refuses.
|
|
15
|
+
* `Makiri::HTML::Document.new`: an empty HTML document in no-quirks mode.
|
|
16
|
+
* `Makiri::HTML::Document#quirks_mode?` and `#compat_mode` (`"BackCompat"` /
|
|
17
|
+
`"CSS1Compat"`, like the DOM's `compatMode`).
|
|
18
|
+
* HTML `Attr#remove` / `#unlink` remove the attribute from its element, as on
|
|
19
|
+
XML. They raised "use delete(name) to remove an attribute".
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
* `tree_version` counts child-list changes only. A Text, Comment, CDATA or
|
|
24
|
+
PI node's `content=` no longer moves it, and an HTML Attr node's
|
|
25
|
+
`content=` moves `attribute_version` instead.
|
|
26
|
+
* `tree_version` and `attribute_version` are documented as cache keys: an
|
|
27
|
+
unchanged version means nothing it covers changed, while a changed one may
|
|
28
|
+
be conservative (an edit refused part-way can still move it).
|
|
29
|
+
* `namespace_uri` and `prefix` are documented as `nil`, never `""`, when
|
|
30
|
+
there is no namespace or prefix, in HTML and XML.
|
|
31
|
+
* `Document#dup` copies the tree node by node instead of re-parsing its
|
|
32
|
+
serialization. An HTML copy keeps a tree with no `<html>` root as it is and
|
|
33
|
+
keeps the quirks mode, and `Node#line` on it is `nil`. An XML copy keeps
|
|
34
|
+
every name and namespace as written (a re-parse invented `ns1:` prefixes),
|
|
35
|
+
copies data XML cannot write, and keeps the original's `max_bytes`.
|
|
36
|
+
* XML `create_processing_instruction` accepts the target `xml` (in any case),
|
|
37
|
+
as the DOM does; `to_xml` and `canonicalize` refuse a tree holding one.
|
|
38
|
+
* HTML `create_processing_instruction` raises `ArgumentError`, as XML does,
|
|
39
|
+
for data containing `?>`. It raised `Makiri::Error`.
|
|
40
|
+
* A refused XML insertion names the DOM rule it breaks, in the same words as
|
|
41
|
+
HTML. It said "invalid placement".
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
* An Attr's `content=` and `remove` raise `FrozenError` when the element
|
|
46
|
+
that owns it is frozen, in HTML and XML.
|
|
47
|
+
* XML `Attr#content=` sets the attribute's value. It raised "operation
|
|
48
|
+
unsupported".
|
|
49
|
+
* HTML `title=` and `meta_encoding=` do nothing on a document with no
|
|
50
|
+
`<head>`, as the DOM's title setter does. They inserted the element directly
|
|
51
|
+
under the root element.
|
|
52
|
+
* HTML `Document#root` and CSS `:root` return the document element, or `nil`.
|
|
53
|
+
Without an `<html>` element they returned the first child of any kind, such
|
|
54
|
+
as a comment, and `title=` raised on such a document.
|
|
55
|
+
* An edit refused for its arguments - a frozen or non-node argument to an
|
|
56
|
+
HTML insertion, an invalid name given to XML `[]=`, `set_attribute_ns` or
|
|
57
|
+
`set_loose_dom_attribute`, a refused HTML `Attr#remove` - no longer moves a
|
|
58
|
+
version.
|
|
59
|
+
* An XML copy that runs out of the document's byte budget (`import_node`,
|
|
60
|
+
`clone_node`, an insertion from another document) gives back what it used.
|
|
61
|
+
The document could not be edited any more after one.
|
|
62
|
+
* Importing HTML into an XML document stores each namespace URI once instead
|
|
63
|
+
of once per element, which used up the byte budget faster.
|
|
64
|
+
* An HTML Attr imported on its own from another HTML document keeps its
|
|
65
|
+
namespace URI as written. It was lower-cased.
|
|
66
|
+
* `to_xml` and `canonicalize` write an element in the XML namespace as
|
|
67
|
+
`xml:local`. They wrote a declaration that does not parse. An element in
|
|
68
|
+
the XMLNS namespace is refused with `Makiri::Error`.
|
|
69
|
+
* XML XPath: a prefixed attribute on a detached element, whose namespace is
|
|
70
|
+
not decided yet, no longer matches an unprefixed name test.
|
|
71
|
+
* XML: references are read by one grammar everywhere. The internal subset
|
|
72
|
+
accepted a character reference to a character XML does not allow
|
|
73
|
+
(`<!ENTITY x "�">`), and a malformed reference next to an entity
|
|
74
|
+
reference was reported as an unsupported DTD construct instead of as
|
|
75
|
+
malformed XML.
|
|
76
|
+
|
|
3
77
|
## [0.13.0] - 2026-10-03
|
|
4
78
|
|
|
5
79
|
### Added
|
data/README.md
CHANGED
|
@@ -184,6 +184,12 @@ and unusually large documents can raise it with `max_bytes:`.
|
|
|
184
184
|
Makiri::XML(huge_xml, max_bytes: 512 * 1024 * 1024) # also Makiri::XML::Document.parse(..., max_bytes:)
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
The limit covers the document's whole life, not only the parse: nodes and
|
|
188
|
+
values made by editing it count too, and a replaced value or a removed node
|
|
189
|
+
keeps its bytes until the document is gone. A document edited for a long time
|
|
190
|
+
can be compacted with `dup`, which copies only what is in it now, under the
|
|
191
|
+
same limit.
|
|
192
|
+
|
|
187
193
|
### Node identity
|
|
188
194
|
|
|
189
195
|
Navigating to the same node always gives the same Ruby object, for as long as
|
data/ext/makiri/rust/build.rs
CHANGED
|
@@ -193,7 +193,6 @@ fn main() {
|
|
|
193
193
|
.allowlist_function("lexbor_hash_insert")
|
|
194
194
|
.allowlist_var("lexbor_hash_search_raw")
|
|
195
195
|
.allowlist_var("lexbor_hash_insert_raw")
|
|
196
|
-
.allowlist_function("lxb_dom_document_root")
|
|
197
196
|
.allowlist_type("lxb_html_token_t")
|
|
198
197
|
.allowlist_type("lxb_html_token_type_t")
|
|
199
198
|
// The token-type FLAGS are in `enum lxb_html_token_type` - no trailing
|
|
@@ -283,6 +282,7 @@ fn main() {
|
|
|
283
282
|
.allowlist_function("lxb_dom_document_type_create")
|
|
284
283
|
.allowlist_function("lxb_dom_document_import_node")
|
|
285
284
|
.allowlist_function("lexbor_str_init")
|
|
285
|
+
.allowlist_function("lxb_html_document_create")
|
|
286
286
|
.allowlist_function("lxb_html_document_destroy")
|
|
287
287
|
// The document title reader, generated rather than hand-declared like
|
|
288
288
|
// the rest of this list.
|
|
@@ -416,12 +416,6 @@ const UNDECLARED_EXPORTS: &[(&str, &str, &str, &str)] = &[
|
|
|
416
416
|
"lxb_dom_element_t *element, const lxb_char_t *prefix, size_t prefix_len, \
|
|
417
417
|
const lxb_char_t *lname, size_t lname_len",
|
|
418
418
|
),
|
|
419
|
-
(
|
|
420
|
-
"lexbor/ns/ns.c",
|
|
421
|
-
"LXB_API const lxb_ns_data_t *",
|
|
422
|
-
"lxb_ns_append",
|
|
423
|
-
"lexbor_hash_t *hash, const lxb_char_t *link, size_t length",
|
|
424
|
-
),
|
|
425
419
|
];
|
|
426
420
|
|
|
427
421
|
fn check_undeclared_exports(include: &std::path::Path) {
|
|
@@ -34,8 +34,8 @@ use crate::bridge::xml::xml_node_document;
|
|
|
34
34
|
use crate::bridge::xml::{xml_mut_result, xml_node_unwrap};
|
|
35
35
|
use crate::lexbor::adapter::cross_import::cross_xml_to_html;
|
|
36
36
|
use crate::lexbor::adapter::html::{RawDoc, RawNode};
|
|
37
|
-
use crate::lexbor::adapter::post_parse::{parse_html, HtmlParseError};
|
|
38
|
-
use crate::lexbor::adapter::tree_guard::{DepthLimit, MAX_SELECT_OPTIONS};
|
|
37
|
+
use crate::lexbor::adapter::post_parse::{empty_html_document, parse_html, HtmlParseError};
|
|
38
|
+
use crate::lexbor::adapter::tree_guard::{DepthLimit, GuardStop, MAX_SELECT_OPTIONS};
|
|
39
39
|
|
|
40
40
|
/* ------------------------------------------------------------------ *
|
|
41
41
|
* parsing *
|
|
@@ -49,13 +49,24 @@ pub fn select_options_error() -> Error {
|
|
|
49
49
|
))
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/// The error for a parse the guard refused (`GuardStop`), for a document and
|
|
53
|
+
/// a fragment alike: the one place that maps a stop to its exception.
|
|
54
|
+
pub fn guard_error(stop: GuardStop, limit: DepthLimit) -> Error {
|
|
55
|
+
match stop {
|
|
56
|
+
GuardStop::TooDeep => tree_depth_error(limit),
|
|
57
|
+
GuardStop::TooManyOptions => select_options_error(),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
/// The error for a parse the tree-depth limit refused: `Makiri::Error`, naming
|
|
53
62
|
/// the limit, for a document and a fragment alike.
|
|
54
63
|
pub fn tree_depth_error(limit: DepthLimit) -> Error {
|
|
55
64
|
match limit.max_depth() {
|
|
56
65
|
Some(n) => makiri_error(format!("document tree depth limit exceeded ({n})")),
|
|
57
|
-
/*
|
|
58
|
-
None =>
|
|
66
|
+
/* An unlimited parse is never refused for depth. */
|
|
67
|
+
None => crate::bridge::ruby::internal_error(
|
|
68
|
+
"a parse with no tree-depth limit was refused for depth",
|
|
69
|
+
),
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
@@ -91,8 +102,7 @@ pub fn parse_document(source: Value, limit: DepthLimit) -> Result<Value, Error>
|
|
|
91
102
|
drop(owned);
|
|
92
103
|
|
|
93
104
|
let parsed = result.map_err(|e| match e {
|
|
94
|
-
HtmlParseError::
|
|
95
|
-
HtmlParseError::TooManyOptions => select_options_error(),
|
|
105
|
+
HtmlParseError::Guard(stop) => guard_error(stop, limit),
|
|
96
106
|
HtmlParseError::Failed => makiri_error("failed to parse HTML document"),
|
|
97
107
|
})?;
|
|
98
108
|
/* The GC learns the arena's size in `install`; `owned` is already gone, so
|
|
@@ -100,12 +110,24 @@ pub fn parse_document(source: Value, limit: DepthLimit) -> Result<Value, Error>
|
|
|
100
110
|
Ok(shell.install_html(parsed))
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
/// An empty HTML document - no children - in `compat_mode` (Lexbor's
|
|
114
|
+
/// numbering): `Makiri::HTML::Document.new` (no-quirks) and the copy
|
|
115
|
+
/// `Document#dup` fills.
|
|
116
|
+
pub fn new_empty_document(compat_mode: u32) -> Result<Value, Error> {
|
|
117
|
+
/* The wrapper first, while nothing needs freeing - see DocumentShell. */
|
|
118
|
+
let shell = DocumentShell::new(DocKind::Html);
|
|
119
|
+
let parsed = empty_html_document(compat_mode)
|
|
120
|
+
.map_err(|_| makiri_error("failed to create HTML document"))?;
|
|
121
|
+
Ok(shell.install_html(parsed))
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
/* ------------------------------------------------------------------ *
|
|
104
125
|
* read-only accessors *
|
|
105
126
|
* ------------------------------------------------------------------ */
|
|
106
127
|
|
|
107
|
-
/// `Document#root`: the root Element node, or nil
|
|
108
|
-
/// parser inserts html/head/body even for empty
|
|
128
|
+
/// `Document#root`: the root Element node, or nil - for `Document.new`, or once
|
|
129
|
+
/// the root is removed (the HTML parser inserts html/head/body even for empty
|
|
130
|
+
/// input).
|
|
109
131
|
pub fn document_root(rb_doc: Value) -> Result<Option<Value>, Error> {
|
|
110
132
|
let Some(root) = html_doc(&rb_doc).as_node().document_root() else {
|
|
111
133
|
return Ok(None);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! The errors the DOM's insertion rules raise, worded once for HTML and XML.
|
|
2
|
+
//!
|
|
3
|
+
//! Both representations keep a refusal's reason as a
|
|
4
|
+
//! [`PreInsertError`](crate::dom_rules::PreInsertError) all the way here, so
|
|
5
|
+
//! the same broken rule reads the same in either - XML used to fold every rule
|
|
6
|
+
//! but two into one sentence.
|
|
7
|
+
|
|
8
|
+
#![forbid(unsafe_code)]
|
|
9
|
+
|
|
10
|
+
use crate::bridge::ruby::makiri_error;
|
|
11
|
+
use crate::dom_rules::{Hierarchy as H, PreInsertError, Violation};
|
|
12
|
+
use magnus::Error;
|
|
13
|
+
|
|
14
|
+
/// The `Makiri::Error` an insertion `e` refused raises.
|
|
15
|
+
pub fn pre_insert_error(e: PreInsertError) -> Error {
|
|
16
|
+
makiri_error(message(e))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn message(e: PreInsertError) -> &'static str {
|
|
20
|
+
match e {
|
|
21
|
+
PreInsertError::NoParent { replacing: true } => "cannot replace a node with no parent",
|
|
22
|
+
PreInsertError::NoParent { replacing: false } => {
|
|
23
|
+
"cannot add a sibling to a node with no parent"
|
|
24
|
+
}
|
|
25
|
+
/* Unreachable through either representation's insertion, which takes
|
|
26
|
+
* the reference child from the parent it names; worded all the same. */
|
|
27
|
+
PreInsertError::Rule(Violation::NotFound) => {
|
|
28
|
+
"the reference node is not a child of the parent"
|
|
29
|
+
}
|
|
30
|
+
PreInsertError::Rule(Violation::HierarchyRequest(h)) => match h {
|
|
31
|
+
H::ParentNotContainer => {
|
|
32
|
+
"only a document, a document fragment or an element can have children"
|
|
33
|
+
}
|
|
34
|
+
H::Ancestor => "cannot insert a node into its own subtree",
|
|
35
|
+
H::AttributeNode => "an attribute node cannot be inserted into the tree",
|
|
36
|
+
H::DocumentNode => "a document node cannot be inserted into the tree",
|
|
37
|
+
H::UnsupportedNode => "this kind of node cannot be inserted into the tree",
|
|
38
|
+
H::DoctypeParent => "a doctype node can only be a child of the document",
|
|
39
|
+
H::DuplicateDoctype => "the document already has a doctype",
|
|
40
|
+
H::DoctypeAfterElement | H::ElementBeforeDoctype => {
|
|
41
|
+
"a doctype must precede the document element"
|
|
42
|
+
}
|
|
43
|
+
H::SecondDocumentElement => "the document already has a root element",
|
|
44
|
+
H::TextUnderDocument => "text cannot be a child of the document",
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -28,8 +28,7 @@ use crate::lexbor::fragment::{FragmentContext, FragmentError, TransientFragment}
|
|
|
28
28
|
/// A fragment-parse failure as `Makiri::Error`.
|
|
29
29
|
fn fragment_error(e: FragmentError, limit: DepthLimit) -> Error {
|
|
30
30
|
match e {
|
|
31
|
-
FragmentError::
|
|
32
|
-
FragmentError::TooManyOptions => crate::bridge::doc::select_options_error(),
|
|
31
|
+
FragmentError::Guard(stop) => crate::bridge::doc::guard_error(stop, limit),
|
|
33
32
|
_ => makiri_error(e.message()),
|
|
34
33
|
}
|
|
35
34
|
}
|
|
@@ -107,8 +107,7 @@ pub fn without_gvl<F: FnOnce() -> R + Send, R>(f: F) -> Result<R, magnus::Error>
|
|
|
107
107
|
}
|
|
108
108
|
if slot.f.is_none() {
|
|
109
109
|
/* It ran and neither answered nor panicked - which `run` cannot do. */
|
|
110
|
-
return Err(
|
|
111
|
-
crate::init::EXC_INTERNAL_ERROR.exception(),
|
|
110
|
+
return Err(crate::bridge::ruby::internal_error(
|
|
112
111
|
"the GVL-released body ran without a result",
|
|
113
112
|
));
|
|
114
113
|
}
|
|
@@ -17,8 +17,7 @@ use crate::init::{
|
|
|
17
17
|
CLASS_HTML_PROCESSING_INSTRUCTION, CLASS_HTML_TEXT, CLASS_XML_DOCUMENT,
|
|
18
18
|
};
|
|
19
19
|
use crate::lexbor::adapter::html::{
|
|
20
|
-
ForeignNode, HtmlNode, HtmlNodeKey, HtmlNodeMut, Insertion, NodeType, Place,
|
|
21
|
-
RawDoc, RawNode,
|
|
20
|
+
ForeignNode, HtmlNode, HtmlNodeKey, HtmlNodeMut, Insertion, NodeType, Place, RawDoc, RawNode,
|
|
22
21
|
};
|
|
23
22
|
use crate::lexbor::fragment::import_with_fixup;
|
|
24
23
|
|
|
@@ -282,7 +281,8 @@ pub(in crate::bridge) fn with_html_node<R>(
|
|
|
282
281
|
|
|
283
282
|
/// The receiver cleared for an edit - not frozen, its document not under
|
|
284
283
|
/// evaluation - and the PROOF of it: [`edit`] is the only way to build one and
|
|
285
|
-
/// [`HtmlEdit::node`] the only way to spend
|
|
284
|
+
/// [`HtmlEdit::node`] (or its attribute and data twins) the only way to spend
|
|
285
|
+
/// it, ONCE - they take `self`. The XML side's `Editing`, spent the same way.
|
|
286
286
|
///
|
|
287
287
|
/// The checks and the index drop are two steps on purpose. The checks come
|
|
288
288
|
/// first, so a frozen receiver is reported before a bad argument. The drop
|
|
@@ -298,12 +298,26 @@ pub struct HtmlEdit<'a> {
|
|
|
298
298
|
/// before [`HtmlEdit::node`].
|
|
299
299
|
pub fn edit(this: &HtmlSelf) -> Result<HtmlEdit<'_>, Error> {
|
|
300
300
|
crate::bridge::ruby::check_frozen(this.value)?;
|
|
301
|
+
check_attr_owner_frozen(this)?;
|
|
301
302
|
ensure_document_mutable(this.document)?;
|
|
302
303
|
/* Before any argument is converted: see `account_growth`. */
|
|
303
304
|
crate::bridge::wrapper::account_growth(this.document);
|
|
304
305
|
Ok(HtmlEdit { this })
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
/// An Attr receiver's edit changes its OWNER's attribute list, so a frozen
|
|
309
|
+
/// owner refuses it as it refuses `delete` - checked with the receiver's own
|
|
310
|
+
/// frozen flag, before and after the arguments are converted.
|
|
311
|
+
fn check_attr_owner_frozen(this: &HtmlSelf) -> Result<(), Error> {
|
|
312
|
+
let node = this.node();
|
|
313
|
+
match (node.node_type(), node.parent()) {
|
|
314
|
+
(NodeType::Attribute, Some(owner)) => {
|
|
315
|
+
crate::bridge::wrapper::check_node_frozen(this.document, RawNode::from(owner))
|
|
316
|
+
}
|
|
317
|
+
_ => Ok(()),
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
307
321
|
impl<'a> HtmlEdit<'a> {
|
|
308
322
|
/// The receiver's node, read-only, for a check that no argument can
|
|
309
323
|
/// change (its node type).
|
|
@@ -325,25 +339,34 @@ impl<'a> HtmlEdit<'a> {
|
|
|
325
339
|
/// to engine calls and checks that call no Ruby (`insert` reads its
|
|
326
340
|
/// argument's node and frozen flag there, and nothing more).
|
|
327
341
|
///
|
|
328
|
-
/// It counts as a change to a child list ([`
|
|
329
|
-
/// attribute edit takes [`HtmlEdit::node_for_attributes`] instead
|
|
342
|
+
/// It counts as a change to a child list ([`record_edit`]); an
|
|
343
|
+
/// attribute edit takes [`HtmlEdit::node_for_attributes`] instead, and a
|
|
344
|
+
/// character-data edit [`HtmlEdit::node_for_data`].
|
|
330
345
|
pub fn node(self) -> Result<HtmlNodeMut<'a>, Error> {
|
|
331
|
-
self.mutable(
|
|
346
|
+
self.mutable(EditKind::ChildList)
|
|
332
347
|
}
|
|
333
348
|
|
|
334
|
-
/// [`HtmlEdit::node`] for an edit of
|
|
335
|
-
/// changes no child list
|
|
349
|
+
/// [`HtmlEdit::node`] for an edit of ATTRIBUTES only - an element's, or an
|
|
350
|
+
/// Attr node's value - which changes no child list: it counts towards the
|
|
351
|
+
/// attribute version ([`record_edit`]) instead of the tree
|
|
352
|
+
/// version.
|
|
336
353
|
pub fn node_for_attributes(self) -> Result<HtmlNodeMut<'a>, Error> {
|
|
337
|
-
self.mutable(
|
|
354
|
+
self.mutable(EditKind::Attributes)
|
|
338
355
|
}
|
|
339
356
|
|
|
340
|
-
|
|
357
|
+
/// [`HtmlEdit::node`] for an edit of a Text, Comment, CDATA or PI node's
|
|
358
|
+
/// DATA, which changes no child list and no attribute: no version counts
|
|
359
|
+
/// it (see [`EditKind::CharacterData`]).
|
|
360
|
+
pub fn node_for_data(self) -> Result<HtmlNodeMut<'a>, Error> {
|
|
361
|
+
self.mutable(EditKind::CharacterData)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn mutable(self, kind: EditKind) -> Result<HtmlNodeMut<'a>, Error> {
|
|
341
365
|
crate::bridge::ruby::check_frozen(self.this.value)?;
|
|
366
|
+
check_attr_owner_frozen(self.this)?;
|
|
342
367
|
ensure_document_mutable(self.this.document)?;
|
|
343
368
|
invalidate_indexes(self.this.document);
|
|
344
|
-
|
|
345
|
-
bump_tree_version(self.this.document);
|
|
346
|
-
}
|
|
369
|
+
record_edit(self.this.document, kind);
|
|
347
370
|
// SAFETY: the receiver is not frozen and no XPath evaluation is
|
|
348
371
|
// reading its document - both checked just now.
|
|
349
372
|
Ok(unsafe { HtmlNodeMut::assume_mutable(self.this.raw().as_node()) })
|
|
@@ -379,16 +402,16 @@ fn adopt_copy<'d>(doc: RawDoc, node: HtmlNode<'_>) -> Result<HtmlNode<'d>, Error
|
|
|
379
402
|
/// document's indexes, which still list it. A structural change to a document
|
|
380
403
|
/// invalidates ITS indexes; this is one, made from another document's method.
|
|
381
404
|
fn adopt_release(src: Value) -> Result<(), Error> {
|
|
405
|
+
/* Invalidated and recorded before the release (`record_edit`); the node
|
|
406
|
+
* borrow below is taken after, as dropping the indexes borrows too. */
|
|
407
|
+
let src_doc = keepalive_document(src)?;
|
|
408
|
+
invalidate_indexes(src_doc);
|
|
409
|
+
record_edit(src_doc, EditKind::ChildList);
|
|
382
410
|
with_arg_node(src, |node| {
|
|
383
411
|
/* SAFETY: the source document was cleared for editing by
|
|
384
412
|
* `take_incoming` before anything was copied out of it. */
|
|
385
413
|
release_from_tree(unsafe { HtmlNodeMut::assume_mutable(node) });
|
|
386
|
-
})
|
|
387
|
-
/* After the borrow `with_arg_node` held: dropping them borrows again. */
|
|
388
|
-
let src_doc = keepalive_document(src)?;
|
|
389
|
-
invalidate_indexes(src_doc);
|
|
390
|
-
bump_tree_version(src_doc);
|
|
391
|
-
Ok(())
|
|
414
|
+
})
|
|
392
415
|
}
|
|
393
416
|
|
|
394
417
|
fn release_from_tree(node: HtmlNodeMut<'_>) {
|
|
@@ -412,7 +435,7 @@ fn release_from_tree(node: HtmlNodeMut<'_>) {
|
|
|
412
435
|
/// [`Insertion::check`], the shared `crate::dom_rules`); after that only the adoption copy can fail, and it
|
|
413
436
|
/// too runs before a link is touched.
|
|
414
437
|
pub fn insert(this: &HtmlSelf, rb_incoming: Value, place: Place) -> Result<Value, Error> {
|
|
415
|
-
let
|
|
438
|
+
let edit = edit(this)?;
|
|
416
439
|
let (key, incoming_doc) = html_node_key(rb_incoming)?;
|
|
417
440
|
/* The argument is relinked too - `place` changes its parent and siblings, and
|
|
418
441
|
* an adoption removes it from its own document - so a frozen argument is a
|
|
@@ -422,6 +445,12 @@ pub fn insert(this: &HtmlSelf, rb_incoming: Value, place: Place) -> Result<Value
|
|
|
422
445
|
* checked, because frozenness lives on the Ruby object and there is no map
|
|
423
446
|
* from a node back to its wrapper. */
|
|
424
447
|
crate::bridge::ruby::check_frozen(rb_incoming)?;
|
|
448
|
+
/* An adoption changes the argument's own document too (see
|
|
449
|
+
* `take_incoming`, which checks again). Refused here as well, so every
|
|
450
|
+
* refusal about the argument comes before `node` records the edit, as the
|
|
451
|
+
* XML side's does. */
|
|
452
|
+
ensure_document_mutable(incoming_doc)?;
|
|
453
|
+
let target = edit.node()?;
|
|
425
454
|
/* The argument, resolved against its own Document, for the checks, the
|
|
426
455
|
* copy or move, and the placing - none of which runs Ruby or wraps a
|
|
427
456
|
* node. Releasing an adopted original and wrapping its copy borrow a
|
|
@@ -429,7 +458,7 @@ pub fn insert(this: &HtmlSelf, rb_incoming: Value, place: Place) -> Result<Value
|
|
|
429
458
|
let (placed, adopted) = with_html_node(incoming_doc, key, |incoming| {
|
|
430
459
|
Insertion::new(target.node(), place, incoming)
|
|
431
460
|
.and_then(|i| i.check())
|
|
432
|
-
.map_err(
|
|
461
|
+
.map_err(crate::bridge::dom_error::pre_insert_error)?;
|
|
433
462
|
let (node, adopted) = take_incoming(target, incoming_doc, incoming)?;
|
|
434
463
|
target.place(node, place);
|
|
435
464
|
Ok::<_, Error>((RawNode::from(node.node()), adopted))
|
|
@@ -441,38 +470,6 @@ pub fn insert(this: &HtmlSelf, rb_incoming: Value, place: Place) -> Result<Value
|
|
|
441
470
|
wrap_html_node(placed, this.document)
|
|
442
471
|
}
|
|
443
472
|
|
|
444
|
-
/// A refused insertion, worded. The one place these messages live.
|
|
445
|
-
fn refused(e: PreInsertError, place: Place) -> Error {
|
|
446
|
-
use crate::dom_rules::{Hierarchy as H, Violation};
|
|
447
|
-
makiri_error(match e {
|
|
448
|
-
PreInsertError::NoParent if place == Place::Replace => {
|
|
449
|
-
"cannot replace a node with no parent"
|
|
450
|
-
}
|
|
451
|
-
PreInsertError::NoParent => "cannot add a sibling to a node with no parent",
|
|
452
|
-
/* Unreachable through `Insertion::new`, which takes the reference child
|
|
453
|
-
* from the parent it names; worded all the same. */
|
|
454
|
-
PreInsertError::Rule(Violation::NotFound) => {
|
|
455
|
-
"the reference node is not a child of the parent"
|
|
456
|
-
}
|
|
457
|
-
PreInsertError::Rule(Violation::HierarchyRequest(h)) => match h {
|
|
458
|
-
H::ParentNotContainer => {
|
|
459
|
-
"only a document, a document fragment or an element can have children"
|
|
460
|
-
}
|
|
461
|
-
H::Ancestor => "cannot insert a node into its own subtree",
|
|
462
|
-
H::AttributeNode => "an attribute node cannot be inserted into the tree",
|
|
463
|
-
H::DocumentNode => "a document node cannot be inserted into the tree",
|
|
464
|
-
H::UnsupportedNode => "this kind of node cannot be inserted into the tree",
|
|
465
|
-
H::DoctypeParent => "a doctype node can only be a child of the document",
|
|
466
|
-
H::DuplicateDoctype => "the document already has a doctype",
|
|
467
|
-
H::DoctypeAfterElement | H::ElementBeforeDoctype => {
|
|
468
|
-
"a doctype must precede the document element"
|
|
469
|
-
}
|
|
470
|
-
H::SecondDocumentElement => "the document already has a root element",
|
|
471
|
-
H::TextUnderDocument => "text cannot be a child of the document",
|
|
472
|
-
},
|
|
473
|
-
})
|
|
474
|
-
}
|
|
475
|
-
|
|
476
473
|
/// The node to put in the tree for `incoming`: itself, taken out of where it
|
|
477
474
|
/// was, or - from another document - a copy made in `target`'s, with `true`
|
|
478
475
|
/// for "release the original once the copy is in" (see [`adopt_release`]).
|
|
@@ -59,6 +59,9 @@ pub mod node_set;
|
|
|
59
59
|
#[cfg(feature = "lexbor")]
|
|
60
60
|
pub mod node_wrap;
|
|
61
61
|
|
|
62
|
+
/// The errors the DOM's insertion rules raise, for HTML and XML alike.
|
|
63
|
+
pub mod dom_error;
|
|
64
|
+
|
|
62
65
|
/// The Ruby <-> XPath engine seam: which backend a query runs on, and
|
|
63
66
|
/// building the engine context for a Ruby node or document.
|
|
64
67
|
#[cfg(feature = "lexbor")]
|
|
@@ -33,6 +33,13 @@ pub fn makiri_error(msg: impl Into<std::borrow::Cow<'static, str>>) -> Error {
|
|
|
33
33
|
Error::new(error_class(), msg)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/// A `Makiri::InternalError` carrying `msg`: a broken invariant of Makiri
|
|
37
|
+
/// itself, which a bare `rescue` (StandardError) passes through - see
|
|
38
|
+
/// [`entry`].
|
|
39
|
+
pub fn internal_error(msg: impl Into<std::borrow::Cow<'static, str>>) -> Error {
|
|
40
|
+
Error::new(crate::init::EXC_INTERNAL_ERROR.exception(), msg)
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
/// Is `v` an instance of the class in `klass`? `false` before `Init_makiri`,
|
|
37
44
|
/// when no class of ours exists for it to be an instance of.
|
|
38
45
|
#[inline]
|
|
@@ -280,8 +287,7 @@ pub fn bool_value(v: VALUE) -> Option<bool> {
|
|
|
280
287
|
pub fn entry<T>(f: impl FnOnce() -> Result<T, Error>) -> Result<T, Error> {
|
|
281
288
|
match std::panic::catch_unwind(core::panic::AssertUnwindSafe(f)) {
|
|
282
289
|
Ok(out) => out,
|
|
283
|
-
Err(payload) => Err(
|
|
284
|
-
crate::init::EXC_INTERNAL_ERROR.exception(),
|
|
290
|
+
Err(payload) => Err(internal_error(
|
|
285
291
|
crate::caught::message(payload.as_ref()).to_owned(),
|
|
286
292
|
)),
|
|
287
293
|
}
|
|
@@ -541,22 +541,46 @@ pub struct Encoding(*mut rb_sys::rb_encoding);
|
|
|
541
541
|
|
|
542
542
|
impl Encoding {
|
|
543
543
|
/// The encoding `str` is tagged with.
|
|
544
|
-
fn of(str: RString) -> Encoding {
|
|
544
|
+
pub(super) fn of(str: RString) -> Encoding {
|
|
545
545
|
// SAFETY: a live String, by type; reading its tag runs no Ruby.
|
|
546
546
|
Encoding(unsafe { rb_sys::rb_enc_get(str.as_raw()) })
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
fn utf8() -> Encoding {
|
|
549
|
+
pub(super) fn utf8() -> Encoding {
|
|
550
550
|
// SAFETY: Ruby's immutable global encoding.
|
|
551
551
|
Encoding(unsafe { rb_sys::rb_utf8_encoding() })
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
|
|
554
|
+
/// The encoding Ruby knows by `name`, or None for a name it does not
|
|
555
|
+
/// know - not an error. MAY AUTOLOAD the encoding, a Ruby allocation and so
|
|
556
|
+
/// a GC point: call it with no borrow of a String's bytes held.
|
|
557
|
+
///
|
|
558
|
+
/// `rb_enc_find` wants a C string and the name is bytes, so the NUL is
|
|
559
|
+
/// added here. A name too long to fit, or holding a NUL, is not one Ruby
|
|
560
|
+
/// knows, so it reads as None rather than being truncated at the NUL.
|
|
561
|
+
pub(super) fn find(name: &[u8]) -> Option<Encoding> {
|
|
562
|
+
let mut buf = [0u8; 64];
|
|
563
|
+
if name.is_empty() || name.len() >= buf.len() || name.contains(&0) {
|
|
564
|
+
return None;
|
|
565
|
+
}
|
|
566
|
+
buf[..name.len()].copy_from_slice(name);
|
|
567
|
+
let c = core::ffi::CStr::from_bytes_with_nul(&buf[..name.len() + 1]).ok()?;
|
|
568
|
+
// SAFETY: a NUL-terminated name; the lookup raises nothing.
|
|
569
|
+
let enc = unsafe { rb_sys::rb_enc_find(c.as_ptr()) };
|
|
570
|
+
(!enc.is_null()).then_some(Encoding(enc))
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// The raw encoding, for a C call in the bridge that takes one.
|
|
574
|
+
pub(super) fn as_raw(self) -> *mut rb_sys::rb_encoding {
|
|
575
|
+
self.0
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
pub(super) fn is_usascii(self) -> bool {
|
|
555
579
|
// SAFETY: Ruby's immutable global encoding.
|
|
556
580
|
self.0 == unsafe { rb_sys::rb_usascii_encoding() }
|
|
557
581
|
}
|
|
558
582
|
|
|
559
|
-
fn is_ascii8bit(self) -> bool {
|
|
583
|
+
pub(super) fn is_ascii8bit(self) -> bool {
|
|
560
584
|
// SAFETY: Ruby's immutable global encoding.
|
|
561
585
|
self.0 == unsafe { rb_sys::rb_ascii8bit_encoding() }
|
|
562
586
|
}
|
|
@@ -573,10 +597,12 @@ impl Encoding {
|
|
|
573
597
|
!self.is_utf8_compatible()
|
|
574
598
|
}
|
|
575
599
|
|
|
576
|
-
/// Whether
|
|
577
|
-
/// or ASCII-8BIT - deliberately raw bytes
|
|
578
|
-
///
|
|
579
|
-
|
|
600
|
+
/// Whether input in this encoding is read as UTF-8 bytes with no
|
|
601
|
+
/// transcode: UTF-8, US-ASCII, or ASCII-8BIT - deliberately raw bytes.
|
|
602
|
+
/// Anything else is transcoded to UTF-8 first. How the bytes are then
|
|
603
|
+
/// checked is each reader's own: HTML repairs invalid UTF-8 to U+FFFD
|
|
604
|
+
/// (`ruby_to_utf8`), XML refuses it (`xml_decode`).
|
|
605
|
+
pub(super) fn reads_as_utf8_bytes(self) -> bool {
|
|
580
606
|
self.is_utf8_compatible() || self.is_ascii8bit()
|
|
581
607
|
}
|
|
582
608
|
|
|
@@ -631,7 +657,7 @@ pub fn to_encoding(v: Value) -> Result<Encoding, Error> {
|
|
|
631
657
|
/// [`ruby_to_utf8_value`]. Called bare, that raise would `longjmp` over the
|
|
632
658
|
/// Rust frames above it.
|
|
633
659
|
unsafe fn ruby_to_utf8(str: RString) -> VALUE {
|
|
634
|
-
if Encoding::of(str).
|
|
660
|
+
if Encoding::of(str).reads_as_utf8_bytes() {
|
|
635
661
|
return str.as_raw();
|
|
636
662
|
}
|
|
637
663
|
const REPLACE: c_int = rb_sys::ruby_econv_flag_type::RUBY_ECONV_INVALID_REPLACE as c_int
|
|
@@ -652,7 +678,7 @@ pub fn ruby_to_utf8_value(s: RString) -> Result<RString, Error> {
|
|
|
652
678
|
let raw = protect(|| unsafe { ruby_to_utf8(s) })?;
|
|
653
679
|
// SAFETY: `rb_str_encode` returns a live value; checked to be a String.
|
|
654
680
|
RString::from_value(unsafe { crate::bridge::ruby::value(raw) })
|
|
655
|
-
.ok_or_else(||
|
|
681
|
+
.ok_or_else(|| crate::bridge::ruby::internal_error("transcoding returned a non-String"))
|
|
656
682
|
}
|
|
657
683
|
|
|
658
684
|
/// A Ruby String as HTML parser input, under the text-input contract: its
|