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.
- data.tar.gz.sig +0 -0
- data/.gemtest +0 -0
- data/ChangeLog +33 -0
- data/History.rdoc +4 -0
- data/Manifest.txt +52 -0
- data/Procfile +4 -0
- data/README.rdoc +74 -0
- data/Rakefile +68 -0
- data/data/strelka-cms/apps/content-feeds +194 -0
- data/data/strelka-cms/apps/content-manager +159 -0
- data/data/strelka-cms/templates/autoindex/default.tmpl +11 -0
- data/data/strelka-cms/templates/layout.tmpl +58 -0
- data/data/strelka-cms/templates/page.tmpl +21 -0
- data/etc/mongrel.rb +34 -0
- data/lib/strelka/cms.rb +42 -0
- data/lib/strelka/cms/page.rb +322 -0
- data/lib/strelka/cms/pagecatalog.rb +121 -0
- data/lib/strelka/cms/pagefilter.rb +76 -0
- data/lib/strelka/cms/pagefilter/autoindex.rb +140 -0
- data/lib/strelka/cms/pagefilter/cleanup.rb +40 -0
- data/lib/strelka/cms/pagefilter/editorial.rb +55 -0
- data/lib/strelka/cms/pagefilter/example.rb +232 -0
- data/lib/strelka/cms/pagefilter/strip.rb +30 -0
- data/lib/strelka/cms/pagefilter/textile.rb +20 -0
- data/public/humans.txt +8 -0
- data/public/images/misc/button-gloss.png +0 -0
- data/public/images/misc/button-overlay.png +0 -0
- data/public/images/misc/custom-form-sprites.png +0 -0
- data/public/images/misc/input-bg-outset.png +0 -0
- data/public/images/misc/input-bg.png +0 -0
- data/public/images/misc/modal-gloss.png +0 -0
- data/public/images/misc/table-sorter.png +0 -0
- data/public/images/orbit/bullets.jpg +0 -0
- data/public/images/orbit/left-arrow.png +0 -0
- data/public/images/orbit/loading.gif +0 -0
- data/public/images/orbit/mask-black.png +0 -0
- data/public/images/orbit/pause-black.png +0 -0
- data/public/images/orbit/right-arrow.png +0 -0
- data/public/images/orbit/rotator-black.png +0 -0
- data/public/images/orbit/timer-black.png +0 -0
- data/public/index.page +46 -0
- data/public/javascripts/app.js +98 -0
- data/public/javascripts/foundation.js +12 -0
- data/public/javascripts/jquery.min.js +4 -0
- data/public/javascripts/modernizr.foundation.js +4 -0
- data/public/robots.txt +4 -0
- data/public/stylesheets/app.css +30 -0
- data/public/stylesheets/foundation.css +1424 -0
- data/public/stylesheets/ie.css +13 -0
- data/spec/data/test.page +22 -0
- data/spec/lib/helpers.rb +58 -0
- data/spec/strelka/cms/page_spec.rb +139 -0
- data/spec/strelka/cms/pagecatalog_spec.rb +94 -0
- data/spec/strelka/cms_spec.rb +27 -0
- metadata +346 -0
- 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
|
+
|