junebug 0.0.3

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