2015-04-06 20:33:58 -04:00
|
|
|
_ = require 'underscore'
|
|
|
|
md = require('markdown').markdown
|
|
|
|
coffee = require 'coffee-script'
|
2015-04-06 14:06:26 -04:00
|
|
|
PDFDocument = require 'pdfkit'
|
|
|
|
|
|
|
|
Array::first ?= -> @[0]
|
|
|
|
|
|
|
|
Array::last ?= -> @[@length - 1]
|
|
|
|
|
2015-04-06 20:33:58 -04:00
|
|
|
# process.chdir(__dirname)
|
2015-04-06 14:06:26 -04:00
|
|
|
|
|
|
|
# style definitions for markdown
|
|
|
|
styles =
|
2015-04-06 15:03:20 -04:00
|
|
|
default:
|
2015-04-06 14:06:26 -04:00
|
|
|
font: 'Times-Roman'
|
|
|
|
fontSize: 12
|
|
|
|
lineGap: 24
|
|
|
|
align: 'left'
|
2015-04-06 15:03:20 -04:00
|
|
|
meta:
|
2015-04-06 14:06:26 -04:00
|
|
|
indent: 0
|
|
|
|
title:
|
|
|
|
align: 'center'
|
|
|
|
para:
|
2015-04-06 15:03:20 -04:00
|
|
|
indent: 72/2
|
|
|
|
blockquote:
|
|
|
|
indent: 0
|
2015-04-06 15:24:51 -04:00
|
|
|
marginLeft: 72
|
2015-04-06 15:03:20 -04:00
|
|
|
color: 'red'
|
|
|
|
font: 'Times-Italic'
|
2015-04-06 14:06:26 -04:00
|
|
|
em:
|
|
|
|
font: 'Times-Italic'
|
|
|
|
strong:
|
|
|
|
font: 'Times-Bold'
|
2015-04-06 16:21:18 -04:00
|
|
|
h1:
|
|
|
|
font: 'Times-Bold'
|
|
|
|
h2:
|
|
|
|
font: 'Times-Italic'
|
|
|
|
h3:
|
|
|
|
font: 'Times-Bold'
|
|
|
|
align: 'center'
|
|
|
|
h4:
|
|
|
|
font: 'Times-Italic'
|
|
|
|
align: 'center'
|
|
|
|
h5:
|
|
|
|
underline: true
|
|
|
|
citationHeader:
|
|
|
|
align: 'center'
|
|
|
|
citation:
|
|
|
|
indent: -72/2
|
|
|
|
marginLeft: 72/2
|
2015-04-06 14:06:26 -04:00
|
|
|
|
|
|
|
# syntax highlighting colors
|
|
|
|
# based on Github's theme
|
|
|
|
colors =
|
|
|
|
keyword: '#cb4b16'
|
|
|
|
atom: '#d33682'
|
|
|
|
number: '#009999'
|
|
|
|
def: '#2aa198'
|
|
|
|
variable: '#108888'
|
|
|
|
'variable-2': '#b58900'
|
|
|
|
'variable-3': '#6c71c4'
|
|
|
|
property: '#2aa198'
|
|
|
|
operator: '#6c71c4'
|
|
|
|
comment: '#999988'
|
|
|
|
string: '#dd1144'
|
|
|
|
'string-2': '#009926'
|
|
|
|
meta: '#768E04'
|
|
|
|
qualifier: '#b58900'
|
|
|
|
builtin: '#d33682'
|
|
|
|
bracket: '#cb4b16'
|
|
|
|
tag: '#93a1a1'
|
|
|
|
attribute: '#2aa198'
|
|
|
|
header: '#586e75'
|
|
|
|
quote: '#93a1a1'
|
|
|
|
link: '#93a1a1'
|
|
|
|
special: '#6c71c4'
|
|
|
|
default: '#002b36'
|
|
|
|
|
|
|
|
codeBlocks = []
|
|
|
|
lastType = null
|
|
|
|
|
|
|
|
# This class represents a node in the markdown tree, and can render it to pdf
|
|
|
|
class Node
|
|
|
|
constructor: (tree) ->
|
|
|
|
# special case for text nodes
|
|
|
|
if typeof tree is 'string'
|
|
|
|
@type = 'text'
|
|
|
|
@text = tree
|
|
|
|
return
|
|
|
|
|
|
|
|
@type = tree.shift()
|
2015-04-06 16:21:18 -04:00
|
|
|
@attrs = {}
|
2015-04-06 14:06:26 -04:00
|
|
|
|
|
|
|
if typeof tree[0] is 'object' and not Array.isArray tree[0]
|
|
|
|
@attrs = tree.shift()
|
|
|
|
|
|
|
|
switch @type
|
|
|
|
when 'header'
|
|
|
|
@type = 'h' + @attrs.level
|
|
|
|
|
2015-04-06 15:03:20 -04:00
|
|
|
# when 'code_block'
|
|
|
|
# # use code mirror to syntax highlight the code block
|
|
|
|
# code = @content[0].text
|
|
|
|
# @content = []
|
|
|
|
# CodeMirror.runMode code, 'coffeescript', (text, style) =>
|
|
|
|
# color = colors[style] or colors.default
|
|
|
|
# opts =
|
|
|
|
# color: color
|
|
|
|
# continued: text isnt '\n'
|
2015-04-06 14:06:26 -04:00
|
|
|
|
2015-04-06 15:03:20 -04:00
|
|
|
# @content.push new Node ['code', opts, text]
|
2015-04-06 14:06:26 -04:00
|
|
|
|
2015-04-06 15:03:20 -04:00
|
|
|
# @content[@content.length - 1]?.attrs.continued = false
|
|
|
|
# codeBlocks.push code
|
2015-04-06 14:06:26 -04:00
|
|
|
|
|
|
|
when 'img'
|
|
|
|
# images are used to generate inline example output
|
|
|
|
# compiles the coffeescript to JS so it can be run
|
|
|
|
# in the render method
|
|
|
|
@type = 'example'
|
|
|
|
code = codeBlocks[@attrs.alt]
|
|
|
|
@code = coffee.compile code if code
|
|
|
|
@height = +@attrs.title or 0
|
|
|
|
|
2015-04-06 16:21:18 -04:00
|
|
|
@style = _.extend({}, styles.default, styles[@type])
|
|
|
|
|
|
|
|
# parse sub nodes
|
|
|
|
@content = while tree.length
|
|
|
|
child = new Node tree.shift()
|
|
|
|
# blockquotes have an embedded paragraph; make sure the inner paragraph doesn't re-define
|
|
|
|
# its indentation
|
|
|
|
child.style?.indent = @style.indent if @style.indent?
|
|
|
|
child
|
|
|
|
|
|
|
|
console.log "content =", @content
|
|
|
|
# console.log "type =", @type
|
|
|
|
|
|
|
|
|
2015-04-06 14:06:26 -04:00
|
|
|
# sets the styles on the document for this node
|
|
|
|
setStyle: (doc) ->
|
|
|
|
if @style.font
|
|
|
|
doc.font @style.font
|
|
|
|
|
|
|
|
if @style.fontSize
|
|
|
|
doc.fontSize @style.fontSize
|
|
|
|
|
2015-04-06 15:03:20 -04:00
|
|
|
if @style.color
|
|
|
|
doc.fillColor @style.color
|
2015-04-06 14:06:26 -04:00
|
|
|
else
|
|
|
|
doc.fillColor 'black'
|
|
|
|
|
|
|
|
# renders this node and its subnodes to the document
|
|
|
|
render: (doc, continued = false) ->
|
2015-04-06 15:24:51 -04:00
|
|
|
# console.log "rendering node: ", @
|
|
|
|
if @style.marginLeft
|
|
|
|
doc.x += @style.marginLeft
|
|
|
|
|
2015-04-06 14:06:26 -04:00
|
|
|
switch @type
|
|
|
|
when 'hr'
|
|
|
|
doc.addPage()
|
|
|
|
else
|
|
|
|
# loop through subnodes and render them
|
|
|
|
for fragment, index in @content
|
|
|
|
if fragment.type is 'text'
|
2015-04-06 15:03:20 -04:00
|
|
|
@setStyle doc
|
2015-04-06 14:06:26 -04:00
|
|
|
|
|
|
|
# remove newlines unless this is code
|
2015-04-06 15:03:20 -04:00
|
|
|
# unless @type is 'code'
|
|
|
|
# fragment.text = fragment.text.replace(/[\r\n]\s*/g, ' ')
|
2015-04-06 14:06:26 -04:00
|
|
|
|
2015-04-06 15:03:20 -04:00
|
|
|
# console.log "rendering text. continued =", continued, 'attrs.continued =', @attrs.continued
|
|
|
|
doc.text fragment.text, _.extend({}, @style, {continued: continued or index < @content.length - 1})
|
2015-04-06 14:06:26 -04:00
|
|
|
else
|
2015-04-06 15:03:20 -04:00
|
|
|
console.log "rendering fragment #{fragment.type}"
|
2015-04-06 14:06:26 -04:00
|
|
|
fragment.render doc, index < @content.length - 1 and @type isnt 'bulletlist'
|
|
|
|
|
|
|
|
lastType = @type
|
|
|
|
|
2015-04-06 15:24:51 -04:00
|
|
|
if @style.marginBottom
|
|
|
|
doc.y += @style.marginBottom
|
|
|
|
if @style.marginLeft
|
|
|
|
doc.x -= @style.marginLeft
|
2015-04-06 14:06:26 -04:00
|
|
|
|
2015-04-06 16:44:44 -04:00
|
|
|
addPageNum = (doc, name) ->
|
|
|
|
range = doc.bufferedPageRange() # => { start: 0, count: 2 }
|
|
|
|
|
|
|
|
# todo: make more generic "draw text" function that sets the document properities based on provided styles
|
|
|
|
doc.font styles.default.font
|
|
|
|
for i in [range.start...range.start + range.count]
|
|
|
|
doc.switchToPage(i)
|
|
|
|
doc.y = 72/2
|
|
|
|
doc.x = 72
|
|
|
|
doc.text "#{name} #{i + 1}", _.extend({}, styles.default, {align: 'right'})
|
|
|
|
|
2015-04-06 14:06:26 -04:00
|
|
|
# reads and renders a markdown/literate coffeescript file to the document
|
2015-04-06 16:44:44 -04:00
|
|
|
render = (doc, tree) ->
|
2015-04-06 14:06:26 -04:00
|
|
|
doc.font 'Times-Roman'
|
|
|
|
doc.fontSize 12
|
|
|
|
|
|
|
|
codeBlocks = []
|
|
|
|
|
2015-04-06 16:21:18 -04:00
|
|
|
onWorksCited = false
|
2015-04-06 14:06:26 -04:00
|
|
|
while tree.length
|
2015-04-06 16:21:18 -04:00
|
|
|
node = new Node(tree.shift())
|
|
|
|
console.log "node =", node
|
|
|
|
if node.type == "h1" && node.content?.first()?.text?.toLowerCase() == "works cited"
|
|
|
|
onWorksCited = true
|
|
|
|
node.style = _.extend({}, styles.default, styles.citationHeader)
|
|
|
|
|
|
|
|
if onWorksCited && node.type == "para"
|
|
|
|
node.style = _.extend({}, styles.default, styles.citation)
|
|
|
|
|
2015-04-06 14:06:26 -04:00
|
|
|
node.render(doc)
|
|
|
|
|
|
|
|
doc
|
|
|
|
|
|
|
|
# renders the title page of the guide
|
2015-04-06 15:03:20 -04:00
|
|
|
# renderTitlePage = (doc) ->
|
|
|
|
# title = 'PDFKit Guide'
|
|
|
|
# author = 'By Devon Govett'
|
|
|
|
# version = 'Version ' + require('./package.json').version
|
|
|
|
|
|
|
|
# doc.font 'fonts/AlegreyaSans-Light.ttf', 60
|
|
|
|
# doc.y = doc.page.height / 2 - doc.currentLineHeight()
|
|
|
|
# doc.text title, align: 'center'
|
|
|
|
# w = doc.widthOfString(title)
|
|
|
|
|
|
|
|
# doc.fontSize 20
|
|
|
|
# doc.y -= 10
|
|
|
|
# doc.text author,
|
|
|
|
# align: 'center'
|
|
|
|
# indent: w - doc.widthOfString(author)
|
|
|
|
|
|
|
|
# doc.font styles.para.font, 10
|
|
|
|
# doc.text version,
|
|
|
|
# align: 'center'
|
|
|
|
# indent: w - doc.widthOfString(version)
|
|
|
|
|
|
|
|
# doc.addPage()
|
2015-04-06 14:06:26 -04:00
|
|
|
|
2015-04-06 20:33:58 -04:00
|
|
|
exports.extractMetadata = (text) ->
|
2015-04-06 16:44:44 -04:00
|
|
|
body = ""
|
|
|
|
metadata = {}
|
|
|
|
|
|
|
|
metadata_pattern = /// ^
|
|
|
|
([\w.-]+) # key
|
|
|
|
\:\ # colon
|
|
|
|
\s* # optional whitespace
|
|
|
|
(.+) # value
|
|
|
|
$///
|
|
|
|
|
|
|
|
for line in text.split("\n")
|
|
|
|
if meta = line.match(metadata_pattern)
|
|
|
|
key = meta[1]
|
|
|
|
value = meta[2]
|
|
|
|
metadata[key] = value
|
|
|
|
else
|
|
|
|
body += line + "\n"
|
|
|
|
|
|
|
|
{metadata: metadata, body: body}
|
|
|
|
|
2015-04-06 20:33:58 -04:00
|
|
|
exports.createMLADocument = (body, metadata, stream) ->
|
2015-04-06 16:44:44 -04:00
|
|
|
tree = md.parse body
|
|
|
|
tree.shift() # ignore 'markdown' first element
|
|
|
|
|
|
|
|
metadata.lastName ||= metadata.author?.split(" ").last()
|
|
|
|
|
2015-04-06 14:06:26 -04:00
|
|
|
doc = new PDFDocument
|
|
|
|
bufferPages: true
|
|
|
|
|
2015-04-06 16:44:44 -04:00
|
|
|
doc.info.Title = metadata.title
|
|
|
|
doc.info.Author = metadata.author
|
|
|
|
doc.info.Creator = "markdowntomla by christian.gen.co"
|
|
|
|
|
|
|
|
# add header
|
|
|
|
doc.font 'Times-Roman'
|
|
|
|
doc.fontSize 12
|
|
|
|
doc.text(metadata.author, _.extend({}, styles.default, styles.meta))
|
|
|
|
doc.text(metadata.instructor, _.extend({}, styles.default, styles.meta))
|
|
|
|
doc.text(metadata.course, _.extend({}, styles.default, styles.meta))
|
|
|
|
doc.text(metadata.date, _.extend({}, styles.default, styles.meta))
|
|
|
|
doc.text(metadata.title, _.extend({}, styles.default, styles.title))
|
|
|
|
|
2015-04-06 20:33:58 -04:00
|
|
|
doc.pipe(stream)
|
2015-04-06 16:44:44 -04:00
|
|
|
render doc, tree
|
|
|
|
addPageNum(doc, metadata.lastName)
|
2015-04-06 20:33:58 -04:00
|
|
|
# doc.flushPages()
|
2015-04-06 14:06:26 -04:00
|
|
|
doc.end()
|
2015-04-06 20:33:58 -04:00
|
|
|
|
|
|
|
# do ->
|
|
|
|
# # command line
|
|
|
|
# fs = require 'fs'
|
|
|
|
# filename = "paper.md"
|
|
|
|
# content = fs.readFileSync(filename, 'utf8')
|
|
|
|
# content = extractMetadata(content)
|
|
|
|
# body = content.body
|
|
|
|
# metadata = content.metadata
|
|
|
|
# stream = fs.createWriteStream("#{metadata.title} by #{metadata.author}.pdf")
|
|
|
|
# createDocument(body, metadata, stream)
|