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.
@@ -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: