├── .gitattributes ├── .gitmodules ├── README.md ├── memory-operations-functional.xqy ├── memory-operations-pure-xquery.xqy ├── memory-operations.xqy ├── mlpm.json ├── node-operations.xqy └── test ├── memory-operations-functional-test.xqy ├── memory-operations-pure-xquery-test.xqy └── memory-operations-test.xqy /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "xray"] 2 | path = xray 3 | url = https://github.com/robwhitby/xray.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XQuery XML Memory Operations 2 | This module is created to provide an optimized way to perform operations on XML in memory. With heavy use XPath axis, node comparisions, and set operators this library is able to make changes to XML while only reconstructing nodes within the direct path of the nodes being altered. It also provides a way to perform multiple operations, while only reconstructiing the XML tree once. 3 | 4 | The goal is to provide a way to bring the functionality of the XQuery Update Facility 1.0 (http://www.w3.org/TR/xquery-update-10/) to MarkLogic. 5 | 6 | ## Advanced Transform Statements 7 | By calling mem:copy($node as node()) as xs:string the following calls to mem operations are stored and not actually executed until mem:execute($transaction-id as xs:string) is called. This allows the document to be rebuilt only once and increases performance. 8 | 9 | You can just perform a copy and execute that will provide a copy of the node free from ancestors. 10 | 11 | Examples of this is as follows: 12 | ```xquery 13 | (: 14 | copy $c := fn:root($file) 15 | modifiy (replace nodes $c/title with element title {"my new title"}, 16 | insert nodes attribute new-attribute {"my new attribute"} as last into 17 | $c) 18 | return $c 19 | => 20 | :) 21 | mem:copy(fn:root($file)) ! 22 | ( 23 | mem:replace(.,$file/title, element title {"my new title"}), 24 | mem:insert-child(.,$file, attribute new-attribute {"my new attribute"}), 25 | mem:execute(.)) 26 | ``` 27 | 28 | By using mem:copy and passing in a node, you indicating what the new root should be. 29 | ```xquery 30 | (: 31 | let $oldx := /a/b/x 32 | return 33 | copy $newx := $oldx 34 | modify (rename node $newx as "newx", 35 | replace value of node $newx with $newx * 2) 36 | return ($oldx, $newx) 37 | => 38 | :) 39 | let $oldx := /a/b/x 40 | return 41 | ($oldx, 42 | mem:copy($oldx) ! 43 | ( 44 | mem:rename(.,$oldx, fn:QName("","newx")), 45 | mem:replace-value(.,$oldx, $oldx * 5), 46 | mem:execute(.) 47 | ) 48 | ) 49 | (: 50 | => 51 | (...,...) 52 | :) 53 | ``` 54 | 55 | ## Other Operations 56 | ```xquery 57 | (: See http://www.w3.org/TR/xquery-update-10/#id-delete :) 58 | mem:delete($file//comment()), 59 | (: See http://www.w3.org/TR/xquery-update-10/#id-insert :) 60 | mem:insert-after($file/title, element new-sibling-after {"my new sibling element"} ), 61 | mem:insert-before($file/title, element new-sibling-before {"my new sibling element"} ), 62 | mem:insert-child($file, attribute new-attribute {"my new attribute"} ), 63 | mem:insert-child-first($file, attribute new-attribute-2 {"my new attribute"} ), 64 | (: See http://www.w3.org/TR/xquery-update-10/#id-rename :) 65 | mem:rename($file//block, fn:QName('http://www.w3.org/1999/xhtml','p')), 66 | (: See http://www.w3.org/TR/xquery-update-10/#id-replacing-node :) 67 | mem:replace($file/title, element title {"my new title"} ), 68 | (: See http://www.w3.org/TR/xquery-update-10/#id-replacing-node-value :) 69 | mem:replace-value($file/title, "my new title" ), 70 | (: Transform by using a function reference :) 71 | mem:transform($title,function($node as node()) as node()* {element new-title {"This is so awesome!"}}) 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /memory-operations-functional.xqy: -------------------------------------------------------------------------------- 1 | xquery version "1.0-ml"; 2 | (:~ 3 | Copyright (c) 2013 Ryan Dew 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | @author Ryan Dew (ryan.j.dew@gmail.com) 18 | @version 1.0.3 19 | @description This is a module with function changing XML in memory by creating subtrees using the ancestor, preceding-sibling, and following-sibling axes 20 | and intersect/except expressions. Requires MarkLogic 7+. 21 | ~:) 22 | module namespace mem-op-fun="http://maxdewpoint.blogspot.com/memory-operations/functional"; 23 | import module namespace mem-op="http://maxdewpoint.blogspot.com/memory-operations" at "memory-operations.xqy"; 24 | import module namespace node-op="http://maxdewpoint.blogspot.com/node-operations" at "node-operations.xqy"; 25 | declare default function namespace "http://www.w3.org/2005/xpath-functions"; 26 | declare namespace xdmp="http://marklogic.com/xdmp"; 27 | declare namespace map="http://marklogic.com/xdmp/map"; 28 | declare option xdmp:mapping "true"; 29 | declare option xdmp:copy-on-validate "true"; 30 | 31 | (: Queue insert a child into the node :) 32 | declare function mem-op-fun:insert-child( 33 | $transaction-map as map:map, 34 | $parent-node as element()*, 35 | $new-nodes as node()*) 36 | as map:map? 37 | { 38 | mem-op-fun:queue( 39 | $transaction-map, $parent-node, $new-nodes, "insert-child") 40 | }; 41 | 42 | (: Queue insert as first child into the node :) 43 | declare function mem-op-fun:insert-child-first( 44 | $transaction-map as map:map, 45 | $parent-node as element()*, 46 | $new-nodes as node()*) 47 | as map:map? 48 | { 49 | mem-op-fun:queue( 50 | $transaction-map, 51 | $parent-node, 52 | $new-nodes, 53 | "insert-child-first") 54 | }; 55 | 56 | (: Queue insert a sibling before the node :) 57 | declare function mem-op-fun:insert-before( 58 | $transaction-map as map:map, 59 | $sibling as node()*, 60 | $new-nodes as node()*) 61 | as map:map? 62 | { 63 | mem-op-fun:queue( 64 | $transaction-map, $sibling, $new-nodes, "insert-before") 65 | }; 66 | 67 | (: Queue insert a sibling after the node :) 68 | declare function mem-op-fun:insert-after( 69 | $transaction-map as map:map, 70 | $sibling as node()*, 71 | $new-nodes as node()*) 72 | as map:map? 73 | { 74 | mem-op-fun:queue( 75 | $transaction-map, $sibling, $new-nodes, "insert-after") 76 | }; 77 | 78 | (: Queue replace of the node :) 79 | declare function mem-op-fun:replace( 80 | $transaction-map as map:map, 81 | $replace-nodes as node()*, 82 | $new-nodes as node()*) 83 | as map:map? 84 | { 85 | mem-op-fun:queue( 86 | $transaction-map, 87 | $replace-nodes except $replace-nodes/descendant::node(), 88 | $new-nodes, 89 | "replace") 90 | }; 91 | 92 | (: Queue delete the node :) 93 | declare function mem-op-fun:delete( 94 | $transaction-map as map:map, 95 | $delete-nodes as node()*) 96 | as map:map? 97 | { 98 | mem-op-fun:queue( 99 | $transaction-map, 100 | $delete-nodes except $delete-nodes/descendant::node(), 101 | (), 102 | "replace") 103 | }; 104 | 105 | (: Queue renaming of node :) 106 | declare function mem-op-fun:rename( 107 | $transaction-map as map:map, 108 | $nodes-to-rename as node()*, 109 | $new-name as xs:QName) 110 | as map:map? 111 | { 112 | mem-op-fun:queue( 113 | $transaction-map, 114 | $nodes-to-rename, 115 | element { $new-name } { }, 116 | "rename") 117 | }; 118 | 119 | (: Queue replacement of a value of an element or attribute :) 120 | declare function mem-op-fun:replace-value( 121 | $transaction-map as map:map, 122 | $nodes-to-change as node()*, 123 | $value as xs:anyAtomicType?) 124 | as map:map? 125 | { 126 | mem-op-fun:queue( 127 | $transaction-map, 128 | $nodes-to-change, 129 | text { $value }, 130 | "replace-value") 131 | }; 132 | 133 | (: Queue replacement of contents of an element :) 134 | declare function mem-op-fun:replace-contents( 135 | $transaction-map as map:map, 136 | $nodes-to-change as node()*, 137 | $contents as node()*) 138 | as map:map? 139 | { 140 | mem-op-fun:queue( 141 | $transaction-map, 142 | $nodes-to-change, 143 | $contents, 144 | "replace-value") 145 | }; 146 | 147 | (: Queues the replacement of the node with the result of the passed function :) 148 | declare function mem-op-fun:transform( 149 | $transaction-map as map:map, 150 | $nodes-to-change as node()*, 151 | $transform-function as function(node()) as node()*) 152 | as map:map? 153 | { 154 | let $function-key as xs:string := mem-op:function-key($transform-function) 155 | return 156 | map:new(( 157 | mem-op-fun:queue( 158 | $transaction-map, 159 | $nodes-to-change, 160 | text { $function-key }, 161 | "transform" 162 | ), 163 | map:entry( 164 | $function-key, 165 | $transform-function 166 | ) 167 | )) 168 | }; 169 | 170 | (: Select the root to return after transaction :) 171 | declare function mem-op-fun:copy($node-to-copy as node()) 172 | as map:map 173 | { 174 | map:entry("copy", $node-to-copy) 175 | }; 176 | 177 | (: Execute transaction :) 178 | declare function mem-op-fun:execute($transaction-map as map:map) 179 | as node()* 180 | { 181 | if (exists(map:get($transaction-map, "nodes-to-modify"))) 182 | then 183 | mem-op:process( 184 | $transaction-map, 185 | (: Ensure nodes to modify are in document order by using union :) 186 | map:get($transaction-map, "nodes-to-modify") | (), 187 | map:get($transaction-map, "modifier-nodes"), 188 | map:get($transaction-map, "operation"), 189 | map:get($transaction-map, "copy") 190 | ) 191 | else 192 | validate lax { 193 | map:get($transaction-map, "copy") 194 | }, 195 | map:clear($transaction-map) 196 | }; 197 | 198 | (: Begin private functions! :) 199 | 200 | (: Queue actions for later execution :) 201 | declare %private 202 | function mem-op-fun:queue( 203 | $transaction-map as map:map, 204 | $nodes-to-modify as node()+, 205 | $modifier-nodes as node()*, 206 | $operation as xs:string?) 207 | as map:map 208 | { 209 | if (fn:exists($nodes-to-modify)) 210 | then 211 | (: Creates elements based off of generate-id (i.e., node is 12439f8e4a3, then we get back ) :) 212 | let $modified-node-ids as element()* := mem-op:id-wrapper($nodes-to-modify) (: This line uses function mapping :) 213 | return 214 | ( 215 | mem-op:all-nodes-from-same-doc($nodes-to-modify,map:get($transaction-map,"copy")), 216 | map:new(( 217 | $transaction-map, 218 | map:entry( 219 | "operation", 220 | ({ 221 | attribute operation { $operation }, 222 | $modified-node-ids 223 | }, 224 | (: Ensure operations are accummulated :) 225 | map:get($transaction-map, "operation")) 226 | ), 227 | map:entry( 228 | "nodes-to-modify", 229 | ($nodes-to-modify, 230 | (: Ensure nodes to modify are accummulated :) 231 | map:get($transaction-map, "nodes-to-modify")) 232 | ), 233 | map:entry( 234 | "modifier-nodes", 235 | ({ 236 | attribute mem-op:operation { $operation }, 237 | $modifier-nodes[self::attribute()], 238 | $modified-node-ids, 239 | $modifier-nodes[not(self::attribute())] 240 | }, 241 | (: Ensure nodes to modifier nodes are accummulated :) 242 | map:get($transaction-map, "modifier-nodes")) 243 | ) 244 | )) 245 | ) 246 | else 247 | $transaction-map 248 | }; 249 | -------------------------------------------------------------------------------- /memory-operations-pure-xquery.xqy: -------------------------------------------------------------------------------- 1 | xquery version "3.0"; 2 | 3 | (:~ 4 | Copyright (c) 2016 Ryan Dew 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | @author Ryan Dew (ryan.j.dew@gmail.com) 19 | @version 1.0.7 20 | @description This is a module with function changing XML in memory by creating subtrees using the ancestor, preceding-sibling, and following-sibling axes 21 | and intersect/except expressions. This version works with eXistDB and BaseX 22 | ~:) 23 | module namespace mem-op="http://maxdewpoint.blogspot.com/memory-operations"; 24 | import module namespace node-op="http://maxdewpoint.blogspot.com/node-operations" at "node-operations.xqy"; 25 | declare default function namespace "http://www.w3.org/2005/xpath-functions"; 26 | 27 | (: Queue insert a child into the node :) 28 | declare function mem-op:insert-child( 29 | $transaction-map as map(*), 30 | $parent-node as element()*, 31 | $new-nodes as node()*) 32 | as map(*)? 33 | { 34 | mem-op:queue( 35 | $transaction-map, $parent-node, $new-nodes, "insert-child") 36 | }; 37 | 38 | (: Queue insert as first child into the node :) 39 | declare function mem-op:insert-child-first( 40 | $transaction-map as map(*), 41 | $parent-node as element()*, 42 | $new-nodes as node()*) 43 | as map(*)? 44 | { 45 | mem-op:queue( 46 | $transaction-map, 47 | $parent-node, 48 | $new-nodes, 49 | "insert-child-first") 50 | }; 51 | 52 | (: Queue insert a sibling before the node :) 53 | declare function mem-op:insert-before( 54 | $transaction-map as map(*), 55 | $sibling as node()*, 56 | $new-nodes as node()*) 57 | as map(*)? 58 | { 59 | mem-op:queue( 60 | $transaction-map, $sibling, $new-nodes, "insert-before") 61 | }; 62 | 63 | (: Queue insert a sibling after the node :) 64 | declare function mem-op:insert-after( 65 | $transaction-map as map(*), 66 | $sibling as node()*, 67 | $new-nodes as node()*) 68 | as map(*)? 69 | { 70 | mem-op:queue( 71 | $transaction-map, $sibling, $new-nodes, "insert-after") 72 | }; 73 | 74 | (: Queue replace of the node :) 75 | declare function mem-op:replace( 76 | $transaction-map as map(*), 77 | $replace-nodes as node()*, 78 | $new-nodes as node()*) 79 | as map(*)? 80 | { 81 | mem-op:queue( 82 | $transaction-map, 83 | $replace-nodes except $replace-nodes/descendant::node(), 84 | $new-nodes, 85 | "replace") 86 | }; 87 | 88 | (: Queue delete the node :) 89 | declare function mem-op:delete( 90 | $transaction-map as map(*), 91 | $delete-nodes as node()*) 92 | as map(*)? 93 | { 94 | mem-op:queue( 95 | $transaction-map, 96 | $delete-nodes except $delete-nodes/descendant::node(), 97 | (), 98 | "replace") 99 | }; 100 | 101 | (: Queue renaming of node :) 102 | declare function mem-op:rename( 103 | $transaction-map as map(*), 104 | $nodes-to-rename as node()*, 105 | $new-name as xs:QName) 106 | as map(*)? 107 | { 108 | mem-op:queue( 109 | $transaction-map, 110 | $nodes-to-rename, 111 | element { $new-name } { }, 112 | "rename") 113 | }; 114 | 115 | (: Queue replacement of a value of an element or attribute :) 116 | declare function mem-op:replace-value( 117 | $transaction-map as map(*), 118 | $nodes-to-change as node()*, 119 | $value as xs:anyAtomicType?) 120 | as map(*)? 121 | { 122 | mem-op:queue( 123 | $transaction-map, 124 | $nodes-to-change, 125 | text { $value }, 126 | "replace-value") 127 | }; 128 | 129 | (: Queue replacement of contents of an element :) 130 | declare function mem-op:replace-contents( 131 | $transaction-map as map(*), 132 | $nodes-to-change as node()*, 133 | $contents as node()*) 134 | as map(*)? 135 | { 136 | mem-op:queue( 137 | $transaction-map, 138 | $nodes-to-change, 139 | $contents, 140 | "replace-value") 141 | }; 142 | 143 | (: Queues the replacement of the node with the result of the passed function :) 144 | declare function mem-op:transform( 145 | $transaction-map as map(*), 146 | $nodes-to-change as node()*, 147 | $transform-function as function(node()) as node()*) 148 | as map(*)? 149 | { 150 | let $function-key as xs:string := mem-op:function-key($transform-function) 151 | return 152 | map:new(( 153 | mem-op:queue( 154 | $transaction-map, 155 | $nodes-to-change, 156 | text { $function-key }, 157 | "transform" 158 | ), 159 | map:entry( 160 | $function-key, 161 | $transform-function 162 | ) 163 | )) 164 | }; 165 | 166 | (: Select the root to return after transaction :) 167 | declare function mem-op:copy($node-to-copy as node()) 168 | as map(*) 169 | { 170 | map:entry("copy", $node-to-copy) 171 | }; 172 | 173 | (: Execute transaction :) 174 | declare function mem-op:execute($transaction-map as map(*)) 175 | as node()* 176 | { 177 | if (exists(map:get($transaction-map, "nodes-to-modify"))) 178 | then 179 | mem-op:process( 180 | $transaction-map, 181 | (: Ensure nodes to modify are in document order by using union :) 182 | map:get($transaction-map, "nodes-to-modify") | (), 183 | map:get($transaction-map, "modifier-nodes"), 184 | map:get($transaction-map, "operation"), 185 | map:get($transaction-map, "copy") 186 | ) 187 | else 188 | mem-op:safe-copy(map:get($transaction-map, "copy")) 189 | }; 190 | 191 | (: Begin private functions! :) 192 | 193 | (: Queue actions for later execution :) 194 | declare %private 195 | function mem-op:queue( 196 | $transaction-map as map(*), 197 | $nodes-to-modify as node()+, 198 | $modifier-nodes as node()*, 199 | $operation as xs:string?) 200 | as map(*) 201 | { 202 | if (fn:exists($nodes-to-modify)) 203 | then 204 | (: Creates elements based off of generate-id (i.e., node is 12439f8e4a3, then we get back ) :) 205 | let $modified-node-ids as element()* := $nodes-to-modify ! mem-op:id-wrapper(.) 206 | return 207 | ( 208 | mem-op:all-nodes-from-same-doc($nodes-to-modify,map:get($transaction-map,"copy")), 209 | map:new(( 210 | $transaction-map, 211 | map:entry( 212 | "operation", 213 | ({ 214 | attribute operation { $operation }, 215 | $modified-node-ids 216 | }, 217 | (: Ensure operations are accummulated :) 218 | map:get($transaction-map, "operation")) 219 | ), 220 | map:entry( 221 | "nodes-to-modify", 222 | ($nodes-to-modify, 223 | (: Ensure nodes to modify are accummulated :) 224 | map:get($transaction-map, "nodes-to-modify")) 225 | ), 226 | map:entry( 227 | "modifier-nodes", 228 | ({ 229 | attribute mem-op:operation { $operation }, 230 | $modifier-nodes[. instance of attribute()], 231 | $modified-node-ids, 232 | $modifier-nodes[not(. instance of attribute())] 233 | }, 234 | (: Ensure nodes to modifier nodes are accummulated :) 235 | map:get($transaction-map, "modifier-nodes")) 236 | ) 237 | )) 238 | ) 239 | else 240 | $transaction-map 241 | }; 242 | 243 | (: Begin private functions! :) 244 | 245 | declare function mem-op:all-nodes-from-same-doc($nodes as node()*,$parent-node as node()) as empty-sequence() { 246 | (: NOTE: must use every in satisfies to account for multiple outermost nodes :) 247 | if (every $n in node-op:outermost(($parent-node,$nodes)) satisfies $n is $parent-node) 248 | then () 249 | else 250 | error(xs:QName("mem-op:MIXEDSOURCES"), "The nodes to change are coming from multiple sources",$nodes) 251 | }; 252 | 253 | (: The process functions handle the core logic for handling forked paths that 254 | need to be altered :) 255 | declare 256 | function mem-op:process( 257 | $nodes-to-modify as node()+, 258 | $new-nodes as node()*, 259 | $operation) 260 | as node()* 261 | { 262 | mem-op:all-nodes-from-same-doc($nodes-to-modify,root($nodes-to-modify[1])), 263 | mem-op:safe-copy(mem-op:process((), $nodes-to-modify, $new-nodes, $operation, ())) 264 | }; 265 | 266 | declare 267 | function mem-op:process( 268 | $transaction-map as map(*)?, 269 | $nodes-to-modify as node()+, 270 | $new-nodes as node()*, 271 | $operation, 272 | $root-node as node()?) 273 | as node()* 274 | { 275 | mem-op:process( 276 | $transaction-map, 277 | $nodes-to-modify, 278 | node-op:outermost($nodes-to-modify), 279 | $new-nodes, 280 | $operation, 281 | $root-node 282 | ) 283 | }; 284 | 285 | declare 286 | function mem-op:process( 287 | $transaction-map as map(*)?, 288 | $nodes-to-modify as node()+, 289 | $outermost-nodes-to-modify as node()+, 290 | $new-nodes as node()*, 291 | $operation, 292 | $root-node as node()?) 293 | as node()* 294 | { 295 | mem-op:process( 296 | $transaction-map, 297 | $nodes-to-modify, 298 | $outermost-nodes-to-modify, 299 | $nodes-to-modify, 300 | $new-nodes, 301 | $operation, 302 | mem-op:find-ancestor-intersect($outermost-nodes-to-modify, 1, ()) 303 | except 304 | (if (exists($root-node)) 305 | then $root-node/ancestor::node() 306 | else ()) 307 | ) 308 | }; 309 | 310 | declare %private 311 | function mem-op:process( 312 | $transaction-map as map(*)?, 313 | $nodes-to-modify as node()+, 314 | $outermost-nodes-to-modify as node()+, 315 | $all-nodes-to-modify as node()*, 316 | $new-nodes as node()*, 317 | $operation, 318 | $common-ancestors as node()*) 319 | as node()* 320 | { 321 | mem-op:process( 322 | $transaction-map, 323 | $nodes-to-modify, 324 | $outermost-nodes-to-modify, 325 | $all-nodes-to-modify, 326 | $new-nodes, 327 | $operation, 328 | $common-ancestors, 329 | (: get the first common parent of all the items to modify 330 | (First going up the tree. Last in document order.) :) 331 | $common-ancestors[last()]) 332 | }; 333 | 334 | declare %private 335 | function mem-op:process( 336 | $transaction-map as map(*)?, 337 | $nodes-to-modify as node()+, 338 | $outermost-nodes-to-modify as node()+, 339 | $all-nodes-to-modify as node()*, 340 | $new-nodes as node()*, 341 | $operation, 342 | $common-ancestors as node()*, 343 | $common-parent as node()?) 344 | as node()* 345 | { 346 | mem-op:process( 347 | $transaction-map, 348 | $nodes-to-modify, 349 | $outermost-nodes-to-modify, 350 | $all-nodes-to-modify, 351 | $new-nodes, 352 | $operation, 353 | $common-ancestors, 354 | $common-parent, 355 | ($common-parent/node(), $common-parent/@*) intersect 356 | $outermost-nodes-to-modify/ancestor-or-self::node()) 357 | }; 358 | 359 | declare %private 360 | function mem-op:process( 361 | $transaction-map as map(*)?, 362 | $nodes-to-modify as node()+, 363 | $outermost-nodes-to-modify as node()+, 364 | $all-nodes-to-modify as node()*, 365 | $new-nodes as node()*, 366 | $operation, 367 | $common-ancestors as node()*, 368 | $common-parent as node()?, 369 | $merging-nodes as node()*) 370 | as node()* 371 | { 372 | mem-op:process( 373 | $transaction-map, 374 | $nodes-to-modify, 375 | $outermost-nodes-to-modify, 376 | $all-nodes-to-modify, 377 | $new-nodes, 378 | $operation, 379 | $common-ancestors, 380 | $common-parent, 381 | $merging-nodes, 382 | (: create new XML trees for all the unique paths to 383 | the items to modify :) 384 | { 385 | if (exists($common-parent)) 386 | then ( 387 | $merging-nodes ! 388 | mem-op:build-subtree( 389 | $transaction-map, 390 | ., 391 | $nodes-to-modify, 392 | $new-nodes, 393 | $operation, 394 | (: get all of the ancestors :) 395 | $common-parent/ancestor-or-self::node()) 396 | ) else ( 397 | let $reference-node as node()? := $nodes-to-modify[1]/.. 398 | for $n in (if (exists($reference-node)) 399 | then ($reference-node/@*,$reference-node/node()) intersect $nodes-to-modify/ancestor-or-self::node() 400 | else $outermost-nodes-to-modify) 401 | return 402 | mem-op:build-subtree( 403 | $transaction-map, 404 | $n, 405 | $nodes-to-modify, 406 | $new-nodes, 407 | $operation, 408 | (: get all of the ancestors :) 409 | $nodes-to-modify[1]/ancestor::node()) 410 | ) 411 | }) 412 | }; 413 | 414 | declare %private 415 | function mem-op:process( 416 | $transaction-map as map(*)?, 417 | $nodes-to-modify as node()+, 418 | $outermost-nodes-to-modify as node()+, 419 | $all-nodes-to-modify as node()*, 420 | $new-nodes as node()*, 421 | $operation, 422 | $common-ancestors as node()*, 423 | $common-parent as node()?, 424 | $merging-nodes as node()*, 425 | $trees as element(mem-op:trees)) 426 | as node()* 427 | { 428 | if (exists($common-parent)) 429 | then 430 | mem-op:process-ancestors( 431 | $transaction-map, 432 | (: Ancestors of the common parent which will be used to walk up the XML tree. :) 433 | reverse($common-ancestors except $common-parent), 434 | $common-parent, 435 | $operation, 436 | $all-nodes-to-modify, 437 | (: Nodes to modify that are part of the common ancestors :) 438 | $all-nodes-to-modify intersect $common-ancestors, 439 | $new-nodes, 440 | mem-op:reconstruct-node-with-additional-modifications( 441 | $transaction-map, 442 | $common-parent, 443 | mem-op:place-trees( 444 | (: Reduce iterations by using outermost nodes :) 445 | $outermost-nodes-to-modify except $common-parent, 446 | (: Pass attributes and child nodes excluding ancestors of nodes to modify. :) 447 | ($common-parent/node(), $common-parent/@*) 448 | except 449 | $merging-nodes, 450 | (: New sub trees to put in place. :) 451 | $trees), 452 | $new-nodes, 453 | (), 454 | $all-nodes-to-modify, 455 | $operation, 456 | fn:false() 457 | ) 458 | ) 459 | else 460 | mem-op:place-trees( 461 | (: Reduce iterations by using outermost nodes :) 462 | $outermost-nodes-to-modify, 463 | (: Pass attributes and child nodes excluding ancestors of nodes to modify. :) 464 | let $copy-node as node()? := 465 | if (fn:exists($transaction-map)) 466 | then map:get($transaction-map,'copy') 467 | else () 468 | let $reference-node as node()? := $nodes-to-modify[1]/.. 469 | return 470 | (if (fn:empty($transaction-map) or ($copy-node << $reference-node or $reference-node is $copy-node)) 471 | then($reference-node/(@*|node())) 472 | else ()) 473 | except 474 | $nodes-to-modify/ancestor-or-self::node(), 475 | (: New sub trees to put in place. :) 476 | $trees) 477 | }; 478 | 479 | declare %private 480 | function mem-op:build-subtree( 481 | $transaction-map as map(*)?, 482 | $mod-node as node(), 483 | $nodes-to-modify as node()*, 484 | $new-nodes as node()*, 485 | $operations, 486 | $all-ancestors as node()*) 487 | as node()* 488 | { 489 | mem-op:subtree( 490 | $transaction-map, 491 | $mod-node, 492 | $nodes-to-modify intersect 493 | ($mod-node/descendant-or-self::node(), 494 | $mod-node/descendant-or-self::node()/@*), 495 | $new-nodes, 496 | $operations, 497 | $all-ancestors) 498 | }; 499 | 500 | declare %private 501 | function mem-op:subtree( 502 | $transaction-map as map(*)?, 503 | $mod-node as node(), 504 | $nodes-to-modify as node()*, 505 | $new-nodes as node()*, 506 | $operations, 507 | $all-ancestors as node()*) 508 | as node()* 509 | { 510 | let $mod-node-id-qn := mem-op:generate-id-qn($nodes-to-modify[1]) 511 | let $descendant-nodes-to-mod := $nodes-to-modify except $mod-node 512 | return 513 | mem-op:wrap-subtree( 514 | $mod-node-id-qn, 515 | if (empty($descendant-nodes-to-mod)) 516 | then 517 | mem-op:process-subtree( 518 | $transaction-map, 519 | $mod-node/ancestor::node() except $all-ancestors, 520 | $mod-node, 521 | $mod-node-id-qn, 522 | $new-nodes, 523 | $operations, 524 | ()) 525 | else 526 | let $outermost-nodes-to-mod as node()+ := node-op:outermost($descendant-nodes-to-mod) 527 | return 528 | mem-op:process( 529 | $transaction-map, 530 | $descendant-nodes-to-mod, 531 | $outermost-nodes-to-mod, 532 | $nodes-to-modify, 533 | $new-nodes, 534 | $operations, 535 | (: find the ancestors that all nodes to modify have in common :) 536 | mem-op:find-ancestor-intersect( 537 | $outermost-nodes-to-mod, 538 | 1, 539 | () 540 | ) 541 | except 542 | $all-ancestors) 543 | ) 544 | }; 545 | 546 | declare %private 547 | function mem-op:wrap-subtree( 548 | $mod-node-id-qn as xs:QName, 549 | $results as node()* 550 | )as node()* 551 | { 552 | if ($results) 553 | then 554 | element { $mod-node-id-qn } { 555 | $results 556 | } 557 | else () 558 | }; 559 | (: Creates a new subtree with the changes made based off of the operation. :) 560 | declare %private 561 | function mem-op:process-subtree( 562 | $transaction-map as map(*)?, 563 | $ancestors as node()*, 564 | $node-to-modify as node(), 565 | $node-to-modify-id-qn as xs:QName, 566 | $new-node as node()*, 567 | $operations, 568 | $ancestor-nodes-to-modify as node()*) 569 | as node()* 570 | { 571 | mem-op:process-ancestors( 572 | $transaction-map, 573 | reverse($ancestors), 574 | (), 575 | $operations, 576 | $node-to-modify, 577 | $ancestor-nodes-to-modify, 578 | $new-node, 579 | mem-op:build-new-xml( 580 | $transaction-map, 581 | $node-to-modify, 582 | typeswitch ($operations) 583 | case xs:string return $operations 584 | default return 585 | $operations[*[node-name(.) eq $node-to-modify-id-qn]]/ 586 | @operation, 587 | typeswitch ($new-node) 588 | case element(mem-op:modifier-nodes)* return $new-node[*[node-name(.) eq $node-to-modify-id-qn]] 589 | default return 590 | { 591 | attribute mem-op:operation { $operations }, 592 | $new-node 593 | })) 594 | }; 595 | 596 | (: Find all of the common ancestors of a given set of nodes :) 597 | declare %private 598 | function mem-op:find-ancestor-intersect( 599 | $items as node()*, 600 | $current-position as xs:integer, 601 | $ancestor-intersect as node()*) 602 | as node()* 603 | { 604 | if (empty($items)) 605 | then $ancestor-intersect 606 | else if ($current-position gt 1) 607 | (: if ancestor-intersect already exists intersect with the current item's ancestors :) 608 | then 609 | if (empty($ancestor-intersect)) 610 | (: short circuit if intersect is empty :) 611 | then () 612 | else 613 | $ancestor-intersect intersect head($items)/ancestor::node() intersect $items[fn:last()]/ancestor::node() 614 | (: otherwise just use the current item's ancestors :) 615 | else 616 | mem-op:find-ancestor-intersect( 617 | tail($items), 618 | $current-position + 1, 619 | head($items)/ancestor::node()) 620 | }; 621 | 622 | (: Place newly created trees in proper order :) 623 | declare %private 624 | function mem-op:place-trees( 625 | $nodes-to-modify as node()*, 626 | $merging-nodes as node()*, 627 | $trees as element(mem-op:trees)?) 628 | as node()* 629 | { 630 | if (empty($nodes-to-modify) or empty($trees[*])) 631 | then $merging-nodes 632 | else ( 633 | let $tree-ids := $trees/* ! substring-after(local-name(.),'_') 634 | let $count-of-trees := count($tree-ids) 635 | for $tree at $pos in $trees/* 636 | let $previous-tree-pos := $pos - 1 637 | let $previous-tree-id := $tree-ids[position() eq $previous-tree-pos] 638 | let $current-tree-id := $tree-ids[position() eq $pos] 639 | let $previous-node-to-modify := 640 | if (exists($previous-tree-id)) 641 | then $nodes-to-modify[generate-id(.) eq $previous-tree-id][1] 642 | else () 643 | let $node-to-modify := $nodes-to-modify[generate-id(.) eq $current-tree-id][1] 644 | let $nodes-inbetween := node-op:inbetween($merging-nodes, $previous-node-to-modify, $node-to-modify) 645 | return 646 | ( 647 | $nodes-inbetween, 648 | $tree/(attribute::node()|child::node()), 649 | if ($pos eq $count-of-trees) 650 | then 651 | node-op:inbetween($merging-nodes, $node-to-modify, ()) 652 | else () 653 | ) 654 | ) 655 | }; 656 | 657 | (: Go up the tree to build new XML using tail recursion. This is used when there are no side 658 | steps to merge in, only a direct path up the tree. $ancestors is expected to be passed in 659 | REVERSE document order. :) 660 | declare %private 661 | function mem-op:process-ancestors( 662 | $transaction-map as map(*)?, 663 | $ancestors as node()*, 664 | $last-ancestor as node()?, 665 | $operations, 666 | $nodes-to-modify as node()*, 667 | $ancestor-nodes-to-modify as node()*, 668 | $new-node as node()*, 669 | $base as node()*) 670 | as node()* 671 | { 672 | if (exists($ancestors)) 673 | then 674 | mem-op:process-ancestors( 675 | $transaction-map, 676 | tail($ancestors), 677 | head($ancestors), 678 | $operations, 679 | $nodes-to-modify, 680 | $ancestor-nodes-to-modify, 681 | $new-node, 682 | mem-op:reconstruct-node-with-additional-modifications( 683 | $transaction-map, 684 | head($ancestors), 685 | ($last-ancestor/preceding-sibling::node(),$base,$last-ancestor/following-sibling::node()), 686 | $new-node, 687 | $nodes-to-modify, 688 | $ancestor-nodes-to-modify, 689 | $operations, 690 | fn:true() 691 | ) 692 | ) 693 | else 694 | $base 695 | }; 696 | 697 | (: Generic logic for rebuilding document/element nodes and passing in for :) 698 | declare %private 699 | function mem-op:reconstruct-node-with-additional-modifications( 700 | $transaction-map as map(*)?, 701 | $node as node(), 702 | $ordered-content as node()*, 703 | $new-node as node()*, 704 | $nodes-to-modify as node()*, 705 | $ancestor-nodes-to-modify as node()*, 706 | $operations, 707 | $carry-over-attributes as xs:boolean) 708 | { 709 | if (some $n in $ancestor-nodes-to-modify 710 | satisfies $n is $node) 711 | then 712 | mem-op:process-subtree( 713 | $transaction-map, 714 | (), 715 | mem-op:reconstruct-node($node,$ordered-content,$nodes-to-modify,$carry-over-attributes), 716 | mem-op:generate-id-qn($node), 717 | $new-node, 718 | $operations, 719 | () 720 | ) 721 | else 722 | mem-op:reconstruct-node($node,$ordered-content,$nodes-to-modify,$carry-over-attributes) 723 | }; 724 | 725 | (: Generic logic for rebuilding document/element nodes :) 726 | declare %private 727 | function mem-op:reconstruct-node( 728 | $node as node(), 729 | $ordered-content as node()*, 730 | $nodes-to-modify as node()*, 731 | $carry-over-attributes as xs:boolean) 732 | { 733 | typeswitch ($node) 734 | case element() return 735 | element { node-name($node) } { 736 | if ($carry-over-attributes) 737 | then $node/@* except $nodes-to-modify 738 | else (), 739 | $ordered-content 740 | } 741 | case document-node() return 742 | document { 743 | $ordered-content 744 | } 745 | default return () 746 | }; 747 | 748 | (: Generate an id unique to a node in memory. Right now using fn:generate-id. :) 749 | declare 750 | function mem-op:id-wrapper($node as node()) 751 | { 752 | element {mem-op:generate-id-qn($node)} {()} 753 | }; 754 | 755 | (: Generate QName from node :) 756 | declare %private 757 | function mem-op:generate-id-qn($node as node()) 758 | { 759 | QName( 760 | "http://maxdewpoint.blogspot.com/memory-operations", 761 | concat("_", mem-op:generate-id($node))) 762 | }; 763 | 764 | (: Generate an id unique to a node in memory. Right now using fn:generate-id. :) 765 | declare %private 766 | function mem-op:generate-id($node as node()) 767 | { 768 | generate-id($node) 769 | }; 770 | 771 | (: Create a key to uniquely identify a function :) 772 | declare 773 | function mem-op:function-key($function as function(*)) 774 | { 775 | let $qname := (function-name($function), 776 | xs:QName("_" || generate-id(element rand-el {})))[1] 777 | return 778 | namespace-uri-from-QName($qname) || 779 | ":" || 780 | local-name-from-QName($qname) || 781 | "#" || 782 | string(function-arity($function)) 783 | }; 784 | 785 | (: This is where the transformations to the XML take place and this module can be extended. :) 786 | declare %private 787 | function mem-op:build-new-xml( 788 | $transaction-map as map(*)?, 789 | $node as node(), 790 | $operations as xs:string*, 791 | $modifier-nodes as element(mem-op:modifier-nodes)*) 792 | { 793 | mem-op:build-new-xml( 794 | $transaction-map, 795 | $node, 796 | mem-op:weighed-operations(distinct-values($operations)), 797 | $modifier-nodes, 798 | () 799 | ) 800 | }; 801 | 802 | (: This function contains the logic for each of the operations is is going to be the most 803 | likely place extensions will be made. :) 804 | declare %private 805 | function mem-op:build-new-xml( 806 | $transaction-map as map(*)?, 807 | $nodes as node()*, 808 | $operations as xs:string*, 809 | $modifier-nodes as element(mem-op:modifier-nodes)*, 810 | $modifying-node as node()?) 811 | { 812 | if (empty($operations) or empty($nodes)) 813 | then $nodes 814 | else 815 | let $node as node()? := if (count($nodes) eq 1) then $nodes else $modifying-node 816 | let $pivot-pos as xs:integer? := $nodes/(if (. is $node) then position() else ()) 817 | let $operation as xs:string := head($operations) 818 | let $last-in-wins as xs:boolean := $operation = ('replace-value') 819 | let $reverse-mod-nodes as xs:boolean := $operation = ('insert-child') 820 | let $mod-nodes as node()* := 821 | let $modifier-nodes := 822 | if ($last-in-wins) 823 | then ($modifier-nodes[@mem-op:operation eq $operation])[1] 824 | else if ($reverse-mod-nodes) 825 | then reverse($modifier-nodes[@mem-op:operation eq $operation]) 826 | else $modifier-nodes[@mem-op:operation eq $operation] 827 | return 828 | ($modifier-nodes ! @node()[empty(self::attribute(mem-op:operation))], 829 | $modifier-nodes ! node()[empty(self::mem-op:*)]) 830 | let $new-nodes := 831 | switch ($operation) 832 | case "replace" return $mod-nodes 833 | case "insert-child" return 834 | element { node-name($node) } { 835 | let $attributes-to-insert := $mod-nodes[self::attribute()], 836 | $attributes-to-insert-qns := $attributes-to-insert/node-name(.) 837 | return 838 | ($node/@*[not(node-name(.) = $attributes-to-insert-qns)], 839 | $attributes-to-insert, 840 | $node/node(), 841 | $mod-nodes[exists(. except $attributes-to-insert)]) 842 | } 843 | case "insert-child-first" return 844 | element { node-name($node) } { 845 | let $attributes-to-insert := $mod-nodes[self::attribute()], 846 | $attributes-to-insert-qns := $attributes-to-insert/node-name(.) 847 | return 848 | ($attributes-to-insert, 849 | $node/@*[not(node-name(.) = $attributes-to-insert-qns)], 850 | $mod-nodes[exists(. except $attributes-to-insert)], 851 | $node/node()) 852 | } 853 | case "insert-after" return ($node, $mod-nodes) 854 | case "insert-before" return ($mod-nodes, $node) 855 | case "rename" return 856 | element { node-name(($mod-nodes[self::element()])[1]) } { $node/@*, $node/node() } 857 | case "replace-value" return 858 | typeswitch ($node) 859 | case attribute() return attribute { node-name($node) } { $mod-nodes } 860 | case element() return 861 | element { node-name($node) } { $node/@*, $mod-nodes } 862 | case processing-instruction() return 863 | processing-instruction { 864 | node-name($node) 865 | } { 866 | $mod-nodes 867 | } 868 | case comment() return 869 | comment { 870 | $mod-nodes 871 | } 872 | case text() return $mod-nodes 873 | default return () 874 | case "transform" return 875 | map:get( 876 | $transaction-map, 877 | string($mod-nodes))($node) 878 | default return () 879 | let $n-nodes := 880 | unordered {( 881 | $nodes[exists($pivot-pos) and position() lt $pivot-pos] except $node, 882 | $new-nodes, 883 | $nodes[exists($pivot-pos) and position() gt $pivot-pos] except $node 884 | )} 885 | return 886 | mem-op:build-new-xml( 887 | $transaction-map, 888 | $n-nodes, 889 | tail($operations), 890 | $modifier-nodes, 891 | if ($operation = ('insert-after','insert-before')) 892 | then $node 893 | else $new-nodes[1] 894 | ) 895 | }; 896 | 897 | (: Order the operations in such a way that the least amount of stomping on eachother occurs :) 898 | declare %private 899 | function mem-op:weighed-operations( 900 | $operations as xs:string*) as xs:string* 901 | { 902 | $operations[. eq "replace"], 903 | $operations[. eq "replace-value"], 904 | $operations[. eq "insert-child"], 905 | $operations[. eq "insert-child-first"], 906 | $operations[not(. = ("replace","replace-value","insert-child","insert-child-first","transform"))], 907 | $operations[. eq "transform"] 908 | }; 909 | 910 | declare %private 911 | function mem-op:safe-copy( 912 | $node as node()) 913 | as node()? { 914 | mem-op:reconstruct-node($node,$node/*,(),fn:true()) 915 | }; 916 | -------------------------------------------------------------------------------- /memory-operations.xqy: -------------------------------------------------------------------------------- 1 | xquery version "1.0-ml"; 2 | (:~ 3 | Copyright (c) 2013 Ryan Dew 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | @author Ryan Dew (ryan.j.dew@gmail.com) 18 | @version 1.0.7 19 | @description This is a module with function changing XML in memory by creating subtrees using the ancestor, preceding-sibling, and following-sibling axes 20 | and intersect/except expressions. Requires MarkLogic 6+. 21 | ~:) 22 | module namespace mem-op="http://maxdewpoint.blogspot.com/memory-operations"; 23 | import module namespace node-op="http://maxdewpoint.blogspot.com/node-operations" at "node-operations.xqy"; 24 | declare default function namespace "http://www.w3.org/2005/xpath-functions"; 25 | declare namespace xdmp="http://marklogic.com/xdmp"; 26 | declare namespace map="http://marklogic.com/xdmp/map"; 27 | declare option xdmp:mapping "true"; 28 | declare option xdmp:copy-on-validate "true"; 29 | declare %private variable $queue as map:map := map:map(); 30 | declare %private variable $transform-functions as map:map := map:map(); 31 | 32 | (: Insert a child into the node :) 33 | declare function mem-op:insert-child( 34 | $parent-node as element()+, 35 | $new-nodes as node()*) 36 | as node()? 37 | { 38 | mem-op:process($parent-node, $new-nodes, "insert-child") 39 | }; 40 | 41 | (: Queue insert a child into the node :) 42 | declare function mem-op:insert-child( 43 | $transaction-id as xs:string, 44 | $parent-node as element()*, 45 | $new-nodes as node()*) 46 | as empty-sequence() 47 | { 48 | mem-op:queue( 49 | $transaction-id, $parent-node, $new-nodes, "insert-child") 50 | }; 51 | 52 | (: Insert as first child into the node :) 53 | declare function mem-op:insert-child-first( 54 | $parent-node as element()+, 55 | $new-nodes as node()*) 56 | as node()? 57 | { 58 | mem-op:process( 59 | $parent-node, $new-nodes, "insert-child-first") 60 | }; 61 | 62 | (: Queue insert as first child into the node :) 63 | declare function mem-op:insert-child-first( 64 | $transaction-id as xs:string, 65 | $parent-node as element()*, 66 | $new-nodes as node()*) 67 | as empty-sequence() 68 | { 69 | mem-op:queue( 70 | $transaction-id, 71 | $parent-node, 72 | $new-nodes, 73 | "insert-child-first") 74 | }; 75 | 76 | (: Insert a sibling before the node :) 77 | declare function mem-op:insert-before( 78 | $sibling as node()+, 79 | $new-nodes as node()*) 80 | as node()? 81 | { 82 | mem-op:process($sibling, $new-nodes, "insert-before") 83 | }; 84 | 85 | (: Queue insert a sibling before the node :) 86 | declare function mem-op:insert-before( 87 | $transaction-id as xs:string, 88 | $sibling as node()*, 89 | $new-nodes as node()*) 90 | as empty-sequence() 91 | { 92 | mem-op:queue( 93 | $transaction-id, $sibling, $new-nodes, "insert-before") 94 | }; 95 | 96 | (: Insert a sibling after the node :) 97 | declare function mem-op:insert-after( 98 | $sibling as node()+, 99 | $new-nodes as node()*) 100 | as node()? 101 | { 102 | mem-op:process($sibling, $new-nodes, "insert-after") 103 | }; 104 | 105 | (: Queue insert a sibling after the node :) 106 | declare function mem-op:insert-after( 107 | $transaction-id as xs:string, 108 | $sibling as node()*, 109 | $new-nodes as node()*) 110 | as empty-sequence() 111 | { 112 | mem-op:queue( 113 | $transaction-id, $sibling, $new-nodes, "insert-after") 114 | }; 115 | 116 | (: Replace the node :) 117 | declare function mem-op:replace( 118 | $replace-nodes as node()+, 119 | $new-nodes as node()*) 120 | as node()? 121 | { 122 | mem-op:process( 123 | $replace-nodes except $replace-nodes/descendant::node(), 124 | $new-nodes, 125 | "replace") 126 | }; 127 | 128 | (: Queue replace of the node :) 129 | declare function mem-op:replace( 130 | $transaction-id as xs:string, 131 | $replace-nodes as node()*, 132 | $new-nodes as node()*) 133 | as empty-sequence() 134 | { 135 | mem-op:queue( 136 | $transaction-id, 137 | $replace-nodes except $replace-nodes/descendant::node(), 138 | $new-nodes, 139 | "replace") 140 | }; 141 | 142 | (: Delete the node :) 143 | declare function mem-op:delete($delete-nodes as node()+) 144 | as node()? 145 | { 146 | mem-op:process( 147 | $delete-nodes except $delete-nodes/descendant::node(), 148 | (), 149 | "replace") 150 | }; 151 | 152 | (: Queue delete the node :) 153 | declare function mem-op:delete( 154 | $transaction-id as xs:string, 155 | $delete-nodes as node()*) 156 | as empty-sequence() 157 | { 158 | mem-op:queue( 159 | $transaction-id, 160 | $delete-nodes except $delete-nodes/descendant::node(), 161 | (), 162 | "replace") 163 | }; 164 | 165 | (: Rename a node :) 166 | declare function mem-op:rename( 167 | $nodes-to-rename as node()+, 168 | $new-name as xs:QName) 169 | as node()? 170 | { 171 | mem-op:process( 172 | $nodes-to-rename, element { $new-name } { }, "rename") 173 | }; 174 | 175 | (: Queue renaming of node :) 176 | declare function mem-op:rename( 177 | $transaction-id as xs:string, 178 | $nodes-to-rename as node()*, 179 | $new-name as xs:QName) 180 | as empty-sequence() 181 | { 182 | mem-op:queue( 183 | $transaction-id, 184 | $nodes-to-rename, 185 | element { $new-name } { }, 186 | "rename") 187 | }; 188 | 189 | (: Replaces a value of an element or attribute :) 190 | declare function mem-op:replace-value( 191 | $nodes-to-change as node()+, 192 | $value as xs:anyAtomicType?) 193 | as node()? 194 | { 195 | mem-op:process( 196 | $nodes-to-change, text { $value }, "replace-value") 197 | }; 198 | 199 | (: Queue replacement of a value of an element or attribute :) 200 | declare function mem-op:replace-value( 201 | $transaction-id as xs:string, 202 | $nodes-to-change as node()*, 203 | $value as xs:anyAtomicType?) 204 | as empty-sequence() 205 | { 206 | mem-op:queue( 207 | $transaction-id, 208 | $nodes-to-change, 209 | text { $value }, 210 | "replace-value") 211 | }; 212 | 213 | (: Replaces contents of an element :) 214 | declare function mem-op:replace-contents( 215 | $nodes-to-change as node()+, 216 | $contents as node()*) 217 | as node()? 218 | { 219 | mem-op:process( 220 | $nodes-to-change, $contents, "replace-value") 221 | }; 222 | 223 | (: Queue replacement of contents of an element :) 224 | declare function mem-op:replace-contents( 225 | $transaction-id as xs:string, 226 | $nodes-to-change as node()*, 227 | $contents as node()*) 228 | as empty-sequence() 229 | { 230 | mem-op:queue( 231 | $transaction-id, 232 | $nodes-to-change, 233 | $contents, 234 | "replace-value") 235 | }; 236 | 237 | (: Replaces with the result of the passed function :) 238 | declare function mem-op:transform( 239 | $nodes-to-change as node()+, 240 | $transform-function as function(node()) as node()*) 241 | as node()? 242 | { 243 | let $function-key as xs:string := mem-op:function-key($transform-function) 244 | return 245 | (map:put($transform-functions, $function-key, $transform-function), 246 | mem-op:process($nodes-to-change, text { $function-key }, "transform"), 247 | map:delete($transform-functions, $function-key)) 248 | }; 249 | 250 | (: Queues the replacement of the node with the result of the passed function :) 251 | declare function mem-op:transform( 252 | $transaction-id as xs:string, 253 | $nodes-to-change as node()*, 254 | $transform-function as function(node()) as node()*) 255 | as empty-sequence() 256 | { 257 | let $function-key as xs:string := mem-op:function-key($transform-function) 258 | return 259 | (map:put( 260 | map:get($queue, $transaction-id), $function-key, $transform-function), 261 | mem-op:queue( 262 | $transaction-id, 263 | $nodes-to-change, 264 | text { $function-key }, 265 | "transform")) 266 | }; 267 | 268 | (: Select the root to return after transaction :) 269 | declare function mem-op:copy($node-to-copy as node()) 270 | as xs:string 271 | { 272 | let $transaction-id as xs:string := concat(mem-op:generate-id($node-to-copy), current-dateTime()) 273 | let $transaction-map as map:map := map:map() 274 | let $_add-copy-to-transaction-map as empty-sequence() := map:put($transaction-map, "copy", $node-to-copy) 275 | let $_add-transaction-map-to-queue as empty-sequence() := map:put( 276 | $queue, 277 | $transaction-id, 278 | $transaction-map 279 | ) 280 | return $transaction-id 281 | }; 282 | 283 | (: Execute transaction :) 284 | declare function mem-op:execute($transaction-id as xs:string) 285 | as node()* 286 | { 287 | let $transaction-map as map:map := map:get($queue, $transaction-id) 288 | return 289 | ( 290 | if (exists(map:get($transaction-map, "nodes-to-modify"))) 291 | then 292 | mem-op:safe-copy(mem-op:process( 293 | $transaction-map, 294 | (: Ensure nodes to modify are in document order by using union :) 295 | map:get($transaction-map, "nodes-to-modify") | (), 296 | map:get($transaction-map, "modifier-nodes"), 297 | map:get($transaction-map, "operation"), 298 | map:get($transaction-map, "copy") 299 | )) 300 | else 301 | mem-op:safe-copy(map:get($transaction-map, "copy")) 302 | , 303 | map:clear($transaction-map) 304 | ), 305 | map:delete($queue, $transaction-id) 306 | }; 307 | 308 | (: Execute transaction :) 309 | declare function mem-op:execute-section($transaction-id as xs:string, $section-root as node()) 310 | as node()* 311 | { 312 | let $transaction-map as map:map := map:get($queue, $transaction-id), 313 | $nodes-to-mod as node()* := map:get($transaction-map, "nodes-to-modify") intersect ($section-root/descendant-or-self::node(),$section-root/descendant-or-self::*/@*) 314 | return 315 | ( 316 | if (exists($nodes-to-mod)) 317 | then 318 | (mem-op:safe-copy( 319 | mem-op:process( 320 | $transaction-map, 321 | $nodes-to-mod, 322 | map:get($transaction-map, "modifier-nodes"), 323 | map:get($transaction-map, "operation"), 324 | $section-root 325 | ) except $section-root/../(@*|node())) 326 | , 327 | map:put($transaction-map, "nodes-to-modify",map:get($transaction-map, "nodes-to-modify") except $nodes-to-mod) 328 | ) 329 | else 330 | mem-op:safe-copy($section-root) 331 | ) 332 | }; 333 | 334 | (: Begin private functions! :) 335 | 336 | (: Queue actions for later execution :) 337 | declare %private 338 | function mem-op:queue( 339 | $transaction-id as xs:string, 340 | $nodes-to-modify as node()*, 341 | $modifier-nodes as node()*, 342 | $operation as xs:string?) 343 | as empty-sequence() 344 | { 345 | if (exists($nodes-to-modify)) 346 | then 347 | let $transaction-map as map:map := map:get($queue, $transaction-id) 348 | (: Creates elements based off of generate-id (i.e., node is 12439f8e4a3, then we get back ) :) 349 | let $modified-node-ids as element()* := mem-op:id-wrapper($nodes-to-modify) (: This line uses function mapping :) 350 | return 351 | ( 352 | mem-op:all-nodes-from-same-doc($nodes-to-modify,map:get($transaction-map,"copy")), 353 | map:put( 354 | $transaction-map, 355 | "operation", 356 | ({ 357 | attribute operation { $operation }, 358 | $modified-node-ids 359 | }, 360 | (: Ensure operations are accummulated :) 361 | map:get($transaction-map, "operation"))), 362 | map:put( 363 | $transaction-map, 364 | "nodes-to-modify", 365 | ($nodes-to-modify, 366 | (: Ensure nodes to modify are accummulated :) 367 | map:get($transaction-map, "nodes-to-modify"))), 368 | map:put( 369 | $transaction-map, 370 | "modifier-nodes", 371 | ({ 372 | attribute mem-op:operation { $operation }, 373 | $modifier-nodes[self::attribute()], 374 | $modified-node-ids, 375 | $modifier-nodes[not(self::attribute())] 376 | }, 377 | (: Ensure nodes to modifier nodes are accummulated :) 378 | map:get($transaction-map, "modifier-nodes"))) 379 | ) 380 | else () 381 | }; 382 | 383 | declare function mem-op:all-nodes-from-same-doc($nodes as node()*,$parent-node as node()) as empty-sequence() { 384 | (: NOTE: must use every in satisfies to account for multiple outermost nodes :) 385 | if (every $n in node-op:outermost(($parent-node,$nodes)) satisfies $n is $parent-node) 386 | then () 387 | else 388 | error(xs:QName("mem-op:MIXEDSOURCES"), "The nodes to change are coming from multiple sources",$nodes) 389 | }; 390 | 391 | (: The process functions handle the core logic for handling forked paths that 392 | need to be altered :) 393 | declare 394 | function mem-op:process( 395 | $nodes-to-modify as node()+, 396 | $new-nodes as node()*, 397 | $operation) 398 | as node()* 399 | { 400 | mem-op:all-nodes-from-same-doc($nodes-to-modify,root($nodes-to-modify[1])), 401 | mem-op:safe-copy(mem-op:process((), $nodes-to-modify, $new-nodes, $operation, ())) 402 | }; 403 | 404 | declare 405 | function mem-op:process( 406 | $transaction-map as map:map?, 407 | $nodes-to-modify as node()+, 408 | $new-nodes as node()*, 409 | $operation, 410 | $root-node as node()?) 411 | as node()* 412 | { 413 | mem-op:process( 414 | $transaction-map, 415 | $nodes-to-modify, 416 | node-op:outermost($nodes-to-modify), 417 | $new-nodes, 418 | $operation, 419 | $root-node 420 | ) 421 | }; 422 | 423 | declare 424 | function mem-op:process( 425 | $transaction-map as map:map?, 426 | $nodes-to-modify as node()+, 427 | $outermost-nodes-to-modify as node()+, 428 | $new-nodes as node()*, 429 | $operation, 430 | $root-node as node()?) 431 | as node()* 432 | { 433 | mem-op:process( 434 | $transaction-map, 435 | $nodes-to-modify, 436 | $outermost-nodes-to-modify, 437 | $nodes-to-modify, 438 | $new-nodes, 439 | $operation, 440 | mem-op:find-ancestor-intersect($outermost-nodes-to-modify, 1, ()) 441 | except 442 | (if (exists($root-node)) 443 | then $root-node/ancestor::node() 444 | else ()) 445 | ) 446 | }; 447 | 448 | declare %private 449 | function mem-op:process( 450 | $transaction-map as map:map?, 451 | $nodes-to-modify as node()+, 452 | $outermost-nodes-to-modify as node()+, 453 | $all-nodes-to-modify as node()*, 454 | $new-nodes as node()*, 455 | $operation, 456 | $common-ancestors as node()*) 457 | as node()* 458 | { 459 | mem-op:process( 460 | $transaction-map, 461 | $nodes-to-modify, 462 | $outermost-nodes-to-modify, 463 | $all-nodes-to-modify, 464 | $new-nodes, 465 | $operation, 466 | $common-ancestors, 467 | (: get the first common parent of all the items to modify 468 | (First going up the tree. Last in document order.) :) 469 | $common-ancestors[last()]) 470 | }; 471 | 472 | declare %private 473 | function mem-op:process( 474 | $transaction-map as map:map?, 475 | $nodes-to-modify as node()+, 476 | $outermost-nodes-to-modify as node()+, 477 | $all-nodes-to-modify as node()*, 478 | $new-nodes as node()*, 479 | $operation, 480 | $common-ancestors as node()*, 481 | $common-parent as node()?) 482 | as node()* 483 | { 484 | mem-op:process( 485 | $transaction-map, 486 | $nodes-to-modify, 487 | $outermost-nodes-to-modify, 488 | $all-nodes-to-modify, 489 | $new-nodes, 490 | $operation, 491 | $common-ancestors, 492 | $common-parent, 493 | ($common-parent/node(), $common-parent/@node()) intersect 494 | $outermost-nodes-to-modify/ancestor-or-self::node()) 495 | }; 496 | 497 | declare %private 498 | function mem-op:process( 499 | $transaction-map as map:map?, 500 | $nodes-to-modify as node()+, 501 | $outermost-nodes-to-modify as node()+, 502 | $all-nodes-to-modify as node()*, 503 | $new-nodes as node()*, 504 | $operation, 505 | $common-ancestors as node()*, 506 | $common-parent as node()?, 507 | $merging-nodes as node()*) 508 | as node()* 509 | { 510 | mem-op:process( 511 | $transaction-map, 512 | $nodes-to-modify, 513 | $outermost-nodes-to-modify, 514 | $all-nodes-to-modify, 515 | $new-nodes, 516 | $operation, 517 | $common-ancestors, 518 | $common-parent, 519 | $merging-nodes, 520 | (: create new XML trees for all the unique paths to 521 | the items to modify :) 522 | { 523 | if (exists($common-parent)) 524 | then 525 | mem-op:build-subtree( 526 | $transaction-map, 527 | $merging-nodes, 528 | $nodes-to-modify, 529 | $new-nodes, 530 | $operation, 531 | (: get all of the ancestors :) 532 | $common-parent/ancestor-or-self::node()) 533 | else ( 534 | let $reference-node as node()? := $nodes-to-modify[1]/.. 535 | return 536 | mem-op:build-subtree( 537 | $transaction-map, 538 | if (exists($reference-node)) 539 | then ($reference-node/@node(),$reference-node/node()) intersect $nodes-to-modify/ancestor-or-self::node() 540 | else $outermost-nodes-to-modify, 541 | $nodes-to-modify, 542 | $new-nodes, 543 | $operation, 544 | (: get all of the ancestors :) 545 | $nodes-to-modify[1]/ancestor::node()) 546 | ) 547 | }) 548 | }; 549 | 550 | declare %private 551 | function mem-op:process( 552 | $transaction-map as map:map?, 553 | $nodes-to-modify as node()+, 554 | $outermost-nodes-to-modify as node()+, 555 | $all-nodes-to-modify as node()*, 556 | $new-nodes as node()*, 557 | $operation, 558 | $common-ancestors as node()*, 559 | $common-parent as node()?, 560 | $merging-nodes as node()*, 561 | $trees as element(mem-op:trees)) 562 | as node()* 563 | { 564 | if (exists($common-parent)) 565 | then 566 | mem-op:process-ancestors( 567 | $transaction-map, 568 | (: Ancestors of the common parent which will be used to walk up the XML tree. :) 569 | reverse($common-ancestors except $common-parent), 570 | $common-parent, 571 | $operation, 572 | $all-nodes-to-modify, 573 | (: Nodes to modify that are part of the common ancestors :) 574 | $all-nodes-to-modify intersect $common-ancestors, 575 | $new-nodes, 576 | mem-op:reconstruct-node-with-additional-modifications( 577 | $transaction-map, 578 | $common-parent, 579 | mem-op:place-trees( 580 | (: Reduce iterations by using outermost nodes :) 581 | $outermost-nodes-to-modify except $common-parent, 582 | (: Pass attributes and child nodes excluding ancestors of nodes to modify. :) 583 | ($common-parent/node(), $common-parent/@node()) 584 | except 585 | $merging-nodes, 586 | (: New sub trees to put in place. :) 587 | $trees), 588 | $new-nodes, 589 | (), 590 | $all-nodes-to-modify, 591 | $operation, 592 | fn:false() 593 | ) 594 | ) 595 | else 596 | mem-op:place-trees( 597 | (: Reduce iterations by using outermost nodes :) 598 | $outermost-nodes-to-modify, 599 | (: Pass attributes and child nodes excluding ancestors of nodes to modify. :) 600 | let $copy-node as node()? := 601 | if (fn:exists($transaction-map)) 602 | then map:get($transaction-map,'copy') 603 | else () 604 | let $reference-node as node()? := $nodes-to-modify[1]/.. 605 | return 606 | (if (fn:empty($transaction-map) or ($copy-node << $reference-node or $reference-node is $copy-node)) 607 | then($reference-node/(@node()|node())) 608 | else ()) 609 | except 610 | $nodes-to-modify/ancestor-or-self::node(), 611 | (: New sub trees to put in place. :) 612 | $trees) 613 | }; 614 | 615 | declare %private 616 | function mem-op:build-subtree( 617 | $transaction-map as map:map?, 618 | $mod-node as node(), 619 | $nodes-to-modify as node()*, 620 | $new-nodes as node()*, 621 | $operations, 622 | $all-ancestors as node()*) 623 | as node()* 624 | { 625 | mem-op:subtree( 626 | $transaction-map, 627 | $mod-node, 628 | $nodes-to-modify intersect 629 | ($mod-node/descendant-or-self::node(), 630 | $mod-node/descendant-or-self::node()/@node()), 631 | $new-nodes, 632 | $operations, 633 | $all-ancestors) 634 | }; 635 | 636 | declare %private 637 | function mem-op:subtree( 638 | $transaction-map as map:map?, 639 | $mod-node as node(), 640 | $nodes-to-modify as node()*, 641 | $new-nodes as node()*, 642 | $operations, 643 | $all-ancestors as node()*) 644 | as node()* 645 | { 646 | let $mod-node-id-qn := mem-op:generate-id-qn($nodes-to-modify[1]) 647 | let $descendant-nodes-to-mod := $nodes-to-modify except $mod-node 648 | return 649 | mem-op:wrap-subtree( 650 | $mod-node-id-qn, 651 | if (empty($descendant-nodes-to-mod)) 652 | then 653 | mem-op:process-subtree( 654 | $transaction-map, 655 | $mod-node/ancestor::node() except $all-ancestors, 656 | $mod-node, 657 | $mod-node-id-qn, 658 | $new-nodes, 659 | $operations, 660 | ()) 661 | else 662 | let $outermost-nodes-to-mod as node()+ := node-op:outermost($descendant-nodes-to-mod) 663 | return 664 | mem-op:process( 665 | $transaction-map, 666 | $descendant-nodes-to-mod, 667 | $outermost-nodes-to-mod, 668 | $nodes-to-modify, 669 | $new-nodes, 670 | $operations, 671 | (: find the ancestors that all nodes to modify have in common :) 672 | mem-op:find-ancestor-intersect( 673 | $outermost-nodes-to-mod, 674 | 1, 675 | () 676 | ) 677 | except 678 | $all-ancestors) 679 | ) 680 | }; 681 | 682 | declare %private 683 | function mem-op:wrap-subtree( 684 | $mod-node-id-qn as xs:QName, 685 | $results as node()* 686 | )as node()* 687 | { 688 | if ($results) 689 | then 690 | element { $mod-node-id-qn } { 691 | $results 692 | } 693 | else () 694 | }; 695 | (: Creates a new subtree with the changes made based off of the operation. :) 696 | declare %private 697 | function mem-op:process-subtree( 698 | $transaction-map as map:map?, 699 | $ancestors as node()*, 700 | $node-to-modify as node(), 701 | $node-to-modify-id-qn as xs:QName, 702 | $new-node as node()*, 703 | $operations, 704 | $ancestor-nodes-to-modify as node()*) 705 | as node()* 706 | { 707 | mem-op:process-ancestors( 708 | $transaction-map, 709 | reverse($ancestors), 710 | (), 711 | $operations, 712 | $node-to-modify, 713 | $ancestor-nodes-to-modify, 714 | $new-node, 715 | mem-op:build-new-xml( 716 | $transaction-map, 717 | $node-to-modify, 718 | typeswitch ($operations) 719 | case xs:string return $operations 720 | default return 721 | $operations[*[node-name(.) eq $node-to-modify-id-qn]]/ 722 | @operation, 723 | typeswitch ($new-node) 724 | case element(mem-op:modifier-nodes)* return $new-node[*[node-name(.) eq $node-to-modify-id-qn]] 725 | default return 726 | { 727 | attribute mem-op:operation { $operations }, 728 | $new-node 729 | })) 730 | }; 731 | 732 | (: Find all of the common ancestors of a given set of nodes :) 733 | declare %private 734 | function mem-op:find-ancestor-intersect( 735 | $items as node()*, 736 | $current-position as xs:integer, 737 | $ancestor-intersect as node()*) 738 | as node()* 739 | { 740 | if (empty($items)) 741 | then $ancestor-intersect 742 | else if ($current-position gt 1) 743 | (: if ancestor-intersect already exists intersect with the current item's ancestors :) 744 | then 745 | if (empty($ancestor-intersect)) 746 | (: short circuit if intersect is empty :) 747 | then () 748 | else 749 | $ancestor-intersect intersect head($items)/ancestor::node() intersect $items[fn:last()]/ancestor::node() 750 | (: otherwise just use the current item's ancestors :) 751 | else 752 | mem-op:find-ancestor-intersect( 753 | tail($items), 754 | $current-position + 1, 755 | head($items)/ancestor::node()) 756 | }; 757 | 758 | (: Place newly created trees in proper order :) 759 | declare %private 760 | function mem-op:place-trees( 761 | $nodes-to-modify as node()*, 762 | $merging-nodes as node()*, 763 | $trees as element(mem-op:trees)?) 764 | as node()* 765 | { 766 | if (empty($nodes-to-modify) or empty($trees[*])) 767 | then $merging-nodes 768 | else ( 769 | let $tree-ids := $trees/* ! substring-after(local-name(.),'_') 770 | let $count-of-trees := count($tree-ids) 771 | for $tree at $pos in $trees/* 772 | let $previous-tree-pos := $pos - 1 773 | let $previous-tree-id := $tree-ids[position() eq $previous-tree-pos] 774 | let $current-tree-id := $tree-ids[position() eq $pos] 775 | let $previous-node-to-modify := 776 | if (exists($previous-tree-id)) 777 | then $nodes-to-modify[generate-id() eq $previous-tree-id][1] 778 | else () 779 | let $node-to-modify := $nodes-to-modify[generate-id() eq $current-tree-id][1] 780 | return 781 | ( 782 | node-op:inbetween($merging-nodes, $previous-node-to-modify, $node-to-modify), 783 | $tree/(attribute::node()|child::node()), 784 | if ($pos eq $count-of-trees) 785 | then 786 | node-op:inbetween($merging-nodes, $node-to-modify, ()) 787 | else () 788 | ) 789 | ) 790 | }; 791 | 792 | (: Go up the tree to build new XML using tail recursion. This is used when there are no side 793 | steps to merge in, only a direct path up the tree. $ancestors is expected to be passed in 794 | REVERSE document order. :) 795 | declare %private 796 | function mem-op:process-ancestors( 797 | $transaction-map as map:map?, 798 | $ancestors as node()*, 799 | $last-ancestor as node()?, 800 | $operations, 801 | $nodes-to-modify as node()*, 802 | $ancestor-nodes-to-modify as node()*, 803 | $new-node as node()*, 804 | $base as node()*) 805 | as node()* 806 | { 807 | if (exists($ancestors)) 808 | then 809 | mem-op:process-ancestors( 810 | $transaction-map, 811 | tail($ancestors), 812 | head($ancestors), 813 | $operations, 814 | $nodes-to-modify, 815 | $ancestor-nodes-to-modify, 816 | $new-node, 817 | mem-op:reconstruct-node-with-additional-modifications( 818 | $transaction-map, 819 | head($ancestors), 820 | ($last-ancestor/preceding-sibling::node(),$base,$last-ancestor/following-sibling::node()), 821 | $new-node, 822 | $nodes-to-modify, 823 | $ancestor-nodes-to-modify, 824 | $operations, 825 | fn:true() 826 | ) 827 | ) 828 | else 829 | $base 830 | }; 831 | 832 | (: Generic logic for rebuilding document/element nodes and passing in for :) 833 | declare %private 834 | function mem-op:reconstruct-node-with-additional-modifications( 835 | $transaction-map as map:map?, 836 | $node as node(), 837 | $ordered-content as node()*, 838 | $new-node as node()*, 839 | $nodes-to-modify as node()*, 840 | $ancestor-nodes-to-modify as node()*, 841 | $operations, 842 | $carry-over-attributes as xs:boolean) 843 | { 844 | if (some $n in $ancestor-nodes-to-modify 845 | satisfies $n is $node) 846 | then 847 | mem-op:process-subtree( 848 | $transaction-map, 849 | (), 850 | mem-op:reconstruct-node($node,$ordered-content,$nodes-to-modify,$carry-over-attributes), 851 | mem-op:generate-id-qn($node), 852 | $new-node, 853 | $operations, 854 | () 855 | ) 856 | else 857 | mem-op:reconstruct-node($node,$ordered-content,$nodes-to-modify,$carry-over-attributes) 858 | }; 859 | 860 | (: Generic logic for rebuilding document/element nodes :) 861 | declare %private 862 | function mem-op:reconstruct-node( 863 | $node as node(), 864 | $ordered-content as node()*, 865 | $nodes-to-modify as node()*, 866 | $carry-over-attributes as xs:boolean) 867 | { 868 | typeswitch ($node) 869 | case element() return 870 | element { node-name($node) } { 871 | if ($carry-over-attributes) 872 | then $node/@attribute() except $nodes-to-modify 873 | else (), 874 | $node/namespace::*, 875 | $ordered-content 876 | } 877 | case document-node() return 878 | document { 879 | $ordered-content 880 | } 881 | default return () 882 | }; 883 | 884 | (: Generate an id unique to a node in memory. Right now using fn:generate-id. :) 885 | declare 886 | function mem-op:id-wrapper($node as node()) 887 | { 888 | element {mem-op:generate-id-qn($node)} {()} 889 | }; 890 | 891 | (: Generate QName from node :) 892 | declare %private 893 | function mem-op:generate-id-qn($node as node()) 894 | { 895 | QName( 896 | "http://maxdewpoint.blogspot.com/memory-operations", 897 | concat("_", mem-op:generate-id($node))) 898 | }; 899 | 900 | (: Generate an id unique to a node in memory. Right now using fn:generate-id. :) 901 | declare %private 902 | function mem-op:generate-id($node as node()) 903 | { 904 | generate-id($node) 905 | }; 906 | 907 | (: Create a key to uniquely identify a function :) 908 | declare 909 | function mem-op:function-key($function as function(*)) 910 | { 911 | xdmp:key-from-QName( 912 | (function-name($function), 913 | xs:QName("_" || string(xdmp:random())))[1]) || 914 | "#" || 915 | string(function-arity($function)) 916 | }; 917 | 918 | (: This is where the transformations to the XML take place and this module can be extended. :) 919 | declare %private 920 | function mem-op:build-new-xml( 921 | $transaction-map as map:map?, 922 | $node as node(), 923 | $operations as xs:string*, 924 | $modifier-nodes as element(mem-op:modifier-nodes)*) 925 | { 926 | mem-op:build-new-xml( 927 | $transaction-map, 928 | $node, 929 | mem-op:weighed-operations(distinct-values($operations)), 930 | $modifier-nodes, 931 | () 932 | ) 933 | }; 934 | 935 | (: This function contains the logic for each of the operations is is going to be the most 936 | likely place extensions will be made. :) 937 | declare %private 938 | function mem-op:build-new-xml( 939 | $transaction-map as map:map?, 940 | $nodes as node()*, 941 | $operations as xs:string*, 942 | $modifier-nodes as element(mem-op:modifier-nodes)*, 943 | $modifying-node as node()?) 944 | { 945 | if (empty($operations) or empty($nodes)) 946 | then $nodes 947 | else 948 | let $node as node()? := if (count($nodes) eq 1) then $nodes else $modifying-node 949 | let $pivot-pos as xs:integer? := $nodes ! (if (. is $node) then position() else ()) 950 | let $operation as xs:string := head($operations) 951 | let $last-in-wins as xs:boolean := $operation = ('replace-value') 952 | let $reverse-mod-nodes as xs:boolean := $operation = ('insert-child') 953 | let $mod-nodes as node()* := 954 | let $modifier-nodes := 955 | if ($last-in-wins) 956 | then ($modifier-nodes[@mem-op:operation eq $operation])[1] 957 | else if ($reverse-mod-nodes) 958 | then reverse($modifier-nodes[@mem-op:operation eq $operation]) 959 | else $modifier-nodes[@mem-op:operation eq $operation] 960 | return 961 | ($modifier-nodes ! @node()[empty(self::attribute(mem-op:operation))], 962 | $modifier-nodes ! node()[empty(self::mem-op:*)]) 963 | 964 | let $new-nodes := 965 | switch ($operation) 966 | case "replace" return $mod-nodes 967 | case "insert-child" return 968 | element { node-name($node) } { 969 | let $attributes-to-insert := $mod-nodes[self::attribute()], 970 | $attributes-to-insert-qns := $attributes-to-insert/node-name(.) 971 | return 972 | ($node/@*[not(node-name(.) = $attributes-to-insert-qns)], 973 | $attributes-to-insert, 974 | $node/namespace::*, 975 | $node/node(), 976 | $mod-nodes[exists(. except $attributes-to-insert)]) 977 | } 978 | case "insert-child-first" return 979 | element { node-name($node) } { 980 | let $attributes-to-insert := $mod-nodes[self::attribute()], 981 | $attributes-to-insert-qns := $attributes-to-insert/node-name(.) 982 | return 983 | ($attributes-to-insert, 984 | $node/@*[not(node-name(.) = $attributes-to-insert-qns)], 985 | $node/namespace::*, 986 | $mod-nodes[exists(. except $attributes-to-insert)], 987 | $node/node()) 988 | } 989 | case "insert-after" return ($node, $mod-nodes) 990 | case "insert-before" return ($mod-nodes, $node) 991 | case "rename" return 992 | element { node-name(($mod-nodes[self::element()])[1]) } { $node/@*, $node/namespace::*, $node/node() } 993 | case "replace-value" return 994 | typeswitch ($node) 995 | case attribute() return attribute { node-name($node) } { $mod-nodes } 996 | case element() return 997 | element { node-name($node) } { $node/@*, $node/namespace::*, $mod-nodes } 998 | case processing-instruction() return 999 | processing-instruction { 1000 | node-name($node) 1001 | } { 1002 | $mod-nodes 1003 | } 1004 | case comment() return 1005 | comment { 1006 | $mod-nodes 1007 | } 1008 | case text() return $mod-nodes 1009 | default return () 1010 | case "transform" return 1011 | if (exists($transaction-map)) 1012 | then 1013 | map:get( 1014 | $transaction-map, 1015 | string($mod-nodes))( 1016 | $node) 1017 | else 1018 | map:get($transform-functions, string($mod-nodes))( 1019 | $node) 1020 | default return () 1021 | return 1022 | mem-op:build-new-xml( 1023 | $transaction-map, 1024 | unordered { 1025 | $nodes[position() lt $pivot-pos], 1026 | $new-nodes, 1027 | $nodes[position() gt $pivot-pos] 1028 | }, 1029 | tail($operations), 1030 | $modifier-nodes, 1031 | if ($operation = ('insert-after','insert-before')) 1032 | then $node 1033 | else $new-nodes[1] 1034 | ) 1035 | }; 1036 | 1037 | (: Order the operations in such a way that the least amount of stomping on eachother occurs :) 1038 | declare %private 1039 | function mem-op:weighed-operations( 1040 | $operations as xs:string*) as xs:string* 1041 | { 1042 | $operations[. eq "replace"], 1043 | $operations[. eq "replace-value"], 1044 | $operations[not(. = ("replace","replace-value","transform"))], 1045 | $operations[. eq "transform"] 1046 | }; 1047 | 1048 | declare %private 1049 | function mem-op:safe-copy( 1050 | $node as node()) 1051 | as node()? { 1052 | try { 1053 | validate lax { 1054 | $node 1055 | } 1056 | } catch * { 1057 | mem-op:reconstruct-node($node,$node/*,(),fn:true()) 1058 | } 1059 | }; 1060 | -------------------------------------------------------------------------------- /mlpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XQuery-XML-Memory-Operations", 3 | "version": "1.0.7", 4 | "description": "XQuery library module for updating XML in memory", 5 | "repository": "https://github.com/ryanjdew/XQuery-XML-Memory-Operations.git" 6 | } 7 | -------------------------------------------------------------------------------- /node-operations.xqy: -------------------------------------------------------------------------------- 1 | xquery version "3.0"; 2 | (:~ 3 | Copyright (c) 2013 Ryan Dew 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | @author Ryan Dew (ryan.j.dew@gmail.com) 18 | @version 0.7 19 | @description This is a module with function changing XML in memory by creating subtrees using the ancestor, preceding-sibling, and following-sibling axes 20 | and intersect/except expressions. Requires MarkLogic 6+. 21 | :) 22 | 23 | module namespace node-op = "http://maxdewpoint.blogspot.com/node-operations"; 24 | declare default function namespace "http://www.w3.org/2005/xpath-functions"; 25 | 26 | 27 | declare function node-op:innermost($nodes as node()*) { 28 | (: node-op:function-select(( 29 | function-lookup(QName('http://www.w3.org/2005/xpath-functions','innermost'), 1), 30 | function ($nodes as node()*) { :) 31 | $nodes except $nodes/ancestor::node() 32 | (: } 33 | ))($nodes) :) 34 | }; 35 | 36 | declare function node-op:outermost($nodes as node()*) { 37 | (:node-op:function-select(( 38 | function-lookup(QName('http://www.w3.org/2005/xpath-functions','outermost'), 1), 39 | function ($nodes as node()*) { :) 40 | $nodes except $nodes[ancestor::node() intersect $nodes] 41 | (: } 42 | ))($nodes) :) 43 | }; 44 | 45 | declare function node-op:inbetween($nodes as node()*, $start as node()?, $end as node()?) { 46 | node-op:inbetween($nodes, $start, $end, ()) 47 | }; 48 | 49 | declare function node-op:inbetween-inclusive($nodes as node()*, $start as node()?, $end as node()?) { 50 | node-op:inbetween($nodes, $start, $end, ('start','end')) 51 | }; 52 | 53 | declare function node-op:inbetween-inclusive-start($nodes as node()*, $start as node()?, $end as node()?) { 54 | node-op:inbetween($nodes, $start, $end, ('start')) 55 | }; 56 | 57 | declare function node-op:inbetween-inclusive-end($nodes as node()*, $start as node()?, $end as node()?) { 58 | node-op:inbetween($nodes, $start, $end, ('end')) 59 | }; 60 | 61 | declare %private function node-op:inbetween($nodes as node()*, $start as node()?, $end as node()?, $inclusion as xs:string*) { 62 | if (fn:exists($nodes)) 63 | then 64 | ( 65 | if ($inclusion = 'start') 66 | then $nodes intersect $start 67 | else () 68 | ) union ( 69 | if (exists($start) and exists($end)) 70 | then $nodes[. >> $start][. << $end] 71 | else if (exists($start)) 72 | then $nodes[. >> $start] 73 | else if (exists($end)) 74 | then $nodes[. << $end] 75 | else () 76 | ) union ( 77 | if ($inclusion = 'end') 78 | then $nodes intersect $end 79 | else () 80 | ) 81 | else () 82 | }; 83 | 84 | 85 | 86 | declare %private function node-op:function-select($functions as function(*)+) as function(*) { 87 | $functions[1] 88 | }; 89 | -------------------------------------------------------------------------------- /test/memory-operations-functional-test.xqy: -------------------------------------------------------------------------------- 1 | xquery version "1.0-ml"; 2 | module namespace test = "http://github.com/robwhitby/xray/test"; 3 | import module namespace assert = "http://github.com/robwhitby/xray/assertions" at "/xray/src/assertions.xqy"; 4 | 5 | import module namespace mem = "http://maxdewpoint.blogspot.com/memory-operations/functional" at "/memory-operations-functional.xqy"; 6 | 7 | declare variable $test-xml := 8 | 9 | This is a title 10 | 11 | 12 | 13 |
14 |

