1# -*- tcl -*-
2# [expand] utilities for generating XML.
3#
4# Copyright (C) 2001 Joe English <jenglish@sourceforge.net>.
5# Freely redistributable.
6#
7# Copyright (C) 2019 Andreas Kupries <andreas_kupries@sourceforge.net>
8######################################################################
9
10# Handling XML delimiters in content:
11#
12# Plain text is initially passed through unescaped;
13# internally-generated markup is protected by preceding it with \1.
14# The final PostProcess step strips the escape character from
15# real markup and replaces markup characters from content
16# with entity references.
17#
18
19variable attvalMap { {&} &amp;  {<} &lt;  {>} &gt; {"} &quot; {'} &apos; } ; # "
20variable markupMap { {&} {\1&}  {<} {\1<}  {>} {\1>} }
21variable finalMap  { {\1&} {&}  {\1<} {<}  {\1>} {>}
22		     {&} &amp;  {<} &lt;   {>} &gt; }
23
24proc fmt_postprocess {text} {
25    variable finalMap
26    return [string trim [string map $finalMap $text]]\n
27}
28
29# markup text --
30#	Protect markup characters in $text with \1.
31#	These will be stripped out in PostProcess.
32#
33proc markup {text} {
34    variable markupMap
35    return [string map $markupMap $text]
36}
37
38# attlist { n1 v1 n2 v2 ... } --
39#	Return XML-formatted attribute list.
40#	Does *not* escape markup -- the result must be passed through
41#	[markup] before returning it to the expander.
42#
43proc attlist {nvpairs} {
44    variable attvalMap
45    if {[llength $nvpairs] == 1} { set nvpairs [lindex $nvpairs 0] }
46    set attlist ""
47    foreach {name value} $nvpairs {
48    	append attlist " $name='[string map $attvalMap $value]'"
49    }
50    return $attlist
51}
52
53# startTag gi ?attname attval ... ? --
54#	Return start-tag for element $gi with specified attributes.
55#
56proc startTag {gi args} {
57    return [markup "<$gi[attlist $args]>"]
58}
59
60# endTag gi --
61#	Return end-tag for element $gi.
62#
63proc endTag {gi} {
64    return [markup "</$gi>"]
65}
66
67# emptyElement gi ?attribute  value ... ?
68#	Return empty-element tag.
69#
70proc emptyElement {gi args} {
71    return [markup "<$gi[attlist $args]/>"]
72}
73
74# xmlComment text --
75#	Return XML comment declaration containing $text.
76#	NB: if $text includes the sequence "--", it will be mangled.
77#
78proc xmlComment {text} {
79    return [markup "<!-- [string map {-- { - - }} $text] -->"]
80}
81
82# wrap content gi --
83#	Returns $content wrapped inside <$gi> ... </$gi> tags.
84#
85proc wrap {content gi} {
86    return "[startTag $gi]${content}[endTag $gi]"
87}
88
89# wrap? content gi --
90#	Same as [wrap], but returns an empty string if $content is empty.
91#
92proc wrap? {content gi} {
93    if {![string length [string trim $content]]} { return "" }
94    return "[startTag $gi]${content}[endTag $gi]"
95}
96
97# wrapLines? content gi ? gi... ?
98#	Same as [wrap?], but separates entries with newlines
99#       and supports multiple nesting levels.
100#
101proc wrapLines? {content args} {
102    if {![string length $content]} { return "" }
103    foreach gi $args {
104	set content [join [list [startTag $gi] $content [endTag $gi]] "\n"]
105    }
106    return $content
107}
108
109# sequence args --
110#	Handy combinator.
111#
112proc sequence {args} { join $args "\n" }
113
114######################################################################
115# XML context management.
116#
117
118variable elementStack [list]
119
120# start gi ?attribute value ... ? --
121#	Return start-tag for element $gi
122#	As a side-effect, pushes $gi onto the element stack.
123#
124proc start {gi args} {
125    if {[llength $args] == 1} { set args [lindex $args 0] }
126    variable elementStack
127    lappend elementStack $gi
128    return [startTag $gi $args]
129}
130
131# xmlContext {gi1 ... giN} ?default?  --
132#	Pops elements off the element stack until one of
133#	the specified element types is found.
134#
135#	Returns: sequence of end-tags for each element popped.
136#
137#	If none of the specified elements are found, returns
138# 	a start-tag for $default.
139#
140proc xmlContext {gis {default {}}} {
141    variable elementStack
142    set origStack $elementStack
143    set endTags [list]
144    while {[llength $elementStack]} {
145	set current [lindex $elementStack end]
146	if {[lsearch $gis $current] >= 0} {
147	    return [join $endTags \n]
148	}
149	lappend endTags [endTag $current]
150	set elementStack [lreplace $elementStack end end]
151    }
152    # Not found:
153    set elementStack $origStack
154    if {![string length $default]} {
155    	set where "[join $elementStack /] - [info level 1]"
156	puts_stderr "Warning: Cannot start context $gis ($where)"
157    	set default [lindex $gis 0]
158    }
159    lappend elementStack $default
160    return [startTag $default]
161}
162
163# end ? gi ? --
164#	Generate markup to close element $gi, including end-tags
165#	for any elements above it on the element stack.
166#
167#	If element name is omitted, closes the current element.
168#
169proc end {{gi {}}} {
170    variable elementStack
171    if {![string length $gi]} {
172    	set gi [lindex $elementStack end]
173    }
174    set prefix [xmlContext $gi]
175    set elementStack [lreplace $elementStack end end]
176    return [join [list $prefix [endTag $gi]] "\n"]
177}
178
179######################################################################
180# Utilities for multi-pass processing.
181#
182# Not really XML-related, but I find them handy.
183#
184
185variable PassProcs
186variable Buffers
187
188# pass $passNo procName procArgs { body  } --
189#	Specifies procedure definition for pass $n.
190#
191proc pass {pass proc arguments body} {
192    variable PassProcs
193    lappend PassProcs($pass) $proc $arguments $body
194}
195
196proc setPassProcs {pass} {
197    variable PassProcs
198    foreach {proc args body} $PassProcs($pass) {
199	proc $proc $args $body
200    }
201}
202
203# holdBuffers buffer ? buffer ...? --
204#	Declare a list of hold buffers,
205#	to collect data in one pass and output it later.
206#
207proc holdBuffers {args} {
208    variable Buffers
209    foreach arg $args {
210	set Buffers($arg) [list]
211    }
212}
213
214# hold buffer text --
215#	Append text to named buffer
216#
217proc hold {buffer entry} {
218    variable Buffers
219    lappend Buffers($buffer) $entry
220    return
221}
222
223# held buffer --
224#	Returns current contents of named buffer and empty the buffer.
225#
226proc held {buffer} {
227    variable Buffers
228    set content [join $Buffers($buffer) "\n"]
229    set Buffers($buffer) [list]
230    return $content
231}
232
233#*EOF*
234