2023-06-09 16:59:21 +03:00
|
|
|
// TODO paging for large arrays/objects
|
2022-09-10 02:48:13 +08:00
|
|
|
// TODO maps, sets
|
|
|
|
|
// TODO show Errors in red
|
2022-10-17 02:49:21 +08:00
|
|
|
// TODO fns as clickable links (jump to definition), both for header and for
|
|
|
|
|
// content
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
import {el, stringify, scrollIntoViewIfNeeded} from './domutils.js'
|
2023-06-10 23:44:43 +03:00
|
|
|
import {with_code_execution} from '../index.js'
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-01-17 10:28:39 +08:00
|
|
|
|
|
|
|
|
// We test both for Object and globalThis.run_window.Object because objects may
|
|
|
|
|
// come both from run_window and current window (where they are created in
|
|
|
|
|
// metacircular interpreter
|
2022-11-08 21:05:54 +08:00
|
|
|
const has_custom_toString = object =>
|
2023-01-17 12:34:24 +08:00
|
|
|
object.toString != null
|
|
|
|
|
&& object.toString != globalThis.run_window.Object.prototype.toString
|
|
|
|
|
&& object.toString != Object.prototype.toString
|
2022-11-08 21:05:54 +08:00
|
|
|
|
|
|
|
|
const isError = object =>
|
2023-01-17 10:28:39 +08:00
|
|
|
object instanceof Error
|
|
|
|
|
||
|
2022-11-08 21:05:54 +08:00
|
|
|
object instanceof globalThis.run_window.Error
|
|
|
|
|
|
2022-12-02 04:13:32 +08:00
|
|
|
const isPromise = object =>
|
|
|
|
|
object instanceof globalThis.run_window.Promise
|
|
|
|
|
|
2023-06-09 16:59:21 +03:00
|
|
|
// Override behaviour for Date, becase Date has toJSON defined
|
|
|
|
|
const isDate = object =>
|
|
|
|
|
object instanceof globalThis.run_window.Date
|
|
|
|
|
||
|
|
|
|
|
object instanceof globalThis.run_window.Date.__original
|
|
|
|
|
|
|
|
|
|
const toJSON_safe = object => {
|
|
|
|
|
try {
|
2023-06-10 23:44:43 +03:00
|
|
|
return with_code_execution(() => {
|
|
|
|
|
return object.toJSON()
|
|
|
|
|
})
|
2023-06-09 16:59:21 +03:00
|
|
|
} catch(e) {
|
|
|
|
|
return object
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
const displayed_entries = object => {
|
2023-06-11 19:03:43 +03:00
|
|
|
if(object == null || typeof(object) != 'object') {
|
2023-06-09 16:59:21 +03:00
|
|
|
return []
|
2023-06-11 19:03:43 +03:00
|
|
|
} else if((object[Symbol.toStringTag]) == 'Module') {
|
|
|
|
|
return Object.entries(object)
|
2023-06-09 16:59:21 +03:00
|
|
|
} else if(isPromise(object)) {
|
2023-01-17 10:18:26 +08:00
|
|
|
return displayed_entries(
|
|
|
|
|
object.status.ok ? object.status.value : object.status.error
|
|
|
|
|
)
|
|
|
|
|
} else if(Array.isArray(object)) {
|
2022-09-10 02:48:13 +08:00
|
|
|
return object.map((v, i) => [i, v])
|
2023-06-09 16:59:21 +03:00
|
|
|
} else if(typeof(object.toJSON) == 'function') {
|
|
|
|
|
const result = toJSON_safe(object)
|
|
|
|
|
if(result == object) {
|
|
|
|
|
// avoid infinite recursion when toJSON returns itself
|
|
|
|
|
return Object.entries(object)
|
|
|
|
|
} else {
|
|
|
|
|
return displayed_entries(result)
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
2022-11-08 21:05:54 +08:00
|
|
|
return Object.entries(object)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 10:18:26 +08:00
|
|
|
const is_expandable = v =>
|
|
|
|
|
isPromise(v)
|
|
|
|
|
? (
|
|
|
|
|
v.status != null
|
|
|
|
|
&& is_expandable(v.status.ok ? v.status.value : v.status.error)
|
|
|
|
|
)
|
|
|
|
|
: (
|
|
|
|
|
typeof(v) == 'object'
|
|
|
|
|
&& v != null
|
|
|
|
|
&& displayed_entries(v).length != 0
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
|
2023-06-11 19:03:43 +03:00
|
|
|
const stringify_for_header_object = v => {
|
|
|
|
|
if(displayed_entries(v).length == 0) {
|
|
|
|
|
return '{}'
|
|
|
|
|
} else {
|
|
|
|
|
return '{…}'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const stringify_for_header = (v, no_toJSON = false) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
const type = typeof(v)
|
|
|
|
|
|
|
|
|
|
if(v === null) {
|
|
|
|
|
return 'null'
|
|
|
|
|
} else if(v === undefined) {
|
|
|
|
|
return 'undefined'
|
|
|
|
|
} else if(type == 'function') {
|
|
|
|
|
// TODO clickable link, 'fn', cursive
|
|
|
|
|
return 'fn ' + v.name
|
2023-06-05 12:37:39 +03:00
|
|
|
} else if(type == 'string') {
|
|
|
|
|
return JSON.stringify(v)
|
|
|
|
|
} else if(type == 'object') {
|
2023-06-11 19:03:43 +03:00
|
|
|
if((v[Symbol.toStringTag]) == 'Module') {
|
|
|
|
|
// protect against lodash module contains toJSON function
|
|
|
|
|
return stringify_for_header_object(v)
|
|
|
|
|
} else if (isPromise(v)) {
|
2023-06-05 12:37:39 +03:00
|
|
|
if(v.status == null) {
|
|
|
|
|
return `Promise<pending>`
|
2022-12-17 13:55:37 +08:00
|
|
|
} else {
|
2023-06-05 12:37:39 +03:00
|
|
|
if(v.status.ok) {
|
|
|
|
|
return `Promise<fulfilled: ${stringify_for_header(v.status.value)}>`
|
|
|
|
|
} else {
|
|
|
|
|
return `Promise<rejected: ${stringify_for_header(v.status.error)}>`
|
|
|
|
|
}
|
2022-12-17 13:55:37 +08:00
|
|
|
}
|
2023-06-09 16:59:21 +03:00
|
|
|
} else if (isDate(v)) {
|
|
|
|
|
return v.toString()
|
2023-06-05 12:37:39 +03:00
|
|
|
} else if(isError(v)) {
|
|
|
|
|
return v.toString()
|
|
|
|
|
} else if(Array.isArray(v)) {
|
2022-09-10 02:48:13 +08:00
|
|
|
if(v.length == 0) {
|
|
|
|
|
return '[]'
|
|
|
|
|
} else {
|
|
|
|
|
return '[…]'
|
|
|
|
|
}
|
2023-06-11 19:03:43 +03:00
|
|
|
} else if(typeof(v.toJSON) == 'function' && !no_toJSON) {
|
|
|
|
|
const json = toJSON_safe(v)
|
|
|
|
|
if(json == v) {
|
|
|
|
|
// prevent infinite recursion
|
|
|
|
|
return stringify_for_header(json, true)
|
|
|
|
|
} else {
|
|
|
|
|
return stringify_for_header(json)
|
|
|
|
|
}
|
2022-11-08 21:05:54 +08:00
|
|
|
} else if(has_custom_toString(v)) {
|
|
|
|
|
return v.toString()
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
2023-06-11 19:03:43 +03:00
|
|
|
return stringify_for_header_object(v)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return v.toString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-11 19:03:43 +03:00
|
|
|
const header_object = object => {
|
|
|
|
|
const prefix =
|
|
|
|
|
(object.constructor?.name == null || object.constructor?.name == 'Object')
|
|
|
|
|
? ''
|
|
|
|
|
: object.constructor.name + ' '
|
|
|
|
|
const inner = displayed_entries(object)
|
|
|
|
|
.map(([k,v]) => {
|
|
|
|
|
const value = stringify_for_header(v)
|
|
|
|
|
return `${k}: ${value}`
|
|
|
|
|
})
|
|
|
|
|
.join(', ')
|
|
|
|
|
return `${prefix} {${inner}}`
|
|
|
|
|
}
|
2023-06-09 16:59:21 +03:00
|
|
|
|
|
|
|
|
export const header = (object, no_toJSON = false) => {
|
2023-06-05 12:37:39 +03:00
|
|
|
const type = typeof(object)
|
|
|
|
|
|
|
|
|
|
if(object === null) {
|
2022-09-10 02:48:13 +08:00
|
|
|
return 'null'
|
2023-06-05 12:37:39 +03:00
|
|
|
} else if(object === undefined) {
|
|
|
|
|
return 'undefined'
|
|
|
|
|
} else if(type == 'function') {
|
|
|
|
|
// TODO clickable link, 'fn', cursive
|
|
|
|
|
return 'fn ' + object.name
|
|
|
|
|
} else if(type == 'string') {
|
|
|
|
|
return JSON.stringify(object)
|
|
|
|
|
} else if(type == 'object') {
|
2023-06-11 19:03:43 +03:00
|
|
|
if((object[Symbol.toStringTag]) == 'Module') {
|
|
|
|
|
// protect against lodash module contains toJSON function
|
|
|
|
|
return header_object(object)
|
|
|
|
|
} else if(isPromise(object)) {
|
2023-01-17 10:18:26 +08:00
|
|
|
if(object.status == null) {
|
|
|
|
|
return `Promise<pending>`
|
|
|
|
|
} else {
|
|
|
|
|
if(object.status.ok) {
|
|
|
|
|
return `Promise<fulfilled: ${header(object.status.value)}>`
|
|
|
|
|
} else {
|
|
|
|
|
return `Promise<rejected: ${header(object.status.error)}>`
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-06-09 16:59:21 +03:00
|
|
|
} else if(isDate(object)) {
|
|
|
|
|
return object.toString()
|
2022-12-02 04:13:32 +08:00
|
|
|
} else if(isError(object)) {
|
2022-09-10 02:48:13 +08:00
|
|
|
return object.toString()
|
|
|
|
|
} else if(Array.isArray(object)) {
|
|
|
|
|
return '['
|
|
|
|
|
+ object
|
|
|
|
|
.map(stringify_for_header)
|
|
|
|
|
.join(', ')
|
|
|
|
|
+ ']'
|
2023-06-09 16:59:21 +03:00
|
|
|
} else if(typeof(object.toJSON) == 'function' && !no_toJSON) {
|
2023-06-11 19:03:43 +03:00
|
|
|
const json = toJSON_safe(object)
|
|
|
|
|
if(json == object) {
|
|
|
|
|
// prevent infinite recursion
|
2023-06-09 16:59:21 +03:00
|
|
|
return header(object, true)
|
2023-06-11 19:03:43 +03:00
|
|
|
} else {
|
|
|
|
|
return header(json)
|
2023-06-09 16:59:21 +03:00
|
|
|
}
|
2022-11-08 21:05:54 +08:00
|
|
|
} else if(has_custom_toString(object)) {
|
2022-10-18 21:45:55 +08:00
|
|
|
return object.toString()
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
2023-06-11 19:03:43 +03:00
|
|
|
return header_object(object)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return object.toString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const get_path = (o, path) => {
|
|
|
|
|
if(path.length == 0) {
|
|
|
|
|
return o
|
|
|
|
|
} else {
|
|
|
|
|
const [start, ...rest] = path
|
|
|
|
|
return get_path(o[start], rest)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ValueExplorer {
|
|
|
|
|
|
|
|
|
|
constructor({
|
|
|
|
|
container,
|
|
|
|
|
event_target = container,
|
|
|
|
|
scroll_to_element,
|
|
|
|
|
on_escape = () => {},
|
|
|
|
|
} = {}
|
|
|
|
|
) {
|
|
|
|
|
this.container = container
|
|
|
|
|
this.scroll_to_element = scroll_to_element
|
|
|
|
|
this.on_escape = on_escape
|
|
|
|
|
|
|
|
|
|
event_target.addEventListener('keydown', (e) => {
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Right -
|
|
|
|
|
- does not has children - nothing
|
|
|
|
|
- has children - first click expands, second jumps to first element
|
|
|
|
|
|
|
|
|
|
Left -
|
|
|
|
|
- root - nothing
|
|
|
|
|
- not root collapse node, goes to parent if already collapsed
|
|
|
|
|
|
|
|
|
|
Up - goes to prev visible element
|
|
|
|
|
Down - goes to next visible element
|
|
|
|
|
|
|
|
|
|
Click - select and toggles expand
|
|
|
|
|
*/
|
2022-10-25 04:43:35 +08:00
|
|
|
|
|
|
|
|
if(e.key == 'F1') {
|
|
|
|
|
this.on_escape()
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
const current_object = get_path(this.value, this.current_path)
|
|
|
|
|
|
|
|
|
|
if(e.key == 'ArrowDown' || e.key == 'j'){
|
|
|
|
|
// Do not scroll
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
|
|
if(is_expandable(current_object) && this.is_expanded(this.current_path)) {
|
|
|
|
|
this.select_path(this.current_path.concat(
|
|
|
|
|
displayed_entries(current_object)[0][0]
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
const next = p => {
|
|
|
|
|
if(p.length == 0) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const parent = p.slice(0, p.length - 1)
|
|
|
|
|
const children = displayed_entries(get_path(this.value, parent))
|
|
|
|
|
const child_index = children.findIndex(([k,v]) =>
|
|
|
|
|
k == p[p.length - 1]
|
|
|
|
|
)
|
|
|
|
|
const next_child = children[child_index + 1]
|
|
|
|
|
if(next_child == null) {
|
|
|
|
|
return next(parent)
|
|
|
|
|
} else {
|
|
|
|
|
return [...parent, next_child[0]]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const next_path = next(this.current_path)
|
|
|
|
|
if(next_path != null) {
|
|
|
|
|
this.select_path(next_path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(e.key == 'ArrowUp' || e.key == 'k'){
|
|
|
|
|
// Do not scroll
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
|
|
if(this.current_path.length == 0) {
|
|
|
|
|
this.on_escape()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const parent = this.current_path.slice(0, this.current_path.length - 1)
|
|
|
|
|
const children = displayed_entries(get_path(this.value, parent))
|
|
|
|
|
const child_index = children.findIndex(([k,v]) =>
|
|
|
|
|
k == this.current_path[this.current_path.length - 1]
|
|
|
|
|
)
|
|
|
|
|
const next_child = children[child_index - 1]
|
|
|
|
|
if(next_child == null) {
|
|
|
|
|
this.select_path(parent)
|
|
|
|
|
} else {
|
|
|
|
|
const last = p => {
|
|
|
|
|
if(!is_expandable(get_path(this.value, p)) || !this.is_expanded(p)) {
|
|
|
|
|
return p
|
|
|
|
|
} else {
|
|
|
|
|
const children = displayed_entries(get_path(this.value, p))
|
|
|
|
|
.map(([k,v]) => k)
|
|
|
|
|
return last([...p, children[children.length - 1]])
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.select_path(last([...parent, next_child[0]]))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(e.key == 'ArrowLeft' || e.key == 'h'){
|
|
|
|
|
// Do not scroll
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
|
|
const is_expanded = this.is_expanded(this.current_path)
|
|
|
|
|
if(!is_expandable(current_object) || !is_expanded) {
|
|
|
|
|
if(this.current_path.length != 0) {
|
|
|
|
|
const parent = this.current_path.slice(0, this.current_path.length - 1)
|
|
|
|
|
this.select_path(parent)
|
|
|
|
|
} else {
|
|
|
|
|
this.on_escape()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.toggle_expanded()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(e.key == 'ArrowRight' || e.key == 'l'){
|
|
|
|
|
// Do not scroll
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
|
|
if(is_expandable(current_object)) {
|
|
|
|
|
const is_expanded = this.is_expanded(this.current_path)
|
|
|
|
|
if(!is_expanded) {
|
|
|
|
|
this.toggle_expanded()
|
|
|
|
|
} else {
|
|
|
|
|
const children = displayed_entries(get_path(this.value, this.current_path))
|
|
|
|
|
this.select_path(
|
|
|
|
|
[
|
|
|
|
|
...this.current_path,
|
|
|
|
|
children[0][0],
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get_node_data(path, node_data = this.node_data) {
|
|
|
|
|
if(path.length == 0) {
|
|
|
|
|
return node_data
|
|
|
|
|
} else {
|
|
|
|
|
const [start, ...rest] = path
|
|
|
|
|
return this.get_node_data(rest, node_data.children[start])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is_expanded(path) {
|
|
|
|
|
return this.get_node_data(path).is_expanded
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
on_click(path) {
|
|
|
|
|
this.select_path(path)
|
|
|
|
|
this.toggle_expanded()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clear() {
|
|
|
|
|
this.container.innerHTML = ''
|
|
|
|
|
this.node_data = {is_expanded: true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(value) {
|
|
|
|
|
this.clear()
|
|
|
|
|
this.value = value
|
|
|
|
|
const path = []
|
|
|
|
|
this.container.appendChild(this.render_value_explorer_node(null, value, path, this.node_data))
|
|
|
|
|
this.select_path(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select_path(current_path) {
|
|
|
|
|
if(this.current_path != null) {
|
|
|
|
|
this.set_active(this.current_path, false)
|
|
|
|
|
}
|
|
|
|
|
this.current_path = current_path
|
|
|
|
|
this.set_active(this.current_path, true)
|
|
|
|
|
// Check that was already added to document
|
|
|
|
|
if(document.contains(this.container)) {
|
|
|
|
|
const target = this.get_node_data(current_path).el.getElementsByClassName('value_explorer_header')[0]
|
|
|
|
|
if(this.scroll_to_element == null) {
|
|
|
|
|
scrollIntoViewIfNeeded(this.container.parentNode, target)
|
|
|
|
|
} else {
|
|
|
|
|
this.scroll_to_element(target)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set_active(path, is_active) {
|
|
|
|
|
const el = this.get_node_data(path).el.getElementsByClassName('value_explorer_header')[0]
|
|
|
|
|
if(is_active) {
|
|
|
|
|
el.classList.add('active')
|
|
|
|
|
} else {
|
|
|
|
|
el.classList.remove('active')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set_expanded(fn) {
|
|
|
|
|
if(typeof(fn) == 'boolean') {
|
|
|
|
|
return this.set_expanded(() => fn)
|
|
|
|
|
}
|
|
|
|
|
const val = this.is_expanded(this.current_path)
|
|
|
|
|
const data = this.get_node_data(this.current_path)
|
|
|
|
|
data.is_expanded = fn(data.is_expanded)
|
|
|
|
|
const prev_dom_node = data.el
|
|
|
|
|
const key = this.current_path.length == 0
|
|
|
|
|
? null
|
|
|
|
|
: this.current_path[this.current_path.length - 1]
|
|
|
|
|
const value = get_path(this.value, this.current_path)
|
|
|
|
|
const next = this.render_value_explorer_node(key, value, this.current_path, data)
|
|
|
|
|
prev_dom_node.parentNode.replaceChild(next, prev_dom_node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggle_expanded() {
|
|
|
|
|
this.set_expanded(e => !e)
|
|
|
|
|
this.set_active(this.current_path, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render_value_explorer_node(key, value, path, node_data) {
|
|
|
|
|
|
|
|
|
|
const is_exp = is_expandable(value)
|
|
|
|
|
const is_expanded = is_exp && node_data.is_expanded
|
|
|
|
|
|
|
|
|
|
node_data.children = {}
|
|
|
|
|
|
|
|
|
|
const result = el('div', 'value_explorer_node',
|
|
|
|
|
|
|
|
|
|
el('span', {
|
|
|
|
|
class: 'value_explorer_header',
|
|
|
|
|
click: this.on_click.bind(this, path),
|
|
|
|
|
},
|
|
|
|
|
is_exp
|
|
|
|
|
? (is_expanded ? '▼' : '▶')
|
|
|
|
|
: '\xa0',
|
|
|
|
|
|
|
|
|
|
key == null
|
|
|
|
|
? null
|
|
|
|
|
: el('span', 'value_explorer_key', key.toString(), ': '),
|
|
|
|
|
|
|
|
|
|
key == null || !is_exp || !is_expanded
|
|
|
|
|
// Full header
|
|
|
|
|
? header(value)
|
|
|
|
|
// Short header
|
|
|
|
|
: Array.isArray(value)
|
|
|
|
|
? 'Array(' + value.length + ')'
|
|
|
|
|
: ''
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
(is_exp && is_expanded)
|
|
|
|
|
? displayed_entries(value).map(([k,v]) => {
|
|
|
|
|
node_data.children[k] = {}
|
|
|
|
|
return this.render_value_explorer_node(k, v, [...path, k], node_data.children[k])
|
|
|
|
|
})
|
|
|
|
|
: []
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
node_data.el = result
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|