Class: Strelka::CMS::PageFilter::Example

Inherits:
Strelka::CMS::PageFilter
  • Object
show all
Defined in:
lib/strelka/cms/pagefilter/example.rb

Overview

A filter for inline example code or command-line sessions – does syntax-highlighting (via CodeRay), syntax-checking for some languages, and captioning.

Examples are enclosed in XML processing instructions like so:

<?example {language: ruby, testable: true, caption: "A fine example"} ?>
   a = 1
   puts a
<?end example ?>

This will be pulled out into a preformatted section in the HTML, highlighted as Ruby source, checked for valid syntax, and annotated with the specified caption. Valid keys in the example PI are:

language

Specifies which (machine) language the example is in.

testable

If set and there is a testing function for the given language, run it and append any errors to the output.

caption

A small blurb to put below the pulled-out example in the HTML.

Constant Summary collapse

DEFAULTS =
{
	:language     => :shell,
	:line_numbers => :inline,
	:tab_width    => 4,
	:hint         => :debug,
	:testable     => false,
}
EXAMPLE_PI =

PI ::= ‘<?’ PITarget (S (Char* - (Char* ‘?>’ Char*)))? ‘?>’

%r{
		<\?
example				# Instruction Target
(?:					# Optional instruction body
\s+
((?:				# [$1]
	[^?]*			# Run of anything but a question mark
	|				# -or-
	\?(?!>)			# question mark not followed by a closing angle bracket
)*)
)?
		\?>
}x
END_PI =
%r{ <\? end (?: \s+ example )? \s* \?> }x

Instance Method Summary collapse

Instance Method Details

#highlight(content, lang) ⇒ Object

Highlights the given content in language lang.



226
227
228
229
# File 'lib/strelka/cms/pagefilter/example.rb', line 226

def highlight( content, lang )
	source = ERB::Util.html_escape( content )
	return %Q{\n\n<pre><code class="#{lang}">#{source}</code></pre>\n\n}
end

#parse_options(args) ⇒ Object

Parse an options hash for filtering from the given args, which can either be a plain String, in which case it is assumed to be the name of the language the example is in, or a Hash of configuration options.



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/strelka/cms/pagefilter/example.rb', line 156

def parse_options( args )
	args = "{ #{args} }" unless args && args.strip[0] == ?{
	args = YAML.load( args )

	# Convert to Symbol keys and value
	args.keys.each do |k|
		newval = args.delete( k )
		next if newval.nil? || (newval.respond_to?(:size) && newval.size == 0)
		args[ k.to_sym ] = newval.respond_to?( :to_sym ) ? newval.to_sym : newval
	end
	return DEFAULTS.merge( args )
end

#process(source, page) ⇒ Object

Process the given source for <?example … ?> processing-instructions, calling out



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/strelka/cms/pagefilter/example.rb', line 68

def process( source, page )
	scanner = StringScanner.new( source )

	buffer = ''
	until scanner.eos?
		startpos = scanner.pos

		# If we find an example
		if scanner.skip_until( EXAMPLE_PI )
			contents = ''

			# Append the interstitial content to the buffer
			if ( scanner.pos - startpos > scanner.matched.length )
				offset = scanner.pos - scanner.matched.length - 1
				buffer << scanner.string[ startpos..offset ]
			end

			# Append everything up to it to the buffer and save the contents of
			# the tag
			params = scanner[1]

			# Now find the end of the example or complain
			contentpos = scanner.pos
			scanner.skip_until( END_PI ) or
				raise "Unterminated example at line %d" %
					[ scanner.string[0..scanner.pos].count("\n") ]

			# Now build the example and append to the buffer
			if ( scanner.pos - contentpos > scanner.matched.length )
				offset = scanner.pos - scanner.matched.length - 1
				contents = scanner.string[ contentpos..offset ]
			end

			self.log.debug "Processing with params: %p, contents: %p" % [ params, contents ]
			buffer << self.process_example( params, contents, page )
		else
			break
		end

	end
	buffer << scanner.rest
	scanner.terminate

	return buffer
end

#process_example(params, body, page) ⇒ Object

Filter out ‘example’ macros, doing syntax highlighting, and running ‘testable’ examples through a validation process appropriate to the language the example is in.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/strelka/cms/pagefilter/example.rb', line 118

def process_example( params, body, page )
	options = self.parse_options( params )
	caption = options.delete( :caption )
	content = ''
	lang = options.delete( :language ).to_s
	self.log.debug "Processing a %p example..." % [ lang ]

	# Test it if it's testable
	if options[:testable]
		content = test_content( body, lang, page )
	else
		content = body
	end

	# If the language is something that can itself include PIs, look for the
	# special '<??end ?>' token and strip the extra '?'
	if %w[xml html textile xhtml].include?( lang )
		content.gsub!( /<\?\?(.*?)\?>/ ) do
			tag_content = $1
			self.log.debug "Unescaping escaped PI %p in example." % [ tag_content ]
			"<?#{tag_content}?>"
		end

		self.log.debug "  example is now: %p" % [ content ]
	end

	# Strip trailing blank lines and syntax-highlight
	content = highlight( content.strip, lang )
	caption = %{<div class="caption">} + caption.to_s + %{</div>} if caption

	return %{<notextile><div class="example %s-example">%s%s</div></notextile>} %
		[lang, content, caption || '']
end

#test_content(body, language, page) ⇒ Object

Test the given content with a rule specific to the given language.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/strelka/cms/pagefilter/example.rb', line 171

def test_content( body, language, page )
	self.log.debug "Running a testable %p example..." % [ language ]
	case language.to_sym
	when :ruby
		return self.test_ruby_content( body, page )

	when :yaml
		return self.test_yaml_content( body, page )

	else
		self.log.error "...oops, I don't know how to test %p examples." % [ language ]
		return body
	end
end

#test_ruby_content(source, page) ⇒ Object

Test the specified Ruby content for valid syntax



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/strelka/cms/pagefilter/example.rb', line 188

def test_ruby_content( source, page )
	# $stderr.puts "Testing ruby content..."
	libdir = Pathname.new( __FILE__ ).dirname.parent.parent.parent + 'lib'
	extdir = Pathname.new( __FILE__ ).dirname.parent.parent.parent + 'ext'

	options = Rcodetools::XMPFilter::INITIALIZE_OPTS.dup
	options[:include_paths] |= [ libdir.to_s, extdir.to_s ]
	options[:width] = 80

	if page.config['example_prelude']
		prelude = page.config['example_prelude']
		self.log.debug "  prepending prelude:\n#{prelude}"
		source = prelude.strip + "\n\n" + source.strip
	else
		self.log.debug "  no prelude; page config is: %p" % [ page.config ]
	end

	rval = Rcodetools::XMPFilter.run( source, options )

	self.log.debug "test output: %p" % [ rval ]
	return rval.join
rescue Exception => err
	return "%s while testing: %s\n  %s" %
		[ err.class.name, err.message, err.backtrace.join("\n  ") ]
end

#test_yaml_content(source, metadata) ⇒ Object

Test the specified YAML content for valid syntax



216
217
218
219
220
221
222
# File 'lib/strelka/cms/pagefilter/example.rb', line 216

def test_yaml_content( source,  )
	YAML.load( source )
rescue YAML::Error => err
	return "# Invalid YAML: " + err.message + "\n" + source
else
	return source
end