Last active
April 17, 2018 12:06
-
-
Save jmakeig/7273340b3065f442030451d04587c466 to your computer and use it in GitHub Desktop.
Recursive descent identity transform in MarkLogic in JavaScript and XQuery
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
/** | |
* @param {Node} node | |
* @param {string} path | |
* @param {Object} [bindings={}] | |
* @return {Generator} | |
*/ | |
function* xpath(node, path, bindings = {}) { | |
yield* node.xpath(path, bindings); | |
} | |
/** | |
* Force a singleton to be an Iterable of one. | |
* | |
* @param {any} value | |
* @return {Iterable} | |
*/ | |
function makeIterable(value) { | |
if (value[Symbol.iterator]) return value; | |
return [value]; | |
} | |
/** | |
* Recrusive descent transformation of one XML tree to another. | |
* | |
* @param {Node|Iterable<Node>} node | |
* @param {function} [visitor=identity] | |
* @return {Node} | |
*/ | |
function transform(node, visitor = indentity) { | |
if (!(node instanceof Node)) return node; | |
if ('function' !== typeof visitor) throw new TypeError(typeof visitor); | |
/** | |
* @private | |
* | |
* @param {Node|Iterable<Node>} nodes | |
* @param {NodeBuilder} [builder=new NodeBuilder] | |
* @return {Node} | |
*/ | |
function descend(nodes, builder = new NodeBuilder()) { | |
nodes = makeIterable(nodes); | |
const selector = './node()|./attribute()'; | |
for (const nd of nodes) { | |
visitor(nd, builder, () => descend(xpath(nd, selector), builder)); | |
} | |
return builder; | |
} | |
const trans = descend(node); | |
if (trans.toNode) return trans.toNode(); | |
return trans; | |
} | |
/** | |
* Indentity transform. To provide your own transfromation logic, implement | |
* this function signature and pass the function instance as the `visitor` | |
* parameter of `transform()`. | |
* | |
* @param {Node} node - the current node being evaluated | |
* @param {NodeBuilder} builder - the active `NodeBuilder` instance shared among all nodes | |
* @param {function} next - process the current nodes’s children (via closure, don’t provide arguments) | |
* @return {undefined} - the state is maintained within the shared `builder` | |
*/ | |
function indentity(node, builder, next) { | |
switch (node.nodeKind) { | |
case 'element': | |
builder.startElement(fn.name(node), fn.namespaceUri(node)); | |
next(); | |
builder.endElement(); | |
break; | |
case 'text': | |
builder.addText(node.valueOf()); | |
break; | |
case 'attribute': | |
builder.addAttribute( | |
fn.name(node), | |
node.valueOf(), | |
fn.namespaceUri(node) | |
); | |
break; | |
case 'document': | |
builder.startDocument(); | |
next(); | |
builder.endDocument(); | |
break; | |
case 'comment': | |
case 'processing-instruction': | |
break; | |
default: | |
throw new TypeError(node.nodeKind); | |
} | |
} | |
const xml = fn.head( | |
xdmp.unquote(`<a xmlns="a" a="a"> | |
<b>b <c>c</c> d</b> | |
<!-- comment here --> | |
<e xmlns="e" e="e"><f a:a="a" xmlns:a="a"/>e</e> | |
</a>`) | |
); | |
Sequence.from([transform(xml), xml]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
/** | |
* @param {Node} node | |
* @param {string} path | |
* @param {Object} [bindings={}] | |
* @return {Sequence<Node>} | |
*/ | |
function xpath(node, path, bindings) { | |
if (undefined === bindings) bindings = {}; | |
return node.xpath(path, bindings); | |
} | |
/** | |
* Force a singleton to be an Iterable of one. | |
* | |
* @param {any} value | |
* @return {Iterable} | |
*/ | |
function makeIterable(value) { | |
if (value instanceof ValueIterator) { | |
return value.toArray(); | |
} | |
return [].concat(value); | |
} | |
/** | |
* Recrusive descent transformation of one XML tree to another. | |
* | |
* @param {Node|Iterable<Node>} node | |
* @param {function} [visitor=identity] | |
* @return {Node} | |
*/ | |
function transform(node, visitor) { | |
if (undefined === visitor) visitor = identity; | |
if (!(node instanceof Node)) return node; | |
if ('function' !== typeof visitor) throw new TypeError(typeof visitor); | |
/** | |
* @private | |
* | |
* @param {Node|Iterable<Node>} nodes | |
* @param {NodeBuilder} [builder=new NodeBuilder] | |
* @return {Node} | |
*/ | |
function descend(nodes, builder) { | |
if (undefined === builder) builder = new NodeBuilder(); | |
nodes = makeIterable(nodes); | |
var selector = './node()|./attribute()'; | |
for (var i = 0; i < nodes.length; i++) { | |
var nd = nodes[i]; | |
visitor(nd, builder, function() { | |
return descend(xpath(nd, selector), builder); | |
}); | |
} | |
return builder; | |
} | |
var trans = descend(node); | |
if (trans.toNode) return trans.toNode(); | |
return trans; | |
} | |
/** | |
* Indentity transform. To provide your own transfromation logic, implement | |
* this function signature and pass the function instance as the `visitor` | |
* parameter of `transform()`. | |
* | |
* @param {Node} node - the current node being evaluated | |
* @param {NodeBuilder} builder - the active `NodeBuilder` instance shared among all nodes | |
* @param {function} next - process the current nodes’s children (via closure, don’t provide arguments) | |
* @return {undefined} - the state is maintained within the shared `builder` | |
*/ | |
function identity(node, builder, next) { | |
switch (node.nodeKind) { | |
case 'element': | |
builder.startElement(fn.name(node), fn.namespaceUri(node)); | |
next(); | |
builder.endElement(); | |
break; | |
case 'text': | |
builder.addText(node.valueOf()); | |
break; | |
case 'attribute': | |
builder.addAttribute( | |
fn.name(node), | |
node.valueOf(), | |
fn.namespaceUri(node) | |
); | |
break; | |
case 'document': | |
builder.startDocument(); | |
next(); | |
builder.endDocument(); | |
break; | |
case 'comment': | |
case 'processing-instruction': | |
break; | |
default: | |
throw new TypeError(node.nodeKind); | |
} | |
} | |
var xml = fn.head( | |
xdmp.unquote( | |
'<a xmlns="a" a="a"><b>b <c>c</c> d</b><!-- comment here --><e xmlns="e" e="e"><f a:a="a" xmlns:a="a"/>e</e></a>' | |
) | |
); | |
xdmp.arrayValues([transform(xml), xml]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare namespace local = 'local'; | |
declare function local:transform($nodes as node()*) as node()* { | |
for $n in $nodes return | |
typeswitch ($n) | |
(: case element(order:customer) return <order:customer id="{/cust:customer[cust:name = $n]/cust:id}">{string($n)}</order:customer>:) | |
case element() return element { fn:node-name($n) } { local:transform($n/node()|$n/attribute()) } | |
case attribute() return $n | |
case text() return $n | |
case comment() return $n | |
case document-node() return $n | |
default return local:transform($n/node()) | |
}; | |
let $xml := document { | |
<a xmlns="a" a="a"> | |
<b>b <c>c</c> d</b> | |
<!-- comment here --> | |
<e xmlns="e" e="e"><f a:a="a" xmlns:a="a"/>e</e> | |
</a> | |
} | |
return ( | |
local:transform($xml), | |
$xml | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment