能否创建类LISP的功能性自定义HTML元素?
实现类LISP特性的功能性HTML标签方案
你可以通过HTML预处理器(客户端或服务端)实现这类功能性标签,核心是先完成函数定义的收集,再执行标签逻辑,解决递归调用的问题。以下是具体实现思路:
核心设计原则
- 先定义后执行:优先解析所有
<defun>标签,将函数注册到全局环境,再处理其他功能性标签,避免递归调用时函数未定义的问题。 - 环境隔离:为变量和函数维护独立的执行环境,支持局部/全局作用域。
- 标签替换逻辑:将功能性标签的执行结果直接替换原标签的HTML内容,最终输出纯静态HTML。
客户端实现(浏览器端)
利用原生JavaScript结合DOM解析,在页面渲染前完成标签处理:
1. 环境初始化
// 维护变量和函数的全局环境 const runtimeEnv = { variables: {}, functions: {} };
2. 解析函数定义()
先收集所有函数定义,不执行内部递归调用:
function parseDefun(element) { const funcName = element.querySelector('var-name').textContent.trim(); // 提取参数列表 const args = Array.from(element.querySelectorAll('list var-name')) .map(el => el.textContent.trim()); // 提取函数体(跳过前两个子元素:var-name和list) const body = element.children[2].cloneNode(true); runtimeEnv.functions[funcName] = { args, body }; element.remove(); // 移除定义标签,避免干扰后续渲染 }
3. 实现核心标签逻辑
变量赋值
function handleSetf(element) { const varName = element.querySelector('var-name').textContent.trim(); const listItems = Array.from(element.querySelectorAll('list li')) .map(el => el.textContent.trim()); runtimeEnv.variables[varName] = listItems; element.replaceWith(''); // 赋值标签无需保留 }
列表遍历
function handleForeach(element) { const itemVar = element.querySelector('var-name').textContent.trim(); const listVar = element.querySelector('var-val').textContent.trim(); const template = element.querySelector('div').innerHTML; const result = runtimeEnv.variables[listVar] .map(item => template.replace(/<var-val>(.*?)<\/var-val>/g, item)) .join(''); element.replaceWith(result); }
递归函数执行(如)
function executeFunctionCall(element) { const funcName = element.tagName.toLowerCase(); const func = runtimeEnv.functions[funcName]; if (!func) return; // 解析函数参数(支持嵌套标签) const args = Array.from(element.children).map(child => { if (child.tagName.toLowerCase() === 'var-val') { return runtimeEnv.variables[child.textContent.trim()]; } // 递归处理嵌套标签(如<cdr>、<car>) return executeTag(child); }); // 创建局部作用域 const localEnv = { ...runtimeEnv.variables }; func.args.forEach((name, idx) => { localEnv[name] = args[idx]; }); // 执行函数体(以<if>为例) return executeBody(func.body, localEnv); } function executeBody(bodyElement, localEnv) { const tagName = bodyElement.tagName.toLowerCase(); switch(tagName) { case 'if': const nullCheck = bodyElement.querySelector('null'); const targetVal = executeTag(nullCheck.querySelector('var-val'), localEnv); if (targetVal.length === 0) { return executeTag(bodyElement.querySelector('then').firstElementChild, localEnv); } else { return executeTag(bodyElement.querySelector('else').firstElementChild, localEnv); } case 'cons': const carVal = executeTag(bodyElement.querySelector('car'), localEnv); const cdrVal = executeTag(bodyElement.querySelector('cdr'), localEnv); return [carVal, ...cdrVal]; case 'cdr': const cdrTarget = executeTag(bodyElement.querySelector('var-val'), localEnv); return cdrTarget.slice(1); case 'car': const carTarget = executeTag(bodyElement.querySelector('var-val'), localEnv); return carTarget[0]; case 'nil': return []; default: // 处理自定义函数调用 if (runtimeEnv.functions[tagName]) { return executeFunctionCall(bodyElement); } return ''; } } // 通用标签执行入口 function executeTag(element, env = runtimeEnv.variables) { const tagName = element.tagName.toLowerCase(); switch(tagName) { case 'setf': handleSetf(element); break; case 'foreach': handleForeach(element); break; default: if (runtimeEnv.functions[tagName]) { return executeFunctionCall(element); } // 处理变量引用 if (tagName === 'var-val') { return env[element.textContent.trim()]; } break; } }
4. 页面加载时执行处理
document.addEventListener('DOMContentLoaded', () => { // 第一步:收集所有函数定义 document.querySelectorAll('defun').forEach(parseDefun); // 第二步:处理其他功能性标签 document.querySelectorAll('setf, foreach, reverse, if, null, car, cdr, cons').forEach(executeTag); });
服务端实现(Node.js为例)
利用cheerio解析HTML,在服务器端完成标签处理后返回静态HTML:
1. 依赖与环境初始化
const express = require('express'); const cheerio = require('cheerio'); const app = express(); const runtimeEnv = { variables: {}, functions: {} };
2. HTML处理核心逻辑
function processHtml(rawHtml) { const $ = cheerio.load(rawHtml); // 第一步:解析所有<defun>定义 $('defun').each((_, el) => { const funcName = $(el).find('var-name').first().text().trim(); const args = $(el).find('list var-name').map((_, argEl) => $(argEl).text().trim()).get(); const body = $(el).children().eq(2).html(); // 提取函数体 runtimeEnv.functions[funcName] = { args, body }; $(el).remove(); }); // 第二步:处理<setf>变量赋值 $('setf').each((_, el) => { const varName = $(el).find('var-name').first().text().trim(); const values = $(el).find('list li').map((_, liEl) => $(liEl).text().trim()).get(); runtimeEnv.variables[varName] = values; $(el).replaceWith(''); }); // 第三步:处理<foreach>遍历 $('foreach').each((_, el) => { const itemVar = $(el).find('var-name').first().text().trim(); const listVar = $(el).find('var-val').first().text().trim(); const template = $(el).find('div').html(); const result = runtimeEnv.variables[listVar] .map(item => template.replace(/<var-val>(.*?)<\/var-val>/g, item)) .join(''); $(el).replaceWith(result); }); // 第四步:处理递归函数调用(如<reverse>) function resolveFunctions() { let hasUnresolved = false; Object.keys(runtimeEnv.functions).forEach(funcName => { $(funcName).each((_, el) => { hasUnresolved = true; const func = runtimeEnv.functions[funcName]; // 解析参数并执行函数体(逻辑类似客户端实现) // ...省略具体执行逻辑,参考客户端的executeFunctionCall // 最终将元素替换为执行结果 const result = executeServerFunction(func, $(el)); $(el).replaceWith(result.join(', ')); }); }); if (hasUnresolved) resolveFunctions(); // 递归处理直到所有函数调用完成 } resolveFunctions(); return $.html(); }
3. 服务器路由
app.get('/', (req, res) => { const rawHtml = ` <setf> <var-name>X</var-name> <list> <li>a large red box</li> <li>an iron key</li> <li>a mangy cat</li> </list> </setf> <defun> <var-name>reverse</var-name> <list> <var-name>X</var-name> </list> <if> <null> <var-val>X</var-val> </null> <then> <nil /> </then> <else> <cons> <reverse> <cdr> <var-val>X</var-val> </cdr> </reverse> <car> <var-val>X</var-val> </car> </cons> </else> </if> </defun> You see <foreach> <var-name>EL</var-name> <var-val>X</var-val> <div><var-val>EL</var-val>,</div> </foreach> `; const processedHtml = processHtml(rawHtml); res.send(processedHtml); }); app.listen(3000, () => console.log('Server running on port 3000'));
环境选择建议
- 客户端:适合轻量计算、无需服务器的场景,直接在浏览器完成处理,避免服务器请求开销。
- 服务端:适合复杂计算、需要访问服务器资源的场景,处理完成后返回纯静态HTML,兼容性更好。
内容的提问来源于stack exchange,提问作者Phil Goetz
相关产品推荐
相关产品推荐

