Skip to content

Commit 79239f2

Browse files
committedFeb 7, 2013
1.0-ish
1 parent 02bec74 commit 79239f2

File tree

2 files changed

+202
-137
lines changed

2 files changed

+202
-137
lines changed
 

‎journo.js

+53-58
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎journo.litcoffee

+149-79
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ Journo is a blogging program, with a few basic goals. To wit:
1515
1616
* Retina ready.
1717
18-
* Syntax highlight code and handle large photo slideshows.
19-
20-
* Handle minimal metadata (photos have captions, posts have titles).
18+
* Syntax highlight code.
2119
2220
* Publish a feed.
2321
@@ -33,57 +31,70 @@ Journo is a blogging program, with a few basic goals. To wit:
3331
Write in Markdown
3432
-----------------
3533

36-
We'll use the excellent `marked` module to compile Markdown into HTML, and
37-
Underscore for many of its goodies later on.
34+
We'll use the excellent **marked** module to compile Markdown into HTML, and
35+
Underscore for many of its goodies later on. Up top, create a namespace for
36+
shared values needed by more than one function.
3837

3938
marked = require 'marked'
4039
_ = require 'underscore'
4140
shared = {}
4241
43-
A Journo site has a layout file, stored in `layout.html`. To render a post, we
44-
take its raw **source**, treat it as both an Underscore template (for HTML
45-
generation) and as Markdown (for formatting), and insert it into the layout
46-
as `content`.
42+
To render a post, we take its raw `source`, treat it as both an Underscore
43+
template (for HTML generation) and as Markdown (for formatting), and insert it
44+
into the layout as `content`.
4745

4846
Journo.render = (post, source) ->
4947
catchErrors ->
48+
do loadLayout
5049
source or= fs.readFileSync postPath post
51-
shared.layout or= _.template(fs.readFileSync('layout.html').toString())
5250
variables = renderVariables post
5351
markdown = _.template(source.toString()) variables
54-
title = postTitle markdown
52+
title = detectTitle markdown
5553
content = marked.parser marked.lexer markdown
5654
shared.layout _.extend variables, {title, content}
5755
56+
A Journo site has a layout file, stored in `layout.html`, which is used
57+
to wrap every page.
58+
59+
loadLayout = (force) ->
60+
return layout if not force and layout = shared.layout
61+
shared.layout = _.template(fs.readFileSync('layout.html').toString())
62+
5863

5964
Publish to Flat Files
6065
---------------------
6166

6267
A blog is a folder on your hard drive. Within the blog, you have a `posts`
6368
folder for blog posts, a `public` folder for static content, a `layout.html`
64-
file, and a `journo.json` file for configuration. During a `build`, a static
65-
version of the site is rendered into the `site` folder.
69+
file for the layout which wraps every page, and a `journo.json` file for
70+
configuration. During a `build`, a static version of the site is rendered
71+
into the `site` folder, by **rsync**ing over all static files, rendering and
72+
writing every post, and creating an RSS feed.
6673

