import type { RenderHTMLProps } from 'react-native-render-html';
import { RendererCardConfig, ImportStmt } from './toolkit-types';
import defaultImports from './defaultImports';
function normalizeKey(key: string) {
return key.match(/-/) ? `'${key}'` : key;
}
function serializePrimitive(value: boolean | bigint | number | string) {
let ret = '';
switch (typeof value) {
case 'number':
case 'bigint':
case 'boolean':
ret = String(value);
break;
case 'string':
if (value.match('\n')) {
ret = `\`${value.replace('`', '\\`')}\``;
} else {
ret = value.match(/'/) ? `"${value}"` : `'${value}'`;
}
break;
}
return ret;
}
function serializeValue(
key: keyof RenderHTMLProps,
value: RenderHTMLProps[keyof RenderHTMLProps],
config: Required,
indent: number = 1
) {
const { exprSrcMap, fnSrcMap } = config;
let ret = '';
const pad = ' '.repeat(indent);
if (key in exprSrcMap) {
return exprSrcMap[key];
}
switch (typeof value) {
case 'function':
if (value.name in fnSrcMap) {
ret = value.name;
} else {
throw new Error(
`Attempted to serialize a function "${value.name}" but no source mapping was found.`
);
}
break;
case 'object':
let output = '';
output = Object.entries(value)
.map(([key, val]) => {
return `${pad}${normalizeKey(key)}: ${serializeValue(
key as any,
val,
config,
indent + 1
)}`;
})
.join(',\n');
ret = `{\n${output}\n${' '.repeat(indent - 1)}}`;
break;
default:
ret = serializePrimitive(value);
}
return ret;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function isPrimitiveProp([_key, value]: [key: string, value: any]) {
return (
typeof value === 'boolean' ||
(typeof value === 'string' && !value.match('\n')) ||
typeof value === 'bigint' ||
typeof value === 'number'
);
}
function declareProps(
props: [key: string, value: any][],
config: Required
) {
const { fnSrcMap } = config;
let output = '';
for (const key in fnSrcMap) {
output += `${fnSrcMap[key]}\n\n`;
}
for (const [key, value] of props) {
if (!(key in fnSrcMap)) {
//@ts-ignore
output += `const ${key} = ${serializeValue(key, value, config)};\n\n`;
}
}
return output;
}
function inlineProps(
declaredProps: [key: string, value: any][],
primitiveProps: [key: string, value: boolean | number | bigint | string][]
) {
let ret: string[] = [];
for (const entry of declaredProps) {
ret.push(`${entry[0]}={${entry[0]}}`);
}
for (const entry of primitiveProps) {
const isString = typeof entry[1] === 'string';
const rightHand = isString
? serializePrimitive(entry[1])
: `{${serializePrimitive(entry[1])}}`;
ret.push(`${entry[0]}=${rightHand}`);
}
return ret;
}
function mergeStatements(
statement1: ImportStmt,
statement2: ImportStmt
): ImportStmt {
return {
package: statement1.package,
default: statement1.default || statement2.default,
named: [...(statement1.named || []), ...(statement2.named || [])]
};
}
function serializeStatement({
package: pkg,
default: dft,
named
}: ImportStmt): string {
const hasNamedImports = !!named?.length;
return `import ${dft || ''}${dft && hasNamedImports ? ', ' : ''}${
hasNamedImports ? `{ ${named!.join(', ')} }` : ''
} from '${pkg}';`;
}
function flattenStatements(importStmts: ImportStmt[]) {
const mergeReg: Record = {
react: false,
'react-native': false,
'react-native-render-html': false
};
const merged = importStmts.map((stmt) => {
if (stmt.package in defaultImports) {
mergeReg[stmt.package] = true;
return mergeStatements(defaultImports[stmt.package], stmt);
}
return stmt;
});
const unmergedBaseStmts = Object.entries(mergeReg)
.map(([key, isMerged]) => {
if (isMerged) {
return null;
}
return defaultImports[key];
})
.filter((s) => s !== null);
return [...unmergedBaseStmts, ...merged] as ImportStmt[];
}
interface JSXElementExp {
name: string;
inlineProps?: string[];
children: JSXElementExp[];
}
function createJSXStmt(expr: JSXElementExp, indent = 4): string {
const padd = ' '.repeat(indent);
const inlinePropsExpr = expr.inlineProps?.length
? expr.inlineProps.map((p) => `\n${' '.repeat(indent + 2)}${p}`).join('')
: '';
if (expr.children.length) {
return `${padd}<${expr.name}${inlinePropsExpr}>\n${
expr.children.length
? expr.children.map((e) => createJSXStmt(e, indent + 2))
: ''
}\n${padd}${expr.name}>`;
}
return `${padd}<${expr.name}${inlinePropsExpr}\n${padd}/>`;
}
export default function makeSnippet(
props: RenderHTMLProps,
config: Required,
includeSafeAreaView: boolean
) {
const propsEntries = Object.entries(props);
const primitiveProps = propsEntries.filter(isPrimitiveProp);
const declaredProps = propsEntries.filter((en) => !isPrimitiveProp(en));
const importStmts = includeSafeAreaView
? [
...config.importStatements,
{
package: 'react-native-safe-area-context',
named: ['SafeAreaView']
}
]
: config.importStatements;
const innerExpr: JSXElementExp = {
name: 'RenderHtml',
children: [],
inlineProps: [
'contentWidth={width}',
...inlineProps(declaredProps, primitiveProps)
]
};
const wrapperExpr: JSXElementExp = config.wrapperComponent
? {
name: config.wrapperComponent,
children: [innerExpr]
}
: innerExpr;
const safeExpr: JSXElementExp = includeSafeAreaView
? {
name: 'SafeAreaView',
children: [wrapperExpr]
}
: wrapperExpr;
const returnStmt = createJSXStmt(safeExpr);
return `${flattenStatements(importStmts).map(serializeStatement).join('\n')}
${declareProps(declaredProps, config)}\
export default function App() {
const { width } = useWindowDimensions();
return (\n${returnStmt}\n );
}`;
}