Initial commit

This commit is contained in:
Jordan
2026-04-01 23:16:45 +01:00
commit 91cfdaee72
200 changed files with 25589 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
const SAFE_INTERNAL_HOSTS = new Set(["web", "acai-web", "localhost", "127.0.0.1"]);
function parseUrl(url, fieldName, context) {
try {
return new URL(url);
} catch {
throw new Error(`[${context}] Invalid ${fieldName}: ${url || "<empty>"}`);
}
}
export function assertSafeCmsTarget(target, context = "cms") {
const publicUrl = typeof target === "string" ? target : (target?.web_url || "");
const apiUrl = typeof target === "string" ? target : (target?.api_web_url || "");
if (!apiUrl) {
throw new Error(
`[${context}] ACAI_API_WEB_URL is required. Refusing to use ACAI_WEB_URL directly for CMS requests.`
);
}
const parsedApiUrl = parseUrl(apiUrl, "ACAI_API_WEB_URL", context);
if (!SAFE_INTERNAL_HOSTS.has(parsedApiUrl.hostname)) {
throw new Error(
`[${context}] Unsafe ACAI_API_WEB_URL host "${parsedApiUrl.hostname}". ` +
`Only approved local hosts are allowed: ${Array.from(SAFE_INTERNAL_HOSTS).join(", ")}.`
);
}
if (!["http:", "https:"].includes(parsedApiUrl.protocol)) {
throw new Error(
`[${context}] Unsafe ACAI_API_WEB_URL protocol "${parsedApiUrl.protocol}".`
);
}
if (publicUrl) {
const parsedPublicUrl = parseUrl(publicUrl, "ACAI_WEB_URL", context);
const publicIsSafeInternal = SAFE_INTERNAL_HOSTS.has(parsedPublicUrl.hostname);
if (!publicIsSafeInternal && parsedPublicUrl.host === parsedApiUrl.host) {
throw new Error(
`[${context}] ACAI_API_WEB_URL resolves to the same public host as ACAI_WEB_URL (${parsedApiUrl.host}).`
);
}
}
return {
publicUrl,
apiUrl,
forgeHost: typeof target === "string" ? null : (target?.forge_host || null),
};
}
export function isSafeInternalHost(hostname) {
return SAFE_INTERNAL_HOSTS.has(hostname);
}

View File

@@ -0,0 +1,195 @@
/**
* Field and schema manipulation helpers
*/
export const LIST_OPTION_ALIAS_KEYS = ["options", "optionsList", "optionList", "choices", "values", "items"];
export const optionEntryToLine = (entry) => {
if (entry == null) {
return null;
}
if (typeof entry === "string") {
const trimmed = entry.trim();
if (!trimmed) return null;
if (trimmed.includes("|")) return trimmed.replace(/\r/g, "");
return `${trimmed}|${trimmed}`;
}
if (Array.isArray(entry)) {
const [valueRaw, labelRaw] = entry;
const value = valueRaw ?? labelRaw;
const label = labelRaw ?? valueRaw;
if (value == null && label == null) return null;
const valueStr = `${value ?? ""}`.trim();
const labelStr = `${label ?? valueStr}`.trim();
if (!valueStr) return null;
return `${valueStr}|${labelStr || valueStr}`;
}
if (typeof entry === "object") {
const value =
entry.value ??
entry.id ??
entry.key ??
entry.slug ??
entry.code ??
entry.name ??
entry.label ??
entry.text;
const label = entry.label ?? entry.text ?? entry.name ?? entry.title ?? value;
if (value == null && label == null) return null;
const valueStr = `${value ?? ""}`.trim();
const labelStr = `${label ?? valueStr}`.trim();
if (!valueStr) return null;
return `${valueStr}|${labelStr || valueStr}`;
}
return null;
};
export const buildOptionsTextFromInput = (input) => {
if (input == null) {
return "";
}
if (Array.isArray(input)) {
return input.map(optionEntryToLine).filter(Boolean).join("\n");
}
if (typeof input === "object") {
return Object.entries(input)
.map(([value, label]) => optionEntryToLine({ value, label }))
.filter(Boolean)
.join("\n");
}
if (typeof input === "string") {
const trimmed = input.trim();
if (!trimmed) {
return "";
}
if (trimmed.includes("|")) {
return trimmed
.replace(/\r/g, "")
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
}
const hasNewLines = /\r|\n/.test(trimmed);
const separator = hasNewLines ? /\r?\n/ : /,/;
return trimmed
.split(separator)
.map((token) => token.trim())
.filter(Boolean)
.map((token) => `${token}|${token}`)
.join("\n");
}
return "";
};
export const normalizeListFieldDefinition = (field = {}) => {
if (!field || field.type !== "list") {
return field;
}
if (!field.listType) {
field.listType = "pulldown";
}
if (!field.optionsType) {
field.optionsType = "text";
}
if (field.optionsType === "text") {
let source = field.optionsText;
if (Array.isArray(source) || (source && typeof source === "object" && !Array.isArray(source))) {
field.optionsText = buildOptionsTextFromInput(source);
} else {
let aliasValue;
for (const aliasKey of LIST_OPTION_ALIAS_KEYS) {
if (field[aliasKey] != null) {
aliasValue = field[aliasKey];
delete field[aliasKey];
break;
}
}
if (aliasValue != null) {
field.optionsText = buildOptionsTextFromInput(aliasValue);
} else if (typeof field.optionsText === "string") {
field.optionsText = buildOptionsTextFromInput(field.optionsText);
} else {
field.optionsText = field.optionsText ?? "";
}
}
} else {
// Ensure plain text payload for non-text option sources.
if (field.optionsText && typeof field.optionsText !== "string") {
field.optionsText = "";
}
for (const aliasKey of LIST_OPTION_ALIAS_KEYS) {
if (field[aliasKey] != null) {
delete field[aliasKey];
}
}
}
return field;
};
export const normalizeSchemaForSave = (schema = {}) => {
if (!schema || typeof schema !== "object") {
return schema;
}
if (schema.schema && typeof schema.schema === "object") {
const normalized = {};
for (const [fieldName, fieldDefinition] of Object.entries(schema.schema)) {
if (!fieldDefinition || typeof fieldDefinition !== "object") {
normalized[fieldName] = fieldDefinition;
continue;
}
const clonedDefinition = { ...fieldDefinition };
if (clonedDefinition.type === "list") {
normalized[fieldName] = normalizeListFieldDefinition(clonedDefinition);
} else {
normalized[fieldName] = clonedDefinition;
}
}
schema.schema = normalized;
}
return schema;
};
export const mergeTableSchemas = (currentTable, incomingSchema = {}) => {
const merged = {
...currentTable,
schema: { ...(currentTable?.schema || {}) },
schemaInfo: { ...(currentTable?.schemaInfo || {}) },
};
if (!incomingSchema || typeof incomingSchema !== "object") {
return merged;
}
for (const [key, value] of Object.entries(incomingSchema)) {
if (key === "schema" && value && typeof value === "object") {
merged.schema = { ...(currentTable?.schema || {}), ...value };
} else if (key === "schemaInfo" && value && typeof value === "object") {
merged.schemaInfo = { ...(currentTable?.schemaInfo || {}), ...value };
} else {
merged[key] = value;
}
}
return merged;
};

View File

