hanhwa_nexacro/tools/nexacro-gen/index.js

399 lines
13 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const repoRoot = path.resolve(__dirname, "../..");
const specDir = path.join(repoRoot, "spec", "nexacro");
const templateDir = path.join(repoRoot, "templates", "nexacro");
const outputDir = path.join(repoRoot, "client", "nexacro-src");
const previewDir = path.join(repoRoot, "client", "nexacro-deploy");
const UTF8_BOM = "\uFEFF";
const FRAME_TOP_HEIGHT = 56;
const FRAME_LEFT_WIDTH = 240;
const BASE_DIR = "Base";
const FRAMEBASE_DIR = "FrameBase";
function readYaml(filePath) {
return yaml.load(fs.readFileSync(filePath, "utf8"));
}
function loadSpecs(baseDir = specDir) {
const appSpec = readYaml(path.join(baseDir, "app.yaml"));
const formFiles = fs
.readdirSync(baseDir)
.filter((file) => file.endsWith(".yaml") && file !== "app.yaml")
.sort();
const forms = formFiles.map((file) => readYaml(path.join(baseDir, file)));
return { appSpec, forms };
}
function readTemplate(relativePath) {
return fs.readFileSync(path.join(templateDir, relativePath), "utf8");
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function removePathIfExists(targetPath) {
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { recursive: true, force: true });
}
}
function writeGeneratedFile(filePath, content, options = {}) {
const includeBom = options.bom !== false;
const payload = includeBom ? `${UTF8_BOM}${content}` : content;
fs.writeFileSync(filePath, payload);
}
function cleanGeneratedOutput(baseOutputDir, projectName) {
[
`${projectName}.xprj`,
"application.xadl",
"Application_Desktop.xadl",
"environment.xml",
"typedefinition.xml",
"appvariables.xml",
`${projectName}.xprj.bak`,
"$Geninfo$.geninfo",
".DS_Store"
].forEach((file) => removePathIfExists(path.join(baseOutputDir, file)));
["forms", BASE_DIR, "frame", FRAMEBASE_DIR, "lib"].forEach((dir) =>
removePathIfExists(path.join(baseOutputDir, dir))
);
}
function renderTemplate(template, values) {
return Object.entries(values).reduce((content, [key, value]) => {
return content.replaceAll(`{{${key}}}`, value);
}, template);
}
function escapeXml(value = "") {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function indentBlock(content, size = 4) {
const indent = " ".repeat(size);
return content
.split("\n")
.map((line) => (line ? `${indent}${line}` : line))
.join("\n");
}
function toXfdlDataset(dataset) {
const columns = (dataset.columns || [])
.map((column) => ` <Column id="${column.id}" type="${column.type || "STRING"}" size="256"/>`)
.join("\n");
return [
` <Dataset id="${dataset.id}">`,
" <ColumnInfo>",
columns,
" </ColumnInfo>",
" </Dataset>"
].join("\n");
}
function componentTag(component, index = 0) {
const componentType = component.type === "FileUpload" ? "Edit" : component.type;
const attrs = [
`id="${component.id}"`,
`taborder="${index}"`,
`left="${component.left ?? 0}"`,
`top="${component.top ?? 0}"`,
`width="${component.width ?? 120}"`,
`height="${component.height ?? 32}"`
];
if (component.text) {
attrs.push(`text="${escapeXml(component.text)}"`);
}
if (component.prompt && ["Edit", "MaskEdit", "TextArea"].includes(componentType)) {
attrs.push(`displaynulltext="${escapeXml(component.prompt)}"`);
}
if (componentType === "Edit" && component.id.toLowerCase().includes("password")) {
attrs.push('password="true"');
}
if (componentType === "Combo") {
attrs.push('codecolumn="code"');
attrs.push('datacolumn="label"');
}
if (component.type === "FileUpload") {
attrs.push('readonly="true"');
}
return ` <${componentType} ${attrs.join(" ")}/>`;
}
function gridFormats(grid) {
const columns = grid.columns || [];
const width = Math.max(120, Math.floor((grid.width || 800) / Math.max(columns.length, 1)));
const columnXml = columns.map(() => `<Column size="${width}"/>`).join("");
const headerCells = columns
.map((column, index) => `<Cell col="${index}" text="${escapeXml(column.text || column.id)}"/>`)
.join("");
const bodyCells = columns
.map((column, index) => `<Cell col="${index}" text="bind:${column.id}"/>`)
.join("");
return `<Formats><Format id="default"><Columns>${columnXml}</Columns><Rows><Row size="32" band="head"/><Row size="28"/></Rows><Band id="head">${headerCells}</Band><Band id="body">${bodyCells}</Band></Format></Formats>`;
}
function gridTag(grid) {
return [
` <Static id="sta_${grid.id}" taborder="0" left="${grid.left}" top="${grid.top - 28}" width="${grid.width}" height="24" text="${escapeXml(grid.title)}"/>`,
` <Grid id="${grid.id}" taborder="1" left="${grid.left}" top="${grid.top}" width="${grid.width}" height="${grid.height}" binddataset="${grid.dataset}">`,
` ${gridFormats(grid)}`,
" </Grid>"
].join("\n");
}
function buildFormScript(form) {
const transactions = (form.transactions || [])
.map(
(transaction) =>
`this.${transaction.id} = function(obj, e)\n{\n this.gfnShowMessage("${escapeXml(transaction.id)} -> ${escapeXml(transaction.endpoint)}");\n};`
)
.join("\n\n");
const actions = (form.actions || [])
.map(
(action) =>
`this.${action.id} = function(obj, e)\n{\n this.gfnShowMessage("${escapeXml(action.label)}");\n};`
)
.join("\n\n");
return [
"this.Form_onload = function(obj, e)",
"{",
` this.gfnShowMessage("${escapeXml(form.title)} loaded");`,
"};",
"",
transactions,
"",
actions
]
.join("\n")
.trim();
}
function renderMessageStatics(form, layout) {
return (form.messages || [])
.map(
(message, index) =>
` <Static id="staMessage${index}" taborder="${900 + index}" left="36" top="${Math.max(layout.height - 76 + index * 22, 36)}" width="${Math.max(layout.width - 72, 300)}" height="20" text="${escapeXml(message.text)}"/>`
)
.join("\n");
}
function renderXfdl(form, appSpec) {
const datasets = (form.datasets || []).map(toXfdlDataset).join("\n");
const components = (form.components || []).map((component, index) => componentTag(component, index)).join("\n");
const grids = (form.grids || []).map(gridTag).join("\n");
const layout = form.layout || appSpec.layout;
const messages = renderMessageStatics(form, layout);
return [
'<?xml version="1.0" encoding="utf-8"?>',
'<FDL version="2.1">',
` <Form id="${form.formId}" titletext="${escapeXml(form.title)}" width="${layout.width}" height="${layout.height}" onload="Form_onload">`,
" <Objects>",
datasets || " <Dataset id=\"dsEmpty\"><ColumnInfo><Column id=\"dummy\" type=\"STRING\" size=\"1\"/></ColumnInfo></Dataset>",
" </Objects>",
" <Script type=\"xscript5.1\"><![CDATA[",
indentBlock(readTemplate(path.join("common", "common.xjs.tpl")).trimEnd(), 0),
"",
buildFormScript(form),
" ]]></Script>",
components,
grids,
messages,
" <Layouts>",
` <Layout width="${layout.width}" height="${layout.height}" screenid="Desktop_screen"/>`,
" </Layouts>",
" </Form>",
"</FDL>",
""
].join("\n");
}
function renderTopFrame(appSpec) {
return [
'<?xml version="1.0" encoding="utf-8"?>',
'<FDL version="2.1">',
` <Form id="Form_Top" width="${appSpec.layout.width}" height="${FRAME_TOP_HEIGHT}" titletext="Form_Top">`,
` <Static id="staTitle" taborder="0" left="24" top="14" width="420" height="28" text="${escapeXml(appSpec.appTitle)}"/>`,
' <Static id="staGuide" taborder="1" left="460" top="18" width="420" height="20" text="AI generated Nexacro source for Studio review"/>',
" <Layouts>",
` <Layout width="${appSpec.layout.width}" height="${FRAME_TOP_HEIGHT}" screenid="Desktop_screen"/>`,
" </Layouts>",
" </Form>",
"</FDL>",
""
].join("\n");
}
function renderLeftFrame(forms) {
const menuItems = forms
.map(
(form, index) =>
` <Static id="staMenu${index}" taborder="${index + 1}" left="20" top="${72 + index * 42}" width="180" height="24" text="${escapeXml(`${index + 1}. ${form.title}`)}"/>`
)
.join("\n");
return [
'<?xml version="1.0" encoding="utf-8"?>',
'<FDL version="2.1">',
` <Form id="Form_Left" width="${FRAME_LEFT_WIDTH}" height="840" titletext="Form_Left">`,
' <Static id="staMenuTitle" taborder="0" left="20" top="24" width="180" height="28" text="Demo Forms"/>',
menuItems,
" <Layouts>",
' <Layout width="240" height="840" screenid="Desktop_screen"/>',
" </Layouts>",
" </Form>",
"</FDL>",
""
].join("\n");
}
function renderWorkFrame(appSpec, defaultFormId) {
const workWidth = Math.max(appSpec.layout.width - FRAME_LEFT_WIDTH, 800);
const workHeight = Math.max(appSpec.layout.height - FRAME_TOP_HEIGHT, 640);
return [
'<?xml version="1.0" encoding="utf-8"?>',
'<FDL version="2.1">',
` <Form id="Form_Work" width="${workWidth}" height="${workHeight}" titletext="Form_Work">`,
` <Div id="divWork" taborder="0" left="0" top="0" width="${workWidth}" height="${workHeight}" url="Base::${defaultFormId}.xfdl"/>`,
" <Layouts>",
` <Layout width="${workWidth}" height="${workHeight}" screenid="Desktop_screen"/>`,
" </Layouts>",
" </Form>",
"</FDL>",
""
].join("\n");
}
function generateProjectFiles(appSpec, forms, baseOutputDir = outputDir) {
cleanGeneratedOutput(baseOutputDir, appSpec.projectName);
ensureDir(baseOutputDir);
ensureDir(path.join(baseOutputDir, BASE_DIR));
ensureDir(path.join(baseOutputDir, FRAMEBASE_DIR));
ensureDir(path.join(baseOutputDir, "_extlib_"));
writeGeneratedFile(
path.join(baseOutputDir, `${appSpec.projectName}.xprj`),
renderTemplate(readTemplate("project.xml.tpl"), {
projectName: appSpec.projectName
})
);
writeGeneratedFile(
path.join(baseOutputDir, "Application_Desktop.xadl"),
renderTemplate(readTemplate("application.xadl.tpl"), {
appTitle: appSpec.appTitle,
width: String(appSpec.layout.width),
height: String(appSpec.layout.height),
topHeight: String(FRAME_TOP_HEIGHT),
leftWidth: String(FRAME_LEFT_WIDTH)
})
);
writeGeneratedFile(
path.join(baseOutputDir, "environment.xml"),
renderTemplate(readTemplate("environment.xml.tpl"), {
themeId: appSpec.themeId
})
);
writeGeneratedFile(path.join(baseOutputDir, "typedefinition.xml"), readTemplate("typedefinition.xml.tpl"));
writeGeneratedFile(path.join(baseOutputDir, "appvariables.xml"), readTemplate("appvariables.xml.tpl"), { bom: false });
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Top.xfdl"), renderTopFrame(appSpec));
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Left.xfdl"), renderLeftFrame(forms));
const defaultForm = forms.find((form) => form.route === "login") || forms[0];
writeGeneratedFile(
path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Work.xfdl"),
renderWorkFrame(appSpec, defaultForm?.formId || "frmLogin")
);
forms.forEach((form) => {
writeGeneratedFile(path.join(baseOutputDir, BASE_DIR, `${form.formId}.xfdl`), renderXfdl(form, appSpec));
});
}
function previewHtml(appSpec) {
return `<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapeXml(appSpec.previewTitle)}</title>
<link rel="stylesheet" href="/assets/styles.css" />
</head>
<body>
<div id="app"></div>
<script src="/assets/app.js" defer></script>
</body>
</html>
`;
}
function previewScript(forms) {
const manifest = JSON.stringify(
forms.map((form) => ({
formId: form.formId,
title: form.title,
route: form.route,
authority: form.authority,
messages: form.messages || []
})),
null,
2
);
return `window.HANWHA_FORMS = ${manifest};
`;
}
function previewCss() {
return readTemplate("preview.css.tpl");
}
function previewAppJs(appSpec) {
return renderTemplate(readTemplate("preview-app.js.tpl"), {
appTitle: appSpec.appTitle
});
}
function generatePreview(appSpec, forms, basePreviewDir = previewDir) {
ensureDir(path.join(basePreviewDir, "assets"));
ensureDir(path.join(basePreviewDir, "sample-data"));
fs.writeFileSync(path.join(basePreviewDir, "index.html"), previewHtml(appSpec));
fs.writeFileSync(path.join(basePreviewDir, "assets", "forms.js"), previewScript(forms));
fs.writeFileSync(path.join(basePreviewDir, "assets", "styles.css"), previewCss());
const appJs = `${fs.readFileSync(path.join(basePreviewDir, "assets", "forms.js"), "utf8")}\n${previewAppJs(appSpec)}`;
fs.writeFileSync(path.join(basePreviewDir, "assets", "app.js"), appJs);
}
function generate() {
const { appSpec, forms } = loadSpecs();
generateProjectFiles(appSpec, forms);
generatePreview(appSpec, forms);
}
if (require.main === module) {
generate();
}
module.exports = {
loadSpecs,
generateProjectFiles,
generatePreview,
generate,
renderXfdl
};