memorack 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: df0fc4ad433a14e26057d9581ce8cbfaea6935ff
4
- data.tar.gz: f14cfd511ca13b7aeaad9c02033b453e865e01b1
3
+ metadata.gz: 86ce4cc169545b6c715a427e966be9ad7a3832a2
4
+ data.tar.gz: 03e628e48a3764a8ec1dc1636ea6f03b60d51b1b
5
5
  SHA512:
6
- metadata.gz: d0c623c98371ea471b8d0a3cf1f59536adf2a8658454560f1018b33b5fb9e583ebc9909e67ada318b8570e5ec3f9b16ea7f14578e11d3f9a81ebafa6e13892fc
7
- data.tar.gz: 73b25aac401e3243ea35267e0afdce120c984b923386a899605dcf8c60ad119efc0e0df0b56578449e44e3c1d6cdb11571a7d4d4fbd1828514c26018f94b12f7
6
+ metadata.gz: 0c3d8523641d17d57fee096e9c715330b64f3a85b8f0952606e9e9f6d302d5a16aa1b1eb0908fc4b566b225bf6e632c50ea1e837ac0f737a2033fb21dcafbc0d
7
+ data.tar.gz: eff59e8755ff34c51c5ad47ab2b92525a95b81628cbb0d1ef2c5c6edeb79ab83eca89313bb764c53b5c19ac7a0d1e62cbb74bdd4471d9b6b8f7e78fc18b9b3b9
data/.gitignore CHANGED
@@ -1,4 +1,4 @@
1
- /VERSION
1
+ /REVISION
2
2
  *.gem
3
3
  *.rbc
4
4
  .bundle
data/HISTORY.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## History
2
2
 
3
+ ### v0.1.0 / 2013-04-15
4
+
5
+ * generate HTMLs for static site
6
+ * add config.json options(requires, public, site.url)
7
+ * change basic theme directory structure
8
+
3
9
  ### v0.0.4 / 2013-04-12
4
10
 
5
11
  * fix link in content
data/README.md CHANGED
@@ -23,6 +23,7 @@ Or install it yourself as:
23
23
  $ memorack theme THEME # Show theme info
24
24
  $ memorack theme -c THEME # Copy theme
25
25
  $ memorack server PATH # Instant Server
26
+ $ memorack build [PATH] # Build static site
26
27
 
27
28
  Standard startup
28
29
 
