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  }