@deneb-ui/cli 2.0.84 → 2.0.86
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/bin/index.js +82 -12
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +1251 -0
- package/src/arc/adapters/accordion.cjs +32 -0
- package/src/arc/adapters/dialog.cjs +26 -0
- package/src/arc/adapters/embla.cjs +29 -0
- package/src/arc/adapters/gallery.cjs +23 -0
- package/src/arc/adapters/picture-source.cjs +71 -0
- package/src/arc/adapters/registry.cjs +156 -0
- package/src/arc/adapters/slick.cjs +26 -0
- package/src/arc/adapters/swiper.cjs +29 -0
- package/src/arc/adapters/tabs.cjs +29 -0
- package/src/arc/adapters/ui-frameworks.cjs +140 -0
- package/src/arc/adapters.cjs +20 -151
- package/src/arc/canonical-paths.cjs +95 -0
- package/src/arc/data-flow.cjs +158 -0
- package/src/arc/diff.cjs +136 -0
- package/src/arc/explain.cjs +113 -0
- package/src/arc/field-paths.cjs +25 -13
- package/src/arc/fs-utils.cjs +3 -0
- package/src/arc/index.cjs +126 -7
- package/src/arc/ir-builder.cjs +581 -0
- package/src/arc/manifest.cjs +18 -0
- package/src/arc/planner.cjs +106 -2
- package/src/arc/printer.cjs +18 -5
- package/src/arc/residual.cjs +9 -3
- package/src/arc/rsc-boundary.cjs +154 -0
- package/src/arc/runtime-validator.cjs +286 -0
- package/src/arc/semantic.cjs +217 -16
- package/src/arc/text-fragment-analyzer.cjs +173 -0
- package/src/arc/transformer.cjs +819 -20
- package/src/arc/version.cjs +1 -1
- package/src/tools/deneb-doctor.cjs +53 -9
package/src/arc/semantic.cjs
CHANGED
|
@@ -30,6 +30,8 @@ const {
|
|
|
30
30
|
} = require('./adapters.cjs');
|
|
31
31
|
const { shortHash } = require('./fs-utils.cjs');
|
|
32
32
|
const { resolveImportSpecifier } = require('./scanner.cjs');
|
|
33
|
+
const { resolvePublicAssetUrl } = require('./ir-builder.cjs');
|
|
34
|
+
const { analyzeElementFragments } = require('./text-fragment-analyzer.cjs');
|
|
33
35
|
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
34
36
|
|
|
35
37
|
const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
|
|
@@ -239,7 +241,34 @@ function isDynamicExpression(expr, bindings) {
|
|
|
239
241
|
return true;
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
function getBindingVal(bindings, name) {
|
|
245
|
+
if (!bindings) return undefined;
|
|
246
|
+
if (typeof bindings.get === 'function') return bindings.get(name);
|
|
247
|
+
return bindings[name];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hasDynamicExpressionChild(node, bindings) {
|
|
251
|
+
const children = node.children || [];
|
|
252
|
+
for (const child of children) {
|
|
253
|
+
if (!child) continue;
|
|
254
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
255
|
+
const expr = child.expression;
|
|
256
|
+
if (!expr) continue;
|
|
257
|
+
if (expr.type === 'StringLiteral' || (expr.type === 'Literal' && typeof expr.value === 'string')) continue;
|
|
258
|
+
if (expr.type === 'NumericLiteral' || (expr.type === 'Literal' && typeof expr.value === 'number')) continue;
|
|
259
|
+
if (expr.type === 'TemplateLiteral' && (!expr.expressions || expr.expressions.length === 0)) continue;
|
|
260
|
+
if (expr.type === 'Identifier' && typeof getBindingVal(bindings, expr.name) === 'string') continue;
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
242
267
|
function resolveChildText(node, bindings) {
|
|
268
|
+
if (hasDynamicExpressionChild(node, bindings)) {
|
|
269
|
+
return { text: '', dynamic: true, fromBinding: false };
|
|
270
|
+
}
|
|
271
|
+
|
|
243
272
|
const direct = collectJsxText(node);
|
|
244
273
|
if (direct) return { text: direct, dynamic: false, fromBinding: false };
|
|
245
274
|
|
|
@@ -256,9 +285,14 @@ function resolveChildText(node, bindings) {
|
|
|
256
285
|
if (!expr) continue;
|
|
257
286
|
if (expr.type === 'StringLiteral' || (expr.type === 'Literal' && typeof expr.value === 'string')) {
|
|
258
287
|
texts.push(String(expr.value));
|
|
259
|
-
} else if (expr.type === 'Identifier'
|
|
260
|
-
|
|
261
|
-
|
|
288
|
+
} else if (expr.type === 'Identifier') {
|
|
289
|
+
const val = getBindingVal(bindings, expr.name);
|
|
290
|
+
if (typeof val === 'string') {
|
|
291
|
+
texts.push(val);
|
|
292
|
+
fromBinding = true;
|
|
293
|
+
} else {
|
|
294
|
+
return { text: '', dynamic: true, fromBinding: false };
|
|
295
|
+
}
|
|
262
296
|
} else if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
|
|
263
297
|
texts.push(expr.quasis.map((q) => q.value.cooked || '').join(''));
|
|
264
298
|
} else {
|
|
@@ -496,7 +530,7 @@ function collectLooseText(code) {
|
|
|
496
530
|
return /<(h[1-6]|p|Button|span)[^>]*>\s*[A-Za-z]/.test(code);
|
|
497
531
|
}
|
|
498
532
|
|
|
499
|
-
function analyzeFile({ code, relativeFile, profile, graph, ownerScope, componentMeta }) {
|
|
533
|
+
function analyzeFile({ code, relativeFile, profile, graph, ownerScope, componentMeta, ir = profile?.ir || null }) {
|
|
500
534
|
const adapters = activeAdapters(profile);
|
|
501
535
|
const fileSkip = skipReasonForFile(relativeFile, code);
|
|
502
536
|
if (fileSkip) {
|
|
@@ -598,13 +632,73 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
598
632
|
return;
|
|
599
633
|
}
|
|
600
634
|
|
|
635
|
+
const parents = parentNames(pathNode);
|
|
636
|
+
|
|
637
|
+
if (name === 'source' && (parents[0] === 'picture' || recognition?.kind === 'responsive-image-source')) {
|
|
638
|
+
candidates.push({
|
|
639
|
+
loc,
|
|
640
|
+
tag: name,
|
|
641
|
+
kind: 'decoration',
|
|
642
|
+
confidence: 0.95,
|
|
643
|
+
skip: true,
|
|
644
|
+
reason: 'picture-source-child',
|
|
645
|
+
file: relativeFile,
|
|
646
|
+
ownerScope,
|
|
647
|
+
});
|
|
648
|
+
this.traverse(pathNode);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
601
652
|
const href = getJsxAttributeLiteral(node, 'href');
|
|
602
|
-
|
|
653
|
+
let src = getJsxAttributeLiteral(node, 'src');
|
|
654
|
+
let importedAssetIdentifier = null;
|
|
655
|
+
|
|
656
|
+
if (name === 'picture') {
|
|
657
|
+
const children = node.children || [];
|
|
658
|
+
const imgChild = children.find((c) => c && c.type === 'JSXElement' && (getJsxName(c) === 'img' || getJsxName(c) === 'Image'));
|
|
659
|
+
if (imgChild) {
|
|
660
|
+
src = getJsxAttributeLiteral(imgChild, 'src');
|
|
661
|
+
if (!src && imgChild.openingElement) {
|
|
662
|
+
const srcAttr = findJsxAttribute(imgChild, 'src');
|
|
663
|
+
if (srcAttr && srcAttr.value?.type === 'JSXExpressionContainer') {
|
|
664
|
+
const expr = srcAttr.value.expression;
|
|
665
|
+
if (expr?.type === 'Identifier') {
|
|
666
|
+
const spec = imports[expr.name];
|
|
667
|
+
if (spec) {
|
|
668
|
+
const currentFileAbs = profile?.root ? path.join(profile.root, relativeFile) : null;
|
|
669
|
+
const publicUrl = resolvePublicAssetUrl(profile, currentFileAbs, spec);
|
|
670
|
+
if (publicUrl) {
|
|
671
|
+
src = publicUrl;
|
|
672
|
+
importedAssetIdentifier = expr.name;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!src && node.openingElement) {
|
|
682
|
+
const srcAttr = findJsxAttribute(node, 'src');
|
|
683
|
+
if (srcAttr && srcAttr.value?.type === 'JSXExpressionContainer') {
|
|
684
|
+
const expr = srcAttr.value.expression;
|
|
685
|
+
if (expr?.type === 'Identifier') {
|
|
686
|
+
const spec = imports[expr.name];
|
|
687
|
+
if (spec) {
|
|
688
|
+
const currentFileAbs = profile?.root ? path.join(profile.root, relativeFile) : null;
|
|
689
|
+
const publicUrl = resolvePublicAssetUrl(profile, currentFileAbs, spec);
|
|
690
|
+
if (publicUrl) {
|
|
691
|
+
src = publicUrl;
|
|
692
|
+
importedAssetIdentifier = expr.name;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
603
698
|
const alt = getJsxAttributeLiteral(node, 'alt');
|
|
604
699
|
const placeholder = getJsxAttributeLiteral(node, 'placeholder');
|
|
605
700
|
const className = classNameOf(node);
|
|
606
701
|
const textInfo = resolveChildText(node, bindings);
|
|
607
|
-
const parents = parentNames(pathNode);
|
|
608
702
|
const actionNode = resolveActionWithAdapters(node, adapters) || (ACTION_TAGS.has(name) ? node : null);
|
|
609
703
|
|
|
610
704
|
const isHeaderNav =
|
|
@@ -679,10 +773,10 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
679
773
|
kind: 'image',
|
|
680
774
|
operation: 'extract-image',
|
|
681
775
|
value: src,
|
|
682
|
-
extra: { alt },
|
|
776
|
+
extra: { alt, importedIdentifier: importedAssetIdentifier },
|
|
683
777
|
confidence: confidenceFor('image', { dynamic: false }),
|
|
684
|
-
reason: 'literal-image-source',
|
|
685
|
-
fingerprint: fingerprintCandidate({ tag: name, kind: 'image', hasAlt: Boolean(alt) }),
|
|
778
|
+
reason: importedAssetIdentifier ? 'imported-static-asset-image' : 'literal-image-source',
|
|
779
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'image', hasAlt: Boolean(alt), imported: Boolean(importedAssetIdentifier) }),
|
|
686
780
|
});
|
|
687
781
|
if (alt && !isStaticSkipText(alt)) {
|
|
688
782
|
candidates.push({
|
|
@@ -712,8 +806,24 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
712
806
|
});
|
|
713
807
|
}
|
|
714
808
|
|
|
809
|
+
const bgMatch = className && className.match(/\bbg-\[url\(['"]?([^'"\)]+)['"]?\)\]/);
|
|
810
|
+
if (bgMatch && bgMatch[1] && !usedLocs.has(loc + ':bg')) {
|
|
811
|
+
usedLocs.add(loc + ':bg');
|
|
812
|
+
candidates.push({
|
|
813
|
+
...baseMeta,
|
|
814
|
+
kind: 'image',
|
|
815
|
+
operation: 'extract-tailwind-bg',
|
|
816
|
+
value: bgMatch[1],
|
|
817
|
+
extra: { rawClass: className, bgUrl: bgMatch[1] },
|
|
818
|
+
confidence: 0.92,
|
|
819
|
+
reason: 'tailwind-arbitrary-background-image',
|
|
820
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'bg-image', url: bgMatch[1] }),
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
715
824
|
const isCustomComponent = Boolean(name && ((name[0] >= 'A' && name[0] <= 'Z') || name.includes('.')));
|
|
716
825
|
if (isCustomComponent && !apiOwned && node.openingElement && Array.isArray(node.openingElement.attributes)) {
|
|
826
|
+
const passedLiteralProps = {};
|
|
717
827
|
for (const attr of node.openingElement.attributes) {
|
|
718
828
|
if (attr.type === 'JSXAttribute' && attr.name && USER_FACING_PROP_NAMES.has(attr.name.name)) {
|
|
719
829
|
const propName = attr.name.name;
|
|
@@ -725,10 +835,13 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
725
835
|
const expr = attr.value.expression;
|
|
726
836
|
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
727
837
|
propValue = String(expr.value || '');
|
|
838
|
+
} else if (expr && (expr.type === 'NumericLiteral' || (expr.type === 'Literal' && typeof expr.value === 'number'))) {
|
|
839
|
+
propValue = expr.value;
|
|
728
840
|
}
|
|
729
841
|
}
|
|
730
842
|
}
|
|
731
|
-
if (propValue && !isStaticSkipText(propValue)) {
|
|
843
|
+
if (propValue && !isStaticSkipText(String(propValue))) {
|
|
844
|
+
passedLiteralProps[propName] = propValue;
|
|
732
845
|
const propLoc = `${loc}:${propName}`;
|
|
733
846
|
if (!usedLocs.has(propLoc)) {
|
|
734
847
|
usedLocs.add(propLoc);
|
|
@@ -747,6 +860,27 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
747
860
|
}
|
|
748
861
|
}
|
|
749
862
|
}
|
|
863
|
+
if (Object.keys(passedLiteralProps).length > 0 && !usedLocs.has(loc + ':prop-flow')) {
|
|
864
|
+
usedLocs.add(loc + ':prop-flow');
|
|
865
|
+
const compDef = (profile?.components || []).find((c) => c.name === name) ||
|
|
866
|
+
(ir && typeof ir.getComponent === 'function' && ir.getComponent(name));
|
|
867
|
+
if (compDef) {
|
|
868
|
+
candidates.push({
|
|
869
|
+
...baseMeta,
|
|
870
|
+
kind: 'prop-flow',
|
|
871
|
+
operation: 'prop-flow-callsite',
|
|
872
|
+
value: passedLiteralProps,
|
|
873
|
+
extra: {
|
|
874
|
+
componentName: name,
|
|
875
|
+
componentFile: compDef.file,
|
|
876
|
+
literalProps: passedLiteralProps,
|
|
877
|
+
},
|
|
878
|
+
confidence: 0.95,
|
|
879
|
+
reason: `reusable-component-prop-flow-${name}`,
|
|
880
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'prop-flow', props: Object.keys(passedLiteralProps) }),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
750
884
|
}
|
|
751
885
|
|
|
752
886
|
const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
|
|
@@ -850,6 +984,47 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
850
984
|
}
|
|
851
985
|
}
|
|
852
986
|
|
|
987
|
+
const fragAnalysis = analyzeElementFragments(node);
|
|
988
|
+
if (fragAnalysis.isButtonWithIcon && !apiOwned && !textInfo.dynamic && !usedLocs.has(loc)) {
|
|
989
|
+
usedLocs.add(loc);
|
|
990
|
+
candidates.push({
|
|
991
|
+
...baseMeta,
|
|
992
|
+
kind: 'text',
|
|
993
|
+
operation: 'bind-button-with-icon',
|
|
994
|
+
value: fragAnalysis.combinedText,
|
|
995
|
+
extra: { tag: name, cta: true, hasIcons: true },
|
|
996
|
+
confidence: 0.94,
|
|
997
|
+
reason: 'action-button-with-icon-preserved',
|
|
998
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'btn-icon', text: fragAnalysis.combinedText }),
|
|
999
|
+
});
|
|
1000
|
+
this.traverse(pathNode);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (fragAnalysis.isHighlightedHeading && !apiOwned && !textInfo.dynamic && !usedLocs.has(loc)) {
|
|
1005
|
+
usedLocs.add(loc);
|
|
1006
|
+
candidates.push({
|
|
1007
|
+
...baseMeta,
|
|
1008
|
+
kind: 'text',
|
|
1009
|
+
operation: 'bind-highlighted-heading',
|
|
1010
|
+
value: fragAnalysis.combinedText,
|
|
1011
|
+
extra: {
|
|
1012
|
+
tag: name,
|
|
1013
|
+
fragments: fragAnalysis.pureTextFragments.map((f) => ({
|
|
1014
|
+
type: f.type,
|
|
1015
|
+
text: f.text,
|
|
1016
|
+
className: f.className || '',
|
|
1017
|
+
tag: f.tag || 'span',
|
|
1018
|
+
})),
|
|
1019
|
+
},
|
|
1020
|
+
confidence: 0.96,
|
|
1021
|
+
reason: 'semantic-heading-with-highlight-span',
|
|
1022
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'heading-highlight', text: fragAnalysis.combinedText }),
|
|
1023
|
+
});
|
|
1024
|
+
this.traverse(pathNode);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
853
1028
|
const headingLike = HEADING_TAGS.has(name) || (recognition && recognition.kind === 'text' && HEADING_TAGS.has(recognition.tag || name));
|
|
854
1029
|
const textLike = TEXT_TAGS.has(name) || headingLike || name === 'Button' || name === 'button' || (recognition && recognition.kind === 'text');
|
|
855
1030
|
if (textLike && !isAction) {
|
|
@@ -919,12 +1094,33 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
919
1094
|
const { usage: itemUsage, usesItemAsComponent, usesDirectItem } = collectItemFieldUsage(mapInfo.callback, mapInfo.itemParam);
|
|
920
1095
|
const objectItems = arr.items.every((item) => item.type === 'object');
|
|
921
1096
|
const stringItems = arr.items.every((item) => item.type === 'string');
|
|
1097
|
+
const rootTagName = getJsxName(mapInfo.rootElement);
|
|
1098
|
+
const isChildCustomComponent = /^[A-Z]/.test(rootTagName);
|
|
1099
|
+
|
|
1100
|
+
// If the map returns a custom child component (e.g. <ProductCard product={item} />),
|
|
1101
|
+
// derive item fields from the sample object items if direct JSX member usage was empty
|
|
1102
|
+
if (isChildCustomComponent && objectItems && itemUsage.size === 0 && arr.items[0]?.value) {
|
|
1103
|
+
const sampleObj = arr.items[0].value;
|
|
1104
|
+
for (const key of Object.keys(sampleObj)) {
|
|
1105
|
+
if (/^(id|_id|key|slug)$/i.test(key)) continue;
|
|
1106
|
+
const role = /image|photo|avatar|icon|thumb|img/i.test(key)
|
|
1107
|
+
? 'image'
|
|
1108
|
+
: /price|cost|amount|count|rating|stars/i.test(key)
|
|
1109
|
+
? 'number'
|
|
1110
|
+
: /url|link|href/i.test(key)
|
|
1111
|
+
? 'url'
|
|
1112
|
+
: 'text';
|
|
1113
|
+
itemUsage.set(key, role);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
922
1117
|
const boundProperties = [...itemUsage.keys()];
|
|
923
1118
|
const convertible =
|
|
924
1119
|
(objectItems &&
|
|
925
1120
|
boundProperties.length > 0 &&
|
|
926
1121
|
arr.items.some((item) => boundProperties.some((key) => key in item.value))) ||
|
|
927
|
-
(stringItems && usesDirectItem)
|
|
1122
|
+
(stringItems && usesDirectItem) ||
|
|
1123
|
+
(isChildCustomComponent && objectItems && boundProperties.length > 0);
|
|
928
1124
|
|
|
929
1125
|
candidates.push({
|
|
930
1126
|
...baseMeta,
|
|
@@ -939,18 +1135,21 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
939
1135
|
indexParam: mapInfo.indexParam,
|
|
940
1136
|
itemFields: stringItems ? [] : [...itemUsage.entries()].map(([key, role]) => ({ key, role })),
|
|
941
1137
|
objectItems,
|
|
942
|
-
hasComponentRef: usesItemAsComponent,
|
|
1138
|
+
hasComponentRef: usesItemAsComponent && !isChildCustomComponent,
|
|
1139
|
+
childComponentName: isChildCustomComponent ? rootTagName : null,
|
|
943
1140
|
},
|
|
944
1141
|
confidence: convertible
|
|
945
1142
|
? confidenceFor('collection', { staticCollection: true })
|
|
946
1143
|
: usesItemAsComponent
|
|
947
1144
|
? 0.2
|
|
948
1145
|
: 0.82,
|
|
949
|
-
reason: usesItemAsComponent
|
|
1146
|
+
reason: usesItemAsComponent && !isChildCustomComponent
|
|
950
1147
|
? 'collection-holds-component-ref'
|
|
951
|
-
:
|
|
952
|
-
?
|
|
953
|
-
:
|
|
1148
|
+
: isChildCustomComponent
|
|
1149
|
+
? 'collection-with-child-component'
|
|
1150
|
+
: convertible
|
|
1151
|
+
? (stringItems ? 'static-primitive-array-map' : 'static-array-map')
|
|
1152
|
+
: 'collection-shape-partial',
|
|
954
1153
|
fingerprint: fingerprintCandidate({ tag: name, kind: 'collection', size: arr.items.length }),
|
|
955
1154
|
});
|
|
956
1155
|
}
|
|
@@ -995,4 +1194,6 @@ module.exports = {
|
|
|
995
1194
|
fingerprintCandidate,
|
|
996
1195
|
normalizeText,
|
|
997
1196
|
isStaticSkipText,
|
|
1197
|
+
resolveChildText,
|
|
998
1198
|
};
|
|
1199
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getJsxName, collectJsxText, findJsxAttribute } = require('./ast.cjs');
|
|
4
|
+
const { isIconComponent, DECORATIVE_TAGS, HEADING_TAGS, ACTION_TAGS } = require('./adapters.cjs');
|
|
5
|
+
|
|
6
|
+
const INLINE_FORMAT_TAGS = new Set([
|
|
7
|
+
'span', 'strong', 'em', 'b', 'i', 'u', 'code', 'mark', 'small', 'sub', 'sup',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function getAttributeString(node, attrName) {
|
|
11
|
+
const attr = findJsxAttribute(node, attrName);
|
|
12
|
+
if (!attr || !attr.value) return '';
|
|
13
|
+
if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
|
|
14
|
+
return String(attr.value.value || '');
|
|
15
|
+
}
|
|
16
|
+
if (attr.value.type === 'JSXExpressionContainer') {
|
|
17
|
+
const expr = attr.value.expression;
|
|
18
|
+
if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
|
|
19
|
+
return String(expr.value || '');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Analyzes the children of a JSX element into structured fragments.
|
|
27
|
+
* Identifies:
|
|
28
|
+
* - raw text literals
|
|
29
|
+
* - inline formatted elements (span, strong, em, etc.)
|
|
30
|
+
* - decorative icon components
|
|
31
|
+
* - nested links or buttons
|
|
32
|
+
* - dynamic expression containers
|
|
33
|
+
*/
|
|
34
|
+
function analyzeElementFragments(elementNode) {
|
|
35
|
+
if (!elementNode || elementNode.type !== 'JSXElement') {
|
|
36
|
+
return {
|
|
37
|
+
fragments: [],
|
|
38
|
+
hasIcons: false,
|
|
39
|
+
hasInlineFormatting: false,
|
|
40
|
+
hasDynamicExpressions: false,
|
|
41
|
+
isCompoundText: false,
|
|
42
|
+
isButtonWithIcon: false,
|
|
43
|
+
isHighlightedHeading: false,
|
|
44
|
+
pureTextFragments: [],
|
|
45
|
+
combinedText: '',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tag = getJsxName(elementNode);
|
|
50
|
+
const children = elementNode.children || [];
|
|
51
|
+
const fragments = [];
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < children.length; i++) {
|
|
54
|
+
const child = children[i];
|
|
55
|
+
|
|
56
|
+
if (child.type === 'JSXText') {
|
|
57
|
+
const raw = child.value;
|
|
58
|
+
const trimmed = raw.replace(/\s+/g, ' ').trim();
|
|
59
|
+
if (trimmed.length > 0) {
|
|
60
|
+
fragments.push({
|
|
61
|
+
type: 'raw-text',
|
|
62
|
+
raw,
|
|
63
|
+
text: trimmed,
|
|
64
|
+
nodeIndex: i,
|
|
65
|
+
node: child,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (child.type === 'JSXElement') {
|
|
72
|
+
const childTag = getJsxName(child);
|
|
73
|
+
const childClassName = getAttributeString(child, 'className') || getAttributeString(child, 'class');
|
|
74
|
+
|
|
75
|
+
if (isIconComponent(childTag, '') || DECORATIVE_TAGS.has(childTag)) {
|
|
76
|
+
fragments.push({
|
|
77
|
+
type: 'icon',
|
|
78
|
+
tag: childTag,
|
|
79
|
+
nodeIndex: i,
|
|
80
|
+
node: child,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (INLINE_FORMAT_TAGS.has(childTag.toLowerCase())) {
|
|
86
|
+
const innerText = collectJsxText(child).trim();
|
|
87
|
+
fragments.push({
|
|
88
|
+
type: 'inline-format',
|
|
89
|
+
tag: childTag,
|
|
90
|
+
className: childClassName,
|
|
91
|
+
text: innerText,
|
|
92
|
+
nodeIndex: i,
|
|
93
|
+
node: child,
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (childTag === 'a' || childTag === 'Link') {
|
|
99
|
+
const href = getAttributeString(child, 'href');
|
|
100
|
+
const linkText = collectJsxText(child).trim();
|
|
101
|
+
fragments.push({
|
|
102
|
+
type: 'link',
|
|
103
|
+
tag: childTag,
|
|
104
|
+
href,
|
|
105
|
+
text: linkText,
|
|
106
|
+
nodeIndex: i,
|
|
107
|
+
node: child,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Other nested element
|
|
113
|
+
fragments.push({
|
|
114
|
+
type: 'element',
|
|
115
|
+
tag: childTag,
|
|
116
|
+
className: childClassName,
|
|
117
|
+
text: collectJsxText(child).trim(),
|
|
118
|
+
nodeIndex: i,
|
|
119
|
+
node: child,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
125
|
+
fragments.push({
|
|
126
|
+
type: 'dynamic-expr',
|
|
127
|
+
nodeIndex: i,
|
|
128
|
+
node: child,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hasIcons = fragments.some((f) => f.type === 'icon');
|
|
135
|
+
const hasInlineFormatting = fragments.some((f) => f.type === 'inline-format');
|
|
136
|
+
const hasDynamicExpressions = fragments.some((f) => f.type === 'dynamic-expr');
|
|
137
|
+
const pureTextFragments = fragments.filter((f) => f.type === 'raw-text' || f.type === 'inline-format');
|
|
138
|
+
const combinedText = pureTextFragments.map((f) => f.text).join(' ').replace(/\s+/g, ' ').trim();
|
|
139
|
+
|
|
140
|
+
const isButtonWithIcon =
|
|
141
|
+
ACTION_TAGS.has(tag) &&
|
|
142
|
+
hasIcons &&
|
|
143
|
+
pureTextFragments.length >= 1 &&
|
|
144
|
+
!hasDynamicExpressions;
|
|
145
|
+
|
|
146
|
+
const isHighlightedHeading =
|
|
147
|
+
HEADING_TAGS.has(tag) &&
|
|
148
|
+
hasInlineFormatting &&
|
|
149
|
+
pureTextFragments.length >= 2 &&
|
|
150
|
+
!hasDynamicExpressions;
|
|
151
|
+
|
|
152
|
+
const isCompoundText =
|
|
153
|
+
(hasInlineFormatting || (hasIcons && pureTextFragments.length > 0)) &&
|
|
154
|
+
pureTextFragments.length > 0;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
tag,
|
|
158
|
+
fragments,
|
|
159
|
+
hasIcons,
|
|
160
|
+
hasInlineFormatting,
|
|
161
|
+
hasDynamicExpressions,
|
|
162
|
+
isCompoundText,
|
|
163
|
+
isButtonWithIcon,
|
|
164
|
+
isHighlightedHeading,
|
|
165
|
+
pureTextFragments,
|
|
166
|
+
combinedText,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
INLINE_FORMAT_TAGS,
|
|
172
|
+
analyzeElementFragments,
|
|
173
|
+
};
|