[mirotalk] - refactor: extract renderRoomTemplate into standalone roomTemplate.js

This commit is contained in:
Miroslav Pejic
2026-05-03 02:07:25 +02:00
parent 3301dfbe4d
commit a849ecb905
10 changed files with 63 additions and 118 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.8.30 - Environment Configuration
# MiroTalk P2P v.1.8.31 - Environment Configuration
# ====================================================
# App environment
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* ==============================================
* MiroTalk P2P v.1.8.30 - Configuration File
* MiroTalk P2P v.1.8.31 - Configuration File
* ==============================================
*
* This file is the central configuration source.
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies: {
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.30
* @version 1.8.31
*
*/
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mirotalk",
"version": "1.8.30",
"version": "1.8.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mirotalk",
"version": "1.8.30",
"version": "1.8.31",
"license": "AGPL-3.0",
"dependencies": {
"@mattermost/client": "11.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.8.30",
"version": "1.8.31",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
+1 -1
View File
@@ -109,7 +109,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.8.30',
title: 'WebRTC P2P v1.8.31',
html: `
<button
id="support-button"
+3 -48
View File
@@ -15,7 +15,7 @@
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.30
* @version 1.8.31
*
*/
@@ -15752,7 +15752,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.30',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.31',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: renderRoomTemplate('tpl-about-modal', {
@@ -16883,49 +16883,4 @@ function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Render HTML template with provided data
* @param {string} templateId - ID of the <template> element
* @param {object} data - Data to populate the template
* @param {object} data.text - Key-value pairs for text content (data-template-text)
* @param {object} data.html - Key-value pairs for HTML content (data-template-html
* @param {object} data.attrs - Key-value pairs for attributes (data-template-attr-*)
* @returns {string} Rendered HTML string
*/
function renderRoomTemplate(templateId, { text = {}, html = {}, attrs = {} } = {}) {
const template = document.getElementById(templateId);
if (!template || !template.content) return '';
const wrapper = document.createElement('div');
wrapper.appendChild(template.content.cloneNode(true));
wrapper.querySelectorAll('*').forEach((element) => {
element.getAttributeNames().forEach((name) => {
if (!name.startsWith('data-template-attr-')) return;
const attrName = name.replace('data-template-attr-', '');
const key = element.getAttribute(name);
const value = attrs[key];
if (value === undefined || value === null) {
element.removeAttribute(attrName);
} else {
element.setAttribute(attrName, value);
}
element.removeAttribute(name);
});
});
wrapper.querySelectorAll('[data-template-text]').forEach((element) => {
const key = element.getAttribute('data-template-text');
element.textContent = text[key] ?? '';
});
wrapper.querySelectorAll('[data-template-html]').forEach((element) => {
const key = element.getAttribute('data-template-html');
element.innerHTML = html[key] ?? '';
});
return wrapper.innerHTML.trim();
}
// renderRoomTemplate is defined in roomTemplate.js
+48
View File
@@ -0,0 +1,48 @@
'use strict';
/**
* Render HTML template with provided data
* @param {string} templateId - ID of the <template> element
* @param {object} data - Data to populate the template
* @param {object} data.text - Key-value pairs for text content (data-template-text)
* @param {object} data.html - Key-value pairs for HTML content (data-template-html
* @param {object} data.attrs - Key-value pairs for attributes (data-template-attr-*)
* @returns {string} Rendered HTML string
*/
function renderRoomTemplate(templateId, { text = {}, html = {}, attrs = {} } = {}) {
const template = document.getElementById(templateId);
if (!template || !template.content) return '';
const wrapper = document.createElement('div');
wrapper.appendChild(template.content.cloneNode(true));
wrapper.querySelectorAll('*').forEach((element) => {
element.getAttributeNames().forEach((name) => {
if (!name.startsWith('data-template-attr-')) return;
const attrName = name.replace('data-template-attr-', '');
const key = element.getAttribute(name);
const value = attrs[key];
if (value === undefined || value === null) {
element.removeAttribute(attrName);
} else {
element.setAttribute(attrName, value);
}
element.removeAttribute(name);
});
});
wrapper.querySelectorAll('[data-template-text]').forEach((element) => {
const key = element.getAttribute('data-template-text');
element.textContent = text[key] ?? '';
});
wrapper.querySelectorAll('[data-template-html]').forEach((element) => {
const key = element.getAttribute('data-template-html');
element.innerHTML = html[key] ?? '';
});
return wrapper.innerHTML.trim();
}
+1
View File
@@ -1843,6 +1843,7 @@ Note</textarea
<script defer src="../js/fixWebmDuration.js"></script>
<script defer src="../js/screenReader.js"></script>
<script defer src="../js/wakeLock.js"></script>
<script defer src="../js/roomTemplate.js"></script>
<script defer src="../js/client.js"></script>
<script defer src="../js/networkStats.js"></script>
<script defer src="../js/speechRecognition.js"></script>
+4 -63
View File
@@ -7,66 +7,7 @@ const path = require('path');
const vm = require('vm');
const { JSDOM } = require('jsdom');
function extractNamedFunction(source, functionName) {
const signature = `function ${functionName}`;
const start = source.indexOf(signature);
if (start === -1) {
throw new Error(`Unable to find ${functionName} in source file`);
}
const paramsStart = source.indexOf('(', start);
let paramsDepth = 0;
let bodyStart = -1;
for (let index = paramsStart; index < source.length; index++) {
const char = source[index];
if (char === '(') paramsDepth++;
if (char === ')') paramsDepth--;
if (paramsDepth === 0) {
bodyStart = source.indexOf('{', index);
break;
}
}
if (bodyStart === -1) {
throw new Error(`Unable to find ${functionName} body in source file`);
}
let depth = 0;
for (let index = bodyStart; index < source.length; index++) {
const char = source[index];
if (char === '{') depth++;
if (char === '}') depth--;
if (depth === 0) {
return source.slice(start, index + 1);
}
}
throw new Error(`Unable to parse ${functionName} from source file`);
}
function loadNamedFunctionSource(functionName, relativePaths) {
for (const relativePath of relativePaths) {
const sourcePath = path.join(__dirname, '..', ...relativePath.split('/'));
const source = fs.readFileSync(sourcePath, 'utf8');
try {
return extractNamedFunction(source, functionName);
} catch (error) {
if (!error.message.includes(`Unable to find ${functionName}`)) {
throw error;
}
}
}
throw new Error(`Unable to find ${functionName} in source file`);
}
const templateSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'roomTemplate.js'), 'utf8');
describe('test-RoomTemplates', () => {
let renderRoomTemplate;
@@ -76,9 +17,9 @@ describe('test-RoomTemplates', () => {
const dom = new JSDOM('<!doctype html><html><body></body></html>');
document = dom.window.document;
const functionSource = loadNamedFunctionSource('renderRoomTemplate', ['public/js/client.js']);
renderRoomTemplate = vm.runInNewContext(`(${functionSource})`, { document });
const context = vm.createContext({ document, module: {}, exports: {} });
vm.runInContext(templateSource, context);
renderRoomTemplate = context.renderRoomTemplate;
});
it('preserves empty string attributes used by placeholder options', () => {