module Bookshelf
class Syntax
autoload :Highlight, "bookshelf/syntax/highlight"
attr_reader :io
attr_reader :lines
attr_reader :book_dir
attr_reader :format
# Render syntax blocks from specified source code.
#
# dir = Pathname.new(File.dirname(__FILE__))
# text = File.read(dir.join("text/some_file.textile"))
# Bookshelf::Syntax.render(dir, :textile, text)
#
def self.render(book_dir, format, source_code, raw = false)
source_code.gsub(/@@@(.*?)@@@/m) do |match|
new(book_dir, format, $1, raw).process
end
end
# Process each syntax block individually.
#
def initialize(book_dir, format, code, raw = false)
@format = format
@book_dir = book_dir
@io = StringIO.new(code)
@lines = io.readlines.collect(&:chomp)
@language = 'text' if raw
end
# Return unprocessed line codes.
#
def raw
lines[1..-1].join("\n")
end
# Return meta data from syntax annotation.
#
def meta
@meta ||= begin
line = lines.first.squish
_, language, file, modifier, reference = *line.match(/^([^ ]+)(?: ([^:#]+)(?:(:|#)(.*?))?)?$/)
if modifier == "#"
type = :block
elsif modifier == ":"
type = :range
elsif file
type = :file
else
type = :inline
end
{
:language => language,
:file => file,
:type => type,
:reference => reference
}
end
end
# Process syntax block, returning a +pre+ HTML tag.
#
def process
code = raw.to_s.strip_heredoc
code = process_file.gsub(/\n^.*?@(begin|end):.*?$/, "") if meta[:file]
code = Highlight.apply(code, language)
# escape for textile
code = %[#{code}] if format == :textile
code
end
private
# Process line range as in @@@ ruby some_file.rb:15,20 @@@.
#
def process_range(code)
starts, ends = meta[:reference].split(",").collect(&:to_i)
code = StringIO.new(code).readlines[starts-1..ends-1].join("\n").strip_heredoc.chomp
end
# Process block name as in @@@ ruby some_file.rb#some_block @@@.
#
def process_block(code)
code.gsub!(/\r\n/, "\n")
re = %r[@begin: *\b(#{meta[:reference]})\b *[^\n]*\n(.*?)\n[^\n]*@end: \1]im
if code.match(re)
$2.strip_heredoc
else
"[missing '#{meta[:reference]}' block name]"
end
end
# Process file and its relatives.
#
def process_file
file_path = book_dir.join("code/#{meta[:file]}")
if File.exist?(file_path)
code = File.read(file_path)
if meta[:type] == :range
process_range(code)
elsif meta[:type] == :block
process_block(code)
else
code
end
else
"[missing 'code/#{meta[:file]}' file]"
end
end
# Return the language used for this syntax block. Overrideable
# for epub generation.
def language
@language || meta[:language]
end
end
end