leptris 1.9.174.8 → 1.9.174.9
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 +17 -0
- data/TODO.perf/34-fragment-fast-lane.md +41 -0
- data/TODO.perf/35-parse-default-c-face.md +24 -0
- data/ext/leptris/native/native.c +113 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +9 -0
- data/lib/leptris/xml/document_fragment.rb +26 -1
- data/lib/leptris/xml/element.rb +27 -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: 73130c8bf549007d80bf610c22bc831df382f5fe972316835a6dc806885929c5
|
|
4
|
+
data.tar.gz: 3b6bfb23c72a2ed3532eb64886c9f36603975b5d601aa693f6b3ba821bb19de2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 621d0f62f7869c1a4b0c63212dce8694f51cc1914a3f115551b7e91c1f5b2bc0af9a33dcb2baa4d462fc5a12f72225a228dee59f7e4a7f70ed5173e0afe6932b
|
|
7
|
+
data.tar.gz: 2a389319493ace9979ea863853b75c56149f73dcc5e204c362bd8bdbaacd335d94a58bd9bb3d4ed83f6c83df3331b53549bb462fa19980fb996a8c2798c3fbdc
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.174.9] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Fragment fast lane (TODO.perf/34)**: `add_child(String)` runs
|
|
13
|
+
one C dispatch for the whole markup add — fragment parse +
|
|
14
|
+
every child append + a single readonly gate and version bump
|
|
15
|
+
(the move-during-iteration hazard handled: appending detaches,
|
|
16
|
+
so the walk captures next before each move). The fragment parse
|
|
17
|
+
face returns an address (no status MemoryPointer, no Pointer);
|
|
18
|
+
`DocumentFragment#children` rides the bulk face; parse failures
|
|
19
|
+
fall back to the legacy path for the exact error.
|
|
20
|
+
- **Document.parse default path as one C dispatch (TODO.perf/35)**:
|
|
21
|
+
engine parse + ivar-seeded wrapper + lifetime handle in one
|
|
22
|
+
call — small parses measured 15.6µs → 6.8µs (~2.3x, under host
|
|
23
|
+
load ~100; the shape is self-contained).
|
|
24
|
+
|
|
8
25
|
## [1.9.174.8] - 2026-09-16
|
|
9
26
|
|
|
10
27
|
### Fixed
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 34 — Fragment fast lane: C parse, bulk children, one-shot markup append
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.9)
|
|
4
|
+
|
|
5
|
+
The add_child(String) path — moxml's markup-builder shape — pays
|
|
6
|
+
three layers per call: parse_fragment_with_status allocates a
|
|
7
|
+
MemoryPointer for the status out-param, DocumentFragment#children
|
|
8
|
+
runs the FFI fetch+wrap loop (never moved to the bulk faces), and
|
|
9
|
+
each child appends through its own dispatch.
|
|
10
|
+
|
|
11
|
+
Bind the lane: a C parse face returns the fragment's address
|
|
12
|
+
(Integer — no Pointer, no MemoryPointer); the fragment stores
|
|
13
|
+
@c_address with a lazy c_ptr; children rides the bulk face
|
|
14
|
+
(fragment nodes live in the document pool — the document's cache
|
|
15
|
+
and version discipline apply); and a one-shot append face parses
|
|
16
|
+
the markup, walks the fragment's children, and appends every one
|
|
17
|
+
with a single readonly gate + version bump. Parse failures return
|
|
18
|
+
a marker; the Ruby fallback re-runs the old path for the exact
|
|
19
|
+
error message (the rare path, cheap to re-fail).
|
|
20
|
+
|
|
21
|
+
Gates: add_child(String) semantics identical (multiple top-level
|
|
22
|
+
nodes, text, PIs, namespaces lifted per the Node path); fragment
|
|
23
|
+
children identical through the bulk face; suite both modes.
|
|
24
|
+
|
|
25
|
+
## Outcome (1.9.174.9)
|
|
26
|
+
|
|
27
|
+
Three layers landed: the fragment parse face answers the
|
|
28
|
+
fragment ADDRESS (no status MemoryPointer, no Pointer mint —
|
|
29
|
+
failures fall back to the FFI pair for the exact error);
|
|
30
|
+
DocumentFragment stores @c_address with a lazy c_ptr and
|
|
31
|
+
children() rides the bulk face (fragment nodes live in the
|
|
32
|
+
document pool — the document's cache and version discipline
|
|
33
|
+
apply); and Element#add_child(String) runs ONE dispatch for the
|
|
34
|
+
whole markup add (parse + per-child append + single gate/bump —
|
|
35
|
+
with the move-during-iteration hazard caught by the suite:
|
|
36
|
+
appending detaches the child, so the walk captures next BEFORE
|
|
37
|
+
each move). -1 re-runs the legacy path for parse errors;
|
|
38
|
+
-1000-st routes through check_status. The markup row needs a
|
|
39
|
+
quiet-host battery number (this box sat at load 100+ today);
|
|
40
|
+
small-parse measured 6.8µs native vs 15.6µs FFI (~2.3x) under
|
|
41
|
+
load 100.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 35 — Document.parse default path as one C dispatch
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.9)
|
|
4
|
+
|
|
5
|
+
The default parse path (no options, no recover — the adapter
|
|
6
|
+
default) is already allocation-free at the Ruby level (#187),
|
|
7
|
+
but still pays FFI marshaling, the wrap frames, the Freed and
|
|
8
|
+
Pointer constructions, and the handle attach across three Ruby
|
|
9
|
+
layers. One C face runs leptris_parse_string + the full document
|
|
10
|
+
wrapper (ivar-seeded) + lifetime handle and returns it; failure
|
|
11
|
+
raises the same ParseError shape via last_error read in C.
|
|
12
|
+
|
|
13
|
+
Gates: parse defaults identical (errors, readonly flag, root
|
|
14
|
+
access); the options/recover paths untouched; suite both modes.
|
|
15
|
+
|
|
16
|
+
## Outcome (1.9.174.9)
|
|
17
|
+
|
|
18
|
+
Native.parse_binding_document: leptris_parse_string + the full
|
|
19
|
+
ivar-seeded wrapper + lifetime handle in one dispatch (shared
|
|
20
|
+
build_binding_document helper); Document.parse's default path
|
|
21
|
+
(no options, no recover) routes through it, raising the same
|
|
22
|
+
ParseError shape via last_error on failure. Options/recover
|
|
23
|
+
paths untouched. Measured (load ~100, self-contained shape):
|
|
24
|
+
small parse 15.6µs -> 6.8µs (~2.3x).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -94,6 +94,9 @@ typedef void (*visit_fn)(void *, visit_cb_fn, void *);
|
|
|
94
94
|
typedef void (*free_str_fn)(void *);
|
|
95
95
|
typedef const char *(*node_xpath_fn)(void *);
|
|
96
96
|
typedef void *(*elem_copy_fn)(void *, void *);
|
|
97
|
+
typedef void *(*parse_str_fn)(const char *, size_t, void *);
|
|
98
|
+
typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
|
|
99
|
+
typedef const char *(*last_err_fn)(void);
|
|
97
100
|
static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
|
|
98
101
|
static node_unlink_fn f_node_unlink;
|
|
99
102
|
static traverse_fn f_node_traverse;
|
|
@@ -101,6 +104,9 @@ static visit_fn f_node_visit;
|
|
|
101
104
|
static free_str_fn f_free_str;
|
|
102
105
|
static node_xpath_fn f_node_xpath;
|
|
103
106
|
static elem_copy_fn f_elem_copy;
|
|
107
|
+
static parse_str_fn f_parse_str;
|
|
108
|
+
static parse_frag_fn f_parse_frag;
|
|
109
|
+
static last_err_fn f_last_err;
|
|
104
110
|
static set_root_fn f_set_root;
|
|
105
111
|
static doc_free_fn f_doc_free;
|
|
106
112
|
|
|
@@ -221,6 +227,9 @@ static void resolve_symbols(const char *lib_path)
|
|
|
221
227
|
f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
|
|
222
228
|
f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
|
|
223
229
|
f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
|
|
230
|
+
f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
|
|
231
|
+
f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
|
|
232
|
+
f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
|
|
224
233
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
225
234
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
226
235
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
@@ -233,7 +242,8 @@ static void resolve_symbols(const char *lib_path)
|
|
|
233
242
|
!f_pi_data || !f_elem_set_name || !f_elem_set_text ||
|
|
234
243
|
!f_text_set_content || !f_node_unlink ||
|
|
235
244
|
!f_node_traverse || !f_node_visit || !f_free_str ||
|
|
236
|
-
!f_node_xpath || !f_elem_copy ||
|
|
245
|
+
!f_node_xpath || !f_elem_copy || !f_parse_str ||
|
|
246
|
+
!f_parse_frag || !f_last_err ||
|
|
237
247
|
!f_doc_free ||
|
|
238
248
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
239
249
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -1874,6 +1884,102 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
|
|
|
1874
1884
|
return new_doc;
|
|
1875
1885
|
}
|
|
1876
1886
|
|
|
1887
|
+
/* Shared document-wrapper construction from a C document
|
|
1888
|
+
* pointer: ivar-seeded wrapper + lifetime handle. Returns the
|
|
1889
|
+
* wrapper. (TODO.perf/35 — the create face inlines the same
|
|
1890
|
+
* steps.) */
|
|
1891
|
+
static VALUE build_binding_document(void *doc)
|
|
1892
|
+
{
|
|
1893
|
+
VALUE new_doc, doc_addr, handle, freed;
|
|
1894
|
+
struct doc_handle *h;
|
|
1895
|
+
|
|
1896
|
+
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
1897
|
+
new_doc = rb_obj_alloc(c_b_document);
|
|
1898
|
+
rb_iv_set(new_doc, "@c_ptr",
|
|
1899
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
|
|
1900
|
+
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
1901
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
1902
|
+
rb_iv_set(new_doc, "@freed", freed);
|
|
1903
|
+
rb_iv_set(new_doc, "@readonly", Qfalse);
|
|
1904
|
+
rb_iv_set(new_doc, "@version", INT2FIX(0));
|
|
1905
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
1906
|
+
&dh_type, h);
|
|
1907
|
+
h->doc = doc;
|
|
1908
|
+
rb_ivar_set(new_doc, id_iv_doc_handle, handle);
|
|
1909
|
+
return new_doc;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/* Document.parse default path in one dispatch (TODO.perf/35):
|
|
1913
|
+
* leptris_parse_string + wrapper + handle. Qnil = parse failure
|
|
1914
|
+
* (the caller raises with the same message shape). */
|
|
1915
|
+
static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
|
|
1916
|
+
{
|
|
1917
|
+
void *doc;
|
|
1918
|
+
|
|
1919
|
+
(void)self;
|
|
1920
|
+
resolve_binding_classes();
|
|
1921
|
+
doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
|
|
1922
|
+
(size_t)RSTRING_LEN(StringValue(xml)), NULL);
|
|
1923
|
+
return doc ? build_binding_document(doc) : Qnil;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/* Fragment lane (TODO.perf/34): parse the markup against the
|
|
1927
|
+
* document; returns the fragment's ADDRESS, or Qfalse when the
|
|
1928
|
+
* parse fails (status is not surfaced — the caller re-runs the
|
|
1929
|
+
* FFI path for the exact error). */
|
|
1930
|
+
static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
|
|
1931
|
+
VALUE xml)
|
|
1932
|
+
{
|
|
1933
|
+
void *frag;
|
|
1934
|
+
int status;
|
|
1935
|
+
|
|
1936
|
+
(void)self;
|
|
1937
|
+
resolve_binding_classes();
|
|
1938
|
+
frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
|
|
1939
|
+
(size_t)RSTRING_LEN(StringValue(xml)),
|
|
1940
|
+
doc_ptr_of(document), &status);
|
|
1941
|
+
return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/* One-shot markup append (TODO.perf/34): parse the markup, walk
|
|
1945
|
+
* the fragment's children, append each to the parent — one
|
|
1946
|
+
* readonly gate + version bump for the whole batch. Returns the
|
|
1947
|
+
* appended count; -1 = parse failure; status codes surface as
|
|
1948
|
+
* negatives (-1000 - st) for the caller's check_status path. */
|
|
1949
|
+
static VALUE nf_append_markup(VALUE self, VALUE document,
|
|
1950
|
+
VALUE parent_addr, VALUE xml)
|
|
1951
|
+
{
|
|
1952
|
+
void *frag, *parent, *child, *next_child;
|
|
1953
|
+
int status, count = 0;
|
|
1954
|
+
|
|
1955
|
+
(void)self;
|
|
1956
|
+
resolve_binding_classes();
|
|
1957
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
1958
|
+
rb_raise(c_use_after_free_error,
|
|
1959
|
+
"owning document has been freed");
|
|
1960
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
1961
|
+
rb_raise(c_readonly_error,
|
|
1962
|
+
"document is readonly — mutation attempted");
|
|
1963
|
+
frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
|
|
1964
|
+
(size_t)RSTRING_LEN(StringValue(xml)),
|
|
1965
|
+
doc_ptr_of(document), &status);
|
|
1966
|
+
if (!frag)
|
|
1967
|
+
return INT2FIX(-1);
|
|
1968
|
+
parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
1969
|
+
rb_ivar_set(document, id_iv_version,
|
|
1970
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
1971
|
+
/* Appending DETACHES the child from the fragment — capture
|
|
1972
|
+
* the next sibling before each move or the walk corrupts. */
|
|
1973
|
+
for (child = f_first_child(frag); child != NULL; child = next_child) {
|
|
1974
|
+
next_child = f_next_sibling(child);
|
|
1975
|
+
status = f_append_child(parent, child);
|
|
1976
|
+
if (status != 0)
|
|
1977
|
+
return INT2FIX(-1000 - status);
|
|
1978
|
+
count++;
|
|
1979
|
+
}
|
|
1980
|
+
return INT2FIX(count);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1877
1983
|
static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
|
|
1878
1984
|
{
|
|
1879
1985
|
struct doc_handle *h;
|
|
@@ -2097,6 +2203,12 @@ void Init_native(void)
|
|
|
2097
2203
|
nf_copy_binding_element, 2);
|
|
2098
2204
|
rb_define_module_function(m_native, "set_binding_root",
|
|
2099
2205
|
nf_set_binding_root, 2);
|
|
2206
|
+
rb_define_module_function(m_native, "parse_binding_document",
|
|
2207
|
+
nf_parse_binding_document, 1);
|
|
2208
|
+
rb_define_module_function(m_native, "parse_fragment_addr",
|
|
2209
|
+
nf_parse_fragment_addr, 2);
|
|
2210
|
+
rb_define_module_function(m_native, "append_markup",
|
|
2211
|
+
nf_append_markup, 3);
|
|
2100
2212
|
rb_define_module_function(m_native, "fast_inner_xml",
|
|
2101
2213
|
nf_fast_inner_xml, 1);
|
|
2102
2214
|
rb_define_module_function(m_native, "doc_handle_attach",
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -70,6 +70,15 @@ class Leptris::XML::Document
|
|
|
70
70
|
# ParseOptions.new here would only discover flags==0 (#187 —
|
|
71
71
|
# per-call overhead is most of a small-document parse).
|
|
72
72
|
if options.nil? && !recover
|
|
73
|
+
# TODO.perf/35: parse + wrapper + lifetime handle in one C
|
|
74
|
+
# dispatch; failure raises through the shared path below.
|
|
75
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
76
|
+
doc = Leptris::XML::Native.parse_binding_document(xml)
|
|
77
|
+
raise Leptris::XML::ParseError,
|
|
78
|
+
"leptris_parse_string failed: " +
|
|
79
|
+
Leptris::XML::FFI.leptris_last_error.to_s if doc.nil?
|
|
80
|
+
return doc.tap { |d| d.readonly! if readonly }
|
|
81
|
+
end
|
|
73
82
|
raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
|
|
74
83
|
if raw.null?
|
|
75
84
|
raise Leptris::XML::ParseError,
|
|
@@ -9,12 +9,32 @@ class Leptris::XML::DocumentFragment
|
|
|
9
9
|
|
|
10
10
|
attr_reader :document, :c_ptr
|
|
11
11
|
|
|
12
|
-
def initialize(document, c_ptr)
|
|
12
|
+
def initialize(document, c_ptr, c_address: nil)
|
|
13
13
|
@document = document
|
|
14
14
|
@c_ptr = c_ptr
|
|
15
|
+
# TODO.perf/34: the address may arrive before any Pointer
|
|
16
|
+
# exists (the C parse face returns an Integer).
|
|
17
|
+
@c_address = c_address || c_ptr&.address
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Integer twin — reading it never materializes a Pointer.
|
|
21
|
+
def c_address
|
|
22
|
+
@c_address
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def c_ptr
|
|
26
|
+
@c_ptr ||= ::FFI::Pointer.new(@c_address)
|
|
15
27
|
end
|
|
16
28
|
|
|
17
29
|
def self.parse(xml, document)
|
|
30
|
+
# TODO.perf/34: the C face answers the fragment address with
|
|
31
|
+
# no status MemoryPointer and no Pointer mint; failures fall
|
|
32
|
+
# back to the FFI pair for the exact error.
|
|
33
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
34
|
+
addr = Leptris::XML::Native.parse_fragment_addr(
|
|
35
|
+
document, xml.to_s)
|
|
36
|
+
return new(document, nil, c_address: addr) if addr != false
|
|
37
|
+
end
|
|
18
38
|
raw, status = Leptris::XML::FFI.parse_fragment_with_status(
|
|
19
39
|
xml.to_s, document.c_ptr)
|
|
20
40
|
if raw.null?
|
|
@@ -25,6 +45,11 @@ class Leptris::XML::DocumentFragment
|
|
|
25
45
|
end
|
|
26
46
|
|
|
27
47
|
def children
|
|
48
|
+
# TODO.perf/34: one C pass (fragment nodes live in the
|
|
49
|
+
# document pool — the document's cache and version apply).
|
|
50
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
51
|
+
return Leptris::XML::Native.bulk_children(@document, @c_address)
|
|
52
|
+
end
|
|
28
53
|
pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
|
|
29
54
|
nodes = Array.new(pointers.size) do |i|
|
|
30
55
|
Leptris::XML::Node.wrap(pointers[i], @document, node_type: kinds[i])
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -555,6 +555,25 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
555
555
|
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
556
556
|
node_or_markup
|
|
557
557
|
when String
|
|
558
|
+
# TODO.perf/34: one dispatch parses the markup and appends
|
|
559
|
+
# every fragment child (single gate + bump). Negative
|
|
560
|
+
# returns: -1 re-runs the FFI path for the exact parse
|
|
561
|
+
# error; -1000-st routes through check_status.
|
|
562
|
+
if native_fast_children?
|
|
563
|
+
r = Leptris::XML::Native.append_markup(
|
|
564
|
+
@document, @c_address, node_or_markup)
|
|
565
|
+
if r.is_a?(Integer) && r >= 0
|
|
566
|
+
return Leptris::XML::NodeSet.new(@document, []) if r.zero?
|
|
567
|
+
return Leptris::XML::NodeSet.new(@document, last_added(r))
|
|
568
|
+
end
|
|
569
|
+
if r == -1
|
|
570
|
+
# parse failure: reproduce the exact error via the
|
|
571
|
+
# legacy path (it re-fails fast)
|
|
572
|
+
Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
573
|
+
else
|
|
574
|
+
Leptris::XML::FFI.check_status(-r - 1000)
|
|
575
|
+
end
|
|
576
|
+
end
|
|
558
577
|
frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
559
578
|
added = []
|
|
560
579
|
frag.children.each do |n|
|
|
@@ -567,6 +586,14 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
567
586
|
raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
|
|
568
587
|
end
|
|
569
588
|
end
|
|
589
|
+
# The markup-append face appends in fragment order; the added
|
|
590
|
+
# nodes are this element's last +count+ children.
|
|
591
|
+
def last_added(count)
|
|
592
|
+
kids = children.to_a
|
|
593
|
+
kids.last(count)
|
|
594
|
+
end
|
|
595
|
+
private :last_added
|
|
596
|
+
|
|
570
597
|
alias_method :<<, :add_child
|
|
571
598
|
|
|
572
599
|
def namespace
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.174.
|
|
4
|
+
version: 1.9.174.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -102,6 +102,8 @@ files:
|
|
|
102
102
|
- TODO.perf/31-battery-rounds-4-8.md
|
|
103
103
|
- TODO.perf/32-clean-host-floor-table.md
|
|
104
104
|
- TODO.perf/33-cbound-root-set.md
|
|
105
|
+
- TODO.perf/34-fragment-fast-lane.md
|
|
106
|
+
- TODO.perf/35-parse-default-c-face.md
|
|
105
107
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
106
108
|
- TODO.restructure/02-deep-copy-seam.md
|
|
107
109
|
- TODO.restructure/03-evaluation-context-seam.md
|