UPDATE: latest code at the gist: https://gist.github.com/Utsira/9ba147647374a6bd2661
Have you ever wished you could easily format text in Codea, adding different typefaces, bold, italics etc?
I thought I would adapt part of the Markdown syntax, as it’s very easy to write and read (even in the Codea editor), and it’s well known (it’s used on many forums, including this one).
There is a full lua port of markdown, here: https://github.com/stevedonovan/LDoc/blob/master/ldoc/markdown.lua
(ie for converting markdown to HTML)
My code is for outputting to the screen, using Codea’s text
API, and just supports a subset of markdown, namely different levels of headers (including different typefaces), indented block quotes, italic, bold, and bolditalic nesting, typographer’s quotes and long em en-dashes.
Here is a screenshot of what it looks like:
For comparison, I’ve put the same text below, to see how it is rendered on this board.
First though, a question about Lua pattern matching (which I’m relatively new to). I’m using a gmatch
iterator to parse every word (for custom text wrapping, as Codea’s built in wrapping doesn’t tell you where the last line ends, or give you control of first line indent), and to parse the Markdown control characters:
for element, control in string.gmatch(paragraph, "(.-)([%s#*>])") do --separate at white space, #, *, >
The problem is, this only picks up a single instance of #
or *
at a time, so that ###
(Heading 3) registers as 3 separate #
s. I’ve tried adding the +
magic character to the char-set [%s#*>]+
, ie to try to catch one or more #
, but it produces very strange results. Can anyone work out how to extend the char-set to capture 1 or more instances of the control character?
#Top level heading — which has an emphasised word
Some body text
##Second level heading, with a strongly emphasised bit.
Here’s an inline “quotation that nests ‘another quotation’” in the middle of a sentence.
###Third level heading, with some very, very strong emphasis
Let’s add some emphasis, and some strong emphasis, and some really, really strong emphasis here
Here’s a block quote–a longer quotation that is indented in its own block–with some emphasis, strong emphasis, en-dashes — not to mention em-dashes — said by someone famous.
Or you can nest italics inside bold. Cool, no? Unfortunately jumping straight in with bold italic text doesn’t quite work yet. Note the typographer’s ‘quotes’ for the apostrophe and for single quotation marks. Also works for “double” quote marks
--# Main
-- Markdown
function setup()
fontSize(20) --use the regular fontsize command to set the size of the body level text
textImage = Markdown{
width = WIDTH *0.9, --wrap width
text = { --nb each markdown paragraph is a separate item. This is because the new line character \
is not particularly easy to write or to read in the Codea editor. nb2 double quotes must be escaped: \" this messes up Codea's syntax highlighting, but it does run
"#Top level heading --- which has an *emphasised* word",
"Some body text",
"##Second level heading, with a **strongly emphasised** bit.",
"Here's an inline \"quotation that nests '*another* quotation'\" in the middle of a sentence.",
"###Third level heading, with some **very, *very* strong emphasis**",
"Let's add some *emphasis*, and some **strong emphasis**, and some *really, **really** strong emphasis* here",
"> Here's a block quote--a longer quotation that is indented *in its own block*--with *some emphasis, **strong** emphasis*, en-dashes --- not to mention **em-dashes** --- said by someone famous.",
"**Or you can nest *italics* inside bold**. Cool, no? Unfortunately jumping straight in with ***bold italic*** text doesn't quite work yet. Note the typographer's 'quotes' for the apostrophe and for single quotation marks. Also works for \"double\" quote marks"}
}
end
function draw()
background(40, 40, 50)
sprite(textImage, WIDTH*0.5, HEIGHT*0.5)
end
--# Markdown
--markdown-esque string formatting
local style = { --simple cascading styles
body = { --body format
font = "Georgia", --will crash if font doesnt have bold, italic, boldItalic variants
col = color(0, 129, 255, 255)
},
heading = { --the array part of style.heading contains styles for heading1, heading2, etc (overrides global)
{size=2}, --Heading1. Size is proportion of body size
{size=1.5}, --Heading2
{size=1.2}, --Heading 3
all = { --global headings settings
font = "HelveticaNeue",
col = color(0, 177, 255, 255)
}
},
block = { --block quotes
size = 0.9,
font = "HelveticaNeue",
col = color(120, 132, 144, 255)}
}
local face --name of base font currently being used
local size --size of base font
function Markdown(t)
textMode(CORNER)
local _, parSep = textSize("dummy") --paragraph separation defined by body style
size = fontSize() --base size of body text
local img = image(t.width, HEIGHT) --height
setContext(img)
textWrapWidth(0) --we need to turn off text wrapping and implement our own because the built-in wrapping does not give us control over the point at which the text starts (first line indentation), nor tell us where the last line ends.
local cursor = vec2(0,HEIGHT)
local italic = false
local bold = false
local headLevel = 0 --level of heading
local indent = 0 --for block quotations
for _, paragraph in ipairs(t.text) do
--PRE-PROCESS TYPOGRAPHY
paragraph = string.gsub(paragraph, "(%S+)'", "%1\\u{2019}") --right single quote. Do this first in order to catch apostrophes
paragraph = string.gsub(paragraph, "'(%S+)", "\\u{2018}%1") --left single quote
paragraph = string.gsub(paragraph, "%-%-%-", "\\u{2014}") --em-dash
paragraph = string.gsub(paragraph, "%-%-", "\\u{2013}") --en-dash
paragraph = string.gsub(paragraph, "\"(%S+)", "\\u{201C}%1") --left double quote
paragraph = string.gsub(paragraph, "(%S+)\"", "%1\\u{201D}") --right double quote
--RESET TO DEFAULT BODY FONT FOR NEW PARAGRAPH
style.set(style.body)
cursor.x = 0
headLevel = 0
indent = 0
fontSize(size)
paragraph = paragraph.."\
" --add return (this also allows final part of line to be captured)
local cursorSet = false --set to true once initial cursor position for paragraph is set according to font size, paragraph separation
local prevControl, prevPrevControl --remember the previous control characters, in order to count number of * etc
for element, control in string.gmatch(paragraph, "(.-)([%s#*>])") do --separate at white space, #, *, >
if control==" " and prevControl~=">" then --if whitespace
element = element.." " --put spaces back in
end
--HEADINGS
if control=="#" then
style.set(style.heading.all) --global heading settings
headLevel = headLevel + 1
style.set(style.heading[headLevel]) --level specific settings
end
--BLOCK
if control==">" then
indent = size * 3 --indent paragraph
cursor.x = indent
style.set(style.block)
end
local w,h = textSize(element)
if t.debug then print(element,control) end --debug print
if not cursorSet then --place first line of paragraph (paragraph separation etc)
cursor.y = cursor.y - (h+parSep)
cursorSet = true
end
--WRAPPING
if cursor.x + w > t.width then --if word will take us over edge
cursor.x = indent --carriage return
cursor.y = cursor.y - h
end
text(element, cursor.x, cursor.y) --print word
cursor.x = cursor.x + w
--BOLD AND ITALICS
if control=="*" then
--[[
if prevControl=="*" and prevPrevControl=="*" and element=="" then --three asterisks with nothing separating them
italic = not italic --reinstate previously cancelled-out italics false flag
print ("BO-IT")
else
]]
if prevControl=="*" and element=="" then --two asterisks with nothing separating them
bold = not bold
italic = not italic --cancel out previous italics false flag
else
italic = not italic
end
if bold and italic then
font(face.."-BoldItalic")
elseif bold then
font(face.."-Bold")
elseif italic then
font(face.."-Italic")
else
font(face)
end
end
prevControl = control --remember control code, to count no of asterisks
-- prevPrevControl = prevControl
end
end
setContext()
return img
end
function style.set(sty)
for func, val in pairs(sty) do --set font features for whatever keys are in the style table
style[func](val)
end
end
--the function names below correspond to the bottom level keys in the style table, eg font, col, size
function style.font(f)
face = f
font(face)
end
function style.col(col)
fill(col)
end
function style.size(s)
fontSize(size * s)
end