code_slide 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aebc8a3009b9b263056b648e126f867b1b8dca98
4
+ data.tar.gz: 8f306a98bf274c541c4c65a4d6c5c6c9a9e3c118
5
+ SHA512:
6
+ metadata.gz: f1ea745fc473cabdf4a48679e6da926a8f6074611e3d82ea1054f51eecc00d3a42d6aaff6e66a0b9596a7c3971a504b4c39b8c5ea1b41fcdabb75acac11321d9
7
+ data.tar.gz: ea9222bc9825af6abf019abfee1496db11f507234d72a87e52995e79bcdc86dbec9df4f20cf43fe12ea1db2493e02ca530e6bbe4415f379ff69333621e7ecd5b
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Jamis Buck
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,82 @@
1
+ # CodeSlide
2
+
3
+ A library for turning code snippets into slides. Automatically turn your
4
+ source code into syntax-highlighted PDFs and PNGs. Take the tedium out of
5
+ building your presentation's slide deck!
6
+
7
+
8
+ ## Installation
9
+
10
+ If you want to be able to automatically generate PNG files, you'll need to have
11
+ GhostScript installed (https://ghostscript.com/).
12
+
13
+ Aside from that, installation is as easy as:
14
+
15
+ $ gem install code_slide
16
+
17
+ And you're good to go!
18
+
19
+
20
+ ## Usage
21
+
22
+ Look in the `examples` directory for some demonstrations of CodeSlide. In
23
+ brief, though, it works like this:
24
+
25
+ ```ruby
26
+ require 'code_slide'
27
+
28
+ snippet = CodeSlide::Snippet.new(<<-RUBY, lang: :ruby)
29
+ 1.upto(100) do |n|
30
+ print 'Fizz' if (n % 3).zero?
31
+ print 'Buzz' if (n % 5).zero?
32
+ puts if (n % 3).zero? || (n % 5).zero?
33
+ end
34
+ RUBY
35
+
36
+ snippet.make_pdf('snippet.pdf')
37
+ ```
38
+
39
+ You can also source your snippet from a file directly, even specifying which
40
+ range of lines you want to use:
41
+
42
+ ```ruby
43
+ snippet = CodeSlide::Snippet.from_file('fizz-buzz.rb', start: 5, finish: 15)
44
+ snippet.make_pdf('snippet.pdf')
45
+ ```
46
+
47
+ If you have a (TTF) font you want to use, you can specify the different faces
48
+ in the family, and instruct CodeSlide to use them:
49
+
50
+ ```ruby
51
+ snippet = CodeSlide::Snippet.from_file('fizz-buzz.rb', start: 5, finish: 15)
52
+ snippet.use_font('myfont-regular.ttf',
53
+ bold: 'myfont-bold.ttf',
54
+ italic: 'myfont-italic.ttf',
55
+ bold_italic: 'myfont-bold-italic.ttf')
56
+ snippet.make_pdf('snippet.pdf')
57
+ ```
58
+
59
+ You can choose between the "light" and "dark" themes (or make your own):
60
+
61
+ ```ruby
62
+ snippet = CodeSlide::Snippet.from_file('fizz-buzz.rb', theme: :dark)
63
+ snippet.make_pdf('dark-snippet.pdf')
64
+ ```
65
+
66
+ And if you want to generate a PNG directly, you can do that too (though again,
67
+ you need to have GhostScript installed):
68
+
69
+ ```ruby
70
+ snippet.make_png('snippet.png')
71
+ ```
72
+
73
+
74
+ ## License
75
+
76
+ This software is released under the terms of the MIT license. See the
77
+ `MIT-LICENSE` for full details.
78
+
79
+
80
+ ## Author
81
+
82
+ This software is written and distributed by Jamis Buck <jamis@jamisbuck.org>.
@@ -0,0 +1,2 @@
1
+ require 'code_slide/snippet'
2
+ require 'code_slide/version'
@@ -0,0 +1,134 @@
1
+ require 'coderay'
2
+ require 'nokogiri'
3
+ require 'code_slide/theme_manager'
4
+
5
+ module CodeSlide
6
+ class Analyzer
7
+ attr_reader :elements
8
+ attr_reader :width
9
+ attr_reader :height
10
+ attr_reader :styles
11
+ attr_reader :gutter_width
12
+
13
+ def initialize(text, options = {})
14
+ @styles = options[:styles] ||
15
+ (options[:theme] && ThemeManager.load_theme(options[:theme])) ||
16
+ ThemeManager.load_theme(:light)
17
+
18
+ line_numbers = options[:line_numbers] && :inline
19
+ bold_every = options[:bold_every] || nil
20
+ line_start = options[:line_number_start] || 1
21
+
22
+ text = _preprocess_text(text)
23
+ _compute_gutter(line_numbers, line_start)
24
+ html = _generate_html(text, options[:lang],
25
+ line_numbers, line_start, bold_every)
26
+
27
+ _format_html(html)
28
+ end
29
+
30
+ def _compute_gutter(line_numbers, line_start)
31
+ @gutter_width = 0
32
+ return unless line_numbers
33
+
34
+ max = line_start + height
35
+ @gutter_width = Math.log10(max + 1).ceil + 1
36
+ end
37
+
38
+ def _generate_html(text, lang, line_numbers, line_start, bold_every)
39
+ CodeRay.scan(text, lang).
40
+ html(line_numbers: line_numbers,
41
+ line_number_anchors: false,
42
+ line_number_start: line_start,
43
+ bold_every: bold_every).
44
+ gsub(%r{</strong>}, '</span>').
45
+ gsub(/<strong>/, '<span class="highlight">')
46
+ end
47
+
48
+ # removes leading and trailing blank lines
49
+ def _preprocess_text(text)
50
+ lines = text.lines.map(&:rstrip)
51
+ lines.delete_at(0) while lines[0].empty?
52
+ lines.delete_at(-1) while lines[-1].empty?
53
+
54
+ @width = lines.map(&:length).max
55
+ @height = lines.length
56
+
57
+ lines.join("\n")
58
+ end
59
+
60
+ def _format_html(html)
61
+ doc = Nokogiri::HTML.fragment(html)
62
+
63
+ @current_attributes = [{}]
64
+ @elements = []
65
+
66
+ _format(doc)
67
+ end
68
+
69
+ def _format(parent)
70
+ style = _lookup_class(parent['class'])
71
+ @current_attributes.push(_merge_style(style))
72
+ parent.children.each { |child| _format_node(child) }
73
+ ensure
74
+ attrs = @current_attributes.pop
75
+ @elements.last[:text] << attrs[:after] if attrs[:after]
76
+ end
77
+
78
+ def _format_node(node)
79
+ if node.text?
80
+ @elements << _style(node)
81
+ elsif node.element?
82
+ _format(node)
83
+ else
84
+ raise 'unknown node type'
85
+ end
86
+ end
87
+
88
+ def _style(node)
89
+ text = _text(node)
90
+ attrs = @current_attributes.last
91
+ attrs.merge(text: text)
92
+ end
93
+
94
+ def _merge_style(style)
95
+ styles = @current_attributes.last[:styles]
96
+ if style[:styles]
97
+ styles ||= []
98
+ styles = [*style[:styles], *styles]
99
+ end
100
+
101
+ @current_attributes.last.merge(style).tap do |attrs|
102
+ attrs[:styles] = styles if styles
103
+
104
+ # 'after' is never inherited
105
+ attrs[:after] = nil unless style[:after]
106
+ end
107
+ end
108
+
109
+ def _lookup_class(class_name)
110
+ return (@styles[:default] || {}) unless class_name
111
+
112
+ class_name && (@styles[class_name] || @styles[class_name.to_sym]) ||
113
+ begin
114
+ warn "no definition for #{class_name}"
115
+ { class: class_name }
116
+ end
117
+ end
118
+
119
+ ENTITY = {
120
+ 'lt' => '<',
121
+ 'gt' => '>',
122
+ 'amp' => '&'
123
+ }.freeze
124
+
125
+ def _text(text)
126
+ text.to_s.
127
+ tr(' ', "\u00A0").
128
+ gsub(/\&(.*?);/) do |match|
129
+ entity = Regexp.last_match(1)
130
+ ENTITY[entity.downcase] || match
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,93 @@
1
+ require 'prawn'
2
+
3
+ module CodeSlide
4
+ class PDFFormatter
5
+ def initialize(analyzer)
6
+ @analyzer = analyzer
7
+ end
8
+
9
+ def use_font(path, bold: path, italic: path, bold_italic: path)
10
+ @font = path
11
+ @bold = bold
12
+ @italic = italic
13
+ @bold_italic = bold_italic
14
+
15
+ self
16
+ end
17
+
18
+ def build_pdf(gravity: :center,
19
+ background_color: nil,
20
+ font_size: 16, page_width: 792, page_height: 612)
21
+ Prawn::Document.new(page_size: [page_width, page_height]).tap do |pdf|
22
+ Prawn::Font::AFM.hide_m17n_warning = true
23
+
24
+ _prepare_background(pdf, background_color)
25
+ pdf.font _prepare_font(pdf), size: font_size
26
+ options = _prepare_options(pdf, gravity)
27
+
28
+ box = Prawn::Text::Formatted::Box.new(@analyzer.elements, options)
29
+ box.render
30
+ end
31
+ end
32
+
33
+ def _prepare_background(pdf, bgcolor)
34
+ bgcolor ||= @analyzer.styles[:background] &&
35
+ @analyzer.styles[:background][:color]
36
+ return unless bgcolor
37
+
38
+ pdf.canvas do
39
+ pdf.fill_color bgcolor
40
+ pdf.fill_rectangle [pdf.bounds.left, pdf.bounds.top],
41
+ pdf.bounds.right, pdf.bounds.top
42
+ end
43
+ end
44
+
45
+ def _prepare_font(pdf)
46
+ if @font.nil?
47
+ 'Courier'
48
+ else
49
+ pdf.font_families.update(
50
+ 'Custom' => { normal: @font,
51
+ bold: @bold,
52
+ italic: @italic,
53
+ bold_italic: @bold_italic })
54
+ 'Custom'
55
+ end
56
+ end
57
+
58
+ def _prepare_options(pdf, gravity)
59
+ width = pdf.width_of('M') * @analyzer.width
60
+ height = pdf.height_of('M') * @analyzer.height
61
+
62
+ width += @analyzer.gutter_width
63
+
64
+ {
65
+ document: pdf,
66
+ overflow: :overflow,
67
+ at: _box_position(pdf, width, height, gravity)
68
+ }
69
+ end
70
+
71
+ def _box_position(pdf, width, height, gravity)
72
+ left = 0
73
+ hcenter = (pdf.bounds.width - width) / 2
74
+ right = pdf.bounds.width - width
75
+ top = pdf.bounds.height
76
+ vcenter = pdf.bounds.height - (pdf.bounds.height - height) / 2
77
+ bottom = height
78
+
79
+ case gravity
80
+ when :northwest then [left, top ]
81
+ when :north then [hcenter, top ]
82
+ when :northeast then [right, top ]
83
+ when :west then [left, vcenter]
84
+ when :center then [hcenter, vcenter]
85
+ when :east then [right, vcenter]
86
+ when :southwest then [left, bottom ]
87
+ when :south then [hcenter, bottom ]
88
+ when :southeast then [right, bottom ]
89
+ else raise ArguentError, "unsupported gravity #{gravity.inspect}"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,12 @@
1
+ module CodeSlide
2
+ class PNGFormatter
3
+ def initialize(pdf_filename)
4
+ @pdf_filename = pdf_filename
5
+ end
6
+
7
+ def generate_png(filename, dpi: 300, keep_pdf: false)
8
+ system 'gs -q -sDEVICE=png16m -dTextAlphaBits=4 ' \
9
+ "-r#{dpi} -o #{filename} #{@pdf_filename}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,72 @@
1
+ require 'code_slide/analyzer'
2
+ require 'code_slide/pdf_formatter'
3
+ require 'code_slide/png_formatter'
4
+
5
+ module CodeSlide
6
+ class Snippet
7
+ def self.from_file(filename, options = {})
8
+ options = options.dup
9
+
10
+ start = options.delete(:start) || 1
11
+ finish = options.delete(:finish) || -1
12
+
13
+ line_start = options[:line_number_start] || start
14
+
15
+ # assume people number their file lines starting at 1
16
+ start -= 1 if start > 0
17
+ finish -= 1 if finish > 0
18
+
19
+ text = File.read(filename).lines[start..finish].join
20
+ new(text,
21
+ options.merge(lang: options[:lang] || detect_language_type(filename),
22
+ line_number_start: line_start))
23
+ end
24
+
25
+ EXTMAP = { # rubocop:disable Style/MutableConstant
26
+ '.rb' => :ruby,
27
+ '.py' => :python,
28
+ '.c' => :c,
29
+ '.java' => :java
30
+ }
31
+
32
+ def self.detect_language_type(filename)
33
+ EXTMAP[File.extname(filename).downcase]
34
+ end
35
+
36
+ def initialize(snippet, options = {})
37
+ @analyzer = CodeSlide::Analyzer.new(snippet, options)
38
+ use_font(nil)
39
+ end
40
+
41
+ def use_font(path, bold: path, italic: path, bold_italic: path)
42
+ @font = path
43
+ @bold = bold
44
+ @italic = italic
45
+ @bold_italic = bold_italic
46
+
47
+ self
48
+ end
49
+
50
+ def make_pdf(filename, options = {})
51
+ PDFFormatter.new(@analyzer).
52
+ use_font(@font, bold: @bold,
53
+ italic: @italic,
54
+ bold_italic: @bold_italic).
55
+ build_pdf(options).
56
+ render_file(filename)
57
+ end
58
+
59
+ def make_png(filename, options = {})
60
+ options = options.dup
61
+ dpi = options.delete(:dpi)
62
+ keep_pdf = options.delete(:keep_pdf)
63
+
64
+ pdf_name = File.basename(filename, File.extname(filename)) + '.pdf'
65
+
66
+ make_pdf(pdf_name, options)
67
+ PNGFormatter.new(pdf_name).
68
+ generate_png(filename, dpi: dpi)
69
+ File.delete(pdf_name) unless keep_pdf
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ require 'yaml'
2
+
3
+ module CodeSlide
4
+ class ThemeManager
5
+ @search_paths = []
6
+ @search_paths << '.'
7
+ @search_paths << File.join(File.dirname(__FILE__), 'themes')
8
+
9
+ @cached_themes = {}
10
+
11
+ class <<self
12
+ attr_reader :search_paths
13
+
14
+ def load_theme(name)
15
+ @cached_themes[name] ||= begin
16
+ file_name = "#{name}.yml"
17
+ path = @search_paths.
18
+ map { |s| File.join(s, file_name) }.
19
+ find { |s| File.exist?(s) }
20
+
21
+ if path.nil?
22
+ raise ArgumentError, "couldn't find theme #{name.inspect}"
23
+ end
24
+
25
+ _post_process(YAML.load_file(path))
26
+ end
27
+ end
28
+
29
+ def _post_process(hash)
30
+ hash.each.with_object({}) do |(key, value), result|
31
+ key = key.to_sym
32
+ result[key] = _post_process_value(key, value)
33
+ end
34
+ end
35
+
36
+ def _post_process_value(key, value)
37
+ if key == :styles
38
+ value.map(&:to_sym)
39
+ elsif value.is_a?(Hash)
40
+ _post_process(value)
41
+ else
42
+ value
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ # modeled after Atom's "One Dark" syntax theme
2
+ ---
3
+ background:
4
+ color: '282c34'
5
+ default:
6
+ color: 'abb2be'
7
+
8
+ comment:
9
+ color: '5c636f'
10
+ styles: [ italic ]
11
+
12
+ keyword:
13
+ color: 'c57bdb'
14
+ class:
15
+ color: 'd09a6a'
16
+ constant:
17
+ color: 'd09a6a'
18
+ function:
19
+ color: '65b0ec'
20
+ string:
21
+ color: '99c27c'
22
+ delimiter:
23
+ color: '99c27c'
24
+ inline:
25
+ color: '99c27c'
26
+ content:
27
+ color: '99c27c'
28
+ symbol:
29
+ color: '5ab6c1'
30
+ key:
31
+ color: '5ab6c1'
32
+ integer:
33
+ color: 'd09a6a'
34
+ float:
35
+ color: 'd09a6a'
36
+ predefined-constant:
37
+ color: 'd09a6a'
38
+ inline-delimiter:
39
+ color: 'bc5149'
40
+ instance-variable:
41
+ color: 'de6d77'
42
+
43
+ highlight:
44
+ color: '999999'
45
+ styles: [ bold ]
46
+ line-numbers:
47
+ color: '555555'
48
+ after: ' '
@@ -0,0 +1,48 @@
1
+ # modeled after Atom's "One Light" syntax theme
2
+ ---
3
+ background:
4
+ color: 'ffffff'
5
+ default:
6
+ color: '000000'
7
+
8
+ comment:
9
+ color: 'a0a1a7'
10
+ styles: [ italic ]
11
+
12
+ keyword:
13
+ color: 'a42ea2'
14
+ class:
15
+ color: 'c0831e'
16
+ constant:
17
+ color: 'c0831e'
18
+ function:
19
+ color: '447bee'
20
+ string:
21
+ color: '53a053'
22
+ delimiter:
23
+ color: '53a053'
24
+ inline:
25
+ color: '53a053'
26
+ content:
27
+ color: '53a053'
28
+ symbol:
29
+ color: '1485ba'
30
+ key:
31
+ color: '1485ba'
32
+ integer:
33
+ color: '976715'
34
+ float:
35
+ color: '976715'
36
+ predefined-constant:
37
+ color: '976715'
38
+ inline-delimiter:
39
+ color: 'c81846'
40
+ instance-variable:
41
+ color: 'e2574e'
42
+
43
+ highlight:
44
+ color: '999999'
45
+ styles: [ bold ]
46
+ line-numbers:
47
+ color: 'bbbbbb'
48
+ after: ' '
@@ -0,0 +1,3 @@
1
+ module CodeSlide
2
+ VERSION = '1.0.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: code_slide
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prawn
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coderay
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ description: |2
56
+ A library for turning code snippets into slides. Automatically turn your
57
+ source code into syntax-highlighted PDFs and PNGs. Take the tedium out
58
+ of building your presentation's slide deck!
59
+ email:
60
+ - jamis@jamisbuck.org
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - MIT-LICENSE
66
+ - README.md
67
+ - lib/code_slide.rb
68
+ - lib/code_slide/analyzer.rb
69
+ - lib/code_slide/pdf_formatter.rb
70
+ - lib/code_slide/png_formatter.rb
71
+ - lib/code_slide/snippet.rb
72
+ - lib/code_slide/theme_manager.rb
73
+ - lib/code_slide/themes/dark.yml
74
+ - lib/code_slide/themes/light.yml
75
+ - lib/code_slide/version.rb
76
+ homepage: https://github.com/jamis/code_slide
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.5.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Generate PDF/PNG slides from source code
100
+ test_files: []