node-plantuml-2 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.
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ if (require.main === module) {
5
+ require('./lib/node-plantuml-cmd')
6
+ } else {
7
+ module.exports = require('./lib/node-plantuml')
8
+ }
@@ -0,0 +1,110 @@
1
+ 'use strict'
2
+
3
+ var fs = require('fs')
4
+ var os = require('os')
5
+ var commander = require('commander')
6
+ var plantuml = require('./node-plantuml')
7
+ var pack = require('../package.json')
8
+
9
+ function getFormatFromOptions (options) {
10
+ if (options.unicode) {
11
+ return 'unicode'
12
+ } else if (options.ascii) {
13
+ return 'ascii'
14
+ } else if (options.svg) {
15
+ return 'svg'
16
+ } else if (options.eps) {
17
+ return 'eps'
18
+ }
19
+ return 'png'
20
+ }
21
+
22
+ function generate (file, options) {
23
+ options.format = getFormatFromOptions(options)
24
+ var gen
25
+ if (file) {
26
+ gen = plantuml.generate(file, options)
27
+ } else if (options.text) {
28
+ options.text = '@startuml' + os.EOL + options.text + os.EOL + '@enduml'
29
+ gen = plantuml.generate(options.text, options)
30
+ } else {
31
+ gen = plantuml.generate(options)
32
+ process.stdin.pipe(gen.in)
33
+ }
34
+
35
+ if (options.output) {
36
+ var fileStream = fs.createWriteStream(options.output)
37
+ gen.out.pipe(fileStream)
38
+ } else {
39
+ gen.out.pipe(process.stdout)
40
+ }
41
+ }
42
+
43
+ function encode (file, options) {
44
+ var en
45
+ if (file) {
46
+ en = plantuml.encode(file)
47
+ } else if (options.text) {
48
+ en = plantuml.encode(options.text)
49
+ } else {
50
+ en = plantuml.encode()
51
+ process.stdin.pipe(en.in)
52
+ }
53
+
54
+ if (en) {
55
+ en.out.on('data', function (chunk) { process.stdout.write(chunk) })
56
+ en.out.on('end', function () {
57
+ process.stdout.write('\n')
58
+ })
59
+ }
60
+ }
61
+
62
+ function decode (url, options) {
63
+ var de = plantuml.decode(url)
64
+ de.out.pipe(process.stdout)
65
+ }
66
+
67
+ function testdot () {
68
+ var dot = plantuml.testdot()
69
+ dot.out.pipe(process.stdout)
70
+ }
71
+
72
+ commander.version(pack.version)
73
+
74
+ commander
75
+ .command('generate [file]')
76
+ .description('Generate an UML diagram from PlantUML source')
77
+ .option('-p, --png', 'ouput an UML diagram as a PNG image')
78
+ .option('-s, --svg', 'ouput an UML diagram as an SVG image')
79
+ .option('-e, --eps', 'ouput an UML diagram as an EPS image')
80
+ .option('-u, --unicode', 'ouput an UML diagram in unicode text')
81
+ .option('-a, --ascii', 'ouput an UML diagram in ASCII text')
82
+ .option('-o --output [file]', 'the file in which to save the diagram')
83
+ .option('-c, --config [file]', 'config file read before the diagram')
84
+ .option('-t, --text [text]', 'UML text to generate from')
85
+ .option('-d, --dot [file]', 'specify Graphviz dot executable')
86
+ .option('-i, --include [path]', 'specify the path to include from')
87
+ .option('-C, --charset [charset]', 'specify the charset of PlantUML source')
88
+ .action(generate)
89
+
90
+ commander
91
+ .command('encode [file]')
92
+ .description('Encodes PlantUML source')
93
+ .option('-t, --text [text]', 'UML text to encode')
94
+ .action(encode)
95
+
96
+ commander
97
+ .command('decode <url>')
98
+ .description('Decodes PlantUML source')
99
+ .action(decode)
100
+
101
+ commander
102
+ .command('testdot')
103
+ .description('Test the installation of Graphviz dot')
104
+ .action(testdot)
105
+
106
+ commander.parse(process.argv)
107
+
108
+ if (!process.argv.slice(2).length || process.argv[2] === 'help') {
109
+ commander.outputHelp()
110
+ }
@@ -0,0 +1,446 @@
1
+ 'use strict'
2
+
3
+ var plantumlExecutor = require('./plantuml-executor')
4
+ var fs = require('fs')
5
+ var stream = require('stream')
6
+ var util = require('util')
7
+ var path = require('path')
8
+ var plantumlEncoder = require('plantuml-encoder')
9
+ var syntaxFixer = require('./plantuml-syntax-fixer')
10
+
11
+ var DECODE = '-decodeurl'
12
+ var PIPE = '-pipe'
13
+ var UNICODE = '-tutxt'
14
+ var ASCII = '-ttxt'
15
+ var SVG = '-tsvg'
16
+ var EPS = '-eps'
17
+ var CONFIG = '-config'
18
+ var TESTDOT = '-testdot'
19
+ var DOT = '-graphvizdot'
20
+ var CHARSET = '-charset'
21
+
22
+ var CONFIGS = {
23
+ classic: path.join(__dirname, '../resources/classic.puml'),
24
+ monochrome: path.join(__dirname, '../resources/monochrome.puml')
25
+ }
26
+
27
+ module.exports.useNailgun = plantumlExecutor.useNailgun
28
+
29
+ function PlantumlEncodeStream () {
30
+ stream.Transform.call(this)
31
+ this.chunks = []
32
+ }
33
+
34
+ util.inherits(PlantumlEncodeStream, stream.Transform)
35
+
36
+ PlantumlEncodeStream.prototype._transform = function (chunk, encoding, done) {
37
+ this.chunks.push(chunk)
38
+ done()
39
+ }
40
+
41
+ PlantumlEncodeStream.prototype._flush = function (done) {
42
+ var uml = Buffer.concat(this.chunks).toString()
43
+ var encoded = plantumlEncoder.encode(uml)
44
+ this.push(Buffer.from(encoded, 'ascii'))
45
+ done()
46
+ }
47
+
48
+ function isPath (input) {
49
+ try {
50
+ fs.lstatSync(input)
51
+ return true
52
+ } catch (e) {
53
+ return false
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Detect if text contains characters that need special font support
59
+ * (Chinese, Japanese, Korean, etc.)
60
+ */
61
+ function needsFontSupport (text) {
62
+ if (!text || typeof text !== 'string') {
63
+ return false
64
+ }
65
+ // Check for CJK characters (Chinese, Japanese Hiragana/Katakana, Korean)
66
+ return /[\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(text)
67
+ }
68
+
69
+ /**
70
+ * Detect if text contains Korean characters
71
+ */
72
+ function hasKorean (text) {
73
+ if (!text || typeof text !== 'string') {
74
+ return false
75
+ }
76
+ // Korean Hangul Syllables: \uac00-\ud7af
77
+ return /[\uac00-\ud7af]/.test(text)
78
+ }
79
+
80
+ /**
81
+ * Get default font name based on platform and detected languages
82
+ * @param {string} code - PlantUML source code to detect languages from
83
+ */
84
+ function getDefaultFont (code) {
85
+ var platform = process.platform
86
+ var hasKR = hasKorean(code || '')
87
+
88
+ if (platform === 'win32') {
89
+ // Windows: Use Malgun Gothic (맑은 고딕) for Korean, Microsoft YaHei for CJ
90
+ if (hasKR) {
91
+ return 'Malgun Gothic'
92
+ }
93
+ return 'Microsoft YaHei'
94
+ } else if (platform === 'darwin') {
95
+ // macOS: Use AppleGothic for Korean, PingFang SC for CJ
96
+ if (hasKR) {
97
+ return 'AppleGothic'
98
+ }
99
+ return 'PingFang SC'
100
+ } else {
101
+ // Linux: Use Noto Sans CJK (supports all CJK languages)
102
+ return 'Noto Sans CJK SC'
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Add font configuration to PlantUML code if needed
108
+ * @param {string} code - PlantUML source code
109
+ * @param {Object} options - Options object (may contain fontName)
110
+ * @returns {string} - Modified code with font config if needed
111
+ */
112
+ function addFontConfigIfNeeded (code, options) {
113
+ if (!code || typeof code !== 'string') {
114
+ return code
115
+ }
116
+
117
+ // If user explicitly specified a font, don't override
118
+ if (options && options.fontName) {
119
+ return code
120
+ }
121
+
122
+ // Check if font config already exists
123
+ if (code.includes('defaultFontName') || code.includes('skinparam defaultFontName')) {
124
+ return code
125
+ }
126
+
127
+ // Check if code needs font support
128
+ if (!needsFontSupport(code)) {
129
+ return code
130
+ }
131
+
132
+ // Get default font for platform, considering detected languages in code
133
+ var fontName = getDefaultFont(code)
134
+ var fontSize = options && options.fontSize ? options.fontSize : 12
135
+
136
+ // Add font configuration
137
+ // Priority: after !theme (if exists), otherwise after @startuml/@startgantt/@startmindmap
138
+ var fontConfig = 'skinparam defaultFontName "' + fontName + '"\n'
139
+
140
+ // Check if fontSize is already set, if not add it
141
+ if (!code.includes('defaultFontSize') && !code.includes('skinparam defaultFontSize')) {
142
+ fontConfig += 'skinparam defaultFontSize ' + fontSize + '\n'
143
+ }
144
+
145
+ // Try to insert after !theme first (theme may override fonts, so we add after it)
146
+ var themePattern = /(!theme\s+[^\n]+\n)/i
147
+ if (themePattern.test(code)) {
148
+ return code.replace(themePattern, '$1' + fontConfig)
149
+ }
150
+
151
+ // Otherwise, insert after @startuml, @startgantt, or @startmindmap
152
+ var patterns = [
153
+ /(@startuml\s*\n)/i,
154
+ /(@startgantt\s*\n)/i,
155
+ /(@startmindmap\s*\n)/i
156
+ ]
157
+
158
+ for (var i = 0; i < patterns.length; i++) {
159
+ if (patterns[i].test(code)) {
160
+ return code.replace(patterns[i], '$1' + fontConfig)
161
+ }
162
+ }
163
+
164
+ // If no start directive found, prepend to the beginning
165
+ return fontConfig + code
166
+ }
167
+
168
+ function arrangeArguments (input, options, callback) {
169
+ if (typeof input === 'function') {
170
+ callback = input
171
+ input = undefined
172
+ } else {
173
+ if (typeof options === 'function') {
174
+ callback = options
175
+ options = undefined
176
+ }
177
+ if (typeof input !== 'string' && !(input instanceof String)) {
178
+ options = input
179
+ input = undefined
180
+ }
181
+ }
182
+
183
+ return {
184
+ input: input,
185
+ options: options,
186
+ callback: callback
187
+ }
188
+ }
189
+
190
+ function joinOptions (argv, options) {
191
+ options.format = options.format || 'png'
192
+ switch (options.format) {
193
+ case 'ascii':
194
+ argv.push(ASCII)
195
+ break
196
+ case 'unicode':
197
+ argv.push(UNICODE)
198
+ break
199
+ case 'svg':
200
+ argv.push(SVG)
201
+ break
202
+ case 'eps':
203
+ argv.push(EPS)
204
+ break
205
+ case 'png':
206
+ default:
207
+ break
208
+ }
209
+
210
+ if (options.config) {
211
+ var template = CONFIGS[options.config]
212
+ var file = template || options.config
213
+ argv.push(CONFIG)
214
+ argv.push(file)
215
+ }
216
+
217
+ if (options.dot) {
218
+ argv.push(DOT)
219
+ argv.push(options.dot)
220
+ }
221
+
222
+ if (options.charset) {
223
+ argv.push(CHARSET)
224
+ argv.push(options.charset)
225
+ }
226
+
227
+ return argv
228
+ }
229
+
230
+ // function generateFromStdin (child) {
231
+ // return {
232
+ // in: child.stdin,
233
+ // out: child.stdout
234
+ // }
235
+ // }
236
+
237
+ function generateFromFile (filePath, child, options) {
238
+ // Read file content, fix syntax if enabled, add font config if needed, then write to stdin
239
+ var code = fs.readFileSync(filePath, 'utf-8')
240
+ code = syntaxFixer.fixPlantUmlSyntax(code, options)
241
+ code = addFontConfigIfNeeded(code, options)
242
+ child.stdin.write(code, 'utf-8')
243
+ child.stdin.end()
244
+
245
+ return {
246
+ out: child.stdout
247
+ }
248
+ }
249
+
250
+ function generateFromText (text, child, options) {
251
+ text = syntaxFixer.fixPlantUmlSyntax(text, options)
252
+ text = addFontConfigIfNeeded(text, options)
253
+ child.stdin.write(text)
254
+ child.stdin.end()
255
+
256
+ return {
257
+ out: child.stdout
258
+ }
259
+ }
260
+
261
+ module.exports.generate = function (input, options, callback) {
262
+ var args = arrangeArguments(input, options, callback)
263
+ input = args.input
264
+ options = args.options || {}
265
+ callback = args.callback
266
+
267
+ var o = joinOptions([PIPE], options)
268
+ var child = plantumlExecutor.exec(o, options.include, callback)
269
+
270
+ if (!input) {
271
+ // For stdin, we need to handle font config in a transform stream
272
+ var Transform = stream.Transform
273
+ var fontTransform = new Transform({
274
+ transform: function (chunk, encoding, done) {
275
+ // Collect all chunks first
276
+ if (!this._chunks) {
277
+ this._chunks = []
278
+ }
279
+ this._chunks.push(chunk)
280
+ done()
281
+ },
282
+ flush: function (done) {
283
+ var code = Buffer.concat(this._chunks).toString('utf-8')
284
+ code = syntaxFixer.fixPlantUmlSyntax(code, options)
285
+ code = addFontConfigIfNeeded(code, options)
286
+ this.push(Buffer.from(code, 'utf-8'))
287
+ done()
288
+ }
289
+ })
290
+ fontTransform.pipe(child.stdin)
291
+ return {
292
+ in: fontTransform,
293
+ out: child.stdout
294
+ }
295
+ } else {
296
+ if (isPath(input)) {
297
+ return generateFromFile(input, child, options)
298
+ } else {
299
+ return generateFromText(input, child, options)
300
+ }
301
+ }
302
+ }
303
+
304
+ function encodeFromStdin (encodeStream) {
305
+ return {
306
+ in: encodeStream,
307
+ out: encodeStream
308
+ }
309
+ }
310
+
311
+ function encodeFromFile (path, encodeStream) {
312
+ var rs = fs.createReadStream(path)
313
+ rs.pipe(encodeStream)
314
+
315
+ return {
316
+ out: encodeStream
317
+ }
318
+ }
319
+
320
+ function encodeFromText (text, encodeStream) {
321
+ encodeStream.write(text)
322
+ encodeStream.end()
323
+
324
+ return {
325
+ out: encodeStream
326
+ }
327
+ }
328
+
329
+ module.exports.encode = function (input, options, callback) {
330
+ var args = arrangeArguments(input, options, callback)
331
+ input = args.input
332
+ options = args.options || {}
333
+ callback = args.callback
334
+
335
+ var encodeStream = new PlantumlEncodeStream()
336
+
337
+ if (typeof callback === 'function') {
338
+ var chunks = []
339
+ encodeStream.on('data', function (chunk) { chunks.push(chunk) })
340
+ encodeStream.on('end', function () {
341
+ var data = Buffer.concat(chunks)
342
+ callback(null, data.toString())
343
+ })
344
+ }
345
+
346
+ if (!input) {
347
+ return encodeFromStdin(encodeStream)
348
+ } else {
349
+ if (isPath(input)) {
350
+ return encodeFromFile(input, encodeStream)
351
+ } else {
352
+ return encodeFromText(input, encodeStream)
353
+ }
354
+ }
355
+ }
356
+
357
+ module.exports.decode = function (encoded, callback) {
358
+ var child = plantumlExecutor.exec([DECODE, encoded], callback)
359
+
360
+ return {
361
+ out: child.stdout
362
+ }
363
+ }
364
+
365
+ module.exports.testdot = function (callback) {
366
+ var child = plantumlExecutor.exec([TESTDOT])
367
+
368
+ var chunks = []
369
+ child.stdout.on('data', function (chunk) { chunks.push(chunk) })
370
+ child.stdout.on('end', function () {
371
+ var data = Buffer.concat(chunks)
372
+ var dotOkCheck = 'Installation seems OK. File generation OK'
373
+ var dotOk = data.toString().indexOf(dotOkCheck) !== -1
374
+ if (typeof callback === 'function') callback(dotOk)
375
+ })
376
+
377
+ return {
378
+ out: child.stdout
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Standalone syntax fixing service
384
+ * Checks if code has syntax errors, and if so, attempts to fix them
385
+ * @param {string} code - PlantUML source code
386
+ * @param {Object} options - Options object
387
+ * @param {Function} callback - Callback with (error, fixedCode, wasFixed)
388
+ * @returns {void}
389
+ */
390
+ module.exports.fixSyntax = function (code, options, callback) {
391
+ if (typeof options === 'function') {
392
+ callback = options
393
+ options = {}
394
+ }
395
+
396
+ options = options || {}
397
+
398
+ if (!code || typeof code !== 'string') {
399
+ if (typeof callback === 'function') {
400
+ return callback(null, code, false)
401
+ }
402
+ return
403
+ }
404
+
405
+ // First, check if there's a syntax error
406
+ syntaxFixer.checkSyntaxError(code, function (err, hasError, svgOutput) {
407
+ if (err) {
408
+ // If check failed, assume there's an error and try to fix
409
+ hasError = true
410
+ }
411
+
412
+ // If no error detected, return original code unchanged
413
+ if (!hasError) {
414
+ if (typeof callback === 'function') {
415
+ return callback(null, code, false)
416
+ }
417
+ return
418
+ }
419
+
420
+ // There's an error, try to fix it
421
+ var fixOptions = {
422
+ autoFix: true,
423
+ warnOnFix: options.warnOnFix !== false,
424
+ normalizeWhitespace: options.normalizeWhitespace !== false
425
+ }
426
+
427
+ var fixedCode = syntaxFixer.fixPlantUmlSyntax(code, fixOptions)
428
+
429
+ // Check again if the fixed code works
430
+ syntaxFixer.checkSyntaxError(fixedCode, function (err2, stillHasError, svgOutput2) {
431
+ if (err2) {
432
+ // If second check failed, still return the fixed code
433
+ if (typeof callback === 'function') {
434
+ return callback(null, fixedCode, true)
435
+ }
436
+ return
437
+ }
438
+
439
+ // If fixed code still has errors, return it anyway (we tried our best)
440
+ // If fixed code works, return it
441
+ if (typeof callback === 'function') {
442
+ return callback(null, fixedCode, true)
443
+ }
444
+ })
445
+ })
446
+ }