Skip to content

Instantly share code, notes, and snippets.

@bumper314
Last active January 14, 2017 03:14
Show Gist options
  • Save bumper314/7836a1893f77c990db2ecb6da10e909d to your computer and use it in GitHub Desktop.
Save bumper314/7836a1893f77c990db2ecb6da10e909d to your computer and use it in GitHub Desktop.
Pandoc Goalscape Writer
<project type="Goalscape" version="3.0">
<header>
<title>Simple Goal</title>
<selectedGoal>0</selectedGoal>
<centredGoal/>
<maxExpandedLevels>-1</maxExpandedLevels>
<gridData>0|0|0|0</gridData>
<persons/>
<tags/>
<preferences notesFontsize="14" projectViewType="0" notesExpanded="false" panelPercentWidth="27.77777777777778" enableStartDate="false" enableEndDate="true" disableAlarmClocks="false" enableFullColoring="false"/>
</header>
<goals>
$body$
</goals>
</project>
-- Invoke with: pandoc -t goalscape_writer.lua --template=default.goalscape test.md > test.gsp
local escape, attributes, pipe
local GSGoal, GSNote, GSAttachment
local WEIGHT = {
even = "even",
length = "length",
children = "children"
}
-- A tree structure for nesting headers and their content
function GSGoal(init)
local self = {
type = "Goal",
name = "",
parent = nil,
children = {},
weight = WEIGHT.children
}
-- merge init into self
local k,v
for k,v in pairs(init) do self[k] = v end
function self.level()
if not self.parent then return 0 end
return self.parent.level() + 1
end
function self.length()
local l = string.len(self.name)
local k,v
for k,v in pairs(self.children) do
l = l + v.length()
end
return l
end
function self.subgoals(deep)
local subgoals = {}
local k,v, k2, v2
for k,v in pairs(self.children) do
if v.type == 'Goal' then
table.insert(subgoals, v)
if deep then
local subsubgoals = v.subgoals(deep)
for k2,v2 in pairs(subsubgoals) do
table.insert(subgoals, v2)
end
end
end
end
return subgoals
end
-- Importance in GoalScape controls the size of the wedge
function self.importance()
local imp = 100
if self.parent then
local even = 100 / #self.parent.subgoals(false)
local pweight = self.parent.weight
if pweight == WEIGHT.length then
imp = 100 * self.length() / self.parent.length()
elseif pweight == WEIGHT.children then
local subs = #self.subgoals(true) + 1
io.stderr:write(string.format("%s %d %d\n", self.name, subs, #self.parent.subgoals(true)));
imp = 100 * subs / #self.parent.subgoals(true)
elseif pweight == WEIGHT.even then
imp = even
else
io.stderr:write(string.format("WARNING: Unknown weight %s, using 'even'\n", pweight));
imp = even
end
end
return string.format("%.2f", imp)
end
function self.addNote(s)
self.addChild(GSNote({name = s}))
return "" -- DEBUG
end
function self.addAttachment(s, url)
self.addChild(GSAttachment({name = s}, url))
end
function self.addChild(c)
if c then
table.insert(self.children, c)
c.parent = self
c.weight = self.weight
end
end
function self.optimize()
self.squishNotes()
self.squishAttachments()
self.squishTree()
end
function self.squishNotes()
if #self.children > 1 then
-- Squish consequtive notes into a single note
local new_children = {}
local squished = {}
local k,v
for k,v in pairs(self.children) do
if v.type ~= 'Note' then
-- Insert squish notes first
if #squished > 0 then
table.insert(new_children, GSNote({name = table.concat(squished,'<P></P>')}))
squished = {}
end
table.insert(new_children, v)
else
table.insert(squished, v.name)
end
end
if #squished > 0 then
table.insert(new_children, GSNote({name = table.concat(squished,'<P></P>')}))
end
self.children = new_children
end
-- Recurse after squishing
if #self.children > 0 then
local k,v
for k,v in pairs(self.children) do
v.squishNotes()
end
end
end
function self.squishAttachments()
-- TODO
end
function self.squishTree()
-- TODO
end
function self.toTabIndentedList()
local str = ""
local k,v
for k,v in pairs(self.children) do
str = str .. v.toTabIndentedList()
end
local tabs = string.rep('\t',self.level())
return tabs .. self.name .. '\t' .. self.importance() .. '\n' .. str
end
function self.toGoalScapeXML()
local str = ""
-- Recurse Tree and renest if necessary
local k,v
if #self.children == 1 then
for k,v in pairs(self.children) do
str = str .. v.toGoalScapeXML()
end
elseif #self.children > 1 then
for k,v in pairs(self.children) do
if k == 1 and v.type == "Note" then
-- noop: keep notes under this goal rather than creating a subgoal
elseif v.type ~= "Goal" then
-- Renest
local leaf = GSGoal({name = " ", parent = self}) -- DEBUG use "*" to see better
leaf.addChild(v)
self.children[k] = leaf
v = leaf
end
str = str .. v.toGoalScapeXML()
end
end
local attr = {}
attr['name'] = self.name
attr['importance'] = self.importance()
attr['progress'] = "0.00"
attr['relativeFontSize'] = "0"
attr['notesTabIndex'] = "0"
local tabs = string.rep('\t',self.level())
return tabs .. '<goal' .. attributes(attr) .. '>\n' .. str .. '\n' .. tabs .. '</goal>\n'
end
-- return the instance
return self
end
function GSNote(init)
local self = GSGoal(init)
self.type = "Note"
function self.length()
return string.len(self.name)
end
function self.addNote(s)
io.stderr:write("WARNING: Trying to create note on a Note\n");
end
function self.addChild(c)
io.stderr:write("WARNING: Trying to add child on a Note\n");
end
function self.toTabIndentedList()
local tabs = string.rep('\t',self.level())
return tabs .. "NOTE" .. '\n'
end
function self.toGoalScapeXML()
local tabs = string.rep('\t',self.level())
return tabs .. '<notes><![CDATA[<HTML><BODY>' .. self.name .. '</BODY></HTML>]]></notes>\n'
end
return self
end
function GSAttachment(init, url)
local self = GSGoal(init)
self.type = "Attachment"
local purl = url
function self.length()
return 1000
end
function self.addNote(s)
io.stderr:write("WARNING: Trying to create note on an Attachment\n");
end
function self.addChild(c)
io.stderr:write("WARNING: Trying to add child on an Attachment\n");
end
function self.toTabIndentedList()
local tabs = string.rep('\t',self.level())
return tabs .. "ATTACHMENT" .. '\n'
end
function self.toGoalScapeXML()
local tabs = string.rep('\t',self.level())
return tabs .. '<attachments><attachment name="' .. escape(self.name,true) .. '" url="' .. escape(purl,true) .. '"/></attachments>\n'
end
return self
end
-------------------
-- HELPER FUNCTIONS
-------------------
-- Character escaping
function escape(s, in_attribute)
return s:gsub("[<>&\"']",
function(x)
if x == '<' then
return '&lt;'
elseif x == '>' then
return '&gt;'
elseif x == '&' then
return '&amp;'
elseif x == '"' then
return '&quot;'
else
return x
end
end)
end
-- Helper function to convert an attributes table into
-- a string that can be put into HTML tags.
function attributes(attr)
local attr_table = {}
for x,y in pairs(attr) do
if y and y ~= "" then
table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"')
end
end
return table.concat(attr_table)
end
-- Run cmd on a temporary file containing inp and return result.
function pipe(cmd, inp)
local tmp = os.tmpname()
local tmph = io.open(tmp, "w")
tmph:write(inp)
tmph:close()
local outh = io.popen(cmd .. " " .. tmp,"r")
local result = outh:read("*all")
outh:close()
os.remove(tmp)
return result
end
-- Table to store footnotes, so they can be included at the end.
local notes = {}
local root = GSGoal({name = "ROOT"})
local branch = root
-- Blocksep is used to separate block elements.
function Blocksep()
return "\n"
end
-- This function is called once for the whole document. Parameters:
-- body is a string, metadata is a table, variables is a table.
-- This gives you a fragment. You could use the metadata table to
-- fill variables in a custom lua template. Or, pass `--template=...`
-- to pandoc, and pandoc will add do the template processing as
-- usual.
function Doc(body, metadata, variables)
local buffer = {}
local function add(s)
table.insert(buffer, s)
end
-- Fixup the Tree a bit…
root.optimize()
-- Hoist
if #root.children == 1 then
root = root.children[1]
root.parent = nil
end
--add(root.toTabIndentedList())
--add("----------------------------")
add(root.toGoalScapeXML())
return table.concat(buffer,'\n') .. '\n'
end
-- The functions that follow render corresponding pandoc elements.
-- s is always a string, attr is always a table of attributes, and
-- items is always an array of strings (the items in a list).
-- Comments indicate the types of other variables.
function Str(s)
return escape(s)
end
function Space()
return " "
end
function SoftBreak()
return "\n"
end
function LineBreak()
return "<BR/>"
end
function Emph(s)
return "<I>" .. s .. "</I>"
end
function Strong(s)
return "<B>" .. s .. "</B>"
end
function Subscript(s)
return "<sub>" .. s .. "</sub>"
end
function Superscript(s)
return "<sup>" .. s .. "</sup>"
end
function SmallCaps(s)
return '<span style="font-variant: small-caps;">' .. s .. '</span>'
end
function Strikeout(s)
return '<del>' .. s .. '</del>'
end
function Link(s, src, tit, attr)
return '<A HREF="' .. escape(src,true) .. '" TARGET="_blank">' .. s .. '</A>'
end
function Image(s, src, tit, attr)
--branch.addAttachment(tit, src) -- Images as Attachment nodes
--return branch.addNote(Para(Link('<IMG src="' .. escape(src,true) .. '"/>', src, tit, attr))) -- Images embedded in Notes with link
return branch.addNote(Para(Link('IMAGE ' .. tit, src, tit, attr))) -- Images as a text link
end
function Code(s, attr)
return "<code" .. attributes(attr) .. ">" .. escape(s) .. "</code>"
end
function InlineMath(s)
return "\\(" .. escape(s) .. "\\)"
end
function DisplayMath(s)
return "\\[" .. escape(s) .. "\\]"
end
function Note(s)
local num = #notes + 1
-- insert the back reference right before the final closing tag.
s = string.gsub(s,
'(.*)</', '%1 <a href="#fnref' .. num .. '">&#8617;</a></')
-- add a list item with the note to the note table.
table.insert(notes, '<li id="fn' .. num .. '">' .. s .. '</li>')
-- return the footnote reference, linked to the note.
return '<a id="fnref' .. num .. '" href="#fn' .. num ..
'"><sup>' .. num .. '</sup></a>'
end
function Span(s, attr)
return "<span" .. attributes(attr) .. ">" .. s .. "</span>"
end
function RawInline(format, str)
if format == "html" then
return str
end
end
function Cite(s, cs)
local ids = {}
for _,cit in ipairs(cs) do
table.insert(ids, cit.citationId)
end
return "<span class=\"cite\" data-citation-ids=\"" .. table.concat(ids, ",") ..
"\">" .. s .. "</span>"
end
function Plain(s)
return s
end
function Para(s)
return branch.addNote("<P>" .. s .. "</P>")
end
-- lev is an integer, the header level.
function Header(lev, s, attr)
local leaf = nil
local depth = branch.level()
-- Find the branch where this new header will be a child
if lev > depth then
while branch.level() < lev-1 do
-- Create intermediate levels to maintain structure
leaf = GSGoal({name = "INTERMEDIATE"})
branch.addChild(leaf)
branch = leaf
end
else
while branch.level() >= lev do
branch = branch.parent
end
end
leaf = GSGoal({name = s})
branch.addChild(leaf)
branch = leaf
if WEIGHT[attr['class']] ~= nil then
branch.weight = attr['class']
end
return ""
end
function BlockQuote(s)
return "<blockquote>\n" .. s .. "\n</blockquote>"
end
function HorizontalRule()
return "<HR/>"
end
function LineBlock(ls)
return Para(table.concat(ls, '<BR/>\n'))
end
function CodeBlock(s, attr)
-- If code block has class 'dot', pipe the contents through dot
-- and base64, and include the base64-encoded png as a data: URL.
if attr.class and string.match(' ' .. attr.class .. ' ',' dot ') then
local png = pipe("base64", pipe("dot -Tpng", s))
return '<img src="data:image/png;base64,' .. png .. '"/>'
-- otherwise treat as code (one could pipe through a highlighter)
else
return "<pre><code" .. attributes(attr) .. ">" .. escape(s) ..
"</code></pre>"
end
end
function BulletList(items)
local buffer = {}
for _, item in pairs(items) do
table.insert(buffer, "<LI>" .. item .. "</LI>")
end
return branch.addNote("<UL>" .. table.concat(buffer) .. "</UL>")
end
function OrderedList(items)
local buffer = {}
for _, item in pairs(items) do
table.insert(buffer, "<LI>" .. item .. "</LI>")
end
return branch.addNote("<OL>" .. table.concat(buffer) .. "</OL>")
end
function CaptionedImage(src, tit, caption, attr)
return Image("", src, caption, attr)
end
function RawBlock(format, str)
if format == "html" then
return str
end
end
function Div(s, attr)
return "<P" .. attributes(attr) .. ">\n" .. s .. "</P>"
end
-- The following code will produce runtime warnings when you haven't defined
-- all of the functions you need for the custom writer, so it's useful
-- to include when you're working on a writer.
local meta = {}
meta.__index =
function(_, key)
io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key))
return function() return "" end
end
setmetatable(_G, meta)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment