metanorma-plugin-plantuml 1.0.0 → 1.0.2
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/Gemfile +3 -2
- data/README.adoc +151 -1
- data/Rakefile +2 -2
- data/lib/metanorma/plugin/plantuml/backend.rb +55 -26
- data/lib/metanorma/plugin/plantuml/block_processor.rb +12 -44
- data/lib/metanorma/plugin/plantuml/block_processor_base.rb +99 -0
- data/lib/metanorma/plugin/plantuml/config.rb +1 -1
- data/lib/metanorma/plugin/plantuml/errors.rb +13 -5
- data/lib/metanorma/plugin/plantuml/image_block_macro_processor.rb +57 -0
- data/lib/metanorma/plugin/plantuml/utils.rb +0 -8
- data/lib/metanorma/plugin/plantuml/version.rb +2 -2
- data/lib/metanorma/plugin/plantuml/wrapper.rb +137 -58
- data/lib/metanorma-plugin-plantuml.rb +2 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b3797ddb4db236753261836d74b360e635c58ea3b0eeaa09dfc1c7192dacf78
|
4
|
+
data.tar.gz: 0aba3ecbbc90f15570887ea206fa0a0c19b6d3179a7d9481438fb480df305873
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed86eced1c2c434f17ce4f3f5307c88f349c30275ad2051ea7c6063feb1d13ca6c64480ea406951221a75ceececd035ffed82519d9dfe169205f55a160996c9b
|
7
|
+
data.tar.gz: 2e1488a69e6c3ff71a88c17778fead0b264ed2b6c8d2a7d8803e04441d0d7f1b4b2cebdd102050e6428500a477a876d6f3cc8cdf9882da007a7bc6900a19cffa
|
data/Gemfile
CHANGED
@@ -13,9 +13,11 @@ rescue StandardError
|
|
13
13
|
end
|
14
14
|
|
15
15
|
gem "byebug"
|
16
|
+
gem "canon"
|
16
17
|
gem "debug"
|
17
18
|
gem "metanorma"
|
18
|
-
gem "metanorma-standoc", github: "metanorma/metanorma-standoc",
|
19
|
+
gem "metanorma-standoc", github: "metanorma/metanorma-standoc",
|
20
|
+
branch: "main"
|
19
21
|
gem "rake"
|
20
22
|
gem "rspec"
|
21
23
|
gem "rspec-html-matchers"
|
@@ -25,4 +27,3 @@ gem "simplecov"
|
|
25
27
|
gem "timecop"
|
26
28
|
gem "vcr"
|
27
29
|
gem "webmock"
|
28
|
-
gem "canon"
|
data/README.adoc
CHANGED
@@ -65,6 +65,8 @@ the `plantuml` block directive.
|
|
65
65
|
|
66
66
|
=== Basic syntax
|
67
67
|
|
68
|
+
==== Block style
|
69
|
+
|
68
70
|
[source,asciidoc]
|
69
71
|
----
|
70
72
|
[plantuml]
|
@@ -76,6 +78,25 @@ Bob -> Alice: Hi there
|
|
76
78
|
....
|
77
79
|
----
|
78
80
|
|
81
|
+
==== Block macro style
|
82
|
+
|
83
|
+
[source,asciidoc]
|
84
|
+
----
|
85
|
+
plantuml_image::path/to/my-plantuml.puml[]
|
86
|
+
----
|
87
|
+
|
88
|
+
The file `path/to/my-plantuml.puml` looks like:
|
89
|
+
[source,plantuml]
|
90
|
+
----
|
91
|
+
@startuml
|
92
|
+
Alice -> Bob: Hello
|
93
|
+
Bob -> Alice: Hi there
|
94
|
+
@enduml
|
95
|
+
----
|
96
|
+
|
97
|
+
`plantuml_image` block macro performs the same function as the `plantuml` block,
|
98
|
+
but the PlantUML diagram is defined in a separate file instead of a block.
|
99
|
+
|
79
100
|
=== Supported diagram types
|
80
101
|
|
81
102
|
PlantUML supports various diagram types, each with its own `@start` and `@end` directives:
|
@@ -183,6 +204,132 @@ Alice -> Bob: Hello
|
|
183
204
|
....
|
184
205
|
----
|
185
206
|
|
207
|
+
==== Document-level includedirs configuration
|
208
|
+
|
209
|
+
When using `!include` or `!includesub` in your PlantUML diagrams, you can set
|
210
|
+
the default include directories (separated by semicolons) by
|
211
|
+
`plantuml-includedirs` to search for files for all PlantUML diagrams in your
|
212
|
+
document:
|
213
|
+
|
214
|
+
[source,asciidoc]
|
215
|
+
----
|
216
|
+
:plantuml-includedirs: path/to/plantuml/include-1;path/to/plantuml/include-2
|
217
|
+
|
218
|
+
[plantuml]
|
219
|
+
....
|
220
|
+
@startuml
|
221
|
+
!include sequences.puml!1
|
222
|
+
@enduml
|
223
|
+
....
|
224
|
+
|
225
|
+
[plantuml]
|
226
|
+
....
|
227
|
+
@startuml
|
228
|
+
!include components.puml!FRONTEND
|
229
|
+
!include components.puml!BACKEND
|
230
|
+
|
231
|
+
WebApp --> APIGateway
|
232
|
+
MobileApp --> APIGateway
|
233
|
+
APIGateway --> DB
|
234
|
+
@enduml
|
235
|
+
....
|
236
|
+
|
237
|
+
[plantuml]
|
238
|
+
....
|
239
|
+
@startuml
|
240
|
+
title this contains only B and D
|
241
|
+
!includesub subpart.puml!BASIC
|
242
|
+
@enduml
|
243
|
+
....
|
244
|
+
----
|
245
|
+
|
246
|
+
This plugin will search the include directories specified by
|
247
|
+
`includedirs` options (i.e. `path/to/plantuml/include-1` and
|
248
|
+
`path/to/plantuml/include-2`) for the files referenced in `!include` or
|
249
|
+
`!includesub` directives (i.e. `sequences.puml`, `components.puml` and
|
250
|
+
`subpart.puml`).
|
251
|
+
|
252
|
+
You can also use `plantuml_image` to include external PlantUML files as images:
|
253
|
+
|
254
|
+
[source,asciidoc]
|
255
|
+
----
|
256
|
+
:plantuml-includedirs: path/to/plantuml/include-1;path/to/plantuml/include-2
|
257
|
+
|
258
|
+
plantuml_image::path/to/my-plantuml-1.puml[]
|
259
|
+
|
260
|
+
plantuml_image::path/to/my-plantuml-2.puml[]
|
261
|
+
----
|
262
|
+
|
263
|
+
The file `path/to/my-plantuml-1.puml` looks like:
|
264
|
+
[source,plantuml]
|
265
|
+
----
|
266
|
+
@startuml
|
267
|
+
!include sequences.puml!1
|
268
|
+
@enduml
|
269
|
+
----
|
270
|
+
|
271
|
+
The file `path/to/my-plantuml-2.puml` looks like:
|
272
|
+
[source,plantuml]
|
273
|
+
----
|
274
|
+
@startuml
|
275
|
+
!include components.puml!FRONTEND
|
276
|
+
!include components.puml!BACKEND
|
277
|
+
|
278
|
+
WebApp --> APIGateway
|
279
|
+
MobileApp --> APIGateway
|
280
|
+
APIGateway --> DB
|
281
|
+
@enduml
|
282
|
+
----
|
283
|
+
|
284
|
+
When using `plantuml_image`, the path of the directory of the PlantUML file will
|
285
|
+
also be added into the `includedirs`. (i.e. `path/to` will be added to
|
286
|
+
`includedirs`)
|
287
|
+
|
288
|
+
==== Block-level includedirs configuration
|
289
|
+
|
290
|
+
When using `!include` or `!includesub` in your PlantUML diagrams, you can set
|
291
|
+
the default include directories (separated by semicolons) by `includedirs` in
|
292
|
+
block-level to search for files for the PlantUML diagram defined in your block:
|
293
|
+
|
294
|
+
[source,asciidoc]
|
295
|
+
----
|
296
|
+
[plantuml,includedirs="path/to/plantuml/include-1"]
|
297
|
+
....
|
298
|
+
@startuml
|
299
|
+
!include sequences.puml!1
|
300
|
+
@enduml
|
301
|
+
....
|
302
|
+
|
303
|
+
[plantuml,includedirs="path/to/plantuml/include-2"]
|
304
|
+
....
|
305
|
+
@startuml
|
306
|
+
!include components.puml!FRONTEND
|
307
|
+
!include components.puml!BACKEND
|
308
|
+
|
309
|
+
WebApp --> APIGateway
|
310
|
+
MobileApp --> APIGateway
|
311
|
+
APIGateway --> DB
|
312
|
+
@enduml
|
313
|
+
....
|
314
|
+
----
|
315
|
+
|
316
|
+
This plugin will search `sequences.puml` in `path/to/plantuml/include-1` and
|
317
|
+
`components.puml` in `path/to/plantuml/include-2`.
|
318
|
+
|
319
|
+
The block-level `includedirs` configuration can be used together with the
|
320
|
+
document-level configuration to provide more granular control over include
|
321
|
+
paths. You can set multiple paths by separating them with semicolons.
|
322
|
+
|
323
|
+
You can also use `plantuml_image` to set `includedirs` option to include
|
324
|
+
external PlantUML files:
|
325
|
+
|
326
|
+
[source,asciidoc]
|
327
|
+
----
|
328
|
+
plantuml_image::path/to/my-plantuml-1.puml[includedirs=path/to/plantuml/include-1]
|
329
|
+
|
330
|
+
plantuml_image::path/to/my-plantuml-2.puml[includedirs=path/to/plantuml/include-2]
|
331
|
+
----
|
332
|
+
|
186
333
|
=== Image attributes
|
187
334
|
|
188
335
|
Standard AsciiDoc image attributes are supported:
|
@@ -272,7 +419,7 @@ Metanorma integration and PlantUML execution:
|
|
272
419
|
----
|
273
420
|
Metanorma Document
|
274
421
|
↓
|
275
|
-
BlockProcessor ← (processes [plantuml] blocks)
|
422
|
+
BlockProcessor ← (processes [plantuml] blocks) or ImageBlockMacroProcessor ← (processes plantuml_image::{path}[{options}] macros)
|
276
423
|
↓
|
277
424
|
Backend ← (Metanorma integration, paths, validation)
|
278
425
|
↓
|
@@ -284,6 +431,9 @@ PlantUML JAR ← (diagram generation)
|
|
284
431
|
`BlockProcessor`:: Processes `[plantuml]` blocks in Metanorma documents and
|
285
432
|
integrates with the Metanorma rendering pipeline.
|
286
433
|
|
434
|
+
`ImageBlockMacroProcessor`:: Processes `plantuml_image::{path}[{options}]` macros
|
435
|
+
in Metanorma documents and integrates with the Metanorma rendering pipeline.
|
436
|
+
|
287
437
|
`Backend`:: Handles Metanorma-specific logic including document paths, PlantUML
|
288
438
|
source validation, filename extraction, and attribute mapping.
|
289
439
|
|
data/Rakefile
CHANGED
@@ -10,7 +10,7 @@ task default: [
|
|
10
10
|
]
|
11
11
|
|
12
12
|
def uri_open(url)
|
13
|
-
require
|
13
|
+
require "open-uri"
|
14
14
|
URI.parse(url).open
|
15
15
|
end
|
16
16
|
|
@@ -29,7 +29,7 @@ file "data/plantuml.jar" do |file|
|
|
29
29
|
end
|
30
30
|
|
31
31
|
desc "Download PlantUML JAR file"
|
32
|
-
task :
|
32
|
+
task download_jar: "data/plantuml.jar"
|
33
33
|
|
34
34
|
desc "Clean downloaded JAR file"
|
35
35
|
task :clean_jar do
|
@@ -14,31 +14,38 @@ module Metanorma
|
|
14
14
|
class Backend
|
15
15
|
class << self
|
16
16
|
def plantuml_installed?
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
return true if plantuml_available?
|
18
|
+
|
19
|
+
raise "PlantUML not installed"
|
20
20
|
end
|
21
21
|
|
22
22
|
def plantuml_available?
|
23
23
|
Wrapper.available?
|
24
24
|
end
|
25
25
|
|
26
|
-
def generate_file(
|
26
|
+
def generate_file( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
27
|
+
parent, reader, format_override: nil, options: {}
|
28
|
+
)
|
27
29
|
ldir, imagesdir, fmt = generate_file_prep(parent)
|
28
30
|
fmt = format_override if format_override
|
29
|
-
plantuml_content = prep_source(reader)
|
31
|
+
plantuml_content = prep_source(parent, reader)
|
30
32
|
|
31
33
|
# Extract filename from PlantUML source if specified
|
34
|
+
filename = generate_unique_filename(fmt)
|
32
35
|
extracted_filename = extract_plantuml_filename(plantuml_content)
|
33
36
|
|
37
|
+
if extracted_filename
|
38
|
+
filename = "#{extracted_filename}.#{fmt}"
|
39
|
+
end
|
40
|
+
|
34
41
|
absolute_path, relative_path = path_prep(ldir, imagesdir)
|
35
|
-
filename = extracted_filename ? "#{extracted_filename}.#{fmt}" : generate_unique_filename(fmt)
|
36
42
|
output_file = File.join(absolute_path, filename)
|
37
43
|
|
38
44
|
result = Wrapper.generate(
|
39
45
|
plantuml_content,
|
40
46
|
format: fmt,
|
41
|
-
output_file: output_file
|
47
|
+
output_file: output_file,
|
48
|
+
includedirs: options[:includedirs],
|
42
49
|
)
|
43
50
|
|
44
51
|
unless result[:success]
|
@@ -48,10 +55,12 @@ module Metanorma
|
|
48
55
|
File.join(relative_path, filename)
|
49
56
|
end
|
50
57
|
|
51
|
-
def generate_multiple_files(
|
58
|
+
def generate_multiple_files(
|
59
|
+
parent, reader, formats, attrs, options: {}
|
60
|
+
)
|
52
61
|
# Generate files for each format
|
53
62
|
filenames = formats.map do |format|
|
54
|
-
generate_file(parent, reader, format)
|
63
|
+
generate_file(parent, reader, format, options: options)
|
55
64
|
end
|
56
65
|
|
57
66
|
# Return data for BlockProcessor to create image block
|
@@ -68,28 +77,44 @@ module Metanorma
|
|
68
77
|
def generate_file_prep(parent)
|
69
78
|
ldir = localdir(parent)
|
70
79
|
imagesdir = parent.document.attr("imagesdir")
|
71
|
-
fmt = parent.document
|
80
|
+
fmt = parent.document
|
81
|
+
.attr("plantuml-image-format")&.strip&.downcase ||
|
82
|
+
Wrapper::DEFAULT_FORMAT
|
72
83
|
[ldir, imagesdir, fmt]
|
73
84
|
end
|
74
85
|
|
75
86
|
def localdir(parent)
|
76
87
|
ret = Utils.localdir(parent.document)
|
77
|
-
File.writable?(ret)
|
78
|
-
|
79
|
-
ret
|
88
|
+
return ret if File.writable?(ret)
|
89
|
+
|
90
|
+
raise "Destination directory #{ret} not writable for PlantUML!"
|
80
91
|
end
|
81
92
|
|
82
93
|
def path_prep(localdir, imagesdir)
|
83
94
|
path = Pathname.new(File.join(localdir, "_plantuml_images"))
|
84
95
|
sourcepath = imagesdir ? File.join(localdir, imagesdir) : localdir
|
85
96
|
path.mkpath
|
86
|
-
|
97
|
+
|
98
|
+
unless File.writable?(path)
|
87
99
|
raise "Destination path #{path} not writable for PlantUML!"
|
88
|
-
|
100
|
+
end
|
101
|
+
|
102
|
+
[
|
103
|
+
path,
|
104
|
+
Pathname.new(path)
|
105
|
+
.relative_path_from(Pathname.new(sourcepath)).to_s,
|
106
|
+
]
|
89
107
|
end
|
90
108
|
|
91
|
-
def prep_source(reader)
|
92
|
-
src = reader.source
|
109
|
+
def prep_source(parent, reader) # rubocop:disable Metrics/MethodLength
|
110
|
+
src = if reader.respond_to?(:source)
|
111
|
+
# get content from BlockProcessor
|
112
|
+
reader.source
|
113
|
+
else
|
114
|
+
# get content from ImageBlockMacroProcessor
|
115
|
+
docdir = parent.document.attributes["docdir"]
|
116
|
+
File.read(File.join(docdir, reader))
|
117
|
+
end
|
93
118
|
|
94
119
|
# Validate that we have matching start/end pairs
|
95
120
|
validate_plantuml_delimiters(src)
|
@@ -106,8 +131,9 @@ module Metanorma
|
|
106
131
|
|
107
132
|
private
|
108
133
|
|
109
|
-
def validate_plantuml_delimiters(src)
|
110
|
-
# Find @start... pattern
|
134
|
+
def validate_plantuml_delimiters(src) # rubocop:disable Metrics/MethodLength
|
135
|
+
# Find @start... pattern
|
136
|
+
# (case insensitive, support all PlantUML diagram types)
|
111
137
|
start_match = src.match(/^@start(\w+)/i)
|
112
138
|
unless start_match
|
113
139
|
raise "PlantUML content must start with @start... directive!"
|
@@ -117,14 +143,16 @@ module Metanorma
|
|
117
143
|
end_pattern = "@end#{diagram_type}"
|
118
144
|
|
119
145
|
# Look for matching @end... directive (case insensitive)
|
120
|
-
unless
|
121
|
-
raise "@start#{diagram_type} without matching #{end_pattern}
|
146
|
+
unless /#{Regexp.escape(end_pattern)}\s*$/mi.match?(src)
|
147
|
+
raise "@start#{diagram_type} without matching #{end_pattern} " \
|
148
|
+
"in PlantUML!"
|
122
149
|
end
|
123
150
|
end
|
124
151
|
|
125
152
|
def extract_plantuml_filename(plantuml_content)
|
126
153
|
# Extract filename from any @start... line if specified
|
127
|
-
# Only match when filename is on the same line as @start...
|
154
|
+
# Only match when filename is on the same line as @start...
|
155
|
+
# (no newlines)
|
128
156
|
lines = plantuml_content.lines
|
129
157
|
first_line = lines.first&.strip
|
130
158
|
return nil unless first_line
|
@@ -142,11 +170,12 @@ module Metanorma
|
|
142
170
|
def sanitize_filename(filename)
|
143
171
|
# Remove quotes and sanitize for filesystem
|
144
172
|
filename = filename.gsub(/^["']|["']$/, "")
|
145
|
-
# Replace any non-alphanumeric characters
|
173
|
+
# Replace any non-alphanumeric characters
|
174
|
+
# (except dash, underscore, dot) with underscore
|
146
175
|
filename.gsub(/[^\w\-.]/, "_")
|
147
|
-
|
148
|
-
|
149
|
-
|
176
|
+
.gsub(/\.{2,}/, "_") # Replace multiple dots with underscore
|
177
|
+
.gsub(/_{2,}/, "_") # Replace multiple underscores with single
|
178
|
+
.gsub(/^[_\-.]+|[_\-.]+$/, "") # Remove leading/trailing chars
|
150
179
|
end
|
151
180
|
|
152
181
|
def generate_unique_filename(format)
|
@@ -1,30 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "asciidoctor"
|
4
|
-
require "asciidoctor/extensions"
|
5
|
-
require_relative "backend"
|
6
|
-
|
7
3
|
module Metanorma
|
8
4
|
module Plugin
|
9
5
|
module Plantuml
|
10
6
|
# PlantUML block processor for Asciidoctor
|
11
7
|
class BlockProcessor < ::Asciidoctor::Extensions::BlockProcessor
|
8
|
+
include ::Metanorma::Plugin::Plantuml::BlockProcessorBase
|
12
9
|
use_dsl
|
13
10
|
named :plantuml
|
14
11
|
on_context :literal
|
15
12
|
parse_content_as :raw
|
16
13
|
|
17
|
-
def
|
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)
|
14
|
+
def process(parent, reader, attrs) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
28
15
|
# Check for document-level disable flag
|
29
16
|
if parent.document.attr("plantuml-disabled")
|
30
17
|
return abort(parent, reader, attrs, "PlantUML processing disabled")
|
@@ -37,44 +24,25 @@ module Metanorma
|
|
37
24
|
|
38
25
|
# Parse format specifications
|
39
26
|
formats = parse_formats(attrs, parent.document)
|
27
|
+
options = parse_options(parent, reader, attrs)
|
40
28
|
|
41
|
-
|
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
|
29
|
+
process_image_block(parent, reader, attrs, formats, options)
|
52
30
|
rescue StandardError => e
|
53
31
|
abort(parent, reader, attrs, e.message)
|
54
32
|
end
|
55
33
|
|
56
34
|
private
|
57
35
|
|
58
|
-
def
|
59
|
-
|
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
|
36
|
+
def parse_options(parent, _reader, attrs)
|
37
|
+
options = {}
|
70
38
|
|
71
|
-
#
|
72
|
-
|
73
|
-
[
|
74
|
-
|
39
|
+
# Parse include directory
|
40
|
+
options[:includedirs] = parse_doc_includedirs(parent.document)
|
41
|
+
options[:includedirs] = add_attrs_to_includedirs(
|
42
|
+
parent.document, attrs, options[:includedirs]
|
43
|
+
)
|
75
44
|
|
76
|
-
|
77
|
-
%w[png svg pdf txt eps].include?(format)
|
45
|
+
options
|
78
46
|
end
|
79
47
|
end
|
80
48
|
end
|
@@ -0,0 +1,99 @@
|
|
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
|
+
module BlockProcessorBase
|
11
|
+
def abort(parent, reader, attrs, msg)
|
12
|
+
warn msg
|
13
|
+
attrs["language"] = "plantuml"
|
14
|
+
create_listing_block(
|
15
|
+
parent,
|
16
|
+
reader.respond_to?(:source) ? reader.source : reader,
|
17
|
+
attrs.reject { |k, _v| k.to_s.match?(/^\d+$/) },
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_doc_includedirs(document)
|
24
|
+
docdir = document.attributes["docdir"]
|
25
|
+
includedirs = document.attr("plantuml-includedirs")&.split(";") || []
|
26
|
+
|
27
|
+
includedirs.map! do |includedir|
|
28
|
+
if Pathname.new(includedir).relative?
|
29
|
+
Pathname.new(docdir).join(includedir).to_s
|
30
|
+
else
|
31
|
+
includedir
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
includedirs.compact.uniq
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_formats(attrs, document) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength
|
39
|
+
# Check for formats attribute (multiple formats)
|
40
|
+
if attrs["formats"]
|
41
|
+
formats = attrs["formats"].split(",").map(&:strip).map(&:downcase)
|
42
|
+
return formats.select { |f| valid_format?(f) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check for format attribute (single format override)
|
46
|
+
if attrs["format"]
|
47
|
+
format = attrs["format"].strip.downcase
|
48
|
+
return [format] if valid_format?(format)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Fall back to document attribute or default
|
52
|
+
default_format = document
|
53
|
+
.attr("plantuml-image-format")&.strip&.downcase ||
|
54
|
+
Wrapper::DEFAULT_FORMAT
|
55
|
+
|
56
|
+
[default_format]
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid_format?(format)
|
60
|
+
Wrapper::SUPPORTED_FORMATS.include?(format)
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_attrs_to_includedirs(document, attrs, includedirs)
|
64
|
+
docdir = document.attributes["docdir"]
|
65
|
+
attrs_includedirs = attrs["includedirs"]&.split(";") || []
|
66
|
+
|
67
|
+
attrs_includedirs.each do |attrs_includedir|
|
68
|
+
includedirs << if Pathname.new(attrs_includedir).relative?
|
69
|
+
Pathname.new(docdir).join(attrs_includedir).to_s
|
70
|
+
else
|
71
|
+
attrs_includedir
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
includedirs.compact.uniq
|
76
|
+
end
|
77
|
+
|
78
|
+
def process_image_block(parent, reader, attrs, formats, options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
79
|
+
if formats.length == 1
|
80
|
+
# Single format - original behavior
|
81
|
+
filename = Backend.generate_file(
|
82
|
+
parent, reader, format_override: formats.first, options: options
|
83
|
+
)
|
84
|
+
through_attrs = Backend.generate_attrs(attrs)
|
85
|
+
through_attrs["target"] = filename
|
86
|
+
else
|
87
|
+
# Multiple formats - generate multiple files
|
88
|
+
through_attrs = Backend
|
89
|
+
.generate_multiple_files(
|
90
|
+
parent, reader, formats, attrs, options: options
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
create_image_block parent, through_attrs
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -12,16 +12,21 @@ module Metanorma
|
|
12
12
|
|
13
13
|
class JarNotFoundError < PlantumlError
|
14
14
|
def initialize(jar_path = nil)
|
15
|
-
message = jar_path
|
16
|
-
|
17
|
-
|
15
|
+
message = if jar_path
|
16
|
+
"PlantUML JAR file not found at: #{jar_path}"
|
17
|
+
else
|
18
|
+
"PlantUML JAR file not found"
|
19
|
+
end
|
18
20
|
super(message)
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
24
|
class JavaNotFoundError < PlantumlError
|
23
25
|
def initialize
|
24
|
-
super(
|
26
|
+
super(
|
27
|
+
"Java runtime not found. Please ensure Java is installed and " \
|
28
|
+
"available in PATH",
|
29
|
+
)
|
25
30
|
end
|
26
31
|
end
|
27
32
|
|
@@ -34,7 +39,10 @@ module Metanorma
|
|
34
39
|
|
35
40
|
class InvalidFormatError < PlantumlError
|
36
41
|
def initialize(format, available_formats)
|
37
|
-
super(
|
42
|
+
super(
|
43
|
+
"Invalid format '#{format}'. Available formats: " \
|
44
|
+
"#{available_formats.join(', ')}",
|
45
|
+
)
|
38
46
|
end
|
39
47
|
end
|
40
48
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Metanorma
|
4
|
+
module Plugin
|
5
|
+
module Plantuml
|
6
|
+
# PlantUML block processor for Asciidoctor
|
7
|
+
class ImageBlockMacroProcessor < ::Asciidoctor::Extensions::BlockMacroProcessor
|
8
|
+
include ::Metanorma::Plugin::Plantuml::BlockProcessorBase
|
9
|
+
use_dsl
|
10
|
+
named :plantuml_image
|
11
|
+
|
12
|
+
def process(parent, reader, attrs) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
13
|
+
# Check for document-level disable flag
|
14
|
+
if parent.document.attr("plantuml-disabled")
|
15
|
+
return abort(parent, reader, attrs, "PlantUML processing disabled")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check PlantUML availability explicitly
|
19
|
+
unless Backend.plantuml_available?
|
20
|
+
return abort(parent, reader, attrs, "PlantUML not installed")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Parse format specifications
|
24
|
+
formats = parse_formats(attrs, parent.document)
|
25
|
+
options = parse_options(parent, reader, attrs)
|
26
|
+
|
27
|
+
process_image_block(parent, reader, attrs, formats, options)
|
28
|
+
rescue StandardError => e
|
29
|
+
abort(parent, reader, attrs, e.message)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def add_image_path_to_includedirs(document, image_path, includedirs)
|
35
|
+
docdir = document.attributes["docdir"]
|
36
|
+
includedirs << File.dirname(File.join(docdir, image_path))
|
37
|
+
includedirs.compact.uniq
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_options(parent, reader, attrs)
|
41
|
+
options = {}
|
42
|
+
|
43
|
+
# Parse include directory
|
44
|
+
options[:includedirs] = parse_doc_includedirs(parent.document)
|
45
|
+
options[:includedirs] = add_attrs_to_includedirs(
|
46
|
+
parent.document, attrs, options[:includedirs]
|
47
|
+
)
|
48
|
+
options[:includedirs] = add_image_path_to_includedirs(
|
49
|
+
parent.document, reader, options[:includedirs]
|
50
|
+
)
|
51
|
+
|
52
|
+
options
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -13,14 +13,6 @@ module Metanorma
|
|
13
13
|
document.attributes["docdir"] ||
|
14
14
|
File.dirname(document.attributes["docfile"] || ".")
|
15
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
16
|
end
|
25
17
|
end
|
26
18
|
end
|
@@ -12,11 +12,14 @@ module Metanorma
|
|
12
12
|
module Plugin
|
13
13
|
module Plantuml
|
14
14
|
class Wrapper
|
15
|
-
PLANTUML_JAR_NAME = "plantuml.jar"
|
16
|
-
PLANTUML_JAR_PATH = File.join(
|
15
|
+
PLANTUML_JAR_NAME = "plantuml.jar".freeze
|
16
|
+
PLANTUML_JAR_PATH = File.join(
|
17
|
+
File.dirname(__FILE__), "..", "..", "..",
|
18
|
+
"..", "data", PLANTUML_JAR_NAME
|
19
|
+
)
|
17
20
|
|
18
21
|
SUPPORTED_FORMATS = %w[png svg pdf txt eps].freeze
|
19
|
-
DEFAULT_FORMAT = "png"
|
22
|
+
DEFAULT_FORMAT = "png".freeze
|
20
23
|
|
21
24
|
class << self
|
22
25
|
def jvm_options
|
@@ -29,11 +32,19 @@ module Metanorma
|
|
29
32
|
options
|
30
33
|
end
|
31
34
|
|
32
|
-
def generate(
|
35
|
+
def generate( # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
36
|
+
content,
|
37
|
+
format: DEFAULT_FORMAT,
|
38
|
+
output_file: nil,
|
39
|
+
base64: false, **options
|
40
|
+
)
|
33
41
|
validate_format!(format)
|
34
42
|
ensure_jar_available!
|
35
43
|
ensure_java_available!
|
36
44
|
|
45
|
+
include_files = get_include_files(content, options)
|
46
|
+
options[:include_files] = include_files unless include_files.empty?
|
47
|
+
|
37
48
|
result = if output_file
|
38
49
|
generate_to_file(content, format, output_file, options)
|
39
50
|
elsif base64
|
@@ -47,27 +58,48 @@ module Metanorma
|
|
47
58
|
{ success: false, error: e }
|
48
59
|
end
|
49
60
|
|
50
|
-
def
|
61
|
+
def get_include_files(content, _options) # rubocop:disable Metrics/MethodLength
|
62
|
+
include_files = []
|
63
|
+
content.each_line do |line|
|
64
|
+
case line
|
65
|
+
when /(!include|!includesub)\s(.+){1}/
|
66
|
+
found_file = $2.split("!").first
|
67
|
+
|
68
|
+
# skip web links and standard libraries
|
69
|
+
if found_file.start_with?("<", "http")
|
70
|
+
found_file = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
include_files << found_file
|
74
|
+
end
|
75
|
+
end
|
76
|
+
include_files.compact.uniq
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_from_file(
|
80
|
+
input_file, format: DEFAULT_FORMAT,
|
81
|
+
output_file: nil, base64: false, **options
|
82
|
+
)
|
51
83
|
unless File.exist?(input_file)
|
52
84
|
raise GenerationError.new("Input file not found: #{input_file}")
|
53
85
|
end
|
54
86
|
|
55
87
|
content = File.read(input_file)
|
56
|
-
generate(content, format: format, output_file: output_file,
|
88
|
+
generate(content, format: format, output_file: output_file,
|
89
|
+
base64: base64, **options)
|
57
90
|
end
|
58
91
|
|
59
92
|
def version
|
60
93
|
return nil unless available?
|
61
94
|
|
62
|
-
cmd = [configuration.java_path, *jvm_options, "-jar",
|
63
|
-
|
95
|
+
cmd = [configuration.java_path, *jvm_options, "-jar",
|
96
|
+
PLANTUML_JAR_PATH, "-version"]
|
97
|
+
output, _, status = Open3.capture3(*cmd)
|
64
98
|
|
65
99
|
if status.success?
|
66
100
|
# Extract version from output
|
67
101
|
version_match = output.match(/PlantUML version ([\d.]+)/)
|
68
102
|
version_match ? version_match[1] : PLANTUML_JAR_VERSION
|
69
|
-
else
|
70
|
-
nil
|
71
103
|
end
|
72
104
|
rescue StandardError
|
73
105
|
nil
|
@@ -75,6 +107,7 @@ module Metanorma
|
|
75
107
|
|
76
108
|
def available?
|
77
109
|
return false if ENV["PLANTUML_DISABLED"] == "true"
|
110
|
+
|
78
111
|
File.exist?(PLANTUML_JAR_PATH) && java_available?
|
79
112
|
end
|
80
113
|
|
@@ -122,23 +155,28 @@ module Metanorma
|
|
122
155
|
execute_plantuml(content, format, output_file, options)
|
123
156
|
|
124
157
|
unless File.exist?(output_file)
|
125
|
-
raise GenerationError.new(
|
158
|
+
raise GenerationError.new(
|
159
|
+
"Output file was not created: #{output_file}",
|
160
|
+
)
|
126
161
|
end
|
127
162
|
|
128
163
|
{ output_path: File.expand_path(output_file) }
|
129
164
|
end
|
130
165
|
|
131
|
-
def generate_to_base64(content, format, options)
|
132
|
-
Tempfile.create([
|
166
|
+
def generate_to_base64(content, format, options) # rubocop:disable Metrics/MethodLength
|
167
|
+
Tempfile.create(["plantuml_output", ".#{format}"]) do |temp_file|
|
133
168
|
temp_file.close
|
134
169
|
|
135
170
|
execute_plantuml(content, format, temp_file.path, options)
|
136
171
|
|
137
172
|
unless File.exist?(temp_file.path)
|
138
|
-
raise GenerationError.new(
|
173
|
+
raise GenerationError.new(
|
174
|
+
"Temporary output file was not created",
|
175
|
+
)
|
139
176
|
end
|
140
177
|
|
141
|
-
encoded_content = Base64
|
178
|
+
encoded_content = Base64
|
179
|
+
.strict_encode64(File.read(temp_file.path))
|
142
180
|
{ base64: encoded_content }
|
143
181
|
end
|
144
182
|
end
|
@@ -151,45 +189,92 @@ module Metanorma
|
|
151
189
|
generate_to_file(content, format, output_file, options)
|
152
190
|
end
|
153
191
|
|
154
|
-
def execute_plantuml(content, format, output_file, options)
|
155
|
-
|
156
|
-
|
157
|
-
|
192
|
+
def execute_plantuml(content, format, output_file, options) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength
|
193
|
+
# PlantUML generates output files based on filename specified in
|
194
|
+
# @start... line
|
195
|
+
# We need to use a temp directory and then move the file
|
196
|
+
Dir.mktmpdir do |temp_dir| # rubocop:disable Metrics/BlockLength
|
197
|
+
# create input file
|
198
|
+
File.open("#{temp_dir}/plantuml_input.puml", "w") do |f|
|
199
|
+
f.write(content)
|
200
|
+
end
|
158
201
|
|
159
|
-
#
|
160
|
-
|
161
|
-
|
162
|
-
|
202
|
+
# Handle include files
|
203
|
+
if options[:include_files] && !options[:include_files].empty?
|
204
|
+
if options[:includedirs].empty?
|
205
|
+
# raise error when include files are found but includedirs
|
206
|
+
# is nil
|
207
|
+
raise PlantumlError.new(
|
208
|
+
"includedirs is required when include files are specified",
|
209
|
+
)
|
210
|
+
end
|
163
211
|
|
164
|
-
|
212
|
+
options[:include_files].each do |include_file|
|
213
|
+
# find local include file in includedirs
|
214
|
+
found_include_file = nil
|
215
|
+
options[:includedirs].each do |includedir|
|
216
|
+
include_file_path = File.join(includedir, include_file)
|
217
|
+
if File.exist?(include_file_path)
|
218
|
+
found_include_file = include_file_path
|
219
|
+
break
|
220
|
+
end
|
221
|
+
end
|
165
222
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
223
|
+
if found_include_file
|
224
|
+
# create include file in temp directory
|
225
|
+
temp_include_file = File.join(temp_dir, include_file)
|
226
|
+
FileUtils.mkdir_p(File.dirname(temp_include_file))
|
170
227
|
|
171
|
-
|
172
|
-
|
173
|
-
|
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)
|
228
|
+
File.open(temp_include_file, "w") do |f|
|
229
|
+
f.write(File.read(found_include_file))
|
230
|
+
end
|
183
231
|
end
|
184
232
|
end
|
233
|
+
end
|
185
234
|
|
186
|
-
|
235
|
+
cmd = build_command(
|
236
|
+
"#{temp_dir}/plantuml_input.puml",
|
237
|
+
format,
|
238
|
+
temp_dir,
|
239
|
+
options,
|
240
|
+
)
|
241
|
+
|
242
|
+
output, error, status = Open3.capture3(*cmd)
|
243
|
+
|
244
|
+
unless status.success?
|
245
|
+
error_message = if error.empty?
|
246
|
+
"Unknown PlantUML error"
|
247
|
+
else
|
248
|
+
error.strip
|
249
|
+
end
|
250
|
+
raise GenerationError.new(error_message, error)
|
187
251
|
end
|
252
|
+
|
253
|
+
# Find the generated file and move it to the desired location
|
254
|
+
if output_file
|
255
|
+
generated_file = find_generated_file(temp_dir, content,
|
256
|
+
format)
|
257
|
+
if generated_file && File.exist?(generated_file)
|
258
|
+
FileUtils.mv(generated_file, output_file)
|
259
|
+
else
|
260
|
+
# Debug: List what files were actually generated
|
261
|
+
generated_files = Dir.glob(File.join(temp_dir, "*"))
|
262
|
+
error_msg = "Generated file not found in temp directory. "
|
263
|
+
error_msg += "Expected: #{generated_file}. "
|
264
|
+
error_msg += "Found files: #{generated_files.map do |f|
|
265
|
+
File.basename(f)
|
266
|
+
end.join(', ')}"
|
267
|
+
raise GenerationError.new(error_msg)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
output
|
188
272
|
end
|
189
273
|
end
|
190
274
|
|
191
|
-
def find_generated_file(temp_dir, content, format)
|
192
|
-
# PlantUML generates files based on the filename specified in
|
275
|
+
def find_generated_file(temp_dir, content, format) # rubocop:disable Metrics/MethodLength
|
276
|
+
# PlantUML generates files based on the filename specified in
|
277
|
+
# @start... line
|
193
278
|
extension = format.to_s.downcase
|
194
279
|
|
195
280
|
# First, try to extract filename from PlantUML content
|
@@ -197,17 +282,19 @@ module Metanorma
|
|
197
282
|
|
198
283
|
if plantuml_filename
|
199
284
|
# Look for file with PlantUML-specified name
|
200
|
-
generated_file = File.join(temp_dir,
|
285
|
+
generated_file = File.join(temp_dir,
|
286
|
+
"#{plantuml_filename}.#{extension}")
|
201
287
|
return generated_file if File.exist?(generated_file)
|
202
288
|
end
|
203
289
|
|
204
|
-
# Fallback: scan directory for any generated files with
|
290
|
+
# Fallback: scan directory for any generated files with
|
291
|
+
# the correct extension
|
205
292
|
Dir.glob(File.join(temp_dir, "*.#{extension}")).first
|
206
293
|
end
|
207
294
|
|
208
295
|
def extract_plantuml_filename_from_content(content)
|
209
296
|
# Extract the raw filename from @start... line (don't sanitize)
|
210
|
-
match = content.match(/^@start\w+\s+(
|
297
|
+
match = content.match(/^@start\w+\s+(.{1,999})$/mi)
|
211
298
|
return nil unless match
|
212
299
|
|
213
300
|
filename = match[1].strip.split("\n").first&.strip
|
@@ -217,7 +304,7 @@ module Metanorma
|
|
217
304
|
filename.gsub(/^["']|["']$/, "")
|
218
305
|
end
|
219
306
|
|
220
|
-
def build_command(input_file, format, output_dir,
|
307
|
+
def build_command(input_file, format, output_dir, _options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
221
308
|
cmd = [
|
222
309
|
configuration.java_path,
|
223
310
|
*jvm_options,
|
@@ -225,17 +312,9 @@ module Metanorma
|
|
225
312
|
]
|
226
313
|
|
227
314
|
# Add format-specific options
|
228
|
-
|
229
|
-
|
230
|
-
cmd << "-
|
231
|
-
when "svg"
|
232
|
-
cmd << "-tsvg"
|
233
|
-
when "pdf"
|
234
|
-
cmd << "-tpdf"
|
235
|
-
when "txt"
|
236
|
-
cmd << "-ttxt"
|
237
|
-
when "eps"
|
238
|
-
cmd << "-teps"
|
315
|
+
format_str = format.to_s.downcase
|
316
|
+
if SUPPORTED_FORMATS.include?(format_str)
|
317
|
+
cmd << "-t#{format_str}"
|
239
318
|
end
|
240
319
|
|
241
320
|
# Add output directory option
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
module Metanorma
|
3
2
|
module Plugin
|
4
3
|
module Plantuml
|
@@ -12,4 +11,6 @@ require "metanorma/plugin/plantuml/config"
|
|
12
11
|
require "metanorma/plugin/plantuml/wrapper"
|
13
12
|
require "metanorma/plugin/plantuml/utils"
|
14
13
|
require "metanorma/plugin/plantuml/backend"
|
14
|
+
require "metanorma/plugin/plantuml/block_processor_base"
|
15
15
|
require "metanorma/plugin/plantuml/block_processor"
|
16
|
+
require "metanorma/plugin/plantuml/image_block_macro_processor"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: metanorma-plugin-plantuml
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: asciidoctor
|
@@ -58,8 +58,10 @@ files:
|
|
58
58
|
- lib/metanorma-plugin-plantuml.rb
|
59
59
|
- lib/metanorma/plugin/plantuml/backend.rb
|
60
60
|
- lib/metanorma/plugin/plantuml/block_processor.rb
|
61
|
+
- lib/metanorma/plugin/plantuml/block_processor_base.rb
|
61
62
|
- lib/metanorma/plugin/plantuml/config.rb
|
62
63
|
- lib/metanorma/plugin/plantuml/errors.rb
|
64
|
+
- lib/metanorma/plugin/plantuml/image_block_macro_processor.rb
|
63
65
|
- lib/metanorma/plugin/plantuml/utils.rb
|
64
66
|
- lib/metanorma/plugin/plantuml/version.rb
|
65
67
|
- lib/metanorma/plugin/plantuml/wrapper.rb
|