junebug 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|