This is a paragraph.

15 |

This is a paragraph.

16 |

This is a paragraph.

17 |

This is a paragraph.

18 |

This is a paragraph.

19 |
20 |
21 |

This is a paragraph.

22 |

This is a paragraph.

23 |

This is a paragraph.

24 |

This is a paragraph.

25 |

This is a paragraph.

26 |
27 | 28 | ; 29 | 30 | declare %test:case function insert-before-and-insert-attribute() 31 | as item()* 32 | { 33 | let $new-xml := 34 | mem:execute(mem:insert-child(mem:insert-before(mem:copy($test-xml), 35 | $test-xml/body/div/p[@class eq "p3"], 36 | element p { attribute class {"testing"}} 37 | ), 38 | $test-xml/body/div/p[@class eq "p3"], attribute data-testing {"this-is-a-test"}) 39 | ) 40 | return ( 41 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 42 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 43 | return ( 44 | assert:equal(fn:string(($p/preceding-sibling::node())[fn:last()]/@class), 'testing'), 45 | assert:equal(fn:string($p/@data-testing), 'this-is-a-test'), 46 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 47 | ) 48 | ) 49 | }; 50 | 51 | declare %test:case function insert-after-and-insert-attribute() 52 | as item()* 53 | { 54 | let $new-xml := 55 | mem:execute(mem:insert-child(mem:insert-after(mem:copy($test-xml), 56 | $test-xml/body/div/p[@class eq "p3"], 57 | element p { attribute class {"testing"}} 58 | ), 59 | $test-xml/body/div/p[@class eq "p3"], attribute data-testing {"this-is-a-test"}) 60 | ) 61 | return ( 62 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 63 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 64 | return ( 65 | assert:equal(fn:string($p/following-sibling::node()[1]/@class), 'testing'), 66 | assert:equal(fn:string($p/@data-testing), 'this-is-a-test'), 67 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 68 | ) 69 | ) 70 | }; 71 | 72 | declare %test:case function advanced-operation() 73 | as item()* 74 | { 75 | let $new-xml := 76 | mem:execute(mem:insert-child(mem:replace(mem:copy($test-xml), 77 | $test-xml/head/title,element title {"This is so awesome!"} 78 | ), 79 | $test-xml/body/div/p, attribute data-info {"This is also awesome!"}) 80 | ) 81 | 82 | return (assert:equal(fn:string($new-xml/head/title), "This is so awesome!"), 83 | for $p in $new-xml/body/div/p 84 | return assert:equal(fn:string($p/@data-info), "This is also awesome!")) 85 | }; 86 | 87 | declare %test:case function copy() 88 | as item()* 89 | { 90 | let $test-xml := document { $test-xml }/html 91 | let $new-xml := 92 | mem:execute(mem:insert-child(mem:replace(mem:copy($test-xml), 93 | $test-xml/head/title,element title {"This is so awesome!"} 94 | ), 95 | $test-xml/body/div/p, attribute data-info {"This is also awesome!"}) 96 | ) 97 | return (assert:equal($new-xml instance of element(html), fn:true()), 98 | assert:equal(fn:string($new-xml/head/title), "This is so awesome!"), 99 | for $p in $new-xml/body/div/p 100 | return assert:equal(fn:string($p/@data-info), "This is also awesome!")) 101 | }; 102 | 103 | declare %test:case function multiple-operations-on-one-node() 104 | as item()* 105 | { 106 | let $title := $test-xml/head/title 107 | let $new-xml := 108 | mem:execute(mem:replace-value(mem:rename(mem:copy($title), 109 | $title,fn:QName("","new-title") 110 | ), 111 | $title,"This is so awesome!") 112 | ) 113 | return (assert:equal($new-xml instance of element(new-title), fn:true()), 114 | assert:equal(fn:string($new-xml), "This is so awesome!")) 115 | }; 116 | 117 | declare %test:case function transform-function-transaction() 118 | as item()* 119 | { 120 | let $title := $test-xml/head/title 121 | let $new-xml := 122 | mem:execute(mem:transform(mem:copy($title), 123 | $title,function($node as node()) as node()* {element new-title {"This is so awesome!"}} 124 | )) 125 | return (assert:equal($new-xml instance of element(new-title), fn:true()), 126 | assert:equal(fn:string($new-xml), "This is so awesome!")) 127 | }; 128 | -------------------------------------------------------------------------------- /test/memory-operations-pure-xquery-test.xqy: -------------------------------------------------------------------------------- 1 | xquery version "1.0-ml"; 2 | module namespace test = "http://github.com/robwhitby/xray/test"; 3 | import module namespace assert = "http://github.com/robwhitby/xray/assertions" at "/xray/src/assertions.xqy"; 4 | 5 | import module namespace mem = "http://maxdewpoint.blogspot.com/memory-operations" at "/memory-operations-pure-xquery.xqy"; 6 | 7 | declare variable $test-xml := 8 | 9 | This is a title 10 | 11 | 12 | 13 |
14 |

