junebug 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +18 -0
- data/README +60 -0
- data/bin/junebug +15 -0
- data/deploy/config.yml +11 -0
- data/deploy/console +8 -0
- data/deploy/static/base.css +181 -0
- data/deploy/static/yui/fonts.css +34 -0
- data/deploy/static/yui/grids.css +88 -0
- data/deploy/static/yui/reset.css +14 -0
- data/deploy/wiki +13 -0
- data/fixtures/junebug_pages.yml +23 -0
- data/lib/acts_as_versioned.rb +509 -0
- data/lib/junebug/config.rb +11 -0
- data/lib/junebug/controllers.rb +145 -0
- data/lib/junebug/diff.rb +317 -0
- data/lib/junebug/generator.rb +15 -0
- data/lib/junebug/helpers.rb +18 -0
- data/lib/junebug/models.rb +62 -0
- data/lib/junebug/views.rb +289 -0
- data/lib/junebug.rb +50 -0
- metadata +120 -0
data/lib/junebug/diff.rb
ADDED
@@ -0,0 +1,317 @@
|
|
1
|
+
module HTMLDiff
|
2
|
+
|
3
|
+
Match = Struct.new(:start_in_old, :start_in_new, :size)
|
4
|
+
class Match
|
5
|
+
def end_in_old
|
6
|
+
self.start_in_old + self.size
|
7
|
+
end
|
8
|
+
|
9
|
+
def end_in_new
|
10
|
+
self.start_in_new + self.size
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Operation = Struct.new(:action, :start_in_old, :end_in_old, :start_in_new, :end_in_new)
|
15
|
+
|
16
|
+
class DiffBuilder
|
17
|
+
|
18
|
+
def initialize(old_version, new_version)
|
19
|
+
@old_version, @new_version = old_version, new_version
|
20
|
+
@content = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def build
|
24
|
+
split_inputs_to_words
|
25
|
+
index_new_words
|
26
|
+
operations.each { |op| perform_operation(op) }
|
27
|
+
return @content.join
|
28
|
+
end
|
29
|
+
|
30
|
+
def split_inputs_to_words
|
31
|
+
@old_words = convert_html_to_list_of_words(explode(@old_version))
|
32
|
+
@new_words = convert_html_to_list_of_words(explode(@new_version))
|
33
|
+
end
|
34
|
+
|
35
|
+
def index_new_words
|
36
|
+
@word_indices = Hash.new { |h, word| h[word] = [] }
|
37
|
+
@new_words.each_with_index { |word, i| @word_indices[word] << i }
|
38
|
+
end
|
39
|
+
|
40
|
+
def operations
|
41
|
+
position_in_old = position_in_new = 0
|
42
|
+
operations = []
|
43
|
+
|
44
|
+
matches = matching_blocks
|
45
|
+
# an empty match at the end forces the loop below to handle the unmatched tails
|
46
|
+
# I'm sure it can be done more gracefully, but not at 23:52
|
47
|
+
matches << Match.new(@old_words.length, @new_words.length, 0)
|
48
|
+
|
49
|
+
matches.each_with_index do |match, i|
|
50
|
+
match_starts_at_current_position_in_old = (position_in_old == match.start_in_old)
|
51
|
+
match_starts_at_current_position_in_new = (position_in_new == match.start_in_new)
|
52
|
+
|
53
|
+
action_upto_match_positions =
|
54
|
+
case [match_starts_at_current_position_in_old, match_starts_at_current_position_in_new]
|
55
|
+
when [false, false]
|
56
|
+
:replace
|
57
|
+
when [true, false]
|
58
|
+
:insert
|
59
|
+
when [false, true]
|
60
|
+
:delete
|
61
|
+
else
|
62
|
+
# this happens if the first few words are same in both versions
|
63
|
+
:none
|
64
|
+
end
|
65
|
+
|
66
|
+
if action_upto_match_positions != :none
|
67
|
+
operation_upto_match_positions =
|
68
|
+
Operation.new(action_upto_match_positions,
|
69
|
+
position_in_old, match.start_in_old,
|
70
|
+
position_in_new, match.start_in_new)
|
71
|
+
operations << operation_upto_match_positions
|
72
|
+
end
|
73
|
+
if match.size != 0
|
74
|
+
match_operation = Operation.new(:equal,
|
75
|
+
match.start_in_old, match.end_in_old,
|
76
|
+
match.start_in_new, match.end_in_new)
|
77
|
+
operations << match_operation
|
78
|
+
end
|
79
|
+
|
80
|
+
position_in_old = match.end_in_old
|
81
|
+
position_in_new = match.end_in_new
|
82
|
+
end
|
83
|
+
|
84
|
+
operations
|
85
|
+
end
|
86
|
+
|
87
|
+
def matching_blocks
|
88
|
+
matching_blocks = []
|
89
|
+
recursively_find_matching_blocks(0, @old_words.size, 0, @new_words.size, matching_blocks)
|
90
|
+
matching_blocks
|
91
|
+
end
|
92
|
+
|
93
|
+
def recursively_find_matching_blocks(start_in_old, end_in_old, start_in_new, end_in_new, matching_blocks)
|
94
|
+
match = find_match(start_in_old, end_in_old, start_in_new, end_in_new)
|
95
|
+
if match
|
96
|
+
if start_in_old < match.start_in_old and start_in_new < match.start_in_new
|
97
|
+
recursively_find_matching_blocks(
|
98
|
+
start_in_old, match.start_in_old, start_in_new, match.start_in_new, matching_blocks)
|
99
|
+
end
|
100
|
+
matching_blocks << match
|
101
|
+
if match.end_in_old < end_in_old and match.end_in_new < end_in_new
|
102
|
+
recursively_find_matching_blocks(
|
103
|
+
match.end_in_old, end_in_old, match.end_in_new, end_in_new, matching_blocks)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_match(start_in_old, end_in_old, start_in_new, end_in_new)
|
109
|
+
|
110
|
+
best_match_in_old = start_in_old
|
111
|
+
best_match_in_new = start_in_new
|
112
|
+
best_match_size = 0
|
113
|
+
|
114
|
+
match_length_at = Hash.new { |h, index| h[index] = 0 }
|
115
|
+
|
116
|
+
start_in_old.upto(end_in_old - 1) do |index_in_old|
|
117
|
+
|
118
|
+
new_match_length_at = Hash.new { |h, index| h[index] = 0 }
|
119
|
+
|
120
|
+
@word_indices[@old_words[index_in_old]].each do |index_in_new|
|
121
|
+
next if index_in_new < start_in_new
|
122
|
+
break if index_in_new >= end_in_new
|
123
|
+
|
124
|
+
new_match_length = match_length_at[index_in_new - 1] + 1
|
125
|
+
new_match_length_at[index_in_new] = new_match_length
|
126
|
+
|
127
|
+
if new_match_length > best_match_size
|
128
|
+
best_match_in_old = index_in_old - new_match_length + 1
|
129
|
+
best_match_in_new = index_in_new - new_match_length + 1
|
130
|
+
best_match_size = new_match_length
|
131
|
+
end
|
132
|
+
end
|
133
|
+
match_length_at = new_match_length_at
|
134
|
+
end
|
135
|
+
|
136
|
+
# best_match_in_old, best_match_in_new, best_match_size = add_matching_words_left(
|
137
|
+
# best_match_in_old, best_match_in_new, best_match_size, start_in_old, start_in_new)
|
138
|
+
# best_match_in_old, best_match_in_new, match_size = add_matching_words_right(
|
139
|
+
# best_match_in_old, best_match_in_new, best_match_size, end_in_old, end_in_new)
|
140
|
+
|
141
|
+
return (best_match_size != 0 ? Match.new(best_match_in_old, best_match_in_new, best_match_size) : nil)
|
142
|
+
end
|
143
|
+
|
144
|
+
def add_matching_words_left(match_in_old, match_in_new, match_size, start_in_old, start_in_new)
|
145
|
+
while match_in_old > start_in_old and
|
146
|
+
match_in_new > start_in_new and
|
147
|
+
@old_words[match_in_old - 1] == @new_words[match_in_new - 1]
|
148
|
+
match_in_old -= 1
|
149
|
+
match_in_new -= 1
|
150
|
+
match_size += 1
|
151
|
+
end
|
152
|
+
[match_in_old, match_in_new, match_size]
|
153
|
+
end
|
154
|
+
|
155
|
+
def add_matching_words_right(match_in_old, match_in_new, match_size, end_in_old, end_in_new)
|
156
|
+
while match_in_old + match_size < end_in_old and
|
157
|
+
match_in_new + match_size < end_in_new and
|
158
|
+
@old_words[match_in_old + match_size] == @new_words[match_in_new + match_size]
|
159
|
+
match_size += 1
|
160
|
+
end
|
161
|
+
[match_in_old, match_in_new, match_size]
|
162
|
+
end
|
163
|
+
|
164
|
+
VALID_METHODS = [:replace, :insert, :delete, :equal]
|
165
|
+
|
166
|
+
def perform_operation(operation)
|
167
|
+
@operation = operation
|
168
|
+
self.send operation.action, operation
|
169
|
+
end
|
170
|
+
|
171
|
+
def replace(operation)
|
172
|
+
delete(operation, 'diffmod')
|
173
|
+
insert(operation, 'diffmod')
|
174
|
+
end
|
175
|
+
|
176
|
+
def insert(operation, tagclass = 'diffins')
|
177
|
+
insert_tag('ins', tagclass, @new_words[operation.start_in_new...operation.end_in_new])
|
178
|
+
end
|
179
|
+
|
180
|
+
def delete(operation, tagclass = 'diffdel')
|
181
|
+
insert_tag('del', tagclass, @old_words[operation.start_in_old...operation.end_in_old])
|
182
|
+
end
|
183
|
+
|
184
|
+
def equal(operation)
|
185
|
+
# no tags to insert, simply copy the matching words from one of the versions
|
186
|
+
@content += @new_words[operation.start_in_new...operation.end_in_new]
|
187
|
+
end
|
188
|
+
|
189
|
+
def opening_tag?(item)
|
190
|
+
item =~ %r!^\s*<[^>]+>\s*$!
|
191
|
+
end
|
192
|
+
|
193
|
+
def closing_tag?(item)
|
194
|
+
item =~ %r!^\s*</[^>]+>\s*$!
|
195
|
+
end
|
196
|
+
|
197
|
+
def tag?(item)
|
198
|
+
opening_tag?(item) or closing_tag?(item)
|
199
|
+
end
|
200
|
+
|
201
|
+
def extract_consecutive_words(words, &condition)
|
202
|
+
index_of_first_tag = nil
|
203
|
+
words.each_with_index do |word, i|
|
204
|
+
if !condition.call(word)
|
205
|
+
index_of_first_tag = i
|
206
|
+
break
|
207
|
+
end
|
208
|
+
end
|
209
|
+
if index_of_first_tag
|
210
|
+
return words.slice!(0...index_of_first_tag)
|
211
|
+
else
|
212
|
+
return words.slice!(0..words.length)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# This method encloses words within a specified tag (ins or del), and adds this into @content,
|
217
|
+
# with a twist: if there are words contain tags, it actually creates multiple ins or del,
|
218
|
+
# so that they don't include any ins or del. This handles cases like
|
219
|
+
# old: '<p>a</p>'
|
220
|
+
# new: '<p>ab</p><p>c</b>'
|
221
|
+
# diff result: '<p>a<ins>b</ins></p><p><ins>c</ins></p>'
|
222
|
+
# this still doesn't guarantee valid HTML (hint: think about diffing a text containing ins or
|
223
|
+
# del tags), but handles correctly more cases than the earlier version.
|
224
|
+
#
|
225
|
+
# P.S.: Spare a thought for people who write HTML browsers. They live in this ... every day.
|
226
|
+
|
227
|
+
def insert_tag(tagname, cssclass, words)
|
228
|
+
loop do
|
229
|
+
break if words.empty?
|
230
|
+
non_tags = extract_consecutive_words(words) { |word| not tag?(word) }
|
231
|
+
@content << wrap_text(non_tags.join, tagname, cssclass) unless non_tags.empty?
|
232
|
+
|
233
|
+
break if words.empty?
|
234
|
+
@content += extract_consecutive_words(words) { |word| tag?(word) }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def wrap_text(text, tagname, cssclass)
|
239
|
+
%(<#{tagname} class="#{cssclass}">#{text}</#{tagname}>)
|
240
|
+
end
|
241
|
+
|
242
|
+
def explode(sequence)
|
243
|
+
sequence.is_a?(String) ? sequence.split(//) : sequence
|
244
|
+
end
|
245
|
+
|
246
|
+
def end_of_tag?(char)
|
247
|
+
char == '>'
|
248
|
+
end
|
249
|
+
|
250
|
+
def start_of_tag?(char)
|
251
|
+
char == '<'
|
252
|
+
end
|
253
|
+
|
254
|
+
def whitespace?(char)
|
255
|
+
char =~ /\s/
|
256
|
+
end
|
257
|
+
|
258
|
+
def convert_html_to_list_of_words(x, use_brackets = false)
|
259
|
+
mode = :char
|
260
|
+
current_word = ''
|
261
|
+
words = []
|
262
|
+
|
263
|
+
explode(x).each do |char|
|
264
|
+
case mode
|
265
|
+
when :tag
|
266
|
+
if end_of_tag? char
|
267
|
+
current_word << (use_brackets ? ']' : '>')
|
268
|
+
words << current_word
|
269
|
+
current_word = ''
|
270
|
+
if whitespace?(char)
|
271
|
+
mode = :whitespace
|
272
|
+
else
|
273
|
+
mode = :char
|
274
|
+
end
|
275
|
+
else
|
276
|
+
current_word << char
|
277
|
+
end
|
278
|
+
when :char
|
279
|
+
if start_of_tag? char
|
280
|
+
words << current_word unless current_word.empty?
|
281
|
+
current_word = (use_brackets ? '[' : '<')
|
282
|
+
mode = :tag
|
283
|
+
elsif /\s/.match char
|
284
|
+
words << current_word unless current_word.empty?
|
285
|
+
current_word = char
|
286
|
+
mode = :whitespace
|
287
|
+
else
|
288
|
+
current_word << char
|
289
|
+
end
|
290
|
+
when :whitespace
|
291
|
+
if start_of_tag? char
|
292
|
+
words << current_word unless current_word.empty?
|
293
|
+
current_word = (use_brackets ? '[' : '<')
|
294
|
+
mode = :tag
|
295
|
+
elsif /\s/.match char
|
296
|
+
current_word << char
|
297
|
+
else
|
298
|
+
words << current_word unless current_word.empty?
|
299
|
+
current_word = char
|
300
|
+
mode = :char
|
301
|
+
end
|
302
|
+
else
|
303
|
+
raise "Unknown mode #{mode.inspect}"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
words << current_word unless current_word.empty?
|
307
|
+
words
|
308
|
+
end
|
309
|
+
|
310
|
+
end # of class Diff Builder
|
311
|
+
|
312
|
+
def diff(a, b)
|
313
|
+
DiffBuilder.new(a, b).build
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Junebug
|
4
|
+
module Generator
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def generate(args)
|
8
|
+
src_root = File.dirname(__FILE__) + '/../../deploy'
|
9
|
+
app = ARGV.first
|
10
|
+
FileUtils.cp_r(src_root, app)
|
11
|
+
FileUtils.chmod(0755, app+'/wiki')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
module Junebug::Helpers
|
3
|
+
def last_updated(page)
|
4
|
+
from = page.updated_at.to_i
|
5
|
+
to = Time.now.to_i
|
6
|
+
from = from.to_time if from.respond_to?(:to_time)
|
7
|
+
to = to.to_time if to.respond_to?(:to_time)
|
8
|
+
distance = (((to - from).abs)/60).round
|
9
|
+
case distance
|
10
|
+
when 0..1 : return (distance==0) ? 'less than a minute' : '1 minute'
|
11
|
+
when 2..45 : "#{distance} minutes"
|
12
|
+
when 46..90 : 'about 1 hour'
|
13
|
+
when 90..1440 : "about #{(distance.to_f / 60.0).round} hours"
|
14
|
+
when 1441..2880: '1 day'
|
15
|
+
else "#{(distance / 1440).round} days"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'acts_as_versioned'
|
3
|
+
|
4
|
+
module Junebug::Models
|
5
|
+
|
6
|
+
class User < Base
|
7
|
+
validates_length_of :username, :within=>3..30
|
8
|
+
validates_length_of :password, :within=>5..30
|
9
|
+
has_many :pages
|
10
|
+
end
|
11
|
+
|
12
|
+
class Page < Base
|
13
|
+
belongs_to :user
|
14
|
+
#PAGE_LINK = /\[\[([^\]|]*)[|]?([^\]]*)\]\]/
|
15
|
+
#before_save { |r| r.title = r.title.underscore }
|
16
|
+
PAGE_LINK = /([A-Z][a-z]+[A-Z]\w+)/
|
17
|
+
validates_uniqueness_of :title
|
18
|
+
validates_format_of :title, :with => PAGE_LINK
|
19
|
+
validates_presence_of :title
|
20
|
+
acts_as_versioned
|
21
|
+
non_versioned_fields.push 'title'
|
22
|
+
end
|
23
|
+
|
24
|
+
class Page::Version < Base
|
25
|
+
belongs_to :user
|
26
|
+
end
|
27
|
+
|
28
|
+
class CreateJunebug < V 1.0
|
29
|
+
def self.up
|
30
|
+
create_table :junebug_users do |t|
|
31
|
+
t.column :id, :integer, :null => false
|
32
|
+
t.column :username, :string
|
33
|
+
t.column :password, :string
|
34
|
+
end
|
35
|
+
create_table :junebug_pages do |t|
|
36
|
+
t.column :title, :string, :limit => 255
|
37
|
+
t.column :body, :text
|
38
|
+
t.column :user_id, :integer, :null => false
|
39
|
+
t.column :readonly, :boolean
|
40
|
+
t.column :created_at, :datetime
|
41
|
+
t.column :updated_at, :datetime
|
42
|
+
end
|
43
|
+
Page.create_versioned_table
|
44
|
+
Page.reset_column_information
|
45
|
+
|
46
|
+
# Create admin account
|
47
|
+
admin = User.create :username => 'admin', :password => 'password'
|
48
|
+
|
49
|
+
# Install some default pages
|
50
|
+
pages_file = File.dirname(__FILE__) + "/../../fixtures/junebug_pages.yml"
|
51
|
+
#puts pages_file
|
52
|
+
#pages_file = '../fixtures/junebug_pages.yml'
|
53
|
+
YAML.load_file(pages_file).each {|page_data|Page.create(page_data) } if File.exist?(pages_file)
|
54
|
+
end
|
55
|
+
def self.down
|
56
|
+
drop_table :junebug_pages
|
57
|
+
drop_table :junebug_users
|
58
|
+
Page.drop_versioned_table
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'redcloth'
|
2
|
+
|
3
|
+
module Junebug::Views
|
4
|
+
def layout
|
5
|
+
html {
|
6
|
+
head {
|
7
|
+
title @page_title ? @page_title : @page.title
|
8
|
+
link :href=>'/static/yui/reset.css', :type=>'text/css', :rel=>'stylesheet'
|
9
|
+
link :href=>'/static/yui/fonts.css', :type=>'text/css', :rel=>'stylesheet'
|
10
|
+
link :href=>'/static/yui/grids.css', :type=>'text/css', :rel=>'stylesheet'
|
11
|
+
link :href=>'/static/base.css', :type=>'text/css', :rel=>'stylesheet'
|
12
|
+
}
|
13
|
+
body {
|
14
|
+
div :id=>'doc', :class=>'yui-t7' do
|
15
|
+
self << yield
|
16
|
+
end
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def show
|
22
|
+
_header (@version.version == @page.version ? :show : :backlinks), @page.title
|
23
|
+
_body {
|
24
|
+
div.content {
|
25
|
+
_button 'Edit page', {:href => R(Edit, @page.title, @version.version), :style=>'float: right; margin: 0 0 5px 5px;'} if @version.version == @page.version
|
26
|
+
_markup @version.body
|
27
|
+
}
|
28
|
+
}
|
29
|
+
_footer {
|
30
|
+
text "Last edited by <b>#{@version.user.username}</b> on #{@page.updated_at.strftime('%B %d, %Y %I:%M %p')}"
|
31
|
+
if @version.version > 1
|
32
|
+
text " ("
|
33
|
+
a 'diff', :href => R(Diff,@page.title,@version.version-1,@version.version)
|
34
|
+
text ")"
|
35
|
+
end
|
36
|
+
br
|
37
|
+
span.actions {
|
38
|
+
text "Version #{@version.version} "
|
39
|
+
text "(current) " if @version.version == @page.version
|
40
|
+
#text 'Other versions: '
|
41
|
+
a '«older', :href => R(Show, @page.title, @version.version-1) unless @version.version == 1
|
42
|
+
a 'newer»', :href => R(Show, @page.title, @version.version+1) unless @version.version == @page.version
|
43
|
+
a 'current', :href => R(Show, @page.title) unless @version.version == @page.version
|
44
|
+
a 'show all', :href => R(Versions, @page.title)
|
45
|
+
}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def edit
|
50
|
+
_header :backlinks, @page.title
|
51
|
+
_body {
|
52
|
+
h1 @page_title
|
53
|
+
div.formbox {
|
54
|
+
form :method => 'post', :action => R(Edit, @page.title) do
|
55
|
+
p {
|
56
|
+
label 'Page Title'
|
57
|
+
br
|
58
|
+
input :value => @page.title, :name => 'post_title', :size => 30,
|
59
|
+
:type => 'text'
|
60
|
+
small " [ CamelCase only ]"
|
61
|
+
}
|
62
|
+
p {
|
63
|
+
label 'Page Content'
|
64
|
+
br
|
65
|
+
textarea @page.body, :name => 'post_body', :rows => 20, :cols => 80
|
66
|
+
}
|
67
|
+
input :type => 'submit', :value=>'save'
|
68
|
+
end
|
69
|
+
_button 'cancel', :href => R(Show, @page.title, @page.version); text ' '
|
70
|
+
a 'syntax help', :href => 'http://hobix.com/textile/', :target=>'_blank'
|
71
|
+
br :clear=>'all'
|
72
|
+
}
|
73
|
+
}
|
74
|
+
_footer { '' }
|
75
|
+
end
|
76
|
+
|
77
|
+
def versions
|
78
|
+
_header :backlinks, @page.title
|
79
|
+
_body {
|
80
|
+
h1 @page_title
|
81
|
+
ul {
|
82
|
+
@versions.each_with_index do |page,i|
|
83
|
+
li {
|
84
|
+
a "version #{page.version}", :href => R(Show, @page.title, page.version)
|
85
|
+
if page.version > 1
|
86
|
+
text ' ('
|
87
|
+
a 'diff', :href => R(Diff, @page.title, page.version-1, page.version)
|
88
|
+
text ')'
|
89
|
+
end
|
90
|
+
text' - created '
|
91
|
+
text last_updated(page)
|
92
|
+
text ' ago by '
|
93
|
+
strong page.user.username
|
94
|
+
text ' (current)' if i == 0
|
95
|
+
}
|
96
|
+
end
|
97
|
+
}
|
98
|
+
}
|
99
|
+
_footer { '' }
|
100
|
+
end
|
101
|
+
|
102
|
+
def backlinks
|
103
|
+
_header :static, @page.title
|
104
|
+
_body {
|
105
|
+
h1 "Backlinks to #{@page.title}"
|
106
|
+
ul {
|
107
|
+
@pages.each { |p| li{ a p.title, :href => R(Show, p.title) } }
|
108
|
+
}
|
109
|
+
}
|
110
|
+
_footer { '' }
|
111
|
+
end
|
112
|
+
|
113
|
+
def list
|
114
|
+
_header :backlinks, Junebug.config['startpage']
|
115
|
+
_body {
|
116
|
+
h1 "All Wiki Pages"
|
117
|
+
ul {
|
118
|
+
@pages.each { |p| li{ a p.title, :href => R(Show, p.title) } }
|
119
|
+
}
|
120
|
+
}
|
121
|
+
_footer { '' }
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def recent
|
126
|
+
_header :static, @page_title
|
127
|
+
_body {
|
128
|
+
h1 "Updates in the last 30 days"
|
129
|
+
page = @pages.shift
|
130
|
+
while page
|
131
|
+
yday = page.updated_at.yday
|
132
|
+
h2 page.updated_at.strftime('%B %d, %Y')
|
133
|
+
ul {
|
134
|
+
loop do
|
135
|
+
li {
|
136
|
+
a page.title, :href => R(Show, page.title)
|
137
|
+
text ' ('
|
138
|
+
a 'versions', :href => R(Versions, page.title)
|
139
|
+
text ') '
|
140
|
+
span page.updated_at.strftime('%I:%M %p')
|
141
|
+
}
|
142
|
+
page = @pages.shift
|
143
|
+
break unless page && (page.updated_at.yday == yday)
|
144
|
+
end
|
145
|
+
}
|
146
|
+
end
|
147
|
+
}
|
148
|
+
_footer { '' }
|
149
|
+
end
|
150
|
+
|
151
|
+
def diff
|
152
|
+
_header :backlinks, @page.title
|
153
|
+
_body {
|
154
|
+
text 'Comparing '
|
155
|
+
span "version #{@v2.version}", :style=>"background-color: #cfc; padding: 1px 4px;"
|
156
|
+
text ' and '
|
157
|
+
span "version #{@v1.version}", :style=>"background-color: #ddd; padding: 1px 4px;"
|
158
|
+
text ' '
|
159
|
+
a "back", :href => R(Show, @page.title)
|
160
|
+
br
|
161
|
+
br
|
162
|
+
div.diff {
|
163
|
+
text @difftext
|
164
|
+
}
|
165
|
+
}
|
166
|
+
_footer { '' }
|
167
|
+
end
|
168
|
+
|
169
|
+
def login
|
170
|
+
div.login {
|
171
|
+
h1 @page_title
|
172
|
+
p.notice { @notice } if @notice
|
173
|
+
form :action => R(Login), :method => 'post' do
|
174
|
+
label 'Username', :for => 'username'; br
|
175
|
+
input :name => 'username', :type => 'text', :value=>( @user ? @user.username : '') ; br
|
176
|
+
|
177
|
+
label 'Password', :for => 'password'; br
|
178
|
+
input :name => 'password', :type => 'password'; br
|
179
|
+
|
180
|
+
input :type => 'submit', :name => 'login', :value => 'Login'
|
181
|
+
end
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def _button(text, options={})
|
186
|
+
form :method=>:get, :action=>options[:href] do
|
187
|
+
input.button :type=>'submit', :name=>'submit', :value=>text, :style=>options[:style]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def _markup txt
|
192
|
+
return '' if txt.blank?
|
193
|
+
txt.gsub!(Junebug::Models::Page::PAGE_LINK) do
|
194
|
+
page = title = $1
|
195
|
+
# title = $2 unless $2.empty?
|
196
|
+
# page = page.gsub /\W/, '_'
|
197
|
+
if Junebug::Models::Page.find(:all, :select => 'title').collect { |p| p.title }.include?(page)
|
198
|
+
%Q{<a href="#{self/R(Show, page)}">#{title}</a>}
|
199
|
+
else
|
200
|
+
%Q{<span>#{title}<a href="#{self/R(Edit, page, 1)}">?</a></span>}
|
201
|
+
end
|
202
|
+
end
|
203
|
+
text RedCloth.new(txt, [ ]).to_html
|
204
|
+
end
|
205
|
+
|
206
|
+
def _header type, page_title
|
207
|
+
div :id=>'hd' do
|
208
|
+
span :id=>'userlinks', :style=>'float: right;' do
|
209
|
+
@state.user_id.blank? ? a('login', :href=>R(Login)) : (text "#{@state.user_username} - " ; a('logout', :href=>R(Logout)))
|
210
|
+
end
|
211
|
+
if type == :static
|
212
|
+
h1 page_title
|
213
|
+
elsif type == :show
|
214
|
+
h1 { a page_title, :href => R(Backlinks, page_title) }
|
215
|
+
else
|
216
|
+
h1 { a page_title, :href => R(Show, page_title) }
|
217
|
+
end
|
218
|
+
span {
|
219
|
+
a 'Home', :href => R(Show, Junebug.config['startpage'])
|
220
|
+
text ' | '
|
221
|
+
a 'RecentChanges', :href => R(Recent)
|
222
|
+
text ' | '
|
223
|
+
a 'All Pages', :href => R(List)
|
224
|
+
text ' | '
|
225
|
+
a 'Help', :href => R(Show, "JunebugHelp")
|
226
|
+
}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def _body
|
231
|
+
div.content {
|
232
|
+
div :id=>'bd' do
|
233
|
+
div :id=>'yui-main' do
|
234
|
+
div :class=>'yui-b' do
|
235
|
+
yield
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
def _footer
|
243
|
+
div :id=>'ft' do
|
244
|
+
span :style=>'float: right;' do
|
245
|
+
text 'Powered by '
|
246
|
+
a 'JunebugWiki', :href => 'http://www.junebugwiki.com/'
|
247
|
+
end
|
248
|
+
yield
|
249
|
+
br :clear=>'all'
|
250
|
+
end
|
251
|
+
text <<END
|
252
|
+
<p>
|
253
|
+
<a href="http://validator.w3.org/check?uri=referer"><img
|
254
|
+
src="http://www.w3.org/Icons/valid-xhtml10"
|
255
|
+
alt="Valid XHTML 1.0 Transitional" height="31" width="88" /></a>
|
256
|
+
</p>
|
257
|
+
END
|
258
|
+
end
|
259
|
+
|
260
|
+
def self.feed
|
261
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
262
|
+
|
263
|
+
xml.instruct!
|
264
|
+
xml.feed "xmlns"=>"http://www.w3.org/2005/Atom" do
|
265
|
+
|
266
|
+
xml.title "Recently Updated Wiki Pages"
|
267
|
+
xml.id Junebug.config['url'] + '/'
|
268
|
+
xml.link "rel" => "self", "href" => Junebug.config['feed']
|
269
|
+
|
270
|
+
pages = Junebug::Models::Page.find(:all, :order => 'updated_at DESC', :limit => 20)
|
271
|
+
xml.updated pages.first.updated_at.xmlschema
|
272
|
+
|
273
|
+
pages.each do |page|
|
274
|
+
xml.entry do
|
275
|
+
xml.id Junebug.config['url'] + '/s/' + page.title
|
276
|
+
xml.title page.title
|
277
|
+
xml.author { xml.name "Anonymous" }
|
278
|
+
xml.updated page.updated_at.xmlschema
|
279
|
+
xml.link "rel" => "alternate", "href" => Junebug.config['url'] + '/s/' + page.title
|
280
|
+
xml.summary "#{page.title}"
|
281
|
+
xml.content 'type' => 'html' do
|
282
|
+
xml.text! page.body.gsub("\n", '<br/>').gsub("\r", '')
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|