6774
fs = require 'fs'
6875
path = require 'path'
6976
{spawn, exec} = require 'child_process'
7077
7178
Journo.build = ->
72-
loadConfig()
73-
loadManifest()
79+
do loadManifest
7480
fs.mkdirSync('site') unless fs.existsSync('site')
75-
exec "rsync -vur --delete public site", (err, stdout, stderr) ->
81+
82+
exec "rsync -vur --delete public/ site", (err, stdout, stderr) ->
7683
throw err if err
84+
7785
for post in folderContents('posts')
7886
html = Journo.render post
7987
file = htmlPath post
8088
fs.mkdirSync path.dirname(file) unless fs.existsSync path.dirname(file)
8189
fs.writeFileSync file, html
90+
8291
fs.writeFileSync "site/feed.rss", Journo.feed()
8392
84-
The `config.json` configuration file is where you keep the nitty gritty details,
85-
like how to connect to your FTP server. The settings are: `host`, `port`,
86-
`secure`, `user`, and `password`.
93+
The `config.json` configuration file is where you keep the configuration
94+
details of your blog, and how to connect to the server you'd like to publish
95+
it on. The valid settings are: `title`, `description`, `author` (for RSS), `url
96+
`, `publish` (the `user@host:path` location to **rsync** to), and `publishPort`
97+
(if your server doesn't listen to SSH on the usual one).
8798

8899
loadConfig = ->
89100
return if shared.config
@@ -97,10 +108,13 @@ like how to connect to your FTP server. The settings are: `host`, `port`,
97108
Publish via rsync
98109
-----------------
99110

111+
Publishing is nice and rudimentary. We build out an entirely static version of
112+
the site and **rysnc** it up to the server.
113+
100114
Journo.publish = ->
101-
Journo.build()
115+
do Journo.build
102116
port = "ssh -p #{shared.config.publishPort or 22}"
103-
rsync = spawn "rsync", ['-vurz', '--delete', '-e', port, 'site', shared.config.publish]
117+
rsync = spawn "rsync", ['-vurz', '--delete', '-e', port, 'site/', shared.config.publish]
104118
rsync.stdout.on 'data', (out) -> console.log out.toString()
105119
rsync.stderr.on 'data', (err) -> console.error err.toString()
106120
@@ -114,69 +128,74 @@ and last recorded modified time) about each post.
114128
manifestPath = 'journo-manifest.json'
115129
116130
loadManifest = ->
131+
do loadConfig
132+
117133
shared.manifest = if fs.existsSync manifestPath
118134
JSON.parse fs.readFileSync manifestPath
119135
else
120136
{}
121-
todo = compareManifest()
122-
writeManifest()
123-
todo
124137
125-
writeManifest = ->
138+
do updateManifest
126139
fs.writeFileSync manifestPath, JSON.stringify shared.manifest
127140
128141
We update the manifest by looping through every post and every entry in the
129-
existing manifest, and looking for differences. Return a list of the posts
130-
that need to be `PUT` to the server, and the posts that should be `DELETE`d.
142+
existing manifest, looking for differences in `mtime`, and recording those
143+
along with the title and description of each post.
131144

132-
compareManifest = ->
145+
updateManifest = ->
146+
manifest = shared.manifest
133147
posts = folderContents 'posts'
134-
puts = []
135-
deletes = []
136-
for file, meta of shared.manifest when file not in posts
137-
deletes.push file
138-
delete shared.manifest[file]
139-
for file in posts
140-
stat = fs.statSync "posts/#{file}"
141-
entry = shared.manifest[file]
148+
149+
delete manifest[post] for post of manifest when post not in posts
150+
151+
for post in posts
152+
stat = fs.statSync postPath post
153+
entry = manifest[post]
142154
if not entry or entry.mtime isnt stat.mtime
143-
entry or= {pubtime: new Date}
155+
entry or= {pubtime: stat.ctime}
144156
entry.mtime = stat.mtime
145-
content = fs.readFileSync("posts/#{file}").toString()
146-
entry.title = postTitle content
147-
puts.push file
148-
shared.manifest[file] = entry
149-
{puts, deletes}
157+
content = fs.readFileSync(postPath post).toString()
158+
entry.title = detectTitle content
159+
entry.description = detectDescription content, post
160+
manifest[post] = entry
161+
162+
yes
150163
151164

152165
Retina Ready
153166
------------
154167

168+
In the future, it may make sense for Journo to have some sort of built-in
169+
facility for automatically downsizing photos from retina to regular sizes ...
170+
But for now, this bit is up to you.
171+
155172

156173
Syntax Highlight Code
157174
---------------------
158175

176+
We syntax-highlight blocks of code with the nifty **highlight** package that
177+
includes heuristics for auto-language detection, so you don't have to specify
178+
what you're coding in.
179+
159180
{Highlight} = require 'highlight'
160181
161182
marked.setOptions
162183
highlight: (code, lang) ->
163184
Highlight code
164185
165186

166-
Create Photo Slideshows
167-
-----------------------
168-
169-
170-
Handle Minimal Metadata
171-
-----------------------
172-
173-
174187
Publish a Feed
175188
--------------
176189