@@ -49,6 +50,14 @@ OS X (Pow + powder)
49
50
  * [Pow: Zero-configuration Rack server for Mac OS X](http://pow.cx)
50
51
  * `gem install powder`
51
52
 
53
+ Build static site
54
+
55
+ $ memorack create memo
56
+ $ cd memo
57
+ (Customizing...)
58
+ $ memorack build --url http://foo.bar.baz
59
+ Build 'content/' -> '_site'
60
+
52
61
  ## Directory
53
62
 
54
63
  Template
@@ -111,10 +120,25 @@ Add code to `index.html`
111
120
  <script src="/highlight.js/highlight.pack.js"></script>
112
121
  <script>hljs.initHighlightingOnLoad();</script>
113
122
 
123
+ ### org-mode
124
+
125
+ Install
126
+
127
+ $ gem install org-ruby
128
+
129
+ Edit `config.json`
130
+
131
+ {
132
+ "formats": ["markdown", "org"],
133
+ "requires": ["org-ruby"],
134
+ ...
135
+ }
136
+
114
137
  #### mustache variables
115
138
 
116
139
  Basic variables -- `{{VAR}}`
117
140
 
141
+ * `site.url`
118
142
  * `title`
119
143
  * `page.title`
120
144
  * `app.name`
@@ -131,9 +155,9 @@ Special variables -- `{{{VAR}}}`
131
155
 
132
156
  * Template comments translate english
133
157
  * Add customizing tips
134
- * Server test program
135
- * Generate HTMLs for static site
158
+ * More test program
136
159
  * Plugin
160
+ * Generate EPUB3
137
161
 
138
162
  ## Contributing
139
163
 
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- require File.expand_path('../etc/tools/update_version', __FILE__)
1
+ require File.expand_path('../etc/tools/update_revision', __FILE__)
2
2
  require "bundler/gem_tasks"
3
3
 
4
4
  # Spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,14 @@
1
+ def update_revision(path = 'REVISION')
2
+ rev = `git describe --dirty --long 2>/dev/null`.chomp
3
+ rev = `git describe --dirty --long --tags --always`.chomp if rev.empty?
4
+ rev.gsub!(/^v([0-9])/, '\1')
5
+ rev.gsub!(/(^|-)([a-z0-9]+(-dirty)?)$/, '/\2')
6
+ rev.gsub!(/^([0-9.]+)-0\//, '\1/')
7
+
8
+ return if File.exists?(path) && rev == open(path).read.chomp
9
+
10
+ open(path, 'w') { |f| f.puts rev }
11
+ end
12
+
13
+
14
+ update_revision
@@ -0,0 +1,171 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'set'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'rubygems'
7
+
8
+ require 'memorack/core'
9
+
10
+ module MemoRack
11
+ class Builder < Core
12
+
13
+ DEFAULT_BUILD_OPTIONS = {
14
+ output: '_site',
15
+ prefix: '/',
16
+ suffix: '.html',
17
+ uri_escape: true,
18
+ }
19
+
20
+ def generate(options = {}, &callback)
21
+ options = DEFAULT_BUILD_OPTIONS.merge(options)
22
+
23
+ url = @site[:url]
24
+ options[:prefix] = File.join(url, options[:prefix]) unless url.empty?
25
+
26
+ output = File.expand_path(options[:output])
27
+ dir_init(output)
28
+
29
+ @contents = contents(options)
30
+ @templates = Set.new @contents.files.collect { |file| file[:path] }
31
+
32
+ # トップページを作成する
33
+ content_write(:index, '.html', output) { |template|
34
+ render_with_mustache template, :markdown
35
+ }
36
+
37
+ suffix = options[:suffix]
38
+ suffix = '/index.html' if ['', '/'].member?(suffix)
39
+
40
+ # コンテンツのレンダリングを行う
41
+ @templates.each { |path|
42
+ callback.call(path) if callback
43
+
44
+ content_write(path, suffix, output) { |template|
45
+ render_content(template)
46
+ }
47
+ }
48
+
49
+ # テーマの公開ファイルをコピー
50
+ copy_public(@themes, output, &callback)
51
+
52
+ # 静的ファイルをコピーする
53
+ copy_statics(@root, output, &callback)
54
+ end
55
+
56
+ # ディレクトリを初期化する
57
+ def dir_init(dir)
58
+ if Dir.exists?(dir)
59
+ Dir.glob(File.join(dir, '*'), File::FNM_DOTMATCH) { |path|
60
+ next if /(^|\/)(\.|\.\.)$/ =~ path
61
+
62
+ FileUtils.remove_entry_secure(path)
63
+ }
64
+ else
65
+ FileUtils.mkpath(dir)
66
+ end
67
+ end
68
+
69
+ # テーマから公開用のファイルを収集する
70
+ def public_files
71
+ unless @public_files
72
+ @public_files = Set.new
73
+
74
+ @public.each { |path_info|
75
+ if path_info[-1] == '/'
76
+ @themes.each { |theme|
77
+ if Dir.exists?(File.join(theme, path_info))
78
+ Dir.chdir(theme) { |dir|
79
+ @public_files += Dir.glob(File.join(path_info, '**/*'))
80
+ }
81
+ end
82
+ }
83
+ else
84
+ @public_files << path_info
85
+ end
86
+ }
87
+ end
88
+
89
+ @public_files
90
+ end
91
+
92
+ # コンテンツをファイルに出力する
93
+ def content_write(template, suffix, output)
94
+ begin
95
+ content = yield(template)
96
+ return unless content
97
+
98
+ path = template
99
+ path = path.sub(/\.[^.]*$/, '') if path.kind_of?(String)
100
+
101
+ to = path.to_s + suffix
102
+ to = File.join(output, to)
103
+
104
+ FileUtils.mkpath(File.dirname(to))
105
+ File.write(to, content)
106
+ rescue
107
+ end
108
+ end
109
+
110
+ # ファイルをコピーする
111
+ def copy_file(path, output, &callback)
112
+ return if File.directory?(path)
113
+ return if @templates.include?(path)
114
+
115
+ callback.call(path) if callback
116
+
117
+ if output.kind_of?(Array)
118
+ to = File.join(*output)
119
+ else
120
+ to = File.join(output, path)
121
+ end
122
+
123
+ FileUtils.mkpath(File.dirname(to))
124
+ FileUtils.copy_entry(path, to)
125
+ end
126
+
127
+ # 静的ファイルをコピーする
128
+ def copy_statics(dir, output, &callback)
129
+ Dir.chdir(dir) { |dir|
130
+ Dir.glob('**/*') { |path|
131
+ copy_file(path, output, &callback)
132
+ }
133
+ }
134
+ end
135
+
136
+ # テーマの公開ファイルをコピーする
137
+ def copy_public(views, output, &callback)
138
+ public_files.each { |path_info|
139
+ callback.call(path_info) if callback
140
+
141
+ ext = split_extname(path_info)[1]
142
+
143
+ if css_exts.include?(ext)
144
+ content_write(path_info, '.css', output) { |template|
145
+ content = render_css(template)
146
+ }
147
+ else
148
+ fullpath = file_search(path_info, {views: views}, nil)
149
+ copy_file(fullpath, [output, path_info]) if fullpath
150
+ end
151
+ }
152
+ end
153
+
154
+ #### テンプレート
155
+
156
+ # インデックスを作成
157
+ template :index do
158
+ begin
159
+ render :markdown, 'index.md', {views: @themes}
160
+ rescue
161
+ ''
162
+ end
163
+ end
164
+
165
+ # メニューを作成
166
+ template :menu do
167
+ @contents.generate(StringIO.new).string
168
+ end
169
+
170
+ end
171
+ end
@@ -28,6 +28,7 @@ module MemoRack
28
28
  Usage: #{opts.program_name} create [options] PATH
29
29
  #{opts.program_name} theme [options] [THEME]
30
30
  #{opts.program_name} server [options] PATH
31
+ #{opts.program_name} build [options] [PATH]
31
32
  BANNER
32
33
 
33
34
  opts.separator ""
@@ -174,6 +175,38 @@ module MemoRack
174
175
  abort opts.help if argv.empty?
175
176
  }
176
177
 
178
+ # 静的サイトのビルド
179
+ define_options(:build, '[options] [PATH]') { |opts, argv, options|
180
+ default_options = {
181
+ output: '_site',
182
+ theme: 'custom',
183
+ url: '',
184
+
185
+ local: false,
186
+ prettify: false,
187
+ }
188
+
189
+ options.merge!(default_options)
190
+
191
+ opts.separator ""
192
+ opts.on("-o", "--output DIRECTORY", String,
193
+ t(:output, options)) { |arg| options[:output] = arg }
194
+ opts.on("-t", "--theme THEME", String,
195
+ t(:theme, options)) { |arg| options[:theme] = arg }
196
+ opts.on("--url URL", String,
197
+ t(:url, options)) { |arg| options[:url] = arg }
198
+ opts.on("--local",
199
+ t(:local)) { options[:local] = true }
200
+ opts.on("--prettify",
201
+ t(:prettify)) { options[:prettify] = true }
202
+ opts.on("-h", "--help", t(:help)) { abort opts.help }
203
+
204
+ opts.parse!(argv)
205
+
206
+ options[:url] = 'file://' + File.expand_path(options[:output]) if options[:local]
207
+ options[:suffix] = '' if options[:prettify]
208
+ }
209
+
177
210
 
178
211
  # サブコマンド
179
212
 
@@ -259,5 +292,25 @@ module MemoRack
259
292
  server_options[:app] = app
260
293
  Rack::Server.new(server_options).start
261
294
  end
295
+
296
+ # 静的サイトのビルド
297
+ def memorack_build(options, *argv)
298
+ path = argv.shift
299
+ path = 'content/' unless path
300
+ abort "Directory not exists '#{path}'" unless File.exists?(path)
301
+
302
+ require 'memorack/builder'
303
+ require 'tmpdir'
304
+
305
+ Dir.mktmpdir do |tmpdir|
306
+ site = {}
307
+ site[:url] = File.join(options[:url], '').gsub(/\/$/, '') if options[:url]
308
+
309
+ builder = MemoRack::Builder.new(theme: options[:theme], root: path, tmpdir: tmpdir, site: site)
310
+ builder.generate(options)
311
+ end
312
+
313
+ puts "Build '#{path}' -> '#{options[:output]}'"
314
+ end
262
315
  end
263
316
  end
@@ -0,0 +1,369 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'pathname'
4
+ require 'rubygems'
5
+
6
+ require 'memorack/tilt-mustache'
7
+ require 'memorack/locals'
8
+ require 'memorack/mdmenu'
9
+
10
+ module MemoRack
11
+ class Core
12
+ attr_reader :themes, :options_chain
13
+
14
+ DEFAULT_APP_OPTIONS = {
15
+ root: 'content/',
16
+ themes_folder: 'themes/',
17
+ tmpdir: 'tmp/',
18
+ theme: 'basic',
19
+ markdown: 'redcarpet',
20
+ formats: ['markdown'],
21
+ css: nil,
22
+ suffix: '',
23
+ public: [],
24
+ site: {},
25
+ requires: [],
26
+ directory_watcher: false
27
+ }
28
+
29
+ # テンプレートエンジンのオプション
30
+ DEFAULT_TEMPLATE_OPTIONS = {
31
+ tables: true
32
+ }
33
+
34
+ # テンプレートで使用するローカル変数の初期値
35
+ DEFAULT_LOCALS = {
36
+ title: 'memo'
37
+ }
38
+
39
+ DEFAULT_OPTIONS = DEFAULT_APP_OPTIONS.merge(DEFAULT_TEMPLATE_OPTIONS).merge(DEFAULT_LOCALS)
40
+
41
+ def initialize(options={})
42
+ options = DEFAULT_OPTIONS.merge(to_sym_keys(options))
43
+
44
+ @themes_folders = [options[:themes_folder], File.expand_path('../themes/', __FILE__)]
45
+ read_config(options[:theme], options)
46
+ read_config(DEFAULT_APP_OPTIONS[:theme], options) if @themes.empty?
47
+
48
+ @options = options
49
+
50
+ # DEFAULT_APP_OPTIONS に含まれるキーをすべてインスタンス変数に登録する
51
+ DEFAULT_APP_OPTIONS.each { |key, item|
52
+ instance_variable_set("@#{key}".to_sym, options[key])
53
+
54
+ # @options からテンプレートで使わないものを削除
55
+ @options.delete(key)
56
+ }
57
+
58
+ @requires.each { |lib| require lib }
59
+ @locals = default_locals(@options)
60
+
61
+ use_engine(@markdown)
62
+ end
63
+
64
+ # テーマのパスを取得する
65
+ def theme_path(theme)
66
+ return nil unless theme
67
+
68
+ @themes_folders.each { |folder|
69
+ path = theme && File.join(folder, theme)
70
+ return path if File.exists?(path) && FileTest::directory?(path)
71
+ }
72
+
73
+ nil
74
+ end
75
+
76
+ # デフォルトの locals を生成する
77
+ def default_locals(locals = {})
78
+ locals = Locals[locals]
79
+
80
+ locals[:site] ||= @site
81
+
82
+ locals[:app] ||= Locals[]
83
+ locals[:app][:name] ||= MemoRack::name
84
+ locals[:app][:version] ||= MemoRack::VERSION
85
+ locals[:app][:url] ||= MemoRack::HOMEPAGE
86
+
87
+ locals.define_key(:__menu__) { |hash, key|
88
+ @menu = nil unless @directory_watcher # ファイル監視していない場合はメニューを初期化
89
+ @menu ||= render :markdown, :menu, @options
90
+ }
91
+
92
+ locals
93
+ end
94
+
95
+ # 設定ファイルを読込む
96
+ def read_config(theme, options = {})
97
+ @themes ||= []
98
+ @options_chain = []
99
+ @theme_chain = []
100
+
101
+ begin
102
+ require 'json'
103
+
104
+ while theme
105
+ dir = theme_path(theme)
106
+ break unless dir
107
+ break if @themes.member?(dir)
108
+
109
+ # テーマ・チェインに追加
110
+ @themes << File.join(dir, '')
111
+
112
+ # config の読込み
113
+ path = File.join(dir, 'config.json')
114
+ break unless File.readable?(path)
115
+
116
+ data = File.read(path)
117
+ @options_chain << to_sym_keys(JSON.parse(data))
118
+
119
+ theme = @options_chain.last[:theme]
120
+ end
121
+ rescue
122
+ end
123
+
124
+ # オプションをマージ
125
+ @options_chain.reverse.each { |opts| options.merge!(opts) }
126
+ options
127
+ end
128
+
129
+ # テンプレートエンジンを使用できるようにする
130
+ def use_engine(engine)
131
+ require engine if engine
132
+
133
+ # Tilt で Redcarpet 2.x を使うためのおまじない
134
+ Object.send(:remove_const, :RedcarpetCompat) if defined?(RedcarpetCompat) == 'constant'
135
+ end
136
+
137
+ # css の拡張子リストを作成する
138
+ def css_exts
139
+ @css_exts ||= Set.new ['css', *@css]
140
+ end
141
+
142
+ # ファイルを探す
143
+ def file_search(template, options = {}, exts = enable_exts)
144
+ options = {views: @root}.merge(options)
145
+
146
+ if options[:views].kind_of?(Array)
147
+ err = nil
148
+
149
+ options[:views].each { |views|
150
+ options[:views] = views
151
+
152
+ begin
153
+ path = file_search(template, options, exts)
154
+ return path if path
155
+ rescue Errno::ENOENT => e
156
+ err = e
157
+ end
158
+ }
159
+
160
+ raise err if err
161
+ return nil
162
+ end
163
+
164
+ if exts
165
+ exts.each { |ext|
166
+ path = File.join(options[:views], "#{template}.#{ext}")
167
+ return path if File.exists?(path)
168
+ }
169
+ else
170
+ path = File.join(options[:views], template)
171
+ return path if File.exists?(path)
172
+ end
173
+
174
+ return nil
175
+ end
176
+
177
+ # テンプレートエンジンで render する
178
+ def render(engine, template, options = {}, locals = {})
179
+ options = {views: @root}.merge(options)
180
+
181
+ if template.kind_of?(Pathname)
182
+ path = template
183
+ elsif options[:views].kind_of?(Array)
184
+ err = nil
185
+
186
+ options[:views].each { |views|
187
+ options[:views] = views
188
+
189
+ begin
190
+ return render(engine, template, options, locals)
191
+ rescue Errno::ENOENT => e
192
+ err = e
193
+ end
194
+ }
195
+
196
+ raise err
197
+ else
198
+ fname = template.kind_of?(String) ? template : "#{template}.#{engine}"
199
+ path = File.join(options[:views], fname)
200
+ end
201
+
202
+ engine = Tilt.new(File.join(File.dirname(path), ".#{engine}"), options) {
203
+ method = MemoApp.template_method(template)
204
+
205
+ if method && respond_to?(method)
206
+ data = send(method)
207
+ else
208
+ data = File.binread(path)
209
+ data.force_encoding('UTF-8')
210
+ end
211
+
212
+ data
213
+ }
214
+ engine.render(options, locals).force_encoding('UTF-8')
215
+ end
216
+
217
+ # レイアウトに mustache を適用してテンプレートエンジンでレンダリングする
218
+ def render_with_mustache(template, engine = :markdown, options = {}, locals = {})
219
+ begin
220
+ mustache_templ = options[:mustache] || 'index.html'
221
+
222
+ options = @options.merge(options)
223
+ locals = @locals.merge(locals)
224
+
225
+ locals.define_key(:__content__) { |hash, key|
226
+ if engine
227
+ render engine, template, options
228
+ else
229
+ template
230
+ end
231
+ }
232
+
233
+ locals[:content] = true unless template == :index
234
+ locals[:page] = page = Locals[locals[:page] || {}]
235
+
236
+ page.define_key(:name) { |hash, key|
237
+ unless template == :index
238
+ fname = locals[:path_info]
239
+ fname ||= template.to_s.force_encoding('UTF-8')
240
+ File.basename(fname)
241
+ end
242
+ }
243
+
244
+ page.define_key(:title) { |hash, key|
245
+ page_title = home_title = locals[:title]
246
+ page_name = hash[:name]
247
+ page_title = "#{page_name} | #{home_title}" if page_name
248
+
249
+ page_title
250
+ }
251
+
252
+ render :mustache, mustache_templ, {views: @themes}, locals
253
+ rescue => e
254
+ e.to_s
255
+ end
256
+ end
257
+
258
+ # コンテンツをレンダリングする
259
+ def render_content(path_info, locals = {})
260
+ path, ext = split_extname(path_info)
261
+
262
+ if @suffix == ''
263
+ fullpath = file_search(path_info, @options)
264
+
265
+ if fullpath
266
+ path = path_info
267
+ ext = split_extname(fullpath)[1]
268
+ end
269
+ end
270
+
271
+ return nil unless ext && Tilt.registered?(ext)
272
+
273
+ template = fullpath ? Pathname.new(fullpath) : path.to_sym
274
+ content = render_with_mustache template, ext, {}, locals
275
+ end
276
+
277
+ # CSSをレンダリングする
278
+ def render_css(path_info, locals = {})
279
+ return unless @css
280
+
281
+ path, = split_extname(path_info)
282
+ options = {views: @themes}
283
+
284
+ fullpath = file_search(path, options, css_exts)
285
+ return nil unless fullpath
286
+
287
+ ext = split_extname(fullpath)[1]
288
+
289
+ case ext
290
+ when 'css'
291
+ return File.binread(fullpath)
292
+ when 'scss', 'sass'
293
+ options[:cache_location] = File.expand_path('sass-cache', @tmpdir)
294
+ end
295
+
296
+ render ext, Pathname.new(fullpath), options, locals
297
+ end
298
+
299
+ # 拡張子を取出す
300
+ def split_extname(path)
301
+ return [$1, $2] if /^(.+)\.([^.]+)/ =~ path
302
+
303
+ [path]
304
+ end
305
+
306
+ # キーをシンボルに変換する
307
+ def to_sym_keys(hash)
308
+ hash.inject({}) { |memo, entry|
309
+ key, value = entry
310
+ value = to_sym_keys(value) if value.kind_of?(Hash)
311
+
312
+ memo[key.to_sym] = value
313
+
314
+ memo
315
+ }
316
+ end
317
+
318
+ # Tilt に登録されている拡張子を集める
319
+ def extnames(extname)
320
+ klass = Tilt[extname]
321
+ Tilt.mappings.select { |key, value| value.member?(klass) }.collect { |key, value| key }
322
+ end
323
+
324
+ # 対応フォーマットを取得する
325
+ def collect_formats
326
+ unless @collect_formats
327
+ @collect_formats = {}
328
+
329
+ @formats.each { |item|
330
+ if item.kind_of?(Array)
331
+ @collect_formats[item.first] = item
332
+ elsif item.kind_of?(Hash)
333
+ @collect_formats.merge!(item)
334
+ else
335
+ @collect_formats[item] = extnames(item)
336
+ end
337
+ }
338
+ end
339
+
340
+ @collect_formats
341
+ end
342
+
343
+ # 対応している拡張子
344
+ def enable_exts
345
+ @enable_exts ||= collect_formats.values.flatten
346
+ end
347
+
348
+ # コンテンツファイルの収集する
349
+ def contents(options = {})
350
+ default_options = {prefix: '/', suffix: @suffix, uri_escape: true, formats: collect_formats}
351
+ options = default_options.merge(options)
352
+
353
+ mdmenu = MdMenu.new(options)
354
+ Dir.chdir(@root) { |path| mdmenu.collection('.') }
355
+ mdmenu
356
+ end
357
+
358
+ # テンプレート名
359
+ def self.template_method(name)
360
+ name.kind_of?(Symbol) && "template_#{name}".to_sym
361
+ end
362
+
363
+ # テンプレートを作成する
364
+ def self.template(name, &block)
365
+ define_method(self.template_method(name), &block)
366
+ end
367
+
368
+ end
369
+ end