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  }