Last active
January 11, 2017 16:30
-
-
Save masyukun/4190b195afb02e940453c3fbb829dcc4 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
(:~ | |
This module converts an XML document into a JSON document. | |
- Element ordering is preserved. | |
- Attributes and namespaces are ignored. | |
- Identical element names become JSON arrays. | |
Example usage: | |
let $uri := "somedoc.xml" | |
let $doc := fn:doc($uri) | |
return jsontools:jsonify($doc) | |
This version allows attribute array="true" to specify that an element should contain a JSON array. | |
This version returns a JSON null when the value of the text node is "null" and not specifically typed. | |
This version looks for an optional attribute on text nodes called type, with valid values of | |
- "boolean" or "number" = which will cause the text value to be cast to the native JSON equivalent type. | |
- "ignore" = which preserves any element marked such as XML text inside the JSON object. | |
This version supports a second function call with an optional top-level JSON array of objects. | |
This version supports creating a headless JSON object, by passing the option "headless" | |
- Example: jsontools:jsonify($doc/child::*, ("headless")) | |
@author Matthew Royal | |
@version 1.7 | |
@since 1.0 | |
@see https://gist.github.com/masyukun | |
@see Pigritia Impatiens Hubris | |
:) | |
xquery version "1.0-ml"; | |
module namespace jsontools = "http://matthewroyal.com/marklogic/jsontools"; | |
import module namespace json = "http://marklogic.com/xdmp/json" at "/MarkLogic/json/json.xqy"; | |
(: Generate JSON object from map structure :) | |
declare function jsontools:jsonify($documents as node()+) { | |
if (fn:count($documents) le 1) then | |
xdmp:to-json(map:entry(fn:local-name($documents), jsontools:mapify($documents))) | |
else | |
xdmp:to-json(map:entry(fn:local-name($documents[1]), for $d in $documents return jsontools:mapify($d))) | |
}; | |
(: Generate JSON object from map structure :) | |
declare function jsontools:jsonify($documents as node()+, $options) { | |
if (fn:exists($options) and $options instance of xs:string+) then | |
if (fn:count($options) gt 1) then | |
fn:error(xs:QName("INVALID-OPTION"), "Only one option is allowed: (headless|wraparray)") | |
else if ($options = "headless") then | |
let $headlessObj := map:map() | |
let $_ := | |
for $d in $documents | |
let $local := fn:local-name($d) | |
let $e := map:get($headlessObj, $local ) | |
return | |
if (fn:not(fn:exists($e))) then | |
map:put( $headlessObj, $local, jsontools:mapify($d) ) | |
else | |
let $_ := | |
if ($e instance of json:array) then () | |
else | |
let $swap := $e | |
let $_ := map:put($headlessObj, $local, json:array()) | |
return json:array-push(map:get($headlessObj, $local), $swap) | |
return json:array-push(map:get($headlessObj, $local), jsontools:mapify($d) ) | |
return | |
xdmp:to-json( | |
$headlessObj | |
) | |
else if ($options = "wraparray") then | |
jsontools:jsonify($documents, fn:true()) | |
else | |
fn:error(xs:QName("INVALID-OPTION"), "Unrecognized option") | |
else if (fn:exists($options) and $options instance of xs:boolean) then | |
(: Return multiple nodes as a JSON array without a key :) | |
let $array := json:array() | |
let $_ := | |
for $d in $documents | |
return json:array-push( $array, jsontools:mapify($d) ) | |
return $array | |
else | |
jsontools:jsonify($documents) | |
}; | |
(: Generate nested map of XML document structure :) | |
declare function jsontools:mapify( $document as node() ) { | |
let $type := fn:lower-case($document/@type) | |
(: Get all the names of the child names :) | |
let $childNames := | |
for $n in $document/child::node() | |
return | |
if (not(local-name($n))) then () | |
else local-name($n) | |
(: Keep elements in original order (mostly -- except repeated element names, which become arrays) :) | |
let $o := json:object-define( fn:distinct-values(($childNames)) ) | |
(: Configure the ones used more than once into arrays :) | |
let $_ := | |
for $n in fn:distinct-values(($childNames)) | |
let $single := fn:count($childNames[. eq $n]) eq 1 | |
return if ($single) then () else map:put($o, $n, json:array()) | |
(:: Configure ones explicitly marked with array="true" as arrays :) | |
let $_ := | |
for $n in $document/child::node() | |
let $isArray := fn:lower-case($n/@array) eq "true" | |
return if ($isArray) then map:put($o, local-name($n), json:array()) else () | |
(: Add ignored elements into the text component :) | |
let $ignoredNodeTexts := | |
for $n in $document/child::node() | |
return | |
if ($type eq "ignore") then xdmp:quote($n) | |
else () | |
(: Get the text node :) | |
let $text := | |
fn:replace( | |
fn:normalize-space( | |
fn:string-join(($ignoredNodeTexts, $document/text())," ") | |
) | |
, "(^ +| +$)", "" | |
) | |
let $_ := | |
if ($text) then () | |
else | |
for $n in $document/child::node() | |
let $childtype := fn:lower-case($n/@type) | |
let $childtext := fn:normalize-space(fn:string-join($n/text()," ")) | |
return | |
if ($type eq "ignore") then () | |
else if (not(local-name($n))) then () | |
(: Multiple identical names will be grouped into arrays, also items with array="true" :) | |
else if ( (fn:count($childNames[. eq local-name($n)]) gt 1) or (fn:lower-case($n/@array) eq "true") ) then | |
let $thisarray := map:get($o, local-name($n)) | |
return | |
if ($childtext) then json:array-push($thisarray, | |
if ($childtype eq "number") then xs:decimal(fn:replace($childtext, "[^0-9\.\-]", "")) | |
else if ($childtype eq "boolean") then xs:boolean(fn:lower-case($childtext)) | |
else if (not(exists($childtext)) or $childtext eq "null") then () | |
else $childtext | |
) | |
else json:array-push($thisarray, jsontools:mapify($n)) | |
(: Single items :) | |
else | |
if ($childtext) then map:put($o, local-name($n), | |
if ($childtype eq "number") then xs:decimal(fn:replace($childtext, "[^0-9\.\-]", "")) | |
else if ($childtype eq "boolean") then xs:boolean(fn:lower-case($childtext)) | |
else if (not(exists($childtext)) or $childtext eq "null") then () | |
else $childtext | |
) | |
else map:put($o, local-name($n), jsontools:mapify($n)) | |
(: Return the map :) | |
return | |
if ($text) then | |
if ($type eq "number") then xs:decimal(fn:replace($text, "[^0-9\.\-]", "")) | |
else if ($type eq "boolean") then xs:boolean(fn:lower-case($text)) | |
else if (not(exists($text)) or $text eq "null") then () | |
else $text | |
else $o | |
}; | |
Version 1.6 includes the type="ignore" attribute. Adding this attribute to an element will result in jsonify ignoring that element and its children, and instead convert its children into text node(s) attached to the parent.
Version 1.7 accepts string options
- "headless" = Create a JSON object with no head. Example usage: jsontools:jsonify($doc/child::*, ("headless"))
- "wraparray" = Return a JSON array instead of a JSON object
- backwardly compatible: boolean option still works
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Lots of changes in 1.5, especially allowing explicit creation of JSON arrays and value type (boolean, number, null)