Markdown-like text formatting in Codea
--# Main | |
-- Markdown Codea | |
-- by Yojimbo2000 | |
displayMode(FULLSCREEN) | |
function setup() | |
setText() | |
y,vel = 0,0 | |
scrollY={} --store deltas for smooth scrolling upon finger release | |
end | |
function setText() | |
fontSize(25) --use the regular fontsize command to set the size of the body level text | |
textImage = Markdown{ | |
-- debug = true, --debug print | |
width = WIDTH *0.9, --wrap width | |
text = testString | |
} | |
end | |
function draw() | |
background(214, 212, 203, 255) | |
if y<0 then vel = math.abs(vel) * 0.5 --scroll bounce | |
elseif y>HEIGHT then vel = -math.abs(vel) * 0.5 | |
end | |
if not touching then vel = vel * 0.95 end --friction upon touch end | |
y = y + vel | |
sprite(textImage, WIDTH*0.5, y) | |
end | |
function touched(t) | |
if t.state==ENDED then | |
local av = 0 --calculate average deltas over last 10 | |
for i=1, #scrollY do | |
av = av + scrollY[i] | |
end | |
vel = av / #scrollY | |
scrollY = {} --reset delta table | |
touching = false | |
else | |
if #scrollY>10 then --store last 10 deltas | |
table.remove(scrollY, 1) | |
else | |
scrollY[#scrollY+1]=t.deltaY | |
end | |
vel = t.deltaY | |
touching = true | |
end | |
end | |
function orientationChanged() | |
setText() | |
end | |
--# Markdown | |
--markdown-esque string formatting | |
local style = { --simple cascading styles | |
body = { --body format | |
font = "IowanOldStyle", --"Baskerville", --"Optima", | |
col = color(91, 73, 51, 255) | |
}, | |
heading = { --the array part of style.heading contains styles for heading1, heading2, etc (overrides global) | |
{--font = "HelveticaNeueUltraLight", | |
size=2.5}, --Heading1. Size is proportion of body size | |
{size=1.5}, --Heading2 | |
{size=1.2}, --Heading 3 | |
all = { --global headings settings | |
font = "Avenir", --"HelveticaNeueLight", | |
col = color(95, 89, 135, 255) | |
} | |
}, | |
block = { --block quotes | |
size = 0.9, | |
font = "Optima", --"Verdana", -- "HelveticaNeue", | |
col = color(89, 78, 66, 255)}, | |
bullet = { --bullet points | |
size = 0.9, | |
col = color(133, 97, 72, 255), | |
font = "Optima" | |
}, | |
--non-standard names for font families. add entries here if they dont conform to suffix pattern of "", "-Italic", "-Bold", "-BoldItalic", (eg -Black for bold, -Oblique for italic etc) | |
Palatino = { | |
regular = "Palatino-Roman" | |
}, | |
HoeflerText = { | |
regular = "HoeflerText-Regular", | |
bold = "HoeflerText-Black", | |
boldItalic = "HoeflerText-BlackItalic" | |
}, | |
Optima = { | |
regular = "Optima-Regular" | |
}, | |
HelveticaNeueUltraLight = { | |
regular = "HelveticaNeue-UltraLight", | |
italic = "HelveticaNeue-UltraLightItalic", | |
bold = "HelveticaNeue-Light", | |
boldItalic = "HelveticaNeue-LightItalic" | |
}, | |
HelveticaNeueLight = { | |
regular = "HelveticaNeue-Light", | |
italic = "HelveticaNeue-LightItalic", | |
bold = "HelveticaNeue", | |
boldItalic = "HelveticaNeue-Italic" | |
}, | |
IowanOldStyle = { | |
regular = "IowanOldStyle-Roman" | |
}, | |
Avenir = { | |
regular = "Avenir-Roman", | |
italic = "Avenir-Oblique", | |
bold = "Avenir-Heavy", | |
boldItalic = "Avenir-HeavyOblique" | |
} | |
} | |
font("Didot-Bold") | |
local face --name of base font currently being used | |
local size --size of base font | |
function Markdown(t) | |
textMode(CORNER) | |
local _, baseHeight = textSize("dummy") --defines paragraph separation | |
size = fontSize() --base size of body text | |
local img = image(t.width, HEIGHT * 2) --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 * 2) | |
local italic = false | |
local bold = false | |
-- local tightList = false --remove paragraph separation for bullets | |
local indent = 0 --for block quotations | |
local parSep = baseHeight | |
for paragraph,sep in string.gmatch(testString, "(.-)(\n%s*)") do | |
print (paragraph) | |
--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 | |
indent = 0 | |
fontSize(size) | |
paragraph = paragraph.."\n" --add return (this also allows final part of line to be captured, as return is a whitespace character) | |
local cursorSet = false --set to true once initial cursor position for paragraph is set according to font size, paragraph separation | |
--CHECK ELEMENTS THAT ONLY COME AT START OF PARAGRAPH | |
--BLOCK | |
local bl | |
paragraph, bl = string.gsub(paragraph, "^> ", "", 1) | |
if bl>0 then | |
indent = size * 3 --indent paragraph | |
cursor.x = indent | |
style.set(style.block) | |
end | |
--BULLETS | |
local bu | |
paragraph, bu = string.gsub(paragraph, "^%- ", "", 1) | |
if bu>0 then | |
cursor.y = cursor.y - (parSep + baseHeight) | |
--[[ | |
if not tightList then | |
cursor.y = cursor.y - parSep | |
end | |
]] | |
-- tightList = true | |
style.set(style.bullet) | |
text("\u{2022}", size * 1.75, cursor.y) | |
cursorSet = true | |
indent = size * 3 | |
cursor.x = indent | |
-- paragraph = "\u{2022} "..paragraph | |
--[[ | |
else | |
tightList = false | |
]] | |
end | |
--HEADINGS | |
local hBegin, hEnd = string.find(paragraph, "^%#+") --look for number of hashes at start of para | |
if hBegin then | |
local headLevel = hEnd + 1 - hBegin | |
paragraph = string.gsub(paragraph, "^%#+%s?", "") | |
style.set(style.heading.all) --global heading settings | |
style.set(style.heading[headLevel]) --level specific settings | |
end | |
--PARSE WORDS | |
for element, control in string.gmatch(paragraph, "(.-)([%s*]+)") do --separate at white space, * | |
if string.find(control, "%s") then --if whitespace | |
element = element.." " --put spaces back in | |
end | |
local w,h = textSize(element) --find size of word | |
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 | |
local eBegin, eEnd = string.find(control, "%*+") --count number of asterisks | |
if eBegin then | |
local emph = eEnd + 1 - eBegin | |
if emph==3 then | |
bold = not bold | |
italic = not italic | |
elseif emph==2 then | |
bold = not bold | |
else | |
italic = not italic | |
end | |
if bold and italic then | |
font(style.boldItalic(face)) | |
elseif bold then | |
font(style.bold(face)) | |
elseif italic then | |
font(style.italic(face)) | |
else | |
style.font(face) | |
end | |
end | |
end --of word | |
--CHECK PARAGRAPH SEPARATION (NUMBER OF RETURNS) FOR TIGHT/LOOSE LISTS | |
local _,returns = string.gsub(sep, "\n", "") | |
if returns==1 then | |
parSep = 0 --tight list | |
else | |
parSep = baseHeight --loose list | |
end | |
end --of paragraph | |
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 | |
--3 functions to handle non-standard named fonts (eg -Black for bold, -Oblique for italc etc) | |
function style.bold(f) | |
if style[f] and style[f].bold then --check if a nonstandard bold face is specified | |
return style[f].bold | |
end | |
return f.."-Bold" --else just append bold to family name | |
end | |
function style.italic(f) | |
if style[f] and style[f].italic then | |
return style[f].italic | |
end | |
return f.."-Italic" | |
end | |
function style.boldItalic(f) | |
if style[f] and style[f].boldItalic then | |
return style[f].boldItalic | |
end | |
return f.."-BoldItalic" | |
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 | |
if style[face] and style[face].regular then | |
font(style[face].regular) | |
else | |
font(face) | |
end | |
end | |
function style.col(col) | |
fill(col) | |
end | |
function style.size(s) | |
fontSize(size * s) | |
end | |
--# Sampletext | |
testString = [[ | |
# *Markdown*-like text formatting --- in **Codea!** | |
Have you ever wanted an easy way to format text --- adding *italic*, **bold**, ***bold-italic,*** different type faces, font sizes and colours, indented block quotes, plus typography features such as "smart quotes" and em-dashes, all of them nestable within one-another --- on the fly? | |
## Well now you can, with **Markdown Codea.** | |
> *Try switching the orientation of your device to test the hand-made text wrapping feature! Touch the screen to scroll the text* | |
### "But --- *what **is** this **Markdown**?!?*" I hear you yell | |
Markdown is a way of adding rich formatting, such as: | |
- *Emphasis* | |
- **Strong emphasis** | |
- *Really, **really** strong emphasis* | |
- Or **really, *really* strong emphasis** if you prefer | |
- Block quotes, different headings... | |
- Oh, and ***bullet points!*** Bullet points can be displayed in a tight list like this, by only separating each item with one return | |
Or, if you prefer, you can have: | |
- A loose list | |
- Of bullet points | |
- Just separate each bullet with two returns | |
And it's all done using plain text. So it's great for using in plain-text environments such as code editors. As *Markdown's* creator John Gruber said: | |
> The overriding design goal for *Markdown's* formatting syntax is to make it as **readable** as possible. The idea is that a *Markdown*-formatted document should be publishable **as-is, as plain text, *without* looking like it's been marked up with tags** or formatting instructions | |
**But the best thing about Markdown is --- *you already know how to use it***. It's used on lots of forums, including *Codea Talk.* I've thrown in some nice, *Pandoc*-inspired extras such as typographer's quotes for the apostrophe and for 'single quotation marks' and "double" quote marks, plus en--dash and em---dash | |
]] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Nice