- corrected references to some missing emojis
- renamed and exposed emoji table in mod namespace
- added private string-matching helper function
- removed extraneous table.get_index helper function
- revamped parser with conditional pattern matching
- included support for symbols via escape codes
- added color and symbol tables to mod namespace
- allowed for overriding defaults in message parser
- separated various constants into head of script
- added option for changing normal text color
- consolidated parsing of item and emoji tags
- compensated for oddity with textarea positions
This commit is contained in:
Leslie Krause 2020-03-03 18:10:30 -05:00
parent 757e327286
commit 6ac436e9f4
2 changed files with 343 additions and 82 deletions

View File

@ -1,4 +1,4 @@
Bedrock Markup Language Mod v1.2
Bedrock Markup Language Mod v1.3
By Leslie E. Krause
Bedrock Markup Language is an extensible markup language and API specifically tailored
@ -6,13 +6,176 @@ for Minetest formspecs with simple-to-use tags for layout and formatting (e.g. c
headers, borders, rows and columns), builtin word-wrapping, and support for embedded
images (e.g. skins, items, etc.)
It makes a particularly nice drop-in replacement for the default sign and book editors,
if you want to give your users the ability to create nicer looking messages, rather than
accepting raw formspec strings which could pose security risks.
There is support for 15 text colors (including white) via the following inline tags:
[q=gray][/q] - gray text
[q=red][/q] - red text
[q=green][/q] - green text
[q=blue][/q] - blue text
[q=cyan][/q] - cyan text
[q=magenta][/q] - magenta text
[q=yellow][/q] - yellow text
[q=black][/q] - black text
[q=brown][/q] - brown text
[q=teal][/q] - teal text
[q=purple][/q] - purple text
[q=olive][/q] - olive text
[q=indigo][/q] - indigo text
[q=maroon][/q] - maroon text
There is also support for rows and columns via the [r] and [c] tags. Both tags accept an
optional numeric attribute to alter the size of the cell to be rendered.
You can set the depth of all cells on the next row using [r=#], and you can set the width
of the next cell using [c=#]. In both cases, # must be a number (or 0 for the default).
The unit of measurement is an approximation of the standard "em" used in typography.
Here is a basic table with two rows and two columns:
> Upper left cell[c]Upper right cell
> [r]
> Lower left cell[c]Lower right cell
Notice how the initial [r] tag is missing, and the initial [c] tags are also missing. Not
to worry! The lexer automatically fills in these tags behind the scenes. It is entirely
optional to include them. This markup is more verbose, but produces the same results:
> [r=0][c=0]Upper left cell[c=0]Upper right cell
> [r=0][c=0]Lower left cell[c=0]Lower right cell
By default rows and columns are evenly spaced vertically and/or horizontally for the most
pleasing appearance. The lexer is capable of automatically resizing rows and columns to
fit within the allowed dimensions of the formspec and to prevent overruns.
In addition to all of the features described above, you can easily add bordered text,
headline text, and even images (both skin and item textures) into your formspecs!
[b]<text>
Insert a new border row with a depth of 1.0
[b=#]<text>
Same as above, but with the specified depth
[h]<text>
Insert a new header row with a depth of 1.0
[h=#]<text>
Same as above, but with the specified depth
[i]<item_name>
Insert the specified item texture with a width of 2.0 (if image is too big to fit on
the current row, then it will be shrunk to fit).
[i=#]<item_name>
Same as above, but with the specified width
[s]<skin_name>
Insert the specified skin texture with a width of 2.0 (if image is too big to fit on
the current row, then it will be shrunk to fit).
[s=#]<skin_name>
Same as above, but with the specified width
It is also possible to include dynamic text using variable interpolation:
$name - the name of the current player
$item - the wielded item of the current player (for use with the [­i] tag)
$skin - the selected skin of the current player (for use with the [­s] tag)
$date - the current world date
$time - the current world time
$cur_users - the current number of online players
$max_users - the maximum number of online players
And a variety of special characters can be inserted by means of escape codes:
&amp; - ampersand
&gt; - greater-than
&lt; - less-than
&rb; - right bracket
&lt; - left bracket
&copy; - copyright
&sect; - section
&half; - one-half
&deg; - degree
&pm; - plus-or-minus
&div; - division
&mul; - multiplication
&dash; - em-dash
&bull; - bullet
&lq; - opening quote
&rq; - closing quote
&lsq; - opening single-quote
&rsq; - closing single-quote
The Bedrock Markup Language also supports emojis! It's simple and easy to embed smilies
and other symbols into your formspecs using the existing item tag. Just type one of the
following emoji shortnames preceded by a colon, such as [i]:cupid_heart or [i]:smitten.
happy silly annoyed mad heart
hungry amused disappointed cool cupid_heart
smug confused angry cheerful black_heart
frustrated surprised sad smitten frozen_heart
laughing kissing crying sleeping heartbreak
warning danger keep_out cone lock
The following functions are available as part of the Bedrock Markup Language API:
* markup.get_builtin_vars( player_name )
Return a table consisting of builtin variables for use by the parser. You can add or
remove builtin variables or even disable them entirely by overriding this function.
* 'player_name' is the player for whom the formspec string will be generated.
* markup.parse_message( message, vars )
Parse the given message and return the rows as a table.
* 'message' is the message consisting of Bedrock Markup Language.
* 'vars' is a table of variables, with each key being the variable name and each
value being the corresponding string value of the variable.
* markup.get_formspec_string(
rows, min_horz, min_vert, max_horz, max_vert, border_color, header_color )
Generate a formspec string from the rows table returned by markup.parse_message( ).
* 'rows' is the rows table returned by the parser
* 'min_horz' is the left position of the rendering area in formspec coordinates
* 'min_vert' is the bottom position of the rendering area in formspec coordinates
* 'max_horz' is the right position of the rendering area in formspec coordinates
* 'max_vert' is the bottom position of the rendering area in formspec coordinates
Note that the parser will skip unknown tags and undefined variables and symbols, rather
than stripping them from the original message.
Repository
----------------------
Browse source code...
https://bitbucket.org/sorcerykid/markup
Download archive...
https://bitbucket.org/sorcerykid/markup/get/master.zip
https://bitbucket.org/sorcerykid/markup/get/master.tar.gz
Compatability
----------------------
Minetest 0.4.15+ required
Installation
----------------------
1) Unzip the archive into the mods directory of your game.
2) Rename the markup-master directory to "markup".
3) Add "markup" as a dependency to any mods using the API.
Source Code License
----------------------
MIT License
The MIT License (MIT)
Copyright (c) 2016-2019, Leslie E. Krause.
Copyright (c) 2019-2020, Leslie E. Krause.
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software

256
init.lua
View File

@ -2,14 +2,20 @@
-- Minetest :: Bedrock Markup Language Mod (markup)
--
-- See README.txt for licensing and other information.
-- Copyright (c) 2016-2019, Leslie Ellen Krause
-- Copyright (c) 2019-2020, Leslie Ellen Krause
--
-- ./games/just_test_tribute/mods/markup/init.lua
-- ./games/minetest_game/mods/markup/init.lua
--------------------------------------------------------
markup = { }
local registered_icons = {
local UNKNOWN_SKIN_TEXTURE = "character_preview.png"
local UNKNOWN_ITEM_TEXTURE = "unknown_item.png"
local UNKNOWN_EMOJI_TEXTURE = "emoji_unknown.png"
-------------------------
markup.registered_emojis = {
happy = "emoji_happy.png",
silly = "emoji_silly.png",
annoyed = "emoji_annoyed.png",
@ -40,39 +46,124 @@ local registered_icons = {
keep_out = "emoji_keep_out.png",
cone = "emoji_cone.png",
lock = "emoji_lock.png",
left_arrow = "emoji_left_arrow.png",
right_arrow = "emoji_right_arrow.png",
yum = "emoji_yum.png",
grin = "emoji_grin.png",
}
function table.get_index( self, sel_val, def_idx )
for idx, val in ipairs( self ) do
if val == sel_val then return idx end
end
return def_idx
markup.registered_colors = {
cyan = "#44FFFF",
magenta = "#FF44FF",
yellow = "#FFFF44",
red = "#FF4444",
green = "#44FF44",
blue = "#4444FF",
black = "#000000",
gray = "#AAAAAA",
brown = "#DDAA00",
teal = "#00DDAA",
purple = "#AA00DD",
olive = "#AADD00",
indigo = "#00AADD",
maroon = "#DD00AA",
}
markup.registered_symbols = {
amp = "&",
gt = ">",
lt = "<",
rb = "]",
lb = "[",
copy = "©",
sect = "§",
half = "½",
deg = "°",
pm = "±",
div = "÷",
mul = "×",
dash = "",
bull = "",
lq = "",
rq = "",
lsq = "",
rsq = "",
}
local _ = { }
local function is_match( text, glob )
-- use array for captures
_ = { string.match( text, glob ) }
return #_ > 0 and _ or nil
end
-------------------------
markup.get_builtin_vars = function ( player_name )
local player = minetest.get_player_by_name( player_name )
return {
["$name"] = player_name,
["$hash"] = cipher.tokenize( cipher.get_checksum( player_name ) ),
["$rank"] = default.rank_to_string( default.get_player_rank( player_name ) ),
["$item"] = player:get_wielded_item( ):get_name( ),
["$skin"] = skins.skins[ player_name ],
["$home"] = minetest.pos_to_string( beds.player_pos[ player_name ] or default.spawn_pos ),
["$lifetime"] = math.floor( player:get_lifetime( ) / 60 ),
["$uptime"] = math.floor( minetest.get_server_info( ).uptime / 60 ),
["$time"] = minetest.get_time_string( ),
["$date"] = minetest.get_date_string( ),
["$cur_users"] = #default.player_list,
["$max_users"] = minetest.setting_get( "max_users" ),
name = player_name,
item = player:get_wielded_item( ):get_name( ),
skin = skins.skins[ player_name ],
cur_users = #registry.player_list,
max_users = minetest.setting_get( "max_users" ),
}
end
markup.parse_message = function ( message, vars )
local text_colors = { cyan = "#44FFFF", magenta = "#FF44FF", yellow = "#FFFF44", red = "#FF4444", green = "#00DD00", blue = "#0000DD", black = "#000000", gray = "#AAAAAA" }
markup.parse_message = function ( message, vars, defs )
if not defs then defs = { } end
if not defs.colors then
defs.colors = markup.registered_colors
end
if not defs.emojis then
defs.emojis = markup.registered_emojis
end
if not defs.symbols then
defs.symbols = markup.registered_symbols
end
--[[ -- instantiate generic filter for parsing expressions
local filter = GenericFilter( )
local filter_vars = {
name = { type = FILTER_TYPE_STRING, value = vars.name },
rank = { type = FILTER_TYPE_STRING, value = vars.rank },
item = { type = FILTER_TYPE_STRING, value = vars.item },
skin = { type = FILTER_TYPE_STRING, value = vars.skin },
home = { type = FILTER_TYPE_STRING, value = vars.home },
lifetime = { type = FILTER_TYPE_PERIOD, value = vars.lifetime },
uptime = { type = FILTER_TYPE_PERIOD, value = vars.lifetime },
time = { type = FILTER_TYPE_STRING, value = vars.time },
date = { type = FILTER_TYPE_STRING, value = vars.date },
max_lag = { type = FILTER_TYPE_NUMBER, value = vars.max_lag },
avg_lag = { type = FILTER_TYPE_NUMBER, value = vars.avg_lag },
cur_users = { type = FILTER_TYPE_NUMBER, value = vars.cur_users },
max_users = { type = FILTER_TYPE_NUMBER, value = vars.max_users },
}
filter.add_preset_vars( filter_vars )
filter.define_func( "str", FILTER_TYPE_STRING, { FILTER_TYPE_NUMBER },
function ( v, a ) return tostring( a ) end )
filter.define_func( "join", FILTER_TYPE_STRING, { FILTER_TYPE_SERIES, FILTER_TYPE_STRING },
function ( v, a, b ) return table.concat( a, b ) end )
filter.define_func( "when", FILTER_TYPE_STRING, { FILTER_TYPE_PERIOD, FILTER_TYPE_STRING },
function ( v, a, b ) local f = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 }; return f[ b ] and ( math.floor( a / f[ b ] ) .. b ) or "?" end )
filter.define_func( "cal", FILTER_TYPE_STRING, { FILTER_TYPE_MOMENT, FILTER_TYPE_STRING },
function ( v, a, b ) local f = { ["Y"] = "%y", ["YY"] = "%Y", ["M"] = "%m", ["MM"] = "%b", ["D"] = "%d", ["DD"] = "%a", ["h"] = "%H", ["m"] = "%M", ["s"] = "%S" }; return os.date( string.gsub( b, "%a+", f ), a ) end )
filter.define_func( "rand", FILTER_TYPE_NUMBER, { FILTER_TYPE_NUMBER },
function ( v, a ) return math.random( a ) end )
local evaluate = function ( expr )
local oper = filter.translate( expr, filter_vars )
if not oper or oper.type ~= FILTER_TYPE_STRING then
return "?"
end
return oper.value
end
]]
-- preprocess the table tags (we should use tokenizer!)
message = string.gsub( message, "%[r", "[rn" ) -- normal
@ -91,50 +182,47 @@ markup.parse_message = function ( message, vars )
local c_types = { t = "text", i = "item", s = "skin", f = "form" }
for r_idx, r_val in ipairs( input_rows ) do
local p, v, t = string.match( r_val, "^(.)=(%d+)%](.*)" )
local type = "normal"
local vert = 0.0
if v then
vert = tonumber( v ) / 2
type = r_types[ p ]
r_val = t
else
r_val = string.gsub( r_val, "^(.)=?%]", function ( p )
type = r_types[ p ]
return ""
end ) -- allow default tags
if is_match( r_val, "^(.)=(%d+)%](.*)" ) or is_match( r_val, "^(.)=(%d+%.%d)%](.*)" ) then
vert = tonumber( _[ 2 ] ) / 2
type = r_types[ _[ 1 ] ]
r_val = _[ 3 ]
elseif is_match( r_val, "^(.)=?%](.*)" ) then -- permit default tag
type = r_types[ _[ 1 ] ]
r_val = _[ 2 ]
end
r_val = string.trim( r_val )
if t or r_val ~= "" then
if r_val ~= "" then
local cols = { }
local input_cols = string.split( r_val, "[c", true )
for c_idx, c_val in ipairs( input_cols ) do
local p, h, t = string.match( c_val, "^(.)=(%d+)%](.*)" )
local type = "text"
local horz = 0.0
if h then
horz = tonumber( h ) / 2
type = c_types[ p ]
c_val = t
else
c_val = string.gsub( c_val, "^(.)=?%]", function ( p )
type = c_types[ p ]
return ""
end ) -- allow default tags
if is_match( c_val, "^(.)=(%d+)%](.*)" ) or is_match( c_val, "^(.)=(%d+%.%d)%](.*)" ) then
horz = tonumber( _[ 2 ] ) / 2
type = c_types[ _[ 1 ] ]
c_val = _[ 3 ]
elseif is_match( c_val, "^(.)=?%](.*)" ) then -- permit default tag
type = c_types[ _[ 1 ] ]
c_val = _[ 2 ]
end
c_val = string.trim( c_val )
if t or c_val ~= "" then
c_val = string.gsub( c_val, "%$[a-zA-Z_]+", vars ) -- interpolate variables
-- c_val = string.gsub( c_val, "[^\]%[.-%]", "" ) -- strip undefined tags?
if c_val ~= "" or c_idx > 1 then
c_val = string.gsub( c_val, "&([a-z]+);", defs.symbols ) -- expand escape codes
-- c_val = string.gsub( c_val, "%%{(.-)}", evaluate ) -- interpolate functions
c_val = string.gsub( c_val, "%$([a-zA-Z_]+)", vars ) -- interpolate variables
if type == "text" then
local text = string.gsub( c_val, "%[q=([a-z]+)%](.-)%[/q%]", function ( code, text )
return text_colors[ code ] and minetest.colorize( text_colors[ code ], text ) or "?" .. text
return defs.colors[ code ] and minetest.colorize( defs.colors[ code ], text ) or "?" .. text
end )
table.insert( cols, { horz = horz, text = text, type = type } )
@ -142,27 +230,33 @@ markup.parse_message = function ( message, vars )
local text = string.gsub( c_val, "%[q[^%]]*%]", "" )
table.insert( cols, { horz = horz, text = text, type = type } )
elseif type == "item" and string.find( c_val, "^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$" ) then
local itemdef = minetest.registered_items[ c_val ]
elseif type == "item" then
local text
if not itemdef then
text = "unknown_item.png"
elseif itemdef.type == "node" and not itemdef.inventory_image then
text = itemdef.tiles[ 1 ]
if string.find( c_val, "^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$" ) then
local itemdef = minetest.registered_items[ c_val ]
if not itemdef then
text = UNKNOWN_ITEM_TEXTURE
elseif itemdef.type == "node" and not itemdef.inventory_image then
text = itemdef.tiles[ 1 ]
else
text = itemdef.inventory_image -- always fallback to inventory image
end
elseif type == "item" and c_val == ":blank" then
text = "blank.png"
elseif type == "item" and string.find( c_val, "^:[a-zA-Z0-9_]+$" ) then
text = defs.emojis[ string.sub( c_val, 2 ) ] or UNKNOWN_EMOJI_TEXTURE
else
text = itemdef.inventory_image -- always fallback to inventory image
text = UNKNOWN_ITEM_TEXTURE
end
table.insert( cols, { horz = horz, text = text, type = type } )
elseif type == "skin" then
if string.find( c_val, "^character_%d+$" ) then
text = skins.meta[ c_val ] and c_val .. "_preview.png" or UNKNOWN_SKIN_TEXTURE
else
text = UNKNOWN_SKIN_TEXTURE
end
table.insert( cols, { horz = horz, text = text, type = type } )
elseif type == "item" and string.find( c_val, "^:[a-zA-Z0-9_]+$" ) then
local text = registered_icons[ string.sub( c_val, 2 ) ] or "emoji_unknown.png"
table.insert( cols, { horz = horz, text = text, type = type } )
elseif type == "skin" and string.find( c_val, "^character_%d+$" ) then
local text = skins.meta[ c_val ] and c_val .. "_preview.png" or "character_preview.png"
table.insert( cols, { horz = horz, text = text, type = type } )
elseif type == "icon" and string.find( c_val, "^%w+$" ) then
end
end
end
@ -173,7 +267,7 @@ markup.parse_message = function ( message, vars )
return rows
end
markup.get_formspec_string = function ( rows, min_horz, min_vert, max_horz, max_vert, border_color, header_color )
markup.get_formspec_string = function ( rows, min_horz, min_vert, max_horz, max_vert, border_color, header_color, normal_color )
-- now render all the cells of the table
local formspec = ""
@ -205,12 +299,12 @@ markup.get_formspec_string = function ( rows, min_horz, min_vert, max_horz, max_
if r.type == "border" then
formspec = formspec ..
string.format( "box[%0.2f,%0.2f;%0.2f,%0.2f;%s]", off_horz - 0.5, off_vert, max_horz - off_horz + 0.2, depth + 0.1, border_color )
string.format( "box[%0.2f,%0.2f;%0.2f,%0.2f;%s]", off_horz - 0.2, off_vert, max_horz - off_horz + 0.2, depth + 0.1, border_color )
off_vert = off_vert + 0.1
elseif r.type == "header" then
formspec = formspec ..
string.format( "box[%0.2f,%0.2f;%0.2f,%0.2f;%s]", off_horz - 0.4, off_vert + depth - 0.05, max_horz - off_horz, 0.05, header_color )
string.format( "box[%0.2f,%0.2f;%0.2f,%0.2f;%s]", off_horz - 0.1, off_vert + depth - 0.05, max_horz - off_horz, 0.05, header_color )
end
for c_idx, c in ipairs( r.cols ) do
@ -228,33 +322,37 @@ markup.get_formspec_string = function ( rows, min_horz, min_vert, max_horz, max_
end
if c.type == "text" then
if normal_color and normal_color ~= "#FFFFFF" then
local color_escape = minetest.get_color_escape_sequence( normal_color )
c.text = color_escape .. c.text
c.text = string.gsub( c.text, string.char( 0x1b ) .. "%(c@#ffffff%)", color_escape )
end
if r.type == "header" then
formspec = formspec ..
string.format( "textarea[%0.2f,%0.2f;%0.2f,%0.2f;;%s;]", off_horz + 0.0, off_vert, width, depth + depth / 6,
string.format( "textarea[%0.2f,%0.2f;%0.2f,%0.2f;;%s;]", off_horz + 0.3, off_vert, width, depth + depth / 6,
minetest.formspec_escape( c.text ) )
end
formspec = formspec ..
-- correct for oddity with textarea stretching
string.format( "textarea[%0.2f,%0.2f;%0.2f,%0.2f;;%s;]", off_horz, off_vert, width, depth + depth / 6,
-- NB: correct for odditiese with dimension and position of textarea
string.format( "textarea[%0.2f,%0.2f;%0.2f,%0.2f;;%s;]", off_horz + 0.3, off_vert, width, depth + depth / 6,
minetest.formspec_escape( c.text ) )
elseif c.type == "form" then
formspec = formspec .. string.format( "field[%0.1f,%0.1f;%0.1f,1.3;userdata;;%s]", off_horz, off_vert, width,
formspec = formspec .. string.format( "field[%0.1f,%0.1f;%0.1f,1.3;userdata;;%s]", off_horz + 0.3, off_vert, width,
minetest.formspec_escape( c.text ) )
elseif c.type == "item" then
-- NB: image must be shifted left to correspond with formspec text
if depth >= 0.5 then
formspec = formspec .. string.format( "image[%0.1f,%0.1f;%0.1f,%0.1f;%s]",
off_horz - 0.3, off_vert, math.min( width, depth ), math.min( width, depth ), c.text )
off_horz, off_vert, math.min( width, depth ), math.min( width, depth ), c.text )
end
elseif c.type == "skin" then
-- NB: image must be shifted left to correspond with formspec text
if depth >= 0.5 then
formspec = formspec .. string.format( "image[%0.1f,%0.1f;%0.1f,%0.1f;%s]",
off_horz - 0.3, off_vert, math.min( width, depth / 2 ), math.min( width * 2, depth ), c.text )
off_horz, off_vert, math.min( width, depth / 2 ), math.min( width * 2, depth ), c.text )
end
end