makiri 0.11.0.rc1 → 0.11.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 +39 -0
- data/NOKOGIRI_DIFFERENCES.md +37 -4
- data/ext/makiri/rust/build.rs +0 -1
- data/ext/makiri/rust/src/bridge/html.rs +18 -0
- data/ext/makiri/rust/src/bridge/string.rs +34 -2
- data/ext/makiri/rust/src/bridge/xml.rs +22 -1
- data/ext/makiri/rust/src/glue/html_node/mod.rs +1 -0
- data/ext/makiri/rust/src/glue/html_node/mutate.rs +70 -37
- data/ext/makiri/rust/src/glue/xml_node/mod.rs +4 -0
- data/ext/makiri/rust/src/glue/xml_node/mutate.rs +34 -10
- data/ext/makiri/rust/src/glue/xml_node/ns.rs +1 -1
- data/ext/makiri/rust/src/glue/xml_node/serialize.rs +15 -1
- data/ext/makiri/rust/src/lexbor/adapter/cross_import.rs +22 -8
- data/ext/makiri/rust/src/lexbor/adapter/html/build.rs +24 -10
- data/ext/makiri/rust/src/lexbor/adapter/html/mod.rs +11 -1
- data/ext/makiri/rust/src/lexbor/css_match/simple.rs +61 -15
- data/ext/makiri/rust/src/lexbor/tests.rs +26 -11
- data/ext/makiri/rust/src/xml/arena.rs +42 -1
- data/ext/makiri/rust/src/xml/dom_name.rs +72 -0
- data/ext/makiri/rust/src/xml/model.rs +21 -5
- data/ext/makiri/rust/src/xml/mutate/attr.rs +85 -5
- data/ext/makiri/rust/src/xml/mutate/factory.rs +22 -15
- data/ext/makiri/rust/src/xml/mutate/mod.rs +3 -1
- data/ext/makiri/rust/src/xml/mutate/ns.rs +2 -2
- data/ext/makiri/rust/src/xml/qname.rs +0 -22
- data/ext/makiri/rust/src/xml/selftest.rs +26 -0
- data/ext/makiri/rust/src/xml/serialize/c14n.rs +20 -3
- data/ext/makiri/rust/src/xml/serialize/mod.rs +62 -7
- data/ext/makiri/rust/src/xml/serialize/xml.rs +29 -8
- data/ext/makiri/rust/src/xml/tree/mod.rs +1 -1
- data/ext/makiri/rust/src/xml/xpath.rs +3 -4
- data/lib/makiri/version.rb +1 -1
- data/script/check_alloc_failures.rb +6 -0
- data/script/check_unsafe_boundaries.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c12838a29d87a6bd9acb72b97e1e25dbdfbdaff37e38609cce6fd562e0f76d7
|
|
4
|
+
data.tar.gz: f4531547107db81c7db834d6c991adcf68b8a0d2fd714e158d69b164140a18bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcdb5e1656c3ab6781c1bae97919b5ce65c5cbaa2bc3a0429a9088483f45407715e060afb2b6478d5ecb8a2d7495c319e3db54acd17f419c728d181a2b9becb4
|
|
7
|
+
data.tar.gz: 784ef5eaac9439e44ea99903544bcefac25869f18dc6312f0629669465f92a8f306f72b1bb62d5671babccfca2374f19c026dbb0da2a4f859f917c9822093b9b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
No code changes since 0.11.0.rc2. Coming from 0.10.x, read the rc2 and rc1
|
|
6
|
+
notes below as well.
|
|
7
|
+
|
|
8
|
+
## [0.11.0.rc2] - 2026-09-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* `Makiri::HTML::Document#create_element_ns(namespace_uri, qualified_name)`
|
|
13
|
+
(DOM `createElementNS`). An SVG or MathML element made this way keeps its
|
|
14
|
+
name's case and behaves like a parsed one. An upper-case name in the HTML
|
|
15
|
+
namespace that names a known element (`BR`, `DIV`) raises `Makiri::Error`.
|
|
16
|
+
* `Makiri::XML::Element#set_loose_dom_attribute(qualified_name, value)` (DOM
|
|
17
|
+
`setAttribute`): the attribute is in no namespace and keeps the name as
|
|
18
|
+
given (`xmlns`, `xlink:href`, `v-on:click`). `to_xml` raises while the
|
|
19
|
+
document holds one XML cannot write.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
* A NUL in an element, attribute, doctype or PI target name raises
|
|
24
|
+
`ArgumentError`, like any other invalid name. A namespace that does not fit
|
|
25
|
+
the name still raises `Makiri::Error`.
|
|
26
|
+
* HTML type selectors are case-sensitive on SVG and MathML elements, as in
|
|
27
|
+
browsers: `fegaussianblur` no longer matches `feGaussianBlur`.
|
|
28
|
+
* `set_attribute_ns` accepts the XML namespace with any prefix or none again
|
|
29
|
+
(rc1 raised); `to_xml` writes such an attribute as `xml:name`.
|
|
30
|
+
* XML `set_attribute_ns` accepts a namespace declaration XML forbids, such as
|
|
31
|
+
`xmlns:foo=""`. It binds nothing, and `to_xml` raises while it is present.
|
|
32
|
+
* XML `create_document_type` accepts any public and system id, and any name
|
|
33
|
+
without whitespace, NUL or `>`. `to_xml` raises if the doctype cannot be
|
|
34
|
+
written as XML.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
* `import_node` from XML to HTML keeps an upper-case XHTML element name
|
|
39
|
+
(`Foo`) instead of lower-casing it, and raises for one that names a known
|
|
40
|
+
element (`BR`).
|
|
41
|
+
|
|
3
42
|
## [0.11.0.rc1] - 2026-09-29
|
|
4
43
|
|
|
5
44
|
### Changed
|
data/NOKOGIRI_DIFFERENCES.md
CHANGED
|
@@ -107,6 +107,19 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
107
107
|
invents one (`ns1`, `ns2`, ...) rather than shadow the other, as browsers do.
|
|
108
108
|
* An element in no namespace stays that way under a default namespace,
|
|
109
109
|
serialized as `xmlns=""`.
|
|
110
|
+
* `create_document_type` takes what the DOM's `createDocumentType` takes -
|
|
111
|
+
any id, and a name without whitespace, NUL or `>` - and `to_xml` refuses a
|
|
112
|
+
doctype XML cannot write (`create_document_type("q", "abcde", %(x"'y))`).
|
|
113
|
+
Nokogiri writes that system id as `"x"'y"`, which parses but reads
|
|
114
|
+
back as the id `x"'y` - a literal expands no references.
|
|
115
|
+
* `set_attribute_ns(XMLNS_NS, "xmlns:foo", "")` - a declaration Namespaces in
|
|
116
|
+
XML forbids, which the DOM's `setAttributeNS` accepts - is kept as an
|
|
117
|
+
attribute that binds nothing, and `to_xml` refuses the tree while it is
|
|
118
|
+
there. `root["xmlns:foo"] = ""` still raises.
|
|
119
|
+
* An attribute in the XML namespace is written as `xml:local`, whatever
|
|
120
|
+
prefix it was given (`set_attribute_ns(XML_NS, "a:bb")`, as the DOM
|
|
121
|
+
allows): Namespaces in XML binds that namespace to `xml` alone. It re-reads
|
|
122
|
+
to the same namespace and local name. `canonicalize` writes it so too.
|
|
110
123
|
* Nodes from the factories (`create_element` and friends) still take their
|
|
111
124
|
namespace from the context they are first inserted into, so a subtree can be
|
|
112
125
|
built detached and attached afterwards. Only later moves carry.
|
|
@@ -165,7 +178,17 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
165
178
|
The names HTML actually uses (`data-*`, `aria-*`, `@click`, `:href`,
|
|
166
179
|
`v-on:x`, custom elements) are accepted.
|
|
167
180
|
* `set_attribute_ns(nil, "x:y")` raises, as the DOM's `setAttributeNS` does:
|
|
168
|
-
a prefix needs a namespace.
|
|
181
|
+
a prefix needs a namespace. `[]=` (HTML) and `set_loose_dom_attribute`
|
|
182
|
+
(XML) are the DOM's `setAttribute`, which makes such an attribute.
|
|
183
|
+
* A refused name raises `ArgumentError` - a NUL in it too - and a namespace
|
|
184
|
+
that does not fit the name (`set_attribute_ns`, `create_element_ns`)
|
|
185
|
+
raises `Makiri::Error`. Invalid UTF-8 raises `Makiri::Error` for every
|
|
186
|
+
argument, names included.
|
|
187
|
+
* `create_element_ns` refuses an HTML-namespace name in upper case that
|
|
188
|
+
lower-cases to an element Lexbor knows (`BR`, `DIV`), where the DOM makes
|
|
189
|
+
an unknown element: Lexbor would make that element (`BR` void, its
|
|
190
|
+
children never written). Other names keep their case (`MY-EL`). Nokogiri
|
|
191
|
+
has no `create_element_ns`.
|
|
169
192
|
* An HTML `<template>` follows the WHATWG content model, which Nokogiri does
|
|
170
193
|
not: its parsed contents live in the separate fragment `Element#content_fragment`
|
|
171
194
|
returns, `template.children` is empty, and `inner_html` / `inner_html=`
|
|
@@ -257,8 +280,15 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
257
280
|
.matches?("p")` is true, on both representations). Nokogiri raises
|
|
258
281
|
`NoMethodError` there - it implements `#matches?` as a search from
|
|
259
282
|
`ancestors.last`, which a detached node does not have.
|
|
260
|
-
*
|
|
261
|
-
|
|
283
|
+
* Type selectors in an HTML document follow the HTML Standard's
|
|
284
|
+
case-sensitivity rule, as browsers do: lower-cased for an HTML element (`LI`
|
|
285
|
+
matches `<li>`), as written for any other (`feGaussianBlur` matches the SVG
|
|
286
|
+
element, `fegaussianblur` does not). An HTML element named in upper case
|
|
287
|
+
(`create_element_ns(XHTML, "MY-EL")`, which keeps its name as the DOM does)
|
|
288
|
+
therefore matches no type selector.
|
|
289
|
+
* `Nokogiri::HTML5` is case-sensitive on HTML elements too, so `LI` does not
|
|
290
|
+
match `<li>` there. `Makiri::XML`'s `#css` is case-sensitive, as XML names
|
|
291
|
+
are.
|
|
262
292
|
|
|
263
293
|
## Serialization
|
|
264
294
|
|
|
@@ -285,7 +315,10 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
285
315
|
and attribute values (`[]=`, `set_attribute_ns`) - and stored/read back
|
|
286
316
|
verbatim, matching the WHATWG DOM / browsers (`document.createTextNode("\0")`).
|
|
287
317
|
It is still rejected in names, tag names, namespaces, PI target/data, CSS
|
|
288
|
-
selectors, and XPath expressions/variable names
|
|
318
|
+
selectors, and XPath expressions/variable names. A NUL in a name given to a
|
|
319
|
+
factory or setter (element, attribute, doctype and PI target names) raises
|
|
320
|
+
`ArgumentError`, as any other refused name does; anywhere else it raises
|
|
321
|
+
`Makiri::Error`.
|
|
289
322
|
* On re-parse, the HTML tokenizer replaces a U+0000 in text/attributes with
|
|
290
323
|
U+FFFD (WHATWG), so a serialized-then-reparsed round-trip is not byte-identical.
|
|
291
324
|
* `Makiri::XML` rejects NUL everywhere: XML 1.0 has no legal U+0000 character,
|
data/ext/makiri/rust/build.rs
CHANGED
|
@@ -267,7 +267,6 @@ fn main() {
|
|
|
267
267
|
.allowlist_function("lxb_dom_document_create_processing_instruction")
|
|
268
268
|
.allowlist_function("lxb_dom_document_create_document_fragment")
|
|
269
269
|
.allowlist_function("lxb_dom_document_type_create")
|
|
270
|
-
.allowlist_function("lxb_dom_document_type_valid_name")
|
|
271
270
|
.allowlist_function("lxb_dom_document_import_node")
|
|
272
271
|
.allowlist_function("lexbor_str_init")
|
|
273
272
|
.allowlist_function("lxb_html_document_destroy")
|
|
@@ -563,6 +563,24 @@ pub fn create_element<'d>(doc: HtmlDoc<'d>, name: &RubyText) -> Option<RawNode>
|
|
|
563
563
|
doc.create_element(name.as_bytes()).map(RawNode::from)
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
/// The refusal of an HTML-namespace name Lexbor would make as another element
|
|
567
|
+
/// (`HtmlDoc::misreads_html_name`), from `create_element_ns` and the
|
|
568
|
+
/// XML-to-HTML import alike.
|
|
569
|
+
pub const HTML_NAME_CASE: &str = "cannot make an HTML element whose upper-case name \
|
|
570
|
+
lower-cases to a known element (BR, SCRIPT, ...): it would be made as that element";
|
|
571
|
+
|
|
572
|
+
/// A new element named `local` in namespace `ns` ("" = none), with `prefix`
|
|
573
|
+
/// ("" = none) - the DOM's createElementNS, once the caller has validated and
|
|
574
|
+
/// split the name.
|
|
575
|
+
pub fn create_element_ns<'d>(
|
|
576
|
+
doc: HtmlDoc<'d>,
|
|
577
|
+
local: &[u8],
|
|
578
|
+
ns: &[u8],
|
|
579
|
+
prefix: &[u8],
|
|
580
|
+
) -> Option<RawNode> {
|
|
581
|
+
doc.create_element_ns(local, ns, prefix).map(RawNode::from)
|
|
582
|
+
}
|
|
583
|
+
|
|
566
584
|
/// A new Text node in `doc`.
|
|
567
585
|
pub fn create_text(doc: HtmlDoc<'_>, text: &RubyData) -> Option<RawNode> {
|
|
568
586
|
doc.create_text(text.as_bytes()).map(RawNode::from)
|
|
@@ -435,9 +435,41 @@ pub fn namespace_arg(in_: Value, what: &str) -> Result<Option<RubyText>, Error>
|
|
|
435
435
|
/// Coerce to a String and enforce the strict contract (valid UTF-8, no NUL),
|
|
436
436
|
/// naming `what` in the error. The names-and-engine-input path.
|
|
437
437
|
pub fn ruby_verified_text(in_: Value, what: &str) -> Result<RubyText, Error> {
|
|
438
|
+
verified_text_raising(in_, what, makiri_error)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// [`ruby_verified_text`] for a NAME given to a factory or a setter: the same
|
|
442
|
+
/// contract, but a NUL raises `ArgumentError` - the DOM's
|
|
443
|
+
/// InvalidCharacterError, which is what every other character a naming rule
|
|
444
|
+
/// refuses raises. It raised `Makiri::Error` from here, before any naming rule
|
|
445
|
+
/// ran, so a caller translating `ArgumentError` into a DOMException let that
|
|
446
|
+
/// one through. Invalid UTF-8 stays `Makiri::Error`: it breaks the String
|
|
447
|
+
/// contract every input shares, not a naming rule.
|
|
448
|
+
pub fn ruby_verified_name(in_: Value, what: &str) -> Result<RubyText, Error> {
|
|
449
|
+
verified_text_raising(in_, what, crate::bridge::ruby::arg_error)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// The one acquisition behind [`ruby_verified_text`] and
|
|
453
|
+
/// [`ruby_verified_name`], which differ only in what a NUL raises: `on_nul`
|
|
454
|
+
/// is handed the message. Everything else - invalid UTF-8, a copy that ran out
|
|
455
|
+
/// of memory, a raise while locking - is answered the same way for both.
|
|
456
|
+
fn verified_text_raising(
|
|
457
|
+
in_: Value,
|
|
458
|
+
what: &str,
|
|
459
|
+
on_nul: fn(String) -> Error,
|
|
460
|
+
) -> Result<RubyText, Error> {
|
|
461
|
+
use crate::cutf8::TextVerdict;
|
|
438
462
|
let s = string_of(in_)?;
|
|
439
|
-
RubyText::acquire(s, |s, b|
|
|
440
|
-
|
|
463
|
+
RubyText::acquire(s, |s, b| {
|
|
464
|
+
let v = text_check(s, b);
|
|
465
|
+
(v != TextVerdict::Ok).then_some(v)
|
|
466
|
+
})
|
|
467
|
+
.map_err(|r| match r {
|
|
468
|
+
Refusal::Check(TextVerdict::HasNul) => on_nul(format!(
|
|
469
|
+
"{what} {}",
|
|
470
|
+
TextVerdict::HasNul.problem().unwrap_or_default()
|
|
471
|
+
)),
|
|
472
|
+
Refusal::Check(v) => text_error(what, v.problem().unwrap_or_default()),
|
|
441
473
|
Refusal::Oom => oom_reading(),
|
|
442
474
|
Refusal::Raised(e) => e,
|
|
443
475
|
})
|
|
@@ -208,6 +208,7 @@ fn xml_mut_error(st: MutError) -> Error {
|
|
|
208
208
|
MutError::BadName => {
|
|
209
209
|
return crate::bridge::ruby::arg_error("not a well-formed XML name");
|
|
210
210
|
}
|
|
211
|
+
MutError::BadDomName(why) => return crate::bridge::ruby::arg_error(why),
|
|
211
212
|
MutError::BadChars => "value contains a character or sequence not permitted in XML",
|
|
212
213
|
MutError::UnboundNs => "namespace prefix is not bound in this scope",
|
|
213
214
|
MutError::Type => "operation unsupported for this node type",
|
|
@@ -243,6 +244,7 @@ namespace (only xmlns=\"\" undeclares, and only the default)"
|
|
|
243
244
|
"the namespace does not fit the qualified name (a prefix needs a namespace; \
|
|
244
245
|
xml and xmlns take only their own)"
|
|
245
246
|
}
|
|
247
|
+
MutError::HtmlNameCase => crate::bridge::html::HTML_NAME_CASE,
|
|
246
248
|
MutError::Internal => "internal error mutating XML (no document)",
|
|
247
249
|
/* The document's own budget, not the machine's memory - so the same
|
|
248
250
|
* exception a parse raises for the same cause. */
|
|
@@ -358,13 +360,32 @@ pub fn begin_edit(this: XmlSelf) -> Result<Editing, Error> {
|
|
|
358
360
|
/// A String argument verified as an engine string - valid UTF-8, no NUL - and
|
|
359
361
|
/// short enough for an arena span (4 GiB).
|
|
360
362
|
pub fn verified_text(v: Value, what: &str) -> Result<RubyText, Error> {
|
|
361
|
-
|
|
363
|
+
fits_xml_node(ruby_verified_text(v, what)?)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// `t`, unless it is too long for an XML node's `u32` span.
|
|
367
|
+
fn fits_xml_node(t: RubyText) -> Result<RubyText, Error> {
|
|
362
368
|
if u32::try_from(t.len()).is_err() {
|
|
363
369
|
return Err(makiri_error("string too long for an XML node (max 4 GiB)"));
|
|
364
370
|
}
|
|
365
371
|
Ok(t)
|
|
366
372
|
}
|
|
367
373
|
|
|
374
|
+
/// [`verified_text`] for a name given to a factory or a setter: a NUL raises
|
|
375
|
+
/// `ArgumentError`, as any other refused name does
|
|
376
|
+
/// ([`crate::bridge::string::ruby_verified_name`]).
|
|
377
|
+
pub fn verified_name(v: Value, what: &str) -> Result<RubyText, Error> {
|
|
378
|
+
fits_xml_node(crate::bridge::string::ruby_verified_name(v, what)?)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// [`verified_name`] for an optional argument: `nil` is `None`.
|
|
382
|
+
pub fn verified_name_opt(v: Value, what: &str) -> Result<Option<RubyText>, Error> {
|
|
383
|
+
if v.is_nil() {
|
|
384
|
+
return Ok(None);
|
|
385
|
+
}
|
|
386
|
+
verified_name(v, what).map(Some)
|
|
387
|
+
}
|
|
388
|
+
|
|
368
389
|
/// [`verified_text`] for an optional argument: `nil` is `None` - the same
|
|
369
390
|
/// shape as `bridge::string::ruby_verified_text_opt`.
|
|
370
391
|
pub fn verified_text_opt(v: Value, what: &str) -> Result<Option<RubyText>, Error> {
|
|
@@ -170,6 +170,7 @@ fn init_mutate() -> Result<(), Error> {
|
|
|
170
170
|
m.define_method("content=", method!(mutate::set_content, 1))?;
|
|
171
171
|
|
|
172
172
|
doc.define_method("create_element", method!(mutate::create_element, 1))?;
|
|
173
|
+
doc.define_method("create_element_ns", method!(mutate::create_element_ns, 2))?;
|
|
173
174
|
doc.define_method(
|
|
174
175
|
"create_document_type",
|
|
175
176
|
method!(mutate::create_document_type, -1),
|
|
@@ -21,11 +21,11 @@ use crate::bridge::ruby::{makiri_error, string_of};
|
|
|
21
21
|
use crate::bridge::fragment::{set_template_inner_html, stage_fragment_in};
|
|
22
22
|
use crate::bridge::html::{edit, insert, owning_doc, wrap_html_node, HtmlEdit, HtmlSelf};
|
|
23
23
|
use crate::bridge::string::{
|
|
24
|
-
namespace_arg, ruby_verified_data,
|
|
24
|
+
namespace_arg, ruby_verified_data, ruby_verified_name, ruby_verified_text,
|
|
25
|
+
ruby_verified_text_opt,
|
|
25
26
|
};
|
|
26
27
|
use crate::lexbor::adapter::html::{HtmlElementMut, NodeType, Place, RawNode};
|
|
27
28
|
use crate::xml::dom_name;
|
|
28
|
-
use crate::xml::qname::Split;
|
|
29
29
|
|
|
30
30
|
/// `name` held to the WHATWG DOM rule `ok`, else `ArgumentError` - the DOM's
|
|
31
31
|
/// InvalidCharacterError, and what the XML side raises for a bad name.
|
|
@@ -49,6 +49,29 @@ fn check_dom_name(
|
|
|
49
49
|
))
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/// The refusal of a namespace that does not fit the qualified name it came with.
|
|
53
|
+
const NS_MISFIT: &str =
|
|
54
|
+
"the namespace does not fit the qualified name (a prefix needs a namespace; \
|
|
55
|
+
xml and xmlns take only their own)";
|
|
56
|
+
|
|
57
|
+
/// [`dom_name::validate_and_extract`] with its refusals as the method raises
|
|
58
|
+
/// them: `ArgumentError` for a name, `Makiri::Error` for a namespace.
|
|
59
|
+
fn extract<'q>(
|
|
60
|
+
ruby: &Ruby,
|
|
61
|
+
ns: &[u8],
|
|
62
|
+
qname: &'q crate::bridge::string::RubyText,
|
|
63
|
+
local_ok: fn(&[u8]) -> bool,
|
|
64
|
+
what: &str,
|
|
65
|
+
) -> Result<(&'q [u8], &'q [u8]), Error> {
|
|
66
|
+
dom_name::validate_and_extract(ns, qname.as_bytes(), local_ok).map_err(|e| match e {
|
|
67
|
+
dom_name::ExtractError::Name => Error::new(
|
|
68
|
+
ruby.exception_arg_error(),
|
|
69
|
+
format!("invalid HTML {what} name"),
|
|
70
|
+
),
|
|
71
|
+
dom_name::ExtractError::Namespace => makiri_error(NS_MISFIT),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
/// The receiver as an element, once every argument is converted. Its node type
|
|
53
76
|
/// was checked before the conversion (an argument cannot change it), so the
|
|
54
77
|
/// `None` arm is unreachable - it answers `refusal` rather than assuming so.
|
|
@@ -116,7 +139,7 @@ pub fn aset(ruby: &Ruby, this: HtmlSelf, rb_name: Value, rb_value: Value) -> Res
|
|
|
116
139
|
if edit.node_type() != NodeType::Element {
|
|
117
140
|
return Err(makiri_error(REFUSAL));
|
|
118
141
|
}
|
|
119
|
-
let nv =
|
|
142
|
+
let nv = ruby_verified_name(rb_name, "attribute name")?;
|
|
120
143
|
let vv = ruby_verified_data(rb_value, "attribute value")?;
|
|
121
144
|
check_dom_name(ruby, &nv, dom_name::valid_attribute_local_name, "attribute")?;
|
|
122
145
|
let el = element_of(edit, REFUSAL)?;
|
|
@@ -140,40 +163,19 @@ pub fn set_attribute_ns(
|
|
|
140
163
|
if edit.node_type() != NodeType::Element {
|
|
141
164
|
return Err(makiri_error(REFUSAL));
|
|
142
165
|
}
|
|
143
|
-
let qv =
|
|
166
|
+
let qv = ruby_verified_name(rb_qname, "attribute qualified name")?;
|
|
144
167
|
let vv = ruby_verified_data(rb_value, "attribute value")?;
|
|
145
168
|
let nv = namespace_arg(rb_ns, "namespace")?;
|
|
146
|
-
/* The DOM's "validate and extract"
|
|
147
|
-
*
|
|
148
|
-
* set_attribute_ns applies too (`xml::qname::ns_fits_name`). It named
|
|
149
|
-
* `(nil, "x:y")` a prefixed attribute in no namespace. */
|
|
150
|
-
let q = qv.as_bytes();
|
|
151
|
-
let colon = q.iter().position(|&b| b == b':');
|
|
152
|
-
let (prefix, local) = match colon {
|
|
153
|
-
Some(i) => (&q[..i], &q[i + 1..]),
|
|
154
|
-
None => (&b""[..], q),
|
|
155
|
-
};
|
|
156
|
-
let names_ok = dom_name::valid_attribute_local_name(local)
|
|
157
|
-
&& (colon.is_none() || dom_name::valid_namespace_prefix(prefix));
|
|
158
|
-
check_dom_name(ruby, &qv, |_| names_ok, "attribute")?;
|
|
169
|
+
/* The DOM's "validate and extract". It named `(nil, "x:y")` a
|
|
170
|
+
* prefixed attribute in no namespace. */
|
|
159
171
|
let ns = nv.as_ref().map_or(&b""[..], |n| n.as_bytes());
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
let split = match colon {
|
|
168
|
-
Some(i) => Split::prefixed(i as u32, (q.len() - i - 1) as u32),
|
|
169
|
-
None => Split::unprefixed(q.len() as u32),
|
|
170
|
-
};
|
|
171
|
-
if !crate::xml::qname::ns_fits_name(ns, q, &split) {
|
|
172
|
-
return Err(makiri_error(
|
|
173
|
-
"the namespace does not fit the qualified name (a prefix needs a namespace; \
|
|
174
|
-
xml and xmlns take only their own)",
|
|
175
|
-
));
|
|
176
|
-
}
|
|
172
|
+
extract(
|
|
173
|
+
ruby,
|
|
174
|
+
ns,
|
|
175
|
+
&qv,
|
|
176
|
+
dom_name::valid_attribute_local_name,
|
|
177
|
+
"attribute",
|
|
178
|
+
)?;
|
|
177
179
|
let el = element_of(edit, REFUSAL)?;
|
|
178
180
|
crate::bridge::html::set_attribute_ns(el, nv.as_ref().map(|n| n.as_bytes()), &qv, &vv)
|
|
179
181
|
.map_err(|_| makiri_error("failed to set namespaced attribute"))?;
|
|
@@ -304,7 +306,7 @@ fn created(node: Option<RawNode>, rb_self: Value, what: &str) -> Result<Value, E
|
|
|
304
306
|
pub fn create_element(ruby: &Ruby, rb_self: Value, rb_name: Value) -> Result<Value, Error> {
|
|
305
307
|
crate::bridge::ruby::entry(|| {
|
|
306
308
|
let doc = owning_doc(&rb_self)?;
|
|
307
|
-
let nv =
|
|
309
|
+
let nv = ruby_verified_name(rb_name, "element name")?;
|
|
308
310
|
check_dom_name(ruby, &nv, dom_name::valid_element_local_name, "element")?;
|
|
309
311
|
created(
|
|
310
312
|
crate::bridge::html::create_element(doc, &nv),
|
|
@@ -314,6 +316,37 @@ pub fn create_element(ruby: &Ruby, rb_self: Value, rb_name: Value) -> Result<Val
|
|
|
314
316
|
})
|
|
315
317
|
}
|
|
316
318
|
|
|
319
|
+
/// `Document#create_element_ns(namespace_uri, qualified_name)` -> Element.
|
|
320
|
+
///
|
|
321
|
+
/// The DOM's createElementNS: the name is split at its first colon and each
|
|
322
|
+
/// half held to the DOM's rule (`ArgumentError`), then the namespace to the
|
|
323
|
+
/// name (`Makiri::Error`). Not `create_element`'s rule - `"0:a"` is prefix `0`
|
|
324
|
+
/// and local name `a` - and the name keeps its case, as a parsed foreign
|
|
325
|
+
/// element's does.
|
|
326
|
+
pub fn create_element_ns(
|
|
327
|
+
ruby: &Ruby,
|
|
328
|
+
rb_self: Value,
|
|
329
|
+
rb_ns: Value,
|
|
330
|
+
rb_qname: Value,
|
|
331
|
+
) -> Result<Value, Error> {
|
|
332
|
+
crate::bridge::ruby::entry(|| {
|
|
333
|
+
let doc = owning_doc(&rb_self)?;
|
|
334
|
+
let nv = namespace_arg(rb_ns, "namespace")?;
|
|
335
|
+
let qv = ruby_verified_name(rb_qname, "element qualified name")?;
|
|
336
|
+
let ns = nv.as_ref().map_or(&b""[..], |n| n.as_bytes());
|
|
337
|
+
let (prefix, local) =
|
|
338
|
+
extract(ruby, ns, &qv, dom_name::valid_element_local_name, "element")?;
|
|
339
|
+
if doc.misreads_html_name(local, ns) {
|
|
340
|
+
return Err(makiri_error(crate::bridge::html::HTML_NAME_CASE));
|
|
341
|
+
}
|
|
342
|
+
created(
|
|
343
|
+
crate::bridge::html::create_element_ns(doc, local, ns, prefix),
|
|
344
|
+
rb_self,
|
|
345
|
+
"element",
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
317
350
|
/// `Document#create_text_node(content)` -> Text.
|
|
318
351
|
pub fn create_text_node(_ruby: &Ruby, rb_self: Value, rb_text: Value) -> Result<Value, Error> {
|
|
319
352
|
crate::bridge::ruby::entry(|| {
|
|
@@ -349,7 +382,7 @@ pub fn create_pi(
|
|
|
349
382
|
) -> Result<Value, Error> {
|
|
350
383
|
crate::bridge::ruby::entry(|| {
|
|
351
384
|
let doc = owning_doc(&rb_self)?;
|
|
352
|
-
let tv =
|
|
385
|
+
let tv = ruby_verified_name(rb_target, "processing instruction target")?;
|
|
353
386
|
let dv = ruby_verified_text(rb_data, "processing instruction data")?;
|
|
354
387
|
/* DOM createProcessingInstruction: the target must match the XML Name
|
|
355
388
|
* production. Lexbor leaves that check as a TODO, and an unchecked
|
|
@@ -383,7 +416,7 @@ pub fn create_document_type(ruby: &Ruby, rb_self: Value, args: &[Value]) -> Resu
|
|
|
383
416
|
let (rb_pub, rb_sys_) = args.optional;
|
|
384
417
|
|
|
385
418
|
let doc = owning_doc(&rb_self)?;
|
|
386
|
-
let nv =
|
|
419
|
+
let nv = ruby_verified_name(rb_name, "doctype name")?;
|
|
387
420
|
if !crate::bridge::html::valid_doctype_name(&nv) {
|
|
388
421
|
/* The caller's error, not Lexbor's, so the exception class is picked
|
|
389
422
|
* here - the check itself is the DOM layer's. */
|
|
@@ -131,6 +131,10 @@ fn init_mutate() -> Result<(), Error> {
|
|
|
131
131
|
m.define_method(name, method!(mutate::delete, 1))?;
|
|
132
132
|
}
|
|
133
133
|
m.define_method("set_attribute_ns", method!(mutate::set_attribute_ns, 3))?;
|
|
134
|
+
m.define_method(
|
|
135
|
+
"set_loose_dom_attribute",
|
|
136
|
+
method!(mutate::set_loose_dom_attribute, 2),
|
|
137
|
+
)?;
|
|
134
138
|
m.define_method(
|
|
135
139
|
"remove_attribute_ns",
|
|
136
140
|
method!(mutate::remove_attribute_ns, 2),
|
|
@@ -16,8 +16,9 @@ use crate::bridge::ruby::makiri_error;
|
|
|
16
16
|
use crate::bridge::string::namespace_arg;
|
|
17
17
|
|
|
18
18
|
use crate::bridge::xml::{
|
|
19
|
-
begin_edit, import_copy, incoming_node,
|
|
20
|
-
with_arena_for_new_node, wrap_xml_node as wrap, xml_mut_result, Editing,
|
|
19
|
+
begin_edit, import_copy, incoming_node, verified_name, verified_name_opt, verified_text,
|
|
20
|
+
verified_text_opt, with_arena_for_new_node, wrap_xml_node as wrap, xml_mut_result, Editing,
|
|
21
|
+
XmlSelf,
|
|
21
22
|
};
|
|
22
23
|
use crate::init::CLASS_XML_DOCUMENT;
|
|
23
24
|
use crate::xml::dom_name::split_loose_dom_name;
|
|
@@ -61,7 +62,7 @@ fn element_for(this: XmlSelf) -> Result<Editing, Error> {
|
|
|
61
62
|
pub fn aset(_ruby: &Ruby, this: XmlSelf, name: Value, val: Value) -> Result<Value, Error> {
|
|
62
63
|
crate::bridge::ruby::entry(|| {
|
|
63
64
|
let edit = element_for(this)?;
|
|
64
|
-
let nv =
|
|
65
|
+
let nv = verified_name(name, "attribute name")?;
|
|
65
66
|
let vv = verified_text(val, "attribute value")?;
|
|
66
67
|
let (name, value) = (nv.as_bytes(), vv.as_bytes());
|
|
67
68
|
xml_mut_result(edit.with_arena(|d, n| mutate::set_attribute(d, n, name, value))?)?;
|
|
@@ -79,7 +80,7 @@ pub fn set_attribute_ns(
|
|
|
79
80
|
) -> Result<Value, Error> {
|
|
80
81
|
crate::bridge::ruby::entry(|| {
|
|
81
82
|
let edit = element_for(this)?;
|
|
82
|
-
let qv =
|
|
83
|
+
let qv = verified_name(qname, "attribute qualified name")?;
|
|
83
84
|
let vv = verified_text(val, "attribute value")?;
|
|
84
85
|
let nv = namespace_arg(ns, "namespace")?;
|
|
85
86
|
let (ns, qname, value) = (
|
|
@@ -92,6 +93,29 @@ pub fn set_attribute_ns(
|
|
|
92
93
|
})
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/// `element.set_loose_dom_attribute(qualified_name, value)` -> value.
|
|
97
|
+
///
|
|
98
|
+
/// The DOM's `setAttribute` for a caller building a browser's tree over XML -
|
|
99
|
+
/// `create_loose_dom_element`'s counterpart: the name is not split, and a new
|
|
100
|
+
/// attribute is in no namespace. `ArgumentError` for a name the DOM refuses.
|
|
101
|
+
pub fn set_loose_dom_attribute(
|
|
102
|
+
_ruby: &Ruby,
|
|
103
|
+
this: XmlSelf,
|
|
104
|
+
name: Value,
|
|
105
|
+
val: Value,
|
|
106
|
+
) -> Result<Value, Error> {
|
|
107
|
+
crate::bridge::ruby::entry(|| {
|
|
108
|
+
let edit = element_for(this)?;
|
|
109
|
+
let nv = verified_name(name, "attribute name")?;
|
|
110
|
+
let vv = verified_text(val, "attribute value")?;
|
|
111
|
+
let (name, value) = (nv.as_bytes(), vv.as_bytes());
|
|
112
|
+
xml_mut_result(
|
|
113
|
+
edit.with_arena(|d, n| mutate::set_loose_dom_attribute(d, n, name, value))?,
|
|
114
|
+
)?;
|
|
115
|
+
Ok(val)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
95
119
|
/// `element.remove_attribute_ns(namespace_or_nil, local_name)` -> self.
|
|
96
120
|
pub fn remove_attribute_ns(
|
|
97
121
|
_ruby: &Ruby,
|
|
@@ -223,7 +247,7 @@ pub fn create_element(ruby: &Ruby, rb_self: Value, args: &[Value]) -> Result<Val
|
|
|
223
247
|
}
|
|
224
248
|
}
|
|
225
249
|
|
|
226
|
-
let nv =
|
|
250
|
+
let nv = verified_name(name, "element name")?;
|
|
227
251
|
let cv = verified_text_opt(content, "element content")?;
|
|
228
252
|
let name = nv.as_bytes();
|
|
229
253
|
let el = xml_mut_result(with_arena_for_new_node(rb_self, |d| {
|
|
@@ -267,9 +291,9 @@ pub fn create_loose_dom_element(
|
|
|
267
291
|
ns: Value,
|
|
268
292
|
) -> Result<Value, Error> {
|
|
269
293
|
crate::bridge::ruby::entry(|| {
|
|
270
|
-
let qv =
|
|
271
|
-
let lv =
|
|
272
|
-
let pv =
|
|
294
|
+
let qv = verified_name(qname, "qualified name")?;
|
|
295
|
+
let lv = verified_name(local, "local name")?;
|
|
296
|
+
let pv = verified_name_opt(prefix, "prefix")?;
|
|
273
297
|
let nv = namespace_arg(ns, "namespace URI")?;
|
|
274
298
|
|
|
275
299
|
let qname = qv.as_bytes();
|
|
@@ -296,7 +320,7 @@ pub fn create_document_type(ruby: &Ruby, rb_self: Value, args: &[Value]) -> Resu
|
|
|
296
320
|
>(args)?;
|
|
297
321
|
let name = a.required.0;
|
|
298
322
|
let nil = ruby.qnil().as_value();
|
|
299
|
-
let nv =
|
|
323
|
+
let nv = verified_name(name, "doctype name")?;
|
|
300
324
|
let pv = verified_text_opt(a.optional.0.unwrap_or(nil), "doctype public id")?;
|
|
301
325
|
let sv = verified_text_opt(a.optional.1.unwrap_or(nil), "doctype system id")?;
|
|
302
326
|
/* An empty id is absent, like nil, matching the HTML factory and Nokogiri. */
|
|
@@ -342,7 +366,7 @@ pub fn create_cdata(_ruby: &Ruby, rb_self: Value, t: Value) -> Result<Value, Err
|
|
|
342
366
|
|
|
343
367
|
pub fn create_pi(_ruby: &Ruby, rb_self: Value, target: Value, data: Value) -> Result<Value, Error> {
|
|
344
368
|
crate::bridge::ruby::entry(|| {
|
|
345
|
-
let tg =
|
|
369
|
+
let tg = verified_name(target, "PI target")?;
|
|
346
370
|
let dt = verified_text(data, "PI data")?;
|
|
347
371
|
let (target, data) = (tg.as_bytes(), dt.as_bytes());
|
|
348
372
|
let pi = xml_mut_result(with_arena_for_new_node(rb_self, |d| {
|
|
@@ -43,7 +43,7 @@ fn declarations(d: &XmlDoc, id: NodeId) -> impl Iterator<Item = (NodeId, &[u8],
|
|
|
43
43
|
.into_iter()
|
|
44
44
|
.flatten()
|
|
45
45
|
.filter_map(move |a| {
|
|
46
|
-
let p =
|
|
46
|
+
let p = d.decl_prefix(a)?;
|
|
47
47
|
Some((a, p, d.value(a)))
|
|
48
48
|
})
|
|
49
49
|
}
|
|
@@ -32,7 +32,21 @@ fn to_xml_opts(ruby: &Ruby, args: &[Value]) -> Result<(i32, Value), Error> {
|
|
|
32
32
|
/// A serialization failure as `Makiri::Error`, worded for `verb`.
|
|
33
33
|
fn failure_error(f: Failure, verb: &str) -> Error {
|
|
34
34
|
let msg = match f {
|
|
35
|
-
Failure::DomLooseName =>
|
|
35
|
+
Failure::DomLooseName => {
|
|
36
|
+
format!("cannot {verb} XML containing a DOM-loose element name")
|
|
37
|
+
}
|
|
38
|
+
Failure::DomLooseDoctype => {
|
|
39
|
+
format!(
|
|
40
|
+
"cannot {verb} XML containing a DOM-loose doctype (a name or id XML cannot write)"
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
Failure::ForbiddenDeclaration => format!(
|
|
44
|
+
"cannot {verb} XML containing a namespace declaration XML forbids \
|
|
45
|
+
(such as xmlns:p=\"\", held as set_attribute_ns made it)"
|
|
46
|
+
),
|
|
47
|
+
Failure::DomLooseAttributeName => {
|
|
48
|
+
format!("cannot {verb} XML containing a DOM-loose attribute name")
|
|
49
|
+
}
|
|
36
50
|
Failure::PiTargetColon => {
|
|
37
51
|
format!("cannot {verb} XML containing a processing-instruction target with a colon")
|
|
38
52
|
}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
use crate::falloc::{try_vec_with_capacity, OomOption, OomResult, VecPush};
|
|
23
23
|
use crate::lexbor::adapter::html::{
|
|
24
|
-
BuildingElement, BuildingNode, HtmlDoc, HtmlElement, HtmlNode, NsId,
|
|
24
|
+
has_ascii_uppercase, BuildingElement, BuildingNode, HtmlDoc, HtmlElement, HtmlNode, NsId,
|
|
25
|
+
RawDoc, RawNode,
|
|
25
26
|
};
|
|
26
27
|
use crate::xml::model::{ArenaKind, Document as XmlDoc, MutError, NodeId};
|
|
27
28
|
use crate::xml::mutate;
|
|
@@ -124,11 +125,14 @@ fn h2x_copy_attrs(doc: &mut XmlDoc, s: HtmlElement<'_>, el: NodeId) -> Result<()
|
|
|
124
125
|
* restate one, or move one: `<svg><g xmlns="urn:evil">` put `g`
|
|
125
126
|
* and its children in `urn:evil`. */
|
|
126
127
|
(Some(NsId::XMLNS), _) | (_, Some(_)) => {}
|
|
127
|
-
/* An attribute in the XML namespace
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
(
|
|
128
|
+
/* An attribute in the XML namespace crosses WITH it: the DOM's
|
|
129
|
+
* setAttributeNS gives it any prefix or none (`lang`, `p:lang`),
|
|
130
|
+
* so its name alone does not say so, and the XML serializer
|
|
131
|
+
* writes it as `xml:` whatever it is called. */
|
|
132
|
+
(Some(NsId::XML), _) => {
|
|
133
|
+
mutate::set_attribute_ns(doc, el, crate::xml::XML_NS_URI, name, value)?;
|
|
134
|
+
}
|
|
135
|
+
(None, _) => {
|
|
132
136
|
let colon = name.iter().position(|&b| b == b':');
|
|
133
137
|
match colon {
|
|
134
138
|
Some(c) if &name[..c] != b"xml" => return Err(no_namespace_colon(name)),
|
|
@@ -369,13 +373,23 @@ fn x2h_make<'doc>(
|
|
|
369
373
|
* with its prefix, so the copy's localName is `e` and not `p:e`
|
|
370
374
|
* (as `//q:e` and local-name() read it), and with its case, so an
|
|
371
375
|
* SVG `linearGradient` does not come back `lineargradient`. An
|
|
372
|
-
* XHTML element
|
|
376
|
+
* XHTML element named in lower case is the HTML element of that
|
|
377
|
+
* name. One with upper case is not: it goes the createElementNS
|
|
378
|
+
* way too, keeping its case (`Foo`, where lower-casing it renamed
|
|
379
|
+
* it `foo`) - or refused when Lexbor would make it as a known
|
|
380
|
+
* element (`BR` came out the void `br`, its children gone). */
|
|
373
381
|
let (prefix, ns) = (doc.prefix(s), doc.ns(s));
|
|
374
|
-
let el = if prefix.is_empty()
|
|
382
|
+
let el = if prefix.is_empty()
|
|
383
|
+
&& !has_ascii_uppercase(doc.local(s))
|
|
384
|
+
&& hdoc.lookup_ns(ns) == Some(NsId::HTML)
|
|
385
|
+
{
|
|
375
386
|
let el = hdoc.create_element(doc.qname(s)).or_oom::<MutError>()?;
|
|
376
387
|
el.set_ns(NsId::HTML);
|
|
377
388
|
el
|
|
378
389
|
} else {
|
|
390
|
+
if hdoc.misreads_html_name(doc.local(s), ns) {
|
|
391
|
+
return Err(MutError::HtmlNameCase);
|
|
392
|
+
}
|
|
379
393
|
hdoc.create_element_ns(doc.local(s), ns, prefix)
|
|
380
394
|
.or_oom::<MutError>()?
|
|
381
395
|
};
|