Thimblr 0.6.7
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/thimblr +8 -0
- data/config/demo.yml +204 -0
- data/config/settings.default.yaml +18 -0
- data/config/settings.yaml +18 -0
- data/lib/thimblr.rb +181 -0
- data/lib/thimblr/parser.rb +389 -0
- data/themes/101.html +431 -0
- data/themes/Redux.html +1002 -0
- data/themes/Stationary.html +221 -0
- data/views/help.erb +165 -0
- data/views/index.erb +49 -0
- metadata +83 -0
data/bin/thimblr
ADDED
@@ -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
|
data/config/demo.yml
ADDED
@@ -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
|
data/lib/thimblr.rb
ADDED
@@ -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
|