1.0.5 • Published 4 years ago
cct-html-ast v1.0.5
cct-html-ast html语法树操作与渲染
- 基于hyntax开源项目的html语法树操作
- 支持html及vue文件的ast语法树操作,添加了常用的语法树操作方法与语法树递归钩子
- 修复了hyntax自关闭标签只支持内置标签的问题,支持自定义的自关闭标签
- 添加了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类
- 该类只是提供了一些快捷方法,实际功能是由HTMLCompiler与HTMLRender类实现
- 为了方便扩展自定义的需求预留了astWalk与renderWalk回调函数,在ast递归时与ast渲染时执行
- 建议尽可能的通过ast语法树的节点操作来改变渲染的结果,不建议通过renderWalk来改变html的渲染结果,除非html渲染存在bug或者无法通过ast的修改实现自己的需求
- 默认在递归ast的时候将html中的src与href属性节点保存在了srcList与hrefList对象中以便操作
- 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对象数据结构