clayoven 0.1 → 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|