diff --git a/docs/parser/index.html b/docs/parser/index.html
index a9e1785..e3071f1 100644
--- a/docs/parser/index.html
+++ b/docs/parser/index.html
@@ -3,18 +3,15 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Parser
+
+
+
+
+
+
+
+
+
+
+ Parser
@@ -779,2 +776,3 @@ styleMap[propName] = propValue
-
-
+
+
+
Parser
Parser is the CSS parser for this project, it is made up of two primary functions ParseCSS
, ParseStyleAttribute
, and a few other functions designed to help with the parsing.
# ParseCSS?(go)
ParseCSS
is the function for reading CSS files. It is a RegExp based parser which it converts CSS definitions into a 2d map of strings. If you want to convert the values into absoulte values there are helper functions in the utils documentation.
matches := selectorRegex.FindAllStringSubmatch(css, -1)
First we start off by using a RegExp to find the individual CSS blocks and to sort them into the block selector and the styles for the selector.
selectors := parseSelectors(selectorBlock)
The mapped values are defined by the selector pulled from the parseSelectors function and will include the entire name (this includes the symbol ".","#", and ",")
selectorMap[selector] = parseStyles(styleBlock)
Once the selectors of the file have been parsed, the styles are mapped to the second level of the map with their respective key and value pulled from parseStyles.
NOTE: When parsing duplicate selectors and styles will be merged with the last conflicting selector/style overriding the prevous.
# Implementation
styles := parser.ParseCSS(string(dat))
The only time ParseCSS
is used is in the cstyle
package, and it used to add css files in the first example
styles := parser.ParseCSS(css)
and style tags in the next. As you can see in both examples those functions are for appending the new styles to the current global CSS stylesheet held within the instance of the CSS struct (CSS.StyleSheets
).
NOTE: Style tag is refering to the below
1<style>
2 table td.r,
3 table th.r {
4 text-align: center;
5 }
6</style>
# parseSelectors?(go)
parseSelectors
takes the first output of the RegExp match in ParseCSS and splits it up by commas.
# parseSelectors Example
1
2selectorBlock := `table td.r,
3table th.r`
4
5parseSelectors(selectorBlock)
6
7// Output
8[table td.r table th.r]
9
# parseStyles?(go)
styleRegex := regexp.MustCompile
parseStyles
takes the second output of the RegExp match in ParseCSS and splits it up using this RegExp:
styleMap := make(map[string]string)
for _, match := range matches {
propName := strings.TrimSpace(match[1])
propValue := strings.TrimSpace(match[2])
styleMap[propName] = propValue
}
It then takes the split styles and inserts them into a map[string]string
.
# parseStyles Example
1
2selectorBlock := `text-align: center;
3color: red;`
4
5parseStyles(selectorBlock)
6
7// Output
8map[string]string=map[text-align:center color:red]
9
# ParseStyleAttribute?(go)
inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
ParseStyleAttribute
is for parsing inline styles from elements in the html document on the inital load. It is also used to parse the local styles applied by the "script" via the .style
attribute. It will only be applied to a element.Node
's local styles and will not be add to the global stylesheets. It is used with the cstyle.GetStyles
function that is ran on every cycle.
# ParseStyleAttribute Example
1
2styleAttribute := "color:#f8f8f2;background-color:#272822;"
3
4ParseStyleAttribute(styleAttribute)
5
6//Output
7map[string]string=map[color:#f8f8f2 background-color:#272822]
8
# removeComments?(go)
1package parser
2
3import (
4 "regexp"
5 "strings"
6)
7
8func ParseCSS(css string) map[string]map[string]string {
9 selectorMap := make(map[string]map[string]string)
10
11 // Remove comments
12 css = removeComments(css)
13
14 // Parse regular selectors and styles
15 selectorRegex := regexp.MustCompile(`([^{]+){([^}]+)}`)
16 matches := selectorRegex.FindAllStringSubmatch(css, -1)
17
18 for _, match := range matches {
19 selectorBlock := strings.TrimSpace(match[1])
20 styleBlock := match[2]
21
22 selectors := parseSelectors(selectorBlock)
23 for _, selector := range selectors {
24 selectorMap[selector] = parseStyles(styleBlock)
25 }
26 }
27
28 return selectorMap
29}
30
31func parseSelectors(selectorBlock string) []string {
32 // Split by comma and trim each selector
33 selectors := strings.Split(selectorBlock, ",")
34 for i, selector := range selectors {
35 selectors[i] = strings.TrimSpace(selector)
36 }
37 return selectors
38}
39
40func parseStyles(styleBlock string) map[string]string {
41 styleRegex := regexp.MustCompile(`([a-zA-Z-]+)\s*:\s*([^;]+);`)
42 matches := styleRegex.FindAllStringSubmatch(styleBlock, -1)
43
44 styleMap := make(map[string]string)
45 for _, match := range matches {
46 propName := strings.TrimSpace(match[1])
47 propValue := strings.TrimSpace(match[2])
48 styleMap[propName] = propValue
49 }
50
51 return styleMap
52}
53
54func ParseStyleAttribute(styleValue string) map[string]string {
55 styleMap := make(map[string]string)
56
57 // Regular expression to match key-value pairs in the style attribute
58 re := regexp.MustCompile(`\s*([\w-]+)\s*:\s*([^;]+)\s*;`)
59
60 // Find all matches in the style attribute value
61 matches := re.FindAllStringSubmatch(styleValue, -1)
62
63 // Populate the map with key-value pairs
64 for _, match := range matches {
65 if len(match) == 3 {
66 key := strings.TrimSpace(match[1])
67 value := strings.TrimSpace(match[2])
68 styleMap[key] = value
69 }
70 }
71
72 return styleMap
73}
74
75func removeComments(css string) string {
76 commentRegex := regexp.MustCompile(`(?s)/\*.*?\*/`)
77 return commentRegex.ReplaceAllString(css, "")
78}
1package cstyle
2
3// package aui/goldie
4// https://pkg.go.dev/automated.sh/goldie
5// https://pkg.go.dev/automated.sh/aui
6// https://pkg.go.dev/automated.sh/oat
7
8import (
9 "crypto/md5"
10 "encoding/hex"
11 "fmt"
12 "gui/color"
13 "gui/element"
14 "gui/font"
15 "gui/parser"
16 "gui/utils"
17 "os"
18 "slices"
19 "sort"
20 "strconv"
21 "strings"
22
23 "golang.org/x/net/html"
24)
25
26type Plugin struct {
27 Styles map[string]string
28 Level int
29 Handler func(*element.Node)
30}
31
32type CSS struct {
33 Width float32
34 Height float32
35 StyleSheets []map[string]map[string]string
36 Plugins []Plugin
37 Document *element.Node
38}
39
40func (c *CSS) StyleSheet(path string) {
41 // Parse the CSS file
42 dat, err := os.ReadFile(path)
43 utils.Check(err)
44 styles := parser.ParseCSS(string(dat))
45
46 c.StyleSheets = append(c.StyleSheets, styles)
47}
48
49func (c *CSS) StyleTag(css string) {
50 styles := parser.ParseCSS(css)
51 c.StyleSheets = append(c.StyleSheets, styles)
52}
53
54func (c *CSS) CreateDocument(doc *html.Node) element.Node {
55 id := doc.FirstChild.Data + "0"
56 n := doc.FirstChild
57 node := element.Node{
58 Parent: &element.Node{
59 Properties: element.Properties{
60 Id: "ROOT",
61 X: 0,
62 Y: 0,
63 Width: c.Width,
64 Height: c.Height,
65 EM: 16,
66 Type: 3,
67 Node: &html.Node{Attr: []html.Attribute{
68 {Key: "Width", Val: fmt.Sprint(c.Width)},
69 {Key: "Height", Val: fmt.Sprint(c.Height)},
70 }},
71 },
72
73 Style: map[string]string{
74 "width": strconv.FormatFloat(float64(c.Width), 'f', -1, 32) + "px",
75 "height": strconv.FormatFloat(float64(c.Height), 'f', -1, 32) + "px",
76 },
77 },
78 Properties: element.Properties{
79 Node: n,
80 Id: id,
81 X: 0,
82 Y: 0,
83 Type: 3,
84 },
85 }
86 i := 0
87 for child := n.FirstChild; child != nil; child = child.NextSibling {
88 if child.Type == html.ElementNode {
89 node.Children = append(node.Children, CreateNode(node, child, fmt.Sprint(i)))
90 i++
91 }
92 }
93 return initNodes(&node, *c)
94}
95
96func CreateNode(parent element.Node, n *html.Node, slug string) element.Node {
97 id := n.Data + slug
98 node := element.Node{
99 Parent: &parent,
100 TagName: n.Data,
101 InnerText: utils.GetInnerText(n),
102 Properties: element.Properties{
103 Id: id,
104 Type: n.Type,
105 Node: n,
106 },
107 }
108 for _, attr := range n.Attr {
109 if attr.Key == "class" {
110 classes := strings.Split(attr.Val, " ")
111 for _, class := range classes {
112 node.ClassList.Add(class)
113 }
114 } else if attr.Key == "id" {
115 node.Id = attr.Val
116 } else if attr.Key == "contenteditable" && (attr.Val == "" || attr.Val == "true") {
117 node.Properties.Editable = true
118 } else if attr.Key == "href" {
119 node.Href = attr.Val
120 } else if attr.Key == "src" {
121 node.Src = attr.Val
122 } else if attr.Key == "title" {
123 node.Title = attr.Val
124 }
125 }
126 i := 0
127 for child := n.FirstChild; child != nil; child = child.NextSibling {
128 if child.Type == html.ElementNode {
129 node.Children = append(node.Children, CreateNode(node, child, slug+fmt.Sprint(i)))
130 i++
131 }
132 }
133 return node
134}
135
136var inheritedProps = []string{
137 "color",
138 "cursor",
139 "font",
140 "font-family",
141 "font-size",
142 "font-style",
143 "font-weight",
144 "letter-spacing",
145 "line-height",
146 "text-align",
147 "text-indent",
148 "text-justify",
149 "text-shadow",
150 "text-transform",
151 "visibility",
152 "word-spacing",
153 "display",
154}
155
156// need to get rid of the .props for the most part all styles should be computed dynamically
157// can keep like focusable and stuff that describes the element
158
159// currently the append child does not work due to the props and other stuff not existing so it fails
160// moving to a real time style compute would fix that
161
162// :hover is parsed correctly but because the hash func doesn't invalidate it becuase the val
163// is updated in the props. change to append :hover to style to create the effect
164// or merge the class with the styles? idk have to think more
165
166func (c *CSS) GetStyles(n element.Node) map[string]string {
167 styles := map[string]string{}
168 for k, v := range n.Style {
169 styles[k] = v
170 }
171 if n.Parent != nil {
172 ps := c.GetStyles(*n.Parent)
173 for _, v := range inheritedProps {
174 if ps[v] != "" {
175 styles[v] = ps[v]
176 }
177 }
178
179 }
180 hovered := false
181 if slices.Contains(n.ClassList.Classes, ":hover") {
182 hovered = true
183 }
184
185 for _, styleSheet := range c.StyleSheets {
186 for selector := range styleSheet {
187 // fmt.Println(selector, n.Properties.Id)
188 key := selector
189 if strings.Contains(selector, ":hover") && hovered {
190 selector = strings.Replace(selector, ":hover", "", -1)
191 }
192 if element.TestSelector(selector, &n) {
193 for k, v := range styleSheet[key] {
194 styles[k] = v
195 }
196 }
197
198 }
199 }
200 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
201 styles = utils.Merge(styles, inline)
202 // add hover and focus css events
203
204 return styles
205}
206
207func (c *CSS) Render(doc element.Node) []element.Node {
208 return flatten(doc)
209}
210
211func (c *CSS) AddPlugin(plugin Plugin) {
212 c.Plugins = append(c.Plugins, plugin)
213}
214
215func hash(n *element.Node) string {
216 // Create a new FNV-1a hash
217 hasher := md5.New()
218
219 // Extract and sort the keys
220 var keys []string
221 for key := range n.Style {
222 keys = append(keys, key)
223 }
224 sort.Strings(keys)
225
226 // Concatenate all values into a single string
227 var concatenatedValues string
228 for _, key := range keys {
229 concatenatedValues += key + n.Style[key]
230 }
231 concatenatedValues += n.ClassList.Value
232 concatenatedValues += n.Id
233 hasher.Write([]byte(concatenatedValues))
234 sum := hasher.Sum(nil)
235 str := hex.EncodeToString(sum)
236 if n.Properties.Hash != str {
237 fmt.Println(n.Properties.Id)
238 fmt.Println(concatenatedValues)
239 fmt.Println(n.Properties.Hash, str)
240 }
241
242 return str
243}
244
245func (c *CSS) ComputeNodeStyle(n *element.Node) *element.Node {
246 plugins := c.Plugins
247 hv := hash(n)
248 if n.Properties.Hash != hv {
249 fmt.Println("RELOAD")
250 // this is kinda a sloppy way to do this but it works ig
251 n.Style = c.GetStyles(*n)
252 n.Properties.Hash = hv
253 }
254 styleMap := n.Style
255
256 if styleMap["display"] == "none" {
257 n.Properties.X = 0
258 n.Properties.Y = 0
259 n.Properties.Width = 0
260 n.Properties.Height = 0
261 return n
262 }
263
264 width, height := n.Properties.Width, n.Properties.Height
265 x, y := n.Parent.Properties.X, n.Parent.Properties.Y
266
267 var top, left, right, bottom bool = false, false, false, false
268
269 m := utils.GetMP(*n, "margin")
270 p := utils.GetMP(*n, "padding")
271
272 if styleMap["position"] == "absolute" {
273 base := GetPositionOffsetNode(n)
274 if styleMap["top"] != "" {
275 v, _ := utils.ConvertToPixels(styleMap["top"], float32(n.Properties.EM), n.Parent.Properties.Width)
276 y = v + base.Properties.Y
277 top = true
278 }
279 if styleMap["left"] != "" {
280 v, _ := utils.ConvertToPixels(styleMap["left"], float32(n.Properties.EM), n.Parent.Properties.Width)
281 x = v + base.Properties.X
282 left = true
283 }
284 if styleMap["right"] != "" {
285 v, _ := utils.ConvertToPixels(styleMap["right"], float32(n.Properties.EM), n.Parent.Properties.Width)
286 x = (base.Properties.Width - width) - v
287 right = true
288 }
289 if styleMap["bottom"] != "" {
290 v, _ := utils.ConvertToPixels(styleMap["bottom"], float32(n.Properties.EM), n.Parent.Properties.Width)
291 y = (base.Properties.Height - height) - v
292 bottom = true
293 }
294 } else {
295 for i, v := range n.Parent.Children {
296 if v.Properties.Id == n.Properties.Id {
297 if i-1 > 0 {
298 sibling := n.Parent.Children[i-1]
299 if styleMap["display"] == "inline" {
300 if sibling.Style["display"] == "inline" {
301 y = sibling.Properties.Y
302 } else {
303 y = sibling.Properties.Y + sibling.Properties.Height
304 }
305 } else {
306 y = sibling.Properties.Y + sibling.Properties.Height
307 }
308 }
309 break
310 } else if styleMap["display"] != "inline" {
311 mc := utils.GetMP(v, "margin")
312 pc := utils.GetMP(v, "padding")
313 y += mc.Top + mc.Bottom + pc.Top + pc.Bottom + v.Properties.Height
314 }
315 }
316 }
317
318 // Display modes need to be calculated here
319
320 relPos := !top && !left && !right && !bottom
321
322 if left || relPos {
323 x += m.Left
324 }
325 if top || relPos {
326 y += m.Top
327 }
328 if right {
329 x -= m.Right
330 }
331 if bottom {
332 y -= m.Bottom
333 }
334
335 if len(n.Children) == 0 {
336 // Confirm text exists
337 if len(n.InnerText) > 0 {
338 innerWidth := width
339 innerHeight := height
340 genTextNode(n, &innerWidth, &innerHeight, p)
341 width = innerWidth + p.Left + p.Right
342 height = innerHeight
343 }
344 }
345
346 n.Properties.X = x
347 n.Properties.Y = y
348 n.Properties.Width = width
349 n.Properties.Height = height
350
351 // Call children here
352
353 var childYOffset float32
354 for i, v := range n.Children {
355 v.Parent = n
356 n.Children[i] = *c.ComputeNodeStyle(&v)
357 if styleMap["height"] == "" {
358 if n.Children[i].Style["position"] != "absolute" && n.Children[i].Properties.Y > childYOffset {
359 childYOffset = n.Children[i].Properties.Y
360 m := utils.GetMP(n.Children[i], "margin")
361 p := utils.GetMP(n.Children[i], "padding")
362 n.Properties.Height += n.Children[i].Properties.Height
363 n.Properties.Height += m.Top
364 n.Properties.Height += m.Bottom
365 n.Properties.Height += p.Top
366 n.Properties.Height += p.Bottom
367 }
368
369 }
370 }
371
372 // Sorting the array by the Level field
373 sort.Slice(plugins, func(i, j int) bool {
374 return plugins[i].Level < plugins[j].Level
375 })
376
377 for _, v := range plugins {
378 matches := true
379 for name, value := range v.Styles {
380 if styleMap[name] != value && !(value == "*") {
381 matches = false
382 }
383 }
384 if matches {
385 v.Handler(n)
386 }
387 }
388
389 return n
390}
391
392func initNodes(n *element.Node, c CSS) element.Node {
393 n = InitNode(n, c)
394 for i, ch := range n.Children {
395 if ch.Properties.Type == html.ElementNode {
396 ch.Parent = n
397 cn := initNodes(&ch, c)
398
399 n.Children[i] = cn
400
401 }
402 }
403
404 return *n
405}
406
407func InitNode(n *element.Node, c CSS) *element.Node {
408 n.Style = c.GetStyles(*n)
409 border, err := CompleteBorder(n.Style)
410 if err == nil {
411 n.Properties.Border = border
412 }
413
414 fs, _ := utils.ConvertToPixels(n.Style["font-size"], n.Parent.Properties.EM, n.Parent.Properties.Width)
415 n.Properties.EM = fs
416
417 width, _ := utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Width)
418 if n.Style["min-width"] != "" {
419 minWidth, _ := utils.ConvertToPixels(n.Style["min-width"], n.Properties.EM, n.Parent.Properties.Width)
420 width = utils.Max(width, minWidth)
421 }
422
423 if n.Style["max-width"] != "" {
424 maxWidth, _ := utils.ConvertToPixels(n.Style["max-width"], n.Properties.EM, n.Parent.Properties.Width)
425 width = utils.Min(width, maxWidth)
426 }
427
428 height, _ := utils.ConvertToPixels(n.Style["height"], n.Properties.EM, n.Parent.Properties.Height)
429 if n.Style["min-height"] != "" {
430 minHeight, _ := utils.ConvertToPixels(n.Style["min-height"], n.Properties.EM, n.Parent.Properties.Height)
431 height = utils.Max(height, minHeight)
432 }
433
434 if n.Style["max-height"] != "" {
435 maxHeight, _ := utils.ConvertToPixels(n.Style["max-height"], n.Properties.EM, n.Parent.Properties.Height)
436 height = utils.Min(height, maxHeight)
437 }
438
439 n.Properties.Width = width
440 n.Properties.Height = height
441
442 bold, italic := false, false
443
444 if n.Style["font-weight"] == "bold" {
445 bold = true
446 }
447
448 if n.Style["font-style"] == "italic" {
449 italic = true
450 }
451
452 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
453 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
454 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
455 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
456 if lineHeight == 0 {
457 lineHeight = n.Properties.EM + 3
458 }
459
460 n.Properties.Text.LineHeight = int(lineHeight)
461 n.Properties.Text.Font = f
462 n.Properties.Text.WordSpacing = int(wordSpacing)
463 n.Properties.Text.LetterSpacing = int(letterSpacing)
464 return n
465}
466
467func parseBorderShorthand(borderShorthand string) (element.Border, error) {
468 // Split the shorthand into components
469 borderComponents := strings.Fields(borderShorthand)
470
471 // Ensure there are at least 1 component (width or style or color)
472 if len(borderComponents) >= 1 {
473 width := "0px" // Default width
474 style := "solid"
475 borderColor := "#000000" // Default color
476
477 // Extract style and color if available
478 if len(borderComponents) >= 1 {
479 width = borderComponents[0]
480 }
481
482 // Extract style and color if available
483 if len(borderComponents) >= 2 {
484 style = borderComponents[1]
485 }
486 if len(borderComponents) >= 3 {
487 borderColor = borderComponents[2]
488 }
489
490 parsedColor, _ := color.Color(borderColor)
491
492 return element.Border{
493 Width: width,
494 Style: style,
495 Color: parsedColor,
496 Radius: "", // Default radius
497 }, nil
498 }
499
500 return element.Border{}, fmt.Errorf("invalid border shorthand format")
501}
502
503func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
504 border, err := parseBorderShorthand(cssProperties["border"])
505 border.Radius = cssProperties["border-radius"]
506
507 return border, err
508}
509
510func flatten(n element.Node) []element.Node {
511 var nodes []element.Node
512 nodes = append(nodes, n)
513
514 children := n.Children
515 if len(children) > 0 {
516 for _, ch := range children {
517 chNodes := flatten(ch)
518 nodes = append(nodes, chNodes...)
519 }
520 }
521 return nodes
522}
523
524func genTextNode(n *element.Node, width, height *float32, p utils.MarginPadding) {
525 wb := " "
526
527 if n.Style["word-wrap"] == "break-word" {
528 wb = ""
529 }
530
531 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
532 wb = ""
533 }
534
535 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, *width)
536 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, *width)
537
538 var dt float32
539
540 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
541 dt = 2
542 } else {
543 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], n.Properties.EM, *width)
544 }
545
546 col := color.Parse(n.Style, "font")
547
548 n.Properties.Text.Color = col
549 n.Properties.Text.Align = n.Style["text-align"]
550 n.Properties.Text.WordBreak = wb
551 n.Properties.Text.WordSpacing = int(wordSpacing)
552 n.Properties.Text.LetterSpacing = int(letterSpacing)
553 n.Properties.Text.WhiteSpace = n.Style["white-space"]
554 n.Properties.Text.DecorationThickness = int(dt)
555 n.Properties.Text.Overlined = n.Style["text-decoration"] == "overline"
556 n.Properties.Text.Underlined = n.Style["text-decoration"] == "underline"
557 n.Properties.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
558 n.Properties.Text.EM = int(n.Properties.EM)
559 n.Properties.Text.Width = int(n.Parent.Properties.Width)
560
561 if n.Style["word-spacing"] == "" {
562 n.Properties.Text.WordSpacing = font.MeasureSpace(&n.Properties.Text)
563 }
564 if n.Parent.Properties.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
565 *width = (n.Parent.Properties.Width - p.Right) - p.Left
566 } else if n.Style["width"] == "" {
567 *width = utils.Max(*width, float32(font.MeasureLongest(n)))
568 } else if n.Style["width"] != "" {
569 *width, _ = utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Width)
570 }
571
572 n.Properties.Text.Width = int(*width)
573 h := font.Render(n)
574 if n.Style["height"] == "" {
575 *height = h
576 }
577
578}
579
580func GetPositionOffsetNode(n *element.Node) *element.Node {
581 pos := n.Style["position"]
582
583 if pos == "relative" {
584 return n
585 } else {
586 if n.Parent.Properties.Node != nil {
587 return GetPositionOffsetNode(n.Parent)
588 } else {
589 return nil
590 }
591 }
592}