-
Notifications
You must be signed in to change notification settings - Fork 836
Expand file tree
/
Copy pathmarkup.lua
More file actions
557 lines (462 loc) · 15.8 KB
/
markup.lua
File metadata and controls
557 lines (462 loc) · 15.8 KB
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
local string = string
local table = table
local surface = surface
local tostring = tostring
local ipairs = ipairs
local setmetatable = setmetatable
local tonumber = tonumber
local math = math
local utf8 = utf8
local _Color = Color
local MarkupObject = {}
MarkupObject.__index = MarkupObject
RegisterMetaTable( "MarkupObject", MarkupObject )
module( "markup" )
--[[---------------------------------------------------------
Name: Constants used for text alignment.
These must be the same values as in the draw module.
-----------------------------------------------------------]]
TEXT_ALIGN_LEFT = 0
TEXT_ALIGN_CENTER = 1
TEXT_ALIGN_RIGHT = 2
TEXT_ALIGN_TOP = 3
TEXT_ALIGN_BOTTOM = 4
--[[---------------------------------------------------------
Name: Color( Color( r, g, b, a ) )
Desc: Convenience function which converts a Color object into a string
which can be used in the <color=r,g,b,a></color> tag
e.g. Color( 255, 0, 0, 150 ) -> 255,0,0,150
Color( 255, 0, 0 ) -> 255,0,0
Color( 255, 0, 0, 255 ) -> 255,0,0
Usage: markup.Color( Color( r, g, b, a ) )
-----------------------------------------------------------]]
function Color( col )
return
col.r .. "," ..
col.g .. "," ..
col.b ..
-- If the alpha value is 255, we don't need to include it in the <color> tag, so just omit it:
( col.a == 255 and "" or ( "," .. col.a ) )
end
local Color = _Color
--[[---------------------------------------------------------
Name: Temporary information used when building text frames.
-----------------------------------------------------------]]
local colour_stack = { Color( 255, 255, 255 ) }
local font_stack = { "DermaDefault" }
local blocks = {}
local colourmap = {
-- it's all black and white
["black"] = Color( 0, 0, 0 ),
["white"] = Color( 255, 255, 255 ),
-- it's greys
["dkgrey"] = Color( 64, 64, 64 ),
["grey"] = Color( 128, 128, 128 ),
["ltgrey"] = Color( 192, 192, 192 ),
-- account for speeling mistakes
["dkgray"] = Color( 64, 64, 64 ),
["gray"] = Color( 128, 128, 128 ),
["ltgray"] = Color( 192, 192, 192 ),
-- normal colours
["red"] = Color( 255, 0, 0 ),
["green"] = Color( 0, 255, 0 ),
["blue"] = Color( 0, 0, 255 ),
["yellow"] = Color( 255, 255, 0 ),
["purple"] = Color( 255, 0, 255 ),
["cyan"] = Color( 0, 255, 255 ),
["turq"] = Color( 0, 255, 255 ),
-- dark variations
["dkred"] = Color( 128, 0, 0 ),
["dkgreen"] = Color( 0, 128, 0 ),
["dkblue"] = Color( 0, 0, 128 ),
["dkyellow"] = Color( 128, 128, 0 ),
["dkpurple"] = Color( 128, 0, 128 ),
["dkcyan"] = Color( 0, 128, 128 ),
["dkturq"] = Color( 0, 128, 128 ),
-- light variations
["ltred"] = Color( 255, 128, 128 ),
["ltgreen"] = Color( 128, 255, 128 ),
["ltblue"] = Color( 128, 128, 255 ),
["ltyellow"] = Color( 255, 255, 128 ),
["ltpurple"] = Color( 255, 128, 255 ),
["ltcyan"] = Color( 128, 255, 255 ),
["ltturq"] = Color( 128, 255, 255 ),
}
--[[---------------------------------------------------------
Name: colourMatch( c )
Desc: Match a colour name to an rgb value.
Usage: ** INTERNAL ** Do not use!
-----------------------------------------------------------]]
local function colourMatch( c )
return colourmap[ string.lower( c ) ]
end
--[[---------------------------------------------------------
Name: ExtractParams(p1,p2,p3)
Desc: This function is used to extract the tag information.
Usage: ** INTERNAL ** Do not use!
-----------------------------------------------------------]]
local function ExtractParams( p1, p2, p3 )
if ( string.sub( p1, 1, 1 ) == "/" ) then
local tag = string.sub( p1, 2 )
if ( tag == "color" or tag == "colour" ) then
table.remove( colour_stack )
elseif ( tag == "font" or tag == "face" ) then
table.remove( font_stack )
end
else
if ( p1 == "color" or p1 == "colour" ) then
local rgba = colourMatch( p2 )
if ( rgba == nil ) then
rgba = Color( 255, 255, 255, 255 )
local x = { "r", "g", "b", "a" }
local n = 1
for k, v in string.gmatch( p2, "(%d+),?" ) do
rgba[ x[ n ] ] = tonumber( k )
n = n + 1
end
end
table.insert( colour_stack, rgba )
elseif ( p1 == "font" or p1 == "face" ) then
table.insert( font_stack, tostring( p2 ) )
end
end
end
--[[---------------------------------------------------------
Name: CheckTextOrTag( p )
Desc: This function places data in the "blocks" table
depending of if p is a tag, or some text
Usage: ** INTERNAL ** Do not use!
-----------------------------------------------------------]]
local function CheckTextOrTag( p )
if ( p == "" ) then return end
if ( p == nil ) then return end
if ( string.sub( p, 1, 1 ) == "<" ) then
string.gsub( p, "<([/%a]*)=?([^>]*)", ExtractParams )
else
local text_block = {}
text_block.text = p
text_block.colour = colour_stack[ #colour_stack ]
text_block.font = font_stack[ #font_stack ]
table.insert( blocks, text_block )
end
end
--[[---------------------------------------------------------
Name: ProcessMatches(p1,p2,p3)
Desc: CheckTextOrTag for 3 parameters. Called by string.gsub
Usage: ** INTERNAL ** Do not use!
-----------------------------------------------------------]]
local function ProcessMatches( p1, p2, p3 )
if ( p1 ) then CheckTextOrTag( p1 ) end
if ( p2 ) then CheckTextOrTag( p2 ) end
if ( p3 ) then CheckTextOrTag( p3 ) end
end
--[[---------------------------------------------------------
Name: MarkupObject:GetWidth()
Desc: Returns the width of a markup block
Usage: ml:GetWidth()
-----------------------------------------------------------]]
function MarkupObject:GetWidth()
return self.totalWidth
end
--[[---------------------------------------------------------
Name: MarkupObject:GetMaxWidth()
Desc: Returns the maximum width of a markup block
Usage: ml:GetMaxWidth()
-----------------------------------------------------------]]
function MarkupObject:GetMaxWidth()
return self.maxWidth or self.totalWidth
end
--[[---------------------------------------------------------
Name: MarkupObject:GetHeight()
Desc: Returns the height of a markup block
Usage: ml:GetHeight()
-----------------------------------------------------------]]
function MarkupObject:GetHeight()
return self.totalHeight
end
function MarkupObject:Size()
return self.totalWidth, self.totalHeight
end
--[[---------------------------------------------------------
Name: MarkupObject:Draw(xOffset, yOffset, halign, valign, alphaoverride)
Desc: Draw the markup text to the screen as position xOffset, yOffset.
Halign and Valign can be used to align the text relative to its offset.
Alphaoverride can be used to override the alpha value of the text-colour.
textAlign can be used to align the actual text inside of its bounds.
Usage: MarkupObject:Draw(100, 100)
-----------------------------------------------------------]]
function MarkupObject:Draw( xOffset, yOffset, halign, valign, alphaoverride, textAlign )
for i, blk in ipairs( self.blocks ) do
local y = yOffset + ( blk.height - blk.thisY ) + blk.offset.y
local x = xOffset
if ( halign == TEXT_ALIGN_CENTER ) then x = x - ( self.totalWidth / 2 )
elseif ( halign == TEXT_ALIGN_RIGHT ) then x = x - self.totalWidth
end
x = x + blk.offset.x
if ( valign == TEXT_ALIGN_CENTER ) then y = y - ( self.totalHeight / 2 )
elseif ( valign == TEXT_ALIGN_BOTTOM ) then y = y - self.totalHeight
end
local alpha = blk.colour.a
if ( alphaoverride ) then alpha = alphaoverride end
surface.SetFont( blk.font )
surface.SetTextColor( blk.colour.r, blk.colour.g, blk.colour.b, alpha )
surface.SetTextPos( x, y )
if ( textAlign ~= TEXT_ALIGN_LEFT ) then
local lineWidth = self.lineWidths[ blk.offset.y ]
if ( lineWidth ) then
if ( textAlign == TEXT_ALIGN_CENTER ) then
surface.SetTextPos( x + ( ( self.totalWidth - lineWidth ) / 2 ), y )
elseif ( textAlign == TEXT_ALIGN_RIGHT ) then
surface.SetTextPos( x + ( self.totalWidth - lineWidth ), y )
end
end
end
surface.DrawText( blk.text )
end
end
--[[---------------------------------------------------------
Name: Escape(str)
Desc: Converts a string to its escaped, markup-safe equivalent
Usage: markup.Escape( "<font=Default>The font will remain unchanged & these < > & symbols will also appear normally</font>" )
-----------------------------------------------------------]]
local escapeEntities, unescapeEntities = {
["&"] = "&",
["<"] = "<",
[">"] = ">"
}, {
["&"] = "&",
["<"] = "<",
[">"] = ">"
}
function Escape( str )
return ( string.gsub( tostring( str ), "[&<>]", escapeEntities ) )
end
--[[---------------------------------------------------------
Name: Parse(ml, maxwidth)
Desc: Parses the pseudo-html markup language, and creates a
MarkupObject, which can be used to the draw the
text to the screen. Valid tags are: font and colour.
\n and \t are also available to move to the next line,
or insert a tab character.
Maxwidth can be used to make the text wrap to a specific
width and allows for text alignment (e.g. centering) inside
the bounds.
Usage: markup.Parse( "<font=Default>changed font</font>\n<colour=255,0,255,255>changed colour</colour>" )
-----------------------------------------------------------]]
function Parse( ml, maxwidth )
ml = utf8.force( ml ) -- Ensure we have valid UTF-8 data
colour_stack = { Color( 255, 255, 255 ) }
font_stack = { "DermaDefault" }
blocks = {}
if ( !string.find( ml, "<" ) ) then
ml = ml .. "<nop>"
end
string.gsub( ml, "([^<>]*)(<[^>]+.)([^<>]*)", ProcessMatches )
local xOffset = 0
local yOffset = 0
local xSize = 0
local xMax = 0
local thisMaxY = 0
local new_block_list = {}
local ymaxes = {}
local lineWidths = {}
local lineHeight = 0
for i, blk in ipairs( blocks ) do
surface.SetFont( blk.font )
blk.text = string.gsub( blk.text, "(&.-;)", unescapeEntities )
local thisY = 0
local curString = ""
for j, c in utf8.codes( blk.text ) do
local ch = utf8.char( c )
if ( ch == "\n" ) then
if ( thisY == 0 ) then
thisY = lineHeight
thisMaxY = lineHeight
else
lineHeight = thisY
end
if ( string.len( curString ) > 0 ) then
local x1 = surface.GetTextSize( curString )
local new_block = {
text = curString,
font = blk.font,
colour = blk.colour,
thisY = thisY,
thisX = x1,
offset = {
x = xOffset,
y = yOffset
}
}
table.insert( new_block_list, new_block )
if ( xOffset + x1 > xMax ) then
xMax = xOffset + x1
end
end
xOffset = 0
xSize = 0
yOffset = yOffset + thisMaxY
thisY = 0
curString = ""
thisMaxY = 0
elseif ( ch == "\t" ) then
if ( string.len( curString ) > 0 ) then
local x1 = surface.GetTextSize( curString )
local new_block = {
text = curString,
font = blk.font,
colour = blk.colour,
thisY = thisY,
thisX = x1,
offset = {
x = xOffset,
y = yOffset
}
}
table.insert( new_block_list, new_block )
if ( xOffset + x1 > xMax ) then
xMax = xOffset + x1
end
end
curString = ""
local xOldSize = xSize
xSize = 0
local xOldOffset = xOffset
xOffset = math.ceil( ( xOffset + xOldSize ) / 50 ) * 50
if ( xOffset == xOldOffset ) then
xOffset = xOffset + 50
if ( maxwidth and xOffset > maxwidth ) then
-- Needs a new line
if ( thisY == 0 ) then
thisY = lineHeight
thisMaxY = lineHeight
else
lineHeight = thisY
end
xOffset = 0
yOffset = yOffset + thisMaxY
thisY = 0
thisMaxY = 0
end
end
else
local x, y = surface.GetTextSize( ch )
if ( x == nil ) then return end
if ( maxwidth and maxwidth > x ) then
if ( xOffset + xSize + x >= maxwidth ) then
-- need to: find the previous space in the curString
-- if we can't find one, take off the last character
-- and insert as a new block, incrementing the y etc
local lastSpacePos = string.len( curString )
for k = 1,string.len( curString ) do
local chspace = string.sub( curString, k, k )
if ( chspace == " " ) then
lastSpacePos = k
end
end
local previous_block = new_block_list[ #new_block_list ]
local wrap = lastSpacePos == string.len( curString ) && lastSpacePos > 0
if ( previous_block and previous_block.text:match( " $" ) and wrap and surface.GetTextSize( blk.text ) < maxwidth ) then
-- If the block was preceded by a space, wrap the block onto the next line first, as we can probably fit it there
local trimmed, trimCharNum = previous_block.text:gsub( " +$", "" )
if ( trimCharNum > 0 ) then
previous_block.text = trimmed
previous_block.thisX = surface.GetTextSize( previous_block.text )
end
else
if ( wrap ) then
-- If the block takes up multiple lines (and has no spaces), split it up
local sequenceStartPos = utf8.offset( curString, 0, lastSpacePos )
ch = string.match( curString, utf8.charpattern, sequenceStartPos ) .. ch
j = utf8.offset( curString, 1, sequenceStartPos )
curString = string.sub( curString, 1, sequenceStartPos - 1 )
else
-- Otherwise, strip the trailing space and start a new line
ch = string.sub( curString, lastSpacePos + 1 ) .. ch
j = lastSpacePos + 1
curString = string.sub( curString, 1, math.max( lastSpacePos - 1, 0 ) )
end
local m = 1
while ( string.sub( ch, m, m ) == " " ) do
m = m + 1
end
ch = string.sub( ch, m )
local x1, y1 = surface.GetTextSize( curString )
if ( y1 > thisMaxY ) then
thisMaxY = y1
ymaxes[ yOffset ] = thisMaxY
lineHeight = y1
end
local new_block = {
text = curString,
font = blk.font,
colour = blk.colour,
thisY = thisY,
thisX = x1,
offset = {
x = xOffset,
y = yOffset
}
}
table.insert( new_block_list, new_block )
if ( xOffset + x1 > xMax ) then
xMax = xOffset + x1
end
curString = ""
end
xOffset = 0
xSize = 0
x, y = surface.GetTextSize( ch )
yOffset = yOffset + thisMaxY
thisY = 0
thisMaxY = 0
end
end
curString = curString .. ch
thisY = y
xSize = xSize + x
if ( y > thisMaxY ) then
thisMaxY = y
ymaxes[ yOffset ] = thisMaxY
lineHeight = y
end
end
end
if ( string.len( curString ) > 0 ) then
local x1 = surface.GetTextSize( curString )
local new_block = {
text = curString,
font = blk.font,
colour = blk.colour,
thisY = thisY,
thisX = x1,
offset = {
x = xOffset,
y = yOffset
}
}
table.insert( new_block_list, new_block )
lineHeight = thisY
if ( xOffset + x1 > xMax ) then
xMax = xOffset + x1
end
xOffset = xOffset + x1
end
xSize = 0
end
local totalHeight = 0
for i, blk in ipairs( new_block_list ) do
blk.height = ymaxes[ blk.offset.y ]
if ( blk.offset.y + blk.height > totalHeight ) then
totalHeight = blk.offset.y + blk.height
end
lineWidths[ blk.offset.y ] = math.max( lineWidths[ blk.offset.y ] or 0, blk.offset.x + blk.thisX )
end
return setmetatable( {
totalHeight = totalHeight,
totalWidth = xMax,
maxWidth = maxwidth,
lineWidths = lineWidths,
blocks = new_block_list
}, MarkupObject )
end