zine 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +241 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/zine +5 -0
- data/lib/zine.rb +156 -0
- data/lib/zine/cli.rb +57 -0
- data/lib/zine/data_page.rb +23 -0
- data/lib/zine/feed.rb +44 -0
- data/lib/zine/page.rb +118 -0
- data/lib/zine/post.rb +40 -0
- data/lib/zine/server.rb +40 -0
- data/lib/zine/skeleton/source/about.md +5 -0
- data/lib/zine/skeleton/source/posts/2017-01-25-my-new-blog.md +11 -0
- data/lib/zine/skeleton/source/screen.css +1 -0
- data/lib/zine/skeleton/source/templates/default.erb +8 -0
- data/lib/zine/skeleton/source/templates/footer_partial.erb +11 -0
- data/lib/zine/skeleton/source/templates/header_partial.erb +23 -0
- data/lib/zine/skeleton/source/templates/home.erb +18 -0
- data/lib/zine/skeleton/source/templates/new_post.erb +11 -0
- data/lib/zine/skeleton/source/templates/post.erb +16 -0
- data/lib/zine/skeleton/source/templates/post_index.erb +15 -0
- data/lib/zine/skeleton/source/templates/tag.erb +15 -0
- data/lib/zine/skeleton/source/templates/tag_index.erb +15 -0
- data/lib/zine/skeleton/zine.yaml +26 -0
- data/lib/zine/tag.rb +57 -0
- data/lib/zine/templates.rb +14 -0
- data/lib/zine/version.rb +3 -0
- metadata +200 -0
data/lib/zine/cli.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'rainbow'
|
3
|
+
require 'time'
|
4
|
+
require 'yaml'
|
5
|
+
require 'zine'
|
6
|
+
require 'zine/version'
|
7
|
+
|
8
|
+
module Zine
|
9
|
+
# CLI for zine
|
10
|
+
class CLI < Thor
|
11
|
+
include Thor::Actions
|
12
|
+
|
13
|
+
no_commands do
|
14
|
+
def init_site
|
15
|
+
@site ||= Zine::Site.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'build', 'Build the site'
|
20
|
+
def build
|
21
|
+
init_site
|
22
|
+
@site.build_site
|
23
|
+
puts Rainbow('Site built').green
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'nuke', 'Delete the build folder'
|
27
|
+
def nuke
|
28
|
+
init_site
|
29
|
+
FileUtils.remove_dir @site.options['directories']['build'], force: true
|
30
|
+
puts Rainbow('Site nuked. It\'s the only way to be sure.').green
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'post TITLE', 'Create the file for a new blog post, titled TITLE'
|
34
|
+
def post(name)
|
35
|
+
init_site
|
36
|
+
option_dir = @site.options['directories']
|
37
|
+
Zine::CLI.source_root option_dir['templates']
|
38
|
+
@date = DateTime.now
|
39
|
+
@name = name
|
40
|
+
file = "#{@date.strftime('%Y-%m-%d')}-#{Zine::Page.slug(name)}.md"
|
41
|
+
template 'new_post.erb',
|
42
|
+
File.join(Dir.pwd, option_dir['posts'], file)
|
43
|
+
end
|
44
|
+
|
45
|
+
desc 'site', 'Create the skeleton of a new site (overwriting files)'
|
46
|
+
def site
|
47
|
+
@skeleton_dir = File.join File.dirname(__FILE__), 'skeleton', '/.'
|
48
|
+
FileUtils.cp_r @skeleton_dir, Dir.pwd
|
49
|
+
puts Rainbow('New skeleton site created').green
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'version', 'Show the version number'
|
53
|
+
def version
|
54
|
+
puts VERSION
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'zine/page'
|
2
|
+
|
3
|
+
module Zine
|
4
|
+
# A page where the content comes from an array, usually an array of
|
5
|
+
# links to other pages, eg an index page like the home page
|
6
|
+
class DataPage < Zine::Page
|
7
|
+
def initialize(data, templates, site_options)
|
8
|
+
init_templates(templates)
|
9
|
+
@formatted_data = FormattedData.new({}, site_options)
|
10
|
+
@formatted_data.page[:title] = data[:title]
|
11
|
+
@formatted_data.data = data[:post_array]
|
12
|
+
@dest_path = File.join(data[:build_dir],
|
13
|
+
Zine::Page.slug(data[:name]) + '.html')
|
14
|
+
write
|
15
|
+
end
|
16
|
+
|
17
|
+
def write
|
18
|
+
html = template_the_html
|
19
|
+
compressor = HtmlCompressor::Compressor.new
|
20
|
+
File.write(@dest_path, compressor.compress(html))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/zine/feed.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rss'
|
2
|
+
require 'uri'
|
3
|
+
# require 'zine/page'
|
4
|
+
|
5
|
+
module Zine # < Zine::Page
|
6
|
+
# produce the RSS/Atom feed for the site
|
7
|
+
class Feed
|
8
|
+
def initialize(post_array, options)
|
9
|
+
@post_array = post_array
|
10
|
+
@options = options['options']
|
11
|
+
@rss = create_rss
|
12
|
+
@dest_path = File.join(options['directories']['build'], 'rss.xml')
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_rss
|
16
|
+
RSS::Maker.make('atom') do |maker|
|
17
|
+
maker.channel.author = @options['site_author']
|
18
|
+
maker.channel.updated = @post_array[0].formatted_data
|
19
|
+
.page[:date_rfc3339].to_s
|
20
|
+
maker.channel.about = (URI.join @options['site_URL'], 'rss.xml').to_s
|
21
|
+
maker.channel.title = @options['site_name']
|
22
|
+
|
23
|
+
@post_array.each do |post|
|
24
|
+
maker.items.new_item do |item|
|
25
|
+
data = post.formatted_data
|
26
|
+
meta = data.page
|
27
|
+
item.link = data.uri
|
28
|
+
item.title = meta[:title]
|
29
|
+
item.updated = meta[:date_rfc3339].to_s
|
30
|
+
# item.content.content = data.html
|
31
|
+
# item.content.type = 'html'
|
32
|
+
# =><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">
|
33
|
+
item.content.type = 'xhtml'
|
34
|
+
item.content.xml_content = data.html
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end # rss
|
38
|
+
end # fn
|
39
|
+
|
40
|
+
def process
|
41
|
+
File.write(@dest_path, @rss)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/zine/page.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'date'
|
3
|
+
require 'htmlcompressor'
|
4
|
+
require 'kramdown'
|
5
|
+
require 'pathname'
|
6
|
+
require 'rainbow'
|
7
|
+
require 'yaml'
|
8
|
+
require 'zine'
|
9
|
+
require 'zine/templates'
|
10
|
+
|
11
|
+
module Zine
|
12
|
+
# A page on the site where the content comes from a file's markdown, and the
|
13
|
+
# destination's location mirrors its own
|
14
|
+
class Page
|
15
|
+
attr_reader :formatted_data
|
16
|
+
# the meta data, passed formatted to the template
|
17
|
+
class FormattedData
|
18
|
+
attr_accessor :data
|
19
|
+
attr_accessor :footer_partial
|
20
|
+
attr_accessor :header_partial
|
21
|
+
attr_accessor :html
|
22
|
+
attr_reader :page
|
23
|
+
attr_accessor :uri
|
24
|
+
|
25
|
+
def initialize(front_matter, site_opt)
|
26
|
+
@page = { date_rfc3339: front_matter['date'],
|
27
|
+
date_us: parse_date(front_matter['date']),
|
28
|
+
github_name: site_opt['options']['github_name'],
|
29
|
+
num_items_on_home: site_opt['options']['num_items_on_home'],
|
30
|
+
site_author: site_opt['options']['site_author'],
|
31
|
+
site_description: site_opt['options']['site_description'],
|
32
|
+
site_name: site_opt['options']['site_name'],
|
33
|
+
site_URL: site_opt['options']['site_URL'],
|
34
|
+
tags: slugify_tags(front_matter['tags']),
|
35
|
+
title: front_matter['title'],
|
36
|
+
twitter_name: site_opt['options']['twitter_name'] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_date(_d)
|
40
|
+
DateTime.rfc3339(front_matter['date']).strftime('%B %-d, %Y')
|
41
|
+
rescue
|
42
|
+
''
|
43
|
+
end
|
44
|
+
|
45
|
+
def public_binding
|
46
|
+
binding
|
47
|
+
end
|
48
|
+
|
49
|
+
def slugify_tags(tags)
|
50
|
+
return unless tags && tags.any?
|
51
|
+
tags.map { |tag| { name: tag, tag_slug: Page.slug(tag) } }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# the Tags on a Post
|
56
|
+
TagData = Struct.new(:tagsArray, :destURL, :pageTitle, :pageDate,
|
57
|
+
:pageDateUS)
|
58
|
+
|
59
|
+
def initialize(md_file_name, dest, templates, site_options)
|
60
|
+
file_parts = File.open(md_file_name, 'r').read.split('---')
|
61
|
+
@formatted_data = FormattedData.new(parse_yaml(file_parts[1]),
|
62
|
+
site_options)
|
63
|
+
@dest_path = dest
|
64
|
+
@raw_text = file_parts[2]
|
65
|
+
init_templates(templates)
|
66
|
+
end
|
67
|
+
|
68
|
+
def init_templates(templates)
|
69
|
+
@header_partial = templates.header
|
70
|
+
@footer_partial = templates.footer
|
71
|
+
@template = templates.body
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_markdown
|
75
|
+
@formatted_data.html = Kramdown::Document.new(
|
76
|
+
@raw_text,
|
77
|
+
input: 'GFM',
|
78
|
+
auto_ids: false,
|
79
|
+
smart_quotes: %w(apos apos quot quot),
|
80
|
+
syntax_highlighter: 'rouge'
|
81
|
+
).to_html
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_yaml(text)
|
85
|
+
YAML.safe_load text
|
86
|
+
rescue Psych::Exception
|
87
|
+
puts Rainbow("Could not parse front matter for: #{md_file_name}").red
|
88
|
+
{ 'date' => DateTime.now.to_s, 'title' => md_file_name, 'tags' => [] }
|
89
|
+
end
|
90
|
+
|
91
|
+
def rel_path_from_build_dir(path)
|
92
|
+
full = Pathname(path)
|
93
|
+
full.relative_path_from(Pathname(@build_dir))
|
94
|
+
end
|
95
|
+
|
96
|
+
def process
|
97
|
+
parse_markdown
|
98
|
+
html = template_the_html
|
99
|
+
|
100
|
+
compressor = HtmlCompressor::Compressor.new
|
101
|
+
File.write(@dest_path, compressor.compress(html))
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.slug(text)
|
105
|
+
text.downcase
|
106
|
+
.gsub(/[^a-z0-9]+/, '-')
|
107
|
+
.gsub(/^-|-$/, '')
|
108
|
+
end
|
109
|
+
|
110
|
+
def template_the_html
|
111
|
+
@formatted_data.header_partial = @header_partial.result(@formatted_data
|
112
|
+
.public_binding)
|
113
|
+
@formatted_data.footer_partial = @footer_partial.result(@formatted_data
|
114
|
+
.public_binding)
|
115
|
+
@template.result(@formatted_data.public_binding)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/zine/post.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Zine
|
4
|
+
# A post - content comes from the markdown, and the destination from the date
|
5
|
+
class Post < Page
|
6
|
+
def initialize(md_file_name, templates, site_options)
|
7
|
+
file_parts = File.open(md_file_name, 'r').read.split('---')
|
8
|
+
@formatted_data = FormattedData.new(parse_yaml(file_parts[1]),
|
9
|
+
site_options)
|
10
|
+
@raw_text = file_parts[2]
|
11
|
+
init_templates(templates)
|
12
|
+
option_dir = site_options['directories']
|
13
|
+
@build_dir = option_dir['build'] # for tags
|
14
|
+
@dest_path = make_path_from_date option_dir['blog']
|
15
|
+
end
|
16
|
+
|
17
|
+
def make_path_from_date(build_dir)
|
18
|
+
page_data = @formatted_data.page
|
19
|
+
date = DateTime.parse(page_data[:date_rfc3339])
|
20
|
+
dest_dir = File.join(build_dir,
|
21
|
+
date.strftime('%Y'),
|
22
|
+
date.strftime('%-m'))
|
23
|
+
FileUtils.mkdir_p dest_dir
|
24
|
+
slg = Zine::Page.slug(page_data[:title]) + '.html'
|
25
|
+
@dest_path = File.join(dest_dir, slg)
|
26
|
+
end
|
27
|
+
|
28
|
+
def process
|
29
|
+
super
|
30
|
+
page_data = @formatted_data.page
|
31
|
+
file_path = rel_path_from_build_dir(@dest_path).to_s
|
32
|
+
@formatted_data.uri = URI.join(page_data[:site_URL], file_path).to_s
|
33
|
+
TagData.new(page_data[:tags],
|
34
|
+
file_path,
|
35
|
+
page_data[:title],
|
36
|
+
page_data[:date_rfc3339],
|
37
|
+
page_data[:date_us])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/zine/server.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
require 'rack'
|
3
|
+
require 'thin'
|
4
|
+
|
5
|
+
module Zine
|
6
|
+
# Local preview web server
|
7
|
+
class Server
|
8
|
+
def initialize(root)
|
9
|
+
motd
|
10
|
+
Thin::Server.start('127.0.0.1', 8080) do
|
11
|
+
use Rack::Static,
|
12
|
+
urls: ['/'],
|
13
|
+
index: 'index.html',
|
14
|
+
root: root
|
15
|
+
|
16
|
+
now = Time.now
|
17
|
+
a_long_time = 100**4
|
18
|
+
run lambda { |_env|
|
19
|
+
[200,
|
20
|
+
{
|
21
|
+
'Content-Type' => 'text/html',
|
22
|
+
'ETag' => nil,
|
23
|
+
'Last-Modified' => now + a_long_time,
|
24
|
+
'Cache-Control' =>
|
25
|
+
'no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
|
26
|
+
'Pragma' => 'no-cache',
|
27
|
+
'Expires' => now - a_long_time
|
28
|
+
},
|
29
|
+
File.open(File.join(root, 'index.html'), File::RDONLY)]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def motd
|
35
|
+
puts "\nPreview running on " +
|
36
|
+
Rainbow('http://127.0.0.1:8080/').blue.underline +
|
37
|
+
"\nCommand double click the URL to open, Control C to quit\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
*{margin:0;padding:0}html,body{height:100%}body{background-color:#fff;font-family:GillSansRegular,'Gill Sans MT','Gill Sans','Century Gothic',Calibri,'Trebuchet MS',sans-serif;line-height:1.618;color:#333;text-align:center;font-weight:300}h1,h2,h3,h4,h5,h6{color:#333;letter-spacing:.1em}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color:#414f7c;text-decoration:none}h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color:#212721}h1{font-weight:300;font-size:2.618em;margin:1.809em 0 .809em}h2{font-weight:300;font-size:1.618em;margin:1.809em 0 .809em}h3,h4,h5,h6{font-weight:400;font-size:1em;margin:1.809em 0 .809em}p{margin:1em 0}a{color:#414f7c}a:hover{color:#212721}section{margin-bottom:1.618em}section>section{margin-bottom:3.236em}body>header{width:43em;text-align:center;margin:0 auto 0}body>header a{text-decoration:none;margin-left:.5em;margin-right:.5em}body>header,body>main,body>footer{display:block}body>header a{color:#414f7c}body>header a:hover{color:#212721}body>header a.extra{color:#414f7c;margin-left:1em}body>header a.extra:hover{color:#212721}body>header nav ul li{display:inline;list-style:none}.button{width:30px;height:30px;display:inline-block;background-size:100%;text-indent:-999em;text-align:left;margin:20px}.twitter{background:url('/assets/webicon-twitter-m.png');background-image:url('/assets/webicon-twitter.svg'),none}.rss{background:url('/assets/webicon-rss-m.png');background-image:url('/assets/webicon-rss.svg'),none}main{text-align:left;width:43em;margin:3em auto 2em}main li{margin-left:2.618em}.meta{color:#667}footer{width:43em;color:#667;border-top:4px solid #ddd;margin:3em auto 2em;overflow:hidden}footer .contact{float:left;margin-right:3em}footer .contact a,.tags a{color:#414f7c;text-decoration:none}footer .contact a:hover,.tags a:hover{color:#212721;text-decoration:none}.tags ul li{list-style:none;display:inline;font-variant:small-caps;font-size:1.2em}.archive a{text-decoration:none}ul.archive,ul.archive ul{margin-left:0}ul.archive li,ul.archive ul li{list-style:none;margin-left:0}.post pre{border:1px solid #ddd;background-color:#fff;padding:0 .4em}p.date{color:#667}pre{background-color:#eee;padding:1em;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}code{font-family:Consolas,Menlo,Monaco,'Lucida Console','Courier New',monospace,serif;font-size:.8em}blockquote{margin:2em 2em 2em 1em;padding:0 .75em 0 1.25em;border-left:2px solid #ddd;border-right:0 solid #ddd}@media all and (max-width:736px){body>header,main,footer{width:86%;margin:0 auto 0;padding:12px 24px 12px}p{margin-bottom:2em}.button{width:50px;height:50px;margin:20px}}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<footer>
|
2
|
+
<div class="contact">
|
3
|
+
<p>Twitter: <a href="https://twitter.com/<%= page[:twitter_name] %>">@<%= page[:twitter_name] %></a></p>
|
4
|
+
</div>
|
5
|
+
<div class="contact">
|
6
|
+
<p>© 2017<span> <%= page[:site_author] %></span>
|
7
|
+
</p>
|
8
|
+
</div>
|
9
|
+
</footer>
|
10
|
+
</body>
|
11
|
+
</html>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width,user-scalable=yes">
|
6
|
+
<meta name="description" content="<%= page[:site_description] %>">
|
7
|
+
<title><%= page[:site_name] %> | <%= page[:title] %></title>
|
8
|
+
<link rel="home" href="<%= page[:site_URL] %>/rss.xml" type="application/rss+xml" title="<%= page[:site_name] %>">
|
9
|
+
<link rel="stylesheet" href="/screen.css">
|
10
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<header>
|
14
|
+
<div class="logo"></div>
|
15
|
+
<h1><a href="<%= page[:site_URL] %>"><%= page[:site_name] %></a></h1>
|
16
|
+
<nav>
|
17
|
+
<ul>
|
18
|
+
<li><a href="<%= page[:site_URL] %>">Home</a></li>
|
19
|
+
<li><a href="/articles.html">Articles</a></li>
|
20
|
+
<li><a href="/about.html">About</a></li>
|
21
|
+
</ul><a href="https://twitter.com/<%= page[:twitter_name] %>" class="button twitter">Argue with me on Twitter</a><a href="<%= page[:site_URL] %>/rss.xml" rel="home" type="application/rss+xml" class="button rss">RSS Feed</a>
|
22
|
+
</nav>
|
23
|
+
</header>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<%= header_partial %>
|
2
|
+
<main>
|
3
|
+
<section>
|
4
|
+
<% for @post in data %>
|
5
|
+
<h2><%= @post[:page][:title] %></h2>
|
6
|
+
<p class="date"><%= @post[:page][:date_us] %></p>
|
7
|
+
<%= @post[:html] %>
|
8
|
+
<div class="tags">Tags:
|
9
|
+
<ul>
|
10
|
+
<% for @tag in @post[:page][:tags] %>
|
11
|
+
<li><a href="/tags/<%= @tag[:tag_slug] %>.html"><%= @tag[:name] %></a></li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
</div>
|
15
|
+
<% end %>
|
16
|
+
</section>
|
17
|
+
</main>
|
18
|
+
<%= footer_partial %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<%= header_partial %>
|
2
|
+
<main>
|
3
|
+
<section>
|
4
|
+
<h2><%= page[:title] %></h2>
|
5
|
+
<p class="date"><%= page[:date_us] %></p>
|
6
|
+
<%= html %>
|
7
|
+
<div class="tags">Tags:
|
8
|
+
<ul>
|
9
|
+
<% for @tag in page[:tags] %>
|
10
|
+
<li><a href="/tags/<%= @tag[:tag_slug] %>.html"><%= @tag[:name] %></a></li>
|
11
|
+
<% end %>
|
12
|
+
</ul>
|
13
|
+
</div>
|
14
|
+
</section>
|
15
|
+
</main>
|
16
|
+
<%= footer_partial %>
|