strelka-cms 0.0.1.pre.15
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|