nesta 0.9.11 → 0.9.13
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -0
- data/CHANGES +55 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +38 -11
- data/Guardfile +7 -0
- data/Rakefile +2 -0
- data/bin/nesta +73 -12
- data/lib/nesta.rb +8 -0
- data/lib/nesta/app.rb +5 -85
- data/lib/nesta/cache.rb +12 -13
- data/lib/nesta/commands.rb +36 -7
- data/lib/nesta/config.rb +2 -5
- data/lib/nesta/helpers.rb +74 -0
- data/lib/nesta/models.rb +36 -15
- data/lib/nesta/navigation.rb +7 -3
- data/lib/nesta/overrides.rb +5 -0
- data/lib/nesta/plugin.rb +10 -8
- data/lib/nesta/version.rb +1 -1
- data/nesta.gemspec +2 -1
- data/spec/atom_spec.rb +1 -1
- data/spec/commands_spec.rb +27 -2
- data/spec/models_spec.rb +45 -4
- data/spec/page_spec.rb +4 -4
- data/spec/sitemap_spec.rb +4 -4
- data/templates/config.ru +3 -0
- data/templates/themes/README.md +1 -1
- data/views/atom.haml +4 -4
- data/views/error.haml +1 -1
- data/views/feed.haml +1 -1
- data/views/layout.haml +2 -2
- data/views/page_meta.haml +2 -2
- data/views/sitemap.haml +2 -2
- data/views/summaries.haml +2 -2
- metadata +49 -35
- data/lib/nesta/nesta.rb +0 -7
data/lib/nesta/commands.rb
CHANGED
@@ -50,10 +50,28 @@ module Nesta
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
class Edit
|
54
|
+
include Command
|
55
|
+
|
56
|
+
def initialize(*args)
|
57
|
+
@filename = Nesta::Config.page_path(args.shift)
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute
|
61
|
+
editor = ENV.fetch('EDITOR')
|
62
|
+
rescue IndexError
|
63
|
+
$stderr.puts "No editor: set EDITOR environment variable"
|
64
|
+
else
|
65
|
+
system(editor, @filename)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
53
69
|
class New
|
54
70
|
include Command
|
55
71
|
|
56
|
-
def initialize(
|
72
|
+
def initialize(*args)
|
73
|
+
path = args.shift
|
74
|
+
options = args.shift || {}
|
57
75
|
path.nil? && (raise UsageError.new('path not specified'))
|
58
76
|
if File.exist?(path)
|
59
77
|
raise RuntimeError.new("#{path} already exists")
|
@@ -104,7 +122,7 @@ module Nesta
|
|
104
122
|
class Content
|
105
123
|
include Command
|
106
124
|
|
107
|
-
def initialize
|
125
|
+
def initialize(*args)
|
108
126
|
@dir = 'content-demo'
|
109
127
|
end
|
110
128
|
|
@@ -135,7 +153,8 @@ module Nesta
|
|
135
153
|
|
136
154
|
module Plugin
|
137
155
|
class Create
|
138
|
-
def initialize(
|
156
|
+
def initialize(*args)
|
157
|
+
name = args.shift
|
139
158
|
name.nil? && (raise UsageError.new('name not specified'))
|
140
159
|
@name = name
|
141
160
|
@gem_name = "nesta-plugin-#{name}"
|
@@ -210,7 +229,9 @@ end
|
|
210
229
|
class Create
|
211
230
|
include Command
|
212
231
|
|
213
|
-
def initialize(
|
232
|
+
def initialize(*args)
|
233
|
+
name = args.shift
|
234
|
+
options = args.shift || {}
|
214
235
|
name.nil? && (raise UsageError.new('name not specified'))
|
215
236
|
@name = name
|
216
237
|
@theme_path = Nesta::Path.themes(@name)
|
@@ -234,7 +255,9 @@ end
|
|
234
255
|
class Install
|
235
256
|
include Command
|
236
257
|
|
237
|
-
def initialize(
|
258
|
+
def initialize(*args)
|
259
|
+
url = args.shift
|
260
|
+
options = args.shift || {}
|
238
261
|
url.nil? && (raise UsageError.new('URL not specified'))
|
239
262
|
@url = url
|
240
263
|
@name = File.basename(url, '.git').sub(/nesta-theme-/, '')
|
@@ -243,14 +266,20 @@ end
|
|
243
266
|
def execute
|
244
267
|
system('git', 'clone', @url, "themes/#{@name}")
|
245
268
|
FileUtils.rm_r(File.join("themes/#{@name}", '.git'))
|
246
|
-
enable
|
269
|
+
enable
|
270
|
+
end
|
271
|
+
|
272
|
+
def enable
|
273
|
+
Enable.new(@name).execute
|
247
274
|
end
|
248
275
|
end
|
249
276
|
|
250
277
|
class Enable
|
251
278
|
include Command
|
252
279
|
|
253
|
-
def initialize(
|
280
|
+
def initialize(*args)
|
281
|
+
name = args.shift
|
282
|
+
options = args.shift || {}
|
254
283
|
name.nil? && (raise UsageError.new('name not specified'))
|
255
284
|
@name = name
|
256
285
|
end
|
data/lib/nesta/config.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
require "rubygems"
|
4
|
-
require "sinatra"
|
1
|
+
require 'yaml'
|
5
2
|
|
6
3
|
module Nesta
|
7
4
|
class Config
|
@@ -68,7 +65,7 @@ module Nesta
|
|
68
65
|
|
69
66
|
def self.from_yaml(setting)
|
70
67
|
if can_use_yaml?
|
71
|
-
self.yaml_conf ||= YAML::load(IO.read(yaml_path))
|
68
|
+
self.yaml_conf ||= YAML::load(ERB.new(IO.read(yaml_path)).result)
|
72
69
|
rack_env_conf = self.yaml_conf[Nesta::App.environment.to_s]
|
73
70
|
(rack_env_conf && rack_env_conf[setting]) || self.yaml_conf[setting]
|
74
71
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Nesta
|
2
|
+
module View
|
3
|
+
module Helpers
|
4
|
+
def set_from_config(*variables)
|
5
|
+
variables.each do |var|
|
6
|
+
instance_variable_set("@#{var}", Nesta::Config.send(var))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def set_from_page(*variables)
|
11
|
+
variables.each do |var|
|
12
|
+
instance_variable_set("@#{var}", @page.send(var))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def no_widow(text)
|
17
|
+
text.split[0...-1].join(" ") + " #{text.split[-1]}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_common_variables
|
21
|
+
@menu_items = Nesta::Menu.for_path('/')
|
22
|
+
@site_title = Nesta::Config.title
|
23
|
+
set_from_config(:title, :subtitle, :google_analytics_code)
|
24
|
+
@heading = @title
|
25
|
+
end
|
26
|
+
|
27
|
+
def absolute_urls(text)
|
28
|
+
text.gsub!(/(<a href=['"])\//, '\1' + url('/'))
|
29
|
+
text
|
30
|
+
end
|
31
|
+
|
32
|
+
def nesta_atom_id_for_page(page)
|
33
|
+
published = page.date.strftime('%Y-%m-%d')
|
34
|
+
"tag:#{request.host},#{published}:#{page.abspath}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def atom_id(page = nil)
|
38
|
+
if page
|
39
|
+
page.atom_id || nesta_atom_id_for_page(page)
|
40
|
+
else
|
41
|
+
"tag:#{request.host},2009:/"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_date(date)
|
46
|
+
date.strftime("%d %B %Y")
|
47
|
+
end
|
48
|
+
|
49
|
+
def local_stylesheet?
|
50
|
+
Nesta.deprecated('local_stylesheet?', 'use local_stylesheet_link_tag')
|
51
|
+
File.exist?(File.expand_path('views/local.sass', Nesta::App.root))
|
52
|
+
end
|
53
|
+
|
54
|
+
def local_stylesheet_link_tag(name)
|
55
|
+
pattern = File.expand_path("views/#{name}.s{a,c}ss", Nesta::App.root)
|
56
|
+
if Dir.glob(pattern).size > 0
|
57
|
+
haml_tag :link, :href => url("/css/#{name}.css"), :rel => "stylesheet"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def latest_articles(count = 8)
|
62
|
+
Nesta::Page.find_articles[0..count - 1]
|
63
|
+
end
|
64
|
+
|
65
|
+
def article_summaries(articles)
|
66
|
+
haml(:summaries, :layout => false, :locals => { :pages => articles })
|
67
|
+
end
|
68
|
+
|
69
|
+
def articles_heading
|
70
|
+
@page.metadata('articles heading') || "Articles on #{@page.heading}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/nesta/models.rb
CHANGED
@@ -7,6 +7,8 @@ Tilt.register Tilt::RDiscountTemplate, 'mdown'
|
|
7
7
|
Tilt.register Tilt::RedcarpetTemplate, 'mdown'
|
8
8
|
|
9
9
|
module Nesta
|
10
|
+
class MetadataParseError < RuntimeError; end
|
11
|
+
|
10
12
|
class FileModel
|
11
13
|
FORMATS = [:mdown, :haml, :textile]
|
12
14
|
@@cache = {}
|
@@ -123,33 +125,49 @@ module Nesta
|
|
123
125
|
flags && flags.split(',').map { |name| name.strip }.include?(flag)
|
124
126
|
end
|
125
127
|
|
128
|
+
def parse_metadata(first_paragraph)
|
129
|
+
is_metadata = first_paragraph.split("\n").first =~ /^[\w ]+:/
|
130
|
+
raise MetadataParseError unless is_metadata
|
131
|
+
metadata = CaseInsensitiveHash.new
|
132
|
+
first_paragraph.split("\n").each do |line|
|
133
|
+
key, value = line.split(/\s*:\s*/, 2)
|
134
|
+
next if value.nil?
|
135
|
+
metadata[key.downcase] = value.chomp
|
136
|
+
end
|
137
|
+
metadata
|
138
|
+
end
|
139
|
+
|
126
140
|
private
|
127
141
|
def markup
|
128
142
|
@markup
|
129
143
|
end
|
130
144
|
|
131
|
-
def metadata?(text)
|
132
|
-
text.split("\n").first =~ /^[\w ]+:/
|
133
|
-
end
|
134
|
-
|
135
145
|
def parse_file
|
136
146
|
contents = File.open(@filename).read
|
137
147
|
rescue Errno::ENOENT
|
138
148
|
raise Sinatra::NotFound
|
139
149
|
else
|
140
150
|
first_paragraph, remaining = contents.split(/\r?\n\r?\n/, 2)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
151
|
+
begin
|
152
|
+
return parse_metadata(first_paragraph), remaining
|
153
|
+
rescue MetadataParseError
|
154
|
+
return {}, contents
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def tag_lines_of_haml(text)
|
159
|
+
tagged = (text =~ /^\s*%/)
|
160
|
+
if tagged
|
161
|
+
text
|
162
|
+
else
|
163
|
+
text.split(/\r?\n/).inject("") do |accumulator, line|
|
164
|
+
accumulator << "%p #{line}\n"
|
146
165
|
end
|
147
166
|
end
|
148
|
-
markup = metadata?(first_paragraph) ? remaining : contents
|
149
|
-
return metadata, markup
|
150
167
|
end
|
151
168
|
|
152
169
|
def convert_to_html(format, scope, text)
|
170
|
+
text = tag_lines_of_haml(text) if @format == :haml
|
153
171
|
template = Tilt[format].new { text }
|
154
172
|
template.render(scope)
|
155
173
|
end
|
@@ -237,16 +255,19 @@ module Nesta
|
|
237
255
|
end
|
238
256
|
end
|
239
257
|
|
240
|
-
def
|
241
|
-
|
258
|
+
def body_markup
|
259
|
+
case @format
|
242
260
|
when :mdown
|
243
261
|
markup.sub(/^#[^#].*$\r?\n(\r?\n)?/, '')
|
244
262
|
when :haml
|
245
263
|
markup.sub(/^\s*%h1\s+.*$\r?\n(\r?\n)?/, '')
|
246
264
|
when :textile
|
247
265
|
markup.sub(/^\s*h1\.\s+.*$\r?\n(\r?\n)?/, '')
|
248
|
-
|
249
|
-
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def body(scope = nil)
|
270
|
+
convert_to_html(@format, scope, body_markup)
|
250
271
|
end
|
251
272
|
|
252
273
|
def categories
|
data/lib/nesta/navigation.rb
CHANGED
@@ -21,9 +21,9 @@ module Nesta
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
else
|
24
|
-
html_class = (
|
24
|
+
html_class = current_item?(item) ? "current" : nil
|
25
25
|
haml_tag :li, :class => html_class do
|
26
|
-
haml_tag :a, :<, :href => item.abspath do
|
26
|
+
haml_tag :a, :<, :href => url(item.abspath) do
|
27
27
|
haml_concat item.heading
|
28
28
|
end
|
29
29
|
end
|
@@ -44,7 +44,7 @@ module Nesta
|
|
44
44
|
haml_tag :ul, :class => options[:class] do
|
45
45
|
breadcrumb_ancestors[0...-1].each do |page|
|
46
46
|
haml_tag :li do
|
47
|
-
haml_tag :a, :<, :href => page.abspath do
|
47
|
+
haml_tag :a, :<, :href => url(page.abspath) do
|
48
48
|
haml_concat breadcrumb_label(page)
|
49
49
|
end
|
50
50
|
end
|
@@ -56,6 +56,10 @@ module Nesta
|
|
56
56
|
def breadcrumb_label(page)
|
57
57
|
(page.abspath == '/') ? 'Home' : page.heading
|
58
58
|
end
|
59
|
+
|
60
|
+
def current_item?(item)
|
61
|
+
request.path == item.abspath
|
62
|
+
end
|
59
63
|
end
|
60
64
|
end
|
61
65
|
end
|
data/lib/nesta/overrides.rb
CHANGED
@@ -6,6 +6,11 @@ module Nesta
|
|
6
6
|
super(template, defaults.merge(options), locals)
|
7
7
|
end
|
8
8
|
|
9
|
+
def erb(template, options = {}, locals = {})
|
10
|
+
defaults, engine = Overrides.render_options(template, :erb)
|
11
|
+
super(template, defaults.merge(options), locals)
|
12
|
+
end
|
13
|
+
|
9
14
|
def scss(template, options = {}, locals = {})
|
10
15
|
defaults, engine = Overrides.render_options(template, :scss)
|
11
16
|
super(template, defaults.merge(options), locals)
|
data/lib/nesta/plugin.rb
CHANGED
@@ -17,17 +17,19 @@ module Nesta
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.load_local_plugins
|
20
|
+
# This approach is deprecated; plugins should now be distributed
|
21
|
+
# as gems. See http://nestacms.com/docs/plugins/writing-plugins
|
20
22
|
plugins = Dir.glob(File.expand_path('../plugins/*', File.dirname(__FILE__)))
|
21
23
|
plugins.each { |path| require_local_plugin(path) }
|
22
24
|
end
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
def self.require_local_plugin(path)
|
27
|
+
Nesta.deprecated(
|
28
|
+
'loading plugins from ./plugins', "convert #{path} to a gem")
|
29
|
+
require File.join(path, 'lib', File.basename(path))
|
30
|
+
rescue LoadError => e
|
31
|
+
$stderr.write("Couldn't load plugins/#{File.basename(path)}: #{e}\n")
|
32
|
+
end
|
33
|
+
private_class_method :require_local_plugin
|
32
34
|
end
|
33
35
|
end
|
data/lib/nesta/version.rb
CHANGED
data/nesta.gemspec
CHANGED
@@ -35,7 +35,8 @@ EOF
|
|
35
35
|
s.add_dependency('sass', '~> 3.1')
|
36
36
|
s.add_dependency('rdiscount', '~> 1.6')
|
37
37
|
s.add_dependency('RedCloth', '~> 4.2')
|
38
|
-
s.add_dependency('sinatra', '1.
|
38
|
+
s.add_dependency('sinatra', '~> 1.3')
|
39
|
+
s.add_dependency('rack', '~> 1.1')
|
39
40
|
|
40
41
|
# Useful in development
|
41
42
|
s.add_dependency('shotgun', '>= 0.8')
|
data/spec/atom_spec.rb
CHANGED
@@ -32,7 +32,7 @@ describe "atom feed" do
|
|
32
32
|
end
|
33
33
|
|
34
34
|
it "should have an alternate link element" do
|
35
|
-
body.should have_tag("/feed/link[@rel=alternate][@href='http://example.org']")
|
35
|
+
body.should have_tag("/feed/link[@rel=alternate][@href='http://example.org/']")
|
36
36
|
end
|
37
37
|
|
38
38
|
it "should have a self link element" do
|
data/spec/commands_spec.rb
CHANGED
@@ -167,6 +167,29 @@ describe "nesta" do
|
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
170
|
+
describe "edit" do
|
171
|
+
before(:each) do
|
172
|
+
Nesta::Config.stub!(:content_path).and_return('content')
|
173
|
+
@page_path = 'path/to/page.mdown'
|
174
|
+
@command = Nesta::Commands::Edit.new(@page_path)
|
175
|
+
@command.stub!(:system)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should launch the editor" do
|
179
|
+
ENV['EDITOR'] = 'vi'
|
180
|
+
full_path = File.join('content/pages', @page_path)
|
181
|
+
@command.should_receive(:system).with(ENV['EDITOR'], full_path)
|
182
|
+
@command.execute
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should not try and launch an editor if environment not setup" do
|
186
|
+
ENV.delete('EDITOR')
|
187
|
+
@command.should_not_receive(:system)
|
188
|
+
$stderr.stub!(:puts)
|
189
|
+
@command.execute
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
170
193
|
describe "plugin:create" do
|
171
194
|
before(:each) do
|
172
195
|
@name = 'my-feature'
|
@@ -247,6 +270,7 @@ describe "nesta" do
|
|
247
270
|
@theme_dir = 'themes/mine'
|
248
271
|
FileUtils.mkdir_p(File.join(@theme_dir, '.git'))
|
249
272
|
@command = Nesta::Commands::Theme::Install.new(@repo_url)
|
273
|
+
@command.stub!(:enable)
|
250
274
|
@command.stub!(:system)
|
251
275
|
end
|
252
276
|
|
@@ -267,16 +291,17 @@ describe "nesta" do
|
|
267
291
|
end
|
268
292
|
|
269
293
|
it "should enable the freshly installed theme" do
|
270
|
-
@command.should_receive(:enable)
|
294
|
+
@command.should_receive(:enable)
|
271
295
|
@command.execute
|
272
296
|
end
|
273
297
|
|
274
|
-
describe "when theme URL doesn't match
|
298
|
+
describe "when theme URL doesn't match recommended pattern" do
|
275
299
|
before(:each) do
|
276
300
|
@repo_url = 'git://foobar.com/path/to/mytheme.git'
|
277
301
|
@other_theme_dir = 'themes/mytheme'
|
278
302
|
FileUtils.mkdir_p(File.join(@other_theme_dir, '.git'))
|
279
303
|
@command = Nesta::Commands::Theme::Install.new(@repo_url)
|
304
|
+
@command.stub!(:enable)
|
280
305
|
end
|
281
306
|
|
282
307
|
after(:each) do
|
data/spec/models_spec.rb
CHANGED
@@ -58,6 +58,24 @@ describe "Page", :shared => true do
|
|
58
58
|
Nesta::Page.find_by_path('banana').heading.should == 'Banana'
|
59
59
|
end
|
60
60
|
|
61
|
+
it "should respond to #parse_metadata, returning hash of key/value" do
|
62
|
+
page = create_page(:heading => 'Banana', :path => 'banana')
|
63
|
+
metadata = page.parse_metadata('My key: some value')
|
64
|
+
metadata['my key'].should == 'some value'
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be parseable if metadata is invalid" do
|
68
|
+
dodgy_metadata = "Key: value\nKey without value\nAnother key: value"
|
69
|
+
create_page(:heading => 'Banana', :path => 'banana') do |path|
|
70
|
+
text = File.read(path)
|
71
|
+
File.open(path, 'w') do |file|
|
72
|
+
file.puts(dodgy_metadata)
|
73
|
+
file.write(text)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
Nesta::Page.find_by_path('banana')
|
77
|
+
end
|
78
|
+
|
61
79
|
describe "for home page" do
|
62
80
|
it "should set title to heading and site title" do
|
63
81
|
create_page(:heading => 'Home', :path => 'index')
|
@@ -396,6 +414,10 @@ describe "Page", :shared => true do
|
|
396
414
|
it "should not include metadata in the HTML" do
|
397
415
|
@article.to_html.should_not have_tag("p", /^Date/)
|
398
416
|
end
|
417
|
+
|
418
|
+
it "should not include heading in body markup" do
|
419
|
+
@article.body_markup.should_not include("My article")
|
420
|
+
end
|
399
421
|
|
400
422
|
it "should not include heading in body" do
|
401
423
|
@article.body.should_not have_tag("h1", "My article")
|
@@ -501,12 +523,31 @@ describe "Haml page" do
|
|
501
523
|
it_should_behave_like "Page"
|
502
524
|
|
503
525
|
it "should set heading from first h1 tag" do
|
504
|
-
create_page(
|
526
|
+
page = create_page(
|
505
527
|
:path => "a-page",
|
506
528
|
:heading => "First heading",
|
507
529
|
:content => "%h1 Second heading"
|
508
530
|
)
|
509
|
-
|
531
|
+
page.heading.should == "First heading"
|
532
|
+
end
|
533
|
+
|
534
|
+
it "should wrap <p> tags around one line summary text" do
|
535
|
+
page = create_page(
|
536
|
+
:path => "a-page",
|
537
|
+
:heading => "First para",
|
538
|
+
:metadata => { "Summary" => "Wrap me" }
|
539
|
+
)
|
540
|
+
page.summary.should include("<p>Wrap me</p>")
|
541
|
+
end
|
542
|
+
|
543
|
+
it "should wrap <p> tags around multiple lines of summary text" do
|
544
|
+
page = create_page(
|
545
|
+
:path => "a-page",
|
546
|
+
:heading => "First para",
|
547
|
+
:metadata => { "Summary" => 'Wrap me\nIn paragraph tags' }
|
548
|
+
)
|
549
|
+
page.summary.should include("<p>Wrap me</p>")
|
550
|
+
page.summary.should include("<p>In paragraph tags</p>")
|
510
551
|
end
|
511
552
|
end
|
512
553
|
|
@@ -518,12 +559,12 @@ describe "Textile page" do
|
|
518
559
|
it_should_behave_like "Page"
|
519
560
|
|
520
561
|
it "should set heading from first h1 tag" do
|
521
|
-
create_page(
|
562
|
+
page = create_page(
|
522
563
|
:path => "a-page",
|
523
564
|
:heading => "First heading",
|
524
565
|
:content => "h1. Second heading"
|
525
566
|
)
|
526
|
-
|
567
|
+
page.heading.should == "First heading"
|
527
568
|
end
|
528
569
|
end
|
529
570
|
|