@@ -0,0 +1,60 @@
/**
* Utility functions shared across the MCP server
*/
export const encodeBase64 = (value) => Buffer.from(value, "utf-8").toString("base64");
export const cleanDomainValue = (domain) => {
if (!domain) {
return null;
}
return domain.replace(/^https?:\/\//i, "").replace(/\/$/, "").toLowerCase();
};
export const mapDomainsForOutput = (domains = []) =>
domains.map((item) => ({
num: item.num,
domain: item.domain,
label: item.domain,
isMulti: item.is_multi_child === "1" || item.multiSite === "1",
}));
export const extractTokenHash = (payload) => {
if (!payload || typeof payload !== "object") {
return null;
}
if (payload.tokenHash) {
return payload.tokenHash;
}
if (payload.token_hash) {
return payload.token_hash;
}
if (payload.data && typeof payload.data === "object") {
return extractTokenHash(payload.data);
}
return null;
};
export const pickDomain = (domains = [], domainInput, domainNumInput) => {
if (!domains.length) {
return null;
}
if (domainNumInput != null) {
const domainNum = String(domainNumInput).trim();
const domainByNum = domains.find((entry) => String(entry.num).trim() === domainNum);
if (domainByNum) {
return domainByNum;
}
}
if (!domainInput) {
return null;
}
const normalizedDomain = cleanDomainValue(domainInput);
return domains.find((entry) => cleanDomainValue(entry.domain) === normalizedDomain);
};
export const getSessionKey = (username, password) => `${username}::${password}`;

View File

@@ -0,0 +1,4 @@
export * from './helpers.js';
export * from './fieldHelpers.js';

View File

@@ -0,0 +1,436 @@
// Helper function for base64 encoding (works in both browser and Node.js)
const btoa = typeof window !== 'undefined' ? btoa : (str) => Buffer.from(str).toString('base64');
export const builderData = {
"data-field-type" : {
type:"ATTRIBUTE",
description:`Determina que el elemento es editable desde el Builder para los clientes. Se puede añadir un elemento multi que da la posibilidad de añadir distintos bloques a los clientes. Dentro de cada multi se podrán poner campos de edición
<br><br>Nota : Los componentes de estos ejemplos pueden variar dependiendo del analizador léxico utilizado.`,
example : `<div data-field-type="textfield" data-field-label="Label" >Elemento editable</div>
<div data-field-type="headfield" data-field-label="Titulo" >Elemento editable</div>
<div data-field-type="link" data-field-label="Enlace">Elemento editable</div>
<div data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>
<div data-field-type="wysiwyg" data-field-label="Texto Largo enriquecido">Elemento editable</div>
<img data-field-type="upload" data-field-rand="true" data-field-label="Image" data-field-info1="titulo" data-field-width="ancho maximo en pixeles (opcional. Ej 1400)"\>
<div data-field-type="uploadBackground" data-field-label="Imagen de fondo">Elemento editable</div>
<div data-field-type="list" data-field-label="pruebas" data-list-options="opcion1,opcion2,|option3,4|opcion 4" ></div>
<div data-field-type="list" data-field-label="pruebas" data-list-table="nombredeTabla" data-list-value="campoValor" data-list-label="campoLabel">
{{record.name}}
</div>
<div data-field-type="list" data-field-label="Apartados" data-list-query="select num,name from cms_otros_contenidos" data-list-multi>
<div v-for="contenido in otros_contenidos" v-where="num=$record">{{contenido.name}} </div>
</div>
Si se desea que sea multi añadir el atributo data-list-multi
<div data-field-type="list" data-list-multi></div>
<div data-field-type="uploadMulti" data-field-label="Imagenes" data-field-info1="titulo" >
<img src="{{uploadMulti.urlPath | imagec(700)}}"\>
</div>
<ul>
NOTA : El Multi debe tener un nodo padre ( sin hermanos ).
<li data-field-type="multiv2" data-field-label="Records">
<div data-field-type="textfield" data-field-label="Label" >Elemento editable</div>
<div data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>
<img data-field-type="upload" data-field-info1="titulo" data-field-label="Image" data-field-width="ancho maximo en pixeles (opcional. Ej 1400)"\>
</li>
</ul>
`,
shortcode : ``,
directLinks : {
TextField : `<p c-if="label" data-field-type="textfield" data-field-label="Label" >Elemento editable</p>`,
HeadField : `<p c-if="titulo" data-field-type="headfield" data-field-label="Titulo" >Elemento editable</p>`,
'List (Options)' : `<div data-field-type="list" data-field-label="pruebas" data-list-options="opcion1,opcion2,|option3,4|opcion 4" ></div>`,
'List (Table)' : `<div data-field-type="list" data-field-label="pruebas" data-list-table="nombredeTabla" data-list-value="campoValor" data-list-label="campoLabel">
{{record.name}}
</div>`,
Link : `<a c-if="enlace_anchor" data-field-type="link" data-field-label="Enlace">Elemento editable</a>`,
TextBox : `<div c-if="textolargo" data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>`,
Wysiwyg : `<div c-if="textolargoenriquecido" class="wysiwyg" data-field-type="wysiwyg" data-field-label="Texto Largo enriquecido">Elemento editable</div>`,
Upload : `<div c-if="imagen.0.urlPath" class="p-1/6 relative"><img class="absolute top-0 left-0 w-full h-full object-cover object-center lazyload" data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-info1="titulo" data-field-width="1400" alt=""\></div>`,
UploadBackground : `<div c-if="imagendefondo.0.urlPath" class="bg-cover bg-center bg-no-repeat" data-field-type="uploadBackground" data-field-info1="titulo" data-field-label="Imagen de fondo">Elemento editable</div>`,
UploadMulti : `<div c-if="imagenes" data-field-type="uploadMulti" data-field-label="Imagenes" data-field-info1="titulo" >
<img src="{{uploadMulti.urlPath | imagec(700)}}"\>
</div>`,
Multi : `<ul>
<li data-field-type="multiv2" data-field-label="Records">
<div data-field-type="textfield" data-field-label="Label">Elemento editable</div>
<div data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>
<div class="p-1/6 relative"><img class="absolute top-0 left-0 w-full h-full object-cover object-center lazyload" data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-info1="titulo" data-field-width="1400" alt=""\></div>
</li>
</ul>`,
},
shortcuts: {
'C-Form' : `<c-form tableName="" sendToClient="string campo que se usará como cliente" sendTo="string correos separados por coma" honeypot="true" captcha="true" mailRecord="['correos','SOLICITUD']" class="">
</c-form>`,
Hook: `<hook endpoint="/hooks/nombre_del_hook/" result="almacen_del_resultado" :variable="variable" :variable_string="'mi string'"></hook>`,
Dump: `<pre style="display:none">{{dump(thisrecord)}}</pre>`,
TextoGeneral: `{{'' | translate}}`
},
replace : (el,prefixVar) => {
// ACAI ANALYZER
let attr = el.getAttribute("data-field-type");
if (!attr) return el.outerHTML;
el.removeAttribute("data-field-type");
let label = el.getAttribute("data-field-label");
if (!label) {
label = "Untitled " + new Date().getTime();
}else{
el.removeAttribute("data-field-label");
}
let width = el.getAttribute("data-field-width");
if (!width){
width = 1600;
}else{
el.removeAttribute("data-field-width");
}
let infos = [];
for (let i=1;i<5;i++){
var info = el.getAttribute("data-field-info"+i);
if (info){
infos.push(info);
el.removeAttribute("data-field-info"+i);
}
}
const field = prefixVar ? `${prefixVar}["${appParser.cleanString(label)}"]` : `$${appParser.cleanString(label)}`;
const field_anchor = prefixVar ? `${prefixVar}["${appParser.cleanString(label)}_anchor"]` : `$${appParser.cleanString(label)}_anchor`;
const field_tag = prefixVar ? `${prefixVar}["${appParser.cleanString(label)}_tag"]` : `$${appParser.cleanString(label)}_tag`;
let rand = el.getAttribute("data-field-rand");
if (!rand){
rand = ``;
}else{
rand = `<? shuffle(${field});?>`;
}
switch(attr){
case "multiv2":
let php1 = '|*' + btoa(`<? foreach(${field} as $index => $record){ ?>`) + '*|';
let php2 = '|*' + btoa(`<? } ?>`) + '*|';
let string = `${php1}${appParser.parseComponents(el.outerHTML,`$record`)}${php2}`;
el.outerHTML = string;
break;
case "link":
if (el.tagName!='A'){
let php1 = '|*' + btoa(`<? echo ${field}; ?>`) + '*|';
let php2 = '|*' + btoa(`<? echo ${field_anchor}; ?>`) + '*|';
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? `<a href='${php1}'>${appParser.parseComponents(el.innerHTML,prefixVar)}</a>` : `<a href='${php1}'>${php2}</a>`;
}else{
el.setAttribute('href','|*' + btoa(`<? echo @${field}; ?>`) + '*|');
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? appParser.parseComponents(el.innerHTML,prefixVar) : '|*' + btoa(`<? echo @${field_anchor};?>`) + '*|';
}
break;
case "uploadMulti":
let php1up = '|*' + btoa(`${rand}<? foreach(${field} as $index => $uploadMulti){ ?>`) + '*|';
let php2up = '|*' + btoa(`<? } ?>`) + '*|';
let resultVariables = appParser.parseVariables2(el.outerHTML);
let preStringVars = resultVariables[0];
let stringVars = resultVariables[1];
let stringup = `${php1up}${preStringVars}${stringVars}${php2up}`;
el.outerHTML = stringup;
break;
case "list":
const isTable = el.hasAttribute("data-list-table");
const tableSelect = el.getAttribute("data-list-table");
const valueSelect = el.getAttribute("data-list-value");
const labelSelect = el.getAttribute("data-list-label");
const querySelect = el.getAttribute("data-list-query");
el.removeAttribute("data-list-table");
el.removeAttribute("data-list-options");
el.removeAttribute("data-list-value");
el.removeAttribute("data-list-label");
el.removeAttribute("data-list-multi");
el.removeAttribute("data-list-query");
let php1li = '|*' + btoa(`<? ${field} = array_filter(explode("\t",${field}));if (isset($record)) $auxRecord = $record; foreach(${field} as $index => $record){ ?>`) + '*|';
if (isTable) php1li += '|*' + btoa(`<? $schema = loadSchema("${tableSelect}"); if (@$schema) {$record = @dame_registros("${tableSelect}","${valueSelect}='$record'","num desc",1)[0];}else{ global $TABLE_PREFIX; $record = @mysql_query_fetch_all_assoc("SELECT * FROM ".$TABLE_PREFIX."${tableSelect} WHERE ${valueSelect}='$record' LIMIT 1")[0]; } if (!$record) continue;?>`) + '*|';
let php2li = '|*' + btoa(`<? }
if (isset($auxRecord)) $record = $auxRecord;?>`) + '*|';
let stringli = `${php1li}${appParser.parseComponents(el.outerHTML,`$record`)}${php2li}`;
el.outerHTML = stringli;
break;
case "upload":
if (el.hasAttribute("src")){
el.setAttribute('src','|*' + btoa(`${rand}<? echo CustomCode::imagec(${width},${field}[0]['urlPath']);?>`) + '*|');
}else{
// let classString = el.getAttribute("class");
// let styleString = el.getAttribute("style");
// let altString = el.getAttribute("alt");
var srcAttr = "src";
var output = "";
const attrs = el.attributes;
for(var i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name.toLowerCase() == "src") continue;
if (attrs[i].name.toLowerCase() == "data-lazy") { srcAttr="data-src"; continue;}
output += `${attrs[i].name}="${attrs[i].value}" `;
}
console.log({output:output});
let php = '|*' + btoa(`${rand}<? echo CustomCode::imagec(${width},${field}[0]['urlPath']); ?>`) + '*|';
el.outerHTML = `<img ${srcAttr}='${php}' ${output}>`;
}
break;
case "uploadBackground":
let php3 = '|*' + btoa(`${rand}<? echo CustomCode::imagec(${width},${field}[0]['urlPath']); ?>`) + '*|';
el.setAttribute('style',`background-image:url('${php3}')`);
break;
case "headfield":
let outputh = "";
const attrs = el.attributes;
for(var i = attrs.length - 1; i >= 0; i--) {
outputh += `${attrs[i].name}="${attrs[i].value}" `;
}
let phph1 = '|*' + btoa(`<? echo ${field}; ?>`) + '*|';
let phph2 = '|*' + btoa(`<? echo '<'.${field_tag}.' ${outputh}>'; ?>`) + '*|';
let phph3 = '|*' + btoa(`<? echo '</'.${field_tag}.'>'; ?>`) + '*|';
el.outerHTML = `<span>${phph2} ${phph1} ${phph3}</span>`;
break;
default:
//el.classList.add(`wed_${field.replace(/\$/g,"")}:${attr}`);
el.innerHTML = '|*' + btoa(`<? echo @${field} ? nl2br(${field}) : '${el.innerHTML}';?>`) + '*|';
}
return el;
},
replace2 : (el,prefixVar) => {
// TWIG ANALYZER
let attr = el.getAttribute("data-field-type");
if (!attr) return el.outerHTML;
el.removeAttribute("data-field-type");
let label = el.getAttribute("data-field-label");
if (!label) {
label = "Untitled " + new Date().getTime();
}else{
el.removeAttribute("data-field-label");
}
let width = el.getAttribute("data-field-width");
if (!width){
width = 1600;
}else{
el.removeAttribute("data-field-width");
}
let infos = [];
for (let i=1;i<5;i++){
var info = el.getAttribute("data-field-info"+i);
if (info){
infos.push(info);
el.removeAttribute("data-field-info"+i);
}
}
const field = prefixVar ? `${prefixVar}.${appParser.cleanString(label)}` : `${appParser.cleanString(label)}`;
const field_anchor = prefixVar ? `${prefixVar}.${appParser.cleanString(label)}_anchor` : `${appParser.cleanString(label)}_anchor`;
const field_tag = prefixVar ? `${prefixVar}.${appParser.cleanString(label)}_tag` : `${appParser.cleanString(label)}_tag`;
let rand = el.getAttribute("data-field-rand");
if (!rand){
rand = ``;
}else{
rand = `{% ${field} = ${field} | shuffle %}\n`;
}
switch(attr){
case "multiv2":
let php1 = `\n{% for record in ${field} %} {% set index = loop.index0 %}\n`;
let php2 = `\n{% endfor %}\n`;
let string = `${php1}${appParser.parseComponents(el.outerHTML,`record`,2)}${php2}`;
el.outerHTML = string;
break;
case "link":
if (el.tagName!='A'){
let php1 = `{{ ${field} }}`;
let php2 = `{{ ${field_anchor} }}`;
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? `<a href='${php1}'>${appParser.parseComponents(el.innerHTML,prefixVar)}</a>` : `<a href='${php1}'>${php2}</a>`;
}else{
el.setAttribute('href',`{{ ${field} }}`);
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? appParser.parseComponents(el.innerHTML,prefixVar) : `{{ ${field_anchor} }}`;
}
break;
case "uploadMulti":
let php1up = `\n ${rand} \n {% for uploadMulti in ${field} %} \n {% set index = loop.index0 %} \n`;
let php2up = `\n {% endfor %} \n `;
let stringup = `${php1up}${el.outerHTML}${php2up}`;
el.outerHTML = stringup;
break;
case "list":
const isTable = el.hasAttribute("data-list-table");
const tableSelect = el.getAttribute("data-list-table");
const valueSelect = el.getAttribute("data-list-value");
const labelSelect = el.getAttribute("data-list-label");
const querySelect = el.getAttribute("data-list-query");
el.removeAttribute("data-list-table");
el.removeAttribute("data-list-options");
el.removeAttribute("data-list-value");
el.removeAttribute("data-list-label");
el.removeAttribute("data-list-multi");
el.removeAttribute("data-list-query");
let php1li = `\n
{% set list_values = ${field} | trim | split("\t") %} \n
`;
php1li += `\n
{% if record %} \n
{% set auxRecord = record %} \n
{% endif %} \n
`;
php1li += `\n
{% for record in list_values %} \n
{% set index = loop.index0 %} \n
`;
if (isTable) {
php1li += `\n
{% if '${tableSelect}' | loadSchema %} \n
{% set record = '${tableSelect}' | get([{'column':'${valueSelect}','operator':'=','value':record}]) %} \n
{% set record = record.0 %} \n
{% else %} \n
{% set record = 'cms_${tableSelect}' | get([{'column':'${valueSelect}','operator':'=','value':record}],'num desc',1,{'ignoreSchema':true}) %}
{% set record = record.0 %}
{% endif %}
`;
}
php2li = `\n
{% endfor %}\n
{% if auxRecord %} {% set record = auxRecord %} {% endif %}
`;
/*let php1li = '|*' + btoa(`<? ${field} = array_filter(exp lode("\t",${field}));if (isset($record)) $auxRecord = $record; foreach(${field} as $index => $record){ ?>`) + '*|';
if (isTable) php1li += '|*' + btoa(`<? $schema = loadSchema("${tableSelect}"); if (@$schema) {$record = @dame_registros("${tableSelect}","${valueSelect}='$record'","num desc",1)[0];}else{ global $TABLE_PREFIX; $record = @mysql_query_fetch_all_assoc("SELECT * FROM ".$TABLE_PREFIX."${tableSelect} WHERE ${valueSelect}='$record' LIMIT 1")[0]; } if (!$record) continue;?>`) + '*|';
let php2li = '|*' + btoa(`<? }
if (isset($auxRecord)) $record = $auxRecord;?>`) + '*|';*/
let stringli = `${php1li}${appParser.parseComponents(el.outerHTML,`record`,2)}${php2li}`;
el.outerHTML = stringli;
break;
case "upload":
if (el.hasAttribute("src")){
el.setAttribute('src',`${rand}{{ ${field}.0.urlPath | imagec(${width}) }}`);
}else{
// let classString = el.getAttribute("class");
// let styleString = el.getAttribute("style");
// let altString = el.getAttribute("alt");
var srcAttr = "src";
var output = "";
const attrs = el.attributes;
for(var i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name.toLowerCase() == "src") continue;
if (attrs[i].name.toLowerCase() == "data-lazy") { srcAttr="data-src"; continue;}
output += `${attrs[i].name}="${attrs[i].value}" `;
}
console.log({output:output});
let php = `${rand}{{ ${field}.0.urlPath | imagec(${width}) }}`;
el.outerHTML = `<img ${srcAttr}='${php}' ${output}>`;
}
break;
case "uploadBackground":
let php3 = `${rand}{{ ${field}.0.urlPath | imagec(${width}) }}`;
el.setAttribute('style',`background-image:url('${php3}')`);
break;
case "textbox":
//el.classList.add(`wed_${field.replace(/\$/g,"")}:${attr}`);
var filter = "nl2br";
var expre = new RegExp("<(\\S*?)[^>]*>.*?</\\1>|<.*?/>");
el.innerHTML = `
{% if ${field} %} \n
{% if ${field} | isHTML %}
{{ ${field} | raw }} \n
{% else %}
{{ ${field} | nl2br }} \n
{% endif %}
{% else %} \n
{{ "${el.innerHTML.replace(/\x22/g, '\\\x22')}" | nl2br }} \n
{% endif %}
`;
break;
case "headfield":
let outputh = "";
const attrs = el.attributes;
for(var i = attrs.length - 1; i >= 0; i--) {
outputh += `${attrs[i].name}="${attrs[i].value}" `;
}
let phph1 = `{{ ${field} }}`;
let phph2 = `{{ '<' ~ (${field_tag} ? ${field_tag} : 'P') ~ '${outputh}>' }}`;
let phph3 = `{{ '< /' ~ (${field_tag} ? ${field_tag} : 'P') ~ '>' }}`;
// ESTA CODIFICADO EN BASE64 PORQUE ACAI LO CONVIERTE A COMENTARIO POR EL ANALIZADOR LEXICO DE ACAI QUE YA NO DEBE DE ESTAR
let php4 = ``;
let phph4 = ``;
if (prefixVar){
// {% set record = record|merge({'nombre_tag':record.nombre_tag ?: 'P'}) %}
phph4 = `
{% set ${prefixVar} = ${prefixVar}|merge({'${appParser.cleanString(label)}_tag': ${field_tag} ?: 'P'}) %}
{{ ('<' ~ ${field_tag} ~ ' ${outputh}>' ~ (${field} ? ${field} : '${el.innerHTML.replace(/\x22/g, '\\\x22')}') ~ ('PC8=' | base64_decode) ~ ${field_tag} ~ '>') | raw }}
`;
}else{
phph4 = `
{% set ${field_tag} = ${field_tag} ?: 'P' %}
{{ ('<' ~ ${field_tag} ~ ' ${outputh}>' ~ (${field} ? ${field} : '${el.innerHTML.replace(/\x22/g, '\\\x22')}') ~ ('PC8=' | base64_decode) ~ ${field_tag} ~ '>') | raw }}
`;
}
el.outerHTML = `${phph4}`;
break;
default:
//el.classList.add(`wed_${field.replace(/\$/g,"")}:${attr}`);
el.innerHTML = `
{% if ${field} %} \n
{{ ${field} | raw }} \n
{% else %} \n
{{ "${el.innerHTML.replace(/\x22/g, '\\\x22')}" | raw }} \n
{% endif %}
`;
}
return el;
}
}
};

