clayoven 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/clayoven +4 -4
- data/lib/clayoven.rb +30 -21
- data/lib/clayoven/claytext.rb +82 -53
- data/lib/clayoven/config.rb +32 -17
- data/lib/clayoven/httpd.rb +23 -16
- data/lib/clayoven/imapd.rb +37 -39
- metadata +1 -1
data/bin/clayoven
CHANGED
@@ -6,12 +6,12 @@ require 'clayoven'
|
|
6
6
|
|
7
7
|
case ARGV[0]
|
8
8
|
when "httpd"
|
9
|
-
Httpd.start
|
9
|
+
Clayoven::Httpd.start
|
10
10
|
when "imapd"
|
11
11
|
while 1
|
12
|
-
mails = Imapd.poll
|
12
|
+
mails = Clayoven::Imapd.poll
|
13
13
|
if not mails.empty?
|
14
|
-
|
14
|
+
Clayoven.main
|
15
15
|
mails.each { |mail|
|
16
16
|
`git add .`
|
17
17
|
puts `git commit -a -m "#{mail.filename}: new post\n\n#{mail.date}\n#{mail.msgid}"`
|
@@ -20,5 +20,5 @@ when "imapd"
|
|
20
20
|
sleep 1800
|
21
21
|
end
|
22
22
|
else
|
23
|
-
|
23
|
+
Clayoven.main
|
24
24
|
end
|
data/lib/clayoven.rb
CHANGED
@@ -6,6 +6,9 @@ require 'clayoven/claytext'
|
|
6
6
|
require 'clayoven/httpd'
|
7
7
|
require 'clayoven/imapd'
|
8
8
|
|
9
|
+
# Figures out the timestamp of the commit that introduced a specific
|
10
|
+
# file. If the file hasn't been checked into git yet, return the
|
11
|
+
# current time.
|
9
12
|
def when_introduced(filename)
|
10
13
|
timestamp = `git log --reverse --pretty="%at" #{filename} 2>/dev/null | head -n 1`.strip
|
11
14
|
if timestamp == ""
|
@@ -15,20 +18,23 @@ def when_introduced(filename)
|
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
18
|
-
module
|
21
|
+
module Clayoven
|
19
22
|
class Page
|
20
23
|
attr_accessor :filename, :permalink, :timestamp, :title, :topic, :body,
|
21
24
|
:paragraphs, :target, :indexfill, :topics
|
22
25
|
|
26
|
+
# Writes out HTML pages. Takes a list of topics to render
|
27
|
+
#
|
28
|
+
# Prints a "[GEN]" line for every file it writes out.
|
23
29
|
def render(topics)
|
24
30
|
@topics = topics
|
25
31
|
@paragraphs = ClayText.process! @body
|
26
32
|
Slim::Engine.set_default_options pretty: true, sort_attrs: false
|
27
33
|
rendered = Slim::Template.new { IO.read("design/template.slim") }.render(self)
|
28
|
-
File.open(@target, mode="w")
|
34
|
+
File.open(@target, mode="w") do |targetio|
|
29
35
|
nbytes = targetio.write(rendered)
|
30
36
|
puts "[GEN] #{@target} (#{nbytes} bytes out)"
|
31
|
-
|
37
|
+
end
|
32
38
|
end
|
33
39
|
end
|
34
40
|
|
@@ -59,47 +65,50 @@ module Core
|
|
59
65
|
end
|
60
66
|
|
61
67
|
def self.main
|
62
|
-
if not File.exists? "index"
|
63
|
-
puts "error: index file not found; aborting"
|
64
|
-
exit 1
|
65
|
-
end
|
68
|
+
abort "error: index file not found; aborting" if not File.exists? "index"
|
66
69
|
|
67
|
-
config = ConfigData.new
|
70
|
+
config = Clayoven::ConfigData.new
|
68
71
|
all_files = (Dir.entries(".") -
|
69
|
-
[".", "..", ".clayoven", "design"]).reject
|
72
|
+
[".", "..", ".clayoven", "design"]).reject do |entry|
|
70
73
|
config.ignore.any? { |pattern| %r{#{pattern}} =~ entry }
|
71
|
-
|
74
|
+
end
|
72
75
|
|
76
|
+
# We must have a "design" directory. I don't plan on making this
|
77
|
+
# a configuration variable.
|
73
78
|
if not Dir.entries("design").include? "template.slim"
|
74
|
-
|
75
|
-
exit 1
|
79
|
+
abort "error: design/template.slim file not found; aborting"
|
76
80
|
end
|
77
81
|
|
82
|
+
# index_files are files ending in ".index" and "index"
|
83
|
+
# content_files are all other files (we've already applied ignore)
|
84
|
+
# topics is the list of topics. We need it for the sidebar
|
78
85
|
index_files = ["index"] + all_files.select { |file| /\.index$/ =~ file }
|
79
86
|
content_files = all_files - index_files
|
80
|
-
topics = index_files.map { |file| file.split(".index")[0] }
|
87
|
+
topics = index_files.map { |file| file.split(".index")[0] }
|
81
88
|
|
82
|
-
#
|
89
|
+
# Look for stray files. All content_files that don't have a valid
|
90
|
+
# topic before ":" (or don't have ";" in their filename at all)
|
83
91
|
(content_files.reject { |file| topics.include? (file.split(":", 2)[0]) })
|
84
|
-
.each
|
92
|
+
.each do |stray_entry|
|
85
93
|
content_files = content_files - [stray_entry]
|
86
94
|
puts "warning: #{stray_entry} is a stray file or directory; ignored"
|
87
|
-
|
95
|
+
end
|
88
96
|
|
97
|
+
# Turn index_files and content_files into objects
|
89
98
|
index_pages = index_files.map { |filename| IndexPage.new(filename) }
|
90
99
|
content_pages = content_files.map { |filename| ContentPage.new(filename) }
|
91
100
|
|
92
|
-
#
|
93
|
-
(index_pages + content_pages).each
|
101
|
+
# Fill in page.title and page.body by reading the file
|
102
|
+
(index_pages + content_pages).each do |page|
|
94
103
|
page.title, page.body = (IO.read page.filename).split("\n\n", 2)
|
95
|
-
|
104
|
+
end
|
96
105
|
|
97
106
|
# Compute the indexfill for indexes
|
98
|
-
topics.each
|
107
|
+
topics.each do |topic|
|
99
108
|
topic_index = index_pages.select { |page| page.topic == topic }[0]
|
100
109
|
topic_index.indexfill = content_pages.select { |page|
|
101
110
|
page.topic == topic }.sort { |a, b| b.timestamp <=> a.timestamp }
|
102
|
-
|
111
|
+
end
|
103
112
|
|
104
113
|
(index_pages + content_pages).each { |page| page.render topics }
|
105
114
|
end
|
data/lib/clayoven/claytext.rb
CHANGED
@@ -1,80 +1,109 @@
|
|
1
|
-
|
2
|
-
attr_accessor :content, :first, :type
|
1
|
+
module ClayText
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
@first = false
|
7
|
-
@type = :plain
|
8
|
-
end
|
3
|
+
# These are the values that Paragraph.type can take
|
4
|
+
PARAGRAPH_TYPES = [:plain, :emailquote, :codeblock, :header, :footer]
|
9
5
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
# see: http://php.net/manual/en/function.htmlspecialchars.php
|
7
|
+
HTMLESCAPE_RULES = {
|
8
|
+
"&" => "&",
|
9
|
+
"\"" => """,
|
10
|
+
"'" => "'",
|
11
|
+
"<" => "<",
|
12
|
+
">" => ">"
|
13
|
+
}
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
15
|
+
# Key is used to match a paragraph, and value is the lambda
|
16
|
+
# that'll act on it.
|
17
|
+
PARAGRAPH_RULES = {
|
19
18
|
|
20
|
-
|
21
|
-
paragraph
|
22
|
-
|
19
|
+
# If all the lines in a paragraph, begin with "> ", the
|
20
|
+
# paragraph is marked as an :emailquote.
|
21
|
+
Proc.new { |line| line.start_with? "> " } => lambda { |paragraph|
|
22
|
+
paragraph.type = :emailquote },
|
23
|
+
|
24
|
+
# If all the lines in a paragraph, begin with " ", the paragraph is
|
25
|
+
# marked as an :coeblock
|
26
|
+
Proc.new { |line| line.start_with? " " } => lambda { |paragraph|
|
27
|
+
paragraph.type = :codeblock },
|
28
|
+
|
29
|
+
# If all the lines in a paragraph, begin with " ", the paragraph
|
30
|
+
# is marked as :footer. Also, a regex substitution runs on each
|
31
|
+
# line turning every link like http://a-url-over-67-characters
|
32
|
+
# to <a href="http://google.com">64-characters-of-the-li...</a>
|
33
|
+
Proc.new { |line| /^\[\d+\]: / =~ line } => lambda do |paragraph|
|
34
|
+
paragraph.type = :footer
|
35
|
+
paragraph.content.gsub!(%r{^(\[\d+\]:) (.*://(.*))}) do
|
36
|
+
"#{$1} <a href=\"#{$2}\">#{$3[0, 64]}#{%{...} if $3.length > 67}</a>"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
}
|
23
40
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
41
|
+
# A paragraph of text
|
42
|
+
#
|
43
|
+
# :content contains its content
|
44
|
+
# :fist asserts whether it's the first paragraph in the body
|
45
|
+
# :type can be one of PARAGRAPH_TYPES
|
46
|
+
class Paragraph
|
47
|
+
attr_accessor :content, :first, :type
|
48
|
+
|
49
|
+
def initialize(content)
|
50
|
+
@content = content
|
51
|
+
@first = false
|
52
|
+
@type = :plain
|
53
|
+
|
54
|
+
# Generate is_*? methods for PARAGRAPH_TYPES
|
55
|
+
Paragraph.class_eval do
|
56
|
+
ClayText::PARAGRAPH_TYPES.each do |type|
|
57
|
+
define_method("is_#{type.to_s}?") { @type == type }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_first?
|
63
|
+
@first
|
64
|
+
end
|
29
65
|
end
|
30
66
|
|
67
|
+
# Takes a body of claytext, breaks it up into paragraphs, and
|
68
|
+
# applies various rules on it.
|
69
|
+
#
|
70
|
+
# Returns a list of Paragraphs
|
31
71
|
def self.process!(body)
|
32
|
-
htmlescape_rules = {
|
33
|
-
"&" => "&",
|
34
|
-
"\"" => """,
|
35
|
-
"'" => "'",
|
36
|
-
"<" => "<",
|
37
|
-
">" => ">"
|
38
|
-
}.freeze
|
39
|
-
|
40
|
-
paragraph_types = [:plain, :emailquote, :codeblock, :header, :footer]
|
41
|
-
paragraph_rules = {
|
42
|
-
Proc.new { |line| line.start_with? "> " } => method(:mark_emailquote!),
|
43
|
-
Proc.new { |line| line.start_with? " " } => method(:mark_codeblock!),
|
44
|
-
Proc.new { |line| /^\[\d+\]: / =~ line } => method(:anchor_footerlinks!)
|
45
|
-
}.freeze
|
46
72
|
|
47
73
|
# First, htmlescape the body text
|
48
|
-
body.gsub!(/[&"'<>]/,
|
74
|
+
body.gsub!(/[&"'<>]/, ClayText::HTMLESCAPE_RULES)
|
49
75
|
|
76
|
+
# Split the body into Paragraphs
|
50
77
|
paragraphs = []
|
51
|
-
body.split("\n\n").each
|
78
|
+
body.split("\n\n").each do |content|
|
52
79
|
paragraphs << Paragraph.new(content)
|
53
|
-
|
80
|
+
end
|
54
81
|
|
82
|
+
# Special matching for the first paragraph. This paragraph will
|
83
|
+
# be marked header:
|
84
|
+
#
|
85
|
+
# (This is a really long first paragraph blah-blah-blah-blah-blah
|
86
|
+
# that spans to two lines)
|
55
87
|
paragraphs[0].first = true
|
56
88
|
if paragraphs[0].content.start_with? "(" and
|
57
89
|
paragraphs[0].content.end_with? ")"
|
58
90
|
paragraphs[0].type = :header
|
59
91
|
end
|
60
92
|
|
61
|
-
#
|
62
|
-
paragraphs.each
|
63
|
-
|
93
|
+
# Apply the PARAGRAPH_RULES on all the paragraphs
|
94
|
+
paragraphs.each do |paragraph|
|
95
|
+
ClayText::PARAGRAPH_RULES.each do |proc_match, lambda_cb|
|
64
96
|
if paragraph.content.lines.all? &proc_match
|
65
|
-
|
97
|
+
lambda_cb.call paragraph
|
66
98
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# Generate is_*? methods for Paragraph
|
71
|
-
Paragraph.class_eval {
|
72
|
-
paragraph_types.each { |type|
|
73
|
-
define_method("is_#{type.to_s}?") { @type == type }
|
74
|
-
}
|
75
|
-
}
|
99
|
+
end
|
100
|
+
end
|
76
101
|
|
102
|
+
# body is the useless version. If someone is too lazy to use all
|
103
|
+
# the paragraphs individually in their template, they can just use
|
104
|
+
# this.
|
77
105
|
body = paragraphs.map(&:content).join("\n\n")
|
106
|
+
|
78
107
|
paragraphs
|
79
108
|
end
|
80
109
|
end
|
data/lib/clayoven/config.rb
CHANGED
@@ -1,24 +1,39 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
module Clayoven
|
4
|
+
class ConfigData
|
5
|
+
attr_accessor :rootpath, :rcpath, :ignorepath, :rc, :ignore
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
@ignorepath = "#{rootpath}/ignore"
|
10
|
-
@ignore = ["\\.html$", "~$", "^.\#", "^\#.*\#$",
|
11
|
-
"^\\.git$", "^\\.gitignore$", "^\\.htaccess$"]
|
12
|
-
@rc = nil
|
7
|
+
def initialize
|
8
|
+
@rootpath = ".clayoven"
|
9
|
+
Dir.mkdir @rootpath if not Dir.exists? @rootpath
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
initialize_ignore
|
12
|
+
initialize_rc
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize_ignore
|
16
|
+
@ignorepath = "#{rootpath}/ignore"
|
17
|
+
|
18
|
+
# Most common patterns that should sit in .clayoven/ignore.
|
19
|
+
# Written to the file when it doesn't exist.
|
20
|
+
@ignore = ["\\.html$", "~$", "^.\#", "^\#.*\#$",
|
21
|
+
"^\\.git$", "^\\.gitignore$", "^\\.htaccess$"]
|
22
|
+
|
23
|
+
if File.exists? @ignorepath
|
24
|
+
@ignore = IO.read(@ignorepath).split("\n")
|
25
|
+
else
|
26
|
+
File.open(@ignorepath, "w") do |ignoreio|
|
27
|
+
ignoreio.write @ignore.join("\n")
|
28
|
+
end
|
29
|
+
puts "[NOTE] #{@ignorepath} populated with sane defaults"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize_rc
|
34
|
+
@rcpath = File.expand_path "~/.clayovenrc"
|
35
|
+
@rc = nil
|
36
|
+
@rc = YAML.load_file @rcpath if File.exists? @rcpath
|
21
37
|
end
|
22
|
-
@rc = YAML.load_file @rcpath if File.exists? @rcpath
|
23
38
|
end
|
24
39
|
end
|
data/lib/clayoven/httpd.rb
CHANGED
@@ -1,24 +1,31 @@
|
|
1
1
|
require 'webrick'
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
module Clayoven
|
4
|
+
module Httpd
|
5
|
+
def self.start
|
6
|
+
port = 8000
|
7
|
+
callback = Proc.new do |req, res|
|
8
|
+
|
9
|
+
# A couple of URL rewriting rules. Not real URL rewriting
|
10
|
+
# like .htaccess; just a HTTP redirect. / is rewritten to
|
11
|
+
# index.html, and anything-without-a-period is rewritten to
|
12
|
+
# that-thing.html.
|
13
|
+
if %r{^/$} =~ req.path_info
|
14
|
+
res.set_redirect WEBrick::HTTPStatus::Found, "index.html"
|
15
|
+
end
|
16
|
+
if %r{^/([^.]*)$} =~ req.path_info
|
17
|
+
res.set_redirect WEBrick::HTTPStatus::Found, "#{$1}.html"
|
18
|
+
end
|
12
19
|
end
|
13
|
-
}
|
14
20
|
|
15
|
-
|
16
|
-
|
17
|
-
|
21
|
+
server = WEBrick::HTTPServer.new(:Port => port,
|
22
|
+
:RequestCallback => callback,
|
23
|
+
:DocumentRoot => Dir.pwd)
|
18
24
|
|
19
|
-
|
25
|
+
puts "clayoven serving at: http://localhost:#{port}"
|
20
26
|
|
21
|
-
|
22
|
-
|
27
|
+
trap(:INT) { server.shutdown }
|
28
|
+
server.start
|
29
|
+
end
|
23
30
|
end
|
24
31
|
end
|
data/lib/clayoven/imapd.rb
CHANGED
@@ -1,46 +1,44 @@
|
|
1
1
|
require 'net/imap'
|
2
2
|
require_relative 'config'
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
module Clayoven
|
5
|
+
# `clayoven impad` essentially calls Imapd.poll
|
6
|
+
# (but also calls Clayoven.main)
|
7
|
+
module Imapd
|
8
|
+
# Initialites a connection to the IMAP server, and fetches new
|
9
|
+
# messages.
|
10
|
+
#
|
11
|
+
# Returns an unnamed Struct with :filename, :date, :msgid fields
|
12
|
+
def self.poll
|
13
|
+
config = Clayoven::ConfigData.new
|
14
|
+
abort "error: #{config.rcpath} not found; aborting" if not config.rc
|
15
|
+
mails = []
|
16
|
+
server = Net::IMAP.new(config.rc["server"],
|
17
|
+
{:port => config.rc["port"], :ssl => config.rc["ssl"]})
|
18
|
+
trap(:INT) { exit 1 }
|
19
|
+
server.login config.rc["username"], config.rc["password"]
|
20
|
+
puts "[NOTE] LOGIN successful"
|
21
|
+
server.examine "INBOX"
|
22
|
+
server.search(["ALL"]).each do |id|
|
23
|
+
message = server.fetch(id, ["ENVELOPE", "RFC822.TEXT"])[0]
|
6
24
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
mails = []
|
22
|
-
server = Net::IMAP.new(config.rc["server"],
|
23
|
-
{:port => config.rc["port"], :ssl => config.rc["ssl"]})
|
24
|
-
trap(:INT) { exit 1 }
|
25
|
-
server.login config.rc["username"], config.rc["password"]
|
26
|
-
puts "[NOTE] LOGIN successful"
|
27
|
-
server.examine "INBOX"
|
28
|
-
server.search(["ALL"]).each { |id|
|
29
|
-
message = server.fetch(id, ["ENVELOPE", "RFC822.TEXT"])[0]
|
30
|
-
if message.attr["ENVELOPE"].sender[0].mailbox == "artagnon" and
|
31
|
-
message.attr["ENVELOPE"].sender[0].host == "gmail.com" and
|
32
|
-
message.attr["ENVELOPE"].sender[0].name == "Ramkumar Ramachandra"
|
33
|
-
date = message.attr["ENVELOPE"].date
|
34
|
-
msgid = message.attr["ENVELOPE"].message_id
|
35
|
-
title, filename = message.attr["ENVELOPE"].subject.split(" # ")
|
36
|
-
next if File.exists? filename
|
37
|
-
File.open(filename, "w") { |targetio|
|
38
|
-
targetio.write([title, message.attr["RFC822.TEXT"].delete("\r")].join "\n\n")
|
39
|
-
}
|
40
|
-
mails << Mail.new(filename, date, msgid)
|
25
|
+
# This block is only run if we receive email from the trusted
|
26
|
+
# sender (a configuration variable).
|
27
|
+
trustmailbox, trusthost = config.rc["trustfrom"].split("@")
|
28
|
+
if message.attr["ENVELOPE"].sender[0].mailbox == trustmailbox and
|
29
|
+
message.attr["ENVELOPE"].sender[0].host == trusthost
|
30
|
+
date = message.attr["ENVELOPE"].date
|
31
|
+
msgid = message.attr["ENVELOPE"].message_id
|
32
|
+
title, filename = message.attr["ENVELOPE"].subject.split(" # ")
|
33
|
+
next if File.exists? filename
|
34
|
+
File.open(filename, "w") do |targetio|
|
35
|
+
targetio.write([title, message.attr["RFC822.TEXT"].delete("\r")].join "\n\n")
|
36
|
+
end
|
37
|
+
mails << Struct.new(:filename, :date, :msgid).new(filename, date, msgid)
|
38
|
+
end
|
41
39
|
end
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
server.disconnect
|
41
|
+
mails
|
42
|
+
end
|
45
43
|
end
|
46
44
|
end
|