-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathHTMLElementComposer.wry.txt
370 lines (298 loc) · 12.2 KB
/
HTMLElementComposer.wry.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
project : HTML Element Composer
author : Mike Weaver
created : 2018-06-13
copyright : © 2018 All rights reserved.
license : See [*License].
section : ToDo
One thing I added to wCommon/Composer is the ability to generate and then
store arbitrary HTML with keys, then recall them for composition into the
composer.
section : Introduction
An HTML document is built using HTML elements. Elements include start and end
tags, attributes and content. Valid content includes text and child elements,
interspersed and nested to arbitrary levels. See {W3C HTML
syntax}[*elem-url].
elem-url : https://www.w3.org/TR/html5/syntax.html
HTMLElementComposer, or Composer for short, is an object to help compose
proper HTML. Composer keeps track of nested elements. It can indent the
output for easier reading or debugging by humans. For some quick examples,
see the [*Tests] section.
Composer deals with HTML at a low level: tags, attributes, and content. It is
designed such that other objects can inherit and provide more specific
functionality. For example, a FormBuilder object could use Composer to deal
with low-level HTML details, and provide a higher level API for `addButton`,
`addInputField`, etc.
section : Creating an element
The core work of Composer is creating elements. The `getElement` function
below takes an element name, attributes, and content and returns a string of
the composed HTML element. Attributes and content are optional. By default, a
closing tag is not generated and elements are left open in anticipation of
subsequent content. Pass `close=true` to change that behavior and close the
element with an end tag. Note that {void elements}[*Void elements] take no
content and are always closed with ' \>' regardless of the flag.
!!! : Table of arguments
Should put a table in here for the arguments of `getElement`. It will make
it easier to see at a glance what is going on.
The arguments to `getElement` should be valid HTML but the function does not
enforce that. It transforms the keys and values in the `attribs` argument
into HTML attributes, wrapping the values in double quotes. Those values
should not themselves contain unescaped double quotes. The `content` argument
will be composed untouched as the HTML element content.
`getElement` is a true function. It does not change any state or store any
output, it simply returns a string of HTML. Notably, it does not keep track
of previously opened elements and thus has no mechanism to close them. All of
that will be handled with the next batch of functions.
>>> Element functions
func getElement ///([elem], attribs=(,), content='', close=false)
$arg.elem ?= $arg[0]
guard elem
return
'<\{elem}'
for attribs
' \{$key}="\{$val}"'
if isEmptyElement(elem) // See [*Note static]
' />'
else
'>'
content
if close
'</\{elem}>'
return $loc->concat() // See [*Note loc concat]
<<<
note : Note static
Functions `getElement` and `isEmptyElement` operate somewhat like "static"
methods, in that they don't technically need an object context and don't
change any state. It is assumed, however, that they will be called by the
other methods of HTMLElementComposer and so will automatically inherit an
object scope. That object scope is necessary in order to resolve the call
above to `isEmptyElement`.
note : Note loc concat
The `getElement` function, as with the `getIndent` function below, builds
up its return value by adding expressions to the local context. This can
get tricky if the code includes setting variables, which would then also
be in the local context and included in the final concat. To avoid this,
we set values in a different context (typically $arg) so that local
contains only our desired results expressions. Another way to achieve this
is to use the `_` prefix on any local variables, which `concat` will hide
by default.
!!!
For this to work, `key` and `val` can't stick around in $loc. Maybe for
loops these two live in a separate scope that sits in front of $loc
just for the duration of the loop, and assignment still happens in $loc
(not in the temporary loop scope).
The next set of functions comprise the main API of Composer. They use
`getElement` defined above to generate output, which is stored internally;
and they manage nested elements, which are tracked in an internal stack.
These functions are:
* `beginElement`
* `endElement`
* `addElement`
* `addCustom`
* `getHTML`
Open an element with a call to `beginElement` and close it with a matching
call to `endElement`. For an element that doesn't (or can't) take children,
add the entire element, including start and end tag, with a call to
`addElement`. Add arbitrary HTML by calling `addCustom`. All these calls
accumulate output in an internal store. Retrieve the final HTML, and reset
the internal store, with a call to `getHTML`.
It is common to have 'class' as the only attribute on an HTML element. Rather
than embed a 'class' key-value pair as the only item in the `attribs` array,
it can be passed on its own with the `class` parameter to `beginElement` and
`addElement`. This cleans up the call site for a common scenario. The
following two lines are equivalent.
```
composer->addElement('p', attribs=(class='greet'), content='Hello World!')
composer->addElement('p', class='greet', content='Hello World!')
Note the functions below have placeholders for handling indentation, which
will be dealt with later in this document.
<<< Element functions
func beginElement ///([elem], class=nil, attribs=(,), content='')
$arg.elem ?= $arg[0]
guard elem
return
if isEmptyElement(elem) with
'`beginElement` called with void element "\{elem}".'
'Use `addElement` instead.'
throw(msg=$loc->concat(sep=" "))
if class
attribs['class'] = class
tagStack->push(elem) // See [*Note writing variables]
<*Handle indentation>
$obj.store[] = getElement(elem, ~attribs, ~content)
func endElement
elem = tagStack->pop()
guard elem
throw (msg='Too many calls to endElement')
<*Handle indentation>
$obj.store[] = '</\{elem}>'
func addElement // ([elem], class=nil, attribs=(), content='')
$arg.elem ?= $arg[0]
guard elem
return
if class
attribs['class'] = class
<*Handle indentation>
$obj.store[] = getElement(elem, ~attribs, ~content, close: true)
func addCustom // ([string])
$arg.string ?= $arg[0]
$obj.store[] = string
func getHTML // (reset=true)
guard tagStack->isEmpty()
throw (msg='There are unclosed elements')
reset ?= true
defer if reset
$obj.store = (,)
return store->concat()
>>>
note : Note writing variables
Earlier version had all the references to `tagStack` and `store` prefixed
with explicit `$obj` context. But that shouldn't be necessary. Assigning a
new value to an object would definitely need the $obj prefix, (like
resetting `store` in `getHTML`) or else the variable would just be created
in `$loc`. But if we refer to an existing variable and then chain it to a
function call or use subscripting, that object reference has to be
resolved and once resolved the found object can be changed directly.
!!!
Is that still true with subscripting with empty brackets? That sort of
assignment could just as easily generate a new variable in $loc scope.
One upshot of this is you can't mask a variable in the $obj scope by
setting it to null in the $loc scope.
```
func
tagStack = null
tagStack->print()
If `tagStack` exists in the $obj scope (assume it does in the example
above) then it will get printed. Setting it to null does so in the $loc
scope, but does not affect anything in the $obj scope. To truly delete
it, you could either include the $obj scope as you set to null, or
explicitly reference $loc scope.
```
func
$obj.tagStack = null
tagStack->print()
```
func
$loc.tagStack = null
$loc.tagStack->print()
!!!
(2018-08-15) I think with empty brackets, the action should be to edit
an object in the nearest scope (creating it if necessary) so the
references to `store` should all be scoped (or qualified, or whatever
term we come up with). Still thinking about what happens when you chain
an unscoped object, like `tagStack` and what that means in terms of
editing it.
section : HTMLElementComposer object
Here is the overall structure of the HTMLElementComposer. The
{element functions}[*Element functions] have already been defined, and the
remaining pieces will be defined below.
<<< HTMLElementComposer
HTMLElementComposer
<*Internal variables>
<*Element functions>
<*Indentation>
<*Empty elements>
>>>
The element functions rely on internal variables to keep track of nested
elements and accumulated output. Those variables have already been referenced
by the functions, with the names `tagStack` and `store`, respectively. They
are defined next.
!!!
Technically in Wry it is not necessary to set these variables when their
starting value is nil or nil-equivalent. But it is good practice to be
explicit, and IDE's can pick up on the variable names and provide
autocomplete suggestions.
<<< Internal variables
store = ''
tagStack = (,)
>>>
section : Indentation
Most white space is ignored when parsing HTML, indentation is a convenience
for human readers, and can be especially helpful when debugging. Composer
will indent the output HTML if the flag `fIndent` is set to true. By default
it uses a tab character to indent, but that can be changed by setting the
`indentChar` to something else.
<<< Internal variables
fIndent = true
indentChar = "\t"
>>>
The following function derives the indentation level from the level of nested
elements and returns a string of repeated `indentChar`'s.
<<< Indentation
func getIndent
guard fIndent
return
if store // Add a newline if `store` contains existing output.
'\n'
do _i = 0 if _i < tagStack->count()
indentChar
continue _i += 1
return $loc->concat()
>>>
With the `getIndentation` function defined, the placeholder for handling
indentation back in the {element functions}[*Element functions] can now be
completed.
<<< Handle indentation
$obj.store[] = getIndent()
>>>
section : Void elements
Void elements take no content. See {HTML5 void elements}[*void-elem]. The
`getElement` function will automatically close them. To do so, the array
`empty` contains the void elements defined for HTML5 (as keys) and the
`isEmptyElement` function returns true if the supplied `elem` is in the
`empty` list; false otherwise.
void-elem : https://www.w3.org/TR/html5/syntax.html#void-elements
<<< Empty Elements
empty
'area' : true
'base' : true
'br' : true
'col' : true
'embed' : true
'hr' : true
'img' : true
'input' : true
'link' : true
'meta' : true
'param' : true
'source' : true
'track' : true
'wbr' : true
func isEmptyElement // ([elem])
$arg.elem ?= $arg[0]
return empty[elem]
test // empty elements
isEmptyElement('meta') == true
isEmptyElement('div') == false
>>>
section : Tests
<<< HTMLElementComposer
test // `getElement` function
$tst.cp = HTMLElementComposer
cp->getElement('img', attribs=(src='/logo')) == '<img src="/logo" />'
$tst.msg = 'Hello world!'
cp->getElement('p', attribs=(class='g'), content=msg, close=true) \
== '<p class="g">' + msg + 'Hello world!</p>'
$tst.attr = (class='a', id='23')
cp->getElement('div', attribs=attr) == '<div class="a" id="23">'
test // adding elements
$tst.cp = HTMLElementComposer
cp->addElement('p', content='Hello World!')
cp->addElement('p' content='Goodbye')
cp->getHTML() == "<p>Hello World!</p>\n<p>Goodbye</p>"
test // nesting elements
$tst.cp = HTMLElementComposer
cp->beginElement('div', class='z')
cp->beginElement('p')
cp->addCustom('welcome')
cp->endElement()
cp->endElement()
$tst.expectedResult
'<div class="z">'
"\n" + cp.indentChar
'<p>welcome'
"\n" + cp.indentChar
'</p>'
"\n"
'</div>''
cp->getHTML() == expectedResult
>>>