190+
We'll use the **rss** module to build a simple feed of recent posts. Start with
191+
the basic `author`, blog `title`, `description` and `url` configured in the
192+
`config.json`. Then, each post's `title` is the first header present in the
193+
post, the `description` is the first paragraph, and the date is the date you
194+
first created the post file.
195+
177196
Journo.feed = ->
178197
RSS = require 'rss'
179-
loadConfig()
198+
do loadConfig
180199
config = shared.config
181200
182201
feed = new RSS
@@ -187,28 +206,26 @@ Publish a Feed
187206
author: config.author
188207
189208
for post in sortedPosts()[0...20]
190-
content = fs.readFileSync(postPath post).toString()
191-
lexed = marked.lexer content
192-
title = postTitle content
193-
description = _.find(lexed, (token) -> token.type is 'paragraph')?.text + ' ...'
194-
description = marked.parser marked.lexer _.template(description)(renderVariables(post))
195-
209+
entry = shared.manifest[post]
196210
feed.item
197-
title: title
198-
description: description
211+
title: entry.title
212+
description: entry.description
199213
url: postUrl post
200-
date: shared.manifest[post].pubtime
214+
date: entry.pubtime
201215
202216
feed.xml()
203217
204218

205219
Quickly Bootstrap a New Blog
206220
----------------------------
207221

222+
We **init** a new blog into the current directory by copying over the contents
223+
of a basic `bootstrap` folder.
224+
208225
Journo.init = ->
209226
here = fs.realpathSync '.'
210227
if fs.existsSync 'posts'
211-
return console.error "A blog already exists in #{here}"
228+
fatal "A blog already exists in #{here}"
212229
bootstrap = path.join(__dirname, 'bootstrap')
213230
exec "rsync -vur --delete #{bootstrap} .", (err, stdout, stderr) ->
214231
throw err if err
@@ -218,37 +235,56 @@ Quickly Bootstrap a New Blog
218235
Preview via a Local Server
219236
--------------------------
220237

238+
Instead of constantly rebuilding a purely static version of the site, Journo
239+
provides a preview server (which you can start by just typing `journo` from
240+
within your blog).
241+
221242
Journo.preview = ->
222243
http = require 'http'
223244
mime = require 'mime'
224245
url = require 'url'
225246
util = require 'util'
226-
loadConfig()
227-
loadManifest()
247+
do loadManifest
248+
228249
server = http.createServer (req, res) ->
229250
rawPath = url.parse(req.url).pathname.replace(/(^\/|\/$)/g, '') or 'index'
251+
252+
If the request is for a preview of the RSS feed...
253+
230254
if rawPath is 'feed.rss'
231255
res.writeHead 200, 'Content-Type': mime.lookup('.rss')
232256
res.end Journo.feed()
257+
258+
If the request is for a static file that exists in our `public` directory...
259+
233260
else
234261
publicPath = "public/" + rawPath
235262
fs.exists publicPath, (exists) ->
236263
if exists
237264
res.writeHead 200, 'Content-Type': mime.lookup(publicPath)
238265
fs.createReadStream(publicPath).pipe res
266+
267+
If the request is for the slug of a valid post, we reload the layout, and
268+
render it...
269+
239270
else
240271
post = "posts/#{rawPath}.md"
241272
fs.exists post, (exists) ->
242273
if exists
274+
loadLayout true
243275
fs.readFile post, (err, content) ->
244276
res.writeHead 200, 'Content-Type': 'text/html'
245277
res.end Journo.render post, content
278+
279+
Anything else is a 404.
280+
246281
else
247282
res.writeHead 404
248283
res.end '404 Not Found'
249284
250285
server.listen 1234
251286
console.log "Journo is previewing at http://localhost:1234"
287+
exec "open http://localhost:1234"
252288
253289

254290
Work Without JavaScript, But Default to a Fluid JavaScript-Enabled UI
@@ -267,50 +303,85 @@ as well as readers.
267303
Finally, Putting it all Together. Run Journo From the Terminal
268304
--------------------------------------------------------------
269305

