diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index dbc45ae0..8b209adb 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -1212,9 +1212,41 @@ function handleProxy(socket, request_type, request_headers, res, data) { headers["wtv-trusted"] = false; if (typeof res.headers['Content-Type'] === 'string' && res.headers['Content-Type'].startsWith("text")) { + // Get the original URL for relative link fixing + const originalUrl = request_headers.request.split(' ')[1]; + + // Transform HTML content for WebTV compatibility + if (res.headers['Content-Type'].includes('html') && + minisrv_config.services[request_type]?.use_minifying_proxy !== false) { + try { + const WTVProxy = require('./includes/classes/WTVProxy.js'); + const proxy = new WTVProxy(minisrv_config); + + let htmlContent = Buffer.concat(data).toString(); + + // Apply WebTV-specific transformations + const transformOptions = { + removeImages: minisrv_config.services[request_type]?.remove_images || false, + maxImageWidth: minisrv_config.services[request_type]?.max_image_width || 400, + simplifyTables: minisrv_config.services[request_type]?.simplify_tables !== false, + addWTVControls: minisrv_config.services[request_type]?.add_wtv_controls !== false, + maxWidth: minisrv_config.services[request_type]?.max_width || 544 + }; + + htmlContent = proxy.transformHtml(htmlContent, originalUrl, transformOptions); + data = [Buffer.from(htmlContent)]; + + if (minisrv_config.config.verbosity >= 3) { + console.log(` * HTML transformed for WebTV compatibility (${originalUrl})`); + } + } catch (err) { + console.warn(` * HTML transformation failed: ${err.message}`); + } + } + if (request_type != "http" && request_type != "https") { // replace http and https links on non http/https protocol (for proto:// for example) - var data_t = data.toString().replaceAll("http://", request_type + "://").replaceAll("https://", request_type + "://"); + var data_t = Buffer.concat(data).toString().replaceAll("http://", request_type + "://").replaceAll("https://", request_type + "://"); data = [Buffer.from(data_t)] } } diff --git a/zefie_wtvp_minisrv/includes/classes/WTVMinifyingProxy.js b/zefie_wtvp_minisrv/includes/classes/WTVMinifyingProxy.js new file mode 100644 index 00000000..593a4f87 --- /dev/null +++ b/zefie_wtvp_minisrv/includes/classes/WTVMinifyingProxy.js @@ -0,0 +1,457 @@ +'use strict'; +const { WTVShared } = require("./WTVShared.js"); + +class WTVMinifyingProxy { + constructor(minisrv_config) { + this.minisrv_config = minisrv_config; + this.wtvshared = new WTVShared(this.minisrv_config); + + // HTML 3.0/4.0 compatible tags and attributes + this.allowedTags = [ + 'html', 'head', 'title', 'meta', 'body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'p', 'br', 'hr', 'div', 'span', 'a', 'img', 'ul', 'ol', 'li', 'table', 'tr', + 'td', 'th', 'tbody', 'thead', 'tfoot', 'form', 'input', 'textarea', 'select', + 'option', 'button', 'b', 'i', 'u', 'strong', 'em', 'center', 'font', 'big', + 'small', 'sub', 'sup', 'pre', 'code', 'blockquote', 'dl', 'dt', 'dd' + ]; + + this.allowedAttributes = [ + 'href', 'src', 'alt', 'title', 'width', 'height', 'border', 'align', 'valign', + 'bgcolor', 'color', 'size', 'face', 'target', 'name', 'value', 'type', 'action', + 'method', 'cols', 'rows', 'cellpadding', 'cellspacing', 'nowrap' + ]; + + // CSS properties to convert to HTML attributes + this.cssToHtml = { + 'text-align': 'align', + 'vertical-align': 'valign', + 'background-color': 'bgcolor', + 'color': 'color', + 'font-size': 'size', + 'font-family': 'face' + }; + } + + /** + * Transform modern HTML to HTML 3.0/4.0 compatible version + * @param {string} html - The HTML content to transform + * @param {string} url - The original URL (for fixing relative links) + * @returns {string} - Transformed HTML + */ + transformHtml(html, url = '') { + try { + let transformed = html; + + // Step 1: Clean up the HTML structure + transformed = this.cleanHtml(transformed); + + // Step 2: Convert modern tags to compatible ones + transformed = this.convertModernTags(transformed); + + // Step 3: Extract and convert CSS to HTML attributes + transformed = this.convertCssToAttributes(transformed); + + // Step 4: Fix links and images + transformed = this.fixUrls(transformed, url); + + // Step 5: Remove unsupported content + transformed = this.removeUnsupportedContent(transformed); + + // Step 6: Minify and optimize + transformed = this.minifyHtml(transformed); + + // Step 7: Return the processed content (structure will be handled by transformForWebTV) + return transformed; + + } catch (err) { + throw new Error(`HTML transformation failed: ${err.message}`); + } + } + + /** + * Clean HTML by removing comments, normalizing whitespace + */ + cleanHtml(html) { + return html + // Remove HTML comments + .replace(//g, '') + // Remove CDATA sections + .replace(//g, '') + // Remove XML declarations + .replace(/<\?xml[^>]*\?>/g, '') + // Normalize whitespace + .replace(/\s+/g, ' ') + .trim(); + } + + /** + * Convert modern HTML5/CSS3 tags to HTML 3.0/4.0 compatible versions + */ + convertModernTags(html) { + // Convert semantic HTML5 tags to divs with classes + const semanticTags = { + 'header': 'div', + 'footer': 'div', + 'nav': 'div', + 'section': 'div', + 'article': 'div', + 'aside': 'div', + 'main': 'div', + 'figure': 'div', + 'figcaption': 'div' + }; + + Object.entries(semanticTags).forEach(([modern, classic]) => { + // Opening tags + html = html.replace(new RegExp(`<${modern}\\b([^>]*)>`, 'gi'), `<${classic}$1>`); + // Closing tags + html = html.replace(new RegExp(``, 'gi'), ``); + }); + + return html; + } + + /** + * Extract CSS styles and convert them to HTML attributes where possible + */ + convertCssToAttributes(html) { + // Remove