clayoven 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- Core.main
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
- Core.main
23
+ Clayoven.main
24
24
  end
@@ -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 Core
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") { |targetio|
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 { |entry|
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
- puts "error: design/template.slim file not found; aborting"
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] }.uniq
87
+ topics = index_files.map { |file| file.split(".index")[0] }
81
88
 
82
- # Next, look for stray files
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 { |stray_entry|
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
- # First, fill in all the page attributes
93
- (index_pages + content_pages).each { |page|
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 { |topic|
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
@@ -1,80 +1,109 @@
1
- class Paragraph
2
- attr_accessor :content, :first, :type
1
+ module ClayText
3
2
 
4
- def initialize(content)
5
- @content = content
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
- def is_first?
11
- @first
12
- end
13
- end
6
+ # see: http://php.net/manual/en/function.htmlspecialchars.php
7
+ HTMLESCAPE_RULES = {
8
+ "&" => "&amp;",
9
+ "\"" => "&quot;",
10
+ "'" => "&#39;",
11
+ "<" => "&lt;",
12
+ ">" => "&gt;"
13
+ }
14
14
 
15
- module ClayText
16
- def self.mark_emailquote!(paragraph)
17
- paragraph.type = :emailquote
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
- def self.mark_codeblock!(paragraph)
21
- paragraph.type = :codeblock
22
- end
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? "&gt; " } => 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
- def self.anchor_footerlinks!(paragraph)
25
- paragraph.content.gsub!(%r{^(\[\d+\]:) (.*://(.*))}) {
26
- "#{$1} <a href=\"#{$2}\">#{$3[0, 64]}#{%{...} if $3.length > 67}</a>"
27
- }
28
- paragraph.type = :footer
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
- "&" => "&amp;",
34
- "\"" => "&quot;",
35
- "'" => "&#39;",
36
- "<" => "&lt;",
37
- ">" => "&gt;"
38
- }.freeze
39
-
40
- paragraph_types = [:plain, :emailquote, :codeblock, :header, :footer]
41
- paragraph_rules = {
42
- Proc.new { |line| line.start_with? "&gt; " } => 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!(/[&"'<>]/, htmlescape_rules)
74
+ body.gsub!(/[&"'<>]/, ClayText::HTMLESCAPE_RULES)
49
75
 
76
+ # Split the body into Paragraphs
50
77
  paragraphs = []
51
- body.split("\n\n").each { |content|
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
- # Paragraph-level processing
62
- paragraphs.each { |paragraph|
63
- paragraph_rules.each { |proc_match, callback|
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
- callback.call paragraph
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
@@ -1,24 +1,39 @@
1
1
  require 'yaml'
2
2
 
3
- class ConfigData
4
- attr_accessor :rootpath, :rcpath, :ignorepath, :rc, :ignore
3
+ module Clayoven
4
+ class ConfigData
5
+ attr_accessor :rootpath, :rcpath, :ignorepath, :rc, :ignore
5
6
 
6
- def initialize
7
- @rootpath = ".clayoven"
8
- @rcpath = File.expand_path "~/.clayovenrc"
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
- Dir.mkdir @rootpath if not Dir.exists? @rootpath
15
- if File.exists? @ignorepath
16
- @ignore = IO.read(@ignorepath).split("\n")
17
- else
18
- File.open(@ignorepath, "w") { |ignoreio|
19
- ignoreio.write @ignore.join("\n") }
20
- puts "[NOTE] #{@ignorepath} populated with sane defaults"
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
@@ -1,24 +1,31 @@
1
1
  require 'webrick'
2
2
 
3
- module Httpd
4
- def self.start
5
- port = 8000
6
- callback = Proc.new { |req, res|
7
- if %r{^/$} =~ req.path_info
8
- res.set_redirect WEBrick::HTTPStatus::Found, "index.html"
9
- end
10
- if %r{^/([^.]*)$} =~ req.path_info
11
- res.set_redirect WEBrick::HTTPStatus::Found, "#{$1}.html"
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
- server = WEBrick::HTTPServer.new(:Port => port,
16
- :RequestCallback => callback,
17
- :DocumentRoot => Dir.pwd)
21
+ server = WEBrick::HTTPServer.new(:Port => port,
22
+ :RequestCallback => callback,
23
+ :DocumentRoot => Dir.pwd)
18
24
 
19
- puts "clayoven serving at: http://localhost:#{port}"
25
+ puts "clayoven serving at: http://localhost:#{port}"
20
26
 
21
- trap(:INT) { server.shutdown }
22
- server.start
27
+ trap(:INT) { server.shutdown }
28
+ server.start
29
+ end
23
30
  end
24
31
  end
@@ -1,46 +1,44 @@
1
1
  require 'net/imap'
2
2
  require_relative 'config'
3
3
 
4
- class Mail
5
- attr_accessor :filename, :date, :msgid
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
- def initialize(filename, date, msgid)
8
- @filename = filename
9
- @date = date
10
- @msgid = msgid
11
- end
12
- end
13
-
14
- module Imapd
15
- def self.poll
16
- config = ConfigData.new
17
- if not config.rc
18
- puts "error: #{config.rcpath} not found; aborting"
19
- exit 1
20
- end
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
- server.disconnect
44
- mails
40
+ server.disconnect
41
+ mails
42
+ end
45
43
  end
46
44
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clayoven
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: