陈童的博客's Archivers

From everyinch on 2014-01-27 13:29:48

ADS3.13 DOM2核心和DOM2 HTML——将HTML代码转换为DOM代码

创建一个简单的工具,通过它把一段HTML代码转换为DOM代码。

效果演示

DOM生成工具的HTML文件

在HTML文件中最主要的是具有两个<textarea>和一个<button>元素:
[code lang="html"]



DOM Generation
AdvancED DOM Scripting Sample Document













DOM Generation



Source


Enter an HTML document fragment


  • $countContent
    $authorContent

    $metaContent


    $commentContent




  • DOM Code


    and voila! DOM goodness:






    [/code]
    这个页面完全依赖于JavaScript,其中包含一个generateDOM.js文件和注册事件侦听器的load.js脚本:
    [code lang="js"]
    // 注册事件侦听器
    ADS.addEvent(window, 'load', function() {

    // 注册click事件侦听器
    ADS.addEvent('generate','click', function(W3CEvent) {

    // 取得HTML源代码
    var source = ADS.$('source').value;

    // 输出结果
    ADS.$('result').value = generateDOM(source);

    });

    });
    [/code]

    要转换成DOM代码的HTML代码片段

    [code lang="html"]

  • 1

    Mr WordPress


    Aug 22nd, 2006 at 5:09 pm


    Hi, this is a comment.To delete a comment, just log ?in, and view the posts' comments, there you will have the option ?to edit or delete them.




  • [/code]
    这个工具的目标就是取得类似下面的HTML代码片段:
    [code lang="html"]
    Mr WordPress
    [/code]
    并将它转换为等价的DOM代码:
    [code lang="js"]
    var a = document.createElement('A');
    a.setAttribute('href','http://wordpress.org');
    a.setAttribute('rel','external nofollow');
    [/code]
    上面代码的链接只是指向了http://wordpress.org,为了增加通用性,将HTML代码片段中需要变化的地方修改成变量,并使用美元符合($)作为前缀:
    [code lang="html"]

  • $countContent
    $authorContent

    $metaContent


    $commentContent


  • [/code]

    扩充ADS库

    在构建generateDOM.js之前,需要向ADS.js库添加几个方法。首先利用字符串的prototype属性添加两个新方法:
    生成重复的字符串:
    [code lang="js"]
    /**
    * 利用核心对象的 prototype 属性
    * 重复1个字符串
    */
    if (!String.repeat) {
    String.prototype.repeat = function(l){
    return new Array(l+1).join(this);
    }
    }
    [/code]
    上面的函数生成一个参数长度加1的空白字符串,然后使用字符串作为分隔符,例如:

    var example = 'a'.repeat(5);
    //example is now: aaaaa
    [/code]
    清除字符串两端的空白字符:
    [code lang="js"]
    /**
    * 移除字符串头部和结尾的空白字符
    */
    if (!String.trim) {
    String.prototype.trim = function() {
    return this.replace(/^\s+|\s+$/g,'');
    }
    }
    [/code]
    下面是添加到ADS命名空间的camelize()方法:
    [code lang="js"]
    /**
    * 将word-word类型的字符串转换成wordWord类型的字符串
    */
    function camelize(s) {
    return s.replace(/-(\w)/g, function (strMatch, p1){
    return p1.toUpperCase();
    });
    }
    window['ADS']['camelize'] = camelize;
    [/code]

    generateDOM的框架

    框架创建了一个新的命名空间,然后包含一些辅助方法和属性,最后是为window对象赋值:
    [code lang="js"]
    /* generateDOM对象的新命名空间 */
    (function(){

    function encode(str) { }

    function checkForVariable(v) { }

    function processAttribute(tabCount,refParent) { }

    function processNode(tabCount,refParent) { }

    var domCode = '';
    var nodeNameCounters = [];
    var requiredVariables = '';
    var newVariables = '';

    function generate(strHTML,strRoot) { }

    window['generateDOM'] = generate;

    })();
    [/code]

    encode()方法

    generateDOM中的第一个方法是encode()方法。对于生成DOM的方法而言,encode()方法用于保证字符串是一个安全的JavaScript字符串。只需要转义反斜杠、单引号和换行符即可:
    [code lang="js"]
    function encode(str) {
    if (!str) return '';
    // 转义反斜杠
    str = str.replace(/\\/g,'\\\\');
    // 转义单引号
    str = str.replace(/';/g, "\\'");
    // 转义换行符
    str = str.replace(/\s+^/mg, "\\n");
    return str;
    }
    [/code]

    checkForVariable()方法

    generateDOM中的第二个方法是checkForVariable()方法。该方法检查字符串中是否包含一个美元符号,如果是,则返回一个带引号的字符串或者变量名。同时,将变量添加到requiredVariable中,以便在输出结果:
    [code lang="js"]
    function checkForVariable(v) {
    if(v.indexOf('$') == -1) {
    v = '\'' + v + '\'';
    } else {
    // 取得该字符串从$到结尾处的子字符串
    v = v.substring(v.indexOf('$')+1)
    requiredVariables += 'var ' + v + ';\n';
    }
    return v;
    }
    [/code]

    generate()方法

    generate()方法是核心方法。它遍历DOM树并检测其中所有的节点,然后按照节点的类型生成DOM代码:
    [code lang="js"]
    function generate(strHTML,strRoot) {

    //将HTML代码添加到页面主体中
    var domRoot = document.createElement('DIV');
    domRoot.innerHTML = strHTML;

    // 重置变量
    domCode = '';
    nodeNameCounters = [];
    requiredVariables = '';
    newVariables = '';

    // 使用processNode()方法处理domRoot中的所有子节点
    var node = domRoot.firstChild;
    while(node) {
    ADS.walkTheDOMRecursive(processNode,node,0,strRoot);
    node = node.nextSibling;
    }

    // 输出结果
    domCode =
    '/* requiredVariables in this code\n' + requiredVariables + '*/\n\n'
    + domCode + '\n\n'
    + '/* new objects in this code\n' + newVariables + '*/\n\n';

    return domCode;
    }
    [/code]

    processNode()方法

    当遍历domRoot中的子节点时,使用processNode()方法分析树中的每个节点,确定节点的类型、值和属性,以便重新创建适当的DOM代码:
    [code lang="js"]
    function processNode(tabCount,refParent) {
    // 根据树的深度重复制表符
    // 以便对每一行进行适当的缩进
    var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');

    // 确定节点类型
    // 处理元素节点和文本节点
    switch(this.nodeType) {
    case ADS.node.ELEMENT_NODE:
    // 递增计数器
    // 结合标签和计数器来表示变量,例如: a1,a2,a3
    if(nodeNameCounters[this.nodeName]) {
    ++nodeNameCounters[this.nodeName];
    } else {
    nodeNameCounters[this.nodeName] = 1;
    }

    var ref = this.nodeName.toLowerCase()
    + nodeNameCounters[this.nodeName];

    // 创建元素节点
    domCode += tabs
    + 'var '
    + ref
    + ' = document.createElement(\'' + this.nodeName +'\');\n';

    // 将心变量添加到列表中
    newVariables += '' + ref + ';\n';

    // 遍历属性
    // 使用processAttribute()方法遍历属性
    if (this.attributes) {
    for(var i=0; i < this.attributes.length; i++) {
    ADS.walkTheDOMRecursive(
    processAttribute,
    this.attributes[i],
    tabCount,
    ref
    );
    }
    }

    break;

    case ADS.node.TEXT_NODE:

    // 编码文本节点,并去掉空白符
    var value = (this.nodeValue ? encode(this.nodeValue.trim()) : '' );
    if(value) {

    // 递增计数器
    // 使用txt和计数器来表示变量,例如: txt1,txt2,txt3
    if(nodeNameCounters['txt']) {
    ++nodeNameCounters['txt'];
    } else {
    nodeNameCounters['txt'] = 1;
    }
    var ref = 'txt' + nodeNameCounters['txt'];

    // 检查格式。格式要求类似于$var
    value = checkForVariable(value);

    // 创建文本节点
    domCode += tabs
    + 'var '
    + ref
    + ' = document.createTextNode('+ value +');\n';
    // 将新变量添加到列表中
    newVariables += '' + ref + ';\n';

    } else {
    // 如果只有空白符则返回
    // 即这个节点将不会添加到父节点中
    return;
    }
    break;

    default:
    // 忽略其它情况
    break;
    }

    // 添加到父节点
    if(refParent) {
    domCode += tabs + refParent + '.appendChild('+ ref + ');\n';
    }
    return ref;
    }
    [/code]
    该方法主要做了以下几件事:
    1. 基于递归的深度来确定DOM代码缩进的级别,并按照需要重复制表符。这种缩进并不是必要的,但它使生成的代码更清晰,也更容易理解:
    [code lang="js"]
    var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');
    [/code]
    2. 按照节点类型来做相应的处理。本方法中需要处理两种类型的节点:ADS.node.ELEMENT_NODE(类型值为1)ADS.node.TEXT_NODE(类型值为3)。主要逻辑如下:
    [code lang="js"]
    switch(node.nodeType) {
    case ADS.node.ELEMENT_NODE:
    //处理元素节点
    break;
    case ADS.node.TEXT_NODE:
    //处理文本节点
    break;
    default:
    //忽略其它情况
    break;
    }
    [/code]
    所有ELEMENT_NODE节点都可能具有属性。属性也是节点,它无法通过同辈定位的方法进行迭代。属性节点包含在node.attributes数组中,因此必须要单独对它们jinx那个遍历:
    [code lang="js"]
    if (node.attributes) {
    for(var i=0; i < node.attributes.length; i++) {
    myWalkTheDOM(processAttribute,node.attributes[i], tabCount, ref);
    }
    }
    [/code]
    3. 在处理完所有的节点之后,唯一要做的就是将其添加到父节点中:
    [code lang="js"]
    // 添加到父节点
    if(refParent) {
    domCode += tabs + refParent + '.appendChild('+ ref + ');\n';
    }
    [/code]
    在创建processAttribute()方法之前,使用示例的HTML代码片段试验这个工具,将会得到如下的DOM代码:
    [code lang="js"]
    /* requiredVariables in this code
    var countContent;
    var authorContent;
    var metaContent;
    var commentContent;
    */
    var li1 = document.createElement('li');
    document.body.appendChild(li1);
    var a1 = document.createElement('a');
    li1.appendChild(a1);
    var txt1 = document.createTextNode(countContent);
    a1.appendChild(txt1);
    var span1 = document.createElement('span');
    li1.appendChild(span1);
    var a2 = document.createElement('a');
    span1.appendChild(a2);
    var txt2 = document.createTextNode(authorContent);
    a2.appendChild(txt2);
    var small1 = document.createElement('small');
    li1.appendChild(small1);
    var a3 = document.createElement('a');
    small1.appendChild(a3);
    var txt3 = document.createTextNode(metaContent);
    a3.appendChild(txt3);
    var div1 = document.createElement('div');
    li1.appendChild(div1);
    var txt4 = document.createTextNode(commentContent);
    div1.appendChild(txt4);
    /* new objects in this code
    li1;
    a1;
    txt1;
    span1;
    a2;
    txt2;
    small1;
    a3;
    txt3;
    [/code]

    processAttribute()方法

    上面的输出中包含所有的ELEMENT_NODE和TEXT_NODE节点,但还缺少节点中的属性,而这正式需要processAttribute()方法来解决的问题:
    [code lang="js"]
    function processAttribute(tabCount,refParent) {

    // 忽略文本节点
    if(this.nodeType != ADS.node.ATTRIBUTE_NODE) return;

    // 取得属性的值
    var attrValue = (this.nodeValue ? encode(this.nodeValue.trim()) : '');
    if(this.nodeName == 'cssText') alert('true');
    // 如果没有值,则返回
    if(!attrValue) return;

    // 确定缩进的级别
    var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');

    // 根据nodeName进行判断
    switch(this.nodeName){
    default:
    if (this.nodeName.substring(0,2) == 'on') {
    // 如果属性名称以'on'开始,说明是一个事件属性
    // 需要创建一个给属性赋值的函数
    domCode += tabs
    + refParent
    + '.'
    + this.nodeName
    + '= function(){' + attrValue +'}\n';
    } else{

    // 对于其它情况,则使用setAttribute()方法
    domCode += tabs
    + refParent
    + '.setAttribute(\''
    + this.nodeName
    + '\', '
    + checkForVariable(attrValue)
    +');\n';
    }
    break;
    case 'class':
    // 将class属性替换为clasName
    domCode += tabs
    + refParent
    + '.className = '
    + checkForVariable(attrValue)
    + ';\n';
    break;
    case 'style':
    // 使用分好(;)和空白符来分割样式
    var style = attrValue.split(/\s*;\s*/);

    if(style){
    for(pair in style){

    if(!style[pair]) continue;

    // 使用冒号(:)和空白符来分隔样式的属性和属性值
    var prop = style[pair].split(/\s*:\s*/);
    if(!prop[1]) continue;

    // 将css-property格式的CSS属性转换为cssProperty格式
    prop[0] = ADS.camelize(prop[0]);

    var propValue = checkForVariable(prop[1]);
    if (prop[0] == 'float') {
    // 由于float是保留字
    // 将float值替换成cssFloat
    domCode += tabs
    + refParent
    + '.style.cssFloat = '
    + propValue
    + ';\n';
    domCode += tabs
    + refParent
    + '.style.styleFloat = '
    + propValue
    + ';\n';
    } else {
    domCode += tabs
    + refParent
    + '.style.'
    + prop[0]
    + ' = '
    + propValue + ';\n';
    }
    }
    }
    break;
    }
    }
    [/code]
    processAttribute()方法大致遵循了与processNode()方法相同的处理方式。处理过程大致如下:
    1. 属性节点可以通过nodeValue属性取得节点的值,但在processNode()方法已经迭代了TEXT_NODE。所以如果要使用nodeValue属性,就必须跳过非ATTRIBUTE_NODE节点。
    2. 如果不存在class或style属性,则使用setAttribute()方法创建相应的属性。但要检查嵌入的事件属性的情形。
    3. 如果是class属性,就需要将class节点的值赋给节点的className属性。
    4. 如果是style属性,则需要对其进行必要的分割。

    至此将HTML代码转换为DOM代码的工具制作完毕,从而免去了许多无谓的DOM脚本编程的工作量。

    查看完整版本: ADS3.13 DOM2核心和DOM2 HTML——将HTML代码转换为DOM代码

    Tags:


    ©陈童的博客