cct-html-ast

1.0.5 • Public • Published

cct-html-ast html语法树操作与渲染

  1. 基于hyntax开源项目的html语法树操作
  2. 支持html及vue文件的ast语法树操作,添加了常用的语法树操作方法与语法树递归钩子
  3. 修复了hyntax自关闭标签只支持内置标签的问题,支持自定义的自关闭标签
  4. 添加了ast转html代码的能力,添加了ast转html时递归生成html的钩子

安装

使用npm或cnpm安装

    // npm安装
    npm install --save cct-html-ast
    // cnpm安装
    cnpm install --save cct-html-ast
    // yarn安装
    yarn add cct-html-ast

使用

esm模块化引用

    import HTMLAST,{HTMLCompiler,HTMLRender} from 'cct-html-ast';

cjs模块化引用

    const {HTMLCompiler,HTMLRender,default:HTMLAST} = require('cct-html-ast');

HTMLAST类

  1. 该类只是提供了一些快捷方法,实际功能是由HTMLCompiler与HTMLRender类实现
  2. 为了方便扩展自定义的需求预留了astWalk与renderWalk回调函数,在ast递归时与ast渲染时执行
  3. 建议尽可能的通过ast语法树的节点操作来改变渲染的结果,不建议通过renderWalk来改变html的渲染结果,除非html渲染存在bug或者无法通过ast的修改实现自己的需求
  4. 默认在递归ast的时候将html中的src与href属性节点保存在了srcList与hrefList对象中以便操作
  5. ast的节点都继承于Node类,该类提供了一些常用的节点操作的api
import hyntax from './hyntax/index.js';
import { HTMLCompiler } from './lib/html-compiler.js';
import { HTMLRender } from './lib/html-render.js';
export const { tokenize, constructTree } = hyntax;
class HTMLAST {
  constructor ({ content = '', astWalk, renderWalk }) {
    const compiler = new HTMLCompiler({ content, astWalk });
    const render = new HTMLRender({ renderWalk });
    render.updateAst(compiler.ast);
    this._compiler = compiler;
    this._render = render;
  }
 
  get ast () {
    return this._compiler.ast;
  }
 
  get html () {
    return this._render.html;
  }
 
  get srcList () {
    return this._compiler.srcList;
  }
 
  get hrefList () {
    return this._compiler.hrefList;
  }
 
  render () {
    this._render.render();
  }
}
export default HTMLAST;
 

Node节点类

class Node {
  constructor (attrs = {}) {
    for (const key in attrs) {
      this[key] = attrs[key];
    }
  }
 
  index () {
    const { children } = this.parent;
    const { length } = children;
    for (let i = 0; i < length; i++) {
      if (children[i] === this) {
        return i;
      }
    }
    return -1;
  }
 
  createNode (node) {
    /**
     * 如果参数是Node节点对象则直接返回该节点引用,并没有对节点进行复制
     * 或许你本就希望将同一个节点复用并插入在不同节点或位置,引用的好处就是任何对节点的操作都会同步发生变化
     * 也有可能你希望对已有的节点进行复制避免引用造成的问题
     * 还有可能你希望节点的操作像DOM中的节点操作一样,将一个节点插入另一个节点会先将这个节点从原有的父级删除
     * 所以我不太确定每个人的需求是什么样的,因此默认是引用方式没有做其它的处理
     * 其实无论是哪种需求都很简单,所以用的时候有自己的需求就自己来写吧
     */
    if (typeof node === 'string') {
      return new HTMLCompiler({ content: node }).ast.children;
    } else {
      if (node instanceof Node) {
        return node;
      } else {
        throw new Error('无效的node');
      }
    }
  }
 
  append (node) {
    const { children } = this;
    children.push(...this.createNode(node));
  }
 
  prepend (node) {
    const { children } = this;
    children.unshift(...this.createNode(node));
  }
 
  after (node) {
    if (this.parent) {
      const { children } = this.parent;
      const index = this.index() + 1;
      children.splice(index, 0, ...this.createNode(node));
    } else {
      throw new Error('this.parent is not defined!');
    }
  }
 
  before (node) {
    if (this.parent) {
      const { children } = this.parent;
      const index = this.index();
      children.splice(index, 0, ...this.createNode(node));
    } else {
      throw new Error('this.parent is not defined!');
    }
  }
 
  removeChildren () {
    this.children.splice(0);
  }
 
  removeSelf () {
    const { children } = this.parent;
    const { length } = children;
    for (let i = 0; i < length; i++) {
      if (children[i] === this) {
        children.splice(i, 1);
        break;
      }
    }
  }
}
 

HTMLRender类

export class HTMLRender {
  constructor ({ ast = {}, renderWalk } = {}) {
    this.html = '';
    this.ast = ast;
    this.renderWalk = typeof renderWalk === 'function' ? renderWalk : false;
    this.renderNodeType = {
      10: 'renderDocument',
      9: 'renderDoctype',
      8: 'renderComment',
      1: 'renderTag',
      3: 'renderText'
    };
  }
 
  updateAst (ast) {
    if (ast) {
      this.ast = ast;
    }
  }
 
  renderDocument (node) {
    return [''];
  }
 
  renderDoctype ({ attrs }) {
    return [
      `<!Doctype ${this.renderAttrs(attrs)}/>`
    ];
  }
 
  renderComment (node) {
    return [`<!--${node.text}-->`];
  }
 
  renderTag ({ tag, attrs = {}, selfClosing }) {
    return [
      `<${tag} ${this.renderAttrs(attrs)}${selfClosing ? '' : '>'}`,
      selfClosing ? '/>' : `</${tag}>`
    ];
  }
 
  renderText ({ text }) {
    return [`${text}`];
  }
 
  renderAttrs (attrs = {}) {
    return Object.entries(attrs).map(([key, value]) => {
      return value === '' ? key : `${key}="${value}"`;
    }).join(' ');
  }
 
  render () {
    const { ast, renderNodeType, renderWalk } = this;
    const walk = (node) => {
      const { type, children = [], text } = node;
      const renderMethod = renderNodeType[type];
      if (this[renderMethod]) {
        const [openTag, closeTag = ''] = this[renderMethod](node);
        const content = type === 1 && text ? text : children.map(child => {
          return walk(child);
        }).join('');
        const [open, close] = renderWalk ? renderWalk(node, openTag, closeTag) || [] : [];
        return open === undefined && close === undefined ? `${openTag}${content}${closeTag} ` : `${open}${content}${close}`;
      } else {
        throw new Error(`无法解析的节点类型【${type} 】`);
      }
    };
    this.html = walk(ast);
  }
}
 

示例

import HTMLAST from '../src/index.js';
const code = `<!DOCTYPE html>
<html>
 
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>cct-html-ast</title>
</head>
<body>
  <div id="app"></div>
  <!--{{dll}}-->
  <a href="a.html">sdf</a>
  <a href="/a.html">sdf</a>
  <a href="https://nick.com/a.html">sdf</a>
  <img src="a/b/c.png"/>
  <img src="https://nick.com/a/b/c.png"/>
</body>
 
</html>
 
`;
const obj = new HTMLAST({
  content: code,// 要操作的html源码或vue源码
 
// ast语法树递归的钩子函数,接收一个node节点参数,该node节点为引用类型,
// 直接操作该节点将可以改变ast语法树,如果回调函数返回false则该节点将被忽略
// 以下示例是通过node.type过滤掉了所有的text文本节点
// 如果只是想修改node节点的属性可以不返回任何值 比如可以node.tag='div'可以修改节点tag名
  astWalk: node => node.type !== 3,
 
  // ast渲染成html代码的钩子函数,接收一个node节点参数,返回的数据必须是数组格式[openTag,closeTag]
  // 标签中的内容可以在保存在第一个数组中,如果没有结束标签数组第二元素可以为空或undefined
  renderWalk: node => {
    if (node.tag === 'a') {
        //此处通过钩子函数修改了a标签的渲染结果
      return ['<a>test', '</a>'];
    }
  }
});
// 通过ast节点的before方法插入了新的节点
obj.ast.children[1].before('<div>sdfsd</div>adfss');
// 执行ast的渲染
obj.render();
// HTMLAST 对象的结构参见下图
console.log(obj);

HTMLAST对象数据结构 HTMLAST对象数据结构

Package Sidebar

Install

npm i cct-html-ast

Weekly Downloads

7

Version

1.0.5

License

MIT

Unpacked Size

972 kB

Total Files

58

Last publish

Collaborators

  • cuichuanteng