/******************** CONFIGURACIÓN ********************/
const EXPORT_CONFIG = {
SEPARADOR: '─'.repeat(50),
MENU_NAME: '🚀 Exportador ZIP Pro',
MENU_ITEM: '📦 Exportar Proyecto ZIP',
VERSION_PROPERTY: 'EXPORT_ZIP_VERSION',
TRIGGER_ID: 'EXPORTADOR_ZIP_TRIGGER' // Identificador único
};
const EXPORT_MIME_TYPES = [
{ext: ['php'], type: 'application/x-httpd-php'},
{ext: ['js', 'javascript'], type: 'text/javascript'},
{ext: ['html', 'htm'], type: 'text/html'},
{ext: ['css'], type: 'text/css'},
{ext: ['json'], type: 'application/json'},
{ext: ['md', 'markdown'], type: 'text/markdown'},
{ext: ['py'], type: 'text/x-python'},
{ext: ['java'], type: 'text/x-java'},
{ext: ['xml'], type: 'application/xml'},
{ext: ['yaml', 'yml'], type: 'text/yaml'}
];
/******************** MENÚ PRINCIPAL ********************/
/******************** GESTIÓN DE TRIGGERS ********************/
function limpiarTriggersExportador() {
const todosTriggers = ScriptApp.getProjectTriggers();
todosTriggers.forEach(trigger => {
if (trigger.getTag() === EXPORT_CONFIG.TRIGGER_ID) {
try {
ScriptApp.deleteTrigger(trigger);
} catch (e) {
console.warn(`Error eliminando trigger: ${e.message}`);
}
}
});
}
/******************** SELECTOR DE PROYECTOS ********************/
function exportadorMostrarSelectorProyectos() {
limpiarTriggersExportador(); // Limpiar triggers previos
const proyectos = exportadorObtenerListaProyectos();
if (proyectos.length === 0) {
DocumentApp.getUi().alert('❌ No se encontraron proyectos',
'Usa encabezados "Título 1" para cada proyecto:\n# Nombre del Proyecto',
DocumentApp.getUi().ButtonSet.OK);
return;
}
const html = HtmlService.createHtmlOutput(`
<div style="text-align:center;padding:20px;font-family:sans-serif">
<h2>📂 Selector de Proyectos</h2>
<p>Selecciona el proyecto a exportar:</p>
<select id="selectorProyecto" style="width:100%;padding:8px;margin:15px 0;font-size:14px">
${proyectos.map(p => `<option value="${p.index}">${p.nombre}</option>`).join('')}
</select>
<div style="margin-top:20px;">
<button onclick="iniciarExportacion()"
style="padding:10px 25px;background:#1a73e8;color:white;border:none;border-radius:4px;cursor:pointer">
🚀 Exportar Proyecto Seleccionado
</button>
</div>
<div id="resultado" style="margin-top:25px;min-height:50px;"></div>
<script>
function iniciarExportacion() {
const proyectoIndex = document.getElementById('selectorProyecto').value;
document.getElementById('resultado').innerHTML = '<p>⚙️ Preparando exportación...</p>';
google.script.run
.withSuccessHandler(downloadZip)
.withFailureHandler(mostrarError)
.exportadorExportarProyecto(proyectoIndex);
}
function downloadZip(response) {
const link = document.createElement('a');
link.href = 'data:' + response.tipo + ';base64,' + response.data;
link.download = response.nombre;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
document.getElementById("resultado").innerHTML =
'<div style="color:#0f9d58;padding:10px;background:#e6f4ea;border-radius:4px">' +
'<p><strong>✅ Exportación completada</strong></p>' +
'<p>' + response.nombre + ' (' + response.archivos + ' archivos)</p>' +
'</div>';
}
function mostrarError(error) {
document.getElementById("resultado").innerHTML =
'<div style="color:#ea4335;padding:10px;background:#fce8e6;border-radius:4px">' +
'<p><strong>❌ Error:</strong> ' + error.message + '</p>' +
'</div>';
}
</script>
</div>
`).setWidth(500).setHeight(350);
DocumentApp.getUi().showModalDialog(html, 'Selector de Proyectos');
}
/******************** FUNCIONES DE SOPORTE ********************/
function exportadorObtenerListaProyectos() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
const paragraphs = body.getParagraphs();
const proyectos = [];
for (let i = 0; i < paragraphs.length; i++) {
const p = paragraphs[i];
if (p.getHeading() === DocumentApp.ParagraphHeading.HEADING1) {
const nombre = p.getText().trim();
proyectos.push({
index: i,
nombre: nombre
});
}
}
return proyectos;
}
function exportadorObtenerNombreProyecto(paragraphs, proyectoIndex) {
const p = paragraphs[proyectoIndex];
if (p && p.getHeading() === DocumentApp.ParagraphHeading.HEADING1) {
// Extrae solo el nombre sin prefijos como "Proyecto:"
const nombreCompleto = p.getText().trim();
const nombreLimpio = nombreCompleto.replace(/^Proyecto:\s*/i, '');
return nombreLimpio.replace(/[^\w\s\-]/gi, '');
}
return 'Proyecto';
}
/******************** FUNCIÓN PRINCIPAL DE EXPORTACIÓN ********************/
function exportadorExportarProyecto(proyectoIndex) {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
const paragraphs = body.getParagraphs();
const props = PropertiesService.getDocumentProperties();
// Control de versión
let version = parseInt(props.getProperty(EXPORT_CONFIG.VERSION_PROPERTY)) || 0;
version += 1;
props.setProperty(EXPORT_CONFIG.VERSION_PROPERTY, version.toString());
// Buscar el proyecto seleccionado
let proyectoEncontrado = false;
const archivos = {};
let rutaActual = '';
let buffer = [];
let totalArchivos = 0;
// Convertir a número
proyectoIndex = parseInt(proyectoIndex);
// 1. Función para detectar rutas de archivo
const detectarRutaArchivo = (texto) => {
const match = texto.match(/(?:📄|📑) ([^\n\[]+)/);
return match ? match[1].trim() : null;
};
// 2. Función para vaciar el buffer
const flushBuffer = () => {
if (rutaActual && buffer.length) {
archivos[rutaActual] = buffer.join('\n');
totalArchivos++;
buffer = [];
}
};
// 3. Detectar caracter de separador especial
const esSeparador = (texto) => {
// Carácter U+2500 (Box Drawing Light Horizontal)
return /^─{50,}$/.test(texto.replace(/\s/g, ''));
};
for (let i = 0; i < paragraphs.length; i++) {
const p = paragraphs[i];
const texto = p.getText();
const estilo = p.getHeading();
// Detectar inicio del proyecto seleccionado
if (i === proyectoIndex && estilo === DocumentApp.ParagraphHeading.HEADING1) {
proyectoEncontrado = true;
continue;
}
// Detectar fin del proyecto
if (proyectoEncontrado && estilo === DocumentApp.ParagraphHeading.HEADING1 && i !== proyectoIndex) {
break;
}
// Procesar solo dentro del proyecto
if (proyectoEncontrado) {
// Detectar archivos (📄 o 📑)
const posibleRuta = detectarRutaArchivo(texto);
if (posibleRuta) {
flushBuffer(); // Vaciar buffer del archivo anterior
rutaActual = posibleRuta;
}
// Detector de inicio de sección de código (carácter especial)
if (rutaActual && esSeparador(texto)) {
// Activar modo captura de código
continue;
}
// Capturar contenido de código
else if (rutaActual && estilo === DocumentApp.ParagraphHeading.NORMAL) {
// Filtrar líneas de metadatos y progreso
if (!texto.match(/^(🧾|⏳|📄|📦|🔒|✅|❌|⏱️|🔐)/)) {
buffer.push(texto);
}
}
}
}
// Vaciar el último buffer
flushBuffer();
if (totalArchivos === 0) {
throw new Error('No se encontraron archivos exportables. Verifica la estructura.');
}
// Crear blobs para cada archivo
const blobs = Object.entries(archivos).map(([ruta, contenido]) => {
const ext = ruta.split('.').pop().toLowerCase();
const tipo = EXPORT_MIME_TYPES.find(mime => mime.ext.includes(ext))?.type || 'text/plain';
return Utilities.newBlob(contenido, tipo, ruta);
});
// Crear ZIP en memoria
const nombreProyecto = exportadorObtenerNombreProyecto(paragraphs, proyectoIndex);
const nombreZip = `${nombreProyecto}-v${version}.zip`;
const zipBlob = Utilities.zip(blobs, nombreZip);
const base64Data = Utilities.base64Encode(zipBlob.getBytes());
return {
nombre: nombreZip,
data: base64Data,
tipo: "application/zip",
version: version,
archivos: totalArchivos
};
}
/******************** LIMPIEZA AL CERRAR ********************/
function onClose() {
limpiarTriggersExportador();
}