kramdown-plantuml 1.0.5 → 1.1.4

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
@@ -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.fetch(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("#{message}\n")
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
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'theme'
5
+ require_relative 'plantuml_error'
6
+ require_relative 'logger'
7
+ require_relative 'executor'
8
+
9
+ module Kramdown
10
+ module PlantUml
11
+ # Represents a PlantUML diagram that can be converted to SVG.
12
+ class Diagram
13
+ attr_reader :theme, :plantuml, :result
14
+
15
+ def initialize(plantuml, options = {})
16
+ @plantuml = plantuml
17
+ @theme = Theme.new(options || {})
18
+ @logger = Logger.init
19
+ @executor = Executor.new
20
+ end
21
+
22
+ def convert_to_svg
23
+ return @svg unless @svg.nil?
24
+
25
+ if @plantuml.nil? || @plantuml.empty?
26
+ @logger.warn ' kramdown-plantuml: PlantUML diagram is empty'
27
+ return @plantuml
28
+ end
29
+
30
+ @plantuml = @theme.apply(@plantuml)
31
+ @plantuml = plantuml.strip
32
+ log(plantuml)
33
+ @result = @executor.execute(self)
34
+ @result.validate
35
+ @svg = wrap(@result.without_xml_prologue)
36
+ @svg
37
+ end
38
+
39
+ private
40
+
41
+ def wrap(svg)
42
+ theme_class = @theme.name ? "theme-#{@theme.name}" : ''
43
+ class_name = "plantuml #{theme_class}".strip
44
+
45
+ wrapper_element_start = "<div class=\"#{class_name}\">"
46
+ wrapper_element_end = '</div>'
47
+
48
+ "#{wrapper_element_start}#{svg}#{wrapper_element_end}"
49
+ end
50
+
51
+ def log(plantuml)
52
+ @logger.debug ' kramdown-plantuml: PlantUML converting diagram:'
53
+ @logger.debug_with_prefix ' kramdown-plantuml: ', plantuml
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
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(diagram)
22
+ raise ArgumentError, 'diagram cannot be nil' if diagram.nil?
23
+ raise ArgumentError, "diagram must be a #{Diagram}" unless diagram.is_a?(Diagram)
24
+
25
+ cmd = "java -Djava.awt.headless=true -jar #{@plantuml_jar_file} -tsvg -failfast -pipe #{debug_args}"
26
+
27
+ @logger.debug " kramdown-plantuml: Executing '#{cmd}'."
28
+
29
+ stdout, stderr, status = Open3.capture3 cmd, stdin_data: diagram.plantuml
30
+
31
+ @logger.debug " kramdown-plantuml: PlantUML exit code '#{status.exitstatus}'."
32
+
33
+ PlantUmlResult.new(diagram, stdout, stderr, status.exitstatus)
34
+ end
35
+
36
+ private
37
+
38
+ def find_plantuml_jar_file
39
+ dir = File.dirname __dir__
40
+ jar_glob = File.join dir, '../bin/**/plantuml*.jar'
41
+ first_jar = Dir[jar_glob].first
42
+ File.expand_path first_jar unless first_jar.nil?
43
+ end
44
+
45
+ def debug_args
46
+ return ' -verbose' if @logger.debug?
47
+
48
+ ' -nometadata'
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'console_logger'
4
+
5
+ module Kramdown
6
+ module PlantUml
7
+ # Logs stuff
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 debug_with_prefix(prefix, multiline_string)
24
+ return if multiline_string.nil? || multiline_string.empty?
25
+
26
+ lines = multiline_string.lines
27
+ lines.each do |line|
28
+ @logger.debug "#{prefix}#{line.rstrip}"
29
+ end
30
+ end
31
+
32
+ def info(message)
33
+ @logger.info message
34
+ end
35
+
36
+ def warn(message)
37
+ @logger.warn message
38
+ end
39
+
40
+ def error(message)
41
+ @logger.error message
42
+ end
43
+
44
+ def debug?
45
+ self.class.level == :debug
46
+ end
47
+
48
+ def level
49
+ @level ||= level_from_logger || self.class.env
50
+ end
51
+
52
+ class << self
53
+ def init
54
+ inner = nil
55
+
56
+ begin
57
+ require 'jekyll'
58
+ inner = Jekyll.logger
59
+ rescue LoadError
60
+ inner = ConsoleLogger.new level
61
+ end
62
+
63
+ Logger.new inner
64
+ end
65
+
66
+ def level
67
+ @level ||= level_from_env
68
+ end
69
+
70
+ private
71
+
72
+ def level_from_env
73
+ return :debug if BoolEnv.new('DEBUG').true?
74
+ return :debug if BoolEnv.new('VERBOSE').true?
75
+
76
+ :warn
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def level_from_logger
83
+ return @logger.level if @logger.respond_to? :level
84
+
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plantuml_result'
4
+
5
+ module Kramdown
6
+ module PlantUml
7
+ # PlantUML Error
8
+ class PlantUmlError < StandardError
9
+ def initialize(result)
10
+ raise ArgumentError, 'result cannot be nil' if result.nil?
11
+ raise ArgumentError, "result must be a #{PlantUmlResult}" unless result.is_a?(PlantUmlResult)
12
+
13
+ super create_message(result)
14
+ end
15
+
16
+ private
17
+
18
+ def create_message(result)
19
+ header = header(result).gsub("\n", ' ').strip
20
+ plantuml = plantuml(result)
21
+ result = result(result)
22
+ message = <<~MESSAGE
23
+ #{header}
24
+
25
+ #{plantuml}
26
+
27
+ #{result}
28
+ MESSAGE
29
+
30
+ message.strip
31
+ end
32
+
33
+ def header(result)
34
+ if theme_not_found?(result) && !result.diagram.nil? && !result.diagram.theme.nil?
35
+ return <<~HEADER
36
+ Conversion of the following PlantUML result failed because the
37
+ theme '#{result.diagram.theme.name}' can't be found in the directory
38
+ '#{result.diagram.theme.directory}':
39
+ HEADER
40
+ end
41
+
42
+ 'Conversion of the following PlantUML result failed:'
43
+ end
44
+
45
+ def theme_not_found?(result)
46
+ !result.nil? \
47
+ && !result.stderr.nil? \
48
+ && result.stderr.include?('NullPointerException') \
49
+ && result.stderr.include?('getTheme')
50
+ end
51
+
52
+ def plantuml(result)
53
+ return nil if result.nil? || result.diagram.nil?
54
+
55
+ result.diagram.plantuml
56
+ end
57
+
58
+ def result(result)
59
+ return nil if result.nil?
60
+
61
+ <<~RESULT
62
+ The error received from PlantUML was:
63
+
64
+ Exit code: #{result.exitcode}
65
+ #{result.stderr}
66
+ RESULT
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require_relative 'plantuml_error'
5
+ require_relative 'diagram'
6
+
7
+ module Kramdown
8
+ module PlantUml
9
+ # Executes the PlantUML Java application.
10
+ class PlantUmlResult
11
+ attr_reader :diagram, :stdout, :stderr, :exitcode
12
+
13
+ def initialize(diagram, stdout, stderr, exitcode)
14
+ raise ArgumentError, 'diagram cannot be nil' if diagram.nil?
15
+ raise ArgumentError, "diagram must be a #{Diagram}" unless diagram.is_a?(Diagram)
16
+ raise ArgumentError, 'exitcode cannot be nil' if exitcode.nil?
17
+ raise ArgumentError, "exitcode must be a #{Integer}" unless exitcode.is_a?(Integer)
18
+
19
+ @diagram = diagram
20
+ @stdout = stdout
21
+ @stderr = stderr
22
+ @exitcode = exitcode
23
+ @logger = Logger.init
24
+ end
25
+
26
+ def without_xml_prologue
27
+ return @stdout if @stdout.nil? || @stdout.empty?
28
+
29
+ xml_prologue_start = '<?xml'
30
+ xml_prologue_end = '?>'
31
+
32
+ start_index = @stdout.index(xml_prologue_start)
33
+
34
+ return @stdout if start_index.nil?
35
+
36
+ end_index = @stdout.index(xml_prologue_end, xml_prologue_start.length)
37
+
38
+ return @stdout if end_index.nil?
39
+
40
+ end_index += xml_prologue_end.length
41
+
42
+ @stdout.slice! start_index, end_index
43
+
44
+ @stdout
45
+ end
46
+
47
+ def valid?
48
+ return true if @exitcode.zero? || @stderr.nil? || @stderr.empty?
49
+
50
+ # If stderr is not empty, but contains the string 'CoreText note:',
51
+ # the error is caused by a bug in Java, and should be ignored.
52
+ # Circumvents https://bugs.openjdk.java.net/browse/JDK-8244621
53
+ @stderr.include?('CoreText note:')
54
+ end
55
+
56
+ def validate
57
+ raise PlantUmlError, self unless valid?
58
+
59
+ return if @stderr.nil? || @stderr.empty?
60
+
61
+ @logger.debug ' kramdown-plantuml: PlantUML log:'
62
+ @logger.debug_with_prefix ' kramdown-plantuml: ', @stderr
63
+ end
64
+ end
65
+ end
66
+ end