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.
@@ -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