memorack 0.0.1

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.
@@ -0,0 +1,350 @@
1
+ # coding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rack'
5
+ require 'uri'
6
+
7
+ require 'memorack/tilt-mustache'
8
+ require 'memorack/mdmenu'
9
+
10
+ module MemoRack
11
+ class MemoApp
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
+ directory_watcher: false
23
+ }
24
+
25
+ # テンプレートエンジンのオプション
26
+ DEFAULT_TEMPLATE_OPTIONS = {
27
+ tables: true
28
+ }
29
+
30
+ # テンプレートで使用するローカル変数の初期値
31
+ DEFAULT_LOCALS = {
32
+ title: 'memo'
33
+ }
34
+
35
+ DEFAULT_OPTIONS = DEFAULT_APP_OPTIONS.merge(DEFAULT_TEMPLATE_OPTIONS).merge(DEFAULT_LOCALS)
36
+
37
+ def initialize(app, options={})
38
+ options = DEFAULT_OPTIONS.merge(to_sym_keys(options))
39
+
40
+ @themes_folders = [options[:themes_folder], File.expand_path('../themes/', __FILE__)]
41
+ read_config(options[:theme], options)
42
+
43
+ @app = app
44
+ @options = options
45
+
46
+ # DEFAULT_APP_OPTIONS に含まれるキーをすべてインスタンス変数に登録する
47
+ DEFAULT_APP_OPTIONS.each { |key, item|
48
+ instance_variable_set("@#{key}".to_sym, options[key])
49
+
50
+ # @options からテンプレートで使わないものを削除
51
+ @options.delete(key)
52
+ }
53
+
54
+ @locals = default_locals(@options)
55
+
56
+ use_engine(@markdown)
57
+ define_statics(@root, *@themes)
58
+
59
+ # ファイル監視を行う
60
+ watcher(@root) if @directory_watcher
61
+ end
62
+
63
+ def call(env)
64
+ content_type = 'text/html'
65
+
66
+ req = Rack::Request.new(env)
67
+ query = Rack::Utils.parse_query(req.query_string)
68
+ path_info = URI.unescape(req.path_info)
69
+ path, ext = split_extname(path_info)
70
+
71
+ case path_info
72
+ when '/'
73
+ content = render_with_mustache :index, :markdown
74
+ when /\.css$/
75
+ case @css
76
+ when 'scss', 'sass'
77
+ require 'sass'
78
+
79
+ result = pass(env, @statics)
80
+ return result unless result.first == 404
81
+
82
+ content_type = 'text/css'
83
+ cache_location = File.expand_path('sass-cache', @tmpdir)
84
+ content = render @css.to_sym, "#{path}.#{@css}", {views: @themes, cache_location: cache_location}
85
+ end
86
+ else
87
+ return pass(env) unless ext && Tilt.registered?(ext)
88
+
89
+ if query.has_key?('edit')
90
+ fullpath = File.expand_path(File.join(@root, path_info))
91
+
92
+ # @attention リダイレクトはうまく動作しない
93
+ #
94
+ # redirect_url = 'file://' + File.expand_path(File.join(@root, req.path_info))
95
+ # return redirect(redirect_url, 302) if File.exists?(fullpath)
96
+ end
97
+
98
+ content = render_with_mustache path.to_sym, ext
99
+ end
100
+
101
+ return pass(env) unless content
102
+
103
+ [200, {'Content-Type' => content_type}, [content.to_s]]
104
+ end
105
+
106
+
107
+ # リダイレクト
108
+ def redirect(url, code = 301)
109
+ # 301 = 恒久的, 302 = 一時的, 303, 410
110
+ [code, {'Content-Type' => 'text/html', 'Location' => url}, ['Redirect: ', url]]
111
+ end
112
+
113
+ # テンプレートエンジンを使用できるようにする
114
+ def use_engine(engine)
115
+ require engine if engine
116
+
117
+ # Tilt で Redcarpet 2.x を使うためのおまじない
118
+ Object.send(:remove_const, :RedcarpetCompat) if defined?(RedcarpetCompat) == 'constant'
119
+ end
120
+
121
+ # テーマのパスを取得する
122
+ def theme_path(theme)
123
+ return nil unless theme
124
+
125
+ @themes_folders.each { |folder|
126
+ path = theme && File.join(folder, theme)
127
+ return path if File.exists?(path) && FileTest::directory?(path)
128
+ }
129
+
130
+ nil
131
+ end
132
+
133
+ # デフォルトの locals を生成する
134
+ def default_locals(locals = {})
135
+ locals = locals.dup
136
+
137
+ locals[:page] ||= {}
138
+ locals[:page][:title] ||= locals[:title]
139
+
140
+ locals[:app] ||= {}
141
+ locals[:app][:name] ||= MemoRack::name
142
+ locals[:app][:version] ||= MemoRack::VERSION
143
+ locals[:app][:url] ||= MemoRack::HOMEPAGE
144
+
145
+ locals
146
+ end
147
+
148
+ # 設定ファイルを読込む
149
+ def read_config(theme, options = {})
150
+ @themes ||= []
151
+ @options_chain = []
152
+ @theme_chain = []
153
+
154
+ begin
155
+ require 'json'
156
+
157
+ while theme
158
+ dir = theme_path(theme)
159
+ break unless dir
160
+ break if @themes.member?(dir)
161
+
162
+ # テーマ・チェインに追加
163
+ @themes << File.join(dir, '')
164
+
165
+ # config の読込み
166
+ path = File.join(dir, 'config.json')
167
+ break unless File.readable?(path)
168
+
169
+ data = File.read(path)
170
+ @options_chain << to_sym_keys(JSON.parse(data))
171
+
172
+ theme = @options_chain.last[:theme]
173
+ end
174
+ rescue
175
+ end
176
+
177
+ # オプションをマージ
178
+ @options_chain.reverse.each { |opts| options.merge!(opts) }
179
+ options
180
+ end
181
+
182
+ # 静的ファイルの参照先を定義する
183
+ def define_statics(*args)
184
+ @statics = [] unless @statics
185
+
186
+ @statics |= args.collect { |root| Rack::File.new(root) }
187
+ end
188
+
189
+ # 次のアプリにパスする
190
+ def pass(env, apps = @statics + [@app])
191
+ apps.each { |app|
192
+ next unless app
193
+
194
+ result = app.call(env)
195
+ return result unless result.first == 404
196
+ }
197
+
198
+ [404, {'Content-Type' => 'text/plain'}, ['File not found: ', env['PATH_INFO']]]
199
+ end
200
+
201
+ # ファイル監視を行う
202
+ def watcher(path = '.')
203
+ require 'directory_watcher'
204
+
205
+ dw = DirectoryWatcher.new path, :pre_load => true
206
+ dw.interval = 1
207
+ dw.stable = 2
208
+ dw.glob = '**/*'
209
+ dw.add_observer { |*args|
210
+ t = Time.now.strftime("%Y-%m-%d %H:%M:%S")
211
+ puts "[#{t}] regeneration: #{args.size} files changed"
212
+
213
+ @menu = nil
214
+ }
215
+
216
+ dw.start
217
+ end
218
+
219
+ # テンプレートエンジンで render する
220
+ def render(engine, template, options = {}, locals = {})
221
+ options = {views: @root}.merge(options)
222
+
223
+ if options[:views].kind_of?(Array)
224
+ err = nil
225
+
226
+ options[:views].each { |views|
227
+ options[:views] = views
228
+
229
+ begin
230
+ return render(engine, template, options, locals)
231
+ rescue Errno::ENOENT => e
232
+ err = e
233
+ end
234
+ }
235
+
236
+ raise err
237
+ end
238
+
239
+ fname = template.kind_of?(String) ? template : "#{template}.#{engine}"
240
+ path = File.join(options[:views], fname)
241
+
242
+ engine = Tilt.new(File.join(File.dirname(path), ".#{engine}"), options) {
243
+ method = MemoApp.template_method(template)
244
+
245
+ if method && respond_to?(method)
246
+ data = send(method)
247
+ else
248
+ data = File.binread(path)
249
+ data.force_encoding('UTF-8')
250
+ end
251
+
252
+ data
253
+ }
254
+ engine.render(options, locals).force_encoding('UTF-8')
255
+ end
256
+
257
+ # レイアウトに mustache を適用してテンプレートエンジンでレンダリングする
258
+ def render_with_mustache(template, engine = :markdown, options = {}, locals = {})
259
+ begin
260
+ options = @options.merge(options)
261
+
262
+ @menu = nil unless @directory_watcher # ファイル監視していない場合はメニューを初期化
263
+
264
+ @menu ||= render :markdown, :menu, options
265
+ content = render engine, template, options
266
+ fname = template.to_s.force_encoding('UTF-8')
267
+
268
+ locals = @locals.merge(locals)
269
+
270
+ locals[:__menu__] = @menu
271
+ locals[:__content__] = content
272
+ locals[:page][:title] = [File.basename(fname), locals[:title]].join(' | ') unless template == :index
273
+
274
+ render :mustache, 'index.html', {views: @themes}, locals
275
+ rescue => e
276
+ e.to_s
277
+ end
278
+ end
279
+
280
+ # 拡張子を取出す
281
+ def split_extname(path)
282
+ return [$1, $2] if /^(.+)\.([^.]+)/ =~ path
283
+
284
+ [path]
285
+ end
286
+
287
+ # キーをシンボルに変換する
288
+ def to_sym_keys(hash)
289
+ hash.inject({}) { |memo, entry|
290
+ key, value = entry
291
+ memo[key.to_sym] = value
292
+ memo
293
+ }
294
+ end
295
+
296
+ # Tilt に登録されている拡張子を集める
297
+ def extnames(extname)
298
+ klass = Tilt[extname]
299
+ Tilt.mappings.select { |key, value| value.member?(klass) }.collect { |key, value| key }
300
+ end
301
+
302
+ # 対応フォーマットを取得する
303
+ def collect_formats
304
+ unless @collect_formats
305
+ @collect_formats = {}
306
+
307
+ @formats.each { |item|
308
+ if item.kind_of?(Array)
309
+ @collect_formats[item.first] = item
310
+ elsif item.kind_of?(Hash)
311
+ @collect_formats.merge!(item)
312
+ else
313
+ @collect_formats[item] = extnames(item)
314
+ end
315
+ }
316
+ end
317
+
318
+ @collect_formats
319
+ end
320
+
321
+ # テンプレート名
322
+ def self.template_method(name)
323
+ name.kind_of?(Symbol) && "template_#{name}".to_sym
324
+ end
325
+
326
+ # テンプレートを作成する
327
+ def self.template(name, &block)
328
+ define_method(self.template_method(name), &block)
329
+ end
330
+
331
+ #### テンプレート
332
+
333
+ # インデックスを作成
334
+ template :index do
335
+ begin
336
+ render :markdown, 'index.md', {views: @themes}
337
+ rescue
338
+ ''
339
+ end
340
+ end
341
+
342
+ # メニューを作成
343
+ template :menu do
344
+ mdmenu = MdMenu.new({prefix: '/', uri_escape: true, formats: collect_formats})
345
+ Dir.chdir(@root) { |path| mdmenu.collection('.') }
346
+ mdmenu.generate(StringIO.new).string
347
+ end
348
+
349
+ end
350
+ end
@@ -0,0 +1,13 @@
1
+ # coding: utf-8
2
+
3
+ require 'sinatra/base'
4
+ require 'memorack/tilt-mustache'
5
+
6
+
7
+ module Sinatra
8
+ module Templates
9
+ def mustache(template, options={}, locals={})
10
+ render :mustache, template, options, locals
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ tmp
2
+ .sass-cache
3
+ .ruby-version
4
+ *~
5
+
6
+ # old skool
7
+ .svn
8
+
9
+ # osx noise
10
+ .DS_Store
11
+ profile
12
+ Icon
@@ -0,0 +1 @@
1
+ export PATH="$HOME/.rbenv/shims:$PATH"
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gem 'memorack'
4
+
5
+ group :directory_watcher do
6
+ gem 'directory_watcher'
7
+ end
@@ -0,0 +1,5 @@
1
+ # coding: utf-8
2
+
3
+ require 'memorack'
4
+
5
+ run MemoRack::MemoApp.new(nil, theme: 'custom')
@@ -0,0 +1,3 @@
1
+ #### MemoRack について
2
+
3
+ このディレクトリに markdown 等のメモファイルを作成してください
@@ -0,0 +1,15 @@
1
+ {
2
+ // "directory_watcher": true, // ファイルの変更を監視するか決める
3
+ "theme": "oreilly", // テーマ(省略すると basic )
4
+ // "markdown": "kramdown", // 使用する markdownライブラリ(省略すると redcarpet )
5
+ // "formats": ["markdown"], // "markdown", "rdoc", "textile" or "wiki" (要 Tilt に対応した gem ライブラリ)
6
+
7
+ // redcarpet の拡張構文
8
+ // "tables": false, // テーブル
9
+ // "strikethrough": false, // 例: ~~good~~
10
+ // "fenced_code_blocks": false, // 例: ```ruby
11
+ // "superscript": false, // 例: 2^10
12
+ // "autolink": true, // 例: http://0.0.0.0/
13
+
14
+ "title": "覚書き" // タイトル
15
+ }
@@ -0,0 +1,3 @@
1
+ ### 覚書きについて
2
+
3
+ 私的な覚書き
@@ -0,0 +1,60 @@
1
+ /* 2カラム表示 */
2
+ #content-container {
3
+ width: 100%;
4
+ }
5
+
6
+ #menu {
7
+ width: 30%;
8
+ min-height: 100px;
9
+ float: left;
10
+ margin: 20px 0 20px 0;
11
+ padding: 0 10px 0 10px;
12
+ border: solid 1px #888;
13
+ border-radius: 10px;
14
+
15
+ ul {
16
+ padding-left: 20px;
17
+ }
18
+
19
+ a:link {
20
+ color: #369;
21
+ text-decoration: none;
22
+ }
23
+
24
+ a:visited {
25
+ color: #369;
26
+ }
27
+
28
+ a:hover {
29
+ color: #f00;
30
+ }
31
+
32
+ li {
33
+ padding-left: 4px;
34
+ margin-left: -4px;
35
+ }
36
+
37
+ li.selected {
38
+ background: #58b;
39
+ a {
40
+ color: #fff;
41
+ }
42
+ }
43
+ }
44
+
45
+ #content {
46
+ width: 65%;
47
+ float: right;
48
+ margin: 0;
49
+ position: relative;
50
+ }
51
+
52
+ .clear {
53
+ clear: both;
54
+ }
55
+
56
+ /* 文中の折返し */
57
+ pre {
58
+ white-space: pre-wrap;
59
+ word-break: break-all;
60
+ }
@@ -0,0 +1,82 @@
1
+ /* 基本 */
2
+ h1 {
3
+ font-size:1.8em;
4
+ }
5
+
6
+ h2 {
7
+ font-size:1.6em;
8
+ }
9
+
10
+ h3 {
11
+ font-size:1.2em;
12
+ }
13
+
14
+ h4, h5, h6 {
15
+ font-size:1em;
16
+ }
17
+
18
+
19
+ /* テーブル */
20
+ table {
21
+ width: 100%;
22
+ border-collapse: collapse;
23
+ margin: 10px 0px;
24
+ }
25
+
26
+ tr {
27
+ background: #FFF;
28
+
29
+ .altrow {
30
+ background: #F9F9F9;
31
+ }
32
+ }
33
+
34
+ th, caption {
35
+ text-align:left;
36
+ }
37
+
38
+ th, td {
39
+ padding: .5em;
40
+ line-height:1.5em;
41
+ border:1px solid #ddd;
42
+ text-align: left;
43
+ font-size: .9em
44
+ }
45
+
46
+ th {
47
+ color: #555;
48
+ background: #f2f2f2;
49
+ padding: .5em .5em;
50
+ font-weight:bold;
51
+ text-align: center;
52
+ }
53
+
54
+ td {
55
+ padding: .5em;
56
+ }
57
+
58
+ caption {
59
+ font-style: italic;
60
+ color: #777;
61
+ }
62
+
63
+
64
+ /* コード */
65
+ code {
66
+ font-family: monospace;
67
+ white-space: pre;
68
+ background: #F7F7F7;
69
+ border: 1px solid #D7D7D7;
70
+
71
+ display: inline;
72
+ padding: 0 4px 0;
73
+ margin: 0 2px 0;
74
+ }
75
+
76
+ pre code {
77
+ display: block;
78
+ padding: 4px 12px 4px;
79
+ white-space: pre-wrap;
80
+ word-break: break-all;
81
+ }
82
+
@@ -0,0 +1,14 @@
1
+ {
2
+ "directory_watcher": false, // ファイルの変更を監視するか決める
3
+ "css": "scss", // scss の自動変換を使用する
4
+ // "markdown": "kramdown", // 使用する markdownライブラリ
5
+ // "formats": ["markdown"], // "markdown", "rdoc", "textile" or "wiki" (要 Tilt に対応した gem ライブラリ)
6
+
7
+ // redcarpet の拡張構文
8
+ "strikethrough": true, // 例: ~~good~~
9
+ "fenced_code_blocks": true, // 例: ```ruby
10
+ "superscript": true, // 例: 2^10
11
+ // "autolink": true, // 例: http://0.0.0.0/
12
+
13
+ "title": "メモ"
14
+ }
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+
6
+ <style>
7
+ article, aside, dialog, figure, footer, header,
8
+ hgroup, menu, nav, section { display: block; }
9
+ </style>
10
+
11
+ <meta name="keywords" content="" />
12
+ <meta name="description" content="" />
13
+ <title>{{page.title}}</title>
14
+
15
+ <link type="text/css" href="/styles.css" rel="stylesheet" media="all" />
16
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
17
+ <script type="text/javascript">
18
+ $(function(){
19
+ // 表示しているメニューを選択
20
+ $('#menu a[href="' + document.location.pathname + '"]').parent().addClass('selected');
21
+
22
+ // 外部サイトのリンクに target='_blank' を追加
23
+ $('a[href^=http]').not('[href*=":' + location.hostname + '"]').attr('target', '_blank');
24
+ });
25
+ </script>
26
+
27
+ </head>
28
+ <body>
29
+ <div id="page">
30
+ <header>
31
+ <h1><a href="/">{{title}}</a></h1>
32
+ </header>
33
+
34
+ <div id="content-container">
35
+ <div id="menu" class="importdoc">{{{__menu__}}}</div>
36
+ <div id="content">{{{__content__}}}</div>
37
+ <div class="clear"></div>
38
+ </div>
39
+
40
+ <footer>
41
+ <p>Powered by <a href="{{app.url}}" target="_blank"> {{app.name}} {{app.version}}</a></p>
42
+ </footer>
43
+ </div>
44
+ </body>
45
+ </html>
@@ -0,0 +1,9 @@
1
+ @charset "utf-8";
2
+
3
+ @import "basic-styles";
4
+ @import "2-column";
5
+
6
+ #page {
7
+ width: 900px;
8
+ margin-left: 36px;
9
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "theme": "basic"
3
+ }
@@ -0,0 +1,33 @@
1
+ /* mixin */
2
+
3
+ @mixin box-shadow($args) {
4
+ -moz-box-shadow: $args;
5
+ -webkit-box-shadow: $args;
6
+ box-shadow: $args;
7
+ }
8
+
9
+
10
+ @mixin transform($args) {
11
+ -moz-transform: $args;
12
+ -webkit-transform: $args;
13
+ transform: $args;
14
+ }
15
+
16
+
17
+ /* basic style */
18
+
19
+ body {
20
+ color:#333;
21
+ font-size: 14px;
22
+ font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif;
23
+ line-height: 1.5;
24
+ -webkit-font-smoothing: antialiased;
25
+ }
26
+
27
+ h1, h2, h3, h4, h5, h6 {
28
+ font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif;
29
+ font-weight: bold;
30
+ letter-spacing: -0.018em;
31
+ page-break-after: avoid;
32
+ }
33
+