strelka-cms 0.0.1.pre.15

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