306+
We'll do the simplest possible command-line interface. If a public function
307+
exists on the `Journo` object, you can run it. *Note that this lets you do
308+
silly things, like* `journo toString` *but no big deal.*
309+
270310
Journo.run = ->
271-
args = process.argv.slice 2
272-
command = args[0] or 'preview'
273-
if Journo[command]
274-
do Journo[command]
275-
else
276-
console.error "Journo doesn't know how to '#{command}'"
311+
command = process.argv[2] or 'preview'
312+
return do Journo[command] if Journo[command]
313+
console.error "Journo doesn't know how to '#{command}'"
277314
278315

279316
Miscellaneous Bits and Utilities
280317
--------------------------------
281318

282-
For convenience, keep functions handy for finding the local file path to a post,
283-
and the URL for a post on the server.
319+
Little utility functions that are useful up above.
320+
321+
The file path to the source of a given `post`.
284322

285323
postPath = (post) -> "posts/#{post}"
286324
325+
The server-side path to the HTML for a given `post`.
326+
287327
htmlPath = (post) ->
288328
name = postName post
289329
if name is 'index'
290330
'site/index.html'
291331
else
292332
"site/#{name}/index.html"
293333
334+
The name (or slug) of a post, taken from the filename.
335+
294336
postName = (post) -> path.basename post, '.md'
295337
338+
The full, absolute URL for a published post.
339+
296340
postUrl = (post) -> "#{shared.siteUrl}/#{postName(post)}/"
297341
298-
postTitle = (content) ->
342+
Starting with the string contents of a post, detect the title --
343+
the first heading.
344+
345+
detectTitle = (content) ->
299346
_.find(marked.lexer(content), (token) -> token.type is 'heading')?.text
300347
348+
Starting with the string contents of a post, detect the description --
349+
the first paragraph.
350+
351+
detectDescription = (content, post) ->
352+
desc = _.find(marked.lexer(content), (token) -> token.type is 'paragraph')?.text
353+
marked.parser marked.lexer _.template("#{desc}...")(renderVariables(post))
354+
355+
Helper function to read in the contents of a folder, ignoring hidden files
356+
and directories.
357+
301358
folderContents = (folder) ->
302359
fs.readdirSync(folder).filter (f) -> f.charAt(0) isnt '.'
303360
361+
Return the list of posts currently in the manifest, sorted by their date of
362+
publication.
363+
304364
sortedPosts = ->
305365
_.sortBy _.without(_.keys(shared.manifest), 'index.md'), (post) ->
306366
shared.manifest[post].pubtime
307367
308-
The shared variables we want to allow our templates to use in their evaluations.
368+
The shared variables we want to allow our templates (both posts, and layout)
369+
to use in their evaluations.
309370

310371
renderVariables = (post) ->
311-
{_, fs, path, folderContents, mapLink, postName, post: path.basename(post), posts: sortedPosts(), manifest: shared.manifest}
312-
313-
Quick function to creating a link to a Google Map.
372+
{
373+
_
374+
fs
375+
path
376+
mapLink
377+
postName
378+
folderContents
379+
posts: sortedPosts()
380+
post: path.basename(post)
381+
manifest: shared.manifest
382+
}
383+
384+
Quick function which creates a link to a Google Map of the place.
314385

315386
mapLink = (place, additional = '', zoom = 15) ->
316387
query = encodeURIComponent("#{place}, #{additional}")
@@ -320,13 +391,12 @@ Convenience function for catching errors (keeping the preview server from
320391
crashing while testing code), and printing them out.
321392

322393
catchErrors = (func) ->
323-
try
324-
func()
394+
try do func
325395
catch err
326396
console.error err.stack
327397
"<pre>#{err.stack}</pre>"
328398
329-
And then for errors that you want the app to die on -- things that would break
399+
Finally, for errors that you want the app to die on -- things that should break
330400
the site build.
331401

332402
fatal = (message) ->

0 commit comments

Comments
 (0)
Please sign in to comment.