dsh-drawai 0.1.1 → 0.2.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.
@@ -41,6 +41,7 @@
41
41
  // ── 规范键序:落盘顺序稳定、diff 友好;不在表里的键(开放集合)按插入序排在后面 ──
42
42
  var STYLE_KEY_ORDER = [
43
43
  'shape', 'ellipse', 'rhombus', 'rounded', 'arcSize', 'curved', 'direction', 'flipH', 'flipV',
44
+ 'imageAspect', 'image', 'imageBackground', 'imageBorder',
44
45
  'whiteSpace', 'html', 'fontSize', 'fontFamily', 'fontColor', 'fontStyle',
45
46
  'fillColor', 'strokeColor', 'strokeWidth', 'dashed', 'dashPattern',
46
47
  'edgeStyle', 'orthogonalLoop', 'jettySize', 'sourceJettySize', 'targetJettySize',
@@ -86,14 +87,32 @@ var NODE_SHAPE_STYLE = {
86
87
  cylinder: 'shape=cylinder3',
87
88
  document: 'shape=document',
88
89
  hexagon: 'shape=hexagon',
90
+ // svg:节点内容是一段**内嵌 SVG 标记**(不是标签文字),见下面 SVG 那一节。
91
+ // imageAspect=0 = 拉伸铺满节点框(drawio 插入图片时写的也是 0,见 Actions.js:1848);
92
+ // 想等比缩放居中就显式写 imageAspect=1。
93
+ svg: 'shape=image;imageAspect=0',
94
+ // image:内嵌位图(png/jpeg/gif/webp 的 data URI)。落盘同样是 drawio 的图片形状;
95
+ // 缺省 imageAspect=1(等比居中)—— 截图/照片拉伸会变形,和 svg 图标的缺省相反。
96
+ image: 'shape=image;imageAspect=1',
97
+ // 分区(飞书式区块 / drawio 的 group 容器):可插入的容器元素,不是"选中后成组"。
98
+ // verticalAlign=top + align=left:标题贴在框顶部左侧(区块名),里面再放其它节点。
99
+ section:
100
+ 'group;container=1;collapsible=0;recursiveResize=0;fillColor=#f5f5f5;strokeColor=#9aa0a6;dashed=1;dashPattern=6 3;verticalAlign=top;align=left;spacingLeft=10;spacingTop=6;fontStyle=0',
89
101
  }
90
102
 
91
103
  /** 形状枚举的顺序(UI 菜单、read 输出的稳定顺序)。顺序对齐 drawio 的形状面板。 */
92
- var NODE_SHAPES = ['rect', 'rounded', 'text', 'stadium', 'ellipse', 'diamond', 'parallelogram', 'cylinder', 'document', 'hexagon']
104
+ var NODE_SHAPES = ['rect', 'rounded', 'text', 'section', 'stadium', 'ellipse', 'diamond', 'parallelogram', 'cylinder', 'document', 'hexagon', 'svg', 'image']
93
105
 
94
106
  /** 文字形状独有的"隐形"设置:离开文字形状时要清掉(它们不该跟着变成别的形状)。 */
95
107
  var TEXT_SHAPE_ONLY_KEYS = { strokeColor: 'none', fillColor: 'none' }
96
108
 
109
+ /** SVG / 位图形状独有的键:离开这类形状时要清掉(否则文件里留着一大段看不见的 data URI)。 */
110
+ var SVG_SHAPE_ONLY_KEYS = ['image', 'imageAspect']
111
+ var IMAGE_SHAPE_ONLY_KEYS = SVG_SHAPE_ONLY_KEYS
112
+
113
+ /** 分区形状独有的键:离开 section 时要清掉(否则普通矩形仍带着 container=1)。 */
114
+ var SECTION_SHAPE_ONLY_KEYS = ['group', 'container', 'collapsible', 'recursiveResize', 'spacingLeft', 'spacingTop', 'fontStyle']
115
+
97
116
  /** 线型枚举 → style 键。solid 是缺省,落盘即"没有 dashed 键"。 */
98
117
  var DASH_PRESETS = {
99
118
  solid: null,
@@ -167,6 +186,17 @@ function parseStyle(text) {
167
186
  var parts = String(text).split(';')
168
187
  for (var p = 0; p < parts.length; p += 1) {
169
188
  var part = parts[p].trim()
189
+ // 内嵌图片的 data URI 里**自带一个分号**(`data:image/png;base64,iVBOR…`),
190
+ // 按 ';' 硬分会把 `base64,iVBOR…` 当成一个独立的键 —— 拼回去就变成
191
+ // `…;base64,iVBOR…=1;`,那张图片没了(实测:任何一次 setStyle 都会把它写坏)。
192
+ // base64 的字母表里没有 ';',所以"base64,"开头的那一段一定是上一段的续行。
193
+ if (part.indexOf('base64,') === 0 && p > 0) {
194
+ var lastKey = lastKeyOf(out)
195
+ if (lastKey !== null) {
196
+ out[lastKey] = out[lastKey] + ';' + part
197
+ continue
198
+ }
199
+ }
170
200
  if (part.length === 0) continue
171
201
  var eq = part.indexOf('=')
172
202
  if (eq < 0) out[part] = '1'
@@ -179,6 +209,12 @@ function parseStyle(text) {
179
209
  return out
180
210
  }
181
211
 
212
+ /** 对象里最后插入的键(parseStyle 合并 `;base64,` 续行时要找"上一段是谁")。 */
213
+ function lastKeyOf(map) {
214
+ var names = Object.keys(map)
215
+ return names.length === 0 ? null : names[names.length - 1]
216
+ }
217
+
182
218
  /** 把键值对象拼回 style 串:规范键序 + 未知键按插入序 + 结尾分号。空对象 → 空串。 */
183
219
  function formatStyle(value) {
184
220
  var map = parseStyle(value)
@@ -257,7 +293,21 @@ function styleIsEmpty(style) {
257
293
  function nodeShapeFromStyle(style) {
258
294
  // 独立文字:drawio 用裸键 `text`(不是 `shape=text`)表达,所以要单独认一次。
259
295
  if (styleGet(style, 'text', null) !== null) return 'text'
296
+ // 分区 / 分组容器:drawio 用裸键 `group` + `container=1`(不是 shape=group)。
297
+ if (styleGet(style, 'group', null) !== null) return 'section'
298
+ if (styleGet(style, 'container', null) === '1' && styleGet(style, 'swimlane', null) === null) return 'section'
260
299
  var shape = styleGet(style, 'shape', null)
300
+ // 图片形状:`shape=image`(或 drawio 的裸键写法留下的 `image=` data URI)。
301
+ // · SVG data URI → 工具形状 `svg`(可编辑标记)
302
+ // · 位图 data URI(png/jpeg/gif/webp)→ 工具形状 `image`(画布用 <image> 画出来)
303
+ // · 外链/相对路径 → 仍落回 rect(文件读不到,notes 里说明按矩形显示)
304
+ var imageVal = styleGet(style, 'image', null)
305
+ var looksLikeImageShape =
306
+ shape === 'image' || (typeof imageVal === 'string' && imageVal.indexOf('data:image/') === 0)
307
+ if (looksLikeImageShape) {
308
+ if (isSvgDataUri(imageVal)) return 'svg'
309
+ if (isRasterDataUri(imageVal)) return 'image'
310
+ }
261
311
  if (shape === 'parallelogram') return 'parallelogram'
262
312
  if (shape === 'cylinder3' || shape === 'cylinder') return 'cylinder'
263
313
  if (shape === 'document') return 'document'
@@ -282,6 +332,9 @@ function styleFromNodeShape(shape) {
282
332
  function styleWithNodeShape(style, shape) {
283
333
  var map = parseStyle(style)
284
334
  var wasText = nodeShapeFromStyle(style) === 'text'
335
+ var wasSvg = nodeShapeFromStyle(style) === 'svg'
336
+ var wasImage = nodeShapeFromStyle(style) === 'image'
337
+ var wasSection = nodeShapeFromStyle(style) === 'section'
285
338
  var shapeKeys = ['shape', 'ellipse', 'rhombus', 'rounded', 'arcSize', 'text']
286
339
  for (var i = 0; i < shapeKeys.length; i += 1) delete map[shapeKeys[i]]
287
340
  // 从"文字"换成别的形状:把它的隐形设置也清掉,否则换出来的矩形/椭圆是看不见的
@@ -292,12 +345,163 @@ function styleWithNodeShape(style, shape) {
292
345
  if (String(map[noneKeys[n]]).toLowerCase() === TEXT_SHAPE_ONLY_KEYS[noneKeys[n]]) delete map[noneKeys[n]]
293
346
  }
294
347
  }
348
+ // 同理:从"SVG 图形 / 位图"换成别的形状时,把那段内嵌 data URI 一起清掉 —— 留着它
349
+ // 没人看得见,却会让文件白白胖一大截(而且下次换回会突然冒出一张旧图)。
350
+ // svg ↔ image 互切也要清:一个是标记、一个是位图,不能共用同一段 image=。
351
+ if ((wasSvg || wasImage) && shape !== 'svg' && shape !== 'image') {
352
+ for (var s = 0; s < SVG_SHAPE_ONLY_KEYS.length; s += 1) delete map[SVG_SHAPE_ONLY_KEYS[s]]
353
+ } else if (wasSvg && shape === 'image') {
354
+ for (var s2 = 0; s2 < SVG_SHAPE_ONLY_KEYS.length; s2 += 1) delete map[SVG_SHAPE_ONLY_KEYS[s2]]
355
+ } else if (wasImage && shape === 'svg') {
356
+ for (var s3 = 0; s3 < IMAGE_SHAPE_ONLY_KEYS.length; s3 += 1) delete map[IMAGE_SHAPE_ONLY_KEYS[s3]]
357
+ }
358
+ // 离开分区:清掉容器键,否则普通矩形还会带着 container=1,被当成分区。
359
+ if (wasSection && shape !== 'section') {
360
+ for (var c = 0; c < SECTION_SHAPE_ONLY_KEYS.length; c += 1) delete map[SECTION_SHAPE_ONLY_KEYS[c]]
361
+ }
295
362
  var preset = parseStyle(styleFromNodeShape(shape))
296
363
  var names = Object.keys(preset)
297
364
  for (var p = 0; p < names.length; p += 1) map[names[p]] = preset[names[p]]
298
365
  return formatStyle(map)
299
366
  }
300
367
 
368
+ // ── 内嵌 SVG 内容(形状枚举里的 `svg`)──────────────────────────────────────
369
+ /**
370
+ * `svg` 形状的落盘形态,逐字对齐 drawio:
371
+ *
372
+ * shape=image;imageAspect=0;image=data:image/svg+xml,<encodeURIComponent(标记)>
373
+ *
374
+ * 两条依据都来自 drawio v31.4.5 源码(不是猜的):
375
+ * · `mxImageShape.apply`:`preserveImageAspect = getNumber(style, STYLE_IMAGE_ASPECT, 1) == 1`
376
+ * (mxgraph/src/shape/mxImageShape.js)—— 键缺省是 1(等比缩放居中),
377
+ * 而 drawio 自己插入图片时写的是 `imageAspect=0`(js/grapheditor/Actions.js:1848),
378
+ * 所以"拉伸铺满"要显式写 0,这也是我们的缺省。
379
+ * · `mxGraph.postProcessCellStyle`(js/app.min.js:1315)认的形式就是
380
+ * `data:image/svg+xml,%3C…`(百分号编码)与 `data:image/…;base64,…` 两种 ——
381
+ * 也就是说这个写法在 drawio 桌面版里也能原样渲染出来,不只是本画布能看。
382
+ *
383
+ * 为什么用百分号编码而不是 base64:编码后的串里**一个 ';' 都没有**,而 drawio 与本内核的
384
+ * style 解析都是按 ';' 分段 —— base64 的 `data:image/svg+xml;base64,…` 里那个分号是解析器的雷
385
+ * (postProcessCellStyle 也正是把非 svg 的 data URI 转成 base64 形态,两套并存)。
386
+ */
387
+ var SVG_IMAGE_VALUE_PREFIX = 'data:image/svg+xml,'
388
+ var SVG_IMAGE_VALUE_PREFIX_B64 = 'data:image/svg+xml;base64,'
389
+ var SVG_XMLNS = 'xmlns="http://www.w3.org/2000/svg"'
390
+
391
+ /** 一段文本看起来像不像 SVG 标记(粗判:不做 XML 解析 —— 内核零依赖、两侧都要能跑)。 */
392
+ function isSvgMarkup(value) {
393
+ return typeof value === 'string' && /<svg[\s>]/i.test(value)
394
+ }
395
+
396
+ /**
397
+ * 归一化 SVG 标记:去首尾空白;**缺 xmlns 时补上**。
398
+ *
399
+ * 补 xmlns 不是洁癖:SVG 被当作图片加载时(<image href>、<img src>)按 XML 解析,
400
+ * 根元素没有 xmlns 的文档会被直接丢弃 —— 表现就是"节点框里什么都没有",且不报任何错。
401
+ * 模型随手写的 `<svg viewBox="0 0 24 24">…` 十有八九没有 xmlns,所以在这里补。
402
+ */
403
+ function normalizeSvgMarkup(markup) {
404
+ var text = String(markup === null || markup === undefined ? '' : markup)
405
+ text = text.replace(/^\s+/, '').replace(/\s+$/, '')
406
+ var head = text.slice(0, 1024)
407
+ if (/<svg[\s>]/i.test(head) && /xmlns\s*=/i.test(head) === false) {
408
+ text = text.replace(/<svg\b/i, '<svg ' + SVG_XMLNS)
409
+ }
410
+ return text
411
+ }
412
+
413
+ /** 是不是 SVG 的 data URI(百分号编码与 base64 两种形态都认)。 */
414
+ function isSvgDataUri(value) {
415
+ if (typeof value !== 'string') return false
416
+ return value.indexOf(SVG_IMAGE_VALUE_PREFIX) === 0 || value.indexOf(SVG_IMAGE_VALUE_PREFIX_B64) === 0
417
+ }
418
+
419
+ /**
420
+ * 是不是本画布能画的**位图** data URI(png / jpeg / gif / webp)。
421
+ *
422
+ * 外链、相对路径、以及画布解不开的格式(bmp/tiff/svg 以外的……)都不算 ——
423
+ * 那些仍按矩形占位,notes 里会说明。
424
+ */
425
+ function isRasterDataUri(value) {
426
+ if (typeof value !== 'string') return false
427
+ return /^data:image\/(png|jpe?g|gif|webp);base64,/i.test(value)
428
+ }
429
+
430
+ /**
431
+ * SVG data URI → 标记文本;**只解百分号编码那一种**,base64 返回 null。
432
+ *
433
+ * 为什么不做 base64:解 base64 要用 atob(浏览器)或 Buffer(Node),
434
+ * 而本内核是宿主与客户端共用的一份纯函数,不许碰任何环境全局。
435
+ * 宿主那边要读 base64 形态时自己用 Buffer 补(见 src/index.js)。
436
+ */
437
+ function svgMarkupFromImageValue(value) {
438
+ if (typeof value !== 'string') return null
439
+ if (value.indexOf(SVG_IMAGE_VALUE_PREFIX) !== 0) return null
440
+ try {
441
+ return decodeURIComponent(value.slice(SVG_IMAGE_VALUE_PREFIX.length))
442
+ } catch (error) {
443
+ return null
444
+ }
445
+ }
446
+
447
+ /** 标记文本 → data URI(与 drawio 自己写出来的形态逐字一致)。 */
448
+ function imageValueFromSvgMarkup(markup) {
449
+ return SVG_IMAGE_VALUE_PREFIX + encodeURIComponent(normalizeSvgMarkup(markup))
450
+ }
451
+
452
+ /** 从 style 取 `image` 键的值(没有/空串 → null)。渲染端直接拿它喂 <image href>。 */
453
+ function imageValueFromStyle(style) {
454
+ var value = styleGet(style, 'image', null)
455
+ if (typeof value !== 'string' || value.length === 0) return null
456
+ return value
457
+ }
458
+
459
+ /** 从 style 取内嵌的 SVG 标记:不是 SVG 内容节点(或只有 base64)就返回 null。 */
460
+ function svgMarkupFromStyle(style) {
461
+ var value = imageValueFromStyle(style)
462
+ if (value === null || isSvgDataUri(value) === false) return null
463
+ return svgMarkupFromImageValue(value)
464
+ }
465
+
466
+ /** 写内嵌 SVG 内容:形状键一起落成 image 形态,其它键(配色/字号/…)原样保留。 */
467
+ function styleWithSvgMarkup(style, markup, aspect) {
468
+ var fit = String(aspect) === '1'
469
+ return stylePatch(style, {
470
+ shape: 'image',
471
+ imageAspect: fit ? '1' : '0',
472
+ image: imageValueFromSvgMarkup(markup),
473
+ })
474
+ }
475
+
476
+ /**
477
+ * 写内嵌位图:`image=` 必须是 `data:image/(png|jpeg|gif|webp);base64,…`。
478
+ * 缺省 imageAspect=1(等比);显式传 0 则拉伸铺满。
479
+ */
480
+ function styleWithImageDataUri(style, dataUri, aspect) {
481
+ if (isRasterDataUri(dataUri) === false) {
482
+ throw new Error('styleWithImageDataUri: dataUri 必须是 data:image/(png|jpeg|gif|webp);base64,…')
483
+ }
484
+ var fit = aspect === undefined || aspect === null ? true : String(aspect) === '1'
485
+ return stylePatch(style, {
486
+ shape: 'image',
487
+ imageAspect: fit ? '1' : '0',
488
+ image: dataUri,
489
+ })
490
+ }
491
+
492
+ /** 清掉内嵌 SVG / 位图内容(回到矩形):image / imageAspect / shape 一起删。 */
493
+ function styleWithoutSvgMarkup(style) {
494
+ var map = parseStyle(style)
495
+ for (var i = 0; i < SVG_SHAPE_ONLY_KEYS.length; i += 1) delete map[SVG_SHAPE_ONLY_KEYS[i]]
496
+ if (map.shape === 'image') delete map.shape
497
+ return formatStyle(map)
498
+ }
499
+
500
+ /** 与 styleWithoutSvgMarkup 同义:位图与 SVG 共用同一组 image 键。 */
501
+ function styleWithoutImageDataUri(style) {
502
+ return styleWithoutSvgMarkup(style)
503
+ }
504
+
301
505
  // ── 配色 ─────────────────────────────────────────────────────────────────────
302
506
 
303
507
  /** 从 style 取填充/描边/字色,缺省即 drawio 缺省(白底黑边黑字)。 */
@@ -810,6 +1014,10 @@ export {
810
1014
  DEFAULT_FILL, DEFAULT_STROKE, DEFAULT_FONT, DEFAULT_NODE_STYLE, DEFAULT_EDGE_STYLE,
811
1015
  parseStyle, formatStyle, styleGet, styleSet, stylePatch, styleIsEmpty, styleNumber,
812
1016
  nodeShapeFromStyle, styleFromNodeShape, styleWithNodeShape,
1017
+ isSvgMarkup, normalizeSvgMarkup, isSvgDataUri, isRasterDataUri, svgMarkupFromImageValue, imageValueFromSvgMarkup,
1018
+ imageValueFromStyle, svgMarkupFromStyle, styleWithSvgMarkup, styleWithoutSvgMarkup,
1019
+ styleWithImageDataUri, styleWithoutImageDataUri,
1020
+ SVG_IMAGE_VALUE_PREFIX, SVG_IMAGE_VALUE_PREFIX_B64,
813
1021
  colorsFromStyle, colorNameFromStyle, styleWithColorName, styleWithColors,
814
1022
  textColorNameFromStyle, styleWithTextColorName,
815
1023
  dashFromStyle, styleWithDash, dashPatternFromStyle,