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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/NOKOGIRI_DIFFERENCES.md +37 -4
  4. data/ext/makiri/rust/build.rs +0 -1
  5. data/ext/makiri/rust/src/bridge/html.rs +18 -0
  6. data/ext/makiri/rust/src/bridge/string.rs +34 -2
  7. data/ext/makiri/rust/src/bridge/xml.rs +22 -1
  8. data/ext/makiri/rust/src/glue/html_node/mod.rs +1 -0
  9. data/ext/makiri/rust/src/glue/html_node/mutate.rs +70 -37
  10. data/ext/makiri/rust/src/glue/xml_node/mod.rs +4 -0
  11. data/ext/makiri/rust/src/glue/xml_node/mutate.rs +34 -10
  12. data/ext/makiri/rust/src/glue/xml_node/ns.rs +1 -1
  13. data/ext/makiri/rust/src/glue/xml_node/serialize.rs +15 -1
  14. data/ext/makiri/rust/src/lexbor/adapter/cross_import.rs +22 -8
  15. data/ext/makiri/rust/src/lexbor/adapter/html/build.rs +24 -10
  16. data/ext/makiri/rust/src/lexbor/adapter/html/mod.rs +11 -1
  17. data/ext/makiri/rust/src/lexbor/css_match/simple.rs +61 -15
  18. data/ext/makiri/rust/src/lexbor/tests.rs +26 -11
  19. data/ext/makiri/rust/src/xml/arena.rs +42 -1
  20. data/ext/makiri/rust/src/xml/dom_name.rs +72 -0
  21. data/ext/makiri/rust/src/xml/model.rs +21 -5
  22. data/ext/makiri/rust/src/xml/mutate/attr.rs +85 -5
  23. data/ext/makiri/rust/src/xml/mutate/factory.rs +22 -15
  24. data/ext/makiri/rust/src/xml/mutate/mod.rs +3 -1
  25. data/ext/makiri/rust/src/xml/mutate/ns.rs +2 -2
  26. data/ext/makiri/rust/src/xml/qname.rs +0 -22
  27. data/ext/makiri/rust/src/xml/selftest.rs +26 -0
  28. data/ext/makiri/rust/src/xml/serialize/c14n.rs +20 -3
  29. data/ext/makiri/rust/src/xml/serialize/mod.rs +62 -7
  30. data/ext/makiri/rust/src/xml/serialize/xml.rs +29 -8
  31. data/ext/makiri/rust/src/xml/tree/mod.rs +1 -1
  32. data/ext/makiri/rust/src/xml/xpath.rs +3 -4
  33. data/lib/makiri/version.rb +1 -1
  34. data/script/check_alloc_failures.rb +6 -0
  35. data/script/check_unsafe_boundaries.rb +1 -1
  36. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7421d59a83c782fab157fb3b220761908b885190db8405dd5173ddaeb7343e83
4
- data.tar.gz: 4f64149f513b8a8f5288da9158c6dd5fa673e79ca33f5acc2c06a863d6f46641
3
+ metadata.gz: 9c12838a29d87a6bd9acb72b97e1e25dbdfbdaff37e38609cce6fd562e0f76d7
4
+ data.tar.gz: f4531547107db81c7db834d6c991adcf68b8a0d2fd714e158d69b164140a18bc
5
5
  SHA512:
6
- metadata.gz: b1da6427faed2ac5e96ff02c6eeeb207c999eee5322c8b65e63b95e6038dee2ffbcaa457580f2980b73de6a9d7436e8e602134e7a3bb5e0e5fc399072b6eddaf
7
- data.tar.gz: 4ee2def4dbe0cc740450bfa2e0d8a448a7908f1356f6de877d2c91f1d7e58aa92d022fd9f553b55463e6e444875f0889f567102c2883ece072f886a93eb06a44
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
@@ -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
- * * Type selectors are ASCII case-insensitive (CSS-correct for HTML; `LI` matches `<li>`)
261
- * `Nokogiri::HTML5` is case-sensitive there.
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 (a NUL there raises).
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,
@@ -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| text_check(s, b).problem()).map_err(|r| match r {
440
- Refusal::Check(problem) => text_error(what, problem),
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
- let t = ruby_verified_text(v, what)?;
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, ruby_verified_text, ruby_verified_text_opt,
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 = ruby_verified_text(rb_name, "attribute name")?;
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 = ruby_verified_text(rb_qname, "attribute qualified name")?;
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": split at the first colon, check
147
- * both halves, then that the namespace fits them - the rule XML's
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
- /* `Split` holds u32 lengths; a name past that would be split wrong, so
161
- * it is refused rather than truncated (each `as u32` below is exact). */
162
- if u32::try_from(q.len()).is_err() {
163
- return Err(makiri_error(
164
- "attribute qualified name too long (max 4 GiB)",
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 = ruby_verified_text(rb_name, "element name")?;
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 = ruby_verified_text(rb_target, "processing instruction target")?;
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 = ruby_verified_text(rb_name, "doctype name")?;
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, verified_text, verified_text_opt,
20
- with_arena_for_new_node, wrap_xml_node as wrap, xml_mut_result, Editing, XmlSelf,
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 = verified_text(name, "attribute name")?;
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 = verified_text(qname, "attribute qualified name")?;
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 = verified_text(name, "element name")?;
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 = verified_text(qname, "qualified name")?;
271
- let lv = verified_text(local, "local name")?;
272
- let pv = verified_text_opt(prefix, "prefix")?;
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 = verified_text(name, "doctype name")?;
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 = verified_text(target, "PI target")?;
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 = crate::xml::qname::xmlns_prefix(d.qname(a))?;
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 => format!("cannot {verb} XML containing a DOM-loose element name"),
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, RawDoc, RawNode,
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 is always named `xml:`: the
128
- * parser's foreign-attribute table gives it that prefix, and
129
- * `set_attribute_ns` refuses the XML namespace under any other
130
- * name (`ns_fits_name`), so `set_attribute` resolves it back. */
131
- (None | Some(NsId::XML), _) => {
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 is an HTML element, whose name is lower case. */
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() && hdoc.lookup_ns(ns) == Some(NsId::HTML) {
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
  };