Last active
January 14, 2017 03:14
-
-
Save bumper314/7836a1893f77c990db2ecb6da10e909d to your computer and use it in GitHub Desktop.
Pandoc Goalscape Writer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 '<' | |
elseif x == '>' then | |
return '>' | |
elseif x == '&' then | |
return '&' | |
elseif x == '"' then | |
return '"' | |
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 .. '">↩</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