metanorma-plugin-plantuml 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.
- checksums.yaml +7 -0
- data/.github/workflows/plantuml-version-sync.yml +113 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +25 -0
- data/Gemfile +28 -0
- data/LICENSE +25 -0
- data/README.adoc +374 -0
- data/Rakefile +38 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/data/plantuml.jar +0 -0
- data/lib/metanorma/plugin/plantuml/backend.rb +160 -0
- data/lib/metanorma/plugin/plantuml/block_processor.rb +82 -0
- data/lib/metanorma/plugin/plantuml/config.rb +48 -0
- data/lib/metanorma/plugin/plantuml/errors.rb +42 -0
- data/lib/metanorma/plugin/plantuml/utils.rb +28 -0
- data/lib/metanorma/plugin/plantuml/version.rb +8 -0
- data/lib/metanorma/plugin/plantuml/wrapper.rb +256 -0
- data/lib/metanorma-plugin-plantuml.rb +15 -0
- data/metanorma-plugin-plantuml.gemspec +35 -0
- metadata +91 -0
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "fileutils"
|
5
|
+
require_relative "wrapper"
|
6
|
+
require_relative "utils"
|
7
|
+
require_relative "errors"
|
8
|
+
|
9
|
+
module Metanorma
|
10
|
+
module Plugin
|
11
|
+
module Plantuml
|
12
|
+
# Backend class for PlantUML diagram generation
|
13
|
+
# Adapted from metanorma-standoc's PlantUMLBlockMacroBackend
|
14
|
+
class Backend
|
15
|
+
class << self
|
16
|
+
def plantuml_installed?
|
17
|
+
unless Wrapper.available?
|
18
|
+
raise "PlantUML not installed"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def plantuml_available?
|
23
|
+
Wrapper.available?
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_file(parent, reader, format_override = nil)
|
27
|
+
ldir, imagesdir, fmt = generate_file_prep(parent)
|
28
|
+
fmt = format_override if format_override
|
29
|
+
plantuml_content = prep_source(reader)
|
30
|
+
|
31
|
+
# Extract filename from PlantUML source if specified
|
32
|
+
extracted_filename = extract_plantuml_filename(plantuml_content)
|
33
|
+
|
34
|
+
absolute_path, relative_path = path_prep(ldir, imagesdir)
|
35
|
+
filename = extracted_filename ? "#{extracted_filename}.#{fmt}" : generate_unique_filename(fmt)
|
36
|
+
output_file = File.join(absolute_path, filename)
|
37
|
+
|
38
|
+
result = Wrapper.generate(
|
39
|
+
plantuml_content,
|
40
|
+
format: fmt,
|
41
|
+
output_file: output_file
|
42
|
+
)
|
43
|
+
|
44
|
+
unless result[:success]
|
45
|
+
raise "No image output from PlantUML: #{result[:error].message}"
|
46
|
+
end
|
47
|
+
|
48
|
+
File.join(relative_path, filename)
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate_multiple_files(parent, reader, formats, attrs)
|
52
|
+
# Generate files for each format
|
53
|
+
filenames = formats.map do |format|
|
54
|
+
generate_file(parent, reader, format)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return data for BlockProcessor to create image block
|
58
|
+
through_attrs = generate_attrs attrs
|
59
|
+
through_attrs["target"] = filenames.first
|
60
|
+
|
61
|
+
# Store additional formats for potential future use
|
62
|
+
through_attrs["data-formats"] = formats.join(",")
|
63
|
+
through_attrs["data-files"] = filenames.join(",")
|
64
|
+
|
65
|
+
through_attrs
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_file_prep(parent)
|
69
|
+
ldir = localdir(parent)
|
70
|
+
imagesdir = parent.document.attr("imagesdir")
|
71
|
+
fmt = parent.document.attr("plantuml-image-format")&.strip&.downcase || "png"
|
72
|
+
[ldir, imagesdir, fmt]
|
73
|
+
end
|
74
|
+
|
75
|
+
def localdir(parent)
|
76
|
+
ret = Utils.localdir(parent.document)
|
77
|
+
File.writable?(ret) or
|
78
|
+
raise "Destination directory #{ret} not writable for PlantUML!"
|
79
|
+
ret
|
80
|
+
end
|
81
|
+
|
82
|
+
def path_prep(localdir, imagesdir)
|
83
|
+
path = Pathname.new(File.join(localdir, "_plantuml_images"))
|
84
|
+
sourcepath = imagesdir ? File.join(localdir, imagesdir) : localdir
|
85
|
+
path.mkpath
|
86
|
+
File.writable?(path) or
|
87
|
+
raise "Destination path #{path} not writable for PlantUML!"
|
88
|
+
[path, Pathname.new(path).relative_path_from(Pathname.new(sourcepath)).to_s]
|
89
|
+
end
|
90
|
+
|
91
|
+
def prep_source(reader)
|
92
|
+
src = reader.source
|
93
|
+
|
94
|
+
# Validate that we have matching start/end pairs
|
95
|
+
validate_plantuml_delimiters(src)
|
96
|
+
src
|
97
|
+
end
|
98
|
+
|
99
|
+
def generate_attrs(attrs)
|
100
|
+
%w(id align float title role width height alt)
|
101
|
+
.inject({}) do |memo, key|
|
102
|
+
memo[key] = attrs[key] if attrs.has_key?(key)
|
103
|
+
memo
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def validate_plantuml_delimiters(src)
|
110
|
+
# Find @start... pattern (case insensitive, support all PlantUML diagram types)
|
111
|
+
start_match = src.match(/^@start(\w+)/i)
|
112
|
+
unless start_match
|
113
|
+
raise "PlantUML content must start with @start... directive!"
|
114
|
+
end
|
115
|
+
|
116
|
+
diagram_type = start_match[1].downcase
|
117
|
+
end_pattern = "@end#{diagram_type}"
|
118
|
+
|
119
|
+
# Look for matching @end... directive (case insensitive)
|
120
|
+
unless src.match(/#{Regexp.escape(end_pattern)}\s*$/mi)
|
121
|
+
raise "@start#{diagram_type} without matching #{end_pattern} in PlantUML!"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def extract_plantuml_filename(plantuml_content)
|
126
|
+
# Extract filename from any @start... line if specified
|
127
|
+
# Only match when filename is on the same line as @start... (no newlines)
|
128
|
+
lines = plantuml_content.lines
|
129
|
+
first_line = lines.first&.strip
|
130
|
+
return nil unless first_line
|
131
|
+
|
132
|
+
match = first_line.match(/^@start\w+\s+(.+)$/i)
|
133
|
+
return nil unless match
|
134
|
+
|
135
|
+
filename = match[1].strip
|
136
|
+
return nil if filename.nil? || filename.empty?
|
137
|
+
|
138
|
+
# Sanitize filename for filesystem use
|
139
|
+
sanitize_filename(filename)
|
140
|
+
end
|
141
|
+
|
142
|
+
def sanitize_filename(filename)
|
143
|
+
# Remove quotes and sanitize for filesystem
|
144
|
+
filename = filename.gsub(/^["']|["']$/, "")
|
145
|
+
# Replace any non-alphanumeric characters (except dash, underscore, dot) with underscore
|
146
|
+
filename.gsub(/[^\w\-.]/, "_")
|
147
|
+
.gsub(/\.{2,}/, "_") # Replace multiple dots with underscore
|
148
|
+
.gsub(/_{2,}/, "_") # Replace multiple underscores with single
|
149
|
+
.gsub(/^[_\-\.]+|[_\-\.]+$/, "") # Remove leading/trailing special chars
|
150
|
+
end
|
151
|
+
|
152
|
+
def generate_unique_filename(format)
|
153
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S_%L")
|
154
|
+
"plantuml_#{timestamp}.#{format}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "asciidoctor"
|
4
|
+
require "asciidoctor/extensions"
|
5
|
+
require_relative "backend"
|
6
|
+
|
7
|
+
module Metanorma
|
8
|
+
module Plugin
|
9
|
+
module Plantuml
|
10
|
+
# PlantUML block processor for Asciidoctor
|
11
|
+
class BlockProcessor < ::Asciidoctor::Extensions::BlockProcessor
|
12
|
+
use_dsl
|
13
|
+
named :plantuml
|
14
|
+
on_context :literal
|
15
|
+
parse_content_as :raw
|
16
|
+
|
17
|
+
def abort(parent, reader, attrs, msg)
|
18
|
+
warn msg
|
19
|
+
attrs["language"] = "plantuml"
|
20
|
+
create_listing_block(
|
21
|
+
parent,
|
22
|
+
reader.source,
|
23
|
+
(attrs.reject { |k, _v| k.to_s.match?(/^\d+$/) })
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def process(parent, reader, attrs)
|
28
|
+
# Check for document-level disable flag
|
29
|
+
if parent.document.attr("plantuml-disabled")
|
30
|
+
return abort(parent, reader, attrs, "PlantUML processing disabled")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check PlantUML availability explicitly
|
34
|
+
unless Backend.plantuml_available?
|
35
|
+
return abort(parent, reader, attrs, "PlantUML not installed")
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parse format specifications
|
39
|
+
formats = parse_formats(attrs, parent.document)
|
40
|
+
|
41
|
+
if formats.length == 1
|
42
|
+
# Single format - original behavior
|
43
|
+
filename = Backend.generate_file(parent, reader, formats.first)
|
44
|
+
through_attrs = Backend.generate_attrs attrs
|
45
|
+
through_attrs["target"] = filename
|
46
|
+
create_image_block parent, through_attrs
|
47
|
+
else
|
48
|
+
# Multiple formats - generate multiple files
|
49
|
+
through_attrs = Backend.generate_multiple_files(parent, reader, formats, attrs)
|
50
|
+
create_image_block parent, through_attrs
|
51
|
+
end
|
52
|
+
rescue StandardError => e
|
53
|
+
abort(parent, reader, attrs, e.message)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def parse_formats(attrs, document)
|
59
|
+
# Check for formats attribute (multiple formats)
|
60
|
+
if attrs["formats"]
|
61
|
+
formats = attrs["formats"].split(",").map(&:strip).map(&:downcase)
|
62
|
+
return formats.select { |f| valid_format?(f) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check for format attribute (single format override)
|
66
|
+
if attrs["format"]
|
67
|
+
format = attrs["format"].strip.downcase
|
68
|
+
return [format] if valid_format?(format)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Fall back to document attribute or default
|
72
|
+
default_format = document.attr("plantuml-image-format")&.strip&.downcase || "png"
|
73
|
+
[default_format]
|
74
|
+
end
|
75
|
+
|
76
|
+
def valid_format?(format)
|
77
|
+
%w[png svg pdf txt eps].include?(format)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "rbconfig"
|
2
|
+
|
3
|
+
module Metanorma
|
4
|
+
module Plugin
|
5
|
+
module Plantuml
|
6
|
+
class Config
|
7
|
+
attr_accessor :java_path, :memory_limit, :output_dir, :temp_dir
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@java_path = "java"
|
11
|
+
@memory_limit = "1024m"
|
12
|
+
@output_dir = nil
|
13
|
+
@temp_dir = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def jvm_options
|
17
|
+
options = [
|
18
|
+
"-Xss5m",
|
19
|
+
"-Xmx#{memory_limit}",
|
20
|
+
"-Djava.awt.headless=true"
|
21
|
+
]
|
22
|
+
|
23
|
+
if RbConfig::CONFIG["host_os"].match?(/darwin|mac os/)
|
24
|
+
options << "-Dapple.awt.UIElement=true"
|
25
|
+
end
|
26
|
+
|
27
|
+
options
|
28
|
+
end
|
29
|
+
|
30
|
+
def java_command
|
31
|
+
[java_path, *jvm_options]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
attr_writer :configuration
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.configuration
|
40
|
+
@configuration ||= Config.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.configure
|
44
|
+
yield(configuration)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Metanorma
|
2
|
+
module Plugin
|
3
|
+
module Plantuml
|
4
|
+
class PlantumlError < StandardError
|
5
|
+
def initialize(message = nil, original_error = nil)
|
6
|
+
super(message)
|
7
|
+
@original_error = original_error
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :original_error
|
11
|
+
end
|
12
|
+
|
13
|
+
class JarNotFoundError < PlantumlError
|
14
|
+
def initialize(jar_path = nil)
|
15
|
+
message = jar_path ?
|
16
|
+
"PlantUML JAR file not found at: #{jar_path}" :
|
17
|
+
"PlantUML JAR file not found"
|
18
|
+
super(message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class JavaNotFoundError < PlantumlError
|
23
|
+
def initialize
|
24
|
+
super("Java runtime not found. Please ensure Java is installed and available in PATH")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class GenerationError < PlantumlError
|
29
|
+
def initialize(message, java_error = nil)
|
30
|
+
super("PlantUML generation failed: #{message}")
|
31
|
+
@original_error = java_error
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class InvalidFormatError < PlantumlError
|
36
|
+
def initialize(format, available_formats)
|
37
|
+
super("Invalid format '#{format}'. Available formats: #{available_formats.join(', ')}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "liquid"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module Metanorma
|
7
|
+
module Plugin
|
8
|
+
module Plantuml
|
9
|
+
module Utils
|
10
|
+
class << self
|
11
|
+
def localdir(document)
|
12
|
+
document.attributes["localdir"] ||
|
13
|
+
document.attributes["docdir"] ||
|
14
|
+
File.dirname(document.attributes["docfile"] || ".")
|
15
|
+
end
|
16
|
+
|
17
|
+
def relative_file_path(document, file_path)
|
18
|
+
return file_path if Pathname.new(file_path).absolute?
|
19
|
+
|
20
|
+
docdir = document.attributes["docdir"] || Dir.pwd
|
21
|
+
File.expand_path(file_path, docdir)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
require "open3"
|
2
|
+
require "base64"
|
3
|
+
require "tempfile"
|
4
|
+
require "fileutils"
|
5
|
+
require "tmpdir"
|
6
|
+
require "rbconfig"
|
7
|
+
require_relative "version"
|
8
|
+
require_relative "errors"
|
9
|
+
require_relative "config"
|
10
|
+
|
11
|
+
module Metanorma
|
12
|
+
module Plugin
|
13
|
+
module Plantuml
|
14
|
+
class Wrapper
|
15
|
+
PLANTUML_JAR_NAME = "plantuml.jar"
|
16
|
+
PLANTUML_JAR_PATH = File.join(File.dirname(__FILE__), "..", "..", "..", "..", "data", PLANTUML_JAR_NAME)
|
17
|
+
|
18
|
+
SUPPORTED_FORMATS = %w[png svg pdf txt eps].freeze
|
19
|
+
DEFAULT_FORMAT = "png"
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def jvm_options
|
23
|
+
options = ["-Xss5m", "-Xmx1024m"]
|
24
|
+
|
25
|
+
if RbConfig::CONFIG["host_os"].match?(/darwin|mac os/)
|
26
|
+
options << "-Dapple.awt.UIElement=true"
|
27
|
+
end
|
28
|
+
|
29
|
+
options
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate(content, format: DEFAULT_FORMAT, output_file: nil, base64: false, **options)
|
33
|
+
validate_format!(format)
|
34
|
+
ensure_jar_available!
|
35
|
+
ensure_java_available!
|
36
|
+
|
37
|
+
result = if output_file
|
38
|
+
generate_to_file(content, format, output_file, options)
|
39
|
+
elsif base64
|
40
|
+
generate_to_base64(content, format, options)
|
41
|
+
else
|
42
|
+
generate_to_temp_file(content, format, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
{ success: true }.merge(result)
|
46
|
+
rescue PlantumlError => e
|
47
|
+
{ success: false, error: e }
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_from_file(input_file, format: DEFAULT_FORMAT, output_file: nil, base64: false, **options)
|
51
|
+
unless File.exist?(input_file)
|
52
|
+
raise GenerationError.new("Input file not found: #{input_file}")
|
53
|
+
end
|
54
|
+
|
55
|
+
content = File.read(input_file)
|
56
|
+
generate(content, format: format, output_file: output_file, base64: base64, **options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def version
|
60
|
+
return nil unless available?
|
61
|
+
|
62
|
+
cmd = [configuration.java_path, *jvm_options, "-jar", PLANTUML_JAR_PATH, "-version"]
|
63
|
+
output, error, status = Open3.capture3(*cmd)
|
64
|
+
|
65
|
+
if status.success?
|
66
|
+
# Extract version from output
|
67
|
+
version_match = output.match(/PlantUML version ([\d.]+)/)
|
68
|
+
version_match ? version_match[1] : PLANTUML_JAR_VERSION
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
rescue StandardError
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def available?
|
77
|
+
return false if ENV["PLANTUML_DISABLED"] == "true"
|
78
|
+
File.exist?(PLANTUML_JAR_PATH) && java_available?
|
79
|
+
end
|
80
|
+
|
81
|
+
def jar_path
|
82
|
+
PLANTUML_JAR_PATH
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def configuration
|
88
|
+
Plantuml.configuration
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_format!(format)
|
92
|
+
format_str = format.to_s.downcase
|
93
|
+
unless SUPPORTED_FORMATS.include?(format_str)
|
94
|
+
raise InvalidFormatError.new(format, SUPPORTED_FORMATS)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def ensure_jar_available!
|
99
|
+
unless File.exist?(PLANTUML_JAR_PATH)
|
100
|
+
raise JarNotFoundError.new(PLANTUML_JAR_PATH)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def ensure_java_available!
|
105
|
+
unless java_available?
|
106
|
+
raise JavaNotFoundError.new
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def java_available?
|
111
|
+
cmd = [configuration.java_path, "-version"]
|
112
|
+
_, _, status = Open3.capture3(*cmd)
|
113
|
+
status.success?
|
114
|
+
rescue StandardError
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
def generate_to_file(content, format, output_file, options)
|
119
|
+
output_dir = File.dirname(output_file)
|
120
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
121
|
+
|
122
|
+
execute_plantuml(content, format, output_file, options)
|
123
|
+
|
124
|
+
unless File.exist?(output_file)
|
125
|
+
raise GenerationError.new("Output file was not created: #{output_file}")
|
126
|
+
end
|
127
|
+
|
128
|
+
{ output_path: File.expand_path(output_file) }
|
129
|
+
end
|
130
|
+
|
131
|
+
def generate_to_base64(content, format, options)
|
132
|
+
Tempfile.create(['plantuml_output', ".#{format}"]) do |temp_file|
|
133
|
+
temp_file.close
|
134
|
+
|
135
|
+
execute_plantuml(content, format, temp_file.path, options)
|
136
|
+
|
137
|
+
unless File.exist?(temp_file.path)
|
138
|
+
raise GenerationError.new("Temporary output file was not created")
|
139
|
+
end
|
140
|
+
|
141
|
+
encoded_content = Base64.strict_encode64(File.read(temp_file.path))
|
142
|
+
{ base64: encoded_content }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def generate_to_temp_file(content, format, options)
|
147
|
+
temp_dir = configuration.temp_dir || Dir.tmpdir
|
148
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S_%L")
|
149
|
+
output_file = File.join(temp_dir, "plantuml_#{timestamp}.#{format}")
|
150
|
+
|
151
|
+
generate_to_file(content, format, output_file, options)
|
152
|
+
end
|
153
|
+
|
154
|
+
def execute_plantuml(content, format, output_file, options)
|
155
|
+
Tempfile.create(['plantuml_input', '.puml']) do |input_file|
|
156
|
+
input_file.write(content)
|
157
|
+
input_file.close
|
158
|
+
|
159
|
+
# PlantUML generates output files based on filename specified in @start... line
|
160
|
+
# We need to use a temp directory and then move the file
|
161
|
+
Dir.mktmpdir do |temp_dir|
|
162
|
+
cmd = build_command(input_file.path, format, temp_dir, options)
|
163
|
+
|
164
|
+
output, error, status = Open3.capture3(*cmd)
|
165
|
+
|
166
|
+
unless status.success?
|
167
|
+
error_message = error.empty? ? "Unknown PlantUML error" : error.strip
|
168
|
+
raise GenerationError.new(error_message, error)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Find the generated file and move it to the desired location
|
172
|
+
if output_file
|
173
|
+
generated_file = find_generated_file(temp_dir, content, format)
|
174
|
+
if generated_file && File.exist?(generated_file)
|
175
|
+
FileUtils.mv(generated_file, output_file)
|
176
|
+
else
|
177
|
+
# Debug: List what files were actually generated
|
178
|
+
generated_files = Dir.glob(File.join(temp_dir, "*"))
|
179
|
+
error_msg = "Generated file not found in temp directory. "
|
180
|
+
error_msg += "Expected: #{generated_file}. "
|
181
|
+
error_msg += "Found files: #{generated_files.map { |f| File.basename(f) }.join(', ')}"
|
182
|
+
raise GenerationError.new(error_msg)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
output
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def find_generated_file(temp_dir, content, format)
|
192
|
+
# PlantUML generates files based on the filename specified in @start... line
|
193
|
+
extension = format.to_s.downcase
|
194
|
+
|
195
|
+
# First, try to extract filename from PlantUML content
|
196
|
+
plantuml_filename = extract_plantuml_filename_from_content(content)
|
197
|
+
|
198
|
+
if plantuml_filename
|
199
|
+
# Look for file with PlantUML-specified name
|
200
|
+
generated_file = File.join(temp_dir, "#{plantuml_filename}.#{extension}")
|
201
|
+
return generated_file if File.exist?(generated_file)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Fallback: scan directory for any generated files with correct extension
|
205
|
+
Dir.glob(File.join(temp_dir, "*.#{extension}")).first
|
206
|
+
end
|
207
|
+
|
208
|
+
def extract_plantuml_filename_from_content(content)
|
209
|
+
# Extract the raw filename from @start... line (don't sanitize)
|
210
|
+
match = content.match(/^@start\w+\s+(.+)$/mi)
|
211
|
+
return nil unless match
|
212
|
+
|
213
|
+
filename = match[1].strip.split("\n").first&.strip
|
214
|
+
return nil if filename.nil? || filename.empty?
|
215
|
+
|
216
|
+
# Remove quotes but keep the original name as PlantUML will use it
|
217
|
+
filename.gsub(/^["']|["']$/, "")
|
218
|
+
end
|
219
|
+
|
220
|
+
def build_command(input_file, format, output_dir, options)
|
221
|
+
cmd = [
|
222
|
+
configuration.java_path,
|
223
|
+
*jvm_options,
|
224
|
+
"-jar", PLANTUML_JAR_PATH
|
225
|
+
]
|
226
|
+
|
227
|
+
# Add format-specific options
|
228
|
+
case format.to_s.downcase
|
229
|
+
when "png"
|
230
|
+
cmd << "-tpng"
|
231
|
+
when "svg"
|
232
|
+
cmd << "-tsvg"
|
233
|
+
when "pdf"
|
234
|
+
cmd << "-tpdf"
|
235
|
+
when "txt"
|
236
|
+
cmd << "-ttxt"
|
237
|
+
when "eps"
|
238
|
+
cmd << "-teps"
|
239
|
+
end
|
240
|
+
|
241
|
+
# Add output directory option
|
242
|
+
cmd << "-o" << output_dir
|
243
|
+
|
244
|
+
# Add charset option for better compatibility
|
245
|
+
cmd << "-charset" << "UTF-8"
|
246
|
+
|
247
|
+
# Add input file
|
248
|
+
cmd << input_file
|
249
|
+
|
250
|
+
cmd
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
module Metanorma
|
3
|
+
module Plugin
|
4
|
+
module Plantuml
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require "metanorma/plugin/plantuml/version"
|
10
|
+
require "metanorma/plugin/plantuml/errors"
|
11
|
+
require "metanorma/plugin/plantuml/config"
|
12
|
+
require "metanorma/plugin/plantuml/wrapper"
|
13
|
+
require "metanorma/plugin/plantuml/utils"
|
14
|
+
require "metanorma/plugin/plantuml/backend"
|
15
|
+
require "metanorma/plugin/plantuml/block_processor"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "metanorma/plugin/plantuml/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "metanorma-plugin-plantuml"
|
7
|
+
spec.version = Metanorma::Plugin::Plantuml::VERSION
|
8
|
+
spec.authors = ["Ribose Inc."]
|
9
|
+
spec.email = ["open.source@ribose.com"]
|
10
|
+
|
11
|
+
spec.summary = "Metanorma plugin for PlantUML"
|
12
|
+
spec.description = "Metanorma plugin for PlantUML"
|
13
|
+
|
14
|
+
spec.homepage = "https://github.com/metanorma/metanorma-plugin-plantuml"
|
15
|
+
spec.license = "BSD-2-Clause"
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been
|
19
|
+
# added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{^(test|spec|features)/})
|
23
|
+
end
|
24
|
+
end + ["data/plantuml.jar"]
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.required_ruby_version = ">= 2.7.0" # rubocop:disable Gemspec/RequiredRubyVersion
|
30
|
+
|
31
|
+
spec.add_dependency "asciidoctor"
|
32
|
+
spec.add_dependency "isodoc"
|
33
|
+
|
34
|
+
spec.metadata["rubygems_mfa_required"] = "false"
|
35
|
+
end
|