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.
- checksums.yaml +4 -4
- data/.github/codecov.yml +2 -0
- data/.github/dependabot.yml +23 -0
- data/.github/mergify.yml +22 -0
- data/.github/scripts/amend.sh +176 -0
- data/.github/scripts/build-gem.sh +6 -0
- data/.github/scripts/inspect-gem.sh +72 -0
- data/.github/scripts/publish-gem.sh +121 -0
- data/.github/scripts/test-gem.sh +170 -0
- data/.github/scripts/variables.sh +72 -0
- data/.github/workflows/amend.yml +19 -0
- data/.github/workflows/no-java.yml +22 -0
- data/.github/workflows/no-plantuml.yml +23 -0
- data/.github/workflows/ruby.yml +176 -0
- data/.github/workflows/shell.yml +11 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.rubocop.yml +18 -0
- data/CODE_OF_CONDUCT.md +131 -0
- data/Gemfile +8 -0
- data/GitVersion.yml +5 -0
- data/LICENSE +201 -0
- data/README.md +208 -0
- data/Rakefile +40 -0
- data/bin/net/sourceforge/plantuml/plantuml/1.2021.8/plantuml-1.2021.8.jar +0 -0
- data/kramdown-plantuml.gemspec +48 -0
- data/lib/kramdown-plantuml.rb +5 -0
- data/lib/kramdown-plantuml/bool_env.rb +24 -0
- data/lib/kramdown-plantuml/console_logger.rb +56 -0
- data/lib/kramdown-plantuml/converter.rb +42 -0
- data/lib/kramdown-plantuml/executor.rb +48 -0
- data/lib/kramdown-plantuml/hash.rb +14 -0
- data/lib/kramdown-plantuml/logger.rb +68 -0
- data/lib/kramdown-plantuml/plantuml_error.rb +33 -0
- data/lib/kramdown-plantuml/plantuml_result.rb +49 -0
- data/lib/kramdown-plantuml/themer.rb +50 -0
- data/lib/kramdown-plantuml/version.rb +7 -0
- data/lib/kramdown_html.rb +27 -0
- data/lib/which.rb +15 -0
- data/pom.xml +16 -0
- 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,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
|