github.com/matislovas/ratago@v0.0.0-20240408115641-cc0857415a7a/xslt/context.go (about) 1 package xslt 2 3 import ( 4 "container/list" 5 "errors" 6 "fmt" 7 "path/filepath" 8 "strings" 9 "unsafe" 10 11 "github.com/matislovas/gokogiri/xml" 12 "github.com/matislovas/gokogiri/xpath" 13 ) 14 15 // ExecutionContext is passed to XSLT instructions during processing. 16 type ExecutionContext struct { 17 Style *Stylesheet // The master stylesheet 18 Output xml.Document // The output document 19 Source xml.Document // The source input document 20 OutputNode xml.Node // The current output node 21 Current xml.Node // The node that will be returned for "current()" 22 XPathContext *xpath.XPath //the XPath context 23 Mode string //The current template mode 24 Stack list.List //stack used for scoping local variables 25 InputDocuments map[string]*xml.XmlDocument //additional input documents via document() 26 } 27 28 func (context *ExecutionContext) EvalXPath(xmlNode xml.Node, data interface{}) (result interface{}, err error) { 29 switch data := data.(type) { 30 case string: 31 if xpathExpr := xpath.Compile(data); xpathExpr != nil { 32 defer xpathExpr.Free() 33 result, err = context.EvalXPath(xmlNode, xpathExpr) 34 } else { 35 err = errors.New("cannot compile xpath: " + data) 36 } 37 case []byte: 38 result, err = context.EvalXPath(xmlNode, string(data)) 39 case *xpath.Expression: 40 xpathCtx := context.XPathContext 41 xpathCtx.SetResolver(context) 42 err := xpathCtx.Evaluate(xmlNode.NodePtr(), data) 43 if err != nil { 44 return nil, err 45 } 46 rt := xpathCtx.ReturnType() 47 switch rt { 48 case xpath.XPATH_NODESET, xpath.XPATH_XSLT_TREE: 49 nodePtrs, err := xpathCtx.ResultAsNodeset() 50 if err != nil { 51 return nil, err 52 } 53 var output []xml.Node 54 for _, nodePtr := range nodePtrs { 55 output = append(output, xml.NewNode(nodePtr, xmlNode.MyDocument())) 56 } 57 result = output 58 case xpath.XPATH_NUMBER: 59 result, err = xpathCtx.ResultAsNumber() 60 case xpath.XPATH_BOOLEAN: 61 result, err = xpathCtx.ResultAsBoolean() 62 default: 63 result, err = xpathCtx.ResultAsString() 64 } 65 default: 66 err = errors.New("Strange type passed to ExecutionContext.EvalXPath") 67 } 68 return 69 } 70 71 // Register the namespaces in scope with libxml2 so that XPaths with namespaces 72 // are resolved correctly. 73 // 74 // libxml2 probably already makes this info available 75 func (context *ExecutionContext) RegisterXPathNamespaces(node xml.Node) (err error) { 76 seen := make(map[string]bool) 77 for n := node; n != nil; n = n.Parent() { 78 for _, decl := range n.DeclaredNamespaces() { 79 alreadySeen, _ := seen[decl.Prefix] 80 if !alreadySeen { 81 context.XPathContext.RegisterNamespace(decl.Prefix, decl.Uri) 82 seen[decl.Prefix] = true 83 } 84 } 85 } 86 return 87 } 88 89 // Attempt to map a prefix to a URI. 90 func (context *ExecutionContext) LookupNamespace(prefix string, node xml.Node) (uri string) { 91 //if given a context node, see if the prefix is in scope 92 if node != nil { 93 for n := node; n != nil; n = n.Parent() { 94 for _, decl := range n.DeclaredNamespaces() { 95 if decl.Prefix == prefix { 96 return decl.Uri 97 } 98 } 99 } 100 return 101 } 102 103 //if no context node, simply check the stylesheet map 104 for href, pre := range context.Style.NamespaceMapping { 105 if pre == prefix { 106 return href 107 } 108 } 109 return 110 } 111 112 func (context *ExecutionContext) EvalXPathAsNodeset(xmlNode xml.Node, data interface{}) (result xml.Nodeset, err error) { 113 _, err = context.EvalXPath(xmlNode, data) 114 if err != nil { 115 return nil, err 116 } 117 nodePtrs, err := context.XPathContext.ResultAsNodeset() 118 if err != nil { 119 return nil, err 120 } 121 var output xml.Nodeset 122 for _, nodePtr := range nodePtrs { 123 output = append(output, xml.NewNode(nodePtr, xmlNode.MyDocument())) 124 } 125 result = output 126 return 127 } 128 129 func (context *ExecutionContext) EvalXPathAsBoolean(xmlNode xml.Node, data interface{}) (result bool) { 130 _, err := context.EvalXPath(xmlNode, data) 131 if err != nil { 132 return false 133 } 134 result, _ = context.XPathContext.ResultAsBoolean() 135 return 136 } 137 138 func (context *ExecutionContext) EvalXPathAsString(xmlNode xml.Node, data interface{}) (result string, err error) { 139 _, err = context.EvalXPath(xmlNode, data) 140 if err != nil { 141 return 142 } 143 result, err = context.XPathContext.ResultAsString() 144 return 145 } 146 147 // ChildrenOf returns the node children, ignoring any whitespace-only text nodes that 148 // are stripped by strip-space or xml:space 149 func (context *ExecutionContext) ChildrenOf(node xml.Node) (children []xml.Node) { 150 151 for cur := node.FirstChild(); cur != nil; cur = cur.NextSibling() { 152 //don't count stripped nodes 153 if context.ShouldStrip(cur) { 154 continue 155 } 156 children = append(children, cur) 157 } 158 return 159 } 160 161 // ShouldStrip evaluates the strip-space, preserve-space, and xml:space rules 162 // and returns true if a node is a whitespace-only text node that should 163 // be stripped. 164 func (context *ExecutionContext) ShouldStrip(xmlNode xml.Node) bool { 165 if xmlNode.NodeType() != xml.XML_TEXT_NODE { 166 return false 167 } 168 if !IsBlank(xmlNode) { 169 return false 170 } 171 //do we have a match in strip-space? 172 elem := xmlNode.Parent().Name() 173 ns := xmlNode.Parent().Namespace() 174 for _, pat := range context.Style.StripSpace { 175 if pat == elem { 176 return true 177 } 178 if pat == "*" { 179 return true 180 } 181 if strings.Contains(pat, ":") { 182 uri, name := context.ResolveQName(pat) 183 if uri == ns { 184 if name == elem || name == "*" { 185 return true 186 } 187 } 188 } 189 } 190 //do we have a match in preserve-space? 191 //resolve conflicts by priority (QName, ns:*, *) 192 //return a value 193 return false 194 } 195 196 func (context *ExecutionContext) ResolveQName(qname string) (ns, name string) { 197 if !strings.Contains(qname, ":") { 198 //TODO: lookup default namespace 199 return "", name 200 } 201 parts := strings.Split(qname, ":") 202 for uri, prefix := range context.Style.NamespaceMapping { 203 if prefix == parts[0] { 204 return uri, parts[1] 205 } 206 } 207 return 208 } 209 210 func (context *ExecutionContext) UseCDataSection(node xml.Node) bool { 211 if node.NodeType() != xml.XML_ELEMENT_NODE { 212 return false 213 } 214 name := node.Name() 215 ns := node.Namespace() 216 for _, el := range context.Style.CDataElements { 217 if el == name { 218 return true 219 } 220 uri, elname := context.ResolveQName(el) 221 if uri == ns && name == elname { 222 return true 223 } 224 } 225 return false 226 } 227 228 func (context *ExecutionContext) ResolveVariable(name, ns string) (ret interface{}) { 229 v := context.FindVariable(name, ns) 230 231 if v == nil { 232 return 233 } 234 235 switch val := v.Value.(type) { 236 case xml.Nodeset: 237 return unsafe.Pointer(val.ToXPathNodeset()) 238 case []xml.Node: 239 nodeset := xml.Nodeset(val) 240 return unsafe.Pointer(nodeset.ToXPathNodeset()) 241 default: 242 return val 243 } 244 } 245 246 func (context *ExecutionContext) FindVariable(name, ns string) (ret *Variable) { 247 //consult local vars 248 //consult local params 249 v := context.LookupLocalVariable(name, ns) 250 if v != nil { 251 return v 252 } 253 //consult global vars (ss) 254 //consult global params (ss) 255 v, ok := context.Style.Variables[name] 256 if ok { 257 return v 258 } 259 return nil 260 } 261 262 func (context *ExecutionContext) DeclareLocalVariable(name, ns string, v *Variable) error { 263 if context.Stack.Len() == 0 { 264 return errors.New("Attempting to declare a local variable without a stack frame") 265 } 266 e := context.Stack.Front() 267 scope := e.Value.(map[string]*Variable) 268 scope[name] = v 269 //fmt.Println("DECLARE", name, v) 270 return nil 271 } 272 273 func (context *ExecutionContext) LookupLocalVariable(name, ns string) (ret *Variable) { 274 for e := context.Stack.Front(); e != nil; e = e.Next() { 275 scope := e.Value.(map[string]*Variable) 276 v, ok := scope[name] 277 if ok { 278 //fmt.Println("FOUND", name, v) 279 return v 280 } 281 } 282 return 283 } 284 285 // create a local scope for variable resolution 286 func (context *ExecutionContext) PushStack() { 287 scope := make(map[string]*Variable) 288 context.Stack.PushFront(scope) 289 } 290 291 // leave the variable scope 292 func (context *ExecutionContext) PopStack() { 293 if context.Stack.Len() == 0 { 294 return 295 } 296 context.Stack.Remove(context.Stack.Front()) 297 } 298 299 func (context *ExecutionContext) IsFunctionRegistered(name, ns string) bool { 300 qname := fmt.Sprintf("{%s}%s", ns, name) 301 _, ok := context.Style.Functions[qname] 302 return ok 303 } 304 305 func (context *ExecutionContext) ResolveFunction(name, ns string) xpath.XPathFunction { 306 qname := fmt.Sprintf("{%s}%s", ns, name) 307 f, ok := context.Style.Functions[qname] 308 if ok { 309 return f 310 } 311 return nil 312 } 313 314 // Determine the default namespace currently defined in scope 315 func (context *ExecutionContext) DefaultNamespace(node xml.Node) string { 316 //get the list of in-scope namespaces 317 // any with a null prefix? return that 318 decl := node.DeclaredNamespaces() 319 for _, d := range decl { 320 if d.Prefix == "" { 321 return d.Uri 322 } 323 } 324 return "" 325 } 326 327 // Propagate namespaces to the root of the output document 328 func (context *ExecutionContext) DeclareStylesheetNamespacesIfRoot(node xml.Node) { 329 if context.OutputNode.NodeType() != xml.XML_DOCUMENT_NODE { 330 return 331 } 332 //add all namespace declarations to r 333 for uri, prefix := range context.Style.NamespaceMapping { 334 if uri != XSLT_NAMESPACE { 335 //these don't actually change if there is no alias 336 _, uri = ResolveAlias(context.Style, prefix, uri) 337 if !context.Style.IsExcluded(prefix) { 338 node.DeclareNamespace(prefix, uri) 339 } 340 } 341 } 342 } 343 344 func (context *ExecutionContext) FetchInputDocument(loc string, relativeToSource bool) (doc *xml.XmlDocument) { 345 //create the map if needed 346 if context.InputDocuments == nil { 347 context.InputDocuments = make(map[string]*xml.XmlDocument) 348 } 349 350 // rely on caller to tell us how to resolve relative paths 351 base := "" 352 if relativeToSource { 353 base, _ = filepath.Abs(filepath.Dir(context.Source.Uri())) 354 } else { 355 base, _ = filepath.Abs(filepath.Dir(context.Style.Doc.Uri())) 356 } 357 resolvedLoc := filepath.Join(base, loc) 358 359 //if abspath in map return existing document 360 doc, ok := context.InputDocuments[resolvedLoc] 361 if ok { 362 return 363 } 364 365 //else load the document and add to map 366 doc, e := xml.ReadFile(resolvedLoc, xml.StrictParseOption) 367 if e != nil { 368 fmt.Println(e) 369 return 370 } 371 context.InputDocuments[resolvedLoc] = doc 372 return 373 }