Class: Dommy::CharacterDataNode
Overview
CharacterData base — TextNode and CommentNode share the data /
nodeValue / textContent API and remove / cloneNode semantics.
Constant Summary
Constants included
from Node
Node::ATTRIBUTE_NODE, Node::CDATA_SECTION_NODE, Node::COMMENT_NODE, Node::DOCUMENT_FRAGMENT_NODE, Node::DOCUMENT_NODE, Node::DOCUMENT_POSITION_CONTAINED_BY, Node::DOCUMENT_POSITION_CONTAINS, Node::DOCUMENT_POSITION_DISCONNECTED, Node::DOCUMENT_POSITION_FOLLOWING, Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, Node::DOCUMENT_POSITION_PRECEDING, Node::DOCUMENT_TYPE_NODE, Node::ELEMENT_NODE, Node::HTML_NAMESPACE, Node::PROCESSING_INSTRUCTION_NODE, Node::TEXT_NODE, Node::XMLNS_NAMESPACE, Node::XML_NAMESPACE
Instance Attribute Summary collapse
-
#document ⇒ Object
readonly
The owning Dommy document (as Element exposes), so cross-document adoption checks work for text/comment nodes too.
Instance Method Summary
collapse
-
#[](key) ⇒ Object
-
#[]=(key, value) ⇒ Object
-
#__dommy_backend_node__ ⇒ Object
-
#__internal_event_parent__ ⇒ Object
EventTarget needs a parent for event propagation; a character-data node bubbles to its parent element.
-
#__js_call__(method, args) ⇒ Object
-
#__js_get__(key) ⇒ Object
-
#__js_set__(key, value) ⇒ Object
-
#after(*args) ⇒ Object
-
#append_data(value) ⇒ Object
-
#assigned_slot ⇒ Object
Slottable mixin: the this text node is assigned to.
-
#before(*args) ⇒ Object
ChildNode mixin: WHATWG DOM defines before, after, replaceWith on all child nodes, including Text and Comment.
-
#data ⇒ Object
Snake_case facade (CRuby idiomatic).
-
#data=(value) ⇒ Object
-
#delete_data(offset, count) ⇒ Object
-
#dom_string(value) ⇒ Object
WebIDL DOMString coercion for a CharacterData mutation argument: JS null becomes the string "null" (undefined already stringifies to "undefined").
-
#initialize(document, nokogiri_node) ⇒ CharacterDataNode
constructor
A new instance of CharacterDataNode.
-
#insert_data(offset, value) ⇒ Object
-
#length ⇒ Object
CharacterData length / mutation methods.
-
#next_sibling ⇒ Object
-
#node_name ⇒ Object
WHATWG nodeName for character-data nodes is a per-type constant ("#text" / "#comment" / "#cdata-section"), not the element name.
-
#node_value ⇒ Object
-
#node_value=(value) ⇒ Object
-
#parent_element ⇒ Object
parentElement is the parent only when it is an element (a document or fragment parent is a parentNode but not a parentElement).
-
#parent_node ⇒ Object
-
#previous_sibling ⇒ Object
-
#remove ⇒ Object
-
#replace_data(offset, count, value) ⇒ Object
-
#replace_with(*args) ⇒ Object
-
#split_text(offset) ⇒ Object
Text.splitText / CharacterData split: break the node at offset, keeping [0, offset) here and returning a new sibling node with the remainder.
-
#substring_data(offset, count) ⇒ Object
-
#text_content ⇒ Object
-
#text_content=(value) ⇒ Object
-
#to_uint32(value) ⇒ Object
ECMAScript ToUint32 — WebIDL unsigned long conversion for a data offset or count: ToNumber (a non-numeric string is NaN), truncate toward zero, then take modulo 2**32 (so -1 wraps to 4294967295, 0x100000000+2 to 2).
-
#utf16_length(str) ⇒ Object
CharacterData offsets and counts are measured in UTF-16 code units, not Unicode code points, so an astral character (e.g. an emoji) counts as 2.
-
#utf16_slice(str, offset, count) ⇒ Object
Extract count UTF-16 code units from str starting at code unit offset.
included
#child_node_after, #child_node_before, #child_node_replace_with, #nullable_dom_string, #validate_insert_before_ref!
#__dommy_dump_event_failure__, #__internal_deliver_event__, #__internal_process_event_handler_return__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, #event_name_from_on, #invoke_listener_isolated, js_truthy?, #on_handler, #remove_event_listener, #set_on_handler
Methods included from Node
#compare_document_position, #get_root_node, #is_default_namespace, #is_equal_node, #is_same_node, #lookup_namespace_uri, #lookup_prefix
Constructor Details
#initialize(document, nokogiri_node) ⇒ CharacterDataNode
Returns a new instance of CharacterDataNode.
313
314
315
316
|
# File 'lib/dommy/element.rb', line 313
def initialize(document, nokogiri_node)
@document = document
@__node__ = nokogiri_node
end
|
Instance Attribute Details
#document ⇒ Object
The owning Dommy document (as Element exposes), so cross-document adoption
checks work for text/comment nodes too.
284
285
286
|
# File 'lib/dommy/element.rb', line 284
def document
@document
end
|
Instance Method Details
#[](key) ⇒ Object
367
368
369
|
# File 'lib/dommy/element.rb', line 367
def [](key)
__js_get__(key.to_s)
end
|
#[]=(key, value) ⇒ Object
371
372
373
|
# File 'lib/dommy/element.rb', line 371
def []=(key, value)
__js_set__(key.to_s, value)
end
|
#__dommy_backend_node__ ⇒ Object
286
|
# File 'lib/dommy/element.rb', line 286
def __dommy_backend_node__ = @__node__
|
#__internal_event_parent__ ⇒ Object
EventTarget needs a parent for event propagation; a character-data node
bubbles to its parent element.
290
291
292
|
# File 'lib/dommy/element.rb', line 290
def __internal_event_parent__
@__node__.parent && @document.wrap_node(@__node__.parent)
end
|
#__js_call__(method, args) ⇒ Object
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
|
# File 'lib/dommy/element.rb', line 546
def __js_call__(method, args)
case method
when "hasChildNodes"
false
when "contains"
args[0].respond_to?(:__dommy_backend_node__) &&
args[0].__dommy_backend_node__ == @__node__
when "appendChild", "insertBefore", "replaceChild"
raise Bridge::TypeError, "Argument is not a Node." unless args[0].is_a?(Dommy::Node)
raise DOMException::HierarchyRequestError, "this node type does not support children"
when "removeChild"
raise Bridge::TypeError, "Argument is not a Node." unless args[0].is_a?(Dommy::Node)
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
when "compareDocumentPosition"
compare_document_position(args[0])
when "isSameNode"
is_same_node(args[0])
when "getRootNode"
get_root_node(args[0])
when "lookupNamespaceURI"
lookup_namespace_uri(args[0])
when "lookupPrefix"
lookup_prefix(args[0])
when "isDefaultNamespace"
is_default_namespace(args[0])
when "normalize"
nil when "splitText"
split_text(args[0])
when "addEventListener"
add_event_listener(args[0], args[1], args[2])
when "removeEventListener"
remove_event_listener(args[0], args[1], args[2])
when "dispatchEvent"
dispatch_event(args[0])
when "appendData"
raise Bridge::TypeError, "appendData requires 1 argument." if args.empty?
append_data(args[0])
when "insertData"
insert_data(args[0], args[1])
when "deleteData"
delete_data(args[0], args[1])
when "replaceData"
replace_data(args[0], args[1], args[2])
when "substringData"
raise Bridge::TypeError, "substringData requires 2 arguments." if args.length < 2
substring_data(args[0], args[1])
when "remove"
remove
Bridge::UNDEFINED when "before"
before(*args)
when "after"
after(*args)
when "replaceWith"
replace_with(*args)
when "isEqualNode"
is_equal_node(args[0])
end
end
|
#__js_get__(key) ⇒ Object
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
|
# File 'lib/dommy/element.rb', line 472
def __js_get__(key)
case key
when "nodeType"
node_type
when "nodeName"
node_name
when "textContent"
@__node__.content
when "data"
@__node__.content
when "nodeValue"
@__node__.content
when "length"
length
when "parentNode"
parent_node
when "parentElement"
parent_element
when "ownerDocument"
@document
when "nextSibling"
next_sibling
when "previousSibling"
previous_sibling
when "childNodes"
NodeList.new
when "firstChild", "lastChild"
nil
when "assignedSlot"
assigned_slot
else
Bridge::ABSENT end
end
|
#__js_set__(key, value) ⇒ Object
530
531
532
533
534
535
536
537
|
# File 'lib/dommy/element.rb', line 530
def __js_set__(key, value)
case key
when "textContent", "data", "nodeValue"
write_data(value)
end
nil
end
|
#after(*args) ⇒ Object
626
627
628
|
# File 'lib/dommy/element.rb', line 626
def after(*args)
child_node_after(args)
end
|
#append_data(value) ⇒ Object
444
445
446
|
# File 'lib/dommy/element.rb', line 444
def append_data(value)
write_data(@__node__.content + dom_string(value))
end
|
#assigned_slot ⇒ Object
Slottable mixin: the this text node is assigned to. A text node has
no slot attribute, so it targets the default (unnamed) slot of its parent
element's shadow tree; a closed shadow tree hides the assignment (null).
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
|
# File 'lib/dommy/element.rb', line 514
def assigned_slot
parent = @__node__.parent
return nil unless parent.respond_to?(:element?) && parent.element?
host = @document.wrap_node(parent)
return nil unless host.respond_to?(:shadow_root)
sr = host.shadow_root
return nil unless sr
return nil if sr.__js_get__("mode") == "closed"
sr.query_selector_all("slot").find do |slot|
(slot.respond_to?(:name) ? slot.name.to_s : "") == ""
end
end
|
#before(*args) ⇒ Object
ChildNode mixin: WHATWG DOM defines before, after, replaceWith on
all child nodes, including Text and Comment. The spec-correct algorithm
(viable previous/next sibling, forward insertion, string coercion,
Fragment/cross-document adoption) lives in Internal::ParentNode and is
shared with Element — these just forward to it.
622
623
624
|
# File 'lib/dommy/element.rb', line 622
def before(*args)
child_node_before(args)
end
|
#data ⇒ Object
Snake_case facade (CRuby idiomatic)
320
321
322
|
# File 'lib/dommy/element.rb', line 320
def data
@__node__.content
end
|
#data=(value) ⇒ Object
324
325
326
|
# File 'lib/dommy/element.rb', line 324
def data=(value)
write_data(value)
end
|
#delete_data(offset, count) ⇒ Object
458
459
460
|
# File 'lib/dommy/element.rb', line 458
def delete_data(offset, count)
replace_data(offset, count, "")
end
|
#dom_string(value) ⇒ Object
WebIDL DOMString coercion for a CharacterData mutation argument: JS null
becomes the string "null" (undefined already stringifies to "undefined").
450
451
452
|
# File 'lib/dommy/element.rb', line 450
def dom_string(value)
value.nil? ? "null" : value.to_s
end
|
#insert_data(offset, value) ⇒ Object
454
455
456
|
# File 'lib/dommy/element.rb', line 454
def insert_data(offset, value)
replace_data(offset, 0, value)
end
|
#length ⇒ Object
CharacterData length / mutation methods. Offsets and counts are UTF-16 code
units per spec; for BMP text (the common case) Ruby char indices match.
Each mutating op routes through write_data, which fires the characterData
MutationObserver record.
390
391
392
|
# File 'lib/dommy/element.rb', line 390
def length
utf16_length(@__node__.content)
end
|
#next_sibling ⇒ Object
359
360
361
|
# File 'lib/dommy/element.rb', line 359
def next_sibling
@__node__.next && @document.wrap_node(@__node__.next)
end
|
#node_name ⇒ Object
WHATWG nodeName for character-data nodes is a per-type constant
("#text" / "#comment" / "#cdata-section"), not the element name.
377
378
379
380
381
382
383
|
# File 'lib/dommy/element.rb', line 377
def node_name
case node_type
when 3 then "#text"
when 4 then "#cdata-section"
when 8 then "#comment"
end
end
|
#node_value ⇒ Object
328
329
330
|
# File 'lib/dommy/element.rb', line 328
def node_value
@__node__.content
end
|
#node_value=(value) ⇒ Object
332
333
334
|
# File 'lib/dommy/element.rb', line 332
def node_value=(value)
write_data(value)
end
|
#parent_element ⇒ Object
parentElement is the parent only when it is an element (a document or
fragment parent is a parentNode but not a parentElement).
355
356
357
|
# File 'lib/dommy/element.rb', line 355
def parent_element
@document.wrap_node(@__node__.parent) if @__node__.parent&.element?
end
|
#parent_node ⇒ Object
349
350
351
|
# File 'lib/dommy/element.rb', line 349
def parent_node
@__node__.parent && @document.wrap_node(@__node__.parent)
end
|
#previous_sibling ⇒ Object
363
364
365
|
# File 'lib/dommy/element.rb', line 363
def previous_sibling
@__node__.previous && @document.wrap_node(@__node__.previous)
end
|
#remove ⇒ Object
344
345
346
347
|
# File 'lib/dommy/element.rb', line 344
def remove
@document.remove_node_with_notify(@__node__)
nil
end
|
#replace_data(offset, count, value) ⇒ Object
462
463
464
465
466
467
468
469
470
|
# File 'lib/dommy/element.rb', line 462
def replace_data(offset, count, value)
s = @__node__.content
len = utf16_length(s)
o = to_uint32(offset)
raise DOMException::IndexSizeError, "offset out of bounds" if o > len
c = [to_uint32(count), len - o].min
write_data(utf16_slice(s, 0, o) + dom_string(value) + utf16_slice(s, o + c, len - (o + c)))
end
|
#replace_with(*args) ⇒ Object
630
631
632
|
# File 'lib/dommy/element.rb', line 630
def replace_with(*args)
child_node_replace_with(args)
end
|
#split_text(offset) ⇒ Object
Text.splitText / CharacterData split: break the node at offset, keeping
[0, offset) here and returning a new sibling node with the remainder.
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
|
# File 'lib/dommy/element.rb', line 296
def split_text(offset)
off = offset.to_i
full = @__node__.content
raise DOMException::IndexSizeError, "offset #{off} is out of bounds" if off.negative? || off > full.length
rest = full[off..] || ""
write_data(full[0, off])
new_node = @document.create_text_node(rest)
if @__node__.parent
new_bn = new_node.__dommy_backend_node__
@__node__.add_next_sibling(new_bn)
@document.notify_child_list_mutation(target_node: @__node__.parent, added_nodes: [new_bn], removed_nodes: [])
end
new_node
end
|
#substring_data(offset, count) ⇒ Object
418
419
420
421
422
423
424
425
426
|
# File 'lib/dommy/element.rb', line 418
def substring_data(offset, count)
s = @__node__.content
len = utf16_length(s)
o = to_uint32(offset)
raise DOMException::IndexSizeError, "offset out of bounds" if o > len
c = [to_uint32(count), len - o].min
utf16_slice(s, o, c)
end
|
#text_content ⇒ Object
336
337
338
|
# File 'lib/dommy/element.rb', line 336
def text_content
@__node__.content
end
|
#text_content=(value) ⇒ Object
340
341
342
|
# File 'lib/dommy/element.rb', line 340
def text_content=(value)
write_data(value)
end
|
#to_uint32(value) ⇒ Object
ECMAScript ToUint32 — WebIDL unsigned long conversion for a data offset
or count: ToNumber (a non-numeric string is NaN), truncate toward zero,
then take modulo 2**32 (so -1 wraps to 4294967295, 0x100000000+2 to 2).
431
432
433
434
435
436
437
438
439
440
441
442
|
# File 'lib/dommy/element.rb', line 431
def to_uint32(value)
num =
case value
when Integer then value
when Numeric then value
when nil then 0 else Float(value.to_s) rescue Float::NAN
end
return 0 unless num.respond_to?(:finite?) ? num.finite? : true
num.to_i % (2**32)
end
|
#utf16_length(str) ⇒ Object
CharacterData offsets and counts are measured in UTF-16 code units, not
Unicode code points, so an astral character (e.g. an emoji) counts as 2.
396
397
398
|
# File 'lib/dommy/element.rb', line 396
def utf16_length(str)
str.encode(Encoding::UTF_16LE).bytesize / 2
end
|
#utf16_slice(str, offset, count) ⇒ Object
Extract count UTF-16 code units from str starting at code unit
offset. Slicing on the UTF-16LE byte buffer keeps astral characters
intact for the offsets these APIs actually produce.
If the range starts or ends inside a surrogate pair the result would be a
lone (unpaired) surrogate. JS strings can hold those; a Ruby UTF-8 String
cannot, so re-raise the raw encoding error as a clear, intentional message
rather than leaking "\xDF on UTF-16LE" to the caller. This is a Dommy
limitation and, since splitting a surrogate pair signals a UTF-16 offset
bug in the caller, failing loud is deliberate.
410
411
412
413
414
415
416
|
# File 'lib/dommy/element.rb', line 410
def utf16_slice(str, offset, count)
buf = str.encode(Encoding::UTF_16LE)
buf.byteslice(offset * 2, count * 2).encode(Encoding::UTF_8, Encoding::UTF_16LE)
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
raise "cannot split a UTF-16 surrogate pair: the requested range would " \
"produce a lone surrogate, which Dommy cannot represent"
end
|