github.com/matislovas/ratago@v0.0.0-20240408115641-cc0857415a7a/xslt/instruction.go (about) 1 package xslt 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/matislovas/gokogiri/xml" 8 "github.com/matislovas/gokogiri/xpath" 9 ) 10 11 // Most xsl elements are compiled to an instruction. 12 type XsltInstruction struct { 13 Node xml.Node 14 Name string 15 Children []CompiledStep 16 sorting []*sortCriteria 17 } 18 19 // Compile the instruction. 20 // 21 // TODO: we should validate the structure during this step 22 func (i *XsltInstruction) Compile(node xml.Node) { 23 for cur := node.FirstChild(); cur != nil; cur = cur.NextSibling() { 24 res := CompileSingleNode(cur) 25 if cur.Name() == "sort" && cur.Namespace() == XSLT_NAMESPACE { 26 i.sorting = append(i.sorting, compileSortFunction(res.(*XsltInstruction))) 27 continue 28 } 29 if res != nil { 30 res.Compile(cur) 31 i.Children = append(i.Children, res) 32 } 33 } 34 } 35 36 // Some instructions (such as xsl:attribute) require the template body 37 // to be instantiated as a string. 38 39 // In those cases, it is an error if any non-text nodes are generated in the 40 // course of evaluation. 41 func (i *XsltInstruction) evalChildrenAsText(node xml.Node, context *ExecutionContext) (out string, err error) { 42 curOutput := context.OutputNode 43 context.OutputNode = context.Output.CreateElementNode("RVT") 44 for _, c := range i.Children { 45 c.Apply(node, context) 46 } 47 for cur := context.OutputNode.FirstChild(); cur != nil; cur = cur.NextSibling() { 48 //TODO: generate error if cur is not a text node 49 out = out + cur.Content() 50 } 51 context.OutputNode = curOutput 52 return 53 } 54 55 // Evaluate an instruction and generate output nodes 56 func (i *XsltInstruction) Apply(node xml.Node, context *ExecutionContext) { 57 //push context if children to apply! 58 switch i.Name { 59 case "apply-templates": 60 scope := i.Node.Attr("select") 61 mode := i.Node.Attr("mode") 62 // #current is a 2.0 keyword 63 if mode != context.Mode && mode != "#current" { 64 context.Mode = mode 65 } 66 // TODO: determine with-params at compile time 67 var params []*Variable 68 for _, cur := range i.Children { 69 switch p := cur.(type) { 70 case *Variable: 71 if IsXsltName(p.Node, "with-param") { 72 p.Apply(node, context) 73 params = append(params, p) 74 } 75 } 76 } 77 // By default, scope is children of current node 78 if scope == "" { 79 children := context.ChildrenOf(node) 80 if i.sorting != nil { 81 i.Sort(children, context) 82 } 83 total := len(children) 84 oldpos, oldtotal := context.XPathContext.GetContextPosition() 85 oldcurr := context.Current 86 for i, cur := range children { 87 context.XPathContext.SetContextPosition(i+1, total) 88 //processNode will update Context.Current whenever a template is invoked 89 context.Style.processNode(cur, context, params) 90 } 91 context.XPathContext.SetContextPosition(oldpos, oldtotal) 92 context.Current = oldcurr 93 return 94 } 95 context.RegisterXPathNamespaces(i.Node) 96 e := xpath.Compile(scope) 97 // TODO: ensure we apply strip-space if required 98 nodes, err := context.EvalXPathAsNodeset(node, e) 99 if err != nil { 100 fmt.Println("apply-templates @select", err) 101 } 102 if i.sorting != nil { 103 i.Sort(nodes, context) 104 } 105 total := len(nodes) 106 oldpos, oldtotal := context.XPathContext.GetContextPosition() 107 oldcurr := context.Current 108 for i, cur := range nodes { 109 context.XPathContext.SetContextPosition(i+1, total) 110 context.Style.processNode(cur, context, params) 111 } 112 context.XPathContext.SetContextPosition(oldpos, oldtotal) 113 context.Current = oldcurr 114 case "number": 115 i.numbering(node, context) 116 117 case "text": 118 disableEscaping := i.Node.Attr("disable-output-escaping") == "yes" 119 120 content := i.Node.Content() 121 //don't bother creating a text node for an empty string 122 if content != "" { 123 r := context.Output.CreateTextNode(content) 124 if disableEscaping { 125 r.DisableOutputEscaping() 126 } 127 context.OutputNode.AddChild(r) 128 } 129 130 case "call-template": 131 name := i.Node.Attr("name") 132 t, ok := context.Style.NamedTemplates[name] 133 if ok && t != nil { 134 // TODO: determine with-params at compile time 135 var params []*Variable 136 for _, cur := range i.Children { 137 switch p := cur.(type) { 138 case *Variable: 139 if IsXsltName(p.Node, "with-param") { 140 p.Apply(node, context) 141 params = append(params, p) 142 } 143 } 144 } 145 t.Apply(node, context, params) 146 } 147 148 case "element": 149 ename := i.Node.Attr("name") 150 if strings.ContainsRune(ename, '{') { 151 ename = evalAVT(ename, node, context) 152 } 153 r := context.Output.CreateElementNode(ename) 154 ns := i.Node.Attr("namespace") 155 if strings.ContainsRune(ns, '{') { 156 ns = evalAVT(ns, node, context) 157 } 158 if ns != "" { 159 //TODO: search through namespaces in-scope 160 // not just top-level stylesheet mappings 161 prefix, _ := context.Style.NamespaceMapping[ns] 162 r.SetNamespace(prefix, ns) 163 } else { 164 // if no namespace specified, use the default namespace 165 // in scope at this point in the stylesheet 166 defaultNS := context.DefaultNamespace(i.Node) 167 if defaultNS != "" { 168 r.SetNamespace("", defaultNS) 169 } 170 } 171 context.OutputNode.AddChild(r) 172 context.DeclareStylesheetNamespacesIfRoot(r) 173 old := context.OutputNode 174 context.OutputNode = r 175 176 attsets := i.Node.Attr("use-attribute-sets") 177 if attsets != "" { 178 asets := strings.Fields(attsets) 179 for _, attsetname := range asets { 180 a, _ := context.Style.AttributeSets[attsetname] 181 if a != nil { 182 a.Apply(node, context) 183 } 184 } 185 } 186 for _, c := range i.Children { 187 c.Apply(node, context) 188 } 189 context.OutputNode = old 190 191 case "comment": 192 val, _ := i.evalChildrenAsText(node, context) 193 r := context.Output.CreateCommentNode(val) 194 context.OutputNode.AddChild(r) 195 196 case "processing-instruction": 197 name := i.Node.Attr("name") 198 val, _ := i.evalChildrenAsText(node, context) 199 //TODO: it is an error if val contains "?>" 200 r := context.Output.CreatePINode(name, val) 201 context.OutputNode.AddChild(r) 202 203 case "attribute": 204 aname := i.Node.Attr("name") 205 if strings.ContainsRune(aname, '{') { 206 aname = evalAVT(aname, node, context) 207 } 208 ahref := i.Node.Attr("namespace") 209 if strings.ContainsRune(ahref, '{') { 210 ahref = evalAVT(ahref, node, context) 211 } 212 val, _ := i.evalChildrenAsText(node, context) 213 if ahref == "" { 214 context.OutputNode.SetAttr(aname, val) 215 } else { 216 decl := context.OutputNode.DeclaredNamespaces() 217 dfound := false 218 for _, d := range decl { 219 if ahref == d.Uri { 220 dfound = true 221 break 222 } 223 } 224 if !dfound && ahref != XML_NAMESPACE { 225 //TODO: increment val of generated prefix 226 context.OutputNode.DeclareNamespace("ns_1", ahref) 227 } 228 //if a QName, we ignore the prefix when setting namespace 229 if strings.Contains(aname, ":") { 230 aname = aname[strings.Index(aname, ":")+1:] 231 } 232 context.OutputNode.SetNsAttr(ahref, aname, val) 233 } 234 //context.OutputNode.AddChild(a) 235 236 case "value-of": 237 e := xpath.Compile(i.Node.Attr("select")) 238 disableEscaping := i.Node.Attr("disable-output-escaping") == "yes" 239 240 context.RegisterXPathNamespaces(i.Node) 241 content, _ := context.EvalXPathAsString(node, e) 242 //don't bother creating a text node for an empty string 243 if content != "" { 244 if context.UseCDataSection(context.OutputNode) { 245 olddata := context.OutputNode.LastChild() 246 if olddata == nil || olddata.(*xml.CDataNode) == nil { 247 r := context.Output.CreateCDataNode(content) 248 context.OutputNode.AddChild(r) 249 } else { 250 r := context.Output.CreateCDataNode(olddata.Content() + content) 251 context.OutputNode.AddChild(r) 252 olddata.Remove() 253 } 254 } else { 255 r := context.Output.CreateTextNode(content) 256 if disableEscaping { 257 r.DisableOutputEscaping() 258 } 259 context.OutputNode.AddChild(r) 260 } 261 } 262 case "when": 263 case "if": 264 e := xpath.Compile(i.Node.Attr("test")) 265 if context.EvalXPathAsBoolean(node, e) { 266 for _, c := range i.Children { 267 c.Apply(node, context) 268 } 269 } 270 case "attribute-set": 271 for _, c := range i.Children { 272 c.Apply(node, context) 273 } 274 othersets := i.Node.Attr("use-attribute-sets") 275 if othersets != "" { 276 asets := strings.Fields(othersets) 277 for _, attsetname := range asets { 278 a := context.Style.LookupAttributeSet(attsetname) 279 if a != nil { 280 a.Apply(node, context) 281 } 282 } 283 } 284 case "fallback": 285 for _, c := range i.Children { 286 c.Apply(node, context) 287 } 288 case "otherwise": 289 for _, c := range i.Children { 290 c.Apply(node, context) 291 } 292 293 case "choose": 294 for _, c := range i.Children { 295 inst := c.(*XsltInstruction) 296 if inst.Node.Name() == "when" { 297 xp := xpath.Compile(inst.Node.Attr("test")) 298 if context.EvalXPathAsBoolean(node, xp) { 299 for _, wc := range inst.Children { 300 wc.Apply(node, context) 301 } 302 break 303 } 304 } else { 305 inst.Apply(node, context) 306 } 307 } 308 case "copy": 309 //i.copyToOutput(cur, context, false) 310 switch node.NodeType() { 311 case xml.XML_TEXT_NODE: 312 if context.UseCDataSection(context.OutputNode) { 313 r := context.Output.CreateCDataNode(node.Content()) 314 context.OutputNode.AddChild(r) 315 } else { 316 r := context.Output.CreateTextNode(node.Content()) 317 context.OutputNode.AddChild(r) 318 } 319 case xml.XML_ATTRIBUTE_NODE: 320 aname := node.Name() 321 ahref := node.Namespace() 322 val := node.Content() 323 if ahref == "" { 324 context.OutputNode.SetAttr(aname, val) 325 } else { 326 context.OutputNode.SetNsAttr(ahref, aname, val) 327 } 328 case xml.XML_COMMENT_NODE: 329 r := context.Output.CreateCommentNode(node.Content()) 330 context.OutputNode.AddChild(r) 331 case xml.XML_PI_NODE: 332 name := node.Name() 333 r := context.Output.CreatePINode(name, node.Content()) 334 context.OutputNode.AddChild(r) 335 case xml.XML_ELEMENT_NODE: 336 aname := node.Name() 337 r := context.Output.CreateElementNode(aname) 338 context.OutputNode.AddChild(r) 339 ns := node.Namespace() 340 if ns != "" { 341 //TODO: search through namespaces in-scope 342 prefix, _ := context.Style.NamespaceMapping[ns] 343 r.SetNamespace(prefix, ns) 344 } 345 346 //copy namespace declarations 347 for _, decl := range node.DeclaredNamespaces() { 348 r.DeclareNamespace(decl.Prefix, decl.Uri) 349 } 350 351 old := context.OutputNode 352 context.OutputNode = r 353 354 attsets := i.Node.Attr("use-attribute-sets") 355 if attsets != "" { 356 asets := strings.Fields(attsets) 357 for _, attsetname := range asets { 358 a := context.Style.LookupAttributeSet(attsetname) 359 if a != nil { 360 a.Apply(node, context) 361 } 362 } 363 } 364 for _, c := range i.Children { 365 c.Apply(node, context) 366 } 367 context.OutputNode = old 368 } 369 case "for-each": 370 scope := i.Node.Attr("select") 371 e := xpath.Compile(scope) 372 context.RegisterXPathNamespaces(i.Node) 373 nodes, _ := context.EvalXPathAsNodeset(node, e) 374 if i.sorting != nil { 375 i.Sort(nodes, context) 376 } 377 total := len(nodes) 378 old_curr := context.Current 379 for j, cur := range nodes { 380 context.PushStack() 381 context.XPathContext.SetContextPosition(j+1, total) 382 context.Current = cur 383 for _, c := range i.Children { 384 c.Apply(cur, context) 385 switch v := c.(type) { 386 case *Variable: 387 _ = context.DeclareLocalVariable(v.Name, "", v) 388 } 389 } 390 context.PopStack() 391 } 392 context.Current = old_curr 393 case "copy-of": 394 scope := i.Node.Attr("select") 395 e := xpath.Compile(scope) 396 context.RegisterXPathNamespaces(i.Node) 397 nodes, _ := context.EvalXPathAsNodeset(node, e) 398 total := len(nodes) 399 for j, cur := range nodes { 400 context.XPathContext.SetContextPosition(j+1, total) 401 i.copyToOutput(cur, context, true) 402 } 403 404 case "message": 405 val, _ := i.evalChildrenAsText(node, context) 406 terminate := i.Node.Attr("terminate") 407 if terminate == "yes" { 408 //TODO: fixup error flow to terminate more gracefully 409 panic(val) 410 } else { 411 fmt.Println(val) 412 } 413 case "apply-imports": 414 fmt.Println("TODO handle xsl:apply-imports instruction") 415 default: 416 hasFallback := false 417 for _, c := range i.Children { 418 switch v := c.(type) { 419 case *XsltInstruction: 420 if v.Name == "fallback" { 421 c.Apply(node, context) 422 hasFallback = true 423 break 424 } 425 } 426 } 427 if !hasFallback { 428 fmt.Println("UNKNOWN instruction ", i.Name) 429 } 430 } 431 } 432 433 func (i *XsltInstruction) numbering(node xml.Node, context *ExecutionContext) { 434 //level 435 level := i.Node.Attr("level") 436 if level == "" { 437 level = "single" 438 } 439 //count 440 count := i.Node.Attr("count") 441 if count == "" { 442 //TODO: qname (should match NS as well 443 count = node.Name() 444 } 445 //from 446 from := i.Node.Attr("from") 447 //value 448 valattr := i.Node.Attr("value") 449 //format 450 format := i.Node.Attr("format") 451 if format == "" { 452 format = "1" 453 } 454 //lang 455 //letter-value 456 //grouping-seperator 457 //grouping-size 458 459 var numbers []int 460 //if value, just use that! 461 if valattr != "" { 462 v, _ := node.EvalXPath(valattr, context) 463 if v == nil { 464 numbers = append(numbers, 0) 465 } else { 466 numbers = append(numbers, int(v.(float64))) 467 } 468 } else { 469 470 target := findTarget(node, count) 471 v := countNodes(level, target, count, from) 472 numbers = append(numbers, v) 473 474 if level == "multiple" { 475 for cur := target.Parent(); cur != nil; cur = cur.Parent() { 476 v = countNodes(level, cur, count, from) 477 if v > 0 { 478 numbers = append(numbers, v) 479 } 480 } 481 if len(numbers) > 1 { 482 for i, j := 0, len(numbers)-1; i < j; i, j = i+1, j-1 { 483 numbers[i], numbers[j] = numbers[j], numbers[i] 484 } 485 } 486 } 487 } 488 489 // level = multiple 490 // count preceding siblings AT EACH LEVEL 491 492 // format using the format string 493 outtxt := formatNumbers(numbers, format) 494 r := context.Output.CreateTextNode(outtxt) 495 context.OutputNode.AddChild(r) 496 } 497 498 func (i *XsltInstruction) copyToOutput(node xml.Node, context *ExecutionContext, recursive bool) { 499 switch node.NodeType() { 500 case xml.XML_TEXT_NODE: 501 if context.UseCDataSection(context.OutputNode) { 502 r := context.Output.CreateCDataNode(node.Content()) 503 context.OutputNode.AddChild(r) 504 } else { 505 r := context.Output.CreateTextNode(node.Content()) 506 context.OutputNode.AddChild(r) 507 } 508 case xml.XML_ATTRIBUTE_NODE: 509 aname := node.Name() 510 ahref := node.Namespace() 511 val := node.Content() 512 if ahref == "" { 513 context.OutputNode.SetAttr(aname, val) 514 } else { 515 context.OutputNode.SetNsAttr(ahref, aname, val) 516 } 517 case xml.XML_COMMENT_NODE: 518 r := context.Output.CreateCommentNode(node.Content()) 519 context.OutputNode.AddChild(r) 520 case xml.XML_PI_NODE: 521 name := node.Attr("name") 522 r := context.Output.CreatePINode(name, node.Content()) 523 context.OutputNode.AddChild(r) 524 case xml.XML_NAMESPACE_DECL: 525 //in theory this should work 526 //in practice it's a little complicated due to the fact 527 //that namespace declarations don't map to the node type 528 //very well 529 //will need to revisit 530 //context.OutputNode.DeclareNamespace(node.Name(), node.Content()) 531 case xml.XML_ELEMENT_NODE: 532 aname := node.Name() 533 r := context.Output.CreateElementNode(aname) 534 context.OutputNode.AddChild(r) 535 ns := node.Namespace() 536 if ns != "" { 537 //TODO: search through namespaces in-scope 538 prefix, _ := context.Style.NamespaceMapping[ns] 539 r.SetNamespace(prefix, ns) 540 } else { 541 //may need to explicitly reset to empty namespace 542 def := context.DefaultNamespace(context.OutputNode) 543 if def != "" { 544 r.SetNamespace("", "") 545 } 546 } 547 548 //copy namespace declarations 549 for _, decl := range node.DeclaredNamespaces() { 550 r.DeclareNamespace(decl.Prefix, decl.Uri) 551 } 552 553 old := context.OutputNode 554 context.OutputNode = r 555 if recursive { 556 //copy attributes 557 for _, attr := range node.AttributeList() { 558 i.copyToOutput(attr, context, recursive) 559 } 560 for cur := node.FirstChild(); cur != nil; cur = cur.NextSibling() { 561 i.copyToOutput(cur, context, recursive) 562 } 563 } 564 context.OutputNode = old 565 case xml.XML_DOCUMENT_NODE: 566 if recursive { 567 for cur := node.FirstChild(); cur != nil; cur = cur.NextSibling() { 568 i.copyToOutput(cur, context, recursive) 569 } 570 } 571 } 572 }