Markdown Codea with paragraph stitching
--# Main | |
-- Markdown Codea | |
-- paragraph-stitching version | |
-- by Yojimbo2000 | |
displayMode(OVERLAY) | |
displayMode(FULLSCREEN) | |
function setup() | |
state = Stitch | |
state.init() | |
end | |
function draw() | |
background(130, 158, 189, 255) | |
state.draw() | |
end | |
function touched(t) | |
state.touched(t) | |
end | |
function orientationChanged() | |
state.orientation() | |
end | |
--# Stitch | |
Stitch = {} | |
function Stitch.init() | |
para = 1 | |
trackY = HEIGHT * 0.6 | |
paragraphs = {} | |
paragraphs[1] = Paragraph(para, trackY) | |
trackY = trackY - (paragraphs[1].img.height * 0.5) | |
scrollY = trackY | |
parameter.watch("para") | |
fontSize(20) | |
Notify{ | |
delay = 2, | |
recurring = 4, | |
pos = vec2(WIDTH*0.5, HEIGHT * 0.25), | |
condition = function() return not endOfText end, | |
img = Markdown{ | |
width = WIDTH * 0.4, | |
palette = "dark", | |
paragraph = [[## Touch the lower half of the screen to move down]] | |
} | |
} | |
Notify { | |
delay = 6, | |
pos = vec2(WIDTH*0.5, HEIGHT * 0.75), | |
img = Markdown{ | |
width = WIDTH * 0.4, | |
palette = "dark", | |
paragraph = [[## Touch the top half of the screen to move back up]] | |
} | |
} | |
end | |
function Stitch.draw() | |
pushMatrix() | |
scrollY = scrollY + (trackY - scrollY) * 0.1 | |
translate(0, -scrollY+HEIGHT*0.3) | |
for i,v in ipairs(paragraphs) do | |
if v.kill then | |
table.remove(paragraphs, i) | |
else | |
v:draw() | |
end | |
end | |
popMatrix() | |
Notify.updateAll() | |
end | |
function Stitch.touched(t) | |
if t.state==ENDED then | |
Notify.touchAll() | |
if t.y < HEIGHT * 0.5 then | |
para = para + 1 | |
-- local lastPara = #paragraphs --remember the last non-new para (to animate sliding up) | |
if para>#paragraphs then --see if there's a new para | |
local new = Paragraph(para, trackY) | |
if new.kill then --no more paragraphs | |
para = para - 1 | |
endOfText = true | |
return | |
end | |
paragraphs[#paragraphs+1] = new | |
else | |
trackY = trackY - (paragraphs[para-1].img.height * 0.5) | |
end | |
paragraphs[para-1]:fade(128) --fade animations (paragraph focus) | |
paragraphs[para]:fade(255) | |
-- local h = (paragraphs[para].img.height + paragraphs[para-1].img.height)*0.5 | |
trackY = trackY - (paragraphs[para].img.height * 0.5) | |
--[[ | |
for i=1,lastPara do | |
paragraphs[i]:fade(255-(math.abs(i-para)*40)) | |
end | |
]] | |
elseif para>1 then | |
local h = (paragraphs[para].img.height + paragraphs[para-1].img.height)*0.5 | |
trackY = trackY + h | |
para = para - 1 | |
endOfText = false | |
--[[ | |
for i,v in ipairs(paragraphs) do | |
paragraphs[i]:fade(255-(math.abs(i-para)*40)) | |
end | |
]] | |
paragraphs[para]:fade(255) | |
paragraphs[para+1]:fade(128) | |
end | |
end | |
end | |
function Stitch.orientation() | |
for i,v in ipairs(paragraphs) do | |
v:setText() --rerender the text | |
v.m.texture = v.img | |
v.w = v.img.width --reset width/ height | |
v.h = v.img.height | |
if i>1 then | |
local u = paragraphs[i-1] | |
v.y = u.y - ((v.h + u.h) * 0.5) --recalculate y position | |
end | |
end | |
trackY = paragraphs[para].y - paragraphs[para].h * 0.5 --recalculate focus/ insertion point. This is sometimes slightly off. | |
end | |
--# Paragraph | |
Paragraph = class() | |
local size = 25 | |
function Paragraph:init(para) | |
self.text = parseParagraph(testString, para) | |
if not self.text then self.kill=true return end | |
self:setText() | |
self.m = mesh() | |
self.m.texture = self.img | |
self.m:addRect(0,0,0,0) | |
trackY = trackY - (self.img.height * 0.5) | |
self.y = trackY | |
self.w, self.h = self.img.width * 0.5, self.img.height * 0.5 | |
self.alpha = 0 | |
self.targetAlpha = 255 | |
tween(1, self, {w = self.img.width, h=self.img.height}, tween.easing.sineOut) | |
end | |
function Paragraph:setText() | |
fontSize(size) --use the regular fontsize command to set the size of the body level text | |
self.img = Markdown{ | |
-- debug = true, --debug print | |
palette = "solarized", | |
width = WIDTH *0.8, --wrap width | |
paragraph = self.text | |
} | |
end | |
function Paragraph:draw() | |
self.alpha = self.alpha + (self.targetAlpha - self.alpha) * 0.1 | |
self.m:setRect(1,WIDTH*0.5, self.y, self.w, self.h) | |
self.m:setRectColor(1, 255, 255, 255, self.alpha) | |
self.m:draw() | |
end | |
function Paragraph:fade(alpha) | |
self.targetAlpha = alpha | |
end | |
--# Notify | |
Notify = class() | |
local notifications = {} --store notifications | |
function Notify:init(t) | |
local del = t.delay or 0 | |
self.timer = ElapsedTime + del --time until notification should appear | |
self.pos = t.pos or (vec2(WIDTH, HEIGHT)*0.5) | |
self.img = t.img --the only mandatory input for this class, the image that will be displayed | |
self.alpha = 0 | |
self.targetAlpha = 255 | |
self.recurring = t.recurring --set this if the notification should reoccur, set to seconds delay for subsequent notifications | |
self.condition = t.condition --condition which must be met for recurrance | |
self.duration = t.duration --seconds until auto dismissal. omit this if notification should stay until a touch event | |
notifications[#notifications+1] = self | |
end | |
function Notify:update() | |
if self.condition and not self.condition() then return end | |
if ElapsedTime > self.timer then | |
self.alpha = self.alpha + (self.targetAlpha - self.alpha) * 0.1 --fade in or out | |
tint(255, self.alpha) | |
sprite(self.img, self.pos.x, self.pos.y, self.img.width) | |
tint() | |
if self.targetAlpha == 0 and self.alpha<0.1 then --if fade-out has completed | |
if self.recurring then | |
-- if self.condition() then | |
self.timer = ElapsedTime + self.recurring --reset ready for next fade-in | |
self.recurring = self.recurring * 2 --delay gradually gets longer | |
self.targetAlpha = 255 | |
-- end | |
else | |
self.kill = true | |
end | |
end | |
if self.duration and ElapsedTime > self.timer + self.duration then | |
self.targetAlpha = 0 --auto-dismissal of notification (if this is set.) | |
end | |
end | |
end | |
function Notify:touched() | |
if ElapsedTime > self.timer + 0.5 then --split second delay | |
self.targetAlpha = 0 --before dismssal | |
end | |
end | |
--functions below are not part of the class | |
function Notify.updateAll() | |
for i,v in ipairs(notifications) do | |
if v.kill then | |
table.remove(notifications, i) | |
else | |
v:update() | |
end | |
end | |
end | |
function Notify.touchAll() | |
for i,v in ipairs(notifications) do | |
v:touched() | |
end | |
end | |
--# Markdown | |
--markdown-esque string formatting | |
local style = { --simple cascading styles | |
palette = { | |
solarized = {color(214, 212, 203, 255), | |
color(91, 73, 51, 255), | |
color(95, 89, 135, 255), | |
color(89, 78, 66, 255), | |
color(133, 97, 72, 255) | |
}, | |
dark = {color(28, 41, 60, 200), | |
color(210, 208, 181, 255), | |
color(0, 255, 225, 255), | |
color(223, 202, 159, 255), | |
color(215, 223, 171, 255) | |
} | |
}, | |
body = { --body format | |
font = "IowanOldStyle", --"Baskerville", --"Optima", | |
col = 2, | |
}, | |
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 = 3, | |
} | |
}, | |
block = { --block quotes | |
size = 0.9, | |
font = "Optima", --"Verdana", -- "HelveticaNeue", | |
col = 4, }, | |
bullet = { --bullet points | |
size = 0.9, | |
col = 5, | |
font = "Optima" | |
}, | |
--non-standard names for font families. add entries here if family doesnt conform to suffix pattern of "[no suffix]" for regular, "-Italic" for italic, "-Bold", "-BoldItalic", (eg if the family has, say, "-Roman" for regular, "-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 | |
local palette --color palette in use | |
function parseParagraph(tex, paraNo) | |
tex = tex.."\n\n" | |
local paraCount = 0 | |
for paragraph in string.gmatch(tex, "(.-)\n\n") do | |
paraCount = paraCount + 1 | |
if paraCount == paraNo then | |
return paragraph | |
end | |
end | |
end | |
function Markdown(t) | |
local paragraph = t.paragraph | |
pushStyle() | |
local _, baseHeight = textSize("dummy") --defines paragraph separation | |
size = fontSize() --base size of body text | |
palette = style.palette[t.palette] --color scheme in use | |
local gutter = size * 0.5 --border around the text | |
--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) | |
local indent = gutter | |
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 = gutter + (size * 3) --indent paragraph | |
style.set(style.block) | |
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 | |
textMode(CORNER) | |
textWrapWidth(t.width - (indent - gutter)) --use built in wrapping to | |
local _, height = textSize(paragraph) --guestimate height of paragraph | |
local img = image(t.width+(gutter*2), height+(gutter*2)) | |
setContext(img) | |
local back = t.background or palette[1] | |
background(back) | |
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(indent,height+gutter) | |
local italic = false | |
local bold = false | |
paragraph = paragraph.."\n" --add return (this also allows final part of line to be captured) | |
for subpara in string.gmatch(paragraph, "(.-)\n") do --for tight-nested lists | |
subpara = subpara.." " | |
--BULLETS | |
local bu | |
subpara, bu = string.gsub(subpara, "^%- ", "", 1) | |
if bu>0 then | |
style.set(style.bullet) | |
local _, h = textSize("dummy") | |
cursor.y = cursor.y - h | |
text("\u{2022}", gutter + (size * 1.75), cursor.y) | |
cursorSet = true | |
indent = gutter + (size * 3) | |
cursor.x = indent | |
end | |
--PARSE WORDS | |
for element, control in string.gmatch(subpara, "(.-)([%s*]+)") do -- "(.-)(%*+%s+)" 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 | |
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 | |
end --of subpara | |
setContext() | |
popStyle() | |
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(palette[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