665 lines
22 KiB
Lua
Raw Normal View History

2021-10-02 19:52:25 -04:00
-- _ _ ____ ___
-- /\/\ __ _ _ __| | ____| | _____ ___ __ |___ \ / __\__ _ __ _ __ ___ ___ _ __ ___ ___
-- / \ / _` | '__| |/ / _` |/ _ \ \ /\ / / '_ \ __) |/ _\/ _ \| '__| '_ ` _ \/ __| '_ \ / _ \/ __|
-- / /\/\ \ (_| | | | < (_| | (_) \ V V /| | | |/ __// / | (_) | | | | | | | \__ \ |_) | __/ (__
-- \/ \/\__,_|_| |_|\_\__,_|\___/ \_/\_/ |_| |_|_____\/ \___/|_| |_| |_| |_|___/ .__/ \___|\___|
-- |_|
--
-- Markdown2Formspec (md2f)
-- MIT License
-- Copyright ExeVirus 2021
--
-- This file has all local parsing functions first,
-- followed by the global api access for other mods
-- to work with markdown 2 formspec.
----------------Local Functions----------------
local function loadFile(filename)
local file = io.open(filename, "rb") -- r read mode and b binary mode
if not file then return nil end
local content = file:read "*a" -- *a or *all reads the whole file
file:close()
return content
end
-- The returned formspec header
local function header(x,y,w,h,name)
return "hypertext["..x..","..y..";"..w..","..h..";"..name..";"
end
-- The returned formspec footer
local function footer()
return "]"
end
-- Predeclare parseLine, because it's a large function,
-- and it makes sense to have it defined later
local parseLine = nil
-- The main parsing function for md2f
local function unpack(text,width, settings)
2021-10-02 19:52:25 -04:00
-- 1. Convert newlines to \n
text = text:gsub("\r\n", "\n") --windows
text = text:gsub("\r", "\n") -- MacOs 9 and older
-- 2. Break apart lines into array
lines = {}
for s in text:gmatch("([^\n]*)\n?") do
table.insert(lines, s)
end
-- 3. declare tracking table for keeping track of our state (text, bold, block quote, etc.)
local state = {
formspec = "",
width = width,
carried_text = "",
settings = settings
2021-10-02 19:52:25 -04:00
}
-- 3a. Handle global settings
if state.settings ~= nil then
--handle if not all settings are set
if settings.background_color and
settings.font_color and
settings.heading_1_color and
settings.heading_2_color and
settings.heading_3_color and
settings.heading_4_color and
settings.heading_5_color and
settings.heading_6_color and
settings.code_block_mono_color and
settings.code_block_font_size and
settings.mono_color and
settings.block_quote_color
then
--set the globals
state.formspec = "<global background="..settings.background_color.. " color=".. settings.font_color ..">"
else
state.settings = nil
end
end
2021-10-02 19:52:25 -04:00
-- 4. iterate over lines, parsing linearly
for lineNumber=1, #lines, 1 do
parseLine(lines[lineNumber], state) --state is changed within function
end
-- 5. return the parsed formspec
2021-10-02 19:52:25 -04:00
return state.formspec
end
-- parseLine helper functions
local function trim(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
--Remove extra whitespace in between words or characters
local function trimMd(s)
return s:gsub("\t", " "):gsub("%s+", " ")
end
local function escapeHypertext(s)
return s:gsub("<", "\\<"):gsub(";","\\;"):gsub("]","\\]")
end
2021-10-02 19:52:25 -04:00
------------------------------------------------------------
-- escape()
-- escapes any characters that need escaped
------------------------------------------------------------
local function escape(text)
local function replace(char)
text = text:gsub("\\"..char, "Qqo"..string.byte(char:sub(-1)).."Qqo")
end
replace("\\")
replace("`")
replace("%*")
replace("_")
replace("{")
replace("}")
replace("%[")
replace("]")
replace("<")
replace(">")
replace("%(")
replace("%)")
replace("#")
replace("%+")
replace("%-")
replace("%.")
replace("!")
replace("|")
return text
end
------------------------------------------------------------
-- escape()
-- reverts any characters that are escaped
------------------------------------------------------------
local function unescape(text)
local function replace(char)
text = text:gsub("Qqo"..string.byte(char:sub(-1)).."Qqo", char)
end
replace("\\")
replace("`")
replace("%*")
replace("_")
replace("{")
replace("}")
replace("%[")
replace("]")
replace("<")
replace(">")
replace("%(")
replace("%)")
replace("#")
replace("%+")
replace("%-")
replace("%.")
replace("!")
replace("|")
return text
end
------------------------------------------------------------
-- emphasisParse()
-- handles bold, italics, etc. for a group of text
-- It also handles formspec escaping such as \] and \\\\ messiness
-- text: text to be added
------------------------------------------------------------
local function emphasisParse(text, state)
2021-10-02 19:52:25 -04:00
text = escape(text)
local finished = false
local last = 1
while finished == false do
if text:find("<%S.-%S>", last) then --url
local start,finish = text:find("<%S.-%S>", last)
text = text:sub(1,start-1) .. "<style color=#77AAFF>" .. text:sub(start+1,finish-1) .. "</style>" .. text:sub(finish+1)
2021-10-02 19:52:25 -04:00
last = finish + 36 --number of characters added minus 2
else
finished = true
end
end
finished = false
while finished == false do
if text:find("`%S.-%S`") then --monospaced
local start,finish = text:find("`%S.-%S`")
local mono_start, mono_end
if state.settings ~= nil then
mono_start = "<mono><style color=".. state.settings.mono_color ..">"
mono_end = "</mono></style>"
else
mono_start = "<mono>"
mono_end = "</mono>"
end
text = text:sub(1,start-1) .. mono_start .. text:sub(start+1,finish-1) .. mono_end .. text:sub(finish+1)
2021-10-02 19:52:25 -04:00
else
finished = true
end
end
finished = false
while finished == false do
if text:find("%*%*%*%S.-%S?%*%*%*") then --Handle all Bold and Italics combo first
local start,finish = text:find("%*%*%*%S.-%S?%*%*%*")
text = text:sub(1,start-1) .. "<b><i>" .. text:sub(start+3,finish-3) .. "</b></i>" .. text:sub(finish+1)
elseif text:find("%*%*%S.-%S%*%*") then -- then all bold
local start,finish = text:find("%*%*%S.-%S%*%*")
text = text:sub(1,start-1) .. "<b>" .. text:sub(start+2,finish-2) .. "</b>" .. text:sub(finish+1)
elseif text:find("%*%S.-%S%*") then -- then all italics
local start,finish = text:find("%*%S.-%S%*")
text = text:sub(1,start-1) .. "<i>" .. text:sub(start+1,finish-1) .. "</i>" .. text:sub(finish+1)
else
finished = true
end
end
text = unescape(text)
--Now to handle formspec escaping
text = text:gsub("]", "\\]") -- ]
text = text:gsub(";", "\\;") -- ;
2021-10-02 19:52:25 -04:00
return escapeHypertext(text)
2021-10-02 19:52:25 -04:00
end
--This function will close all states such as
-- bold, italics, block quote, etc.
local function finishParse(state)
--Finish Plain Text first
if state.carried_text ~= "" then
state.formspec = state.formspec .. emphasisParse(state.carried_text, state) .. "\n"
2021-10-02 19:52:25 -04:00
state.carried_text = ""
end
--Finish Block Quote
if state.in_quote then
--revert the color and add the bottom image
state.formspec = state.formspec ..
"<img name=halo width=".. (60*state.width) * 0.8 .." height=5>\n"
if state.settings ~= nil then
state.formspec = state.formspec .. "<global color=".. state.settings.font_color ..">"
else
state.formspec = state.formspec .. "<global color=#FFF>"
end
2021-10-02 19:52:25 -04:00
state.in_quote = nil
end
--Finish Ordered List
state.in_ordered_list = nil
--Finish Unordered List
state.in_unordered_list = nil
end
------------------------------------------------------------
-- handlePlainText()
-- Handles Emphasis and carries text until the end of a
-- paragraph or similar
-- text: text to be added
-- state: Shared state variable of parser
------------------------------------------------------------
local function handlePlainText(text, state)
--track if we handled it
local handled = false
if text:find(".+") then -- 'something' sanity check
state.carried_text = state.carried_text .. " " .. text
handled = true
end
return handled
end
------------------------------------------------------------
-- handleHeading()
-- Checks/Handles if the given line is a '## heading' line
-- Automatically ends previous lines
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleHeading(line, state)
--track if we handled it
local handled = false
if line:find("^#+%s+") then --heading
local _,_,text = line:find("^#+%s+(.*)")
text = text or "" -- handle no characters
text = trimMd(text) -- remove superluous whitespace
local _,count = line:find("#+")
--check that this is a valid header:
if text:find("%w") and count > 0 and count < 7 then
-- Finish any previous parsing whenever a new header is started
finishParse(state)
--handle colors from settings
local color = { [1]="", [2]="", [3]="", [4]="", [5]="", [6]=""}
if state.settings ~= nil then
color = {
[1]=" color=" .. state.settings.heading_1_color,
[2]=" color=" .. state.settings.heading_2_color,
[3]=" color=" .. state.settings.heading_3_color,
[4]=" color=" .. state.settings.heading_4_color,
[5]=" color=" .. state.settings.heading_5_color,
[6]=" color=" .. state.settings.heading_6_color,
}
end
2021-10-02 19:52:25 -04:00
if count == 1 then
state.formspec = state.formspec .. "<style size=48"..color[1]..">"..emphasisParse(text, state).."</style>\n"
2021-10-02 19:52:25 -04:00
elseif count == 2 then
state.formspec = state.formspec .. "<style size=36"..color[2]..">"..emphasisParse(text, state).."</style>\n"
2021-10-02 19:52:25 -04:00
elseif count == 3 then
state.formspec = state.formspec .. "<style size=30"..color[3]..">"..emphasisParse(text, state).."</style>\n"
2021-10-02 19:52:25 -04:00
elseif count == 4 then
state.formspec = state.formspec .. "<style size=24"..color[4]..">"..emphasisParse(text, state).."</style>\n"
2021-10-02 19:52:25 -04:00
elseif count == 5 then
state.formspec = state.formspec .. "<style size=16"..color[5]..">"..emphasisParse(text, state).."</style>\n"
2021-10-02 19:52:25 -04:00
elseif count == 6 then
state.formspec = state.formspec .. "<style size=12"..color[6]..">"..emphasisParse(text, state).."</style>\n"
2021-10-02 19:52:25 -04:00
end
handled = true
end
end
return handled
end
------------------------------------------------------------
-- handleQuote()
-- Checks/Handles if the given line is a '> quote' line
-- Automatically ends previous lines
-- Note: even though quotes are supposed to be contiguous, we can't
-- do indents safely, so each will be considered on its own. We'll
-- use an image on the left and different colored text. Probably
-- will become a setting.
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleQuote(line, state)
--track if we handled it
local handled = false
local is_quote = false
local _,text = nil,""
if line:find("^>%s+") then --quote
_,_,text = line:find("^>%s+(.*)")
is_quote = true
elseif line:find("^>") then -- empty quote
_,_,text = line:find("^>(.*)")
is_quote = true
end
if is_quote then
text = text or "" -- handle no characters
text = trimMd(text) -- remove superluous whitespace
if not state.in_quote then
--Finish any previous lines
finishParse(state)
--Place the image bar on the top
state.formspec = state.formspec ..
"<img name=halo width=" .. (60*state.width) * 0.8 .. " height=5>\n"
2021-10-02 19:52:25 -04:00
--Change the text color
if state.settings ~= nil then
state.formspec = state.formspec .. "<global color=".. state.settings.block_quote_color ..">"
else
state.formspec = state.formspec .. "<global color=#CC8>"
end
2021-10-02 19:52:25 -04:00
--Then modify our state to being in a quote block
state.in_quote = true
else
if text == "" or text == " " then
state.in_quote = false
finishParse(state)
state.in_quote = true
end
end
end
if is_quote and state.in_quote then
--Now process as normal plaintext (which handles continuations)
handlePlainText(text, state)
handled = true
end
return handled
end
------------------------------------------------------------
-- handleCodeBlock()
-- handles ``` codeblocks
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleCodeBlock(line, state)
--track if we handled it
local handled = false
if state.block_quote then
if line:find("^```") then
state.block_quote = nil
state.formspec = state.formspec .. "</mono>"
if state.settings ~= nil then
state.formspec = state.formspec .. "</style>"
end
handled = true
else
state.formspec = state.formspec .. escapeHypertext(line) .. "\n"
handled = true
end
else
if line:find("^```") then
--Finish any previous lines
finishParse(state)
state.block_quote = true
state.formspec = state.formspec .. "<mono>"
if state.settings ~= nil then
state.formspec = state.formspec ..
"<style color=".. state.settings.code_block_mono_color
.." size=".. state.settings.code_block_font_size .. ">"
end
handled = true
end
end
return handled
end
2021-10-02 19:52:25 -04:00
------------------------------------------------------------
-- handleOrderedList()
-- Any number followed by a '.' and space will
-- count for this List
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleOrderedList(line, state)
--track if we handled it
local handled = false
if line:find("^%d+%.%s+") then -- '#. '
local _,_,text = line:find("^%d+%.%s+(.*)")
text = text or "" -- handle no characters
text = trimMd(text) -- remove superluous whitespace
if not state.in_ordered_list then
--Finish any previous lines
finishParse(state)
--Place the number followed by the line
state.formspec = state.formspec .. " 1. " .. emphasisParse(text, state) .. "\n"
2021-10-02 19:52:25 -04:00
-- Then make note of our state
state.in_ordered_list = true
state.listnum = 2
else
-- Place next number followed by line
state.formspec = state.formspec .. " "..state.listnum.. ". " .. emphasisParse(text, state) .. "\n"
2021-10-02 19:52:25 -04:00
-- Increment for next number
state.listnum = state.listnum + 1
end
handled = true
end
return handled
end
------------------------------------------------------------
-- handleUnorderedList()
-- Any number followed by a '.' and space will
-- count for this List
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleUnorderedList(line, state)
--track if we handled it
local handled = false
if line:find("^[%-%*]%s+") then -- '#. '
local _,_,text = line:find("^[%-%*]%s+(.*)")
text = text or "" -- handle no characters
text = trimMd(text) -- remove superluous whitespace
if not state.in_unordered_list then
--Finish any previous lines
finishParse(state)
--Place the number followed by the line
state.formspec = state.formspec .. " - " .. emphasisParse(text, state) .. "\n"
2021-10-02 19:52:25 -04:00
-- Then make note of our state
state.in_unordered_list = true
else
state.formspec = state.formspec .. " - " .. emphasisParse(text, state) .. "\n"
2021-10-02 19:52:25 -04:00
end
handled = true
end
return handled
end
------------------------------------------------------------
-- handleImage()
-- ![###,###](filename) the ###,### are optional, but if present
-- allows the image to be absolutely sized.
-- i.e. 25,25 = 25width/height in pixels
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleImage(line, state)
--track if we handled it
local handled = false
if line:find("^!%[%d*,?%d*%]%(%w+%)") then -- ![##,##](filename)
--Finish any previous lines
finishParse(state)
local _,_,w,h,filename = line:find("^!%[(%d*),?(%d*)%]%((%w+)%)")
if w ~= "" and h ~= "" then
state.formspec = state.formspec .. "<global halign=center>\n<img name="..escapeHypertext(filename).." width="..w.." height="..h.."><global halign=left>\n"
2021-10-02 19:52:25 -04:00
else
state.formspec = state.formspec .. "<global halign=center>\n<img name="..escapeHypertext(filename).."><global halign=left>\n"
2021-10-02 19:52:25 -04:00
end
handled = true
end
return handled
end
------------------------------------------------------------
-- handleHorizontalRule()
-- ___ or *** or --- or more on a line by themselves will result
-- in a horizontal rule (line) output
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleHorizontalRule(line, state)
--track if we handled it
local handled = false
-- track if it matched any of the line patterns
local doline = false
if line:find("^%-%-%-+") then -- '---'
local _,_,stuff = line:find("^%-%-%-+(.*)")
if stuff == "" then
doline = true
end
elseif line:find("^___+") then -- '___'
local _,_,stuff = line:find("^___+(.*)")
if stuff == "" then
doline = true
end
elseif line:find("^%*%*%*+") then -- '***'
local _,_,stuff = line:find("^%*%*%*+(.*)")
if stuff == "" then
doline = true
end
end
if doline then
--Finish any previous lines
finishParse(state)
-- Add horizontal rule
state.formspec = state.formspec .. "<img name=halo width="..(60*state.width).." height=4>\n"
2021-10-02 19:52:25 -04:00
handled = true
end
return handled
end
------------------------------------------------------------
-- handleNewline()
-- Just Finish the previous parsing
-- line: Line to be handled
-- state: Shared state variable of parser
------------------------------------------------------------
local function handleNewLine(line, state)
--track if we handled it
local handled = false
if trimMd(line) == " " or line == "" then -- '\n'
finishParse(state)
handled = true
end
return handled
end
------------------------------------------------------------
-- parseLine()
-- line: line of text to be unparsed
-- state: current state of parser
------------------------------------------------------------
parseLine = function(line, state)
if handleCodeBlock(line,state) then
return
end
2021-10-02 19:52:25 -04:00
-- Remove preceding and trailing whitespace
line = trim(line)
if handleHeading(line,state) then
return
elseif handleQuote(line,state) then
return
elseif handleOrderedList(line,state) then
return
elseif handleUnorderedList(line,state) then
return
elseif handleImage(line,state) then
return
elseif handleHorizontalRule(line,state) then
return
elseif handleNewLine(line,state) then
return
else --plaintext
if state.in_quote or state.in_ordered_list or state.in_unordered_list then
finishParse(state)
end
2021-10-02 19:52:25 -04:00
if handlePlainText(line,state) then
return
end
end
--Only errors should get here
state.formspec = state.formspec or ""
state.formspec = state.formspec + "\nMarkdown2Formspec ERROR, please report\n"
end
-----------------------------------------------------------------------
----------------------------global namespace---------------------------
md2f = {}
-- md2f()
--
-- x: position of hypertext[] element
-- y: position of hypertext[] element
-- w: width of hypertext element. roughly 60 pixels per 1 unit
-- h: height of hypertext element. Scrollbar appears when it overflows
-- text: markdown text to convert
-- name: optional name for hypertext element
md2f.md2f = function(x,y,w,h,text,name,settings)
2021-10-02 19:52:25 -04:00
if text == nil then
minetest.log(filename .. "does not exist or is empty")
2021-10-02 19:52:25 -04:00
return ""
end
name = name or "markdown"
return header(x,y,w,h,name) .. unpack(text,w,settings) .. footer()
2021-10-02 19:52:25 -04:00
end
-- md2ff()
--
-- x: position of hypertext[] element
-- y: position of hypertext[] element
-- w: width of hypertext element. roughly 60 pixels per 1 unit
-- h: height of hypertext element. Scrollbar appears when it overflows
-- file: exact name and location of the markdown file to convert
-- name: optional name for hypertext element
md2f.md2ff = function(x,y,w,h,filename,name,settings)
2021-10-02 19:52:25 -04:00
local text = loadFile(filename)
return md2f.md2f(x,y,w,h,text,name,settings)
2021-10-02 19:52:25 -04:00
end
-- header()
--
-- Default formspec header for the lazy
md2f.header = function()
return "formspec_version[4]size[20,20]position[0.5,0.5]bgcolor[#111E]\n"
end