memorack 0.0.1

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