This is a paragraph.

15 |

This is a paragraph.

16 |

This is a paragraph.

17 |

This is a paragraph.

18 |

This is a paragraph.

19 |
20 |
21 |

This is a paragraph.

22 |

This is a paragraph.

23 |

This is a paragraph.

24 |

This is a paragraph.

25 |

This is a paragraph.

26 |
27 | 28 | ; 29 | 30 | declare %test:case function insert-before-and-insert-attribute() 31 | as item()* 32 | { 33 | let $new-xml := 34 | mem:execute(mem:insert-child(mem:insert-before(mem:copy($test-xml), 35 | $test-xml/body/div/p[@class eq "p3"], 36 | element p { attribute class {"testing"}} 37 | ), 38 | $test-xml/body/div/p[@class eq "p3"], attribute data-testing {"this-is-a-test"}) 39 | ) 40 | return ( 41 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 42 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 43 | return ( 44 | assert:equal(fn:string(($p/preceding-sibling::node())[fn:last()]/@class), 'testing'), 45 | assert:equal(fn:string($p/@data-testing), 'this-is-a-test'), 46 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 47 | ) 48 | ) 49 | }; 50 | 51 | declare %test:case function insert-after-and-insert-attribute() 52 | as item()* 53 | { 54 | let $new-xml := 55 | mem:execute(mem:insert-child(mem:insert-after(mem:copy($test-xml), 56 | $test-xml/body/div/p[@class eq "p3"], 57 | element p { attribute class {"testing"}} 58 | ), 59 | $test-xml/body/div/p[@class eq "p3"], attribute data-testing {"this-is-a-test"}) 60 | ) 61 | return ( 62 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 63 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 64 | return ( 65 | assert:equal(fn:string($p/following-sibling::node()[1]/@class), 'testing'), 66 | assert:equal(fn:string($p/@data-testing), 'this-is-a-test'), 67 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 68 | ) 69 | ) 70 | }; 71 | 72 | declare %test:case function advanced-operation() 73 | as item()* 74 | { 75 | let $new-xml := 76 | mem:execute(mem:insert-child(mem:replace(mem:copy($test-xml), 77 | $test-xml/head/title,element title {"This is so awesome!"} 78 | ), 79 | $test-xml/body/div/p, attribute data-info {"This is also awesome!"}) 80 | ) 81 | 82 | return (assert:equal(fn:string($new-xml/head/title), "This is so awesome!"), 83 | for $p in $new-xml/body/div/p 84 | return assert:equal(fn:string($p/@data-info), "This is also awesome!")) 85 | }; 86 | 87 | declare %test:case function copy() 88 | as item()* 89 | { 90 | let $test-xml := document { $test-xml }/html 91 | let $new-xml := 92 | mem:execute(mem:insert-child(mem:replace(mem:copy($test-xml), 93 | $test-xml/head/title,element title {"This is so awesome!"} 94 | ), 95 | $test-xml/body/div/p, attribute data-info {"This is also awesome!"}) 96 | ) 97 | return (assert:equal($new-xml instance of element(html), fn:true()), 98 | assert:equal(fn:string($new-xml/head/title), "This is so awesome!"), 99 | for $p in $new-xml/body/div/p 100 | return assert:equal(fn:string($p/@data-info), "This is also awesome!")) 101 | }; 102 | 103 | declare %test:case function multiple-operations-on-one-node() 104 | as item()* 105 | { 106 | let $title := $test-xml/head/title 107 | let $new-xml := 108 | mem:execute(mem:replace-value(mem:rename(mem:copy($title), 109 | $title,fn:QName("","new-title") 110 | ), 111 | $title,"This is so awesome!") 112 | ) 113 | return (assert:equal($new-xml instance of element(new-title), fn:true()), 114 | assert:equal(fn:string($new-xml), "This is so awesome!")) 115 | }; 116 | 117 | declare %test:case function transform-function-transaction() 118 | as item()* 119 | { 120 | let $title := $test-xml/head/title 121 | let $new-xml := 122 | mem:execute(mem:transform(mem:copy($title), 123 | $title,function($node as node()) as node()* {element new-title {"This is so awesome!"}} 124 | )) 125 | return (assert:equal($new-xml instance of element(new-title), fn:true()), 126 | assert:equal(fn:string($new-xml), "This is so awesome!")) 127 | }; 128 | -------------------------------------------------------------------------------- /test/memory-operations-test.xqy: -------------------------------------------------------------------------------- 1 | xquery version "1.0-ml"; 2 | module namespace test = "http://github.com/robwhitby/xray/test"; 3 | import module namespace assert = "http://github.com/robwhitby/xray/assertions" at "/xray/src/assertions.xqy"; 4 | 5 | import module namespace mem = "http://maxdewpoint.blogspot.com/memory-operations" at "/memory-operations.xqy"; 6 | 7 | declare variable $test-xml := 8 | 9 | This is a title 10 | 11 | 12 | 13 |
14 |

