inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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/README.md +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
(globalThis as unknown as { figma: unknown }).figma = {
|
|
4
|
+
notify: () => undefined,
|
|
5
|
+
showUI: () => undefined,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
applyAspectRatioIfPossible,
|
|
10
|
+
applyFullWidthIfPossible,
|
|
11
|
+
hasAspectRatio,
|
|
12
|
+
markAspectRatio,
|
|
13
|
+
markFullWidthNode,
|
|
14
|
+
} from '../src/layout/deferred-layout';
|
|
15
|
+
|
|
16
|
+
// Stubs sized to expose the bits the resolvers inspect.
|
|
17
|
+
type StubFrame = {
|
|
18
|
+
type: 'FRAME';
|
|
19
|
+
name?: string;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
paddingLeft: number;
|
|
23
|
+
paddingRight: number;
|
|
24
|
+
paddingTop: number;
|
|
25
|
+
paddingBottom: number;
|
|
26
|
+
layoutMode: 'NONE' | 'VERTICAL' | 'HORIZONTAL';
|
|
27
|
+
primaryAxisSizingMode: 'AUTO' | 'FIXED';
|
|
28
|
+
counterAxisSizingMode: 'AUTO' | 'FIXED';
|
|
29
|
+
primaryAxisAlignItems?: string;
|
|
30
|
+
layoutAlign?: string;
|
|
31
|
+
layoutGrow?: number;
|
|
32
|
+
resize(w: number, h: number): void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function frame(width: number, height: number, layoutMode: 'NONE' | 'VERTICAL' | 'HORIZONTAL' = 'NONE'): StubFrame {
|
|
36
|
+
return {
|
|
37
|
+
type: 'FRAME',
|
|
38
|
+
name: '',
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
paddingLeft: 0,
|
|
42
|
+
paddingRight: 0,
|
|
43
|
+
paddingTop: 0,
|
|
44
|
+
paddingBottom: 0,
|
|
45
|
+
layoutMode,
|
|
46
|
+
primaryAxisSizingMode: 'AUTO',
|
|
47
|
+
counterAxisSizingMode: 'AUTO',
|
|
48
|
+
resize(w, h) {
|
|
49
|
+
this.width = w;
|
|
50
|
+
this.height = h;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runRegression(): void {
|
|
56
|
+
// ---- Inline SVG marked w-full + h-full inside a layoutMode=NONE parent --
|
|
57
|
+
// Mirrors the round-trip section's arrows SVG: `<svg className="absolute
|
|
58
|
+
// inset-0 h-full w-full">` rendered into a wrapper that defaults to 24×24
|
|
59
|
+
// until layout marks resize it.
|
|
60
|
+
const parent = frame(720, 432, 'NONE');
|
|
61
|
+
const svgWrap = frame(24, 24);
|
|
62
|
+
markFullWidthNode(svgWrap as unknown as SceneNode);
|
|
63
|
+
// markFullHeightNode is private to deferred-layout, but ui-builder exports
|
|
64
|
+
// it for plugin internals; here we simulate by setting the underlying mark
|
|
65
|
+
// through an alternative entry. Calling applyFullWidthIfPossible directly
|
|
66
|
+
// requires both marks already set — verify via the public path:
|
|
67
|
+
// After the SVG handler propagates marks, `applyFullWidthIfPossible` should
|
|
68
|
+
// resize the wrapped svg to (parent.width, parent.height).
|
|
69
|
+
// For test purposes we simulate this by calling applyFullWidthIfPossible
|
|
70
|
+
// with a parent that has finite dims.
|
|
71
|
+
applyFullWidthIfPossible(svgWrap as unknown as SceneNode, parent as unknown as FrameNode);
|
|
72
|
+
assert.equal(svgWrap.width, 720, 'w-full on NONE parent → resize to parent.width');
|
|
73
|
+
assert.equal(
|
|
74
|
+
svgWrap.primaryAxisSizingMode === 'FIXED' || svgWrap.counterAxisSizingMode === 'FIXED',
|
|
75
|
+
true,
|
|
76
|
+
'sizing mode locked'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// ---- aspect-ratio applied to the wrapped svg ----------------------------
|
|
80
|
+
const svgAspect = frame(0, 0);
|
|
81
|
+
markFullWidthNode(svgAspect as unknown as SceneNode);
|
|
82
|
+
markAspectRatio(svgAspect as unknown as SceneNode, 5 / 3); // aspect-5/3
|
|
83
|
+
// Resolve width first.
|
|
84
|
+
applyFullWidthIfPossible(svgAspect as unknown as SceneNode, frame(720, 999, 'NONE') as unknown as FrameNode);
|
|
85
|
+
applyAspectRatioIfPossible(svgAspect as unknown as SceneNode);
|
|
86
|
+
assert.equal(svgAspect.width, 720, 'aspect svg width = parent width');
|
|
87
|
+
assert.equal(svgAspect.height, 432, 'aspect-5/3 height = 720 * 3/5 = 432');
|
|
88
|
+
assert.equal(hasAspectRatio(svgAspect as unknown as SceneNode), false, 'aspect mark consumed');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
runRegression();
|
|
93
|
+
console.log('svg-fill-parent-regression: PASS');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('svg-fill-parent-regression: FAIL');
|
|
96
|
+
console.error(err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
(globalThis as unknown as { figma: unknown }).figma = {
|
|
4
|
+
notify: () => undefined,
|
|
5
|
+
showUI: () => undefined,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
import { flattenGroupInheritance, flattenSvgRootInheritance } from '../src/effects/icon-builder';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regression: Figma's `createNodeFromSvg` does NOT honor SVG `<g>` attribute
|
|
12
|
+
* inheritance. A `<path>` inside `<g stroke="…">` gets neither the stroke
|
|
13
|
+
* nor a fill, and the icon-builder's paint-fallback then fills it with the
|
|
14
|
+
* icon color — turning the round-trip arrow arcs into filled green wedges
|
|
15
|
+
* instead of dashed stroked arcs with arrowheads.
|
|
16
|
+
*
|
|
17
|
+
* The fix in `preprocessSvgForFigma` is to flatten `<g>` stroke / fill /
|
|
18
|
+
* stroke-* attributes onto child shape elements before sending to Figma.
|
|
19
|
+
* This fixture pins the flattening helper itself.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Case 1 — the round-trip arrows pattern: `<g stroke="X" stroke-width="2"
|
|
24
|
+
// stroke-linecap="round"><path d="…" stroke-dasharray="6 6" marker-end="…"
|
|
25
|
+
// /></g>`. Each path must inherit stroke, stroke-width, stroke-linecap.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
{
|
|
28
|
+
const input = `<svg viewBox="0 0 600 360" fill="none">
|
|
29
|
+
<g stroke="#0a0" stroke-width="2" stroke-linecap="round">
|
|
30
|
+
<path d="M 112 50 A 195 195 0 0 1 488 50" stroke-dasharray="6 6" marker-end="url(#a)" />
|
|
31
|
+
<path d="M 482 170 A 195 195 0 0 1 360 285" stroke-dasharray="6 6" />
|
|
32
|
+
</g>
|
|
33
|
+
</svg>`;
|
|
34
|
+
const output = flattenGroupInheritance(input);
|
|
35
|
+
// Each path should now carry stroke, stroke-width, stroke-linecap inline.
|
|
36
|
+
const pathMatches = output.match(/<path[^>]*>/g) || [];
|
|
37
|
+
assert.equal(pathMatches.length, 2, 'two paths in fixture');
|
|
38
|
+
for (const p of pathMatches) {
|
|
39
|
+
assert.ok(/\sstroke="#0a0"/.test(p), 'path inherits stroke from <g>: ' + p);
|
|
40
|
+
assert.ok(/\sstroke-width="2"/.test(p), 'path inherits stroke-width from <g>: ' + p);
|
|
41
|
+
assert.ok(/\sstroke-linecap="round"/.test(p), 'path inherits stroke-linecap from <g>: ' + p);
|
|
42
|
+
}
|
|
43
|
+
// Path's own stroke-dasharray and marker-end must be preserved.
|
|
44
|
+
assert.ok(/stroke-dasharray="6 6"/.test(output), 'path own stroke-dasharray preserved');
|
|
45
|
+
assert.ok(/marker-end="url\(#a\)"/.test(output), 'path own marker-end preserved');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Case 2 — child stroke wins over `<g>` stroke (CSS inheritance: own
|
|
50
|
+
// declaration beats inherited).
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
{
|
|
53
|
+
const input = `<g stroke="#aaa" stroke-width="2">
|
|
54
|
+
<path d="M 0 0 L 10 10" stroke="#000" />
|
|
55
|
+
</g>`;
|
|
56
|
+
const output = flattenGroupInheritance(input);
|
|
57
|
+
const pathMatch = output.match(/<path[^>]*>/);
|
|
58
|
+
assert.ok(pathMatch, 'path captured');
|
|
59
|
+
// Child's own stroke="#000" must NOT be replaced by parent's "#aaa".
|
|
60
|
+
assert.ok(/\sstroke="#000"/.test(pathMatch![0]), 'child own stroke wins: ' + pathMatch![0]);
|
|
61
|
+
assert.ok(!/\sstroke="#aaa"/.test(pathMatch![0]), 'parent stroke not added when child has own: ' + pathMatch![0]);
|
|
62
|
+
// But stroke-width (which child doesn't have) IS inherited.
|
|
63
|
+
assert.ok(/\sstroke-width="2"/.test(pathMatch![0]), 'sibling attr stroke-width still inherited: ' + pathMatch![0]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Case 3 — nested `<g>` propagates through both levels. The inner-most
|
|
68
|
+
// pass runs first; the outer pass then propagates to anything not covered.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
{
|
|
71
|
+
const input = `<g fill="#red">
|
|
72
|
+
<g stroke="#0a0">
|
|
73
|
+
<path d="M 0 0 L 10 10" />
|
|
74
|
+
</g>
|
|
75
|
+
</g>`;
|
|
76
|
+
const output = flattenGroupInheritance(input);
|
|
77
|
+
const pathMatch = output.match(/<path[^>]*>/);
|
|
78
|
+
assert.ok(pathMatch, 'path captured');
|
|
79
|
+
assert.ok(/\sstroke="#0a0"/.test(pathMatch![0]), 'inner <g> stroke inherited: ' + pathMatch![0]);
|
|
80
|
+
assert.ok(/\sfill="#red"/.test(pathMatch![0]), 'outer <g> fill inherited through nested level: ' + pathMatch![0]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Case 4 — `<g>` with no inheritable attrs is a no-op (no spurious changes).
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
{
|
|
87
|
+
const input = `<g transform="translate(10,10)">
|
|
88
|
+
<path d="M 0 0 L 10 10" />
|
|
89
|
+
</g>`;
|
|
90
|
+
const output = flattenGroupInheritance(input);
|
|
91
|
+
assert.equal(output, input, 'no-op when <g> has no inheritable attrs');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Case 5 — multiple shape types (path, line, circle) all inherit.
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
{
|
|
98
|
+
const input = `<g stroke="#blue" stroke-width="3">
|
|
99
|
+
<path d="M 0 0" />
|
|
100
|
+
<line x1="0" y1="0" x2="10" y2="10" />
|
|
101
|
+
<circle cx="5" cy="5" r="3" />
|
|
102
|
+
</g>`;
|
|
103
|
+
const output = flattenGroupInheritance(input);
|
|
104
|
+
for (const tag of ['path', 'line', 'circle']) {
|
|
105
|
+
const re = new RegExp('<' + tag + '[^>]*>');
|
|
106
|
+
const m = output.match(re);
|
|
107
|
+
assert.ok(m, tag + ' captured');
|
|
108
|
+
assert.ok(/\sstroke="#blue"/.test(m![0]), tag + ' inherits stroke: ' + m![0]);
|
|
109
|
+
assert.ok(/\sstroke-width="3"/.test(m![0]), tag + ' inherits stroke-width: ' + m![0]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Case 6 — opacity / fill-opacity / stroke-opacity inherit from `<g>`.
|
|
115
|
+
// Common in duotone icon libraries (Lucide duotone, Heroicons solid/outline
|
|
116
|
+
// pairs) that apply opacity to a `<g>` wrapping accent paths.
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
{
|
|
119
|
+
const input = `<g opacity="0.5" fill-opacity="0.6">
|
|
120
|
+
<path d="M 0 0 L 10 10" />
|
|
121
|
+
</g>`;
|
|
122
|
+
const output = flattenGroupInheritance(input);
|
|
123
|
+
const pathMatch = output.match(/<path[^>]*>/);
|
|
124
|
+
assert.ok(pathMatch, 'path captured');
|
|
125
|
+
assert.ok(/\sopacity="0\.5"/.test(pathMatch![0]), 'path inherits opacity: ' + pathMatch![0]);
|
|
126
|
+
assert.ok(/\sfill-opacity="0\.6"/.test(pathMatch![0]), 'path inherits fill-opacity: ' + pathMatch![0]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Case 7 — root `<svg>` inheritable attrs flatten onto child shapes. Many
|
|
131
|
+
// icon libraries (Lucide, Heroicons) set `stroke="currentColor" fill="none"
|
|
132
|
+
// stroke-width="2"` at the SVG root. Figma's createNodeFromSvg doesn't
|
|
133
|
+
// propagate these — without `flattenSvgRootInheritance`, paths render
|
|
134
|
+
// without stroke / fill / stroke-width.
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
{
|
|
137
|
+
const input = `<svg stroke="#0a0" fill="none" stroke-width="2" viewBox="0 0 24 24">
|
|
138
|
+
<path d="M 4 4 L 20 20" />
|
|
139
|
+
<circle cx="12" cy="12" r="5" />
|
|
140
|
+
</svg>`;
|
|
141
|
+
const output = flattenSvgRootInheritance(input);
|
|
142
|
+
const pathMatch = output.match(/<path[^>]*>/);
|
|
143
|
+
const circleMatch = output.match(/<circle[^>]*>/);
|
|
144
|
+
assert.ok(pathMatch && circleMatch, 'shapes captured');
|
|
145
|
+
for (const m of [pathMatch![0], circleMatch![0]]) {
|
|
146
|
+
assert.ok(/\sstroke="#0a0"/.test(m), 'shape inherits stroke from <svg>: ' + m);
|
|
147
|
+
assert.ok(/\sfill="none"/.test(m), 'shape inherits fill from <svg>: ' + m);
|
|
148
|
+
assert.ok(/\sstroke-width="2"/.test(m), 'shape inherits stroke-width from <svg>: ' + m);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Case 8 — root SVG attrs don't override shape's own (CSS inheritance).
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
{
|
|
156
|
+
const input = `<svg stroke="#aaa" fill="none">
|
|
157
|
+
<path d="M 0 0 L 10 10" stroke="#000" />
|
|
158
|
+
</svg>`;
|
|
159
|
+
const output = flattenSvgRootInheritance(input);
|
|
160
|
+
const pathMatch = output.match(/<path[^>]*>/);
|
|
161
|
+
assert.ok(/\sstroke="#000"/.test(pathMatch![0]), 'shape own stroke wins over root: ' + pathMatch![0]);
|
|
162
|
+
assert.ok(!/\sstroke="#aaa"/.test(pathMatch![0]), 'root stroke not added when shape has own: ' + pathMatch![0]);
|
|
163
|
+
assert.ok(/\sfill="none"/.test(pathMatch![0]), 'fill from root inherited (shape has no own fill): ' + pathMatch![0]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('svg-group-inheritance-regression: PASS');
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
(globalThis as unknown as { figma: unknown }).figma = {
|
|
4
|
+
notify: () => undefined,
|
|
5
|
+
showUI: () => undefined,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
import { inlineSvgMarkers } from '../src/effects/icon-builder';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regression: Figma's `createNodeFromSvg` does NOT render `<marker>`
|
|
12
|
+
* references — `marker-end` arrowheads disappear silently. The fix in
|
|
13
|
+
* `preprocessSvgForFigma` inlines the marker geometry as a transformed
|
|
14
|
+
* `<g transform="translate(x y) rotate(θ) scale(sx sy) translate(-refX -refY)">`
|
|
15
|
+
* at the path endpoint, oriented along the tangent.
|
|
16
|
+
*
|
|
17
|
+
* This fixture pins the inlining helper for the round-trip arc pattern:
|
|
18
|
+
* <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
|
|
19
|
+
* markerWidth="4" markerHeight="4">
|
|
20
|
+
* <path d="M0,0 L10,5 L0,10 Z" fill="..." />
|
|
21
|
+
* </marker>
|
|
22
|
+
* <path d="M 112 50 A 195 195 0 0 1 488 50" marker-end="url(#arrow)" />
|
|
23
|
+
*
|
|
24
|
+
* Expected: an extra `<g transform="…">` block appears before `</svg>`,
|
|
25
|
+
* positioning the triangle at the arc endpoint with the correct tangent
|
|
26
|
+
* rotation.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Case 1: arc path with marker-end — endpoint + tangent must inject geometry.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
{
|
|
33
|
+
const svg = `<svg viewBox="0 0 600 360">
|
|
34
|
+
<defs>
|
|
35
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="4" markerHeight="4">
|
|
36
|
+
<path d="M0,0 L10,5 L0,10 Z" fill="#00aa00" />
|
|
37
|
+
</marker>
|
|
38
|
+
</defs>
|
|
39
|
+
<path d="M 112 50 A 195 195 0 0 1 488 50" marker-end="url(#arrow)" />
|
|
40
|
+
</svg>`;
|
|
41
|
+
|
|
42
|
+
const out = inlineSvgMarkers(svg);
|
|
43
|
+
// Should contain a new <g transform=...> with the marker triangle inside.
|
|
44
|
+
assert.match(
|
|
45
|
+
out,
|
|
46
|
+
/<g\s+transform="translate\(488 50\)\s+rotate\([^"]+\)\s+scale\(0\.4 0\.4\)\s+translate\(-9 -5\)">/,
|
|
47
|
+
'inlined marker <g> with translate-to-endpoint, rotate, scale, refX/refY-align'
|
|
48
|
+
);
|
|
49
|
+
assert.ok(out.indexOf('M0,0 L10,5 L0,10 Z') !== -1, 'marker triangle path preserved');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Case 2: tangent angle for a CW horizontal-chord arc points DOWN at right
|
|
54
|
+
// endpoint. For arc `M 112 50 A 195 195 0 0 1 488 50` (sweep CW, lar=small),
|
|
55
|
+
// chord is horizontal; center lies BELOW the chord (sign of perp); tangent
|
|
56
|
+
// at endpoint should point downward (positive y). Roughly 90° in SVG
|
|
57
|
+
// y-down coords.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
{
|
|
60
|
+
const svg = `<svg viewBox="0 0 600 360">
|
|
61
|
+
<defs>
|
|
62
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="4" markerHeight="4">
|
|
63
|
+
<path d="M0,0 L10,5 L0,10 Z" />
|
|
64
|
+
</marker>
|
|
65
|
+
</defs>
|
|
66
|
+
<path d="M 112 50 A 195 195 0 0 1 488 50" marker-end="url(#arrow)" />
|
|
67
|
+
</svg>`;
|
|
68
|
+
const out = inlineSvgMarkers(svg);
|
|
69
|
+
const rotMatch = out.match(/rotate\(([-\d.]+)\)/);
|
|
70
|
+
assert.ok(rotMatch, 'rotation present');
|
|
71
|
+
const angle = parseFloat(rotMatch![1]);
|
|
72
|
+
// Arc `M 112 50 A 195 195 0 0 1 488 50`: small arc + sweep=1.
|
|
73
|
+
// Center at (300, 101.78) — below chord in SVG y-down (visually above the
|
|
74
|
+
// arc since y-down inverts). Arc passes through (300, -93) — bulges UP
|
|
75
|
+
// visually. Velocity at endpoint (488, 50) is (-r sin θ_end, r cos θ_end)
|
|
76
|
+
// where θ_end ≈ -15.4°: velocity ≈ (52, 188). Angle ≈ atan2(188, 52) ≈
|
|
77
|
+
// 74.5° — pointing RIGHT and DOWN, which is the direction of travel as
|
|
78
|
+
// the arc descends from its apex into the endpoint.
|
|
79
|
+
assert.ok(angle > 60 && angle < 90, `rotation angle ${angle} should be ≈74.5° (right-down, direction of travel at arc endpoint)`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Case 3: path WITHOUT marker-end — no inlining.
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
{
|
|
86
|
+
const svg = `<svg viewBox="0 0 100 100">
|
|
87
|
+
<defs><marker id="arrow"><path d="M0 0 L10 5 L0 10 Z" /></marker></defs>
|
|
88
|
+
<path d="M 10 10 L 90 90" />
|
|
89
|
+
</svg>`;
|
|
90
|
+
const out = inlineSvgMarkers(svg);
|
|
91
|
+
assert.equal(out, svg, 'no marker-end → no inlining (output unchanged)');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Case 4: line path (M + L) — tangent is the direction from start to end.
|
|
96
|
+
// For `M 0 0 L 100 0` (horizontal right), tangent angle = 0°.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
{
|
|
99
|
+
const svg = `<svg viewBox="0 0 200 100">
|
|
100
|
+
<defs>
|
|
101
|
+
<marker id="m" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6">
|
|
102
|
+
<path d="M0,0 L10,5 L0,10 Z" />
|
|
103
|
+
</marker>
|
|
104
|
+
</defs>
|
|
105
|
+
<path d="M 0 0 L 100 0" marker-end="url(#m)" />
|
|
106
|
+
</svg>`;
|
|
107
|
+
const out = inlineSvgMarkers(svg);
|
|
108
|
+
assert.match(
|
|
109
|
+
out,
|
|
110
|
+
/translate\(100 0\)\s+rotate\(0\)/,
|
|
111
|
+
'horizontal line → endpoint (100, 0) and rotation 0°'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Case 5: line going DOWN (positive y in SVG) — tangent 90°.
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
{
|
|
119
|
+
const svg = `<svg viewBox="0 0 100 200">
|
|
120
|
+
<defs>
|
|
121
|
+
<marker id="m" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="3" markerHeight="3">
|
|
122
|
+
<path d="M0,0 L10,5 L0,10 Z" />
|
|
123
|
+
</marker>
|
|
124
|
+
</defs>
|
|
125
|
+
<path d="M 50 0 L 50 100" marker-end="url(#m)" />
|
|
126
|
+
</svg>`;
|
|
127
|
+
const out = inlineSvgMarkers(svg);
|
|
128
|
+
assert.match(
|
|
129
|
+
out,
|
|
130
|
+
/translate\(50 100\)\s+rotate\(90\)/,
|
|
131
|
+
'vertical-down line → endpoint (50, 100) and rotation 90°'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Case 6: missing marker definition — silently skipped (no crash, no inline).
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
{
|
|
139
|
+
const svg = `<svg viewBox="0 0 100 100">
|
|
140
|
+
<path d="M 0 0 L 100 100" marker-end="url(#nonexistent)" />
|
|
141
|
+
</svg>`;
|
|
142
|
+
const out = inlineSvgMarkers(svg);
|
|
143
|
+
assert.equal(out, svg, 'unknown marker id → no-op');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Case 7: cubic bezier (C command). Tangent at end = direction from second
|
|
148
|
+
// control to endpoint. For `M 0 0 C 50 0 100 50 100 100`: last control
|
|
149
|
+
// (100, 50), end (100, 100). Tangent = (0, 50), angle = atan2(50, 0) = 90°.
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
{
|
|
152
|
+
const svg = `<svg viewBox="0 0 200 200">
|
|
153
|
+
<defs>
|
|
154
|
+
<marker id="m" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="4" markerHeight="4">
|
|
155
|
+
<path d="M0,0 L10,5 L0,10 Z" />
|
|
156
|
+
</marker>
|
|
157
|
+
</defs>
|
|
158
|
+
<path d="M 0 0 C 50 0 100 50 100 100" marker-end="url(#m)" />
|
|
159
|
+
</svg>`;
|
|
160
|
+
const out = inlineSvgMarkers(svg);
|
|
161
|
+
assert.match(
|
|
162
|
+
out,
|
|
163
|
+
/translate\(100 100\)\s+rotate\(90\)/,
|
|
164
|
+
'cubic bezier endpoint at (100, 100), tangent direction angle 90°'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Case 8: quadratic bezier (Q command). Tangent at end = direction from
|
|
170
|
+
// control to endpoint. For `M 0 0 Q 50 0 100 50`: control (50, 0), end
|
|
171
|
+
// (100, 50). Tangent = (50, 50), angle = 45°.
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
{
|
|
174
|
+
const svg = `<svg viewBox="0 0 200 200">
|
|
175
|
+
<defs>
|
|
176
|
+
<marker id="m" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="4" markerHeight="4">
|
|
177
|
+
<path d="M0,0 L10,5 L0,10 Z" />
|
|
178
|
+
</marker>
|
|
179
|
+
</defs>
|
|
180
|
+
<path d="M 0 0 Q 50 0 100 50" marker-end="url(#m)" />
|
|
181
|
+
</svg>`;
|
|
182
|
+
const out = inlineSvgMarkers(svg);
|
|
183
|
+
assert.match(
|
|
184
|
+
out,
|
|
185
|
+
/translate\(100 50\)\s+rotate\(45\)/,
|
|
186
|
+
'quadratic bezier endpoint at (100, 50), tangent direction angle 45°'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Case 9: horizontal lineto (H) — endpoint inherits Y from previous point,
|
|
192
|
+
// tangent points along the H direction.
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
{
|
|
195
|
+
const svg = `<svg viewBox="0 0 200 200">
|
|
196
|
+
<defs>
|
|
197
|
+
<marker id="m" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="4" markerHeight="4">
|
|
198
|
+
<path d="M0,0 L10,5 L0,10 Z" />
|
|
199
|
+
</marker>
|
|
200
|
+
</defs>
|
|
201
|
+
<path d="M 10 20 H 100" marker-end="url(#m)" />
|
|
202
|
+
</svg>`;
|
|
203
|
+
const out = inlineSvgMarkers(svg);
|
|
204
|
+
assert.match(
|
|
205
|
+
out,
|
|
206
|
+
/translate\(100 20\)\s+rotate\(0\)/,
|
|
207
|
+
'H 100 from (10, 20) → endpoint (100, 20), horizontal tangent 0°'
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log('svg-marker-inline-regression: PASS');
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { nodeIrToSvg } from '../src/effects/icon-builder';
|
|
4
|
+
import type { NodeIR } from '../src/tailwind/node-ir';
|
|
5
|
+
|
|
6
|
+
// Fixture: serialise a JSX-style svg tree (NodeIR) back to an SVG string and
|
|
7
|
+
// verify two things this regression locks in:
|
|
8
|
+
// 1. <marker> children inside <defs> survive serialisation (they used to be
|
|
9
|
+
// filtered out because `marker` wasn't in SVG_CHILD_TAGS, so any
|
|
10
|
+
// arrowhead defined this way would silently disappear in Figma).
|
|
11
|
+
// 2. SVG attributes that are camelCase in the spec — `viewBox`,
|
|
12
|
+
// `preserveAspectRatio`, `refX`, `refY`, `markerWidth`, `markerHeight` —
|
|
13
|
+
// are emitted as-is, NOT converted to kebab-case (e.g. `marker-width`),
|
|
14
|
+
// which would produce invalid SVG that `figma.createNodeFromSvg` won't
|
|
15
|
+
// render.
|
|
16
|
+
//
|
|
17
|
+
// Originating bug: the round-trip section's bidirectional arrows used
|
|
18
|
+
// <marker> + markerEnd to draw arrowheads. Without this fix, the arrowheads
|
|
19
|
+
// disappeared in the Figma render and the bridge silhouette lost its
|
|
20
|
+
// `xMidYMin slice` framing.
|
|
21
|
+
|
|
22
|
+
function elem(tagLower: string, props: Record<string, unknown>, children: NodeIR[] = []): NodeIR {
|
|
23
|
+
return {
|
|
24
|
+
kind: 'element',
|
|
25
|
+
tagName: tagLower,
|
|
26
|
+
tagLower,
|
|
27
|
+
props,
|
|
28
|
+
children,
|
|
29
|
+
classes: [],
|
|
30
|
+
} as unknown as NodeIR;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runRegression(): void {
|
|
34
|
+
const tree = elem(
|
|
35
|
+
'svg',
|
|
36
|
+
{
|
|
37
|
+
viewBox: '0 0 600 360',
|
|
38
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
39
|
+
width: '100%',
|
|
40
|
+
height: '100%',
|
|
41
|
+
fill: 'none',
|
|
42
|
+
},
|
|
43
|
+
[
|
|
44
|
+
elem('defs', {}, [
|
|
45
|
+
elem(
|
|
46
|
+
'marker',
|
|
47
|
+
{
|
|
48
|
+
id: 'rt-arrow',
|
|
49
|
+
viewBox: '0 0 10 10',
|
|
50
|
+
refX: 9,
|
|
51
|
+
refY: 5,
|
|
52
|
+
markerWidth: 4,
|
|
53
|
+
markerHeight: 4,
|
|
54
|
+
orient: 'auto-start-reverse',
|
|
55
|
+
},
|
|
56
|
+
[
|
|
57
|
+
elem('path', {
|
|
58
|
+
d: 'M0,0 L10,5 L0,10 Z',
|
|
59
|
+
fill: 'currentColor',
|
|
60
|
+
}),
|
|
61
|
+
]
|
|
62
|
+
),
|
|
63
|
+
]),
|
|
64
|
+
elem('path', {
|
|
65
|
+
d: 'M 100 100 A 50 50 0 0 1 200 100',
|
|
66
|
+
strokeWidth: 2,
|
|
67
|
+
strokeDasharray: '6 6',
|
|
68
|
+
markerEnd: 'url(#rt-arrow)',
|
|
69
|
+
}),
|
|
70
|
+
]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const svg = nodeIrToSvg(tree);
|
|
74
|
+
assert.ok(svg, 'expected nodeIrToSvg to return a non-empty string');
|
|
75
|
+
|
|
76
|
+
// Wrapper attrs: viewBox + preserveAspectRatio preserved with original
|
|
77
|
+
// (camelCase) spelling.
|
|
78
|
+
assert.match(svg!, /viewBox="0 0 600 360"/, 'wrapper viewBox must be preserved');
|
|
79
|
+
assert.match(
|
|
80
|
+
svg!,
|
|
81
|
+
/preserveAspectRatio="xMidYMid meet"/,
|
|
82
|
+
'wrapper preserveAspectRatio must be preserved'
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Marker tag survives.
|
|
86
|
+
assert.match(svg!, /<marker /, '<marker> tag must be present');
|
|
87
|
+
assert.match(svg!, /id="rt-arrow"/, 'marker id must be preserved');
|
|
88
|
+
|
|
89
|
+
// Marker camelCase attributes preserved (NOT marker-width / ref-x).
|
|
90
|
+
assert.match(svg!, /markerWidth="4"/, 'markerWidth must keep camelCase');
|
|
91
|
+
assert.match(svg!, /markerHeight="4"/, 'markerHeight must keep camelCase');
|
|
92
|
+
assert.match(svg!, /refX="9"/, 'refX must keep camelCase');
|
|
93
|
+
assert.match(svg!, /refY="5"/, 'refY must keep camelCase');
|
|
94
|
+
assert.doesNotMatch(svg!, /marker-width=/, 'must not emit kebab marker-width');
|
|
95
|
+
assert.doesNotMatch(svg!, /ref-x=/, 'must not emit kebab ref-x');
|
|
96
|
+
|
|
97
|
+
// marker child <path> survives.
|
|
98
|
+
assert.match(svg!, /<path d="M0,0 L10,5 L0,10 Z"/, 'marker child path must be present');
|
|
99
|
+
|
|
100
|
+
// Path-level presentation attributes ARE kebab-case in SVG.
|
|
101
|
+
assert.match(svg!, /stroke-dasharray="6 6"/, 'strokeDasharray must convert to kebab-case');
|
|
102
|
+
assert.match(
|
|
103
|
+
svg!,
|
|
104
|
+
/marker-end="url\(#rt-arrow\)"/,
|
|
105
|
+
'markerEnd reference must convert to kebab-case (the SVG attribute is marker-end)'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
runRegression();
|
|
111
|
+
console.log('svg-marker-regression: PASS');
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('svg-marker-regression: FAIL');
|
|
114
|
+
console.error(err);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|