kramdown-plantuml 1.0.0 → 1.1.1

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/codecov.yml +2 -0
  3. data/.github/dependabot.yml +23 -0
  4. data/.github/mergify.yml +22 -0
  5. data/.github/scripts/amend.sh +176 -0
  6. data/.github/scripts/build-gem.sh +6 -0
  7. data/.github/scripts/inspect-gem.sh +72 -0
  8. data/.github/scripts/publish-gem.sh +121 -0
  9. data/.github/scripts/test-gem.sh +170 -0
  10. data/.github/scripts/variables.sh +72 -0
  11. data/.github/workflows/amend.yml +19 -0
  12. data/.github/workflows/no-java.yml +22 -0
  13. data/.github/workflows/no-plantuml.yml +23 -0
  14. data/.github/workflows/ruby.yml +176 -0
  15. data/.github/workflows/shell.yml +11 -0
  16. data/.gitignore +19 -0
  17. data/.rspec +1 -0
  18. data/.rubocop.yml +18 -0
  19. data/CODE_OF_CONDUCT.md +131 -0
  20. data/Gemfile +8 -0
  21. data/GitVersion.yml +5 -0
  22. data/LICENSE +201 -0
  23. data/README.md +208 -0
  24. data/Rakefile +40 -0
  25. data/bin/net/sourceforge/plantuml/plantuml/1.2021.8/plantuml-1.2021.8.jar +0 -0
  26. data/kramdown-plantuml.gemspec +48 -0
  27. data/lib/kramdown-plantuml.rb +5 -0
  28. data/lib/kramdown-plantuml/bool_env.rb +24 -0
  29. data/lib/kramdown-plantuml/console_logger.rb +56 -0
  30. data/lib/kramdown-plantuml/converter.rb +42 -0
  31. data/lib/kramdown-plantuml/executor.rb +48 -0
  32. data/lib/kramdown-plantuml/hash.rb +14 -0
  33. data/lib/kramdown-plantuml/logger.rb +68 -0
  34. data/lib/kramdown-plantuml/plantuml_error.rb +33 -0
  35. data/lib/kramdown-plantuml/plantuml_result.rb +49 -0
  36. data/lib/kramdown-plantuml/themer.rb +50 -0
  37. data/lib/kramdown-plantuml/version.rb +7 -0
  38. data/lib/kramdown_html.rb +27 -0
  39. data/lib/which.rb +15 -0
  40. data/pom.xml +16 -0
  41. metadata +148 -10
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/which'
4
+ require_relative 'lib/kramdown-plantuml/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'kramdown-plantuml'
8
+ spec.version = Kramdown::PlantUml::VERSION
9
+ spec.authors = ['Swedbank Pay']
10
+ spec.email = ['opensource@swedbankpay.com']
11
+
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
16
+ spec.homepage = 'https://github.com/SwedbankPay/kramdown-plantuml'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/SwedbankPay/kramdown-plantuml'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into Git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ if Which.which('git')
27
+ files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ # Explicitly add plantuml.jar to the list of files as it is not committed to Git.
29
+ files.append(Dir['bin/**/plantuml*.jar'].first)
30
+ else
31
+ puts "Git not found, no files added to #{spec.name}."
32
+ end
33
+ end
34
+ spec.bindir = 'exe'
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ['lib']
37
+
38
+ spec.add_dependency 'kramdown', '~> 2.3'
39
+ spec.add_dependency 'kramdown-parser-gfm', '~> 1.1'
40
+ spec.add_dependency 'open3', '~> 0.1'
41
+
42
+ spec.add_development_dependency 'rake', '~> 13.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.2'
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'
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'kramdown-plantuml/version'
4
+ require_relative 'kramdown-plantuml/converter'
5
+ require_relative 'kramdown_html'
@@ -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
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'themer'
5
+ require_relative 'plantuml_error'
6
+ require_relative 'logger'
7
+ require_relative 'executor'
8
+
9
+ module Kramdown
10
+ module PlantUml
11
+ # Converts PlantUML markup to SVG
12
+ class Converter
13
+ def initialize(options = {})
14
+ @themer = Themer.new(options)
15
+ @logger = Logger.init
16
+ @executor = Executor.new
17
+ end
18
+
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
28
+
29
+ private
30
+
31
+ def wrap(svg)
32
+ theme_class = @themer.theme_name ? "theme-#{@themer.theme_name}" : ''
33
+ class_name = "plantuml #{theme_class}".strip
34
+
35
+ wrapper_element_start = "<div class=\"#{class_name}\">"
36
+ wrapper_element_end = '</div>'
37
+
38
+ "#{wrapper_element_start}#{svg}#{wrapper_element_end}"
39
+ end
40
+ end
41
+ end
42
+ 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