sicua
Version:
A tool for analyzing project structure and dependencies
588 lines (587 loc) • 21.9 kB
JavaScript
;
/**
* Enhanced text content extractor for JSX elements
* Handles complex patterns like conditional rendering, variables, and nested elements
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TextContentExtractor = void 0;
const jsxAnalysisUtils_1 = require("./jsxAnalysisUtils");
const constants_1 = require("../constants");
class TextContentExtractor {
/**
* Extracts accessible text content from JSX elements
* Handles conditional rendering, variables, nested text, and ARIA labeling
*/
static extractAccessibleText(element) {
// First check for ARIA labeling attributes
const ariaText = this.extractAriaText(element);
if (ariaText) {
return ariaText;
}
// Check for title attribute
const titleText = this.extractTitleText(element);
if (titleText) {
return titleText;
}
// Extract text content from element and children
const contentText = this.extractContentText(element);
if (contentText) {
return contentText;
}
return null;
}
/**
* Extracts text from ARIA labeling attributes
*/
static extractAriaText(element) {
for (const ariaAttr of constants_1.ARIA_LABELING_ATTRIBUTES) {
const prop = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropValue(element, ariaAttr);
if (prop) {
const text = this.extractTextFromProp(prop);
if (text && text.trim()) {
return text.trim();
}
}
}
return null;
}
/**
* Extracts text from title attribute
*/
static extractTitleText(element) {
const titleProp = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropValue(element, "title");
if (titleProp) {
const text = this.extractTextFromProp(titleProp);
if (text && text.trim()) {
return text.trim();
}
}
return null;
}
/**
* Extracts text content from element's children and text content
*/
static extractContentText(element) {
const textParts = [];
// Check direct text content
if (element.textContent && element.textContent.trim()) {
textParts.push(element.textContent.trim());
}
// Check children for text content
const childrenText = this.extractTextFromChildren(element.children);
if (childrenText) {
textParts.push(childrenText);
}
// Handle JSX expressions that might contain text
const expressionText = this.extractTextFromExpressions(element);
if (expressionText) {
textParts.push(expressionText);
}
return textParts.length > 0 ? textParts.join(" ").trim() : null;
}
/**
* Extracts text from JSX prop values handling different types
*/
static extractTextFromProp(prop) {
switch (prop.type) {
case "string":
return prop.value;
case "number":
case "boolean":
return String(prop.value);
case "expression":
// Try to extract text from expression
if (prop.rawValue) {
return this.extractTextFromExpression(prop.rawValue);
}
return null;
default:
return null;
}
}
/**
* Recursively extracts text from child elements
*/
static extractTextFromChildren(children) {
const textParts = [];
for (const child of children) {
// Skip certain non-text elements
if (this.isNonTextElement(child)) {
continue;
}
// Get text from child
const childText = this.extractAccessibleText(child);
if (childText && childText.trim()) {
textParts.push(childText.trim());
}
}
return textParts.length > 0 ? textParts.join(" ").trim() : null;
}
/**
* Extracts text from JSX expressions in props or children
*/
static extractTextFromExpressions(element) {
const textParts = [];
// Check all props for expressions that might contain text
for (const [propName, propValue] of Object.entries(element.props)) {
if (propValue.type === "expression" && propValue.rawValue) {
const expressionText = this.extractTextFromExpression(propValue.rawValue);
if (expressionText) {
textParts.push(expressionText);
}
}
}
return textParts.length > 0 ? textParts.join(" ").trim() : null;
}
/**
* Extracts text from JSX expression strings
* Handles conditional rendering, variables, and template literals
*/
static extractTextFromExpression(expression) {
const cleaned = expression.trim();
// Handle string literals
if (this.isStringLiteral(cleaned)) {
return this.extractStringLiteral(cleaned);
}
// Handle conditional expressions (ternary operators)
const conditionalText = this.extractTextFromConditional(cleaned);
if (conditionalText) {
return conditionalText;
}
// Handle logical expressions (&&, ||)
const logicalText = this.extractTextFromLogical(cleaned);
if (logicalText) {
return logicalText;
}
// Handle template literals
const templateText = this.extractTextFromTemplate(cleaned);
if (templateText) {
return templateText;
}
// Handle variable references that might contain text
const variableText = this.extractTextFromVariable(cleaned);
if (variableText) {
return variableText;
}
return null;
}
/**
* Checks if a string is a string literal
*/
static isStringLiteral(expression) {
return ((expression.startsWith('"') && expression.endsWith('"')) ||
(expression.startsWith("'") && expression.endsWith("'")) ||
(expression.startsWith("`") && expression.endsWith("`")));
}
/**
* Extracts text from string literals
*/
static extractStringLiteral(expression) {
return expression.slice(1, -1); // Remove quotes
}
/**
* Extracts text from conditional expressions (ternary)
* Pattern: condition ? "text1" : "text2"
*/
static extractTextFromConditional(expression) {
const ternaryMatch = expression.match(/.*\?\s*(.+?)\s*:\s*(.+)/);
if (ternaryMatch) {
const [, trueBranch, falseBranch] = ternaryMatch;
const trueText = this.extractTextFromExpression(trueBranch.trim());
const falseText = this.extractTextFromExpression(falseBranch.trim());
// Return the first valid text found
if (trueText && trueText.trim()) {
return trueText.trim();
}
if (falseText && falseText.trim()) {
return falseText.trim();
}
}
return null;
}
/**
* Extracts text from logical expressions
* Pattern: condition && "text" or condition || "text"
*/
static extractTextFromLogical(expression) {
// Handle && expressions
const andMatch = expression.match(/.*&&\s*(.+)/);
if (andMatch) {
const rightSide = andMatch[1].trim();
const text = this.extractTextFromExpression(rightSide);
if (text && text.trim()) {
return text.trim();
}
}
// Handle || expressions
const orMatch = expression.match(/(.+?)\s*\|\|\s*(.+)/);
if (orMatch) {
const [, leftSide, rightSide] = orMatch;
const leftText = this.extractTextFromExpression(leftSide.trim());
if (leftText && leftText.trim()) {
return leftText.trim();
}
const rightText = this.extractTextFromExpression(rightSide.trim());
if (rightText && rightText.trim()) {
return rightText.trim();
}
}
return null;
}
/**
* Extracts text from template literals
*/
static extractTextFromTemplate(expression) {
if (expression.startsWith("`") && expression.endsWith("`")) {
// Simple template literal without expressions
const content = expression.slice(1, -1);
if (!content.includes("${")) {
return content;
}
// Template with expressions - extract static parts
const staticParts = content.split(/\$\{[^}]+\}/);
const staticText = staticParts.join("").trim();
if (staticText) {
return staticText;
}
}
return null;
}
/**
* Extracts text from variable references
* Handles common variable names that likely contain text
*/
static extractTextFromVariable(expression) {
// Common variable patterns that likely contain text
const textVariablePatterns = [
/\b(text|label|title|message|content|name|value)\b/i,
/\b\w*Text\b/i,
/\b\w*Label\b/i,
/\b\w*Title\b/i,
/\b\w*Message\b/i,
/\b\w*Content\b/i,
];
for (const pattern of textVariablePatterns) {
if (pattern.test(expression)) {
// This is likely a variable containing text
// We can't evaluate it statically, but we know it probably has text
return "[Variable Text]";
}
}
return null;
}
/**
* Checks if an element is non-text (decorative, hidden, etc.)
*/
static isNonTextElement(element) {
// Check for aria-hidden="true"
const ariaHidden = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "aria-hidden");
if (ariaHidden === "true") {
return true;
}
// Check for decorative role
const role = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "role");
if (role === "presentation" || role === "none") {
return true;
}
// Check for common decorative elements
const decorativeElements = ["svg", "img", "icon", "loader", "spinner"];
if (decorativeElements.includes(element.tagName.toLowerCase())) {
// For images, check if they have empty alt text (decorative)
if (element.tagName.toLowerCase() === "img") {
const alt = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "alt");
return alt === "";
}
return true;
}
// Check for screen reader only content (common class patterns)
const className = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "className");
if (className) {
const srOnlyPatterns = [
/\bsr-only\b/,
/\bscreen-reader-only\b/,
/\bvisually-hidden\b/,
/\bhidden\b/,
];
for (const pattern of srOnlyPatterns) {
if (pattern.test(className)) {
return false; // SR-only content IS accessible text
}
}
}
return false;
}
/**
* Checks if an element has any form of accessible text
*/
static hasAccessibleText(element) {
const text = this.extractAccessibleText(element);
const hasStaticText = text !== null && text.trim().length > 0 && text !== "[Variable Text]";
if (hasStaticText) {
return true;
}
// Enhanced JSX expression analysis
return this.hasJSXExpressionText(element);
}
/**
* Checks if an element likely has text but we can't extract it statically
*/
static likelyHasText(element) {
const text = this.extractAccessibleText(element);
const hasVariableText = text === "[Variable Text]";
if (hasVariableText) {
return true;
}
return (this.hasJSXExpressionText(element) || this.hasTextIndicatingProps(element));
}
/**
* Check for JSX expressions that likely contain text
*/
static hasJSXExpressionText(element) {
// Check if we have access to the original JSX content through context
if (element.context) {
return this.analyzeJSXContext(element.context);
}
// Check children for expressions
if (element.children && element.children.length > 0) {
return this.analyzeChildrenForExpressions(element.children);
}
// Check props for children expressions
const childrenProp = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropValue(element, "children");
if (childrenProp &&
childrenProp.type === "expression" &&
childrenProp.rawValue) {
return this.isTextExpression(childrenProp.rawValue);
}
return false;
}
/**
* Analyze JSX context string for text expressions
*/
static analyzeJSXContext(context) {
// Look for translation function calls in JSX
const translationInJSX = [
/\{[^}]*t\s*\([^)]+\)[^}]*\}/g, // {t("key")}
/\{[^}]*i18n\s*\([^)]+\)[^}]*\}/g, // {i18n("key")}
/\{[^}]*translate\s*\([^)]+\)[^}]*\}/g, // {translate("key")}
/\{[^}]*_\s*\([^)]+\)[^}]*\}/g, // {_("key")}
/\{[^}]*formatMessage\s*\([^)]+\)[^}]*\}/g, // {formatMessage()}
/\{[^}]*getText\s*\([^)]+\)[^}]*\}/g, // {getText()}
];
if (translationInJSX.some((pattern) => pattern.test(context))) {
return true;
}
// Look for string literals in JSX expressions
const stringLiteralsInJSX = /\{[^}]*["'`][^"'`]+["'`][^}]*\}/g;
if (stringLiteralsInJSX.test(context)) {
return true;
}
// Look for conditional expressions with text
const conditionalWithText = [
/\{[^}]*\?[^}]*["'`][^"'`]+["'`][^}]*:[^}]*["'`][^"'`]+["'`][^}]*\}/g, // {condition ? "text1" : "text2"}
/\{[^}]*\?[^}]*t\s*\([^)]+\)[^}]*:[^}]*t\s*\([^)]+\)[^}]*\}/g, // {condition ? t("key1") : t("key2")}
];
if (conditionalWithText.some((pattern) => pattern.test(context))) {
return true;
}
// Look for template literals
const templateLiterals = /\{[^}]*`[^`]*\$\{[^}]*\}[^`]*`[^}]*\}/g;
if (templateLiterals.test(context)) {
return true;
}
// Look for variable names that likely contain text
const textVariables = [
/\{[^}]*(text|label|title|message|content|name|children)[^}]*\}/gi,
/\{[^}]*[a-zA-Z_$][a-zA-Z0-9_$]*Text[^}]*\}/g,
/\{[^}]*[a-zA-Z_$][a-zA-Z0-9_$]*Label[^}]*\}/g,
];
if (textVariables.some((pattern) => pattern.test(context))) {
return true;
}
return false;
}
/**
* Analyze children for JSX expressions that contain text
*/
static analyzeChildrenForExpressions(children) {
for (const child of children) {
// Skip decorative elements
if (this.isDecorativeElement(child)) {
continue;
}
// Check if child has text content
if (child.textContent && child.textContent.trim()) {
return true;
}
// Check child's context for expressions
if (child.context && this.analyzeJSXContext(child.context)) {
return true;
}
// Recursively check child's children
if (child.children && child.children.length > 0) {
if (this.analyzeChildrenForExpressions(child.children)) {
return true;
}
}
}
return false;
}
/**
* Check if element has props that indicate text content
*/
static hasTextIndicatingProps(element) {
const textProps = [
"children",
"label",
"text",
"title",
"value",
"placeholder",
];
for (const propName of textProps) {
const prop = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropValue(element, propName);
if (prop) {
if (prop.type === "string" &&
prop.value &&
prop.value.trim()) {
return true;
}
if (prop.type === "expression" &&
prop.rawValue &&
this.isTextExpression(prop.rawValue)) {
return true;
}
}
}
return false;
}
/**
* Enhanced text expression detection
*/
static isTextExpression(expression) {
const cleanExpr = expression.trim();
// Translation function patterns (more comprehensive)
const translationPatterns = [
/^t\s*\(/, // t("key")
/^i18n\s*\(/, // i18n("key")
/^translate\s*\(/, // translate("key")
/^_\s*\(/, // _("key")
/^__\s*\(/, // __("key")
/^tr\s*\(/, // tr("key")
/^getText\s*\(/, // getText("key")
/^gettext\s*\(/, // gettext("key")
/^formatMessage\s*\(/, // formatMessage({id: "key"})
/^intl\.formatMessage\s*\(/, // intl.formatMessage()
/^this\.props\.intl\.formatMessage\s*\(/, // this.props.intl.formatMessage()
];
if (translationPatterns.some((pattern) => pattern.test(cleanExpr))) {
return true;
}
// String literals
if (/^["'`]/.test(cleanExpr) &&
/["'`]$/.test(cleanExpr) &&
cleanExpr.length > 2) {
const content = cleanExpr.slice(1, -1);
return content.trim().length > 0;
}
// Template literals with interpolation
if (/^`[^`]*\$\{.*\}[^`]*`$/.test(cleanExpr)) {
return true;
}
// Conditional expressions with text
const conditionalWithText = [
/.*\?\s*["'`][^"'`]+["'`]\s*:\s*["'`][^"'`]+["'`].*/, // condition ? "text1" : "text2"
/.*\?\s*t\s*\([^)]+\)\s*:\s*t\s*\([^)]+\).*/, // condition ? t("key1") : t("key2")
/.*\?\s*["'`][^"'`]+["'`]\s*:\s*t\s*\([^)]+\).*/, // condition ? "text" : t("key")
];
if (conditionalWithText.some((pattern) => pattern.test(cleanExpr))) {
return true;
}
// Variable names that likely contain text
const textVariablePatterns = [
/^(text|label|title|message|content|name|value|children)$/i,
/^[a-zA-Z_$][a-zA-Z0-9_$]*Text$/,
/^[a-zA-Z_$][a-zA-Z0-9_$]*Label$/,
/^[a-zA-Z_$][a-zA-Z0-9_$]*Message$/,
/^[a-zA-Z_$][a-zA-Z0-9_$]*Content$/,
/^(buttonText|btnText|linkText|menuText|tooltipText)$/i,
];
if (textVariablePatterns.some((pattern) => pattern.test(cleanExpr))) {
return true;
}
// Property access for text
if (/^[a-zA-Z_$][a-zA-Z0-9_$.]*\.(text|label|title|message|content|name|value|children)\b/i.test(cleanExpr)) {
return true;
}
// Function calls that likely return text
const textFunctionPatterns = [
/^get[A-Z][a-zA-Z]*Text\s*\(/,
/^format[A-Z][a-zA-Z]*\s*\(/,
/^render[A-Z][a-zA-Z]*\s*\(/,
/^[a-zA-Z_$][a-zA-Z0-9_$]*\.getText\s*\(/,
/^[a-zA-Z_$][a-zA-Z0-9_$]*\.getLabel\s*\(/,
];
if (textFunctionPatterns.some((pattern) => pattern.test(cleanExpr))) {
return true;
}
// Logical expressions where one side likely has text
if (/.*\|\|\s*["'`][^"'`]+["'`]/.test(cleanExpr) ||
/.*\|\|\s*t\s*\(/.test(cleanExpr)) {
return true;
}
return false;
}
/**
* Check if element is decorative (enhanced version)
*/
static isDecorativeElement(element) {
const tagName = element.tagName.toLowerCase();
// Icon and loader components
const decorativeComponents = [
"loader",
"loader2",
"spinner",
"loading",
"icon",
"svg",
"upload",
"eye",
"creditcard",
"chevrondown",
"chevronsupdown",
"mappin",
"check",
"x",
"plus",
"minus",
"arrow",
"caret",
];
if (decorativeComponents.some((comp) => tagName.includes(comp))) {
return true;
}
// Check for aria-hidden
const ariaHidden = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "aria-hidden");
if (ariaHidden === "true") {
return true;
}
// Check for decorative classes
const className = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "className");
if (className) {
const decorativePatterns = [
/\bicon\b/i,
/\bspinner\b/i,
/\bloader\b/i,
/\banimate-spin\b/i,
/\bsr-only\b/i,
/\bhidden\b/i,
/\bopacity-\d+\b/i,
];
if (decorativePatterns.some((pattern) => pattern.test(className))) {
return true;
}
}
return false;
}
}
exports.TextContentExtractor = TextContentExtractor;