zine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 %>
|