This is a paragraph.

15 |

This is a paragraph.

16 |

This is a paragraph.

17 |

This is a paragraph.

18 |

This is a paragraph.

19 |
20 |
21 |

This is a paragraph.

22 |

This is a paragraph.

23 |

This is a paragraph.

24 |

This is a paragraph.

25 |

This is a paragraph.

26 |
27 | 28 | ; 29 | 30 | 31 | 32 | declare %test:case function insert-child-into-root-attribute() 33 | as item()* 34 | { 35 | let $new-xml := mem:insert-child( 36 | $test-xml, 37 | attribute test {"testing"} 38 | ) 39 | return assert:equal(fn:string($new-xml/@test), 'testing') 40 | }; 41 | 42 | declare %test:case function insert-child-into-many-items-attribute() 43 | as item()* 44 | { 45 | let $new-xml := mem:insert-child( 46 | ($test-xml,$test-xml/body/div[@id eq "div1"], 47 | $test-xml/body/div/p), 48 | attribute test {"testing"} 49 | ) 50 | for $i in ($new-xml,$new-xml/body/div[@id eq "div1"], 51 | $new-xml/body/div/p) 52 | return assert:equal(fn:string($i/@test), 'testing') 53 | }; 54 | 55 | declare %test:case function insert-child-into-root-element() 56 | as item()* 57 | { 58 | let $new-xml := mem:insert-child( 59 | $test-xml, 60 | element test {"testing"} 61 | ) 62 | return assert:equal(fn:string($new-xml/test), 'testing') 63 | }; 64 | 65 | declare %test:case function insert-child-into-many-items-element() 66 | as item()* 67 | { 68 | let $new-xml := mem:insert-child( 69 | ($test-xml,$test-xml/body/div[@id eq "div1"], 70 | $test-xml/body/div/p), 71 | element test {"testing"} 72 | ) 73 | for $i in ($new-xml,$new-xml/body/div[@id eq "div1"], 74 | $new-xml/body/div/p) 75 | return assert:equal(fn:string($i/test), 'testing') 76 | }; 77 | 78 | declare %test:case function insert-before() 79 | as item()* 80 | { 81 | let $new-xml := mem:insert-before( 82 | $test-xml/body/div/p[@class eq "p3"], 83 | element p { attribute class {"testing"}} 84 | ) 85 | return ( 86 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 87 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 88 | return ( 89 | assert:equal(fn:string(($p/preceding-sibling::node())[fn:last()]/@class), 'testing'), 90 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 91 | ) 92 | ) 93 | }; 94 | 95 | declare %test:case function insert-before-and-insert-attribute() 96 | as item()* 97 | { 98 | let $new-xml := 99 | let $id := mem:copy($test-xml) 100 | return ( 101 | mem:insert-before($id, 102 | $test-xml/body/div/p[@class eq "p3"], 103 | element p { attribute class {"testing"}} 104 | ), 105 | mem:insert-child($id, $test-xml/body/div/p[@class eq "p3"], attribute data-testing {"this-is-a-test"}), 106 | mem:execute($id) 107 | ) 108 | return ( 109 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 110 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 111 | return ( 112 | assert:equal(fn:string(($p/preceding-sibling::node())[fn:last()]/@class), 'testing'), 113 | assert:equal(fn:string($p/@data-testing), 'this-is-a-test'), 114 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 115 | ) 116 | ) 117 | }; 118 | 119 | declare %test:case function insert-after() 120 | as item()* 121 | { 122 | let $new-xml := mem:insert-after( 123 | $test-xml/body/div/p[@class eq "p3"], 124 | element p { attribute class {"testing"}} 125 | ) 126 | return ( 127 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 128 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 129 | return ( 130 | assert:equal(fn:string($p/following-sibling::node()[1]/@class), 'testing'), 131 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 132 | ) 133 | ) 134 | }; 135 | 136 | declare %test:case function insert-after-and-insert-attribute() 137 | as item()* 138 | { 139 | let $new-xml := 140 | let $id := mem:copy($test-xml) 141 | return ( 142 | mem:insert-after($id, 143 | $test-xml/body/div/p[@class eq "p3"], 144 | element p { attribute class {"testing"}} 145 | ), 146 | mem:insert-child($id, $test-xml/body/div/p[@class eq "p3"], attribute data-testing {"this-is-a-test"}), 147 | mem:execute($id) 148 | ) 149 | return ( 150 | assert:equal(fn:count($new-xml/body/div/p[@class eq "p3"]), 2), 151 | for $p at $pos in $new-xml/body/div/p[@class eq "p3"] 152 | return ( 153 | assert:equal(fn:string($p/following-sibling::node()[1]/@class), 'testing'), 154 | assert:equal(fn:string($p/@data-testing), 'this-is-a-test'), 155 | assert:equal(fn:string($p/parent::node()/@id), fn:concat('div',$pos)) 156 | ) 157 | ) 158 | }; 159 | declare %test:case function remove-items() 160 | as item()* 161 | { 162 | let $new-xml := mem:delete( 163 | $test-xml//comment() 164 | ) 165 | return (assert:equal(fn:count($test-xml//comment()) gt 0, fn:true()), 166 | assert:equal(fn:count($new-xml//comment()), 0)) 167 | }; 168 | 169 | declare %test:case function replace-items() 170 | as item()* 171 | { 172 | let $new-xml := mem:replace( 173 | $test-xml//comment(), 174 | 175 | ) 176 | return (assert:equal(fn:count($new-xml//comment()), fn:count($test-xml//comment())), 177 | for $c in $new-xml//comment() 178 | return assert:equal(fn:string($c), 'this new comment')) 179 | }; 180 | 181 | declare %test:case function replace-item-values() 182 | as item()* 183 | { 184 | let $new-xml := mem:replace-value( 185 | $test-xml//comment(), 186 | "this new comment" 187 | ) 188 | return (assert:equal(fn:count($new-xml//comment()), fn:count($test-xml//comment())), 189 | for $c in $new-xml//comment() 190 | return assert:equal(fn:string($c), 'this new comment')) 191 | }; 192 | 193 | declare %test:case function replace-attributes() 194 | as item()* 195 | { 196 | let $new-xml := mem:replace( 197 | $test-xml//p/@class, 198 | attribute class {"new-class"} 199 | ) 200 | return (assert:equal(fn:count($new-xml//p/@class), fn:count($test-xml//p/@class)), 201 | for $c in $new-xml//p/@class 202 | return assert:equal(fn:string($c), 'new-class')) 203 | }; 204 | 205 | declare %test:case function replace-value-attributes() 206 | as item()* 207 | { 208 | let $new-xml := mem:replace-value( 209 | $test-xml//p/@class, 210 | "new-class" 211 | ) 212 | return (assert:equal(fn:count($new-xml//p/@class), fn:count($test-xml//p/@class)), 213 | for $c in $new-xml//p/@class 214 | return assert:equal(fn:string($c), 'new-class')) 215 | }; 216 | 217 | declare %test:case function rename() 218 | as item()* 219 | { 220 | let $new-xml-blocks := mem:rename($test-xml//p,fn:QName("","block"))/body/div/block 221 | return (for $p at $pos in $test-xml/body/div/p 222 | return assert:equal($p/(@*|node()), $new-xml-blocks[$pos]/(@*|node()))) 223 | }; 224 | 225 | declare %test:case function advanced-operation() 226 | as item()* 227 | { 228 | let $new-xml := 229 | let $id := mem:copy($test-xml) 230 | return 231 | ( 232 | mem:replace($id,$test-xml/head/title,element title {"This is so awesome!"}), 233 | mem:insert-child($id,$test-xml/body/div/p,attribute data-info {"This is also awesome!"}), 234 | mem:execute($id) 235 | ) 236 | 237 | return (assert:equal(fn:string($new-xml/head/title), "This is so awesome!"), 238 | for $p in $new-xml/body/div/p 239 | return assert:equal(fn:string($p/@data-info), "This is also awesome!")) 240 | }; 241 | 242 | declare %test:case function copy() 243 | as item()* 244 | { 245 | let $test-xml := document { $test-xml }/html 246 | let $new-xml := 247 | let $id := mem:copy($test-xml) 248 | return 249 | ( 250 | mem:replace($id,$test-xml/head/title,element title {"This is so awesome!"}), 251 | mem:insert-child($id,$test-xml/body/div/p,attribute data-info {"This is also awesome!"}), 252 | mem:execute($id) 253 | ) 254 | return (assert:equal($new-xml instance of element(html), fn:true()), 255 | assert:equal(fn:string($new-xml/head/title), "This is so awesome!"), 256 | for $p in $new-xml/body/div/p 257 | return assert:equal(fn:string($p/@data-info), "This is also awesome!")) 258 | }; 259 | 260 | declare %test:case function multiple-operations-on-one-node() 261 | as item()* 262 | { 263 | let $title := $test-xml/head/title 264 | let $new-xml := 265 | let $id := mem:copy($title) 266 | return 267 | ( 268 | mem:rename($id,$title,fn:QName("","new-title")), 269 | mem:replace-value($id,$title,"This is so awesome!"), 270 | mem:execute($id) 271 | ) 272 | return (assert:equal($new-xml instance of element(new-title), fn:true()), 273 | assert:equal(fn:string($new-xml), "This is so awesome!")) 274 | }; 275 | 276 | declare %test:case function transform-function-transaction() 277 | as item()* 278 | { 279 | let $title := $test-xml/head/title 280 | let $new-xml := 281 | let $id := mem:copy($title) 282 | return 283 | ( 284 | mem:transform($id,$title,function($node as node()) as node()* {element new-title {"This is so awesome!"}}), 285 | mem:execute($id) 286 | ) 287 | return (assert:equal($new-xml instance of element(new-title), fn:true()), 288 | assert:equal(fn:string($new-xml), "This is so awesome!")) 289 | }; 290 | 291 | declare %test:case function transform-function() 292 | as item()* 293 | { 294 | let $title := $test-xml/head/title 295 | let $new-xml := mem:transform($title,function($node as node()) as node()* {element new-title {"This is so awesome!"}}) 296 | return assert:equal(fn:string($new-xml/head/new-title), "This is so awesome!") 297 | }; 298 | 299 | declare %test:case function execute-section() 300 | as item()* 301 | { 302 | let $div1 := $test-xml//div[@id = "div1"] 303 | let $new-xml := 304 | let $id := mem:copy($test-xml)[1] 305 | return 306 | ( 307 | mem:insert-child($id,$div1,attribute class {"added-class"}), 308 | mem:replace($id,$div1, 309 | let $copy := mem:execute-section($id, $div1) 310 | for $i in (1 to 10) 311 | let $cid := mem:copy($copy) 312 | return 313 | ( 314 | mem:insert-child($cid,$copy,attribute data-position {$i}), 315 | mem:execute($cid) 316 | ) 317 | ), 318 | mem:execute($id) 319 | ) 320 | return (assert:equal(fn:count($new-xml//div[@id = "div1"]), 10), 321 | for $div at $pos in $new-xml//div[@id = "div1"] 322 | return ( 323 | assert:equal(fn:number($div/@data-position), $pos), 324 | assert:equal(fn:string($div/@class), "added-class") 325 | ) 326 | ) 327 | }; 328 | 329 | declare %test:case function throws-error-on-mixed-sources() 330 | as item()* 331 | { 332 | let $other-doc :=

my paragraph

333 | return 334 | assert:true( 335 | try { 336 | mem:replace(($test-xml,$other-doc)//p, ) 337 | } catch mem:MIXEDSOURCES { 338 | fn:true() 339 | } 340 | ) 341 | }; 342 | 343 | declare %test:case function removes-mem-op-namespace() 344 | as item()* 345 | { 346 | let $title := $test-xml/head/title 347 | let $new-xml := 348 | let $id := mem:copy($test-xml) 349 | return 350 | ( 351 | mem:insert-child($id,$test-xml,attribute test {"testing"}), 352 | mem:transform($id,$title,function($node as node()) as node()* {element new-title {"This is so awesome!"}}), 353 | mem:execute($id) 354 | ) 355 | return 356 | assert:true(fn:empty($new-xml/namespace::mem-op), 'namespace not removed on queued run'), 357 | let $title := $test-xml/head/title 358 | let $new-xml := mem:insert-child($test-xml,attribute test {"testing"}) 359 | return 360 | assert:true(fn:empty($new-xml/namespace::mem-op), 'namespace not removed on direct run') 361 | }; --------------------------------------------------------------------------------