kramdown-plantuml 1.0.5 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,10 +9,13 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Swedbank Pay']
10
10
  spec.email = ['opensource@swedbankpay.com']
11
11
 
12
- spec.summary = "kramdown-plantuml allows you to use PlantUML syntax within fenced code blocks with Kramdown (Jekyll's default Markdown parser)"
12
+ spec.summary = <<~SUMMARY
13
+ kramdown-plantuml allows you to use PlantUML syntax within fenced code
14
+ blocks with Kramdown (Jekyll's default Markdown parser).
15
+ SUMMARY
13
16
  spec.homepage = 'https://github.com/SwedbankPay/kramdown-plantuml'
14
17
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
18
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
16
19
 
17
20
  spec.metadata['homepage_uri'] = spec.homepage
18
21
  spec.metadata['source_code_uri'] = 'https://github.com/SwedbankPay/kramdown-plantuml'
@@ -33,9 +36,13 @@ Gem::Specification.new do |spec|
33
36
  spec.require_paths = ['lib']
34
37
 
35
38
  spec.add_dependency 'kramdown', '~> 2.3'
39
+ spec.add_dependency 'kramdown-parser-gfm', '~> 1.1'
36
40
  spec.add_dependency 'open3', '~> 0.1'
37
41
 
38
42
  spec.add_development_dependency 'rake', '~> 13.0'
39
43
  spec.add_development_dependency 'rspec', '~> 3.2'
40
- spec.add_development_dependency 'rubocop', '~> 0.92'
44
+ spec.add_development_dependency 'rspec-its', '~> 1.3'
45
+ spec.add_development_dependency 'rubocop', '~> 1.12'
46
+ spec.add_development_dependency 'rubocop-rake', '~> 0.6'
47
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.4'
41
48
  end
@@ -3,9 +3,3 @@
3
3
  require_relative 'kramdown-plantuml/version'
4
4
  require_relative 'kramdown-plantuml/converter'
5
5
  require_relative 'kramdown_html'
6
-
7
- module Kramdown
8
- module PlantUml
9
- class Error < StandardError; end
10
- end
11
- end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kramdown
4
+ module PlantUml
5
+ # Converts envrionment variables to boolean values
6
+ class BoolEnv
7
+ TRUTHY_VALUES = %w[t true yes y 1].freeze
8
+ FALSEY_VALUES = %w[f false n no 0].freeze
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @value = ENV.key?(name) ? ENV[name] : nil
13
+ @value = @value.to_s.downcase unless @value.nil?
14
+ end
15
+
16
+ def true?
17
+ return true if TRUTHY_VALUES.include?(@value)
18
+ return false if FALSEY_VALUES.include?(@value) || @value.nil? || value.empty?
19
+
20
+ raise "The value '#{@value}' of '#{@name}' can't be converted to a boolean"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bool_env'
4
+
5
+ module Kramdown
6
+ module PlantUml
7
+ # Logs to $stdout and $stderr
8
+ class ConsoleLogger
9
+ LOG_LEVELS = %i[debug info warn error].freeze
10
+
11
+ def initialize(level)
12
+ @configured_log_level = level
13
+ end
14
+
15
+ def debug(message)
16
+ write(:debug, message)
17
+ end
18
+
19
+ def info(message)
20
+ write(:info, message)
21
+ end
22
+
23
+ def warn(message)
24
+ write(:warn, message)
25
+ end
26
+
27
+ def error(message)
28
+ write(:error, message)
29
+ end
30
+
31
+ private
32
+
33
+ def write(level, message)
34
+ return false unless write_message?(level)
35
+
36
+ pipe = pipe_for(level)
37
+ pipe.write("\n#{message}")
38
+ end
39
+
40
+ def write_message?(level_of_message)
41
+ LOG_LEVELS.index(@configured_log_level) <= LOG_LEVELS.index(level_of_message)
42
+ end
43
+
44
+ def pipe_for(level)
45
+ case level
46
+ when :debug, :info
47
+ $stdout
48
+ when :warn, :error
49
+ $stderr
50
+ else
51
+ raise ArgumentError, "Unknown log level '#{level}'."
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,43 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'open3'
4
- require_relative '../which'
5
3
  require_relative 'version'
4
+ require_relative 'themer'
5
+ require_relative 'plantuml_error'
6
+ require_relative 'logger'
7
+ require_relative 'executor'
6
8
 
7
9
  module Kramdown
8
10
  module PlantUml
9
11
  # Converts PlantUML markup to SVG
10
12
  class Converter
11
- def initialize
12
- dir = File.dirname __dir__
13
- jar_glob = File.join dir, '../bin/**/plantuml*.jar'
14
- @plant_uml_jar_file = Dir[jar_glob].first
15
-
16
- raise IOError, 'Java can not be found' unless Which.which('java')
17
- raise IOError, "No 'plantuml.jar' file could be found" if @plant_uml_jar_file.nil?
18
- raise IOError, "'#{@plant_uml_jar_file}' does not exist" unless File.exist? @plant_uml_jar_file
13
+ def initialize(options = {})
14
+ @themer = Themer.new(options)
15
+ @logger = Logger.init
16
+ @executor = Executor.new
19
17
  end
20
18
 
21
- def convert_plantuml_to_svg(content)
22
- cmd = "java -jar #{@plant_uml_jar_file} -tsvg -pipe"
23
-
24
- stdout, stderr, = Open3.capture3(cmd, stdin_data: content)
25
-
26
- # Circumvention of https://bugs.openjdk.java.net/browse/JDK-8244621
27
- raise stderr unless stderr.empty? || stderr.include?('CoreText note:')
28
-
29
- xml_prologue_start = '<?xml'
30
- xml_prologue_end = '?>'
19
+ def convert_plantuml_to_svg(plantuml)
20
+ plantuml = @themer.apply_theme(plantuml)
21
+ plantuml = plantuml.strip
22
+ @logger.debug "PlantUML converting diagram:\n#{plantuml}"
23
+ result = @executor.execute(plantuml)
24
+ result.validate(plantuml)
25
+ svg = result.without_xml_prologue
26
+ wrap(svg)
27
+ end
31
28
 
32
- start_index = stdout.index(xml_prologue_start)
33
- end_index = stdout.index(xml_prologue_end, xml_prologue_start.length) + xml_prologue_end.length
29
+ private
34
30
 
35
- stdout.slice! start_index, end_index
31
+ def wrap(svg)
32
+ theme_class = @themer.theme_name ? "theme-#{@themer.theme_name}" : ''
33
+ class_name = "plantuml #{theme_class}".strip
36
34
 
37
- wrapper_element_start = '<div class="plantuml">'
35
+ wrapper_element_start = "<div class=\"#{class_name}\">"
38
36
  wrapper_element_end = '</div>'
39
37
 
40
- "#{wrapper_element_start}#{stdout}#{wrapper_element_end}"
38
+ "#{wrapper_element_start}#{svg}#{wrapper_element_end}"
41
39
  end
42
40
  end
43
41
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'open3'
4
+ require_relative '../which'
5
+ require_relative 'logger'
6
+ require_relative 'plantuml_result'
7
+
8
+ module Kramdown
9
+ module PlantUml
10
+ # Executes the PlantUML Java application.
11
+ class Executor
12
+ def initialize
13
+ @logger = Logger.init
14
+ @plantuml_jar_file = find_plantuml_jar_file
15
+
16
+ raise IOError, 'Java can not be found' unless Which.which('java')
17
+ raise IOError, "No 'plantuml.jar' file could be found" if @plantuml_jar_file.nil?
18
+ raise IOError, "'#{@plantuml_jar_file}' does not exist" unless File.exist? @plantuml_jar_file
19
+ end
20
+
21
+ def execute(stdin)
22
+ cmd = "java -Djava.awt.headless=true -jar #{@plantuml_jar_file} -tsvg -failfast -pipe"
23
+ cmd << if @logger.debug?
24
+ ' -verbose'
25
+ else
26
+ ' -nometadata'
27
+ end
28
+
29
+ @logger.debug "PlantUML executing: #{cmd}"
30
+
31
+ stdout, stderr, status = Open3.capture3 cmd, stdin_data: stdin
32
+
33
+ @logger.debug "PlantUML exit code: #{status.exitstatus}"
34
+
35
+ PlantUmlResult.new(stdout, stderr, status)
36
+ end
37
+
38
+ private
39
+
40
+ def find_plantuml_jar_file
41
+ dir = File.dirname __dir__
42
+ jar_glob = File.join dir, '../bin/**/plantuml*.jar'
43
+ first_jar = Dir[jar_glob].first
44
+ File.expand_path first_jar unless first_jar.nil?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby's Hash class.
4
+ class ::Hash
5
+ # Via https://stackoverflow.com/a/25835016/2257038
6
+ def symbolize_keys
7
+ h = map do |k, v|
8
+ v_sym = v.instance_of?(Hash) ? v.symbolize_keys : v
9
+ [k.to_sym, v_sym]
10
+ end
11
+
12
+ h.to_h
13
+ end
14
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'console_logger'
4
+
5
+ module Kramdown
6
+ module PlantUml
7
+ # Provides theming support for PlantUML
8
+ class Logger
9
+ def initialize(logger)
10
+ raise ArgumentError, 'logger cannot be nil' if logger.nil?
11
+ raise ArgumentError, 'logger must respond to #debug' unless logger.respond_to? :debug
12
+ raise ArgumentError, 'logger must respond to #info' unless logger.respond_to? :info
13
+ raise ArgumentError, 'logger must respond to #warn' unless logger.respond_to? :warn
14
+ raise ArgumentError, 'logger must respond to #error' unless logger.respond_to? :error
15
+
16
+ @logger = logger
17
+ end
18
+
19
+ def debug(message)
20
+ @logger.debug(message)
21
+ end
22
+
23
+ def info(message)
24
+ @logger.info(message)
25
+ end
26
+
27
+ def warn(message)
28
+ @logger.warn(message)
29
+ end
30
+
31
+ def error(message)
32
+ @logger.error(message)
33
+ end
34
+
35
+ def debug?
36
+ self.class.level == :debug
37
+ end
38
+
39
+ class << self
40
+ def init
41
+ inner = nil
42
+
43
+ begin
44
+ require 'jekyll'
45
+ inner = Jekyll.logger
46
+ rescue LoadError
47
+ inner = ConsoleLogger.new(level)
48
+ end
49
+
50
+ Logger.new(inner)
51
+ end
52
+
53
+ def level
54
+ @level ||= level_from_env
55
+ end
56
+
57
+ private
58
+
59
+ def level_from_env
60
+ return :debug if BoolEnv.new('DEBUG').true?
61
+ return :debug if BoolEnv.new('VERBOSE').true?
62
+
63
+ :warn
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kramdown
4
+ module PlantUml
5
+ # PlantUML Error
6
+ class PlantUmlError < StandardError
7
+ def initialize(plantuml, stderr, exitcode)
8
+ message = <<~MESSAGE
9
+ Conversion of the following PlantUML diagram failed:
10
+
11
+ #{plantuml}
12
+
13
+ The error received from PlantUML was:
14
+
15
+ Exit code: #{exitcode}
16
+ #{stderr}
17
+ MESSAGE
18
+
19
+ super message
20
+ end
21
+
22
+ def self.should_raise?(exitcode, stderr)
23
+ return false if exitcode.zero?
24
+
25
+ !stderr.nil? && !stderr.empty? && \
26
+ # If stderr is not empty, but contains the string 'CoreText note:',
27
+ # the error is caused by a bug in Java, and should be ignored.
28
+ # Circumvents https://bugs.openjdk.java.net/browse/JDK-8244621
29
+ !stderr.include?('CoreText note:')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require_relative 'plantuml_error'
5
+
6
+ module Kramdown
7
+ module PlantUml
8
+ # Executes the PlantUML Java application.
9
+ class PlantUmlResult
10
+ attr_reader :stdout, :stderr, :exitcode
11
+
12
+ def initialize(stdout, stderr, status)
13
+ @stdout = stdout
14
+ @stderr = stderr
15
+ @exitcode = status.exitstatus
16
+ @logger = Logger.init
17
+ end
18
+
19
+ def without_xml_prologue
20
+ return @stdout if @stdout.nil? || @stdout.empty?
21
+
22
+ xml_prologue_start = '<?xml'
23
+ xml_prologue_end = '?>'
24
+
25
+ start_index = @stdout.index(xml_prologue_start)
26
+
27
+ return @stdout if start_index.nil?
28
+
29
+ end_index = @stdout.index(xml_prologue_end, xml_prologue_start.length)
30
+
31
+ return @stdout if end_index.nil?
32
+
33
+ end_index += xml_prologue_end.length
34
+
35
+ @stdout.slice! start_index, end_index
36
+
37
+ @stdout
38
+ end
39
+
40
+ def validate(plantuml)
41
+ raise PlantUmlError.new(plantuml, @stderr, @exitcode) if PlantUmlError.should_raise?(@exitcode, @stderr)
42
+
43
+ return if @stderr.nil? || @stderr.empty?
44
+
45
+ @logger.debug("PlantUML log:\n#{@stderr}")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: false
2
+
3
+ require_relative 'hash'
4
+ require_relative 'logger'
5
+
6
+ module Kramdown
7
+ module PlantUml
8
+ # Provides theming support for PlantUML
9
+ class Themer
10
+ attr_reader :theme_name, :theme_directory
11
+
12
+ def initialize(options = {})
13
+ options = options.symbolize_keys
14
+ @logger = Logger.init
15
+ @logger.debug(options)
16
+ @theme_name, @theme_directory = theme_options(options)
17
+ end
18
+
19
+ def apply_theme(plantuml)
20
+ return plantuml if plantuml.nil? || plantuml.empty? || @theme_name.nil? || @theme_name.empty?
21
+
22
+ startuml = '@startuml'
23
+ startuml_index = plantuml.index(startuml) + startuml.length
24
+
25
+ return plantuml if startuml_index.nil?
26
+
27
+ /@startuml.*/.match(plantuml) do |match|
28
+ theme_string = "\n!theme #{@theme_name}"
29
+ theme_string << " from #{@theme_directory}" unless @theme_directory.nil?
30
+
31
+ return plantuml.insert match.end(0), theme_string
32
+ end
33
+
34
+ plantuml
35
+ end
36
+
37
+ private
38
+
39
+ def theme_options(options)
40
+ return nil unless options.key?(:theme)
41
+
42
+ theme = options[:theme] || {}
43
+ theme_name = theme.key?(:name) ? theme[:name] : nil
44
+ theme_directory = theme.key?(:directory) ? theme[:directory] : nil
45
+
46
+ [theme_name, theme_directory]
47
+ end
48
+ end
49
+ end
50
+ end