View File

@@ -0,0 +1,33 @@
import { parseComponents as remoteParseComponents, generateBuilderVars as remoteGenerateBuilderVars } from "./remoteParser.js";
export class ModuleParser {
/**
* Parse components (c-if, c-for, c-else, c-hidden) and module tags
* Converts Acai syntax to Twig syntax
* Uses the remote appParser to ensure consistency with frontend
* @param {string} html - The HTML content to parse
* @param {string[]} moduleIds - Optional list of module IDs to recognize as tags
* @param {string} prefixVar - Optional prefix for variables (used internally)
* @param {boolean} skipBuilderData - Optional flag to skip extractBuilderData (to avoid recursion)
* @returns {Promise<string>} - The parsed HTML with Twig syntax
*/
static async parseComponents(html, moduleIds = [], listTables = [], prefixVar = "", skipBuilderData = false) {
if (!html) return html;
// Use the remote appParser (same as frontend)
return await remoteParseComponents(html, moduleIds, listTables, prefixVar, skipBuilderData);
}
/**
* Generate builder variables from code
* Uses the remote appParser to ensure consistency with frontend
* @param {string} code - The HTML code to analyze
* @param {object} previousSchema - Optional previous schema for field name mapping
* @returns {Promise<{codeParsed: string, codeVars: object}>} - Parsed code and variables
*/
static async generateBuilderVars(code, previousSchema = null) {
// Use the remote appParser (same as frontend)
return await remoteGenerateBuilderVars(code, previousSchema);
}
}

