react-native-to-xml-converter 0.0.1
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 +210 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +100 -0
- package/dist/generators/xmlGenerator.d.ts +9 -0
- package/dist/generators/xmlGenerator.js +90 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +29 -0
- package/dist/parser/astTypes.d.ts +35 -0
- package/dist/parser/astTypes.js +3 -0
- package/dist/parser/parseRN.d.ts +6 -0
- package/dist/parser/parseRN.js +72 -0
- package/dist/transformer/layoutBuilder.d.ts +6 -0
- package/dist/transformer/layoutBuilder.js +200 -0
- package/dist/transformer/rnNodeMapper.d.ts +12 -0
- package/dist/transformer/rnNodeMapper.js +48 -0
- package/dist/transformer/styleMapper.d.ts +9 -0
- package/dist/transformer/styleMapper.js +151 -0
- package/dist/utils/helpers.d.ts +29 -0
- package/dist/utils/helpers.js +88 -0
- package/package.json +47 -0
- package/react-native-to-xml-converter-0.0.1.tgz +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# React Native to XML Converter
|
|
2
|
+
|
|
3
|
+
A powerful CLI tool that converts React Native components to Android XML layouts. This tool parses your React Native JSX/TSX files and generates corresponding Android XML layout files, making it easy to port React Native components to native Android.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔄 **Automatic Conversion**: Converts React Native components to Android XML
|
|
8
|
+
- 🎨 **Style Mapping**: Maps React Native styles to Android attributes
|
|
9
|
+
- 📦 **Component Support**: Supports common RN components (View, Text, ScrollView, etc.)
|
|
10
|
+
- 🚀 **CLI Tool**: Easy-to-use command-line interface
|
|
11
|
+
- 📁 **Organized Output**: Generates XML files in a dedicated `xml/` folder
|
|
12
|
+
- ✨ **TypeScript Support**: Written in TypeScript with full type definitions
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Global Installation`
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g react-native-to-xml-converter
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Local Installation
|
|
22
|
+
```bash
|
|
23
|
+
npm install --save-dev react-native-to-xml-converter
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Using npx (No Installation Required)
|
|
27
|
+
```bash
|
|
28
|
+
npx react-native-to-xml-converter <input-file>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Basic Usage
|
|
34
|
+
```bash
|
|
35
|
+
# Using installed package
|
|
36
|
+
rn-to-xml example.ts
|
|
37
|
+
|
|
38
|
+
# Using npx
|
|
39
|
+
npx rn-to-xml example.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The converted XML file will be saved in the `xml/` folder in your current directory.
|
|
43
|
+
|
|
44
|
+
### Example
|
|
45
|
+
|
|
46
|
+
**Input (example.ts):**
|
|
47
|
+
```tsx
|
|
48
|
+
import { View, Text } from 'react-native';
|
|
49
|
+
|
|
50
|
+
export default function Example() {
|
|
51
|
+
return (
|
|
52
|
+
<View style={{ padding: 20, backgroundColor: '#f0f0f0' }}>
|
|
53
|
+
<Text style={{ fontSize: 18, color: '#333' }}>Hello World</Text>
|
|
54
|
+
<View style={{ marginTop: 10 }}>
|
|
55
|
+
<Text>This is a test component</Text>
|
|
56
|
+
</View>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Output (xml/example.xml):**
|
|
63
|
+
```xml
|
|
64
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
65
|
+
<LinearLayout
|
|
66
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
67
|
+
android:id="@+id/view_1"
|
|
68
|
+
android:layout_width="wrap_content"
|
|
69
|
+
android:layout_height="wrap_content"
|
|
70
|
+
android:orientation="vertical"
|
|
71
|
+
android:background="#f0f0f0"
|
|
72
|
+
android:padding="20dp">
|
|
73
|
+
<TextView
|
|
74
|
+
android:id="@+id/text_2"
|
|
75
|
+
android:layout_width="wrap_content"
|
|
76
|
+
android:layout_height="wrap_content"
|
|
77
|
+
android:text="Hello World"
|
|
78
|
+
android:textColor="#333"
|
|
79
|
+
android:textSize="18sp"/>
|
|
80
|
+
<LinearLayout
|
|
81
|
+
android:id="@+id/view_3"
|
|
82
|
+
android:layout_width="wrap_content"
|
|
83
|
+
android:layout_height="wrap_content"
|
|
84
|
+
android:orientation="vertical"
|
|
85
|
+
android:layout_marginTop="10dp">
|
|
86
|
+
<TextView
|
|
87
|
+
android:id="@+id/text_4"
|
|
88
|
+
android:layout_width="wrap_content"
|
|
89
|
+
android:layout_height="wrap_content"
|
|
90
|
+
android:text="This is a test component"/>
|
|
91
|
+
</LinearLayout>
|
|
92
|
+
</LinearLayout>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Supported Components
|
|
96
|
+
|
|
97
|
+
| React Native | Android XML |
|
|
98
|
+
|-------------|-------------|
|
|
99
|
+
| `View` | `LinearLayout` |
|
|
100
|
+
| `Text` | `TextView` |
|
|
101
|
+
| `Image` | `ImageView` |
|
|
102
|
+
| `ScrollView` | `ScrollView` |
|
|
103
|
+
| `TextInput` | `EditText` |
|
|
104
|
+
| `Button` | `Button` |
|
|
105
|
+
| `TouchableOpacity` | `FrameLayout` |
|
|
106
|
+
| `FlatList` | `RecyclerView` |
|
|
107
|
+
|
|
108
|
+
## Supported Styles
|
|
109
|
+
|
|
110
|
+
### Layout Properties
|
|
111
|
+
- `padding`, `paddingTop`, `paddingBottom`, `paddingLeft`, `paddingRight`
|
|
112
|
+
- `paddingHorizontal`, `paddingVertical`
|
|
113
|
+
- `margin`, `marginTop`, `marginBottom`, `marginLeft`, `marginRight`
|
|
114
|
+
- `marginHorizontal`, `marginVertical`
|
|
115
|
+
- `width`, `height`
|
|
116
|
+
|
|
117
|
+
### View Properties
|
|
118
|
+
- `backgroundColor`
|
|
119
|
+
- `flexDirection` (determines layout orientation)
|
|
120
|
+
- `alignItems`, `justifyContent` (converted to gravity)
|
|
121
|
+
|
|
122
|
+
### Text Properties
|
|
123
|
+
- `fontSize`
|
|
124
|
+
- `color`
|
|
125
|
+
- `fontWeight`
|
|
126
|
+
- `textAlign`
|
|
127
|
+
|
|
128
|
+
## Programmatic API
|
|
129
|
+
|
|
130
|
+
You can also use this package programmatically in your Node.js scripts:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { convertRNToXML } from 'react-native-to-xml-converter';
|
|
134
|
+
|
|
135
|
+
const code = `
|
|
136
|
+
import { View, Text } from 'react-native';
|
|
137
|
+
|
|
138
|
+
export default function App() {
|
|
139
|
+
return (
|
|
140
|
+
<View style={{ padding: 20 }}>
|
|
141
|
+
<Text>Hello World</Text>
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
const xml = convertRNToXML(code);
|
|
148
|
+
console.log(xml);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
### Building from Source
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Clone the repository
|
|
157
|
+
git clone https://github.com/Adhidevudayakumar/react-native-to-xml-converter.git
|
|
158
|
+
cd react-native-to-xml-converter
|
|
159
|
+
|
|
160
|
+
# Install dependencies
|
|
161
|
+
npm install
|
|
162
|
+
|
|
163
|
+
# Build the project
|
|
164
|
+
npm run build
|
|
165
|
+
|
|
166
|
+
# Link locally for testing
|
|
167
|
+
npm link
|
|
168
|
+
|
|
169
|
+
# Test the CLI
|
|
170
|
+
npx rn-to-xml example.ts
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Project Structure
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
├── src/
|
|
177
|
+
│ ├── cli/ # CLI entry point
|
|
178
|
+
│ ├── parser/ # AST parsing logic
|
|
179
|
+
│ ├── transformer/ # Component and style mappers
|
|
180
|
+
│ ├── generators/ # XML generation
|
|
181
|
+
│ ├── utils/ # Helper utilities
|
|
182
|
+
│ └── index.ts # Public API
|
|
183
|
+
├── dist/ # Compiled output
|
|
184
|
+
├── xml/ # Generated XML files (created on first run)
|
|
185
|
+
└── package.json
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Limitations
|
|
189
|
+
|
|
190
|
+
- Only supports inline styles (not StyleSheet.create references yet)
|
|
191
|
+
- Some complex React Native components may not have direct Android equivalents
|
|
192
|
+
- Nested Text components are flattened to single TextView
|
|
193
|
+
- Custom components are treated as View by default
|
|
194
|
+
|
|
195
|
+
## Contributing
|
|
196
|
+
|
|
197
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
ISC
|
|
202
|
+
|
|
203
|
+
## Author
|
|
204
|
+
|
|
205
|
+
Adhidev Udayakumar
|
|
206
|
+
|
|
207
|
+
## Links
|
|
208
|
+
|
|
209
|
+
- [GitHub Repository](https://github.com/Adhidevudayakumar/react-native-to-xml-converter)
|
|
210
|
+
- [Report Issues](https://github.com/Adhidevudayakumar/react-native-to-xml-converter/issues)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// CLI entry point
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const index_1 = require("../index");
|
|
41
|
+
/**
|
|
42
|
+
* Main CLI function
|
|
43
|
+
*/
|
|
44
|
+
function main() {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
// Check if file path is provided
|
|
47
|
+
if (args.length === 0) {
|
|
48
|
+
console.error('❌ Error: No input file specified');
|
|
49
|
+
console.log('\nUsage: npx rn-to-xml <input-file.ts|tsx>');
|
|
50
|
+
console.log('Example: npx rn-to-xml example.ts');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const inputFile = args[0];
|
|
54
|
+
const absolutePath = path.resolve(process.cwd(), inputFile);
|
|
55
|
+
// Check if file exists
|
|
56
|
+
if (!fs.existsSync(absolutePath)) {
|
|
57
|
+
console.error(`❌ Error: File not found: ${inputFile}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
// Read input file
|
|
61
|
+
let code;
|
|
62
|
+
try {
|
|
63
|
+
code = fs.readFileSync(absolutePath, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`❌ Error reading file: ${error.message}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Convert to XML
|
|
70
|
+
let xml;
|
|
71
|
+
try {
|
|
72
|
+
console.log('🔄 Converting React Native to XML...');
|
|
73
|
+
xml = (0, index_1.convertRNToXML)(code);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error(`❌ Conversion failed: ${error.message}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
// Create output directory
|
|
80
|
+
const outputDir = path.join(process.cwd(), 'xml');
|
|
81
|
+
if (!fs.existsSync(outputDir)) {
|
|
82
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
// Generate output filename
|
|
85
|
+
const inputBaseName = path.basename(inputFile, path.extname(inputFile));
|
|
86
|
+
const outputFile = path.join(outputDir, `${inputBaseName}.xml`);
|
|
87
|
+
// Write output file
|
|
88
|
+
try {
|
|
89
|
+
fs.writeFileSync(outputFile, xml, 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error(`❌ Error writing output file: ${error.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// Success message
|
|
96
|
+
console.log('✅ Conversion successful!');
|
|
97
|
+
console.log(`📁 Output saved to: ${path.relative(process.cwd(), outputFile)}`);
|
|
98
|
+
}
|
|
99
|
+
// Run CLI
|
|
100
|
+
main();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { XMLNode } from '../parser/astTypes';
|
|
2
|
+
/**
|
|
3
|
+
* Generates XML string from XML tree
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateXml(root: XMLNode): string;
|
|
6
|
+
/**
|
|
7
|
+
* Formats generated XML for better readability
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatGeneratedXml(xml: string): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Converts tree → XML string
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.generateXml = generateXml;
|
|
5
|
+
exports.formatGeneratedXml = formatGeneratedXml;
|
|
6
|
+
const helpers_1 = require("../utils/helpers");
|
|
7
|
+
/**
|
|
8
|
+
* Generates XML string from XML tree
|
|
9
|
+
*/
|
|
10
|
+
function generateXml(root) {
|
|
11
|
+
const xmlDeclaration = '<?xml version="1.0" encoding="utf-8"?>';
|
|
12
|
+
const rootElement = generateElement(root, 0);
|
|
13
|
+
return `${xmlDeclaration}\n${rootElement}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generates XML element recursively
|
|
17
|
+
*/
|
|
18
|
+
function generateElement(node, depth) {
|
|
19
|
+
const indent = ' '.repeat(depth);
|
|
20
|
+
const childIndent = ' '.repeat(depth + 1);
|
|
21
|
+
// Prepare attributes
|
|
22
|
+
const attributes = { ...node.attributes };
|
|
23
|
+
if (depth === 0) {
|
|
24
|
+
attributes['xmlns:android'] = 'http://schemas.android.com/apk/res/android';
|
|
25
|
+
}
|
|
26
|
+
// precise attribute order
|
|
27
|
+
const getAttrOrder = (key) => {
|
|
28
|
+
if (key === 'xmlns:android')
|
|
29
|
+
return 0;
|
|
30
|
+
if (key === 'android:id')
|
|
31
|
+
return 1;
|
|
32
|
+
if (key === 'android:layout_width')
|
|
33
|
+
return 2;
|
|
34
|
+
if (key === 'android:layout_height')
|
|
35
|
+
return 3;
|
|
36
|
+
if (key === 'android:orientation')
|
|
37
|
+
return 4;
|
|
38
|
+
return 10;
|
|
39
|
+
};
|
|
40
|
+
// Build attribute string with sorting
|
|
41
|
+
const attrString = Object.entries(attributes)
|
|
42
|
+
.sort((a, b) => {
|
|
43
|
+
const orderA = getAttrOrder(a[0]);
|
|
44
|
+
const orderB = getAttrOrder(b[0]);
|
|
45
|
+
if (orderA !== orderB)
|
|
46
|
+
return orderA - orderB;
|
|
47
|
+
return a[0].localeCompare(b[0]);
|
|
48
|
+
})
|
|
49
|
+
.map(([key, value]) => `${key}="${(0, helpers_1.escapeXml)(value)}"`)
|
|
50
|
+
.join('\n' + indent + ' ');
|
|
51
|
+
// Decide whether to self-close
|
|
52
|
+
const hasChildren = node.children.length > 0;
|
|
53
|
+
// textContent might still be present for some elements if we didn't move it to attribute
|
|
54
|
+
const hasText = !!node.textContent;
|
|
55
|
+
// Opening tag
|
|
56
|
+
let xml = `${indent}<${node.tag}`;
|
|
57
|
+
if (attrString) {
|
|
58
|
+
xml += `\n${indent} ${attrString}`;
|
|
59
|
+
}
|
|
60
|
+
if (!hasChildren && !hasText) {
|
|
61
|
+
xml += '/>';
|
|
62
|
+
return xml;
|
|
63
|
+
}
|
|
64
|
+
xml += '>';
|
|
65
|
+
// Text content
|
|
66
|
+
if (hasText) {
|
|
67
|
+
xml += `\n${childIndent}${(0, helpers_1.escapeXml)(node.textContent)}`;
|
|
68
|
+
}
|
|
69
|
+
// Children
|
|
70
|
+
if (hasChildren) {
|
|
71
|
+
xml += '\n';
|
|
72
|
+
for (const child of node.children) {
|
|
73
|
+
xml += generateElement(child, depth + 1) + '\n';
|
|
74
|
+
}
|
|
75
|
+
xml += indent;
|
|
76
|
+
}
|
|
77
|
+
else if (hasText) {
|
|
78
|
+
xml += '\n' + indent;
|
|
79
|
+
}
|
|
80
|
+
// Closing tag
|
|
81
|
+
xml += `</${node.tag}>`;
|
|
82
|
+
return xml;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Formats generated XML for better readability
|
|
86
|
+
*/
|
|
87
|
+
function formatGeneratedXml(xml) {
|
|
88
|
+
// The XML is already formatted by generateElement
|
|
89
|
+
return xml;
|
|
90
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts React Native code to Android XML layout
|
|
3
|
+
* @param code - React Native component code (JSX/TSX)
|
|
4
|
+
* @returns Android XML layout string
|
|
5
|
+
*/
|
|
6
|
+
export declare function convertRNToXML(code: string): string;
|
|
7
|
+
export type { XMLNode, ConvertedStyles, ComponentMapping } from './parser/astTypes';
|
|
8
|
+
export type { ParsedRNTree } from './parser/parseRN';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Main public API entry
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.convertRNToXML = convertRNToXML;
|
|
5
|
+
const parseRN_1 = require("./parser/parseRN");
|
|
6
|
+
const layoutBuilder_1 = require("./transformer/layoutBuilder");
|
|
7
|
+
const xmlGenerator_1 = require("./generators/xmlGenerator");
|
|
8
|
+
const helpers_1 = require("./utils/helpers");
|
|
9
|
+
/**
|
|
10
|
+
* Converts React Native code to Android XML layout
|
|
11
|
+
* @param code - React Native component code (JSX/TSX)
|
|
12
|
+
* @returns Android XML layout string
|
|
13
|
+
*/
|
|
14
|
+
function convertRNToXML(code) {
|
|
15
|
+
try {
|
|
16
|
+
// Reset ID counter for each conversion
|
|
17
|
+
(0, helpers_1.resetIdCounter)();
|
|
18
|
+
// Parse React Native code
|
|
19
|
+
const { rootJSXElement } = (0, parseRN_1.parseRN)(code);
|
|
20
|
+
// Build XML tree
|
|
21
|
+
const xmlTree = (0, layoutBuilder_1.buildXmlTree)(rootJSXElement);
|
|
22
|
+
// Generate XML string
|
|
23
|
+
const xml = (0, xmlGenerator_1.generateXml)(xmlTree);
|
|
24
|
+
return xml;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`Conversion failed: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an XML node in the Android layout tree
|
|
3
|
+
*/
|
|
4
|
+
export interface XMLNode {
|
|
5
|
+
/** Tag name (e.g., 'LinearLayout', 'TextView') */
|
|
6
|
+
tag: string;
|
|
7
|
+
/** XML attributes (e.g., { 'android:layout_width': 'match_parent' }) */
|
|
8
|
+
attributes: Record<string, string>;
|
|
9
|
+
/** Child nodes */
|
|
10
|
+
children: XMLNode[];
|
|
11
|
+
/** Text content (for TextView, etc.) */
|
|
12
|
+
textContent?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Represents converted React Native styles
|
|
16
|
+
*/
|
|
17
|
+
export interface ConvertedStyles {
|
|
18
|
+
/** Layout-related attributes */
|
|
19
|
+
layout: Record<string, string>;
|
|
20
|
+
/** View-specific attributes */
|
|
21
|
+
view: Record<string, string>;
|
|
22
|
+
/** Text-specific attributes */
|
|
23
|
+
text: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* React Native component mapping
|
|
27
|
+
*/
|
|
28
|
+
export interface ComponentMapping {
|
|
29
|
+
/** React Native component name */
|
|
30
|
+
rnComponent: string;
|
|
31
|
+
/** Android XML tag */
|
|
32
|
+
xmlTag: string;
|
|
33
|
+
/** Whether it can have children */
|
|
34
|
+
canHaveChildren: boolean;
|
|
35
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Uses Babel / TS AST to parse RN code
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.parseRN = parseRN;
|
|
41
|
+
const parser_1 = require("@babel/parser");
|
|
42
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
43
|
+
const t = __importStar(require("@babel/types"));
|
|
44
|
+
function parseRN(code) {
|
|
45
|
+
try {
|
|
46
|
+
// Parse code into AST
|
|
47
|
+
const ast = (0, parser_1.parse)(code, {
|
|
48
|
+
sourceType: "module",
|
|
49
|
+
plugins: ["jsx", "typescript"],
|
|
50
|
+
});
|
|
51
|
+
let rootJSXElement = null;
|
|
52
|
+
// Traverse AST to find first returned JSX element
|
|
53
|
+
(0, traverse_1.default)(ast, {
|
|
54
|
+
ReturnStatement(path) {
|
|
55
|
+
const arg = path.node.argument;
|
|
56
|
+
if (t.isJSXElement(arg)) {
|
|
57
|
+
rootJSXElement = arg;
|
|
58
|
+
path.stop();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
// If no JSX root found, throw explicit error
|
|
63
|
+
if (!rootJSXElement) {
|
|
64
|
+
throw new Error("No root JSX element found. Make sure your component returns JSX.");
|
|
65
|
+
}
|
|
66
|
+
return { ast, rootJSXElement };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Re-throw with clean message for CLI / caller
|
|
70
|
+
throw new Error(`RN Parser Error: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Builds final XML tree
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.buildXmlTree = buildXmlTree;
|
|
38
|
+
const t = __importStar(require("@babel/types"));
|
|
39
|
+
const rnNodeMapper_1 = require("./rnNodeMapper");
|
|
40
|
+
const styleMapper_1 = require("./styleMapper");
|
|
41
|
+
const helpers_1 = require("../utils/helpers");
|
|
42
|
+
/**
|
|
43
|
+
* Builds an XML tree from a JSX element
|
|
44
|
+
*/
|
|
45
|
+
function buildXmlTree(jsxElement) {
|
|
46
|
+
const elementName = getElementName(jsxElement.openingElement);
|
|
47
|
+
const xmlTag = (0, rnNodeMapper_1.mapComponentToXml)(elementName);
|
|
48
|
+
// Extract attributes and styles
|
|
49
|
+
const { attributes, styles } = extractAttributes(jsxElement.openingElement);
|
|
50
|
+
// Map styles to Android attributes
|
|
51
|
+
const convertedStyles = (0, styleMapper_1.mapStyles)(styles);
|
|
52
|
+
// Merge all attributes
|
|
53
|
+
const allAttributes = {
|
|
54
|
+
...attributes,
|
|
55
|
+
...convertedStyles.layout,
|
|
56
|
+
...convertedStyles.view,
|
|
57
|
+
...convertedStyles.text,
|
|
58
|
+
};
|
|
59
|
+
// Add default layout attributes if not present
|
|
60
|
+
if (!allAttributes['android:layout_width']) {
|
|
61
|
+
allAttributes['android:layout_width'] = 'wrap_content';
|
|
62
|
+
}
|
|
63
|
+
if (!allAttributes['android:layout_height']) {
|
|
64
|
+
allAttributes['android:layout_height'] = 'wrap_content';
|
|
65
|
+
}
|
|
66
|
+
// Add orientation for layouts
|
|
67
|
+
if (xmlTag === 'LinearLayout' && !allAttributes['android:orientation']) {
|
|
68
|
+
const orientation = (0, rnNodeMapper_1.getOrientation)(styles?.flexDirection);
|
|
69
|
+
allAttributes['android:orientation'] = orientation;
|
|
70
|
+
}
|
|
71
|
+
// Generate ID
|
|
72
|
+
allAttributes['android:id'] = (0, helpers_1.generateId)(elementName.toLowerCase());
|
|
73
|
+
// Build children and extract text content
|
|
74
|
+
const children = [];
|
|
75
|
+
let textContent;
|
|
76
|
+
// Always process children to extract text content
|
|
77
|
+
for (const child of jsxElement.children) {
|
|
78
|
+
if (t.isJSXElement(child)) {
|
|
79
|
+
// Only add as child if the component can have children
|
|
80
|
+
if ((0, rnNodeMapper_1.canHaveChildren)(elementName)) {
|
|
81
|
+
children.push(buildXmlTree(child));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (t.isJSXText(child)) {
|
|
85
|
+
const text = child.value.trim();
|
|
86
|
+
if (text) {
|
|
87
|
+
textContent = text;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (t.isJSXExpressionContainer(child)) {
|
|
91
|
+
// Handle expressions like {variable}
|
|
92
|
+
if (t.isStringLiteral(child.expression)) {
|
|
93
|
+
textContent = child.expression.value;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// For other expressions, use placeholder
|
|
97
|
+
textContent = '{expression}';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Move text content to attribute for TextView and Button
|
|
102
|
+
if (textContent && (xmlTag === 'TextView' || xmlTag === 'Button')) {
|
|
103
|
+
allAttributes['android:text'] = textContent;
|
|
104
|
+
textContent = undefined;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
tag: xmlTag,
|
|
108
|
+
attributes: allAttributes,
|
|
109
|
+
children,
|
|
110
|
+
textContent,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Gets the element name from JSX opening element
|
|
115
|
+
*/
|
|
116
|
+
function getElementName(openingElement) {
|
|
117
|
+
const name = openingElement.name;
|
|
118
|
+
if (t.isJSXIdentifier(name)) {
|
|
119
|
+
return name.name;
|
|
120
|
+
}
|
|
121
|
+
return 'View';
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Extracts attributes and styles from JSX opening element
|
|
125
|
+
*/
|
|
126
|
+
function extractAttributes(openingElement) {
|
|
127
|
+
const attributes = {};
|
|
128
|
+
let styles = {};
|
|
129
|
+
for (const attr of openingElement.attributes) {
|
|
130
|
+
if (t.isJSXAttribute(attr)) {
|
|
131
|
+
const attrName = t.isJSXIdentifier(attr.name) ? attr.name.name : '';
|
|
132
|
+
if (attrName === 'style') {
|
|
133
|
+
styles = extractStyleValue(attr.value);
|
|
134
|
+
}
|
|
135
|
+
else if (attrName === 'testID') {
|
|
136
|
+
// Map testID to contentDescription
|
|
137
|
+
const value = extractAttributeValue(attr.value);
|
|
138
|
+
if (value) {
|
|
139
|
+
attributes['android:contentDescription'] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Other attributes - map directly
|
|
144
|
+
const value = extractAttributeValue(attr.value);
|
|
145
|
+
if (value) {
|
|
146
|
+
attributes[attrName] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { attributes, styles };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Extracts the value from JSX attribute value
|
|
155
|
+
*/
|
|
156
|
+
function extractAttributeValue(value) {
|
|
157
|
+
if (t.isStringLiteral(value)) {
|
|
158
|
+
return value.value;
|
|
159
|
+
}
|
|
160
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
161
|
+
if (t.isStringLiteral(value.expression)) {
|
|
162
|
+
return value.expression.value;
|
|
163
|
+
}
|
|
164
|
+
if (t.isNumericLiteral(value.expression)) {
|
|
165
|
+
return value.expression.value.toString();
|
|
166
|
+
}
|
|
167
|
+
if (t.isBooleanLiteral(value.expression)) {
|
|
168
|
+
return value.expression.value.toString();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extracts style object from JSX attribute value
|
|
175
|
+
*/
|
|
176
|
+
function extractStyleValue(value) {
|
|
177
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
178
|
+
if (t.isObjectExpression(value.expression)) {
|
|
179
|
+
// Parse inline style object
|
|
180
|
+
const styleObj = {};
|
|
181
|
+
for (const prop of value.expression.properties) {
|
|
182
|
+
if (t.isObjectProperty(prop)) {
|
|
183
|
+
const key = t.isIdentifier(prop.key) ? prop.key.name : '';
|
|
184
|
+
let val;
|
|
185
|
+
if (t.isNumericLiteral(prop.value)) {
|
|
186
|
+
val = prop.value.value;
|
|
187
|
+
}
|
|
188
|
+
else if (t.isStringLiteral(prop.value)) {
|
|
189
|
+
val = prop.value.value;
|
|
190
|
+
}
|
|
191
|
+
if (key && val !== undefined) {
|
|
192
|
+
styleObj[key] = val;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return styleObj;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a React Native component name to an Android XML tag
|
|
3
|
+
*/
|
|
4
|
+
export declare function mapComponentToXml(componentName: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a component can have children
|
|
7
|
+
*/
|
|
8
|
+
export declare function canHaveChildren(componentName: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Determines the orientation for a layout based on flexDirection
|
|
11
|
+
*/
|
|
12
|
+
export declare function getOrientation(flexDirection?: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Maps RN components → XML nodes
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.mapComponentToXml = mapComponentToXml;
|
|
5
|
+
exports.canHaveChildren = canHaveChildren;
|
|
6
|
+
exports.getOrientation = getOrientation;
|
|
7
|
+
/**
|
|
8
|
+
* Component mapping table
|
|
9
|
+
*/
|
|
10
|
+
const COMPONENT_MAP = [
|
|
11
|
+
{ rnComponent: 'View', xmlTag: 'LinearLayout', canHaveChildren: true },
|
|
12
|
+
{ rnComponent: 'Text', xmlTag: 'TextView', canHaveChildren: false },
|
|
13
|
+
{ rnComponent: 'Image', xmlTag: 'ImageView', canHaveChildren: false },
|
|
14
|
+
{ rnComponent: 'ScrollView', xmlTag: 'ScrollView', canHaveChildren: true },
|
|
15
|
+
{ rnComponent: 'FlatList', xmlTag: 'RecyclerView', canHaveChildren: false },
|
|
16
|
+
{ rnComponent: 'TextInput', xmlTag: 'EditText', canHaveChildren: false },
|
|
17
|
+
{ rnComponent: 'Button', xmlTag: 'Button', canHaveChildren: false },
|
|
18
|
+
{ rnComponent: 'TouchableOpacity', xmlTag: 'FrameLayout', canHaveChildren: true },
|
|
19
|
+
{ rnComponent: 'TouchableHighlight', xmlTag: 'FrameLayout', canHaveChildren: true },
|
|
20
|
+
{ rnComponent: 'Pressable', xmlTag: 'FrameLayout', canHaveChildren: true },
|
|
21
|
+
{ rnComponent: 'SafeAreaView', xmlTag: 'LinearLayout', canHaveChildren: true },
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Maps a React Native component name to an Android XML tag
|
|
25
|
+
*/
|
|
26
|
+
function mapComponentToXml(componentName) {
|
|
27
|
+
const mapping = COMPONENT_MAP.find(m => m.rnComponent === componentName);
|
|
28
|
+
return mapping?.xmlTag || 'View';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a component can have children
|
|
32
|
+
*/
|
|
33
|
+
function canHaveChildren(componentName) {
|
|
34
|
+
const mapping = COMPONENT_MAP.find(m => m.rnComponent === componentName);
|
|
35
|
+
return mapping?.canHaveChildren ?? true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Determines the orientation for a layout based on flexDirection
|
|
39
|
+
*/
|
|
40
|
+
function getOrientation(flexDirection) {
|
|
41
|
+
if (!flexDirection || flexDirection === 'column') {
|
|
42
|
+
return 'vertical';
|
|
43
|
+
}
|
|
44
|
+
if (flexDirection === 'row') {
|
|
45
|
+
return 'horizontal';
|
|
46
|
+
}
|
|
47
|
+
return 'vertical';
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ConvertedStyles } from '../parser/astTypes';
|
|
2
|
+
/**
|
|
3
|
+
* Maps React Native style properties to Android XML attributes
|
|
4
|
+
*/
|
|
5
|
+
export declare function mapStyles(style: any): ConvertedStyles;
|
|
6
|
+
/**
|
|
7
|
+
* Extracts inline styles from JSX element
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractInlineStyles(styleValue: any): any;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Converts RN styles → Android attrs
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.mapStyles = mapStyles;
|
|
5
|
+
exports.extractInlineStyles = extractInlineStyles;
|
|
6
|
+
const helpers_1 = require("../utils/helpers");
|
|
7
|
+
/**
|
|
8
|
+
* Maps React Native style properties to Android XML attributes
|
|
9
|
+
*/
|
|
10
|
+
function mapStyles(style) {
|
|
11
|
+
const result = {
|
|
12
|
+
layout: {},
|
|
13
|
+
view: {},
|
|
14
|
+
text: {},
|
|
15
|
+
};
|
|
16
|
+
if (!style || typeof style !== 'object') {
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
// Layout properties
|
|
20
|
+
if (style.padding !== undefined) {
|
|
21
|
+
result.layout['android:padding'] = (0, helpers_1.toDp)(style.padding);
|
|
22
|
+
}
|
|
23
|
+
if (style.paddingTop !== undefined) {
|
|
24
|
+
result.layout['android:paddingTop'] = (0, helpers_1.toDp)(style.paddingTop);
|
|
25
|
+
}
|
|
26
|
+
if (style.paddingBottom !== undefined) {
|
|
27
|
+
result.layout['android:paddingBottom'] = (0, helpers_1.toDp)(style.paddingBottom);
|
|
28
|
+
}
|
|
29
|
+
if (style.paddingLeft !== undefined) {
|
|
30
|
+
result.layout['android:paddingLeft'] = (0, helpers_1.toDp)(style.paddingLeft);
|
|
31
|
+
}
|
|
32
|
+
if (style.paddingRight !== undefined) {
|
|
33
|
+
result.layout['android:paddingRight'] = (0, helpers_1.toDp)(style.paddingRight);
|
|
34
|
+
}
|
|
35
|
+
if (style.paddingHorizontal !== undefined) {
|
|
36
|
+
result.layout['android:paddingLeft'] = (0, helpers_1.toDp)(style.paddingHorizontal);
|
|
37
|
+
result.layout['android:paddingRight'] = (0, helpers_1.toDp)(style.paddingHorizontal);
|
|
38
|
+
}
|
|
39
|
+
if (style.paddingVertical !== undefined) {
|
|
40
|
+
result.layout['android:paddingTop'] = (0, helpers_1.toDp)(style.paddingVertical);
|
|
41
|
+
result.layout['android:paddingBottom'] = (0, helpers_1.toDp)(style.paddingVertical);
|
|
42
|
+
}
|
|
43
|
+
if (style.margin !== undefined) {
|
|
44
|
+
result.layout['android:layout_margin'] = (0, helpers_1.toDp)(style.margin);
|
|
45
|
+
}
|
|
46
|
+
if (style.marginTop !== undefined) {
|
|
47
|
+
result.layout['android:layout_marginTop'] = (0, helpers_1.toDp)(style.marginTop);
|
|
48
|
+
}
|
|
49
|
+
if (style.marginBottom !== undefined) {
|
|
50
|
+
result.layout['android:layout_marginBottom'] = (0, helpers_1.toDp)(style.marginBottom);
|
|
51
|
+
}
|
|
52
|
+
if (style.marginLeft !== undefined) {
|
|
53
|
+
result.layout['android:layout_marginLeft'] = (0, helpers_1.toDp)(style.marginLeft);
|
|
54
|
+
}
|
|
55
|
+
if (style.marginRight !== undefined) {
|
|
56
|
+
result.layout['android:layout_marginRight'] = (0, helpers_1.toDp)(style.marginRight);
|
|
57
|
+
}
|
|
58
|
+
if (style.marginHorizontal !== undefined) {
|
|
59
|
+
result.layout['android:layout_marginLeft'] = (0, helpers_1.toDp)(style.marginHorizontal);
|
|
60
|
+
result.layout['android:layout_marginRight'] = (0, helpers_1.toDp)(style.marginHorizontal);
|
|
61
|
+
}
|
|
62
|
+
if (style.marginVertical !== undefined) {
|
|
63
|
+
result.layout['android:layout_marginTop'] = (0, helpers_1.toDp)(style.marginVertical);
|
|
64
|
+
result.layout['android:layout_marginBottom'] = (0, helpers_1.toDp)(style.marginVertical);
|
|
65
|
+
}
|
|
66
|
+
// Width and Height
|
|
67
|
+
if (style.width !== undefined) {
|
|
68
|
+
if (style.width === '100%') {
|
|
69
|
+
result.layout['android:layout_width'] = 'match_parent';
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
result.layout['android:layout_width'] = (0, helpers_1.toDp)(style.width);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (style.height !== undefined) {
|
|
76
|
+
if (style.height === '100%') {
|
|
77
|
+
result.layout['android:layout_height'] = 'match_parent';
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result.layout['android:layout_height'] = (0, helpers_1.toDp)(style.height);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// View properties
|
|
84
|
+
if (style.backgroundColor !== undefined) {
|
|
85
|
+
result.view['android:background'] = (0, helpers_1.convertColor)(style.backgroundColor);
|
|
86
|
+
}
|
|
87
|
+
// Text properties
|
|
88
|
+
if (style.fontSize !== undefined) {
|
|
89
|
+
result.text['android:textSize'] = (0, helpers_1.toSp)(style.fontSize);
|
|
90
|
+
}
|
|
91
|
+
if (style.color !== undefined) {
|
|
92
|
+
result.text['android:textColor'] = (0, helpers_1.convertColor)(style.color);
|
|
93
|
+
}
|
|
94
|
+
if (style.fontWeight !== undefined) {
|
|
95
|
+
if (style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 600) {
|
|
96
|
+
result.text['android:textStyle'] = 'bold';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (style.textAlign !== undefined) {
|
|
100
|
+
const alignment = {
|
|
101
|
+
'left': 'left',
|
|
102
|
+
'center': 'center',
|
|
103
|
+
'right': 'right',
|
|
104
|
+
'justify': 'justify',
|
|
105
|
+
};
|
|
106
|
+
result.text['android:textAlignment'] = alignment[style.textAlign] || 'left';
|
|
107
|
+
}
|
|
108
|
+
// Flex properties (basic support)
|
|
109
|
+
if (style.flexDirection !== undefined) {
|
|
110
|
+
// This will be handled at the layout level in layoutBuilder
|
|
111
|
+
}
|
|
112
|
+
if (style.alignItems !== undefined) {
|
|
113
|
+
const gravity = {
|
|
114
|
+
'flex-start': 'start',
|
|
115
|
+
'flex-end': 'end',
|
|
116
|
+
'center': 'center',
|
|
117
|
+
'stretch': 'fill',
|
|
118
|
+
};
|
|
119
|
+
result.view['android:gravity'] = gravity[style.alignItems] || 'start';
|
|
120
|
+
}
|
|
121
|
+
if (style.justifyContent !== undefined) {
|
|
122
|
+
const gravity = {
|
|
123
|
+
'flex-start': 'start',
|
|
124
|
+
'flex-end': 'end',
|
|
125
|
+
'center': 'center',
|
|
126
|
+
'space-between': 'space_between',
|
|
127
|
+
'space-around': 'space_around',
|
|
128
|
+
};
|
|
129
|
+
const currentGravity = result.view['android:gravity'] || '';
|
|
130
|
+
const justifyGravity = gravity[style.justifyContent] || 'start';
|
|
131
|
+
result.view['android:gravity'] = currentGravity
|
|
132
|
+
? `${currentGravity}|${justifyGravity}`
|
|
133
|
+
: justifyGravity;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extracts inline styles from JSX element
|
|
139
|
+
*/
|
|
140
|
+
function extractInlineStyles(styleValue) {
|
|
141
|
+
if (!styleValue) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
// Handle object expression (inline styles)
|
|
145
|
+
if (typeof styleValue === 'object') {
|
|
146
|
+
return styleValue;
|
|
147
|
+
}
|
|
148
|
+
// For now, we'll just return empty object for other cases
|
|
149
|
+
// In a full implementation, you'd resolve StyleSheet.create references
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts camelCase to snake_case
|
|
3
|
+
*/
|
|
4
|
+
export declare function camelToSnakeCase(str: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Converts a numeric value to Android dp units
|
|
7
|
+
*/
|
|
8
|
+
export declare function toDp(value: number | string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Converts a numeric value to Android sp units (for text)
|
|
11
|
+
*/
|
|
12
|
+
export declare function toSp(value: number | string): string;
|
|
13
|
+
export declare function generateId(prefix?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Resets the ID counter (useful for testing)
|
|
16
|
+
*/
|
|
17
|
+
export declare function resetIdCounter(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Converts hex color to Android color format
|
|
20
|
+
*/
|
|
21
|
+
export declare function convertColor(color: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Escapes special XML characters in text content
|
|
24
|
+
*/
|
|
25
|
+
export declare function escapeXml(text: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Formats XML with proper indentation
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatXml(xml: string, indentLevel?: number): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Utility helper functions
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.camelToSnakeCase = camelToSnakeCase;
|
|
5
|
+
exports.toDp = toDp;
|
|
6
|
+
exports.toSp = toSp;
|
|
7
|
+
exports.generateId = generateId;
|
|
8
|
+
exports.resetIdCounter = resetIdCounter;
|
|
9
|
+
exports.convertColor = convertColor;
|
|
10
|
+
exports.escapeXml = escapeXml;
|
|
11
|
+
exports.formatXml = formatXml;
|
|
12
|
+
/**
|
|
13
|
+
* Converts camelCase to snake_case
|
|
14
|
+
*/
|
|
15
|
+
function camelToSnakeCase(str) {
|
|
16
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Converts a numeric value to Android dp units
|
|
20
|
+
*/
|
|
21
|
+
function toDp(value) {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
return value; // Already a string, assume it's formatted
|
|
24
|
+
}
|
|
25
|
+
return `${value}dp`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Converts a numeric value to Android sp units (for text)
|
|
29
|
+
*/
|
|
30
|
+
function toSp(value) {
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return `${value}sp`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generates a unique Android view ID
|
|
38
|
+
*/
|
|
39
|
+
let idCounter = 0;
|
|
40
|
+
function generateId(prefix = 'view') {
|
|
41
|
+
return `@+id/${prefix}_${++idCounter}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resets the ID counter (useful for testing)
|
|
45
|
+
*/
|
|
46
|
+
function resetIdCounter() {
|
|
47
|
+
idCounter = 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Converts hex color to Android color format
|
|
51
|
+
*/
|
|
52
|
+
function convertColor(color) {
|
|
53
|
+
// If it's already a hex color, ensure it has # prefix
|
|
54
|
+
if (color.startsWith('#')) {
|
|
55
|
+
return color;
|
|
56
|
+
}
|
|
57
|
+
// Handle named colors (basic support)
|
|
58
|
+
const namedColors = {
|
|
59
|
+
'black': '#000000',
|
|
60
|
+
'white': '#FFFFFF',
|
|
61
|
+
'red': '#FF0000',
|
|
62
|
+
'green': '#00FF00',
|
|
63
|
+
'blue': '#0000FF',
|
|
64
|
+
'transparent': '#00000000',
|
|
65
|
+
};
|
|
66
|
+
return namedColors[color.toLowerCase()] || color;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Escapes special XML characters in text content
|
|
70
|
+
*/
|
|
71
|
+
function escapeXml(text) {
|
|
72
|
+
return text
|
|
73
|
+
.replace(/&/g, '&')
|
|
74
|
+
.replace(/</g, '<')
|
|
75
|
+
.replace(/>/g, '>')
|
|
76
|
+
.replace(/"/g, '"')
|
|
77
|
+
.replace(/'/g, ''');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Formats XML with proper indentation
|
|
81
|
+
*/
|
|
82
|
+
function formatXml(xml, indentLevel = 0) {
|
|
83
|
+
const indent = ' '.repeat(indentLevel);
|
|
84
|
+
return xml
|
|
85
|
+
.split('\n')
|
|
86
|
+
.map(line => line.trim() ? indent + line : '')
|
|
87
|
+
.join('\n');
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-to-xml-converter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A converter from react native to xml code",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-native",
|
|
7
|
+
"xml",
|
|
8
|
+
"converter",
|
|
9
|
+
"android",
|
|
10
|
+
"development",
|
|
11
|
+
"hybrid"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/Adhidevudayakumar/react-native-to-xml-converter#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Adhidevudayakumar/react-native-to-xml-converter/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Adhidevudayakumar/react-native-to-xml-converter.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"author": "Adhidev Udayakumar",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"bin": {
|
|
25
|
+
"rn-to-xml": "dist/cli/index.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"dev": "ts-node src/cli/index.ts",
|
|
30
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/babel__traverse": "^7.28.0",
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@babel/parser": "^7.28.6",
|
|
42
|
+
"@babel/traverse": "^7.28.6",
|
|
43
|
+
"@babel/types": "^7.28.6",
|
|
44
|
+
"fs": "^0.0.1-security",
|
|
45
|
+
"open": "^11.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
Binary file
|