You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

573 lines
22 KiB

--===========================================================================--
-- --
-- System.Text.TemplateString --
-- --
--===========================================================================--
--===========================================================================--
-- Author : kurapica125@outlook.com --
-- URL : http://github.com/kurapica/PLoop --
-- Create Date : 2020/10/03 --
-- Update Date : 2020/10/03 --
-- Version : 1.0.0 --
--===========================================================================--
PLoop(function(_ENV)
namespace "System.Text"
--- The template string render type
__Sealed__() __AutoIndex__()
enum "TemplateStringRenderType" {
"RecordLine",
"StaticText",
"NewLine",
"LuaCode",
"Expression",
}
--- Represents the template string loader, used to analyze the template string
-- to generate the template string operations, so the template string will be
-- converted to a class that can be used to generate the result string based on
-- the data.
__Sealed__() interface "ITemplateStringLoader" (function(_ENV)
extend "Iterable"
export {
yield = coroutine.yield,
RCT_RecordLine = TemplateStringRenderType.RecordLine,
RCT_StaticText = TemplateStringRenderType.StaticText,
RCT_NewLine = TemplateStringRenderType.NewLine,
}
-----------------------------------------------------------------------
-- method --
-----------------------------------------------------------------------
__Iterator__()
function GetIterator(self, reader) return self:ParseLines(reader) end
--- Parse the lines and yield all content with type
__Abstract__() function ParseLines(self, reader)
for line in reader:ReadLines() do
line = line:gsub("%s+$", "")
yield(RCT_RecordLine, line)
yield(RCT_StaticText, line)
yield(RCT_NewLine)
end
end
end)
--- The default implementation or the template string loader
__Sealed__() class "DefaultTemplateStringLoader" (function(_ENV)
extend "ITemplateStringLoader"
--[=============================[
Rules
I. Full-Line :
Using :
@> lua code
@ keyword + other lua code
Example :
@local x = math.random(100)
@if x > 50
<p> Above 50 </p>
@else
<p> Below 50 </p>
@end
II. In-Line :
Using :
@expression
Example :
@x
@(x+y:%3d)
@"test"
@[[test]]
@self.Data[1].Name:lower()
--]=============================]
export {
yield = coroutine.yield,
loadstring = _G.loadstring or _G.load,
safeset = Toolset.safeset,
RCT_RecordLine = TemplateStringRenderType.RecordLine,
RCT_StaticText = TemplateStringRenderType.StaticText,
RCT_NewLine = TemplateStringRenderType.NewLine,
RCT_LuaCode = TemplateStringRenderType.LuaCode,
RCT_Expression = TemplateStringRenderType.Expression,
_LuaKeyWords = {
["break"] = true,
["do"] = true,
["else"] = true,
["for"] = true,
["if"] = true,
["elseif"] = true,
["return"] = true,
["repeat"] = true,
["while"] = true,
["until"] = true,
["end"] = true,
["function"] = true,
["local"] = true,
},
}
local _PrefixMap = {
["("] = false,
["["] = false,
["{"] = false,
["'"] = false,
['"'] = false,
["+"] = false,
["-"] = false,
["\\"] = System.Text.XmlEntity.Encode,
}
-------------------------------
-- Helper
-------------------------------
-- Get expression from the line
local function parseExpression(line, startp)
local parseCnt
local code = ""
local parsePrintTemp= function(word) code = code .. word return "" end
-- Find the start
startp = line:find("%S", startp)
if not startp then return end
-- Check the prefix
local prest, prend = line:find("^%p", startp)
local prefix
if prest and prend then
prefix = line:sub(prest, prend)
if not _PrefixMap[prefix] then
prefix = nil
else
startp = line:find("%S", prend + 1)
if not startp then return end
end
end
-- Check the expression
line = line:sub(startp)
if line:find("^%(") then
-- Just take the expression inside the brackets
line = line:gsub("^%b()", parsePrintTemp)
elseif line:find("^[+-]?%s*[%d%.]") then
-- First word is a number
if line:find("^[+-]?%s*0[xX]") then
-- Hex [+-]?0x[0-9a-fA-F]*
line = line:gsub("^[+-]?%s*0x[0-9a-fA-F]*", parsePrintTemp)
elseif line:find("^[+-]?%s*%d") then
-- [+-]?%d+%.?%d*[eE]?%-?%d*
line = line:gsub("^([+-]?%s*%d+%.?%d*)([eE]?)(%-?%d*)", function(start, e, tail)
if #e > 0 then
code= code .. start .. e .. tail
else
code= code .. start
end
end)
else
-- [+-]?%.%d*[eE]?%-?%d*
line = line:gsub("^([+-]?%s*%.%d*)([eE]?)(%-?%d*)", function(start, e, tail)
if #e > 0 then
code = code .. start .. e .. tail
else
code = code .. start
end
end)
end
elseif line:find("^'") then
-- The word is a string
line = line:gsub("^%b''", parsePrintTemp)
elseif line:find('^"') then
-- The word is a string
line = line:gsub('^%b""', parsePrintTemp)
elseif line:find("^%[=*%[") then
-- The word is a string
line = line:gsub("^%b[]", parsePrintTemp)
elseif line:find("^[%w_]") then
-- variable first
line = line:gsub("^([%w_]+)%s*", parsePrintTemp)
-- parse the remain expression
while line ~= "" do
local head = line:sub(1, 1)
parseCnt = 0
if head == "." or head == ":" then
line, parseCnt = line:gsub("^([%.:][_%a][%w_]*)%s*", parsePrintTemp)
elseif head == "{" then
line, parseCnt = line:gsub("^(%b{})%s*", parsePrintTemp)
elseif head == "(" then
line, parseCnt = line:gsub("^(%b())%s*", parsePrintTemp)
elseif head == "[" then
line, parseCnt = line:gsub("^(%b[])%s*", parsePrintTemp)
end
if parseCnt == 0 then break end
end
end
if code ~= "" then
-- Check format
local prev, fmt = code:match("^%((.+):(%%[^%(]+)%)$")
local rcode = prev and ("(" .. prev .. ")") or code
if loadstring("return " .. rcode) then
return rcode, startp + #code - 1, fmt, prefix
end
end
end
local function parsePageLine(reader, line)
local cnt
-- Full Line
--- @-- comment
-- No multi-line comment supported,
-- since <!-- --> can also be used without @
if line:find("^%s*@%s*%-%-.*$") then return end
--- @> full line
line, cnt = line:gsub("^%s*@>(.*)$", "%1")
if cnt > 0 then
yield(RCT_LuaCode, line)
return
end
--- @ keyword
if _LuaKeyWords[line:match("^%s*@%s*(%w+)")] then
line = line:gsub("^%s*@%s*(%w+)(.*)$", "%1%2")
yield(RCT_LuaCode, line)
return
end
--- Lua block
if line:find("^%s*@%s*{$") then
local prevSpace = line:match("^%s*")
line = reader:ReadLine()
while line do
yield(RCT_RecordLine, line)
line = line:gsub("%s+$", "")
if line:find("^%s*}$") and line:match("^%s*") == prevSpace then
break
else
yield(RCT_LuaCode, line)
end
line = reader:ReadLine()
end
if not line then error("'@{' must be ended with '}'.") end
return
end
-- Inline
local startp = 1
local pos = line:find("@", 1, true)
while pos do
local chr = line:match("%S", pos + 1)
if chr == "@" then
-- Skip next @
pos = line:find("@", pos + 1, true)
else
-- expression
local exp, endp, format, prefix = parseExpression(line, pos + 1)
if exp then
--- The previous text should be output directly
local prev = line:sub(startp, pos - 1)
if prev and prev ~= "" then
prev = prev:gsub("@@", "@")
yield(RCT_StaticText, prev)
end
startp = pos
-- Expression
yield(RCT_Expression, exp, format, prefix and _PrefixMap[prefix])
-- Continue
startp = endp + 1
pos = endp
end
end
pos = line:find("@", pos + 1, true)
end
local last = line:sub(startp)
if last and last ~= "" then
last = last:gsub("@@", "@")
yield(RCT_StaticText, last)
end
yield(RCT_NewLine)
end
-----------------------------------------------------------------------
-- static property --
-----------------------------------------------------------------------
--- The prefix to function map like `\=>Encode`, so the `@\test => Encode(test)`
__Indexer__(struct { __base = NEString,
function (val, onlyvalid)
if not val:match("^%p$") then
return onlyvalid or "%s must all be punctuation"
end
end
}) __Static__()
property "Prefix" {
type = Callable,
set = function(self, prefix, map)
if _PrefixMap[prefix] == nil then
_PrefixMap = safeset(_PrefixMap, prefix, map)
end
end,
}
-----------------------------------------------------------------------
-- method --
-----------------------------------------------------------------------
function ParseLines(self, reader)
local line = reader:ReadLine()
while line do
line = line:gsub("%s+$", "")
yield(RCT_RecordLine, line)
if line ~= "" then
parsePageLine(reader, line)
end
line = reader:ReadLine()
end
end
end)
__Sealed__() __Final__()
class "TemplateString" (function (_ENV)
export {
with = with,
loadstring = _G.loadstring or _G.load,
loadsnippet = Toolset.loadsnippet,
pcall = pcall,
error = error,
assert = assert,
tonumber = tonumber,
tostring = tostring,
random = math.random,
strlower = string.lower,
strformat = string.format,
strfind = string.find,
strsub = string.sub,
strgsub = string.gsub,
tblconcat = table.concat,
tinsert = table.insert,
tremove = table.remove,
setmetatable = setmetatable,
safeset = Toolset.safeset,
_PL_tostring = Toolset.tostring or tostring,
setfenv = _G.setfenv or _G.debug and debug.setfenv or Toolset.fakefunc,
META_ITEMS = { __index = _G },
RCT_RecordLine = TemplateStringRenderType.RecordLine,
RCT_StaticText = TemplateStringRenderType.StaticText,
RCT_NewLine = TemplateStringRenderType.NewLine,
RCT_LuaCode = TemplateStringRenderType.LuaCode,
RCT_Expression = TemplateStringRenderType.Expression,
StringReader, StringWriter, DefaultTemplateStringLoader, TextWriter,
}
--- Convert the error to the real position
local function raiseError(self, err)
err = tostring(err)
local line, msg = err:match("%b[]:(%d+):(.-)$")
line = tonumber(line)
if line and self.CodeLineMap[line] then
err = ("template string:%d: %s"):format(self.CodeLineMap[line], msg)
end
error(err, 0)
end
local function prepareGenerator(self, items)
for k, v in pairs(self.APIs) do
items[k] = v
end
setmetatable(items, META_ITEMS)
local func, err = pcall(self.Generator)
if not func then
raiseError(self, err)
else
func = err
end
setfenv(func, items)
return func
end
-----------------------------------------------------------------------
-- property --
-----------------------------------------------------------------------
--- The template string
property "TemplateString" { type = String }
--- The code line map
property "CodeLineMap" { set = false, default = Toolset.newtable }
--- The apis could be used in the template string
property "APIs" { set = false, default = function() return { _PL_tostring = function(val) return val == nil and "" or _PL_tostring(val) end } end }
--- The function used to return the generator of the result
property "Generator" { }
-----------------------------------------------------------------------
-- constructor --
-----------------------------------------------------------------------
__Arguments__{ NEString, -ITemplateStringLoader/nil }
function __ctor(self, template, loader)
with(StringReader(template))(function(reader)
reader.DiscardIndents = true
self.TemplateString = template
local recordCount = 0
local definition = { "return function(_ENV, writer)" }
local engine = (loader or DefaultTemplateStringLoader)()
local apis = self.APIs
local temp = {}
local wNewLine = [[writer:WriteLine()]]
local wStaticText = [[writer:Write(%q)]]
local wExpression = [[writer:Write(_PL_tostring(%s))]]
local wWrapExp = [[writer:Write(%s(_PL_tostring(%s)))]]
local wFmtExpression = [[writer:Write((%q):format(_PL_tostring(%s)))]]
local wFmtWrapExp = [[writer:Write(%s((%q):format(_PL_tostring(%s))))]]
local sourceCount = 1
local defineCount = 1
local lineMap = self.CodeLineMap
local pushcode = function (line)
defineCount = defineCount + 1
definition[defineCount] = line
sourceCount = sourceCount + 1
lineMap[sourceCount] = recordCount
end
for ty, ct, fmt, wrap in engine:GetIterator(reader) do
if ty == RCT_RecordLine then
recordCount = recordCount + 1
elseif ty == RCT_LuaCode then
pushcode(ct)
elseif ty == RCT_StaticText then
if ct ~= "" then
pushcode(wStaticText:format(ct))
end
elseif ty == RCT_NewLine then
pushcode(wNewLine)
elseif ty == RCT_Expression then
if fmt then
if wrap then
if not temp[wrap] then
local name = "_PL_" .. strformat("%04X", random(0xffff))
while apis[name] do
name = "_PL_" .. strformat("%04X", random(0xffff))
end
temp[wrap] = name
apis[name] = wrap
end
pushcode(wFmtWrapExp:format(temp[wrap], fmt, ct))
else
pushcode(wFmtExpression:format(fmt, ct))
end
else
if wrap then
if not temp[wrap] then
local name = "_PL_" .. strformat("%04X", random(0xffff))
while apis[name] do
name = "_PL_" .. strformat("%04X", random(0xffff))
end
temp[wrap] = name
apis[name] = wrap
end
pushcode(wWrapExp:format(temp[wrap], ct))
else
pushcode(wExpression:format(ct))
end
end
end
end
tinsert(definition, "end")
definition = tblconcat(definition, "\n")
local ok, err = loadsnippet(definition, "template")
if not ok then
raiseError(self, err)
end
self.Generator = ok
end)
end
-----------------------------------------------------------------------
-- meta-method --
-----------------------------------------------------------------------
__Arguments__{ RawTable/nil }
function __call(self, items)
items = items or {}
local func = prepareGenerator(self, items)
return with(StringWriter())(function(writer)
func(items, writer)
return writer
end, function(err)
raiseError(self, err)
end).Result
end
__Arguments__{ TextWriter, RawTable/nil }
function __call(self, writer, items)
items = items or {}
local func = prepareGenerator(self, items)
with(writer)(function(writer)
func(items, writer)
return writer
end, function(err)
raiseError(self, err)
end)
end
__Arguments__{ Callable, RawTable/nil }
function __call(self, write, items)
items = items or {}
local func = prepareGenerator(self, items)
with(TextWriter{ Write = function(self, ...) return write(...) end })(function(writer)
func(items, writer)
end, function(err)
raiseError(self, err)
end)
end
end)
end)