View File

@@ -0,0 +1,155 @@
import { JSDOM } from 'jsdom';
import axios from 'axios';
import vm from 'vm';
// Cache para los scripts y appParser
let appParserCache = null;
let windowCache = null;
let scriptsCache = null;
/**
* Descarga y ejecuta los scripts remotos necesarios para appParser
* Igual que en el frontend (src/main.js)
*/
async function loadRemoteParser() {
if (appParserCache) {
return { appParser: appParserCache, window: windowCache };
}
const scripts = [
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/lexer.js",
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/mixins/vuecomponents.js",
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/mixins/builderdata.js",
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/mixins/filters.js",
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/parseDocument.js",
];
// Crear un contexto jsdom
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
runScripts: "dangerously",
resources: "usable"
});
const window = dom.window;
const document = window.document;
// Mock de objetos necesarios que pueden no estar en jsdom
if (!window.btoa) {
window.btoa = (str) => Buffer.from(str).toString('base64');
}
if (!window.atob) {
window.atob = (str) => Buffer.from(str, 'base64').toString();
}
// Asegurar que DOMParser esté disponible (jsdom lo tiene en window)
// Pero también lo necesitamos como variable global para los scripts
const DOMParser = window.DOMParser;
// Mock de window.bus (puede que no sea necesario para el parseo)
if (!window.bus) {
window.bus = {
$emit: () => {},
$on: () => {},
$off: () => {}
};
}
// Crear contexto VM con todas las referencias necesarias
// Los scripts remotos esperan que window, document, DOMParser, etc. estén disponibles globalmente
const context = vm.createContext({
window: window,
document: document,
DOMParser: DOMParser, // Añadir DOMParser como variable global
console: console,
Buffer: Buffer,
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearInterval: clearInterval,
// Añadir todas las propiedades globales necesarias
...global,
// Asegurar que las referencias estén disponibles también como variables globales
global: global,
process: process
});
// Descargar y ejecutar cada script
for (const scriptUrl of scripts) {
try {
console.log(`Descargando script: ${scriptUrl}`);
const response = await axios.get(scriptUrl, {
timeout: 10000 // 10 segundos de timeout
});
const scriptContent = response.data;
// Ejecutar el script en el contexto VM
// Los scripts pueden usar 'window', 'document', etc. directamente
vm.runInContext(scriptContent, context);
} catch (error) {
console.error(`Error cargando script ${scriptUrl}:`, error.message);
// Si falla un script crítico, lanzar error
if (scriptUrl.includes('parseDocument.js')) {
throw new Error(`Error crítico cargando parseDocument.js: ${error.message}`);
}
// Continuar con los demás scripts para los no críticos
}
}
// Verificar que appParser esté disponible
if (!window.appParser) {
throw new Error('appParser no se cargó correctamente desde los scripts remotos');
}
appParserCache = window.appParser;
windowCache = window;
scriptsCache = scripts;
return { appParser: appParserCache, window: windowCache };
}
/**
* Obtiene appParser, cargándolo si es necesario
*/
export async function getAppParser() {
const { appParser } = await loadRemoteParser();
return appParser;
}
/**
* Wrapper para parseComponents usando appParser remoto
* Usa tipo 2 (Twig) explícitamente
* Firma real: parseComponents(code, prefixVar, type = 0)
*/
export async function parseComponents(html, moduleIds = [], listTables = [], prefixVar = "", skipBuilderData = false) {
const { appParser, window } = await loadRemoteParser();
// Setear las variables globales en el window real donde se cargó appParser
window.allModules = moduleIds;
window.tables = listTables;
// La firma real es: parseComponents(code, prefixVar, type = 0)
// Pasamos 2 (número) para Twig explícitamente
return appParser.parseComponents(html, prefixVar, 2);
}
/**
* Wrapper para generateBuilderVars usando appParser remoto
* Usa tipo 2 (Twig) explícitamente, igual que en Api.js
* Nota: El código remoto falla si previousSchema es null, así que pasamos {} en su lugar
*/
export async function generateBuilderVars(code, previousSchema = null) {
const appParser = await getAppParser();
// Pasar 2 para Twig (igual que en Api.js: parseInt(type) donde type="2")
// El código remoto falla si previousSchema es null, así que usamos {} en su lugar
const safePreviousSchema = previousSchema || {};
return appParser.generateBuilderVars(code, 2, safePreviousSchema);
}
/**
* Limpia la caché (útil para forzar recarga)
*/
export function clearCache() {
appParserCache = null;
scriptsCache = null;
}