Thimblr 0.6.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'thimblr'
4
+ begin
5
+ Thimblr::Application.run!(*ARGV)
6
+ rescue RuntimeError # TODO: Only for port occupied
7
+ $stderr.puts "The server failed to initialize, are you trying to run two instances at once?"
8
+ end
@@ -0,0 +1,204 @@
1
+ ---
2
+ Title: Demo
3
+ Description: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat."
4
+ AskLabel: "Ask me anything"
5
+ SubmissionsEnabled: true
6
+ TwitterUsername: jphastings
7
+ Pages:
8
+ - Label: About Me
9
+ URL: about_me/
10
+ Following:
11
+ - Name: staff
12
+ Title: Tumblr Staff
13
+ URL: http://staff.tumblr.com/
14
+ PortraitURL-16: "http://30.media.tumblr.com/avatar_013241641371_16.png"
15
+ PortraitURL-24: "http://30.media.tumblr.com/avatar_013241641371_24.png"
16
+ PortraitURL-30: "http://30.media.tumblr.com/avatar_013241641371_30.png"
17
+ PortraitURL-40: "http://30.media.tumblr.com/avatar_013241641371_40.png"
18
+ PortraitURL-48: "http://30.media.tumblr.com/avatar_013241641371_48.png"
19
+ PortraitURL-64: "http://30.media.tumblr.com/avatar_013241641371_64.png"
20
+ PortraitURL-96: "http://30.media.tumblr.com/avatar_013241641371_96.png"
21
+ PortraitURL-128: "http://30.media.tumblr.com/avatar_013241641371_128.png"
22
+ - Name: jacob
23
+ Title: Jacob Bijani
24
+ URL: http://jacobbijani.com
25
+ PortraitURL-16: "http://30.media.tumblr.com/avatar_5b13c55f0688_16.png"
26
+ PortraitURL-24: "http://30.media.tumblr.com/avatar_5b13c55f0688_24.png"
27
+ PortraitURL-30: "http://30.media.tumblr.com/avatar_5b13c55f0688_30.png"
28
+ PortraitURL-40: "http://30.media.tumblr.com/avatar_5b13c55f0688_40.png"
29
+ PortraitURL-48: "http://30.media.tumblr.com/avatar_5b13c55f0688_48.png"
30
+ PortraitURL-64: "http://30.media.tumblr.com/avatar_5b13c55f0688_64.png"
31
+ PortraitURL-96: "http://30.media.tumblr.com/avatar_5b13c55f0688_96.png"
32
+ PortraitURL-128: "http://30.media.tumblr.com/avatar_5b13c55f0688_128.png"
33
+ - Name: petervidani
34
+ Title: Peter Vidani
35
+ URL: http://blog.petervidani.com/
36
+ PortraitURL-16: "http://25.media.tumblr.com/avatar_51777f3c873f_16.png"
37
+ PortraitURL-24: "http://25.media.tumblr.com/avatar_51777f3c873f_24.png"
38
+ PortraitURL-30: "http://25.media.tumblr.com/avatar_51777f3c873f_30.png"
39
+ PortraitURL-40: "http://25.media.tumblr.com/avatar_51777f3c873f_40.png"
40
+ PortraitURL-48: "http://25.media.tumblr.com/avatar_51777f3c873f_48.png"
41
+ PortraitURL-64: "http://25.media.tumblr.com/avatar_51777f3c873f_64.png"
42
+ PortraitURL-96: "http://25.media.tumblr.com/avatar_51777f3c873f_96.png"
43
+ PortraitURL-128: "http://25.media.tumblr.com/avatar_51777f3c873f_128.png"
44
+ - Name: cubicle17
45
+ Title: cubicle17 | a tumblelog by Bill Israel
46
+ URL: http://cubicle17.com
47
+ PortraitURL-16: "http://28.media.tumblr.com/avatar_7222a6c76360_16.png"
48
+ PortraitURL-24: "http://28.media.tumblr.com/avatar_7222a6c76360_24.png"
49
+ PortraitURL-30: "http://28.media.tumblr.com/avatar_7222a6c76360_30.png"
50
+ PortraitURL-40: "http://28.media.tumblr.com/avatar_7222a6c76360_40.png"
51
+ PortraitURL-48: "http://28.media.tumblr.com/avatar_7222a6c76360_48.png"
52
+ PortraitURL-64: "http://28.media.tumblr.com/avatar_7222a6c76360_64.png"
53
+ PortraitURL-96: "http://28.media.tumblr.com/avatar_7222a6c76360_96.png"
54
+ PortraitURL-128: "http://28.media.tumblr.com/avatar_7222a6c76360_128.png"
55
+ - Name: inky
56
+ Title: inky
57
+ URL: http://found.boxofjunk.ws/
58
+ PortraitURL-16: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_16.png"
59
+ PortraitURL-24: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_24.png"
60
+ PortraitURL-30: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_30.png"
61
+ PortraitURL-40: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_40.png"
62
+ PortraitURL-48: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_48.png"
63
+ PortraitURL-64: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_64.png"
64
+ PortraitURL-96: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_96.png"
65
+ PortraitURL-128: "http://27.media.tumblr.com/avatar_d3b0adb5c0fb_128.png"
66
+ - Name: matthewb
67
+ Title: Matthew Buchanan
68
+ URL: http://matthewbuchanan.name/
69
+ PortraitURL-16: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_16.png"
70
+ PortraitURL-24: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_24.png"
71
+ PortraitURL-30: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_30.png"
72
+ PortraitURL-40: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_40.png"
73
+ PortraitURL-48: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_38.png"
74
+ PortraitURL-64: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_64.png"
75
+ PortraitURL-96: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_96.png"
76
+ PortraitURL-128: "http://26.media.tumblr.com/avatar_3ae8a6cc1f8e_128.png"
77
+ Followed:
78
+ - Name: staff
79
+ Title: Tumblr Staff
80
+ URL: http://staff.tumblr.com/
81
+ PortraitURL-16: "http://30.media.tumblr.com/avatar_013241641371_16.png"
82
+ PortraitURL-24: "http://30.media.tumblr.com/avatar_013241641371_24.png"
83
+ PortraitURL-30: "http://30.media.tumblr.com/avatar_013241641371_30.png"
84
+ PortraitURL-40: "http://30.media.tumblr.com/avatar_013241641371_40.png"
85
+ PortraitURL-48: "http://30.media.tumblr.com/avatar_013241641371_48.png"
86
+ PortraitURL-64: "http://30.media.tumblr.com/avatar_013241641371_64.png"
87
+ PortraitURL-96: "http://30.media.tumblr.com/avatar_013241641371_96.png"
88
+ PortraitURL-128: "http://30.media.tumblr.com/avatar_013241641371_128.png"
89
+ Posts:
90
+ - Type: Quote
91
+ Permalink: http://demo.tumblr.com/post/236/it-does-not-matter-how-slow-you-go-so-long-as-you
92
+ PostId: 236
93
+ NoteCount: 3688
94
+ Timestamp: 1163014020
95
+ Tags:
96
+ - wisdom
97
+ Quote: It does not matter how slow you go so long as you do not stop.
98
+ Source: Wisdom of <a href="http://en.wikipedia.org/wiki/Confucius">Confucius</a>
99
+ - Type: Photo
100
+ Permalink: http://demo.tumblr.com/post/459265350/passing-through-times-square-by-mareen-fischinger
101
+ PostId: 459265350
102
+ NoteCount: 49
103
+ Tags:
104
+ - Mareen Fischinger
105
+ - New York City
106
+ - Times Square
107
+ Timestamp: 1163013960
108
+ PhotoURL-500: "http://29.media.tumblr.com/tumblr_kzjlfiTnfe1qz4rgho1_500.jpg"
109
+ PhotoURL-400: "http://29.media.tumblr.com/tumblr_kzjlfiTnfe1qz4rgho1_400.jpg"
110
+ PhotoURL-250: "http://29.media.tumblr.com/tumblr_kzjlfiTnfe1qz4rgho1_250.jpg"
111
+ PhotoURL-100: "http://29.media.tumblr.com/tumblr_kzjlfiTnfe1qz4rgho1_100.jpg"
112
+ PhotoURL-75sq: "http://29.media.tumblr.com/tumblr_kzjlfiTnfe1qz4rgho1_75sq.jpg"
113
+ Caption: <p>Passing through Times Square by <a href="http://www.mareenfischinger.com/">Mareen Fischinger</a></p>
114
+ LinkURL: http://demo.tumblr.com/photo/1280/459265350/1/tumblr_kzjlfiTnfe1qz4rgh
115
+ GroupPostMemberName: jphastings
116
+ - Type: Link
117
+ Permalink: http://demo.tumblr.com/post/234/my-favorite-web-sites
118
+ PostId: 234
119
+ NoteCount: 511
120
+ Timestamp: 1163013900
121
+ URL: http://
122
+ Name: My Favorite Website
123
+ Description: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat."
124
+ GroupPostMemberName: jphastings
125
+ - Type: Chat
126
+ Permalink: http://demo.tumblr.com/post/233/jack-hey-you-know-what-sucks-lindsey
127
+ PostId: 233
128
+ NoteCount: 1055
129
+ Timestamp: 1163013840
130
+ Lines:
131
+ - Jack: "Hey, you know what sucks?"
132
+ - Lindsey: "vaccuums"
133
+ - Jack: "Hey, you know what sucks in a metaphorical sense?"
134
+ - Lindsey: "black holes"
135
+ - Jack: "Hey, you know what just isn't cool?"
136
+ - Lindsey: "lava?"
137
+ GroupPostMemberName: jphastings
138
+ - Type: Audio
139
+ Timestamp: 1162927380
140
+ Permalink: http://demo.tumblr.com/post/459260683/allison-weiss-fingers-crossed
141
+ PostId: 459260683
142
+ NoteCount: 85
143
+ Reblog:
144
+ ReblogParentName: allisonweiss
145
+ ReblogParentTitle: A DAY IN THE LIFE OF ALLISON WEISS
146
+ ReblogParentURL: http://allisonweiss.tumblr.com/
147
+ # Root:
148
+ # ReblogRootName: staff
149
+ # ReblogRootTitle: Tumblr Staff
150
+ # ReblogRootURL: http://staff.tumblr.com/
151
+ Caption: '<p><strong><a href="#" title="http://allisonweiss.tumblr.com/">Allison Weiss</a> —</strong> Fingers Crossed</p>'
152
+ PlayCount: 18307
153
+ AlbumArtURL: http://16.media.tumblr.com/tumblr_ksc4i2SkVU1qz8ouqo1_r2_cover.jpg
154
+ Artist: Allison Weiss
155
+ TrackName: Fingers Crossed
156
+ AudioFile: "http://www.tumblr.com/audio_file/459260683/tumblr_ksc4i2SkVU1qz8ouq"
157
+ GroupPostMemberName: jphastings
158
+ - Type: Text
159
+ Permalink: http://demo.tumblr.com/post/232/an-example-post
160
+ PostId: 232
161
+ Timestamp: 1162927320
162
+ NoteCount: 674
163
+ Title: An example post
164
+ Body: >
165
+ <p>Lorem ipsum dolor sit amet, consectetuer <a href="http:///">adipiscing elit</a>. Aliquam nisi lorem, pulvinar id, commodo feugiat, vehicula et, mauris. Aliquam mattis porta urna. Maecenas dui neque, rhoncus sed, vehicula vitae, auctor at, nisi. Aenean id massa ut lacus molestie porta. Curabitur sit amet quam id libero suscipit venenatis.</p>
166
+ <ul>
167
+ <li>Lorem ipsum dolor sit amet.</li>
168
+ <li>Consectetuer adipiscing elit. </li>
169
+ <li>Nam at tortor quis ipsum tempor aliquet.</li>
170
+ </ul>
171
+ <p>Cum sociis <a href="http:///">natoque penatibus</a> et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse sed ligula. Sed volutpat odio non turpis gravida luctus. Praesent elit pede, iaculis facilisis, vehicula mattis, tempus non, arcu.</p>
172
+ <blockquote>Donec placerat mauris commodo dolor. Nulla tincidunt. Nulla vitae augue.</blockquote>
173
+ <p>Suspendisse ac pede. Cras <a href="http:///">tincidunt pretium</a> felis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque porttitor mi id felis. Maecenas nec augue. Praesent a quam pretium leo congue accumsan.</p>
174
+ GroupPostMemberName: jphastings
175
+ - Type: Text
176
+ Permalink: http://demo.tumblr.com/post/459009076/lorem-ipsum-dolor-sit-amet-consectetuer
177
+ PostId: 459009076
178
+ Timestamp: 1162927260
179
+ Body: >
180
+ <p>Lorem ipsum dolor sit amet, consectetuer <a href="http:///">adipiscing elit</a>. Aliquam nisi lorem, pulvinar id, commodo feugiat, vehicula et, mauris. Aliquam mattis porta urna. Maecenas dui neque, rhoncus sed, vehicula vitae, auctor at, nisi. Aenean id massa ut lacus molestie porta. Curabitur sit amet quam id libero suscipit venenatis.</p>
181
+ GroupPostMemberName: jphastings
182
+ CustomCSS: >
183
+ notag { left:0; z-index:-1; }
184
+ GroupMembers:
185
+ - Name: jphastings
186
+ Title: Musings
187
+ URL: http://blog.byJP.me
188
+ PortraitURL-16: "http://30.media.tumblr.com/avatar_013241641371_16.png"
189
+ PortraitURL-24: "http://30.media.tumblr.com/avatar_013241641371_24.png"
190
+ PortraitURL-30: "http://30.media.tumblr.com/avatar_013241641371_30.png"
191
+ PortraitURL-40: "http://30.media.tumblr.com/avatar_013241641371_40.png"
192
+ PortraitURL-48: "http://30.media.tumblr.com/avatar_013241641371_48.png"
193
+ PortraitURL-64: "http://30.media.tumblr.com/avatar_013241641371_64.png"
194
+ PortraitURL-96: "http://30.media.tumblr.com/avatar_013241641371_96.png"
195
+ PortraitURL-128: "http://30.media.tumblr.com/avatar_013241641371_128.png"
196
+ # Your own portrait urls
197
+ PortraitURL-16: "http://30.media.tumblr.com/avatar_013241641371_16.png"
198
+ PortraitURL-24: "http://30.media.tumblr.com/avatar_013241641371_24.png"
199
+ PortraitURL-30: "http://30.media.tumblr.com/avatar_013241641371_30.png"
200
+ PortraitURL-40: "http://30.media.tumblr.com/avatar_013241641371_40.png"
201
+ PortraitURL-48: "http://30.media.tumblr.com/avatar_013241641371_48.png"
202
+ PortraitURL-64: "http://30.media.tumblr.com/avatar_013241641371_64.png"
203
+ PortraitURL-96: "http://30.media.tumblr.com/avatar_013241641371_96.png"
204
+ PortraitURL-128: "http://30.media.tumblr.com/avatar_013241641371_128.png"
@@ -0,0 +1,18 @@
1
+ ## Settings affecting Tumblr
2
+ Tumblr:
3
+ PostsPerPage: 10
4
+
5
+ ## Settings affecting Thimblr
6
+ # The location of your theme files
7
+ ThemesLocation: themes
8
+ # The location of your data files
9
+ DataLocation: data
10
+
11
+ # This allows the thimblr server to open your chosen editor to edit
12
+ # the theme you're currently viewing. There is a (very small!) risk
13
+ # that this could do malicious things, so you can disable it if you
14
+ # like
15
+ AllowEditing: true
16
+ # The editor you want to use for editing your themes. Below should
17
+ # be what you'd use if you were launching it from the command line
18
+ Editor: mate
@@ -0,0 +1,18 @@
1
+ ## Settings affecting Tumblr
2
+ Tumblr:
3
+ PostsPerPage: 10
4
+
5
+ ## Settings affecting Thimblr
6
+ # The location of your theme files
7
+ ThemesLocation: themes
8
+ # The location of your data files
9
+ DataLocation: data
10
+
11
+ # This allows the thimblr server to open your chosen editor to edit
12
+ # the theme you're currently viewing. There is a (very small!) risk
13
+ # that this could do malicious things, so you can disable it if you
14
+ # like
15
+ AllowEditing: true
16
+ # The editor you want to use for editing your themes. Below should
17
+ # be what you'd use if you were launching it from the command line
18
+ Editor: mate
@@ -0,0 +1,181 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+ require 'json'
4
+ require 'digest/md5'
5
+ require 'pathname'
6
+ require 'launchy'
7
+ require 'thimblr/parser'
8
+ require 'rbconfig'
9
+
10
+ class Thimblr::Application < Sinatra::Application
11
+ Editors = {
12
+ 'textmate' => {'command' => "mate",'platform' => "mac",'name' => "TextMate"},
13
+ 'bbedit' => {'command' => "bbedit",'platform' => 'mac','name' => "BBEdit"},
14
+ 'textedit' => {'command' => "open -a TextEdit.app",'platform' => 'mac','name' => "TextEdit"}
15
+ }
16
+ Locations = [
17
+ {"dir" => "~/Library/Application Support/Thimblr/", 'name' => "Application Support", 'platform' => "mac"},
18
+ {'dir' => "~/.thimblr/",'name' => "Home directory", 'platform' => "nix"}
19
+ ]
20
+
21
+ case RbConfig::CONFIG['target_os']
22
+ when /darwin/i
23
+ Platform = "mac"
24
+ when /mswin32/i
25
+ Platform = "win"
26
+ else
27
+ Platform = "nix"
28
+ end
29
+
30
+ def self.parse_config(s)
31
+ set :themes, File.expand_path((File.directory? s['ThemesLocation']) ? s['ThemesLocation'] : "./themes")
32
+ set :data, File.expand_path((File.directory? s['DataLocation'] || "") ? s['DataLocation'] : "")
33
+ set :allowediting, (s['AllowEditing']) ? true : false
34
+ set :editor, s['Editor'] if s['Editor']
35
+ set :tumblr, Thimblr::Parser::Defaults.merge(s['Tumblr'] || {})
36
+ set :port, (s['Port'].to_i > 0) ? s['Port'].to_i : 4567
37
+ end
38
+
39
+ configure do |s|
40
+ set :root, File.join(File.dirname(__FILE__),"..")
41
+ Dir.chdir root
42
+ set :config, File.join(root,'config')
43
+
44
+ s.parse_config(YAML::load(open(File.join(config,'settings.yaml'))))
45
+ enable :sessions
46
+ set :bind, '127.0.0.1'
47
+ end
48
+
49
+ helpers do
50
+ def get_relative(path)
51
+ Pathname.new(path).relative_path_from(Pathname.new(File.expand_path(settings.root))).to_s
52
+ end
53
+ end
54
+
55
+ get '/' do
56
+ erb :index
57
+ end
58
+
59
+ get '/help' do
60
+ erb :help
61
+ end
62
+
63
+ get '/theme.set' do
64
+ if File.exists?(File.join(settings.themes,"#{params['theme']}.html"))
65
+ response.set_cookie('theme',params['theme'])
66
+ else
67
+ halt 404, "Not found"
68
+ end
69
+ end
70
+
71
+ get '/themes.json' do
72
+ themes = {}
73
+ Dir.glob("#{settings.themes}/*.html").collect do |theme|
74
+ themes[File.basename(theme,".html")] = Digest::MD5.hexdigest(open(theme).read)
75
+ end
76
+ themes.to_json
77
+ end
78
+
79
+ get '/data.set' do
80
+ if File.exists?(File.join(settings.data,"#{params['data']}.yml")) or params['data'] == 'demo'
81
+ response.set_cookie('data',params['data'])
82
+ else
83
+ halt 404, "Not found"
84
+ end
85
+ end
86
+
87
+ get '/data.json' do
88
+ data = {}
89
+ Dir.glob("#{settings.data}/*.yml").collect do |datum|
90
+ data[File.basename(datum,".yml")] = Digest::MD5.hexdigest(open(datum).read)
91
+ end
92
+ data['demo'] = Digest::MD5.hexdigest(open(File.join(settings.config,"demo.yml")).read)
93
+ data.to_json
94
+ end
95
+
96
+
97
+ get %r{^/edit/(theme|data)$} do |file|
98
+ halt 403, "Forbidden" if !settings.allowediting
99
+
100
+ case file
101
+ when 'theme'
102
+ filename = "#{settings.themes}/#{request.cookies['theme']}.html"
103
+ when 'data'
104
+ filename = File.exists?(File.join(settings.data,"#{request.cookies['data']}.yml")) ? "#{settings.data}/#{request.cookies['data']}.yml" : File.join(settings.config,"demo.yml")
105
+ else
106
+ halt 400, "Not a valid edit selection"
107
+ end
108
+ if File.exists? filename
109
+ # TODO: Send useful http status response
110
+ `#{settings.editor} "#{filename}"`
111
+ else
112
+ halt 404, "Odd, I can't find that file"
113
+ end
114
+ end
115
+
116
+ get %r{/(tumblr)?settings.set} do |tumblr|
117
+ halt 501 if tumblr == "tumblr" # TODO: Tumblr settings save
118
+
119
+ settings.parse_config(params)
120
+ open(File.join(settings.config,"settings.yaml"),"w") do |f|
121
+ f.write YAML.dump({
122
+ "Tumblr" => settings.tumblr,
123
+ "ThemesLocation" => get_relative(settings.themes),
124
+ "DataLocation" => get_relative(settings.data),
125
+ "AllowEditing" => settings.allowediting,
126
+ "Editor" => settings.editor,
127
+ "Port" => settings.port
128
+ })
129
+ end
130
+
131
+ "Settings saved"
132
+ end
133
+
134
+ # Downloads feed data from a tumblr site
135
+ get '/import' do
136
+ halt 501, "Sorry, I haven't written this bit yet!"
137
+ end
138
+
139
+ before do
140
+ request.cookies['data'] ||= "demo"
141
+ if request.env['REQUEST_PATH'] =~ /^\/thimblr/
142
+ if File.exists?(File.join(settings.themes,"#{request.cookies['theme']}.html"))
143
+ data = File.exists?(File.join(settings.data,"#{request.cookies['data']}.yml")) ? "#{settings.data}/#{request.cookies['data']}.yml" : File.join(settings.config,"demo.yml")
144
+ @parser = Thimblr::Parser.new(data,"#{settings.themes}/#{request.cookies['theme']}.html",settings.tumblr)
145
+ else
146
+ redirect '/help'
147
+ end
148
+ end
149
+ end
150
+
151
+ # The index page
152
+ get %r{^/thimblr(?:/page/(\d+))?/?$} do |pageno|
153
+ @parser.render_posts((pageno || 1).to_i)
154
+ end
155
+
156
+ # An individual post
157
+ get %r{^/thimblr/post/(\d+)/?.*$} do |postid|
158
+ @parser.render_permalink(postid)
159
+ end
160
+
161
+ get %r{^/thimblr/search/(.+)$} do |query|
162
+ @parser.render_search(query)
163
+ end
164
+
165
+ get %r{^/thimblr/tagged/(.+)$} do |tags|
166
+ halt 501, "Not Implemented"
167
+ end
168
+
169
+ # Protected page names that shouldn't go to pages and aren't implemented in Thimblr
170
+ get %r{^/thimblr/(?:rss|archive)$} do
171
+ halt 501, "Not Implemented"
172
+ end
173
+
174
+ # This is for pages
175
+ get '/thimblr/*' do
176
+ @parser.render_page(params[:splat])
177
+ end
178
+
179
+ # TODO: Only if Sinatra runs successfully
180
+ Launchy.open("http://localhost:#{port}")
181
+ end
@@ -0,0 +1,389 @@
1
+ # A parser for tumblr themes
2
+ #
3
+ #
4
+ # TODO
5
+ # ====
6
+ # * Add a logger so errors with the parse can be displayed
7
+ # * Likes
8
+ # * More blocks
9
+ # * Auto summary? Description tag stripping?
10
+
11
+ require 'yaml'
12
+ require 'cgi'
13
+ require 'time'
14
+
15
+ module Thimblr
16
+ class Parser
17
+ BackCompatibility = {"Type" => {
18
+ "Regular" => "Text",
19
+ "Conversation" => "Chat"
20
+ }}
21
+ Defaults = {
22
+ 'PostsPerPage' => 10
23
+ }
24
+
25
+ def initialize(data_file,theme_file = nil,settings = {})
26
+ template = YAML::load(open(data_file))
27
+ @settings = Defaults.merge settings
28
+ @apid = 0
29
+ @posts = ArrayIO.new(template['Posts'])
30
+ @groupmembers = template['GroupMembers']
31
+ @pages = template['Pages']
32
+ @following = template['Following']
33
+ @followed = template['Followed']
34
+ # Add all suitable @template options to @constants
35
+ @constants = template.delete_if { |key,val| ["Pages","Following","Posts","SubmissionsEnabled","Followed"].include? key }
36
+ @constants['RSS'] = '/thimblr/rss'
37
+ @constants['Favicon'] = '/favicon.ico'
38
+ @blocks = { # These are the defaults
39
+ 'Twitter' => !@constants['TwitterUsername'].empty?,
40
+ 'Description' => !@constants['Description'].empty?,
41
+ 'Pagination' => (@posts.length > @settings['PostsPerPage'].to_i),
42
+ 'SubmissionsEnabled' => template['SubmissionsEnabled'],
43
+ 'AskEnabled' => !@constants['AskLabel'].empty?,
44
+ 'HasPages' => @pages.length > 0,
45
+ 'Following' => @following.length > 0,
46
+ 'Followed' => @followed.length > 0,
47
+ 'More' => true
48
+ }
49
+
50
+ if theme_file and File.exists?(theme_file)
51
+ set_theme(open(theme_file).read)
52
+ end
53
+ end
54
+
55
+ def set_theme(theme_html)
56
+ @theme = theme_html
57
+ # Changes for Thimblr
58
+ @theme.gsub!(/href="\//,"href=\"/thimblr/")
59
+
60
+ # Get the meta constants
61
+ @theme.scan(/(<meta.*?name="(\w+):(.+?)".*?\/>)/).each do |meta|
62
+ value = (meta[0].scan(/content="(.+?)"/)[0] || [])[0]
63
+ if meta[1] == "if"
64
+ @blocks[meta[2].gsub(/(?:\ |^)\w/) {|s| s.strip.upcase}] = (value == 1)
65
+ else
66
+ @constants[meta[1..-1].join(":")] = value
67
+ @blocks[meta[2]+"Image"] = true if meta[1] == "image"
68
+ end
69
+ end
70
+
71
+ @constants['MetaDescription'] = CGI.escapeHTML(@constants['Description'])
72
+ end
73
+
74
+ # Renders a tumblr page from the stored template
75
+ def render_posts(page = 1)
76
+ blocks = @blocks
77
+ constants = @constants
78
+ constants['TotalPages'] = (@posts.length / @settings['PostsPerPage'].to_i).ceil
79
+ blocks['PreviousPage'] = page > 1
80
+ blocks['NextPage'] = page < constants['TotalPages']
81
+ blocks['Posts'] = true
82
+ blocks['IndexPage'] = true
83
+ constants['NextPage'] = page + 1
84
+ constants['CurrentPage'] = page
85
+ constants['PreviousPage'] = page - 1
86
+
87
+ # ffw thru posts array if required
88
+ @posts.seek((page - 1) * @settings['PostsPerPage'].to_i)
89
+ parse(@theme,blocks,constants)
90
+ end
91
+
92
+ # Renders an individual post
93
+ def render_permalink(postid)
94
+ postid = postid.to_i
95
+ blocks = @blocks
96
+ constants = @constants
97
+ @posts.delete_if do |post|
98
+ post['PostId'] != postid
99
+ end
100
+ raise "Post Not Found" if @posts.length != 1
101
+
102
+ blocks['Posts'] = true
103
+ blocks['PostTitle'] = true
104
+ blocks['PostSummary'] = true
105
+ blocks['PermalinkPage'] = true
106
+ blocks['PermalinkPagination'] = (@posts.length > 1)
107
+ blocks['PreviousPost'] = (postid < @posts.length)
108
+ blocks['NextPost'] = (postid > 0)
109
+ constants['PreviousPost'] = "/thimblr/post/#{postid - 1}"
110
+ constants['NextPost'] = "/thimblr/post/#{postid + 1}"
111
+
112
+ # Generate a post summary if a title isn't present
113
+ parse(@theme,blocks,constants)
114
+ end
115
+
116
+ # Renders the search page from the query
117
+ def render_search(query)
118
+ @searchresults = []
119
+ blocks = @blocks
120
+ constants = @constants
121
+ blocks['NoSearchResults'] = (@searchresults.length == 0)
122
+ blocks['SearchResults'] = !blocks['NoSearchResults'] # Is this a supported tag?
123
+ blocks['SearchPage'] = true
124
+ constants['SearchQuery'] = query
125
+ constants['URLSafeSearchQuery'] = CGI.escape(query)
126
+ constants['SearchResultCount'] = @searchresults.length
127
+
128
+ parse(@theme,blocks,constants)
129
+ end
130
+
131
+ # Renders a special page
132
+ def render_page(pageid)
133
+ blocks = @blocks
134
+ constants = @constants
135
+ blocks['Pages'] = true
136
+
137
+ parse(@theme,blocks,constants)
138
+ end
139
+
140
+ private
141
+ def parse(string,blocks = {},constants = {})
142
+ blocks = blocks.dup
143
+ constants = constants.dup
144
+ blocks.merge! constants['}blocks'] if !constants['}blocks'].nil?
145
+ string.gsub(/\{block:([\w:]+)\}(.*?)\{\/block:\1\}|\{([\w\-:]+)\}/m) do |match| # TODO:add not block to the second term
146
+ if $2 # block
147
+ blockname = $1
148
+ content = $2
149
+
150
+ # Back Compatibility
151
+ blockname = BackCompatibility['Type'][blockname] if !BackCompatibility['Type'][blockname].nil?
152
+
153
+ inv = false
154
+ case blockname
155
+ when /^IfNot(.*)$/
156
+ inv = true
157
+ blockname = $1
158
+ when /^If(.*)$/
159
+ blockname = $1
160
+ when 'Posts'
161
+ if @blocks['Posts']
162
+ lastday = nil
163
+ repeat = @settings['PostsPerPage'].times.collect do |n|
164
+ if not (post = @posts.advance).nil?
165
+ post['}blocks'] = {}
166
+ post['}blocks']['Date'] = true # Always render Date on Post pages
167
+ thisday = Time.at(post['Timestamp'])
168
+ post['}blocks']['NewDayDate'] = thisday.strftime("%Y-%m-%d") != lastday
169
+ post['}blocks']['SameDayDate'] = !post['}blocks']['NewDayDate']
170
+
171
+ lastday = thisday.strftime("%Y-%m-%d")
172
+ post['DayOfMonth'] = thisday.day
173
+ post['DayOfMonthWithZero'] = thisday.strftime("%d")
174
+ post['DayOfWeek'] = thisday.strftime("%A")
175
+ post['ShortDayOfWeek'] = thisday.strftime("%a")
176
+ post['DayOfWeekNumber'] = thisday.strftime("%w").to_i + 1
177
+ ordinals = ['st','nd','rd']
178
+ post['DayOfMonthSuffix'] = ([11,12].include? thisday.day) ? "th" : ordinals[thisday.day % 10 - 1]
179
+ post['DayOfYear'] = thisday.strftime("%j")
180
+ post['WeekOfYear'] = thisday.strftime("%W")
181
+ post['Month'] = thisday.strftime("%B")
182
+ post['ShortMonth'] = thisday.strftime("%b")
183
+ post['MonthNumber'] = thisday.month
184
+ post['MonthNumberWithZero'] = thisday.strftime("%w")
185
+ post['Year'] = thisday.strftime("%Y")
186
+ post['ShortYear'] = thisday.strftime("%y")
187
+ post['CapitalAmPm'] = thisday.strftime("%p")
188
+ post['AmPm'] = post['CapitalAmPm'].downcase
189
+ post['12Hour'] = thisday.strftime("%I").sub(/^0/,"")
190
+ post['24Hour'] = thisday.hour
191
+ post['12HourWithZero'] = thisday.strftime("%I")
192
+ post['24HourWithZero'] = thisday.strftime("%H")
193
+ post['Minutes'] = thisday.strftime("%M")
194
+ post['Seconds'] = thisday.strftime("%S")
195
+ post['Beats'] = (thisday.usec / 1000).round
196
+ post['TimeAgo'] = thisday.ago
197
+
198
+ post['Permalink'] = "http://127.0.0.1:4567/thimblr/post/#{post['PostId']}/" # TODO: Port number
199
+ post['ShortURL'] = post['Permalink'] # No need for a real short URL
200
+ post['TagsAsClasses'] = (constants['Tags'] || []).collect{ |tag| tag.gsub(/[^a-z]/i,"_").downcase }.join(" ")
201
+ post['}numberonpage'] = n + 1 # use a } at the begining so the theme can't access it
202
+
203
+ # Group Posts
204
+ if !post['GroupPostMember'].nil?
205
+ poster = nil
206
+ @groupmembers.each do |groupmember|
207
+ p groupmember
208
+ if groupmember['Name'] == post['GroupPostMemberName']
209
+ poster = Hash[*groupmember.to_a.collect {|key,value| ["PostAuthor#{key}",value] }.flatten]
210
+ break
211
+ end
212
+ end
213
+ p poster
214
+ if poster.nil?
215
+ # Add to log, GroupMemberPost not found in datafile
216
+ else
217
+ post.merge! poster
218
+ end
219
+ end
220
+
221
+ post['Title'] ||= "" # This prevents the site's title being used when it shouldn't be
222
+
223
+ case post['Type']
224
+ when 'Photo'
225
+ post['PhotoAlt'] = CGI.escapeHTML(post['Caption'])
226
+ if !post['LinkURL'].nil?
227
+ post['LinkOpenTag'] = "<a href=\"#{post['LinkURL']}\">"
228
+ post['LinkCloseTag'] = "</a>"
229
+ end
230
+ when 'Audio'
231
+ post['AudioPlayerBlack'] = audio_player(post['AudioFile'],"black")
232
+ post['AudioPlayerGrey'] = audio_player(post['AudioFile'],"grey")
233
+ post['AudioPlayerWhite'] = audio_player(post['AudioFile'],"white")
234
+ post['AudioPlayer'] = audio_player(post['AudioFile'])
235
+ post['}blocks']['ExternalAudio'] = !(post['AudioFile'] =~/^http:\/\/(?:www\.)?tumblr\.com/)
236
+ post['AudioFile'] = nil # We don't want this tag to be parsed if it happens to be in there
237
+ post['}blocks']['Artist'] = !post['Artist'].empty?
238
+ post['}blocks']['Album'] = !post['Album'].empty?
239
+ post['}blocks']['TrackName'] = !post['TrackName'].empty?
240
+ end
241
+
242
+ post
243
+ end
244
+ end.compact
245
+ end
246
+ # Post details
247
+ when 'Title'
248
+ blocks['Title'] = !constants['Title'].empty?
249
+ when /^Post(?:[1-9]|1[0-5])$/
250
+ blocks["Post#{$1}"] = true if constants['}numberonpage'] == $1
251
+ when 'Odd'
252
+ blocks["Post#{$1}"] = constants['}numberonpage'] % 2
253
+ when 'Even'
254
+ blocks["Post#{$1}"] = !(constants['}numberonpage'] % 2)
255
+ # Reblogs
256
+ when 'RebloggedFrom'
257
+ if !constants['Reblog'].nil?
258
+ blocks['RebloggedFrom'] = true
259
+ constants.merge! constants['Reblog']
260
+ constants.merge! constants['Root'] if !constants['Root'].nil?
261
+ end
262
+ # Photo Posts
263
+ when 'HighRes'
264
+ blocks['HighRes'] = !constants['HiRes'].empty?
265
+ when 'Caption'
266
+ blocks['Caption'] = !constants['Caption'].empty?
267
+ when 'SearchPage'
268
+ repeat = @searchresults if blocks['SearchPage']
269
+ # Quote Posts
270
+ when 'Source'
271
+ blocks['Source'] = !constants['Source'].empty?
272
+ when 'Description'
273
+ if !constants['Type'].nil?
274
+ blocks['Description'] = !constants['Description'].empty?
275
+ end
276
+ # Chat Posts
277
+ when 'Lines'
278
+ alt = {true => 'odd',false => 'even'}
279
+ iseven = false
280
+ repeat = constants['Lines'].collect do |line|
281
+ parts = line.to_a[0]
282
+ {"Line" => parts[1],"Label" => parts[0],"Alt" => alt[iseven = !iseven]}
283
+ end
284
+ constants['Lines'] = nil
285
+ blocks['Lines'] = true
286
+ when 'Label'
287
+ blocks['Label'] = !constants['Label'].empty?
288
+ # TODO: Notes
289
+ # Tags
290
+ when 'HasTags'
291
+ if constants['Tags'].length > 0
292
+ blocks['HasTags'] = true
293
+ end
294
+ when 'Tags'
295
+ repeat = constants['Tags'].collect do |tag|
296
+ {"Tag" => tag,"URLSafeTag" => tag.gsub(/[^a-zA-Z]/,"_").downcase,"TagURL" => "/thimblr/tagged/#{CGI.escape(tag)}","ChronoTagURL" => "/thimblr/tagged/#{CGI.escape(tag)}"} # TODO: ChronoTagURL
297
+ end
298
+ blocks['Tags'] = repeat.length > 0
299
+ constants['Tags'] = nil
300
+ # Groups
301
+ when 'GroupMembers'
302
+ if !constants['GroupMembers'].nil?
303
+ blocks['GroupMembers'] = true
304
+ end
305
+ when 'GroupMember'
306
+ repeat = constants['GroupMembers'].collect do |groupmember|
307
+ Hash[*groupmember.collect{ |key,value| ["GroupMember#{key}",value] }.flatten]
308
+ end
309
+ blocks['GroupMember'] = repeat.length > 0
310
+ constants['GroupMembers'] = nil
311
+ # TODO: Day Pages
312
+ # TODO: Tag Pages
313
+ end
314
+
315
+ # Process away!
316
+ (repeat || [constants]).collect do |consts|
317
+ if (blocks[blockname] ^ inv) or consts['Type'] == blockname
318
+ parse(content,blocks,(constants.merge consts))
319
+ end
320
+ end.join
321
+ else
322
+ constants[$3]
323
+ end
324
+ end
325
+ end
326
+
327
+ def audio_player(audiofile,colour = "") # Colour is one of 'black', 'white' or 'grey'
328
+ case colour
329
+ when "black"
330
+ colour = "_black"
331
+ when "grey"
332
+ colour = ""
333
+ audiofile += "&color=E4E4E4"
334
+ when "white"
335
+ colour = ""
336
+ audiofile += "&color=FFFFFF"
337
+ else
338
+ colour = ""
339
+ end
340
+ @apid += 1
341
+ return <<-END
342
+ <script type="text/javascript" language="javascript" src="http://assets.tumblr.com/javascript/tumblelog.js?16"></script><span id="audio_player_#{@apid}">[<a href="http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash" target="_blank">Flash 9</a> is required to listen to audio.]</span><script type="text/javascript">replaceIfFlash(9,"audio_player_#{@apid}",'<div class="audio_player"><embed type="application/x-shockwave-flash" src="/audio_player#{colour}.swf?audio_file=#{audiofile}" height="27" width="207" quality="best"></embed></div>')</script>
343
+ END
344
+
345
+ end
346
+ end
347
+
348
+ class ArrayIO < Array
349
+ # Returns the currently selected item and advances the pointer
350
+ def advance
351
+ @position = @position + 1 rescue 1
352
+ self[@position - 1]
353
+ end
354
+
355
+ # Returns the currently selected item and moves the pointer back one
356
+ def retreat
357
+ @position = @position - 1 rescue -1
358
+ self[@position + 1]
359
+ end
360
+
361
+ def seek(n)
362
+ self[@position = n]
363
+ end
364
+
365
+ def tell
366
+ @position
367
+ end
368
+ end
369
+
370
+ class Time < Time
371
+ def ago
372
+ "some time ago"
373
+ end
374
+ end
375
+ end
376
+
377
+ class NilClass
378
+ def empty?
379
+ true
380
+ end
381
+ end
382
+
383
+ =begin
384
+ t = Thimblr::Parser.new("demo")
385
+ t.set_theme(open("themes/101.html").read)
386
+
387
+
388
+ puts t.render_posts
389
+ =end