conversio 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md ADDED
@@ -0,0 +1,8 @@
1
+ ## 0.1.1
2
+
3
+ * finishing Kramdown support
4
+ * adding a couple of templates shipped with the gem
5
+
6
+ ## 0.1.0
7
+
8
+ * First release
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ Conversio by Jörg Behrendt and Victor Penso
2
+
3
+ # Description
4
+
5
+ Renders plain text files with [Markdown][1] syntax to XHTML pages.
6
+ User can define their own Ruby ERB templates to customize the
7
+ XHTML page generation. Also the creation of a table of content
8
+ using the HTML header elements (like `<h1>`) and the syntax
9
+ high-lighting of code snippets is supported.
10
+
11
+ ## Installation
12
+
13
+ Conversio RubyGem:
14
+
15
+ gem install conversio
16
+
17
+ Syntax high-lighting is done with Pyhton [Pygments][2]:
18
+
19
+ easy_install pygments
20
+
21
+ ## Usage Examples
22
+
23
+ Take a look to the help text:
24
+
25
+ conversio -h
26
+
27
+ Convert all files called `*.markdown` inside a defined directory
28
+ and all sub-directories into HTML and store the in the destination
29
+ directory.
30
+
31
+ conversio ~/docs/path/to/files ~/public/path
32
+
33
+ Create a single `readme.html` file including a table of content by
34
+ using the 'dark' template:
35
+
36
+ conversio -t -p dark readme.markdown
37
+
38
+ ## License
39
+
40
+ GPLv3 - see the COPYING file.
41
+
42
+ [1]: http://daringfireball.net/projects/markdown/
43
+ [2]: http://pygments.org/
data/bin/conversio ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $DEBUG = false
4
+
5
+ require 'rubygems'
6
+ require 'erb'
7
+ require 'yaml'
8
+ require 'fileutils'
9
+ require 'pathname'
10
+ require 'getoptlong'
11
+ require 'ostruct'
12
+ require 'conversio'
13
+
14
+ include Conversio
15
+
16
+ help = <<EOF
17
+ Synopsis
18
+ --------
19
+
20
+ #{File.split(__FILE__)[-1]}: Renders Markdown plain text files to HTML
21
+
22
+ Purpose
23
+ -------
24
+
25
+ Uses Ruby ERB Templates to generate XHTML documents rendered from Markdown
26
+ plain text files.
27
+
28
+ Usage
29
+ -----
30
+
31
+ #{File.split(__FILE__)[-1]} [OPTIONS] SRC [DST]
32
+
33
+ SRC: File or directory containing the Markdown formated plain text
34
+ DST: Target directory for the XHTML output.
35
+
36
+ Options
37
+ -------
38
+
39
+ --config:
40
+
41
+ Creates a personal configuration file in ~/.conversiorc
42
+
43
+ -c, --colorize:
44
+
45
+ Enable syntax high-lighting for marked code blocks.
46
+
47
+ -e, --engine:
48
+
49
+ Select the Markdown parser to be used:
50
+ * 'bluecloth' (default)
51
+ * 'kramdown'
52
+
53
+ -f, --template-file FILE:
54
+
55
+ FILE containing an ERB template with:
56
+ * '<%= content %>' to mark the postion inside the body tag
57
+ to place the passed in content.
58
+ * '<%= style %>' to mark the position for placing CSS.
59
+
60
+ -h, --help:
61
+
62
+ Show this help documentation.
63
+
64
+ -i, --ignore-config:
65
+
66
+ Don't read the configuration from ~/.conversiorc
67
+
68
+ -l, --list-templates:
69
+
70
+ Show a list of all available templates.
71
+
72
+ -t, --table-of-content:
73
+
74
+ Enables the creation of a table of content.
75
+
76
+ -p, --template NAME:
77
+
78
+ Select a specific template to be used.
79
+
80
+ -v, --verbose:
81
+
82
+ Print more verbose output.
83
+ EOF
84
+
85
+ config = <<EOF
86
+ colorize: true
87
+ table_of_content: true
88
+ template_file:
89
+ engine:
90
+ EOF
91
+
92
+ # -------------------------------------------------------------
93
+ # helper functions
94
+ # -------------------------------------------------------------
95
+
96
+ def ask?(*question)
97
+ print question.join(' '), '? (y/n) > '
98
+ return true if STDIN.gets.chomp.downcase.eql?('y')
99
+ return false
100
+ end
101
+
102
+ def overwrite?(*str)
103
+ return ask?('Overwrite',str)
104
+ end
105
+
106
+
107
+ # -------------------------------------------------------------
108
+ # class extensions
109
+ # -------------------------------------------------------------
110
+
111
+ class Array
112
+ def resolv_path
113
+ Hash[ *self.collect { |e| [e,e.gsub(/.markdown/,'.html') ] }.flatten ]
114
+ end
115
+ end
116
+
117
+ # -------------------------------------------------------------
118
+ # main program
119
+ # -------------------------------------------------------------
120
+ begin
121
+
122
+ user_config = "#{ENV['HOME']}/.conversiorc"
123
+
124
+
125
+ # defaults for program arguments
126
+ options = OpenStruct.new
127
+ options.ignore_config = false
128
+ options.colorize = false
129
+ options.engine = nil
130
+ options.template_file = nil
131
+ options.table_of_content = false
132
+ options.verbose = false
133
+ options.template = 'default'
134
+
135
+ # list of user options
136
+ opts = GetoptLong.new(
137
+ [ '--colorize', '-c', GetoptLong::NO_ARGUMENT],
138
+ [ '--config', GetoptLong::NO_ARGUMENT],
139
+ [ '--engine', '-e', GetoptLong::OPTIONAL_ARGUMENT],
140
+ [ '--ignore-config', '-i', GetoptLong::NO_ARGUMENT],
141
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
142
+ [ '--list-templates', '-l', GetoptLong::NO_ARGUMENT ],
143
+ [ '--table-of-content', '-t', GetoptLong::NO_ARGUMENT],
144
+ [ '--template-file', '-f', GetoptLong::OPTIONAL_ARGUMENT ],
145
+ [ '--template', '-p', GetoptLong::OPTIONAL_ARGUMENT ],
146
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT]
147
+ )
148
+
149
+ # parse the options from the command line
150
+ opts.each do |opt, arg|
151
+ case opt
152
+ when '--colorize'
153
+ options.colorize = true
154
+ when '--config'
155
+ open(user_config,'w') { |f| f.write config } if overwrite? user_config
156
+ exit 0
157
+ when '--engine'
158
+ options.engine = arg
159
+ when '--ignore-config'
160
+ options.ignore_config = true
161
+ when '--help'
162
+ puts help
163
+ exit 0
164
+ when '--list-templates'
165
+ Template.constants.each { |tpl| puts tpl.downcase }
166
+ exit 0
167
+ when '--table-of-content'
168
+ options.table_of_content = true
169
+ when '--template-file'
170
+ options.template_file = arg
171
+ when '--template'
172
+ options.template = arg
173
+ when '--verbose'
174
+ options.verbose = true
175
+ end
176
+ end
177
+
178
+
179
+ # get the input source
180
+ src = ARGV[0] || raise("no input defined")
181
+ dst = ARGV[1] # optional parameter!
182
+
183
+ # read the default configuration of the user
184
+ if not options.ignore_config and File.exists? user_config then
185
+ STDERR.puts "Reading configuration file: #{user_config}" if options.verbose
186
+ defaults = YAML.load_file user_config
187
+ # command-line arguments have precedents before the suer configuration
188
+ options.colorize = defaults['colorize']
189
+ options.engine = defaults['engine'] if options.engine.nil?
190
+ options.table_of_content = defaults['table_of_content']
191
+ options.template_file = defaults['template_file'] if options.template_file.nil?
192
+ end
193
+
194
+ STDERR.print 'Configuration: ', options, "\n" if options.verbose
195
+
196
+ template = String.new
197
+ # user the default ERB template if the user hasn't defined its own
198
+ if options.template_file.nil? then
199
+ STDERR.print 'Using ERB template: ', options.template, "\n" if options.verbose
200
+ template = Template.const_get(options.template.upcase)
201
+ else
202
+ STDERR.print 'Using ERB template: ', options.template_file, "\n" if options.verbose
203
+ options.template_file = File.expand_path(options.template_file)
204
+ if File.exists? options.template_file
205
+ template = File.read options.template_file
206
+ else
207
+ raise("The specified ERB templates is not existing!")
208
+ end
209
+ end
210
+
211
+ # setup the converter object
212
+ converter = Converter.new(template)
213
+ converter.load_parser(options.engine) unless options.engine.nil?
214
+ converter.color = true if options.colorize
215
+ converter.table_of_content = true if options.table_of_content
216
+
217
+ # get all the input files
218
+ input_files = Array.new
219
+ if File.directory?(src) then
220
+ input_files = Dir["#{src}/**/*.markdown"]
221
+ else
222
+ file = File.expand_path(src)
223
+ input_files << file
224
+ src = File.dirname(file)
225
+ end
226
+ src_dst_pairs = input_files.resolv_path
227
+ # fix the destination path if needed
228
+ unless dst.nil? then
229
+ src_dst_pairs.each_pair do |src_path,dst_path|
230
+ src_dst_pairs[src_path] = dst_path.gsub(/#{src}/,dst)
231
+ end
232
+ end
233
+ # render the XHTML docs
234
+ STDERR.puts 'Created files:' if options.verbose
235
+ src_dst_pairs.each_pair do |s,d|
236
+ converter.markdown_to_xhtml(s,d)
237
+ STDERR.print ' ', d, "\n" if options.verbose
238
+ end
239
+
240
+ exit 0
241
+
242
+ rescue => exc
243
+ STDERR.puts "ERROR: #{exc.message}"
244
+ STDERR.puts " use -h for detailed instructions"
245
+ exit 1
246
+ end
247
+
data/conversio.gemspec ADDED
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+
5
+ s.name = %q{conversio}
6
+ s.version = "0.1.1"
7
+
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
9
+ s.authors = ["Jörg Behrendt","Victor Penso"]
10
+ s.date = %q{2010-11-19}
11
+ s.default_executable = %q{conversio}
12
+ s.homepage = 'https://github.com/vpenso/conversio'
13
+
14
+ s.description = <<-EOF
15
+ Renders plain text files with Markdown syntax to XHTML pages.
16
+ User can define their own Ruby ERB templates to customize the
17
+ XHTML page generation. Also the creation of a table of content
18
+ using the HTML header elements (like `<h1>`) and the syntax
19
+ high-lighting of code snippets is supported.
20
+ EOF
21
+
22
+ s.email = %q{v.penso@gsi.de}
23
+ s.executables = ["conversio"]
24
+ s.extra_rdoc_files = [
25
+ "bin/conversio",
26
+ "lib/conversio.rb",
27
+ "lib/conversio/converter.rb",
28
+ "lib/conversio/htmltoc.rb",
29
+ "lib/conversio/pygmentizer.rb"
30
+ ]
31
+ s.files = [
32
+ "README.md",
33
+ "HISTORY.md",
34
+ "bin/conversio",
35
+ "conversio.gemspec",
36
+ "lib/conversio.rb",
37
+ "lib/conversio/converter.rb",
38
+ "lib/conversio/htmltoc.rb",
39
+ "lib/conversio/pygmentizer.rb",
40
+ "templates/default.erb",
41
+ "templates/no_css.erb",
42
+ "templates/dark.erb",
43
+ "templates/light.erb"
44
+ ]
45
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Conversio"]
46
+ s.require_paths = ["lib"]
47
+ s.rubyforge_project = %q{conversio}
48
+ s.rubygems_version = %q{1.3.6}
49
+ s.summary = %q{Renders Markdown plain text files to HTML}
50
+
51
+ s.add_dependency('bluecloth', '>= 2.0.9')
52
+ s.add_dependency('kramdown')
53
+ s.requirements << 'Pygments (http://pygments.org/)'
54
+ s.licenses = 'GPLv3'
55
+
56
+ end
@@ -0,0 +1,141 @@
1
+ require 'fileutils'
2
+
3
+ module Conversio
4
+
5
+ class Hash
6
+
7
+ def deep_merge(hash)
8
+ target = dup
9
+
10
+ hash.keys.each do |key|
11
+ if hash[key].is_a? Hash and self[key].is_a? Hash
12
+ target[key] = target[key].deep_merge(hash[key])
13
+ next
14
+ end
15
+
16
+ target[key] = hash[key]
17
+ end
18
+
19
+ target
20
+ end
21
+
22
+
23
+ def deep_merge!(second)
24
+ second.each_pair do |k,v|
25
+ if self[k].is_a?(Hash) and second[k].is_a?(Hash)
26
+ self[k].deep_merge!(second[k])
27
+ else
28
+ self[k] = second[k]
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ class Converter
36
+
37
+ attr_accessor :table_of_content, :color
38
+
39
+ def initialize(template)
40
+ @template = template
41
+ @table_of_content = false
42
+ @color = false
43
+ #user_config = "#{ENV['HOME']}/.conversiorc"
44
+ #if File.exists?(user_config)
45
+ # overwrite defaults
46
+ #@meta_data = @meta_data.deep_merge(YAML.load_file(user_config))
47
+ #end
48
+ # Holds the input Markdown plain text
49
+ @source = nil
50
+ # Hold Markdown rendered to HTML
51
+ @content = nil
52
+ # Hold the finished XHTML document
53
+ @result = nil
54
+ # load the default parser
55
+ @parser = 'bluecloth'
56
+ load_parser(@parser)
57
+ end
58
+
59
+ def markdown_to_xhtml(src,dst)
60
+ @source = open(src).readlines.join
61
+ colorize() if @color
62
+ parse()
63
+ generate_table_of_content() if @table_of_content
64
+ render()
65
+ # write the HTML file
66
+ FileUtils::mkdir_p(File.dirname(dst)) unless File.exists?(File.dirname(dst))
67
+ open(dst,'w') { |f| f.write @result }
68
+ end
69
+
70
+ def load_parser(parser)
71
+ begin
72
+ case parser
73
+ when 'bluecloth'
74
+ require 'bluecloth'
75
+ when 'kramdown'
76
+ require 'kramdown'
77
+ else
78
+ raise "Parser '#{parser}' is not a known Markdown library"
79
+ end
80
+ rescue LoadError
81
+ raise "Couldn't load #{parser}."
82
+ end
83
+ @parser = parser
84
+ end
85
+
86
+ private
87
+
88
+ def configure()
89
+ config = nil
90
+ # read the header of the source file
91
+ start = @source.index("|--")
92
+ ende = @source.index("--|")
93
+ #if start != nil and ende != nil then
94
+ if false
95
+ STDERR.puts 'Meta data found in file!' if $DEBUG
96
+ yamlheader = @source[start+3,ende-start-3]
97
+ # overwrite defaults
98
+ config = @meta_data.deep_merge(YAML.load(yamlheader))
99
+ splitted = @source.split('--|',2)
100
+ @source = splitted[1]
101
+ else
102
+ config = @meta_data
103
+ end
104
+ return config
105
+ end
106
+
107
+ def load_template(tpl)
108
+ puts "Loading template : "+tpl.to_s
109
+ raise "Couldn't open ERB template: #{tpl}" unless File.exists?(File.expand_path(tpl))
110
+ return open(File.expand_path(tpl)).readlines.join
111
+ end
112
+
113
+ def parse
114
+ raise "Define source before rendering!" if @source == nil
115
+ case @parser
116
+ when 'bluecloth'
117
+ @content = BlueCloth::new(@source).to_html
118
+ when 'kramdown'
119
+ @content = Kramdown::Document.new(@source).to_html
120
+ else
121
+ puts "Markdown parser #{@parser} not supported yet"
122
+ end
123
+ end
124
+
125
+ def colorize
126
+ @source = Pygmentizer.new.transform_code_blocks(@source)
127
+ end
128
+
129
+ def generate_table_of_content #füge zum geparsten Text ein Inhaltsverzeichnis hinzu -> davor parsen
130
+ raise "No content to generate table of content - Run the parser first!" if @content == nil
131
+ @content = HTMLTableOfContent.new(@content).get_html()
132
+ end
133
+
134
+ def render(values = {})
135
+ values.store(:content, @content)
136
+ @result = ERB.new(@template).result(binding)
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,115 @@
1
+ module Conversio
2
+
3
+ class HTMLTableOfContent
4
+
5
+ attr_accessor :numbering, :table_of_content_div_class
6
+
7
+ def initialize(html_input)
8
+ @numbering = true
9
+ @table_of_content_div_class = 'toc'
10
+ # Variables
11
+ @html_input = Array.new
12
+ @heading_elements = Array.new
13
+ html_input.split("\n").each { |l| @html_input << l }
14
+ scan_for_heading_elements()
15
+ numbers()
16
+ end
17
+
18
+ def get_html_with_anchors()
19
+ inject_anchors()
20
+ return @html_input.join("\n")
21
+ end
22
+
23
+ def get_html_table_of_content()
24
+ output = String.new
25
+ @heading_elements.each do |heading|
26
+ index, level, content, anchor, number = heading
27
+ level = level.to_i
28
+ next if level > 3 # only h1,h2, and h3 tags are used
29
+ space = '&nbsp;&nbsp;'
30
+ case level
31
+ when 2 then output << space
32
+ when 3 then output << space << space
33
+ end
34
+ content = "#{number}&nbsp;#{content}" if numbering?
35
+ output << %{<a href="##{anchor}">#{content}</a><br/>\n}
36
+ end
37
+ return %{<div class="#{@table_of_content_div_class}">\n#{output}</div>\n}
38
+ end
39
+
40
+ def get_html()
41
+ return get_html_table_of_content() << "\n" << get_html_with_anchors
42
+ end
43
+
44
+ protected
45
+
46
+ def numbering?
47
+ return @numbering
48
+ end
49
+
50
+ def scan_for_heading_elements()
51
+ @html_input.each_index do |index|
52
+ if @html_input[index] =~ %r{<h(\d)(.*?)>(.*?)</h\1>$}m then
53
+ # Pattern match values:
54
+ # $1 -- header tag level, e.g. <h2>...</h2> will be 2
55
+ # $3 -- content between the tags
56
+ @heading_elements << [index, $1, $3, anchor($3)]
57
+ end
58
+ end
59
+ end
60
+
61
+ # Transforms the input string into a valid XHTML anchor (ID attribute).
62
+ #
63
+ # anchor("Text with spaces") # textwithspaces
64
+ # anchor("step 1 step 2 step: 3") # step1step2step3
65
+ def anchor(string)
66
+ alnum = String.new
67
+ string.gsub(/[[:alnum:]]/) { |c| alnum << c }
68
+ return alnum.downcase
69
+ end
70
+
71
+ def numbers()
72
+ chapters = 0
73
+ sections = 0
74
+ subsections = 0
75
+ @heading_elements.each_index do |index|
76
+ level = @heading_elements[index][1].to_i
77
+ case level
78
+ when 1
79
+ chapters = chapters.next
80
+ @heading_elements[index] << "#{chapters}"
81
+ sections = 0
82
+ subsections = 0
83
+ when 2
84
+ sections = sections.next
85
+ @heading_elements[index] << "#{chapters}.#{sections}"
86
+ subsections = 0
87
+ when 3
88
+ subsections = subsections.next
89
+ @heading_elements[index] << "#{chapters}.#{sections}.#{subsections}"
90
+ end
91
+ end
92
+ end
93
+
94
+ def inject_anchors()
95
+ @heading_elements.each do |heading|
96
+ line = String.new
97
+ index = heading[0]
98
+ level = heading[1].to_i
99
+ content = heading[2]
100
+ anchor = heading[3]
101
+ next if level > 3 # only h1,h2, and h3 tags are used
102
+ if numbering? then
103
+ number = heading[4]
104
+ line = %{<h#{level}><a name="#{anchor}"></a>#{number}&nbsp;#{content}</h#{level}>}
105
+ else
106
+ line = %{<h#{level}><a name="#{anchor}"></a>#{content}</h#{level}>}
107
+ end
108
+ @html_input[index] = line
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+
@@ -0,0 +1,82 @@
1
+ module Conversio
2
+
3
+
4
+ class Pygmentizer
5
+
6
+ def self.respond_to?( command )
7
+ return true if `which #{command}`.empty?
8
+ return false
9
+ end
10
+
11
+ def self.run(command, input='')
12
+ puts command if $DEBUG
13
+ IO.popen(command, 'r+') do |io|
14
+ io.puts input
15
+ io.close_write
16
+ return io.read
17
+ end
18
+ end
19
+
20
+
21
+
22
+ def output(string)
23
+ @output << "#{string}\n"
24
+ end
25
+
26
+ def transform_code_blocks(text)
27
+ raise RuntimeError, "pygmentize not in path" if
28
+ Pygmentizer::respond_to?("pygmentize")
29
+ @input_by_line = Array.new
30
+ text.each_line { |line| @input_by_line << line.chop }
31
+ @output = String.new
32
+ buffer = Array.new
33
+ rec = false
34
+ @input_by_line.each do |line|
35
+ # true if a Markdown code block is found
36
+ rec = true if !rec and line =~ /^ /
37
+ # store the code block into buffer
38
+ if rec and line =~ /^ / then
39
+ # remove the leading 4 spaces
40
+ line = line.gsub(/^ /,'')
41
+ buffer << line
42
+ # End of code block
43
+ elsif rec
44
+ block_to_html(buffer)
45
+ # Wipe the buffer
46
+ buffer.clear
47
+ rec = false
48
+ # Anyting beside code blocks will be output
49
+ else
50
+ output(line)
51
+ end
52
+ end
53
+ return @output
54
+ end
55
+
56
+
57
+ def block_to_html(block)
58
+ style = get_style(block[0])
59
+ unless style.empty? then
60
+ # remove the style information from the code block
61
+ block.slice!(0)
62
+ output(highlight(block.join("\n"),style))
63
+ else
64
+ # Code blocks without style information will be
65
+ # put to output includeing the 4 leading spaces
66
+ block.each {|line| output(" #{line}") }
67
+ output("")
68
+ end
69
+ end
70
+
71
+
72
+ def get_style(string)
73
+ return string.gsub(/--/,'').strip if string =~ /^--[a-z]* */
74
+ return ""
75
+ end
76
+
77
+ def highlight(string, style)
78
+ return Pygmentizer::run("pygmentize -f html -l #{style}", string) << "\n"
79
+ end
80
+ end
81
+
82
+ end
data/lib/conversio.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'conversio/converter'
2
+ require 'conversio/pygmentizer'
3
+ require 'conversio/htmltoc'
4
+
5
+ module Conversio
6
+ class Template
7
+ end
8
+ end
9
+
10
+ # find the template directory
11
+ templates = File.join(File.dirname(File.expand_path(__FILE__)),'..','templates')
12
+ # find the ERB templates
13
+ Dir.glob("#{templates}/*.erb") do |template|
14
+ # add the template as constant to the class Templates
15
+ Conversio::Template.const_set(
16
+ File.basename(template,".erb").upcase,
17
+ File.read(template))
18
+ end
19
+