local string = string
local table = table
local surface = surface
local tostring = tostring
local pairs = pairs
local setmetatable = setmetatable
local math = math
local utf8 = utf8
local Color = Color
local MarkupObject = {}
MarkupObject.__index = MarkupObject
debug.getregistry().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: 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 = {}
local x = { "r", "g", "b", "a" }
local n = 1
for k, v in string.gmatch( p2, "(%d+),?" ) do
rgba[ x[ n ] ] = 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: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. Alphaoverride can be used to override
the alpha value of the text-colour.
Usage: MarkupObject:Draw(100, 100)
-----------------------------------------------------------]]
function MarkupObject:Draw( xOffset, yOffset, halign, valign, alphaoverride )
for i, blk in pairs( 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 )
surface.DrawText( blk.text )
end
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.
Usage: markup.Parse("changed font\nchanged 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 .. ""
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 lineHeight = 0
for i, blk in pairs( blocks ) do
surface.SetFont( blocks[ i ].font )
local thisY = 0
local curString = ""
blocks[ i ].text = string.gsub( blocks[i].text, ">", ">" )
blocks[ i ].text = string.gsub( blocks[i].text, "<", "<" )
blocks[ i ].text = string.gsub( blocks[i].text, "&", "&" )
for j, c in utf8.codes( blocks[ i ].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, y1 = surface.GetTextSize( curString )
local new_block = {}
new_block.text = curString
new_block.font = blocks[ i ].font
new_block.colour = blocks[ i ].colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.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, y1 = surface.GetTextSize( curString )
local new_block = {}
new_block.text = curString
new_block.font = blocks[ i ].font
new_block.colour = blocks[ i ].colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.y = yOffset
table.insert( new_block_list, new_block )
if ( xOffset + x1 > xMax ) then
xMax = xOffset + x1
end
end
local xOldSize = xSize
xSize = 0
curString = ""
local xOldOffset = xOffset
xOffset = math.ceil( ( xOffset + xOldSize ) / 50 ) * 50
if ( xOffset == xOldOffset ) then
xOffset = xOffset + 50
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 add a -. add the character to ch
-- 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
if ( lastSpacePos == string.len( curString ) && lastSpacePos > 0 ) then
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
ch = string.sub( curString, lastSpacePos + 1 ) .. ch
j = lastSpacePos + 1
curString = string.sub( curString, 1, lastSpacePos )
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 = {}
new_block.text = curString
new_block.font = blocks[ i ].font
new_block.colour = blocks[ i ].colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.y = yOffset
table.insert( new_block_list, new_block )
if ( xOffset + x1 > xMax ) then
xMax = xOffset + x1
end
xOffset = 0
xSize = 0
x, y = surface.GetTextSize( ch )
yOffset = yOffset + thisMaxY
thisY = 0
curString = ""
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, y1 = surface.GetTextSize( curString )
local new_block = {}
new_block.text = curString
new_block.font = blocks[ i ].font
new_block.colour = blocks[ i ].colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.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 pairs( new_block_list ) do
new_block_list[ i ].height = ymaxes[ new_block_list[ i ].offset.y ]
if ( new_block_list[ i ].offset.y + new_block_list[ i ].height > totalHeight ) then
totalHeight = new_block_list[ i ].offset.y + new_block_list[ i ].height
end
end
return setmetatable( {
totalHeight = totalHeight,
totalWidth = xMax,
blocks = new_block_list
}, MarkupObject )
end