'use strict';
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const C = require('./constants');
const U = require('./utils');
// ─── Main build function ───
function build(input, output, opts) {
opts = opts || {};
// Read the human-readable XML
const humanXml = fs.readFileSync(input, 'utf-8');
const inputPath = path.resolve(input);
const inputDir = inputPath.substring(0, inputPath.lastIndexOf(path.sep)) + path.sep;
const inputName = inputPath.substring(inputPath.lastIndexOf(path.sep) + 1);
// Derive a default base name from the input file
let defaultBase = inputName;
if (defaultBase.endsWith('_edit.xml')) {
defaultBase = defaultBase.slice(0, -9);
} else if (defaultBase.endsWith('.xml')) {
defaultBase = defaultBase.slice(0, -4);
}
const defaultAptName = defaultBase + '.apt';
const outputPath = output || (inputDir + defaultAptName);
// Determine output directory and APT file name
const outDir = path.resolve(outputPath).substring(0, path.resolve(outputPath).lastIndexOf(path.sep)) + path.sep;
const outputAptName = path.basename(outputPath);
// Base name always comes from the output .apt file name
let baseName = outputAptName;
if (baseName.endsWith('.apt')) {
baseName = baseName.slice(0, -4);
} else if (baseName.endsWith('.const')) {
baseName = baseName.slice(0, -6);
}
const aptName = baseName + '.apt';
const tmpdir = U.createTempDir();
let success = false;
try {
// Run xmlcleanhint() transformation
const result = xmlcleanhint(humanXml, baseName, outDir, tmpdir, opts.oldStyle);
// Write raw XML to temp dir
U.writeRawXml(tmpdir, baseName, result.rawXml);
// Run xml2apt.exe
U.runXml2Apt(opts.toolsDir, tmpdir, baseName);
// Ensure output directory exists
fs.mkdirSync(outDir, { recursive: true });
// Copy .apt and .const to output directory
U.copyOutputFromTemp(outDir, tmpdir, baseName, aptName);
// Write manifest, .dat, .ru files
if (result.manifest) {
fs.writeFileSync(outDir + baseName + '.xml', result.manifest, 'utf-8');
}
if (result.dat) {
fs.writeFileSync(outDir + baseName + '.dat', result.dat, 'utf-8');
}
if (result.ruCache) {
for (const [rup, data] of Object.entries(result.ruCache)) {
// Ensure subdirectory exists for .ru files (e.g. geometry/2.ru)
fs.mkdirSync(path.dirname(rup), { recursive: true });
fs.writeFileSync(rup, data, 'utf-8');
}
}
success = true;
return outputPath;
} finally {
if (!opts.keepTemp) {
U.cleanupTempDir(tmpdir, false);
} else {
console.warn('Temporary files kept in: ' + tmpdir);
}
}
}
// ─── xmlcleanhint(): human-readable XML → raw apt XML ───
function xmlcleanhint(humanXml, baseName, outDir, tmpdir, oldStyle) {
U.resetLabels();
const $ = cheerio.load(humanXml, { xmlMode: true, decodeEntities: false });
const ActionNames = oldStyle ? C.ActionNames2 : C.ActionNames;
let isbad = false;
// ──────────────────────────────────────────
// 1. Parse text assembly back to action nodes
// ──────────────────────────────────────────
const actionContainers = $('initaction, buttonaction, action, clipaction');
let lastlabel = '';
actionContainers.each(function (j) {
if (isbad) return;
const v = $(this);
let text = v.text();
text = text.split(/[\r\n]+/);
v.empty();
const aa = [];
const fstk = [];
for (let i = 0; i < text.length; i++) {
const action = stringtoaction(text[i], ActionNames, $);
if (typeof action === 'string') {
if (action[0] !== ';') lastlabel = action;
} else if (action) {
if (action.is('bad')) {
isbad = true;
throw new Error('Incorrect script line: ' + text[i].trim() + '\n' +
'In container: ' + v[0].tagName);
}
if (lastlabel) {
action.attr('mylabel', lastlabel);
lastlabel = '';
}
if (action[0].tagName.indexOf('function') > 0) {
fstk.push([text[i], i]);
} else if (action[0].tagName === 'end') {
if (fstk.length === 0) {
throw new Error('Unexpected end of function');
}
fstk.pop();
}
aa.push(action);
}
}
if (fstk.length > 0) {
throw new Error('No end of function for: ' + fstk[0][0]);
}
v.append(aa);
});
// ──────────────────────────────────────────
// 2. Rebuild constant pools
// ──────────────────────────────────────────
const sortf = function (a, b) {
return a.tagName > b.tagName ? 1 : -1;
};
const actionConstants = $('data,' + C.CONST_ACTION_SELECTOR);
// Sort the collection
Array.prototype.sort.call(actionConstants, sortf);
const constmap = { str: {}, num: {} };
actionConstants.each(function () {
if ($(this).is('[str]')) {
constmap.str[$(this).attr('str')] = 1;
} else if ($(this).is('[num]')) {
constmap.num[$(this).attr('num')] = 1;
}
});
let idx = 0;
const consttable = [];
for (const k in constmap.str) {
constmap.str[k] = idx;
consttable[idx++] = k;
}
for (const k in constmap.num) {
constmap.num[k] = idx;
consttable[idx++] = parseInt(k, 10);
}
// Replace constant references with pool indices
const dataConstants = $('data');
dataConstants.each(function () {
const $v = $(this);
if ($v.is('[str]')) {
$v.attr('val', constmap.str[$v.attr('str')]);
} else if ($v.is('[num]')) {
$v.attr('val', constmap.num[$v.attr('num')]);
}
});
let err = false;
actionContainers.each(function () {
const localstr = {};
const localnum = {};
const localconst = [];
let ci = 0;
const myactions = $(this).find(C.CONST_ACTION_SELECTOR);
Array.prototype.sort.call(myactions, sortf);
myactions.each(function () {
const a = $(this);
let li, k;
if (a.is('[str]')) {
k = a.attr('str');
if (localstr[k] === undefined) {
localstr[k] = ci;
localconst[ci++] = k;
}
li = localstr[k];
a.attr('val', li);
} else if (a.is('[num]')) {
k = a.attr('num');
if (localnum[k] === undefined) {
localnum[k] = ci;
localconst[ci++] = parseInt(k, 10);
}
li = localnum[k];
a.attr('val', li);
}
if (li > 255 && !a.is('short')) {
throw new Error('Byte value exceeds 255');
}
});
if (localconst.length) {
const ct = U.el('constantpool', $);
ct.attr('action', C.ACTION_CONSTANTPOOL);
for (let x = 0; x < localconst.length; x++) {
const cc = U.el('constant', $);
if (typeof localconst[x] === 'string') {
cc.attr('val', constmap.str[localconst[x]]);
cc.attr('str', localconst[x]);
} else {
cc.attr('val', constmap.num[localconst[x]]);
cc.attr('num', localconst[x]);
}
ct.append(cc);
}
$(this).prepend(ct);
}
});
// ──────────────────────────────────────────
// 3. Write .const binary file to tmpdir
// ──────────────────────────────────────────
const cf = fs.openSync(tmpdir + baseName + '.const', 'w');
const constbuf = Buffer.alloc(0x20);
constbuf.asciiWrite('Apt constant file\x1A\0\0');
constbuf.writeUInt32LE(111, 0x14);
constbuf.writeUInt32LE(consttable.length, 0x18);
constbuf.writeUInt32LE(0x20, 0x1c);
fs.writeSync(cf, constbuf, 0, 0x20);
const tmpbuf = Buffer.alloc(8);
const strbuf = Buffer.alloc(512);
let strpos = 0x20 + 8 * consttable.length;
for (let i = 0; i < consttable.length; i++) {
const cv = consttable[i];
if (typeof cv === 'string') {
tmpbuf.writeUInt32LE(1);
tmpbuf.writeUInt32LE(strpos, 4);
strbuf.asciiWrite(cv + '\0');
fs.writeSync(cf, strbuf, 0, cv.length + 1, strpos);
strpos += cv.length + 1;
strpos = Math.ceil(strpos / 4) * 4;
} else {
console.warn('Warning: integer constant (' + cv + ') stored in const table');
tmpbuf.writeUInt32LE(7);
tmpbuf.writeUInt32LE(cv, 4);
}
fs.writeSync(cf, tmpbuf, 0, 8, 0x20 + i * 8);
}
fs.closeSync(cf);
// ──────────────────────────────────────────
// 4. Re-number character IDs
// ──────────────────────────────────────────
const aptroot = $('aptdata');
$('movieclip').attr('id', 0);
let characters = aptroot.children();
// Sort by id
const charArray = [];
characters.each(function () { charArray.push(this); });
charArray.sort(function (a, b) {
const ida = isNaN(a.attribs.id) ? a.attribs.id : eval(a.attribs.id);
const idb = isNaN(b.attribs.id) ? b.attribs.id : eval(b.attribs.id);
if (ida > idb) return 1;
if (ida < idb) return -1;
throw new Error('Duplicate ids: ' + a.attribs.id);
});
// Re-attach sorted
charArray.forEach(function (el) { aptroot.append(el); });
const refs = aptroot.find('[character][character!="-1"],[font][font!="-1"],[sprite][sprite!="-1"]');
charArray.forEach(function (v, i) {
const $v = $(v);
const oldid = $v.attr('id');
const newid = i;
if ($v.attr('id') !== undefined) {
$v.attr('id', newid);
}
refs.filter('[character=' + oldid + ']').not('[filtered]').attr({ character: newid, filtered: '1' });
refs.filter('[font=' + oldid + ']').not('[filtered]').attr({ font: newid, filtered: '1' });
refs.filter('[sprite=' + oldid + ']').not('[filtered]').attr({ sprite: newid, filtered: '1' });
});
// Check for unresolved references
let unsolvedref = null;
refs.each(function () {
if (!$(this).attr('filtered')) {
unsolvedref = this;
return false;
}
});
if (unsolvedref) {
throw new Error('Reference not found: ' + $.html(unsolvedref));
}
// ──────────────────────────────────────────
// 5. Resolve jump targets
// ──────────────────────────────────────────
actionContainers.each(function () {
U.calcaln($(this).children('[action]'));
});
const gotos = $('[targetlabel][action]');
const dups = [];
gotos.each(function () {
const v = $(this);
const action = parseInt(v.attr('action'), 10);
const pos = parseInt(v.attr('pos'), 10);
const targetlabel = v.attr('targetlabel');
const list = v.parent().children();
const target = list.filter('[mylabel=' + targetlabel + ']');
if (target.length > 1) {
dups.push(targetlabel);
}
if (target.is('end')) {
const pr = target.prevAll('[action]:first');
const alnp = parseInt(pr.attr('aln'), 10);
target.attr('aln', alnp + U.alnsize(pr, alnp));
}
const nextaln = parseInt(v.nextAll('[action]:first').attr('aln'), 10);
const targetaln = parseInt(target.attr('aln'), 10);
v.attr('pos', targetaln - nextaln);
});
if (dups.length > 0) {
throw new Error('Duplicate labels: ' + dups.join(','));
}
// ──────────────────────────────────────────
// 6. Resolve function sizes
// ──────────────────────────────────────────
const functions = $('definefunction,definefunction2');
functions.each(function () {
const v = $(this);
let n = v.next();
let scope = 1;
while (n.length) {
if (n[0].tagName.indexOf('definefunction') === 0) {
scope++;
} else if (n[0].tagName === 'end') {
scope--;
if (!scope) {
const endn = n.prevAll('[aln]:first');
const alnend = parseInt(endn.attr('aln'), 10) + U.alnsize(endn, parseInt(endn.attr('aln'), 10));
const alnbegin = parseInt(v.attr('aln'), 10) + U.alnsize(v, parseInt(v.attr('aln'), 10));
v.attr('size', alnend - alnbegin);
return;
}
}
n = n.next();
}
});
// ──────────────────────────────────────────
// 7. Renumber frames
// ──────────────────────────────────────────
$('frames').each(function () {
$(this).children('frame').each(function (j) {
$(this).attr('id', j);
$(this).children('framelabel').attr('frame', j);
});
});
// ──────────────────────────────────────────
// 8. Convert symbolic flags back to numeric
// ──────────────────────────────────────────
$('placeobject').each(function () {
$(this).attr('flags', U.flagstovalue($(this).attr('flags') || '', C.PlaceObjectFlags));
});
$('buttonrecord').each(function () {
$(this).attr('flags', U.flagstovalue($(this).attr('flags'), C.ButtonRecordFlags));
});
$('clipaction').each(function () {
const $v = $(this);
const realflags = U.endian(U.flagstovalue($v.attr('flags'), C.ClipEventFlags), 3);
const keycode = parseInt($v.attr('keycode'), 10);
$v.attr('flags', realflags | (keycode << 24));
$v.removeAttr('keycode');
});
$('buttonaction').each(function () {
const $v = $(this);
$v.attr('flags', U.flagstovalue($v.attr('flags'), C.ButtonActionFlags));
});
// ──────────────────────────────────────────
// 8b. Convert rotm00/rotm01/... format → matrix for placeobject flag calculation
// The human-readable XML (expand output) uses expanded rotm00/tx/ty format,
// but the placeobject flag building below needs the compact `matrix` attribute.
// ──────────────────────────────────────────
$('*').each(function () {
const $v = $(this);
const rm00 = $v.attr('rotm00');
if (rm00 !== undefined) {
const rm01 = $v.attr('rotm01') || '0';
const rm10 = $v.attr('rotm10') || '0';
const rm11 = $v.attr('rotm11') || '1';
const tx = $v.attr('tx') || '0';
const ty = $v.attr('ty') || '0';
$v.attr('matrix', [rm00, rm01, rm10, rm11, tx, ty].join(','));
$v.removeAttr('rotm00 rotm01 rotm10 rotm11 tx ty');
}
// unknown (packed int) → rgbaadd (only if non-zero)
const unk = $v.attr('unknown');
if (unk !== undefined && parseInt(unk, 10) !== 0) {
const u = parseInt(unk, 10);
$v.attr('rgbaadd', (u & 0xff) + ',' + ((u >> 8) & 0xff) + ',' +
((u >> 16) & 0xff) + ',' + ((u >> 24) & 0xff));
$v.removeAttr('unknown');
}
});
// ──────────────────────────────────────────
// 9. Build placeobject flags
// ──────────────────────────────────────────
$('placeobject').each(function () {
const $v = $(this);
let flags = parseInt($v.attr('flags'), 10) || 0;
// The flags may be symbolic (e.g. "HasCharacter|HasMatrix") — parse it
if (isNaN(parseInt($v.attr('flags'), 10))) {
// Symbolic flags: use them directly
flags = U.flagstovalue($v.attr('flags'), C.PlaceObjectFlags);
}
if ($v.attr('character') === '-1' || $v.attr('character') === undefined) {
$v.attr('character', '-1');
flags &= ~C.PlaceObjectFlags.HasCharacter;
} else {
flags |= C.PlaceObjectFlags.HasCharacter;
}
// Use compact `matrix` attr (converted from rotm00 in step 8b)
if ($v.attr('matrix') === undefined) {
$v.attr('matrix', '1,0,0,1,0,0');
flags &= ~C.PlaceObjectFlags.HasMatrix;
} else {
flags |= C.PlaceObjectFlags.HasMatrix;
}
// Color transform: presence of rgbamul or rgbaadd
if ($v.attr('rgbamul') === undefined && $v.attr('rgbaadd') === undefined) {
$v.attr('rgbamul', '255,255,255,255');
$v.attr('rgbaadd', '0,0,0,0');
flags &= ~C.PlaceObjectFlags.HasColorTransform;
} else {
flags |= C.PlaceObjectFlags.HasColorTransform;
}
if ($v.attr('ratio') === undefined || $v.attr('ratio') === '0') {
$v.attr('ratio', '0');
flags &= ~C.PlaceObjectFlags.HasRatio;
} else {
flags |= C.PlaceObjectFlags.HasRatio;
}
if ($v.attr('clipdepth') === '-1' || $v.attr('clipdepth') === undefined) {
$v.attr('clipdepth', '-1');
flags &= ~C.PlaceObjectFlags.HasClipDepth;
} else {
flags |= C.PlaceObjectFlags.HasClipDepth;
}
if ($v.find('poname').length === 0) {
flags &= ~C.PlaceObjectFlags.HasName;
} else {
flags |= C.PlaceObjectFlags.HasName;
}
if ($v.find('clipactions').length === 0) {
flags &= ~C.PlaceObjectFlags.HasClipAction;
} else {
flags |= C.PlaceObjectFlags.HasClipAction;
}
$v.attr('flags', flags);
});
// ──────────────────────────────────────────
// 10. Handle shapes, images, .ru, manifest, .dat
// ──────────────────────────────────────────
let newmanifest = "\n" +
'\n' +
'\n' +
'\n' +
'\n';
let newdat = '; Created by AptXMLEditor.\n';
const texcache = {};
let imgid = aptroot.children().length - 1;
let texid = 0;
let ruerror = false;
const rucache = {};
$('shape').each(function () {
const sp = $(this);
const sid = sp.attr('id');
const ru = sp.find('ru');
let rupath = sp.attr('geometry');
let text = ru.text().replace(/t@c/g, 'tc');
text = text.split(/[\r\n]+/g);
let text2 = '';
for (let i = 0; i < text.length; i++) {
let txt = text[i].trim();
if (txt.indexOf('tc') > 0) {
txt = txt.split(':');
if (txt[5].indexOf('.') > 0) {
// filename reference → find or create image
let iid = $('image').filter(function () {
return $(this).attr('image').toLowerCase() === txt[5].toLowerCase();
}).attr('id');
if (iid === undefined) {
// Create new image
imgid++;
const newimg = U.el('image', $);
newimg.attr({ id: imgid, image: txt[5] });
aptroot.append(newimg);
iid = imgid;
}
txt[5] = iid;
} else {
// numeric id reference
const iid = txt[5];
if ($('image[id=' + iid + '],empty[id=' + iid + ']').length === 0) {
throw new Error('Reference not found: ' + rupath + ' => \n';
sp.attr('geometry', sid);
ru.remove();
});
// Process images
$('image').each(function () {
const v = $(this);
let img1 = v.attr('image');
const img = img1.toLowerCase();
const iid = v.attr('id');
if (!texcache[img]) {
let newid;
if (img.indexOf(',') > 0) {
newid = iid;
} else {
texid++;
newid = texid;
}
texcache[img] = newid;
newmanifest += '\n';
}
if (img.indexOf(',') > 0) {
newdat += iid + '=' + img.split(',')[1] + '\n';
} else {
newdat += iid + '->' + texcache[img] + '\n';
}
v.attr('image', iid);
});
newmanifest += '\n';
// ──────────────────────────────────────────
// 11. Attribute cleanup & transformation
// ──────────────────────────────────────────
$('*').removeAttr('filtered mylabel targetlabel aln newid');
// Process each element for attribute conversions
$('*').each(function () {
const $v = $(this);
let t;
// matrix → rotm00/rotm01/rotm10/rotm11/tx/ty
if ((t = $v.attr('matrix'))) {
const a = t.split(',');
$v.attr('rotm00', a[0]);
$v.attr('rotm01', a[1]);
$v.attr('rotm10', a[2]);
$v.attr('rotm11', a[3]);
$v.attr('tx', a[4]);
$v.attr('ty', a[5]);
$v.removeAttr('matrix');
}
// rgbamul/rgba → red,green,blue,alpha
if ((t = $v.attr('rgbamul')) || (t = $v.attr('rgba'))) {
const rgbam = t.split(',');
$v.attr('red', rgbam[0]);
$v.attr('green', rgbam[1]);
$v.attr('blue', rgbam[2]);
$v.attr('alpha', rgbam[3]);
$v.removeAttr('rgbamul rgba');
}
// rgbaadd → unknown (packed int)
if ((t = $v.attr('rgbaadd'))) {
const rgbaa = t.split(',');
$v.attr('unknown', parseInt(rgbaa[0], 10) |
(parseInt(rgbaa[1], 10) << 8) |
(parseInt(rgbaa[2], 10) << 16) |
(parseInt(rgbaa[3], 10) << 24));
$v.removeAttr('rgbaadd');
}
// color (comma-separated) → packed int
if ((t = $v.attr('color'))) {
const rgba = t.split(',');
$v.attr('color', parseInt(rgba[0], 10) |
(parseInt(rgba[1], 10) << 8) |
(parseInt(rgba[2], 10) << 16) |
(parseInt(rgba[3], 10) << 24));
}
// movieclip: framedelay → unknown
if ($v.is('movieclip') && $v.attr('framedelay') !== undefined) {
$v.attr('unknown', $v.attr('framedelay'));
$v.removeAttr('framedelay');
}
});
// Swap red/blue for edittext
$('edittext').each(function () {
const $v = $(this);
const r = $v.attr('red');
const b = $v.attr('blue');
$v.attr('red', b);
$v.attr('blue', r);
});
// Remove end/dummy markers
$('end,dummy').remove();
// Serialize
let output = $.xml();
output = output.replace(/^[\t]+$/gm, '').replace(/[\r\n]+/g, '\n');
return {
rawXml: output,
manifest: newmanifest,
dat: newdat,
ruCache: rucache
};
}
// ─── stringtoaction(): text assembly → XML action node ───
function stringtoaction(s, ActionNames, $) {
s = s.trim();
if (!s) return null;
// Comment line
if (s[0] === ';') {
return s;
}
let m;
// definefunction2: function name(args)(regs:N, reg1:this, ...)
if ((m = s.match(/^function[\s]+([\w]*)\(([^)]*)\)[\s]*\(([^)]*)\)/i))) {
const a = U.el('definefunction2', $);
a.attr('name', m[1] || '');
a.attr('action', C.ACTION_DEFINEFUNCTION2);
a.attr('size', 0);
const args = m[2].replace(/\s/g, '');
const args2 = m[3].replace(/\s/g, '');
const sysa = [...C.SYSTEM_ARGUMENTS];
const sysab = [...C.SYSTEM_ARGUMENTS];
let beginsys = false;
let lastsys = 0;
let realflags = C.FunctionPreloadFlags.SupressThis |
C.FunctionPreloadFlags.SupressArguments |
C.FunctionPreloadFlags.SupressSuper;
let nregs = 20;
if (args2) {
const parts = args2.split(',');
for (let i = 0; i < parts.length; i++) {
if (parts[i]) {
const am = parts[i].match(/^reg([s]|[0-9]+):([\w]+)/i);
if (am) {
const ar = am[1];
const an = am[2];
if (ar[0] === 's') {
nregs = parseInt(an, 10);
continue;
} else {
const arNum = parseInt(ar, 10);
if (sysa.indexOf(an) >= 0) {
if (!beginsys) {
if (arNum !== 1) return bad('system args must start from reg1', $);
beginsys = true;
}
lastsys++;
if (arNum !== lastsys) return bad('system args must be sequential', $);
while (sysa.shift() !== an) { /* empty */ }
const fp = C.FunctionPreloadFlags;
if (an === 'this') { realflags |= fp.PreloadThis; realflags &= ~fp.SupressThis; }
else if (an === 'arguments') { realflags |= fp.PreloadArguments; realflags &= ~fp.SupressArguments; }
else if (an === 'super') { realflags |= fp.PreloadSuper; realflags &= ~fp.SupressSuper; }
else if (an === '_root') { realflags |= fp.PreloadRoot; }
else if (an === '_parent') { realflags |= fp.PreloadParent; }
else if (an === '_global') { realflags |= fp.PreloadGlobal; }
else if (an === '_extern') { realflags |= fp.PreloadExtern; }
} else if (sysab.indexOf(an) >= 0) {
return bad('system argument in wrong order', $);
} else {
return bad('unknown argument type', $);
}
}
} else {
return bad('wrong argument format', $);
}
}
}
}
// Handle normal arguments (first part of the declaration)
if (args) {
const argParts = args.split(',');
for (let i = 0; i < argParts.length; i++) {
if (argParts[i]) {
const am = argParts[i].match(/^reg([0-9]+):([\w]+)/i);
if (am) {
const an = U.el('argument', $);
an.attr('reg', am[1]);
an.attr('name', am[2]);
a.append(an);
}
}
}
}
const flagsVal = (U.endian(realflags, 3) << 8) | (nregs & 0xff);
a.attr('flags', flagsVal);
return a;
}
// definefunction: function name(args)
if ((m = s.match(/^function[\s]+([\w]*)\(([^)]*)\)/i))) {
const a = U.el('definefunction', $);
a.attr('name', m[1] || '');
a.attr('action', C.ACTION_DEFINEFUNCTION);
a.attr('size', 0);
const args = m[2].replace(/\s/g, '');
if (args) {
const argParts = args.split(',');
for (let i = 0; i < argParts.length; i++) {
if (argParts[i]) {
const an = U.el('argument', $);
an.attr('name', argParts[i].trim());
a.append(an);
}
}
}
return a;
}
// label: (starts with ; or continues from previous line)
if (m = s.match(/^([\w]+)[\s]*:$/)) {
return m[1]; // return label name WITHOUT colon (caller uses it as mylabel)
}
// end of function
if (s === 'end') {
return U.el('end', $);
}
// Parse instruction: actionname [args...]
// Split into instruction name and arguments
const parts = s.match(/([\w]+)(?:\s+(.*))?/);
if (!parts) return null;
const instrName = parts[1];
const argStr = parts[2] ? parts[2].trim() : '';
// Look up the action code by name
let aid = -1;
// Prefer the current ActionNames table (might be old or new style)
for (let i = 0; i < ActionNames.length; i++) {
if (ActionNames[i] && ActionNames[i].toLowerCase() === instrName.toLowerCase()) {
aid = i;
break;
}
}
// If not found in current style, search both tables
if (aid < 0) {
for (let i = 0; i < C.ActionNames.length; i++) {
if (C.ActionNames[i] && C.ActionNames[i].toLowerCase() === instrName.toLowerCase()) {
aid = i;
break;
}
}
}
if (aid < 0) {
for (let i = 0; i < C.ActionNames2.length; i++) {
if (C.ActionNames2[i] && C.ActionNames2[i].toLowerCase() === instrName.toLowerCase()) {
aid = i;
break;
}
}
}
if (aid < 0) {
// Check for plain action name like "push" which might map to specific actions
return bad('unknown instruction: ' + instrName, $);
}
// Parse arguments based on action type
const args = argStr ? parseArgs(argStr) : [];
// Branch instructions: instr labelname
if (aid === C.ACTION_BRANCHALWAYS || aid === C.ACTION_BRANCHIFTRUE || aid === C.EA_BRANCHIFFALSE) {
const a = U.el('goto', $);
a.attr('action', aid);
a.attr('targetlabel', argStr);
return a;
}
// goto frame
if (aid === C.ACTION_GOTOFRAME || aid === C.ACTION_SETREGISTER ||
aid === C.ACTION_WITH || aid === C.ACTION_GOTOEXPRESSION) {
const a = U.el('goto', $);
a.attr('action', aid);
a.attr('pos', parseInt(argStr, 10) || 0);
return a;
}
// geturl: instr "url", "target"
if (aid === C.ACTION_GETURL) {
const a = U.el('geturl', $);
a.attr('action', aid);
a.attr('str1', args[0] || '');
a.attr('str2', args[1] || '');
return a;
}
// pushdata: instr val1, val2, ...
if (aid === C.ACTION_PUSHDATA) {
const a = U.el('pushdata', $);
a.attr('action', aid);
for (let i = 0; i < args.length; i++) {
const c = U.el('constant', $);
const numVal = parseInt(args[i], 10);
if (isNaN(numVal) || args[i] === '') {
c.attr('str', args[i] || '');
c.attr('val', 0);
} else {
c.attr('num', numVal);
c.attr('val', 0);
}
a.append(c);
}
return a;
}
// String-type instructions: instr "string"
if (aid === C.EA_PUSHSTRING || aid === C.EA_GETSTRINGVAR ||
aid === C.EA_GETSTRINGMEMBER || aid === C.EA_SETSTRINGVAR ||
aid === C.EA_SETSTRINGMEMBER || aid === C.ACTION_SETTARGET ||
aid === C.ACTION_GOTOLABEL) {
const a = U.el('string', $);
a.attr('action', aid);
a.attr('str', args[0] || argStr.replace(/^"|"$/g, ''));
a.attr('val', 0);
return a;
}
// Constant-referencing: instr "str" or instr num
if (aid === C.EA_CALLNAMEDFUNCTIONPOP || aid === C.EA_CALLNAMEDFUNCTION ||
aid === C.EA_CALLNAMEDMETHODPOP || aid === C.EA_CALLNAMEDMETHOD ||
aid === C.EA_PUSHCONSTANT || aid === C.EA_GETNAMEDMEMBER ||
aid === C.EA_PUSHVALUEOFVAR) {
const a = U.el('byte', $);
a.attr('action', aid);
// Check if the original argument was quoted (indicates a string constant)
// e.g. pushconst "1" is a string, pushconst 1 is a number
if (argStr && argStr[0] === '"') {
a.attr('str', args[0] || '');
} else {
const numVal = parseInt(args[0], 10);
if (isNaN(numVal) || args[0] === '') {
a.attr('str', args[0] || '');
} else {
a.attr('num', numVal);
}
}
return a;
}
// pushbyte and pushregister take a byte
if (aid === C.EA_PUSHBYTE || aid === C.EA_PUSHREGISTER) {
const a = U.el('byte', $);
a.attr('action', aid);
a.attr('val', parseInt(args[0], 10) || 0);
return a;
}
// pushwordconstant
if (aid === C.EA_PUSHWORDCONSTANT) {
const a = U.el('short', $);
a.attr('action', aid);
if (argStr && argStr[0] === '"') {
a.attr('str', args[0] || '');
} else {
const numVal = parseInt(args[0], 10);
if (isNaN(numVal) || args[0] === '') {
a.attr('str', args[0] || '');
} else {
a.attr('num', numVal);
}
}
return a;
}
// pushshort
if (aid === C.EA_PUSHSHORT) {
const a = U.el('short', $);
a.attr('action', aid);
a.attr('val', parseInt(args[0], 10) || 0);
return a;
}
// pushfloat / pushlong
if (aid === C.EA_PUSHFLOAT || aid === C.EA_PUSHLONG) {
const tag = aid === C.EA_PUSHFLOAT ? 'float' : 'long';
const a = U.el(tag, $);
a.attr('action', aid);
if (aid === C.EA_PUSHFLOAT && args[0]) {
// Float is stored as a 4-byte value via parseInt
a.attr('val', parseFloat(args[0]));
} else {
a.attr('val', parseInt(args[0], 10) || 0);
}
return a;
}
// All other opcodes that have no arguments
if (!args.length || (args.length === 1 && args[0] === '')) {
const a = U.el('noarg', $);
a.attr('action', aid);
return a;
}
// Unknown action with unexpected args — make a noarg with val anyway
const a = U.el('noarg', $);
a.attr('action', aid);
a.attr('val', parseInt(args[0], 10) || 0);
return a;
}
// ─── Parse argument string ───
function parseArgs(argStr) {
const args = [];
let current = '';
let inQuote = false;
for (let i = 0; i < argStr.length; i++) {
const c = argStr[i];
if (c === '"') {
inQuote = !inQuote;
current += c;
} else if (c === ',' && !inQuote) {
args.push(current.trim());
current = '';
} else {
current += c;
}
}
if (current.trim()) {
args.push(current.trim());
}
// Strip surrounding quotes from each argument (matching original eval() behavior)
return args.map(s => {
const t = s.trim();
if (t.length >= 2 && t[0] === '"' && t[t.length - 1] === '"') {
return t.slice(1, -1);
}
return t;
});
}
// ─── bad() — create error marker element ───
function bad(msg, $) {
return $('').attr('text', msg);
}
// ─── Exports ───
module.exports = { build };