strelka-cms 0.0.1.pre.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data.tar.gz.sig +0 -0
  2. data/.gemtest +0 -0
  3. data/ChangeLog +33 -0
  4. data/History.rdoc +4 -0
  5. data/Manifest.txt +52 -0
  6. data/Procfile +4 -0
  7. data/README.rdoc +74 -0
  8. data/Rakefile +68 -0
  9. data/data/strelka-cms/apps/content-feeds +194 -0
  10. data/data/strelka-cms/apps/content-manager +159 -0
  11. data/data/strelka-cms/templates/autoindex/default.tmpl +11 -0
  12. data/data/strelka-cms/templates/layout.tmpl +58 -0
  13. data/data/strelka-cms/templates/page.tmpl +21 -0
  14. data/etc/mongrel.rb +34 -0
  15. data/lib/strelka/cms.rb +42 -0
  16. data/lib/strelka/cms/page.rb +322 -0
  17. data/lib/strelka/cms/pagecatalog.rb +121 -0
  18. data/lib/strelka/cms/pagefilter.rb +76 -0
  19. data/lib/strelka/cms/pagefilter/autoindex.rb +140 -0
  20. data/lib/strelka/cms/pagefilter/cleanup.rb +40 -0
  21. data/lib/strelka/cms/pagefilter/editorial.rb +55 -0
  22. data/lib/strelka/cms/pagefilter/example.rb +232 -0
  23. data/lib/strelka/cms/pagefilter/strip.rb +30 -0
  24. data/lib/strelka/cms/pagefilter/textile.rb +20 -0
  25. data/public/humans.txt +8 -0
  26. data/public/images/misc/button-gloss.png +0 -0
  27. data/public/images/misc/button-overlay.png +0 -0
  28. data/public/images/misc/custom-form-sprites.png +0 -0
  29. data/public/images/misc/input-bg-outset.png +0 -0
  30. data/public/images/misc/input-bg.png +0 -0
  31. data/public/images/misc/modal-gloss.png +0 -0
  32. data/public/images/misc/table-sorter.png +0 -0
  33. data/public/images/orbit/bullets.jpg +0 -0
  34. data/public/images/orbit/left-arrow.png +0 -0
  35. data/public/images/orbit/loading.gif +0 -0
  36. data/public/images/orbit/mask-black.png +0 -0
  37. data/public/images/orbit/pause-black.png +0 -0
  38. data/public/images/orbit/right-arrow.png +0 -0
  39. data/public/images/orbit/rotator-black.png +0 -0
  40. data/public/images/orbit/timer-black.png +0 -0
  41. data/public/index.page +46 -0
  42. data/public/javascripts/app.js +98 -0
  43. data/public/javascripts/foundation.js +12 -0
  44. data/public/javascripts/jquery.min.js +4 -0
  45. data/public/javascripts/modernizr.foundation.js +4 -0
  46. data/public/robots.txt +4 -0
  47. data/public/stylesheets/app.css +30 -0
  48. data/public/stylesheets/foundation.css +1424 -0
  49. data/public/stylesheets/ie.css +13 -0
  50. data/spec/data/test.page +22 -0
  51. data/spec/lib/helpers.rb +58 -0
  52. data/spec/strelka/cms/page_spec.rb +139 -0
  53. data/spec/strelka/cms/pagecatalog_spec.rb +94 -0
  54. data/spec/strelka/cms_spec.rb +27 -0
  55. metadata +346 -0
  56. metadata.gz.sig +0 -0
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'configurability'
5
+ require 'strelka'
6
+
7
+ require 'strelka/cms' unless defined?( Strelka::CMS )
8
+ require 'strelka/cms/page' unless defined?( Strelka::CMS::Page )
9
+
10
+ # A catalog of Strelka::CMS::Page objects. It provides a collection interface
11
+ # over a group of pages, suitable for searching for, or iterating over
12
+ # pages matching one or more criteria.
13
+ class Strelka::CMS::PageCatalog
14
+ extend Loggability
15
+ include Enumerable
16
+
17
+ # Loggability API -- log to the deveiate logger
18
+ log_to :strelka_cms
19
+
20
+
21
+ # The default glob pattern to match pages.
22
+ DEFAULT_GLOB_PATTERN = '**/*.page'
23
+
24
+
25
+ #################################################################
26
+ ### I N S T A N C E M E T H O D S
27
+ #################################################################
28
+
29
+ ### Create a new PageCatalog that will find and read pages from the configured
30
+ ### directory.
31
+ def initialize( basedir=Pathname.pwd, pattern=DEFAULT_GLOB_PATTERN )
32
+ pattern.slice!( 0, 1 ) if pattern.start_with?( '/' )
33
+ self.log.debug "New catalog for pages matching: %s in: %s" % [ pattern, basedir ]
34
+ @basedir = Pathname( basedir )
35
+ @pageglob = pattern
36
+ end
37
+
38
+
39
+ ######
40
+ public
41
+ ######
42
+
43
+ # The base directory of the catalog
44
+ attr_reader :basedir
45
+
46
+ # The glob pattern that will match a collection of one or more pages
47
+ attr_reader :pageglob
48
+
49
+
50
+ ### Return the glob pattern for pages in this catalog.
51
+ def glob
52
+ return (self.basedir + self.pageglob).to_s
53
+ end
54
+
55
+
56
+ ### Enumerable interface -- if called with a block, load and yield each page matching the
57
+ ### #pageglob. If called without a block, return an Enumerator.
58
+ def each
59
+ iter = self.page_enumerator
60
+ if block_given?
61
+ block = Proc.new
62
+ iter.each( &block )
63
+ else
64
+ return iter
65
+ end
66
+ end
67
+
68
+
69
+ ### Return a clone of the directory that will limit the pages in the catalog to those
70
+ ### under the given +dir+.
71
+ def relative_to( dir )
72
+ dir = dir.to_s
73
+ dir.slice!( 0, 1 ) if dir.start_with?( '/' )
74
+ self.log.debug "Build new catalog for %p relative to %s" % [ dir, self.basedir ]
75
+ self.class.new( self.basedir + dir, self.pageglob )
76
+ end
77
+
78
+
79
+ ### Return a clone of the directory that will use the specified +pattern+ to find pages
80
+ ### instead of the original.
81
+ def matching_pattern( pattern )
82
+ self.class.new( self.basedir, pattern )
83
+ end
84
+
85
+
86
+ ### Return the catalog object as a human-readable string.
87
+ def inspect
88
+ "#<%s:0x%0x %s, %d documents>" % [
89
+ self.class.name,
90
+ self.object_id / 2,
91
+ self.glob,
92
+ self.page_path_enumerator.count,
93
+ ]
94
+ end
95
+
96
+
97
+ #########
98
+ protected
99
+ #########
100
+
101
+ ### Return an Enumerator that will yield a Pathname for each page matching the #pageglob
102
+ def page_path_enumerator
103
+ self.log.debug "Fetching an enumerator for %s" % [ self.glob ]
104
+ Pathname.glob( self.glob ).each
105
+ end
106
+
107
+
108
+ ### Return an Enumerator that will yield a Strelka::CMS::Page object for each page matching
109
+ ### the #pageglob.
110
+ def page_enumerator
111
+ Enumerator.new do |yielder|
112
+ self.page_path_enumerator.each do |path|
113
+ page = Strelka::CMS::Page.load( path, self )
114
+ yielder.yield( page )
115
+ end
116
+ end
117
+ end
118
+
119
+
120
+ end # class Strelka::CMS::PageCatalog
121
+
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pluginfactory'
4
+ require 'strelka/mixins'
5
+
6
+ require 'strelka/cms' unless defined?( Strelka::CMS )
7
+ require 'strelka/cms/page' unless defined?( Strelka::CMS::Page )
8
+
9
+
10
+ # An abstract base class for page filters in the Strelka CMS.
11
+ #
12
+ # A page filter replaces one or more placeholders with generated or altered
13
+ # content.
14
+ #
15
+ class Strelka::CMS::PageFilter
16
+ extend Loggability,
17
+ PluginFactory
18
+
19
+
20
+ # Loggability API -- log to the deveiate logger
21
+ log_to :strelka_cms
22
+
23
+
24
+ ### PluginFactory API -- list the directories to search for derivatives.
25
+ def self::derivative_dirs
26
+ [ 'strelka/cms/pagefilter' ]
27
+ end
28
+
29
+
30
+ ### Search for plugins in the $LOAD_PATH and load each of them that's found.
31
+ def self::load_all
32
+ loaded = []
33
+ glob_pat = '{' + self.derivative_dirs.join(',') + '}/*.rb'
34
+ $LOAD_PATH.uniq.collect {|path| path.untaint; Pathname(path) }.each do |base|
35
+ self.log.debug " searching for %s" % [ base + glob_pat ]
36
+ Pathname.glob( base + glob_pat ).each do |plugin|
37
+ # Don't load this file twice
38
+ next if Pathname(__FILE__).expand_path == plugin.expand_path
39
+
40
+ begin
41
+ path = plugin.to_s.untaint
42
+ require( path )
43
+ loaded << plugin
44
+ rescue LoadError, SecurityError => err
45
+ self.log.error " %s while loading %s: %s" %
46
+ [ err.class.name, plugin.to_s, err.message ]
47
+ err.backtrace.each do |frame|
48
+ self.log.debug " #{frame}"
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ return loaded
55
+ end
56
+
57
+
58
+ #################################################################
59
+ ### I N S T A N C E M E T H O D S
60
+ #################################################################
61
+
62
+ ### Export any static resources required by this filter to the given +output_dir+.
63
+ def export_resources( output_dir )
64
+ # No-op by default
65
+ end
66
+
67
+
68
+ ### Process the +page+'s source with the filter and return the altered content.
69
+ def process( source, page, index )
70
+ raise NotImplementedError,
71
+ "%s does not implement the #process method" % [ self.class.name ]
72
+ end
73
+
74
+ end # class Strelka::CMS::PageFilter
75
+
76
+
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ostruct'
4
+ require 'configurability'
5
+
6
+ require 'strelka/cms/pagecatalog'
7
+ require 'strelka/cms/page'
8
+ require 'strelka/cms/pagefilter' unless defined?( Strelka::CMS::PageFilter )
9
+
10
+ require 'inversion'
11
+
12
+ # Generate an index for any pages matching the specified <tt>pattern</tt> with one or
13
+ # more +option+s.
14
+ #
15
+ # <?autoindex «pattern» [option]+ ?>
16
+ #
17
+ # Styles are implemented with templates placed in the data/templates/autoindex/ directory.
18
+ #
19
+ # Examples:
20
+ #
21
+ # <?autoindex *.page ?>
22
+ # <?autoindex *.page with default ?>
23
+ # <?autoindex *.page with dl, sort by date ?>
24
+ # <?autoindex blog/*.page limit 10 ?>
25
+ # <?autoindex /*.page ?>
26
+ # <?autoindex /it/is/*.page ?>
27
+ # <?autoindex /it/is/*.page with filetree ?>
28
+ # <?autoindex /it/**/*.page ?>
29
+ #
30
+ class Strelka::CMS::PageFilter::AutoIndex < Strelka::CMS::PageFilter
31
+
32
+ # PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'
33
+ PI = %r{
34
+ <\?
35
+ autoindex # Instruction Target
36
+ \s+
37
+ (\S+) # glob for pages to link [$1]
38
+ (.*?) # options [$2]
39
+ \s*
40
+ \?>
41
+ }xm
42
+
43
+ # Default style template
44
+ DEFAULT_STYLE = 'default'
45
+
46
+ # Autoindex templates subdirectory
47
+ DEFAULT_TEMPLATE_DIR = Pathname( 'autoindex' )
48
+
49
+ # The comment that's inserted if the target page doesn't know what catalog
50
+ # it belongs to.
51
+ NO_CATALOG_COMMENT = %Q{<!-- AutoIndex skipped: page doesn't have a catalog -->}
52
+
53
+
54
+
55
+ ######
56
+ public
57
+ ######
58
+
59
+ ### Process the given +source+ for <?autoindex ... ?> processing-instructions
60
+ def process( source, page )
61
+ if catalog = page.catalog
62
+ self.log.debug "Processing autoindex directives."
63
+ source.gsub!( PI ) do |match|
64
+ self.log.debug " got: %p" % [ match ]
65
+ # Grab the tag values
66
+ pattern = $1
67
+ options = $2
68
+
69
+ self.log.debug "Generating an index for %p relative to %p." % [ pattern, page.path ]
70
+ self.generate_index( pattern, page, catalog, options )
71
+ end
72
+ return source
73
+ else
74
+ self.log.debug "Not generating autoindex sections: no catalog"
75
+ return source.gsub( PI, NO_CATALOG_COMMENT )
76
+ end
77
+ end
78
+
79
+
80
+ ### Create an HTML fragment.
81
+ def generate_index( pattern, page, catalog, options )
82
+ options = self.parse_options( options )
83
+ style = options.style || DEFAULT_STYLE
84
+
85
+ pages = if pattern.start_with?( '/' )
86
+ self.log.debug " absolute pattern; matching relative to catalog basedir %p" %
87
+ [ catalog.basedir.to_s ]
88
+ catalog.matching_pattern( pattern ).to_a
89
+ else
90
+ self.log.debug " relative pattern; matching relative to page directory %p" %
91
+ [ page.path.dirname.to_s ]
92
+ catalog.relative_to( page.path.dirname ).matching_pattern( pattern ).to_a
93
+ end
94
+ pages = self.sort_pages( pages, options ) if options.sortkey
95
+
96
+ style = style + '.tmpl' unless style =~ /\.tmpl$/
97
+ style = DEFAULT_TEMPLATE_DIR + style
98
+
99
+ self.log.debug " generating an HTML index fragment for %d pages under %s" %
100
+ [ pages.length, pattern ]
101
+ template = Inversion::Template.load( style )
102
+
103
+ template.pages = pages
104
+ template.source_page = page
105
+ template.catalog = catalog
106
+ template.list_options = options
107
+
108
+ return template.to_s
109
+ end
110
+
111
+
112
+ #########
113
+ protected
114
+ #########
115
+
116
+ ### Parse the options to the PI, returning the results as an OpenStruct.
117
+ def parse_options( optstring )
118
+ opts = OpenStruct.new
119
+
120
+ opts.limit = Integer($1) if optstring =~ /\blimit\s+(\d+)/i
121
+ opts.style = $1.untaint if optstring =~ /\bwith\s+([a-zA-Z0-9.\/\-]+)/i
122
+ opts.sortkey = $1.untaint if optstring =~ /\bsort\s+by\s+(\w+)/i
123
+
124
+ return opts
125
+ end
126
+
127
+
128
+ ### Return the given +pages+ sorted according to the specified +options+.
129
+ def sort_pages( pages, options )
130
+ case options.sortkey
131
+ when 'date'
132
+ return pages.sort_by {|page| page.path.mtime }.reverse
133
+ when 'title'
134
+ return pages.sort_by {|page| page.title }
135
+ else
136
+ self.log.error "Don't know how to sort the index by %p" % [ options.sortkey ]
137
+ end
138
+ end
139
+
140
+ end # class Strelka::CMS::Page::AutoIndexFilter
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rbconfig'
4
+ require 'tidy'
5
+ require 'nokogiri'
6
+
7
+ require 'strelka/cms/pagefilter' unless defined?( Strelka::CMS::PageFilter )
8
+
9
+ # A filter that cleans up broken HTML using libtidy.
10
+ class Strelka::CMS::PageFilter::Cleanup < Strelka::CMS::PageFilter
11
+
12
+ # Options to pass to libtidy
13
+ TIDY_OPTIONS = {
14
+ :show_warnings => true,
15
+ :indent => true,
16
+ :indent_attributes => false,
17
+ :indent_spaces => 4,
18
+ :vertical_space => true,
19
+ :tab_size => 4,
20
+ :wrap_attributes => true,
21
+ :wrap => 100,
22
+ :output_xhtml => true,
23
+ :char_encoding => 'utf8'
24
+ }
25
+
26
+
27
+ ### Process the +page+'s source with the filter and return the altered content.
28
+ def process( source, page )
29
+ Tidy.open( TIDY_OPTIONS ) do |tidy|
30
+ xml = tidy.clean( source )
31
+
32
+ errors = tidy.errors
33
+ self.log.error( errors.join('; ') ) unless errors.empty?
34
+
35
+ return xml
36
+ end
37
+ end
38
+
39
+ end # class Strelka::CMS::Page::CleanupFilter
40
+
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'digest/md5'
4
+
5
+ require 'strelka/cms/pagefilter' unless defined?( Strelka::CMS::PageFilter )
6
+
7
+ # A class for embedding editorial remarks in a page.
8
+ class Strelka::CMS::PageFilter::Editorial < Strelka::CMS::PageFilter
9
+
10
+ # PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'
11
+ LinkPI = %r{
12
+ <\?
13
+ ed # Instruction Target
14
+ \s+
15
+ (\w+?) # type of editorial mark [$1]
16
+ :? # optional colon
17
+ "
18
+ (.*?) # content that should be edited [$2]
19
+ "
20
+ \s*
21
+ \?>
22
+ }x
23
+
24
+ # Tooltip template
25
+ TOOLTIP_TEMPLATE = %{<div class="tooltip ed %s-ed"><h3>%s</h3><p>%s</p></div>}
26
+
27
+
28
+ ######
29
+ public
30
+ ######
31
+
32
+ ### Process the given +source+ for <?ed ... ?> processing-instructions
33
+ def process( source, page )
34
+ return source.gsub( LinkPI ) do |match|
35
+ # Grab the tag values
36
+ mark_type = $1
37
+ content = $2
38
+
39
+ self.generate_mark( page, mark_type, content )
40
+ end
41
+ end
42
+
43
+
44
+ ### Create an HTML fragment from the parsed LinkPI.
45
+ def generate_mark( current_page, mark_type, content )
46
+ id = Digest::MD5.hexdigest( content )
47
+
48
+ edmark = %{<span class="edmark %s-edmark">(ed.)</span>} % [ mark_type ]
49
+ tooltip = TOOLTIP_TEMPLATE % [ mark_type, mark_type.upcase, content ]
50
+
51
+ return edmark + "\n" + tooltip + "\n"
52
+ end
53
+
54
+
55
+ end # class Strelka::CMS::Page::EditorialFilter
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'strscan'
5
+ require 'yaml'
6
+ require 'rcodetools/xmpfilter'
7
+ require 'tmpdir'
8
+ require 'erb'
9
+
10
+ require 'strelka/cms/page'
11
+ require 'strelka/cms/pagefilter' unless defined?( Strelka::CMS::PageFilter )
12
+
13
+ ### A filter for inline example code or command-line sessions -- does
14
+ ### syntax-highlighting (via CodeRay), syntax-checking for some languages, and
15
+ ### captioning.
16
+ ###
17
+ ### Examples are enclosed in XML processing instructions like so:
18
+ ###
19
+ ### <?example {language: ruby, testable: true, caption: "A fine example"} ?>
20
+ ### a = 1
21
+ ### puts a
22
+ ### <?end example ?>
23
+ ###
24
+ ### This will be pulled out into a preformatted section in the HTML,
25
+ ### highlighted as Ruby source, checked for valid syntax, and annotated with
26
+ ### the specified caption. Valid keys in the example PI are:
27
+ ###
28
+ ### language::
29
+ ### Specifies which (machine) language the example is in.
30
+ ### testable::
31
+ ### If set and there is a testing function for the given language, run it and append
32
+ ### any errors to the output.
33
+ ### caption::
34
+ ### A small blurb to put below the pulled-out example in the HTML.
35
+ class Strelka::CMS::PageFilter::Example < Strelka::CMS::PageFilter
36
+
37
+ DEFAULTS = {
38
+ :language => :shell,
39
+ :line_numbers => :inline,
40
+ :tab_width => 4,
41
+ :hint => :debug,
42
+ :testable => false,
43
+ }
44
+
45
+ # PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'
46
+ EXAMPLE_PI = %r{
47
+ <\?
48
+ example # Instruction Target
49
+ (?: # Optional instruction body
50
+ \s+
51
+ ((?: # [$1]
52
+ [^?]* # Run of anything but a question mark
53
+ | # -or-
54
+ \?(?!>) # question mark not followed by a closing angle bracket
55
+ )*)
56
+ )?
57
+ \?>
58
+ }x
59
+
60
+ END_PI = %r{ <\? end (?: \s+ example )? \s* \?> }x
61
+
62
+
63
+ ######
64
+ public
65
+ ######
66
+
67
+ ### Process the given +source+ for <?example ... ?> processing-instructions, calling out
68
+ def process( source, page )
69
+ scanner = StringScanner.new( source )
70
+
71
+ buffer = ''
72
+ until scanner.eos?
73
+ startpos = scanner.pos
74
+
75
+ # If we find an example
76
+ if scanner.skip_until( EXAMPLE_PI )
77
+ contents = ''
78
+
79
+ # Append the interstitial content to the buffer
80
+ if ( scanner.pos - startpos > scanner.matched.length )
81
+ offset = scanner.pos - scanner.matched.length - 1
82
+ buffer << scanner.string[ startpos..offset ]
83
+ end
84
+
85
+ # Append everything up to it to the buffer and save the contents of
86
+ # the tag
87
+ params = scanner[1]
88
+
89
+ # Now find the end of the example or complain
90
+ contentpos = scanner.pos
91
+ scanner.skip_until( END_PI ) or
92
+ raise "Unterminated example at line %d" %
93
+ [ scanner.string[0..scanner.pos].count("\n") ]
94
+
95
+ # Now build the example and append to the buffer
96
+ if ( scanner.pos - contentpos > scanner.matched.length )
97
+ offset = scanner.pos - scanner.matched.length - 1
98
+ contents = scanner.string[ contentpos..offset ]
99
+ end
100
+
101
+ self.log.debug "Processing with params: %p, contents: %p" % [ params, contents ]
102
+ buffer << self.process_example( params, contents, page )
103
+ else
104
+ break
105
+ end
106
+
107
+ end
108
+ buffer << scanner.rest
109
+ scanner.terminate
110
+
111
+ return buffer
112
+ end
113
+
114
+
115
+ ### Filter out 'example' macros, doing syntax highlighting, and running
116
+ ### 'testable' examples through a validation process appropriate to the
117
+ ### language the example is in.
118
+ def process_example( params, body, page )
119
+ options = self.parse_options( params )
120
+ caption = options.delete( :caption )
121
+ content = ''
122
+ lang = options.delete( :language ).to_s
123
+ self.log.debug "Processing a %p example..." % [ lang ]
124
+
125
+ # Test it if it's testable
126
+ if options[:testable]
127
+ content = test_content( body, lang, page )
128
+ else
129
+ content = body
130
+ end
131
+
132
+ # If the language is something that can itself include PIs, look for the
133
+ # special '<??end ?>' token and strip the extra '?'
134
+ if %w[xml html textile xhtml].include?( lang )
135
+ content.gsub!( /<\?\?(.*?)\?>/ ) do
136
+ tag_content = $1
137
+ self.log.debug "Unescaping escaped PI %p in example." % [ tag_content ]
138
+ "<?#{tag_content}?>"
139
+ end
140
+
141
+ self.log.debug " example is now: %p" % [ content ]
142
+ end
143
+
144
+ # Strip trailing blank lines and syntax-highlight
145
+ content = highlight( content.strip, lang )
146
+ caption = %{<div class="caption">} + caption.to_s + %{</div>} if caption
147
+
148
+ return %{<notextile><div class="example %s-example">%s%s</div></notextile>} %
149
+ [lang, content, caption || '']
150
+ end
151
+
152
+
153
+ ### Parse an options hash for filtering from the given +args+, which can either
154
+ ### be a plain String, in which case it is assumed to be the name of the language the example
155
+ ### is in, or a Hash of configuration options.
156
+ def parse_options( args )
157
+ args = "{ #{args} }" unless args && args.strip[0] == ?{
158
+ args = YAML.load( args )
159
+
160
+ # Convert to Symbol keys and value
161
+ args.keys.each do |k|
162
+ newval = args.delete( k )
163
+ next if newval.nil? || (newval.respond_to?(:size) && newval.size == 0)
164
+ args[ k.to_sym ] = newval.respond_to?( :to_sym ) ? newval.to_sym : newval
165
+ end
166
+ return DEFAULTS.merge( args )
167
+ end
168
+
169
+
170
+ ### Test the given +content+ with a rule specific to the given +language+.
171
+ def test_content( body, language, page )
172
+ self.log.debug "Running a testable %p example..." % [ language ]
173
+ case language.to_sym
174
+ when :ruby
175
+ return self.test_ruby_content( body, page )
176
+
177
+ when :yaml
178
+ return self.test_yaml_content( body, page )
179
+
180
+ else
181
+ self.log.error "...oops, I don't know how to test %p examples." % [ language ]
182
+ return body
183
+ end
184
+ end
185
+
186
+
187
+ ### Test the specified Ruby content for valid syntax
188
+ def test_ruby_content( source, page )
189
+ # $stderr.puts "Testing ruby content..."
190
+ libdir = Pathname.new( __FILE__ ).dirname.parent.parent.parent + 'lib'
191
+ extdir = Pathname.new( __FILE__ ).dirname.parent.parent.parent + 'ext'
192
+
193
+ options = Rcodetools::XMPFilter::INITIALIZE_OPTS.dup
194
+ options[:include_paths] |= [ libdir.to_s, extdir.to_s ]
195
+ options[:width] = 80
196
+
197
+ if page.config['example_prelude']
198
+ prelude = page.config['example_prelude']
199
+ self.log.debug " prepending prelude:\n#{prelude}"
200
+ source = prelude.strip + "\n\n" + source.strip
201
+ else
202
+ self.log.debug " no prelude; page config is: %p" % [ page.config ]
203
+ end
204
+
205
+ rval = Rcodetools::XMPFilter.run( source, options )
206
+
207
+ self.log.debug "test output: %p" % [ rval ]
208
+ return rval.join
209
+ rescue Exception => err
210
+ return "%s while testing: %s\n %s" %
211
+ [ err.class.name, err.message, err.backtrace.join("\n ") ]
212
+ end
213
+
214
+
215
+ ### Test the specified YAML content for valid syntax
216
+ def test_yaml_content( source, metadata )
217
+ YAML.load( source )
218
+ rescue YAML::Error => err
219
+ return "# Invalid YAML: " + err.message + "\n" + source
220
+ else
221
+ return source
222
+ end
223
+
224
+
225
+ ### Highlights the given +content+ in language +lang+.
226
+ def highlight( content, lang )
227
+ source = ERB::Util.html_escape( content )
228
+ return %Q{\n\n<pre class="brush: #{lang}">#{source}</pre>\n\n}
229
+ end
230
+
231
+ end # class Strelka::CMS::PageFilter::Example
232
+