Repository: tableauio/tableau
Pull request #9 (2022-06-08 07:38:27)

feat(xml): The more robust and clearer multi-pass parser
The xml parser was refactored into a three-pass parser:
1. Read xml file with `xmlquery`, construct a recursively self-described tree structure defined in `xml.proto` and put it into memory.
2. Preprocess the tree structure. In this phase the parser will do some necessary jobs before generating a 2-dimensional sheet, like `correctType` which make the types of attributes in the nodes meet the requirements of `protogen`.
3. Transform the recursive tree structure into a 2-dimensional sheet, which can be further processed into `protoconf`.


Patch:
diff --git a/internal/importer/xml.go b/internal/importer/xml.go
index eeaed14..cad2f1f 100644
--- a/internal/importer/xml.go
+++ b/internal/importer/xml.go
@@ -1,11 +1,9 @@
 package importer
 
 import (
-	"bufio"
 	"bytes"
 	"encoding/xml"
 	"fmt"
-	"io"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -13,7 +11,6 @@ import (
 	"strings"
 
 	"github.com/antchfx/xmlquery"
-	"github.com/antchfx/xpath"
 	"github.com/iancoleman/strcase"
 	"github.com/pkg/errors"
 	"github.com/tableauio/tableau/internal/atom"
@@ -24,118 +21,28 @@ import (
 	"github.com/tableauio/tableau/proto/tableaupb"
 )
 
-// metaName defines the meta data of each worksheet.
-const (
-	metaName             = "TABLEAU"
-	emptyMetaSheetRegexp = `^\s*<!--\s*@TABLEAU\s*-->\s*$` // e.g.: <!-- @TABLEAU -->
-)
-
-type Range struct {
-	begin   int // index that Range begins at
-	attrNum int // the number of attrs with the same prefix
-	len     int // total number of columns with the same prefix, including attrs and children
-}
 type XMLImporter struct {
 	*book.Book
 }
 
 type NoNeedParseError struct {
-	err error
 }
 
-func (e NoNeedParseError) Error() string {
-	return "`@TABLEAU` not found"
+func (e *NoNeedParseError) Error() string {
+	return fmt.Sprintf("`%s` not found", book.MetasheetName)
 }
 
-func (e NoNeedParseError) Unwrap() error {
-	return e.err
-}
+var attrRegexp *regexp.Regexp
+var scalarListRegexp *regexp.Regexp
 
-var metaBeginRegexp *regexp.Regexp
-var metaEndRegexp *regexp.Regexp
-var attrValRegexp *regexp.Regexp
+const (
+	xmlProlog         = `<?xml version='1.0' encoding='UTF-8'?>`
+	ungreedyPropGroup = `(\|\{[^\{\}]+\})?`
+)
 
 func init() {
-	metaBeginRegexp = regexp.MustCompile(`^\s*<!--\s*@TABLEAU\s*$|` + emptyMetaSheetRegexp) // e.g.: <!--    @TABLEAU
-	metaEndRegexp = regexp.MustCompile(`^\s*-->\s*$|` + emptyMetaSheetRegexp)               // e.g.:       -->
-	attrValRegexp = regexp.MustCompile(`"` + types.PubTypeGroup + `"`)                                             // e.g.: "map<uint32, Type>"
-}
-
-func matchMetaBeginning(s string) []string {
-	return metaBeginRegexp.FindStringSubmatch(s)
-}
-
-func isMetaBeginning(s string) bool {
-	return matchMetaBeginning(s) != nil
-}
-
-func matchMetaEnding(s string) []string {
-	return metaEndRegexp.FindStringSubmatch(s)
-}
-
-func isMetaEnding(s string) bool {
-	return matchMetaEnding(s) != nil
-}
-
-// getMetaDoc get metaSheet document from `@TABLEAU` comments block. e.g.:
-//
-// <!-- @TABLEAU
-// <ServerConf key="map<uint32,ServerConf> Open="bool">
-// 	...
-// </ServerConf>
-// -->
-//
-// will be converted to
-//
-// <ServerConf key="map<uint32,ServerConf> Open="bool">
-// 	...
-// </ServerConf>
-func getMetaDoc(doc string) (metaDoc string, err error) {
-	metaBuf := bytes.NewBuffer(make([]byte, 0, len(doc)))
-	scanner := bufio.NewScanner(strings.NewReader(doc))
-	inMetaBlock := false
-	foundMeta := false
-	for scanner.Scan() {
-		metaBeginning := isMetaBeginning(scanner.Text())
-		metaEnding := isMetaEnding(scanner.Text()) && (inMetaBlock || metaBeginning)
-		if metaBeginning {
-			foundMeta = true
-		}
-		// close a meta block
-		if metaEnding {
-			break
-		}
-		if metaBeginning && !metaEnding {
-			inMetaBlock = true
-		} else if inMetaBlock {
-			metaBuf.WriteString(scanner.Text() + "\n")
-		}
-	}
-	// `@TABLEAU` must exist
-	if !foundMeta {
-		return metaBuf.String(), &NoNeedParseError{}
-	}
-	return metaBuf.String(), nil
-}
-
-// escapeMetaDoc escape characters for all attribute values in the document. e.g.:
-//
-// <ServerConf key="map<uint32,ServerConf> Open="bool">
-// 	...
-// </ServerConf>
-//
-// will be converted to
-//
-// <ServerConf key="map&lt;uint32,ServerConf&gt; Open="bool">
-// 	...
-// </ServerConf>
-func escapeAttrs(doc string) string {
-	escapedDoc := attrValRegexp.ReplaceAllStringFunc(doc, func(s string) string {
-		var buf bytes.Buffer
-		xml.EscapeText(&buf, []byte(s[1:len(s)-1]))
-		return fmt.Sprintf("\"%s\"", buf.String())
-	})
-	return escapedDoc
+	attrRegexp = regexp.MustCompile(`([0-9A-Za-z_]+)="` + types.TypeGroup + ungreedyPropGroup + `"`)
+	scalarListRegexp = regexp.MustCompile(`([A-Za-z_]+)([0-9]+)`)
 }
 
 // TODO: options
@@ -152,7 +59,7 @@ func NewXMLImporter(filename string, sheets []string) (*XMLImporter, error) {
 		}, nil
 	}
 	// newBook.ExportCSV()
-	
+
 	return &XMLImporter{
 		Book: newBook,
 	}, nil
@@ -162,139 +69,498 @@ func NewXMLImporter(filename string, sheets []string) (*XMLImporter, error) {
 // in TABLEAU grammar which can be exported to protobuf by excel parser.
 func parseXML(filename string, sheetNames []string) (*book.Book, error) {
 	// open xml file and parse the document
-	xmlPath := filename
-	atom.Log.Debugf("xml: %s", xmlPath)
-	buf, err := os.ReadFile(xmlPath)
+	atom.Log.Debugf("xml: %s", filename)
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to open %s", filename)
+	}
+	root, err := xmlquery.Parse(f)
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to open %s", xmlPath)
+		return nil, errors.Wrapf(err, "failed to parse xml:%s", filename)
 	}
 
-	// get metaSheet document
-	metaDoc, err := getMetaDoc(string(buf))
+	// The first pass
+	xmlMeta, sheetNames, err := readXMLFile(root, sheetNames)
 	if err != nil {
-		var noNeedParse *NoNeedParseError
-		if errors.As(err, &noNeedParse) {
-			atom.Log.Debugf("%s no need parse: %s", xmlPath, noNeedParse)
+		switch e := err.(type) {
+		case *NoNeedParseError:
+			atom.Log.Debugf("xml:%s no need parse: @TABLEAU not found", filename)
 			return nil, nil
-		} else {
-			return nil, errors.Wrapf(err, "failed to getMetaDoc from xml content:\n%s", string(buf))
+		default:
+			return nil, e
 		}
 	}
 
-	// escape characters for attribute
-	metaDoc = escapeAttrs(metaDoc)
-	atom.Log.Debug(metaDoc)
+	bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
+	newBook := book.NewBook(bookName, filename, nil)
+	for _, xmlSheet := range xmlMeta.SheetList {
+		sheetName := xmlSheet.Meta.Name
+		// The second pass
+		if err := preprocess(xmlSheet, xmlSheet.Meta); err != nil {
+			return nil, errors.Wrapf(err, "failed to preprocess for sheet: %s", sheetName)
+		}
+		// The third pass
+		sheet, err := genSheet(xmlSheet)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to genSheet for sheet: %s", sheetName)
+		}
+		newBook.AddSheet(sheet)
+	}
+	atom.Log.Debug(sheetNames)
+
+	if len(sheetNames) > 0 {
+		newBook.Squeeze(sheetNames)
+	}
+
+	return newBook, nil
+}
 
-	//------------------------------ The first pass ------------------------------//	
-	// parse the metaSheet
-	// Note that one xml file only has one root
-	// So in order to have multiple roots, we need to use a stream parser
+// --------------------------------------------- THE FIRST PASS ------------------------------------ //
+// The first pass simply read xml file with xmlquery, construct a recursively self-described tree 
+// structure defined in xml.proto and put it into memory.
+//
+// readXMLFile read the raw xml rooted at `root`, specify which sheets to parse and return a XMLBook.
+func readXMLFile(root *xmlquery.Node, sheetNames []string) (*tableaupb.XMLBook, []string, error) {
+	xmlMeta := &tableaupb.XMLBook{
+		SheetMap: make(map[string]int32),
+	}
 	noSheetByUser := len(sheetNames) == 0
-	header := options.NewDefault().Input.Proto.Header
-	type SheetCache struct{
-		metaSheet *xlsxgen.MetaSheet // metaSheet begins with `<-- @TABLEAU`
-		prefixMap map[string]Range // prefix -> [6, 9)
+	foundMetaSheetName := false
+	for n := root.FirstChild; n != nil; n = n.NextSibling {
+		switch n.Type {
+		case xmlquery.CommentNode:
+			if !strings.Contains(n.Data, book.MetasheetName) {
+				continue
+			}
+			foundMetaSheetName = true
+			metaStr := xmlProlog + escapeAttrs(strings.ReplaceAll(n.Data, book.MetasheetName, ""))
+			// atom.Log.Debug(metaStr)
+			metaRoot, err := xmlquery.Parse(strings.NewReader(metaStr))
+			if err != nil {
+				return nil, nil, errors.Wrapf(err, "failed to parse @TABLEAU string: %s", metaStr)
+			}
+			for n := metaRoot.FirstChild; n != nil; n = n.NextSibling {
+				if n.Type != xmlquery.ElementNode {
+					continue
+				}
+				sheetName := n.Data
+				xmlSheet := getXMLSheet(xmlMeta, sheetName)
+				if err := parseMetaNode(n, xmlSheet); err != nil {
+					return nil, nil, errors.Wrapf(err, "failed to parseMetaNode for sheet:%s", sheetName)
+				}
+				// append if user not specified
+				if noSheetByUser {
+					sheetNames = append(sheetNames, sheetName)
+				}
+			}
+		case xmlquery.ElementNode:
+			sheetName := n.Data
+			xmlSheet := getXMLSheet(xmlMeta, sheetName)
+			if err := parseDataNode(n, xmlSheet); err != nil {
+				return nil, nil, errors.Wrapf(err, "failed to parseDataNode for sheet:%s", sheetName)
+			}
+		default:
+		}
 	}
-	sheetMap := make(map[string]*SheetCache)
-	p, err := xmlquery.CreateStreamParser(strings.NewReader(metaDoc), "/")
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to create stream parser from string %s", metaDoc)
+	if !foundMetaSheetName && noSheetByUser {
+		return nil, nil, &NoNeedParseError{}
 	}
-	for {
-		n, err := p.Read()
-		if err == io.EOF {
-			break
+
+	return xmlMeta, sheetNames, nil
+}
+
+// parseMetaNode parse xml node `curr` and construct the meta tree in `xmlSheet`.
+func parseMetaNode(curr *xmlquery.Node, xmlSheet *tableaupb.XMLSheet) error {
+	_, path := getNodePath(curr)
+	meta := xmlSheet.MetaNodeMap[path]
+	for _, attr := range curr.Attr {
+		attrName := attr.Name.Local
+		t := attr.Value
+		if len(meta.AttrMap.List) > 0 && isCrossCell(t) {
+			return fmt.Errorf("%s=\"%s\" is a complex type, must be the first attribute", attrName, t)
 		}
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to read from stream parser")
-		}		
-		nav := xmlquery.CreateXPathNavigator(n)
-		sheetName := nav.LocalName()
-		// add new sheet to cache
-		sheetMap[sheetName] = &SheetCache{xlsxgen.NewMetaSheet(sheetName, header, false), make(map[string]Range)}
-		if err := firstParseSheet(nav, sheetMap[sheetName].metaSheet, sheetMap[sheetName].prefixMap, true); err != nil {
-			return nil, errors.Wrapf(err, "failed to parse `@%s` sheet: %s#%s", metaName, filename, sheetName)
+		if idx, ok := meta.AttrMap.Map[attrName]; !ok {
+			meta.AttrMap.Map[attrName] = int32(len(meta.AttrMap.List))
+			meta.AttrMap.List = append(meta.AttrMap.List, &tableaupb.XMLNode_AttrMap_Attr{
+				Name:  attrName,
+				Value: t,
+			})
+		} else {
+			// replace attribute value by metaSheet
+			metaAttr := meta.AttrMap.List[idx]
+			metaAttr.Value = t
 		}
-		// append if user not specified
-		if noSheetByUser {
-			sheetNames = append(sheetNames, sheetName)
+	}
+	for n := curr.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type != xmlquery.ElementNode {
+			continue
+		}
+		childName := n.Data
+		if _, ok := meta.ChildMap[childName]; !ok {
+			newNode := newNode(childName, meta)
+			meta.ChildMap[childName] = &tableaupb.XMLNode_IndexList{
+				Indexes: []int32{int32(len(meta.ChildList))},
+			}
+			meta.ChildList = append(meta.ChildList, newNode)
+			registerMetaNode(xmlSheet, newNode)
+		}
+		if err := parseMetaNode(n, xmlSheet); err != nil {
+			return errors.Wrapf(err, "failed to parseMetaNode for %s@%s", childName, meta.Name)
 		}
 	}
-	atom.Log.Debug(sheetNames)
+	return nil
+}
 
-	// parse data sheets
-	noSheetInMeta := len(sheetNames) == 0
-	p, err = xmlquery.CreateStreamParser(strings.NewReader(string(buf)), "/")
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to create stream parser from string %s", metaDoc)
+// parseDataNode parse xml node `curr`, complete the meta tree and fill the data into `xmlSheet`.
+func parseDataNode(curr *xmlquery.Node, xmlSheet *tableaupb.XMLSheet) error {
+	_, path := getNodePath(curr)
+	meta := xmlSheet.MetaNodeMap[path]
+	data_nodes := xmlSheet.DataNodeMap[path].Nodes
+	data := data_nodes[len(data_nodes)-1]
+	for _, attr := range curr.Attr {
+		attrName := attr.Name.Local
+		t := inferType(attr.Value)
+		if _, ok := meta.AttrMap.Map[attrName]; !ok {
+			meta.AttrMap.Map[attrName] = int32(len(meta.AttrMap.List))
+			meta.AttrMap.List = append(meta.AttrMap.List, &tableaupb.XMLNode_AttrMap_Attr{
+				Name:  attrName,
+				Value: t,
+			})
+		}
+		data.AttrMap.Map[attrName] = int32(len(data.AttrMap.List))
+		data.AttrMap.List = append(data.AttrMap.List, &tableaupb.XMLNode_AttrMap_Attr{
+			Name:  attrName,
+			Value: attr.Value,
+		})
 	}
-	for {
-		n, err := p.Read()
-		if err == io.EOF {
-			break
+	for n := curr.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type != xmlquery.ElementNode {
+			continue
 		}
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to read from stream parser")
+		childName := n.Data
+		dataChild := newNode(childName, data)
+		registerDataNode(xmlSheet, dataChild)
+		if _, ok := meta.ChildMap[childName]; !ok {
+			newNode := newNode(childName, meta)
+			meta.ChildMap[childName] = &tableaupb.XMLNode_IndexList{
+				Indexes: []int32{int32(len(meta.ChildList))},
+			}
+			meta.ChildList = append(meta.ChildList, newNode)
+			registerMetaNode(xmlSheet, newNode)
 		}
-		nav := xmlquery.CreateXPathNavigator(n)
-		sheetName := nav.LocalName()
-		// add new sheet if not exist
-		if _, ok := sheetMap[sheetName]; !ok {
-			sheetMap[sheetName] = &SheetCache{xlsxgen.NewMetaSheet(sheetName, header, false), make(map[string]Range)}
+		if err := parseDataNode(n, xmlSheet); err != nil {
+			return errors.Wrapf(err, "failed to parseDataNode for %s@%s", childName, meta.Name)
 		}
-		if err := firstParseSheet(nav, sheetMap[sheetName].metaSheet, sheetMap[sheetName].prefixMap, false); err != nil {
-			return nil, errors.Wrapf(err, "failed to parse sheet: %s#%s", filename, sheetName)
+		if list, ok := data.ChildMap[childName]; !ok {
+			data.ChildMap[childName] = &tableaupb.XMLNode_IndexList{
+				Indexes: []int32{int32(len(data.ChildList))},
+			}
+		} else {
+			list.Indexes = append(list.Indexes, int32(len(data.ChildList)))
 		}
-		// generate all if metaSheet not specified
-		if noSheetInMeta {
-			sheetNames = append(sheetNames, sheetName)
+		data.ChildList = append(data.ChildList, dataChild)
+	}
+	return nil
+}
+
+// escapeMetaDoc escape characters for all attribute values in the document. e.g.:
+//
+//  <ServerConf key="map<uint32,ServerConf>" Open="bool">
+// 	 ...
+//  </ServerConf>
+//
+// will be converted to
+//
+//  <ServerConf key="map&lt;uint32,ServerConf&gt;" Open="bool">
+// 	 ...
+//  </ServerConf>
+func escapeAttrs(doc string) string {
+	escapedDoc := attrRegexp.ReplaceAllStringFunc(doc, func(s string) string {
+		matches := matchAttr(s)
+		var typeBuf, propBuf bytes.Buffer
+		xml.EscapeText(&typeBuf, []byte(matches[2]))
+		xml.EscapeText(&propBuf, []byte(matches[3]))
+		return fmt.Sprintf("%s=\"%s%s\"", matches[1], typeBuf.String(), propBuf.String())
+	})
+	return escapedDoc
+}
+
+// getNodePath get the root and the path walking from root to `curr` in the tree.
+func getNodePath(curr *xmlquery.Node) (root *xmlquery.Node, path string) {
+	path = curr.Data
+	for n := curr.Parent; n != nil; n = n.Parent {
+		if n.Data == "" {
+			root = n
+		} else {
+			path = fmt.Sprintf("%s/%s", n.Data, path)
+
 		}
 	}
-	atom.Log.Debug(sheetNames)
+	return root, path
+}
 
-	//------------------------------ The second pass ------------------------------//
-	// only parse data sheets
-	bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
-	newBook := book.NewBook(bookName, filename, nil)
-	p, _ = xmlquery.CreateStreamParser(strings.NewReader(string(buf)), "/")
-	for {
-		n, err := p.Read()
-		if err == io.EOF {
+// inferType infer type from the node value, e.g.: 
+// - 4324342: `int32`
+// - 4324324324324343243432: `int64`
+// - 4535ffdr43t3r: `string`
+func inferType(value string) string {
+	if _, err := strconv.Atoi(value); err == nil {
+		return "int32"
+	} else if _, err := strconv.ParseInt(value, 10, 64); err == nil {
+		return "int64"
+	} else {
+		return "string"
+	}
+}
+
+func matchAttr(s string) []string {
+	return attrRegexp.FindStringSubmatch(s)
+}
+
+func matchScalarList(s string) []string {
+	return scalarListRegexp.FindStringSubmatch(s)
+}
+
+func newOrderedAttrMap() *tableaupb.XMLNode_AttrMap {
+	return &tableaupb.XMLNode_AttrMap{
+		Map: make(map[string]int32),
+	}
+}
+
+func newNode(nodeName string, parent *tableaupb.XMLNode) *tableaupb.XMLNode {
+	node := &tableaupb.XMLNode{
+		Name:     nodeName,
+		AttrMap:  newOrderedAttrMap(),
+		ChildMap: make(map[string]*tableaupb.XMLNode_IndexList),
+		Parent:   parent,
+	}
+	if parent != nil {
+		node.Path = fmt.Sprintf("%s/%s", parent.Path, nodeName)
+	} else {
+		node.Path = nodeName
+	}
+
+	return node
+}
+
+func registerMetaNode(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode) {
+	if _, ok := xmlSheet.MetaNodeMap[node.Path]; !ok {
+		xmlSheet.MetaNodeMap[node.Path] = node
+	} else {
+		atom.Log.Panicf("duplicated path registered in MetaNodeMap|Path:%s", node.Path)
+	}
+}
+
+func registerDataNode(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode) {
+	if list, ok := xmlSheet.DataNodeMap[node.Path]; !ok {
+		xmlSheet.DataNodeMap[node.Path] = &tableaupb.XMLSheet_NodeList{
+			Nodes: []*tableaupb.XMLNode{node},
+		}
+	} else {
+		list.Nodes = append(list.Nodes, node)
+	}
+}
+
+func newXMLSheet(sheetName string) *tableaupb.XMLSheet {
+	return &tableaupb.XMLSheet{
+		Meta:        newNode(sheetName, nil),
+		Data:        newNode(sheetName, nil),
+		MetaNodeMap: make(map[string]*tableaupb.XMLNode),
+		DataNodeMap: make(map[string]*tableaupb.XMLSheet_NodeList),
+	}
+}
+
+func getXMLSheet(xmlMeta *tableaupb.XMLBook, sheetName string) *tableaupb.XMLSheet {
+	if idx, ok := xmlMeta.SheetMap[sheetName]; !ok {
+		xmlSheet := newXMLSheet(sheetName)
+		registerMetaNode(xmlSheet, xmlSheet.Meta)
+		registerDataNode(xmlSheet, xmlSheet.Data)
+		xmlMeta.SheetMap[sheetName] = int32(len(xmlMeta.SheetList))
+		xmlMeta.SheetList = append(xmlMeta.SheetList, xmlSheet)
+		return xmlSheet
+	} else {
+		return xmlMeta.SheetList[idx]
+	}
+}
+
+// --------------------------------------------- THE SECOND PASS ------------------------------------ //
+// The second pass preprocess the tree structure. In this phase the parser will do some necessary jobs 
+// before generating a 2-dimensional sheet, like correctType which make the types of attributes in the 
+// nodes meet the requirements of protogen.
+//
+func preprocess(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode) error {
+	// rearrange attributes
+	if err := rearrangeAttrs(node.AttrMap); err != nil {
+		return errors.Wrapf(err, "failed to rearrangeAttrs")
+	}
+	// fix node types when it is the first attribute
+	for i, attr := range node.AttrMap.List {
+		if i == 0 {
+			attr.Value = fixNodeType(xmlSheet, node, attr.Value)
+		}
+	}
+
+	// recursively preprocess
+	for _, child := range node.ChildList {
+		if err := preprocess(xmlSheet, child); err != nil {
+			return errors.Wrapf(err, "failed to preprocess node:%s", child.Name)
+		}
+	}
+	return nil
+}
+
+// rearrangeAttrs change the order of attributes, e.g.: 
+// - attributes with cross-type types, such as cross-cell map (list, keyed-list, etc.), 
+//   will be placed at the first.
+// - simple list like `Param1, Param2, Param3, ...` will be grouped together and 
+//   the type of `Param1` will be changed to something like `[]int32`.
+func rearrangeAttrs(attrMap *tableaupb.XMLNode_AttrMap) error {
+	typeMap := make(map[string]string)
+	indexMap := make(map[int]int)
+	for i, attr := range attrMap.List {
+		mustFirst := isCrossCell(attr.Value)
+		if mustFirst {
+			swapAttr(attrMap, i, 0)
+			typeMap[attr.Name] = attr.Value
+			continue
+		}
+		matches := matchScalarList(attr.Name)
+		if len(matches) > 0 && types.IsScalarType(attr.Value) {
+			num, err := strconv.Atoi(matches[2])
+			if err != nil {
+				atom.Log.Errorf("strconv.Atoi failed|attr:%s|num:%s|err:%s", attr.Name, matches[2], err)
+				continue
+			}
+			indexMap[num] = i
+		}
+	}
+	// start with 1, e.g.: Param1, Param2, ...
+	for i, dst := 1, len(attrMap.List)-len(indexMap); ; i, dst = i+1, dst+1 {
+		index, ok := indexMap[i]
+		if !ok {
 			break
 		}
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to read from stream parser")
+		if i == 1 {
+			attr := attrMap.List[index]
+			attr.Value = fmt.Sprintf("[]%s", attr.Value)
 		}
-		nav := xmlquery.CreateXPathNavigator(n)
-		sheet, err := secondParseSheet(nav, sheetMap[nav.LocalName()].metaSheet)
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to parse sheet: %s#%s", filename, nav.LocalName())
+		swapAttr(attrMap, index, dst)
+	}
+	if len(typeMap) > 1 {
+		return fmt.Errorf("more than one non-scalar types: %v", typeMap)
+	}
+	return nil
+}
+
+// fixNodeType fix the type of `curr` in the `xmlSheet` based on its `oriType`. e.g.:
+// - map<uint32,Weight>: {Test}map<uint32,Weight>
+// - int32: {StructConf}{Weight}int32
+// - []int64: {MapConf}[]<int64>
+func fixNodeType(xmlSheet *tableaupb.XMLSheet, curr *tableaupb.XMLNode, oriType string) (t string) {
+	t = oriType
+	if types.IsList(t) {
+		matches := types.MatchList(t)
+		colType := strings.TrimSpace(matches[2])
+		if types.IsScalarType(colType) || types.IsEnum(colType) {
+			// list in xml must be keyed list
+			t = fmt.Sprintf("[%s]<%s>", matches[1], colType)
 		}
-		newBook.AddSheet(sheet)
 	}
-	newBook.Squeeze(sheetNames)
+	// add type prefixes
+	for n, c := curr, curr; n != nil && n.Parent != nil; n, c = n.Parent, n {
+		if n == curr {
+			// curr is cross-cell, parent should not add prefix
+			if isCrossCell(oriType) {
+				continue
+			}
+		} else {
+			// not the first attr or not the first child, fix ok
+			if len(n.AttrMap.List) > 0 || !isFirstChild(c) {
+				break
+			}
+		}
+		if isRepeated(xmlSheet, n) {
+			if n == curr {
+				t = fmt.Sprintf("[%s]<%s>", n.Name, t)
+			} else {
+				t = fmt.Sprintf("[%s]%s", n.Name, t)
+			}
+		} else {
+			t = fmt.Sprintf("{%s}%s", n.Name, t)
+		}
+	}
+	return t
+}
 
-	return newBook, nil
+// isRepeated check if `curr` has other sibling nodes with the same name with itself.
+func isRepeated(xmlSheet *tableaupb.XMLSheet, curr *tableaupb.XMLNode) bool {
+	strList := strings.Split(curr.Path, "/")
+	parentPath := strings.Join(strList[:len(strList)-1], "/")
+	if nodes, ok := xmlSheet.DataNodeMap[parentPath]; ok {
+		for _, n := range nodes.Nodes {
+			if indexes, ok := n.ChildMap[curr.Name]; ok && len(indexes.Indexes) > 1 {
+				return true
+			}
+		}
+	}
+	return false
 }
 
-// firstParseSheet do the jobs of first pass of the XML parser. To be more specified, recursively explore the document rooted by `root`
-// and fill the header (first 2 rows) of `metaSheet`, which can fully describe the structure of document.
-func firstParseSheet(root *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet, prefixMap map[string]Range, isMeta bool) error {
-	if err := parseNodeType(root, metaSheet, prefixMap, isMeta, true); err != nil {
-		return errors.Wrapf(err, "failed to parseNodeType for root node %s", metaSheet.Worksheet)
+// isCrossCell check if type string `t` is a cross-cell type.
+func isCrossCell(t string) bool {
+	if types.IsMap(t) { // map case
+		matches := types.MatchMap(t)
+		valueType := strings.TrimSpace(matches[2])
+		return !(types.IsScalarType(valueType) || types.IsEnum(valueType))
+	} else if types.IsList(t) { // list case
+		matches := types.MatchList(t)
+		structType := strings.TrimSpace(matches[1])
+		return structType != ""
+	} else if types.IsKeyedList(t) { // keyed-list case
+		matches := types.MatchKeyedList(t)
+		structType := strings.TrimSpace(matches[1])
+		return structType != ""
+	} else if types.IsStruct(t) { // struct case
+		matches := types.MatchStruct(t)
+		colType := strings.TrimSpace(matches[2])
+		return types.IsScalarType(colType) || types.IsEnum(colType)
 	}
-	atom.Log.Debug(metaSheet)
-	return nil
+	return false
+}
+
+// isFirstChild check if `node` is the first child of its parent node.
+func isFirstChild(node *tableaupb.XMLNode) bool {
+	if node.Parent == nil {
+		return false
+	}
+	return node.Parent.ChildList[0] == node
 }
 
-// secondParseSheet proceed filling the data rows (begins from 5th row) of `metaSheet` on the basis of `firstParseSheet` and
-// return a 2-dimensional book in TABLEAU grammar, which can fully describe both the structure and data of the document.
-func secondParseSheet(root *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet) (sheet *book.Sheet, err error) {
-	if err := parseNodeData(root, metaSheet, int(metaSheet.Datarow)-1); err != nil {
-		return nil, errors.Wrapf(err, "failed to parseNodeData for root node %s", metaSheet.Worksheet)
+func swapAttr(attrMap *tableaupb.XMLNode_AttrMap, i, j int) {
+	attr := attrMap.List[i]
+	attrMap.Map[attr.Name] = int32(j)
+	attrMap.Map[attrMap.List[j].Name] = int32(i)
+	attrMap.List[i] = attrMap.List[j]
+	attrMap.List[j] = attr
+}
+
+// --------------------------------------------- THE THIRD PASS ------------------------------------ //
+// The third pass transforms the recursive tree structure into a 2-dimensional sheet, 
+// which can be further processed into protoconf.
+//
+// genSheet generates a `book.Sheet` which can furtherly processed by `protogen`.
+func genSheet(xmlSheet *tableaupb.XMLSheet) (sheet *book.Sheet, err error) {
+	sheetName := strcase.ToCamel(xmlSheet.Meta.Name)
+	header := options.NewDefault().Input.Proto.Header
+	metaSheet := xlsxgen.NewMetaSheet(sheetName, header, false)
+	// generate sheet header rows
+	if err := genHeaderRows(xmlSheet.Meta, metaSheet, ""); err != nil {
+		return nil, errors.Wrapf(err, "failed to genHeaderRows for sheet: %s", sheetName)
+	}
+	// fill sheet data rows
+	if err := fillDataRows(xmlSheet.Data, metaSheet, "", int(metaSheet.Datarow)-1); err != nil {
+		return nil, errors.Wrapf(err, "failed to fillDataRows for sheet: %s", sheetName)
 	}
-	atom.Log.Debug(metaSheet)
-	
 	// unpack rows from the MetaSheet struct
 	var rows [][]string
 	for i := 0; i < len(metaSheet.Rows); i++ {
@@ -305,11 +571,10 @@ func secondParseSheet(root *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet
 		rows = append(rows, row)
 	}
 	// insert sheets into map for importer
-	header := options.NewDefault().Input.Proto.Header
-	sheet = book.NewSheet(metaSheet.Worksheet, rows)
+	sheet = book.NewSheet(sheetName, rows)
 	sheet.Meta = &tableaupb.SheetMeta{
-		Sheet:    metaSheet.Worksheet,
-		Alias:    metaSheet.Worksheet,
+		Sheet:    sheetName,
+		Alias:    sheetName,
 		Namerow:  header.Namerow,
 		Typerow:  header.Typerow,
 		Noterow:  header.Noterow,
@@ -321,185 +586,55 @@ func secondParseSheet(root *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet
 	return sheet, nil
 }
 
-// parseNodeType parse and convert an xml file to sheet format
-func parseNodeType(nav *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet, prefixMap map[string]Range, isMeta, isFirstChild bool) error {
-	// preprocess
-	prefix := ""
-	continueFindNude := true
-	var parentList []string	
-	var nudeParentTypeList []string
-	// construct prefix
-	for flag, navCopy := true, *nav; flag && navCopy.LocalName() != metaSheet.Worksheet; flag = navCopy.MoveToParent() {
-		if prefix != "" && continueFindNude {
-			if len(navCopy.Current().Attr) > 0 {
-				continueFindNude = false
-			} else {
-				t := fmt.Sprintf("{%s}", navCopy.LocalName())
-				if navCopy.Current().Parent != nil && len(xmlquery.Find(navCopy.Current().Parent, navCopy.LocalName())) > 1 {
-					t = fmt.Sprintf("[%s]", navCopy.LocalName())
-				}
-				nudeParentTypeList = append(nudeParentTypeList, t)				
-			}
-		}
-		prefix = strcase.ToCamel(navCopy.LocalName()) + prefix
-		parentList = append(parentList, navCopy.LocalName())
+// genHeaderRows recursively read meta info from `node` and generates the header rows of `metaSheet`, which is a 2-dimensional IR.
+func genHeaderRows(node *tableaupb.XMLNode, metaSheet *xlsxgen.MetaSheet, prefix string) error {
+	curPrefix := newPrefix(prefix, node.Name, metaSheet.Worksheet)
+	for _, attr := range node.AttrMap.List {
+		metaSheet.SetColType(curPrefix+strcase.ToCamel(attr.Name), attr.Value)
 	}
-	repeated := len(xmlquery.Find(nav.Current().Parent, nav.LocalName())) > 1
-
-	// iterate over attributes
-	for i, attr := range nav.Current().Attr {
-		attrName := attr.Name.Local
-		attrValue := attr.Value
-		_, prefixExist := prefixMap[prefix]
-		tryAddCol(metaSheet, parentList, prefixMap, strcase.ToCamel(attrName))
-
-		t, d := inferType(attrValue)
-		colName := prefix + strcase.ToCamel(attrName)
-		metaSheet.SetDefaultValue(colName, d)
-		if isMeta {
-			if index := strings.Index(attrValue, "|"); index > 0 {
-				t = attrValue[:index]
-				metaSheet.SetDefaultValue(colName, attrValue[index+1:])
-			} else {
-				t = attrValue
-			}
-		}
-
-		atom.Log.Debug(t)		
-		curType := metaSheet.GetColType(colName)
-		matches := types.MatchStruct(curType)
-		// 1. <TABLEAU>
-		// 2. type not set
-		// 3. {Type}int32 -> [Type]int32 (when mistaken it as a struct at first but discover multiple elements later)
-		// NOTE: Map in struct not supported temporarily.
-		needChangeType := isMeta || curType == "" || (len(matches) > 0 && repeated)
-		// 1. new struct(list), not subsequent
-		// 2. {Type}int32 -> [Type]int32 (when mistaken it as a struct at first but discover multiple elements later)
-		setKeyedType := (!prefixExist || (len(matches) > 0 && repeated)) && nav.LocalName() != metaSheet.Worksheet
-		if needChangeType {
-			typePrefix :=  ""
-			for _, parentType := range nudeParentTypeList {
-				typePrefix = parentType + typePrefix
-			}
-			atom.Log.Debug(typePrefix)
-			if matches := types.MatchMap(t); len(matches) >= 3 {
-				t = typePrefix + t
-				// case 1: map<uint32,Type>
-				if !types.IsScalarType(matches[1]) && len(types.MatchEnum(matches[1])) == 0 {
-					return errors.Errorf("%s is not scalar type in node %s attr %s type %s", matches[1], nav.LocalName(), attrName, t)
-				}
-				if strings.TrimSpace(matches[2]) != nav.LocalName() {
-					return errors.Errorf("%s in attr %s type %s must be the same as node name %s", matches[2], attrName, t, nav.LocalName())
-				}
-				metaSheet.SetColType(colName, t)
-			} else if matches := types.MatchKeyedList(t); len(matches) >= 3 {
-				t = typePrefix + t
-				// case 2: [Type]<uint32>
-				if i != 0 {
-					return errors.Errorf("KeyedList attr %s in node %s must be the first attr", attrName, nav.LocalName())
-				}
-				if !types.IsScalarType(matches[2]) && len(types.MatchEnum(matches[2])) == 0 {
-					return errors.Errorf("%s is not scalar type in node %s attr %s type %s", matches[2], nav.LocalName(), attrName, t)
-				}
-				if strings.TrimSpace(matches[1]) != nav.LocalName() {
-					return errors.Errorf("%s in attr %s type %s must be the same as node name %s", matches[1], attrName, t, nav.LocalName())
-				}
-				metaSheet.SetColType(colName, t)
-			} else if matches := types.MatchList(t); len(matches) >= 3 {
-				t = typePrefix + t
-				// case 3: [Type]uint32
-				if i != 0 {
-					return errors.Errorf("KeyedList attr %s in node %s must be the first attr", attrName, nav.LocalName())
-				}
-				if !types.IsScalarType(matches[2]) && len(types.MatchEnum(matches[2])) == 0 {
-					return errors.Errorf("%s is not scalar type in node %s attr %s type %s", matches[2], nav.LocalName(), attrName, t)
-				}
-				if strings.TrimSpace(matches[1]) != nav.LocalName() {
-					return errors.Errorf("%s in attr %s type %s must be the same as node name %s", matches[1], attrName, t, nav.LocalName())
-				}
-				metaSheet.SetColType(colName, t)
-			} else if i == 0 && setKeyedType {				
-				// case 4: {Type}uint32
-				if repeated {
-					metaSheet.SetColType(colName, fmt.Sprintf("%s[%s]<%s>", typePrefix, strcase.ToCamel(nav.LocalName()), t))
-				} else {
-					metaSheet.SetColType(colName, fmt.Sprintf("%s{%s}%s", typePrefix, strcase.ToCamel(nav.LocalName()), t))
-				}
-			} else {				
-				// default: built-in type
-				metaSheet.SetColType(colName, t)
-			}
+	for _, child := range node.ChildList {
+		if err := genHeaderRows(child, metaSheet, curPrefix); err != nil {
+			return errors.Wrapf(err, "failed to genHeaderRows for %s@%s", child.Name, curPrefix)
 		}
 	}
-
-	// iterate over child nodes
-	navCopy := *nav
-	for flag, i := navCopy.MoveToChild(), 0; flag; flag = navCopy.MoveToNext() {
-		// commentNode, documentNode and other meaningless nodes should be filtered
-		if navCopy.NodeType() != xpath.ElementNode {
-			continue
-		}
-		tagName := navCopy.LocalName()
-		if err := parseNodeType(&navCopy, metaSheet, prefixMap, isMeta, i == 0); err != nil {
-			return errors.Wrapf(err, "failed to parseNodeType for the node %s", tagName)
-		}
-		i++
-	}
-
 	return nil
 }
 
-// parseNodeData parse and convert an xml file to sheet format
-func parseNodeData(nav *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet, cursor int) error {
-	// preprocess
-	prefix := ""
-	// construct prefix
-	for flag, navCopy := true, *nav; flag && navCopy.LocalName() != metaSheet.Worksheet; flag = navCopy.MoveToParent() {
-		prefix = strcase.ToCamel(navCopy.LocalName()) + prefix
-	}
-
-	// clear to the bottom
-	if navCopy := *nav; !navCopy.MoveToChild() {
+// fillDataRows recursively read data from `node` and fill them to the data rows of `metaSheet`, which is a 2-dimensional IR.
+func fillDataRows(node *tableaupb.XMLNode, metaSheet *xlsxgen.MetaSheet, prefix string, cursor int) error {
+	curPrefix := newPrefix(prefix, node.Name, metaSheet.Worksheet)
+	// clear to the bottom, since `metaSheet.NewRow()` will copy all data of all columns to create a new row
+	if len(node.ChildList) == 0 {
 		for tmpCusor := cursor; tmpCusor < len(metaSheet.Rows); tmpCusor++ {
 			metaSheet.ForEachCol(tmpCusor, func(name string, cell *xlsxgen.Cell) error {
-				if strings.HasPrefix(name, prefix) {
-					cell.Data = metaSheet.GetDefaultValue(name)
+				if strings.HasPrefix(name, curPrefix) {
+					cell.Data = ""
 				}
 				return nil
 			})
 		}
 	}
-
-	// iterate over attributes
-	for _, attr := range nav.Current().Attr {
-		attrName := attr.Name.Local
-		attrValue := attr.Value
-		colName := prefix + strcase.ToCamel(attrName)
+	for _, attr := range node.AttrMap.List {
+		colName := curPrefix + strcase.ToCamel(attr.Name)
 		// fill values to the bottom when backtrace to top line
 		for tmpCusor := cursor; tmpCusor < len(metaSheet.Rows); tmpCusor++ {
-			metaSheet.Cell(tmpCusor, len(metaSheet.Rows[metaSheet.Namerow-1].Cells), colName).Data = attrValue
+			metaSheet.Cell(tmpCusor, len(metaSheet.Rows[metaSheet.Namerow-1].Cells), colName).Data = attr.Value
 		}
 	}
-
 	// iterate over child nodes
 	nodeMap := make(map[string]int)
-	navCopy := *nav
-	for flag := navCopy.MoveToChild(); flag; flag = navCopy.MoveToNext() {
-		// commentNode, documentNode and other meaningless nodes should be filtered
-		if navCopy.NodeType() != xpath.ElementNode {
-			continue
-		}
-		tagName := navCopy.LocalName()
+	for _, child := range node.ChildList {
+		tagName := child.Name
 		if count, existed := nodeMap[tagName]; existed {
 			// duplicate means a list, should expand vertically
 			row := metaSheet.NewRow()
-			if err := parseNodeData(&navCopy, metaSheet, row.Index); err != nil {
-				return errors.Wrapf(err, "parseNodeData for node %s (index:%d) failed", tagName, count+1)
+			if err := fillDataRows(child, metaSheet, curPrefix, row.Index); err != nil {
+				return errors.Wrapf(err, "fillDataRows %dth node %s@%s failed", count+1, tagName, curPrefix)
 			}
 			nodeMap[tagName]++
 		} else {
-			if err := parseNodeData(&navCopy, metaSheet, cursor); err != nil {
-				return errors.Wrapf(err, "parseNodeData for the first node %s failed", tagName)
+			if err := fillDataRows(child, metaSheet, curPrefix, cursor); err != nil {
+				return errors.Wrapf(err, "fillDataRows 1st node %s@%s failed", tagName, curPrefix)
 			}
 			nodeMap[tagName] = 1
 		}
@@ -508,61 +643,11 @@ func parseNodeData(nav *xmlquery.NodeNavigator, metaSheet *xlsxgen.MetaSheet, cu
 	return nil
 }
 
-// tryAddCol add a new column named `name` to an appropriate location in metaSheet if not exists or do nothing otherwise
-func tryAddCol(metaSheet *xlsxgen.MetaSheet, parentList []string, prefixMap map[string]Range, name string) {
-	prefix := ""
-	var reversedList []string
-	parentMap := make(map[string]bool)
-	for i := len(parentList) - 1; i >= 0; i-- {
-		prefix += parentList[i]
-		parentMap[prefix] = true
-		if i > 0 {
-			reversedList = append(reversedList, prefix)
-		}
-	}
-	colName := prefix + name
-	if metaSheet.HasCol(colName) {
-		return
-	}
-	shift := func(r Range) {
-		for i := 0; i < len(reversedList); i++ {
-			if r, ok := prefixMap[reversedList[i]]; ok {
-				prefixMap[reversedList[i]] = Range{r.begin, r.attrNum, r.len + 1}
-			}
-		}
-		for k, v := range prefixMap {
-			if _, ok := parentMap[k]; !ok && v.begin > r.begin {
-				prefixMap[k] = Range{v.begin + 1, v.attrNum, v.len}
-			}
-		}
-	}
-	// insert prefixMap
-	if r, ok := prefixMap[prefix]; !ok {
-		index := len(metaSheet.Rows[metaSheet.Namerow-1].Cells)
-		if len(reversedList) > 0 {
-			parentPrefix := reversedList[len(reversedList)-1]
-			if r2, ok := prefixMap[parentPrefix]; ok {
-				index = r2.begin + r2.len
-			}
-		}
-		prefixMap[prefix] = Range{index, 1, 1}
-		shift(prefixMap[prefix])
-		metaSheet.Cell(int(metaSheet.Namerow)-1, prefixMap[prefix].begin, colName).Data = colName
-	} else {
-		prefixMap[prefix] = Range{r.begin, r.attrNum + 1, r.len + 1}
-		shift(prefixMap[prefix])
-		metaSheet.Cell(int(metaSheet.Namerow)-1, r.begin+r.attrNum, colName).Data = colName
-	}
-}
-
-func inferType(value string) (string, string) {
-	var t, d string
-	if _, err := strconv.Atoi(value); err == nil {
-		t, d = "int32", "0"
-	} else if _, err := strconv.ParseInt(value, 10, 64); err == nil {
-		t, d = "int64", "0"
+func newPrefix(prefix, curNode, sheetName string) string {
+	// sheet name should not occur in the prefix
+	if strcase.ToCamel(curNode) != sheetName {
+		return prefix + strcase.ToCamel(curNode)
 	} else {
-		t, d = "string", ""
+		return prefix
 	}
-	return t, d
 }
diff --git a/internal/types/types.go b/internal/types/types.go
index 2525de4..44acf27 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -25,7 +25,7 @@ const typeCharSet = `[0-9A-Za-z,_>< \[\]\.\{\}]`
 const typeGroup = `(` + typeCharSet + `+)`
 const looseTypeGroup = typeGroup + `?` // `x?`: zero or one x, prefer one
 const ungreedyTypeGroup = `(` + typeCharSet + `*?)`
-const PubTypeGroup = typeGroup
+const TypeGroup = ungreedyTypeGroup
 
 func init() {
 	mapRegexp = regexp.MustCompile(`^map<` + typeGroup + `,` + typeGroup + `>` + rawPropGroup)               // e.g.: map<uint32,Type>
diff --git a/internal/xproto/value.go b/internal/xproto/value.go
index ae4a0cd..2d46ed3 100644
--- a/internal/xproto/value.go
+++ b/internal/xproto/value.go
@@ -58,6 +58,12 @@ func ParseFieldValue(fd pref.FieldDescriptor, rawValue string, locationName stri
 	}
 
 	value := strings.TrimSpace(rawValue)
+	opts := fd.Options().(*descriptorpb.FieldOptions)
+	fieldOpts := proto.GetExtension(opts, tableaupb.E_Field).(*tableaupb.FieldOptions)
+	if value == "" && fieldOpts != nil && fieldOpts.Prop != nil {
+		value = fieldOpts.Prop.Default
+	}
+
 	switch fd.Kind() {
 	case pref.Int32Kind, pref.Sint32Kind, pref.Sfixed32Kind:
 		if value == "" {
diff --git a/proto/tableau/protobuf/xml.proto b/proto/tableau/protobuf/xml.proto
new file mode 100644
index 0000000..0cfb23d
--- /dev/null
+++ b/proto/tableau/protobuf/xml.proto
@@ -0,0 +1,60 @@
+syntax = "proto3";
+package tableau;
+
+option go_package = "github.com/tableauio/tableau/proto/tableaupb";
+
+message XMLBook {
+  // SheetMap maps sheet name to a XMLSheet struct 
+  // containing two trees which describe the XML structure
+  repeated XMLSheet sheet_list = 1;
+  map<string, int32> sheet_map = 2;
+}
+
+message XMLSheet {  
+  // meta tree, describing the types of attributes of all nodes in XML
+  XMLNode meta = 1;
+
+  // data tree, describing the values of attributes of the explicitly specified nodes in XML
+  XMLNode data = 2;
+
+  // NodeMap maps path to a list of nodes matched the specified path in the tree
+  map<string, XMLNode> meta_node_map = 3;
+  map<string, NodeList> data_node_map = 4;
+
+  message NodeList {
+    repeated XMLNode nodes = 1;
+  }
+}
+
+message XMLNode {
+  string name = 1;
+
+  // duplicated attributes in one node are forbidden
+  AttrMap attr_map = 2;
+
+  // - meta: Each child must be unique, so len(IndexList) must be 1 
+  // - data: Duplicated children are allowed, so len(IndexList) could be greater than 1
+  repeated XMLNode child_list = 3;
+  map<string, IndexList> child_map = 4;
+
+  // record parent node so that we can trace back
+  XMLNode parent = 5;
+
+  // path that walks from root to self node, e.g.: Conf/Server/Toggle
+  string path = 6;
+
+  message IndexList {
+    repeated int32 indexes = 1;
+  }
+  
+  // Combine list and map to get an ordered map
+  message AttrMap {
+    repeated Attr list = 1;
+    map<string, int32> map = 2;
+    
+    message Attr {
+      string name = 1;
+      string value = 2;
+    }
+  }  
+}
diff --git a/proto/tableaupb/xml.pb.go b/proto/tableaupb/xml.pb.go
new file mode 100644
index 0000000..120bde1
--- /dev/null
+++ b/proto/tableaupb/xml.pb.go
@@ -0,0 +1,700 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.27.1
+// 	protoc        v3.17.3
+// source: tableau/protobuf/xml.proto
+
+package tableaupb
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type XMLBook struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// SheetMap maps sheet name to a XMLSheet struct
+	// containing two trees which describe the XML structure
+	SheetList []*XMLSheet      `protobuf:"bytes,1,rep,name=sheet_list,json=sheetList,proto3" json:"sheet_list,omitempty"`
+	SheetMap  map[string]int32 `protobuf:"bytes,2,rep,name=sheet_map,json=sheetMap,proto3" json:"sheet_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
+}
+
+func (x *XMLBook) Reset() {
+	*x = XMLBook{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLBook) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLBook) ProtoMessage() {}
+
+func (x *XMLBook) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLBook.ProtoReflect.Descriptor instead.
+func (*XMLBook) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *XMLBook) GetSheetList() []*XMLSheet {
+	if x != nil {
+		return x.SheetList
+	}
+	return nil
+}
+
+func (x *XMLBook) GetSheetMap() map[string]int32 {
+	if x != nil {
+		return x.SheetMap
+	}
+	return nil
+}
+
+type XMLSheet struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// meta tree, describing the types of attributes of all nodes in XML
+	Meta *XMLNode `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
+	// data tree, describing the values of attributes of the explicitly specified nodes in XML
+	Data *XMLNode `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
+	// NodeMap maps path to a list of nodes matched the specified path in the tree
+	MetaNodeMap map[string]*XMLNode           `protobuf:"bytes,3,rep,name=meta_node_map,json=metaNodeMap,proto3" json:"meta_node_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	DataNodeMap map[string]*XMLSheet_NodeList `protobuf:"bytes,4,rep,name=data_node_map,json=dataNodeMap,proto3" json:"data_node_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+}
+
+func (x *XMLSheet) Reset() {
+	*x = XMLSheet{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLSheet) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLSheet) ProtoMessage() {}
+
+func (x *XMLSheet) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLSheet.ProtoReflect.Descriptor instead.
+func (*XMLSheet) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *XMLSheet) GetMeta() *XMLNode {
+	if x != nil {
+		return x.Meta
+	}
+	return nil
+}
+
+func (x *XMLSheet) GetData() *XMLNode {
+	if x != nil {
+		return x.Data
+	}
+	return nil
+}
+
+func (x *XMLSheet) GetMetaNodeMap() map[string]*XMLNode {
+	if x != nil {
+		return x.MetaNodeMap
+	}
+	return nil
+}
+
+func (x *XMLSheet) GetDataNodeMap() map[string]*XMLSheet_NodeList {
+	if x != nil {
+		return x.DataNodeMap
+	}
+	return nil
+}
+
+type XMLNode struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// duplicated attributes in one node are forbidden
+	AttrMap *XMLNode_AttrMap `protobuf:"bytes,2,opt,name=attr_map,json=attrMap,proto3" json:"attr_map,omitempty"`
+	// - meta: Each child must be unique, so len(IndexList) must be 1
+	// - data: Duplicated children are allowed, so len(IndexList) could be greater than 1
+	ChildList []*XMLNode                    `protobuf:"bytes,3,rep,name=child_list,json=childList,proto3" json:"child_list,omitempty"`
+	ChildMap  map[string]*XMLNode_IndexList `protobuf:"bytes,4,rep,name=child_map,json=childMap,proto3" json:"child_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// record parent node so that we can trace back
+	Parent *XMLNode `protobuf:"bytes,5,opt,name=parent,proto3" json:"parent,omitempty"`
+	// path that walks from root to self node, e.g.: Conf/Server/Toggle
+	Path string `protobuf:"bytes,6,opt,name=path,proto3" json:"path,omitempty"`
+}
+
+func (x *XMLNode) Reset() {
+	*x = XMLNode{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLNode) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLNode) ProtoMessage() {}
+
+func (x *XMLNode) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLNode.ProtoReflect.Descriptor instead.
+func (*XMLNode) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *XMLNode) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *XMLNode) GetAttrMap() *XMLNode_AttrMap {
+	if x != nil {
+		return x.AttrMap
+	}
+	return nil
+}
+
+func (x *XMLNode) GetChildList() []*XMLNode {
+	if x != nil {
+		return x.ChildList
+	}
+	return nil
+}
+
+func (x *XMLNode) GetChildMap() map[string]*XMLNode_IndexList {
+	if x != nil {
+		return x.ChildMap
+	}
+	return nil
+}
+
+func (x *XMLNode) GetParent() *XMLNode {
+	if x != nil {
+		return x.Parent
+	}
+	return nil
+}
+
+func (x *XMLNode) GetPath() string {
+	if x != nil {
+		return x.Path
+	}
+	return ""
+}
+
+type XMLSheet_NodeList struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Nodes []*XMLNode `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"`
+}
+
+func (x *XMLSheet_NodeList) Reset() {
+	*x = XMLSheet_NodeList{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLSheet_NodeList) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLSheet_NodeList) ProtoMessage() {}
+
+func (x *XMLSheet_NodeList) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[6]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLSheet_NodeList.ProtoReflect.Descriptor instead.
+func (*XMLSheet_NodeList) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{1, 2}
+}
+
+func (x *XMLSheet_NodeList) GetNodes() []*XMLNode {
+	if x != nil {
+		return x.Nodes
+	}
+	return nil
+}
+
+type XMLNode_IndexList struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Indexes []int32 `protobuf:"varint,1,rep,packed,name=indexes,proto3" json:"indexes,omitempty"`
+}
+
+func (x *XMLNode_IndexList) Reset() {
+	*x = XMLNode_IndexList{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLNode_IndexList) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLNode_IndexList) ProtoMessage() {}
+
+func (x *XMLNode_IndexList) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[8]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLNode_IndexList.ProtoReflect.Descriptor instead.
+func (*XMLNode_IndexList) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{2, 1}
+}
+
+func (x *XMLNode_IndexList) GetIndexes() []int32 {
+	if x != nil {
+		return x.Indexes
+	}
+	return nil
+}
+
+// Combine list and map to get an ordered map
+type XMLNode_AttrMap struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	List []*XMLNode_AttrMap_Attr `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"`
+	Map  map[string]int32        `protobuf:"bytes,2,rep,name=map,proto3" json:"map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
+}
+
+func (x *XMLNode_AttrMap) Reset() {
+	*x = XMLNode_AttrMap{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLNode_AttrMap) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLNode_AttrMap) ProtoMessage() {}
+
+func (x *XMLNode_AttrMap) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[9]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLNode_AttrMap.ProtoReflect.Descriptor instead.
+func (*XMLNode_AttrMap) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{2, 2}
+}
+
+func (x *XMLNode_AttrMap) GetList() []*XMLNode_AttrMap_Attr {
+	if x != nil {
+		return x.List
+	}
+	return nil
+}
+
+func (x *XMLNode_AttrMap) GetMap() map[string]int32 {
+	if x != nil {
+		return x.Map
+	}
+	return nil
+}
+
+type XMLNode_AttrMap_Attr struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name  string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+}
+
+func (x *XMLNode_AttrMap_Attr) Reset() {
+	*x = XMLNode_AttrMap_Attr{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tableau_protobuf_xml_proto_msgTypes[11]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *XMLNode_AttrMap_Attr) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*XMLNode_AttrMap_Attr) ProtoMessage() {}
+
+func (x *XMLNode_AttrMap_Attr) ProtoReflect() protoreflect.Message {
+	mi := &file_tableau_protobuf_xml_proto_msgTypes[11]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use XMLNode_AttrMap_Attr.ProtoReflect.Descriptor instead.
+func (*XMLNode_AttrMap_Attr) Descriptor() ([]byte, []int) {
+	return file_tableau_protobuf_xml_proto_rawDescGZIP(), []int{2, 2, 1}
+}
+
+func (x *XMLNode_AttrMap_Attr) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *XMLNode_AttrMap_Attr) GetValue() string {
+	if x != nil {
+		return x.Value
+	}
+	return ""
+}
+
+var File_tableau_protobuf_xml_proto protoreflect.FileDescriptor
+
+var file_tableau_protobuf_xml_proto_rawDesc = []byte{
+	0x0a, 0x1a, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x75, 0x66, 0x2f, 0x78, 0x6d, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x74, 0x61,
+	0x62, 0x6c, 0x65, 0x61, 0x75, 0x22, 0xb5, 0x01, 0x0a, 0x07, 0x58, 0x4d, 0x4c, 0x42, 0x6f, 0x6f,
+	0x6b, 0x12, 0x30, 0x0a, 0x0a, 0x73, 0x68, 0x65, 0x65, 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e,
+	0x58, 0x4d, 0x4c, 0x53, 0x68, 0x65, 0x65, 0x74, 0x52, 0x09, 0x73, 0x68, 0x65, 0x65, 0x74, 0x4c,
+	0x69, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x09, 0x73, 0x68, 0x65, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x70,
+	0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75,
+	0x2e, 0x58, 0x4d, 0x4c, 0x42, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x68, 0x65, 0x65, 0x74, 0x4d, 0x61,
+	0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x73, 0x68, 0x65, 0x65, 0x74, 0x4d, 0x61, 0x70,
+	0x1a, 0x3b, 0x0a, 0x0d, 0x53, 0x68, 0x65, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
+	0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc8, 0x03,
+	0x0a, 0x08, 0x58, 0x4d, 0x4c, 0x53, 0x68, 0x65, 0x65, 0x74, 0x12, 0x24, 0x0a, 0x04, 0x6d, 0x65,
+	0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65,
+	0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61,
+	0x12, 0x24, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
+	0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65,
+	0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x6e,
+	0x6f, 0x64, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e,
+	0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x53, 0x68, 0x65, 0x65, 0x74,
+	0x2e, 0x4d, 0x65, 0x74, 0x61, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x52, 0x0b, 0x6d, 0x65, 0x74, 0x61, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x61, 0x70, 0x12, 0x46,
+	0x0a, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x18,
+	0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e,
+	0x58, 0x4d, 0x4c, 0x53, 0x68, 0x65, 0x65, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x4e, 0x6f, 0x64,
+	0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x4e,
+	0x6f, 0x64, 0x65, 0x4d, 0x61, 0x70, 0x1a, 0x50, 0x0a, 0x10, 0x4d, 0x65, 0x74, 0x61, 0x4e, 0x6f,
+	0x64, 0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x61,
+	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5a, 0x0a, 0x10, 0x44, 0x61, 0x74, 0x61,
+	0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
+	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30,
+	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
+	0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x53, 0x68, 0x65, 0x65, 0x74,
+	0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+	0x3a, 0x02, 0x38, 0x01, 0x1a, 0x32, 0x0a, 0x08, 0x4e, 0x6f, 0x64, 0x65, 0x4c, 0x69, 0x73, 0x74,
+	0x12, 0x26, 0x0a, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x10, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64,
+	0x65, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22, 0xdc, 0x04, 0x0a, 0x07, 0x58, 0x4d, 0x4c,
+	0x4e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x61, 0x74, 0x74, 0x72,
+	0x5f, 0x6d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x61, 0x62,
+	0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x41, 0x74, 0x74,
+	0x72, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x61, 0x74, 0x74, 0x72, 0x4d, 0x61, 0x70, 0x12, 0x2f, 0x0a,
+	0x0a, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x10, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e,
+	0x6f, 0x64, 0x65, 0x52, 0x09, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3b,
+	0x0a, 0x09, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x04, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e,
+	0x6f, 0x64, 0x65, 0x2e, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x52, 0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x4d, 0x61, 0x70, 0x12, 0x28, 0x0a, 0x06, 0x70,
+	0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x61,
+	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x70,
+	0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x1a, 0x57, 0x0a, 0x0d, 0x43, 0x68, 0x69,
+	0x6c, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x61,
+	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x49, 0x6e,
+	0x64, 0x65, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
+	0x38, 0x01, 0x1a, 0x25, 0x0a, 0x09, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x12,
+	0x18, 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x05,
+	0x52, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x1a, 0xdb, 0x01, 0x0a, 0x07, 0x41, 0x74,
+	0x74, 0x72, 0x4d, 0x61, 0x70, 0x12, 0x31, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x58, 0x4d,
+	0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x4d, 0x61, 0x70, 0x2e, 0x41, 0x74,
+	0x74, 0x72, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x03, 0x6d, 0x61, 0x70, 0x18,
+	0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e,
+	0x58, 0x4d, 0x4c, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x4d, 0x61, 0x70, 0x2e,
+	0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x6d, 0x61, 0x70, 0x1a, 0x36, 0x0a,
+	0x08, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
+	0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x30, 0x0a, 0x04, 0x41, 0x74, 0x74, 0x72, 0x12, 0x12, 0x0a,
+	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
+	0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75,
+	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x69, 0x6f, 0x2f,
+	0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x61,
+	0x62, 0x6c, 0x65, 0x61, 0x75, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_tableau_protobuf_xml_proto_rawDescOnce sync.Once
+	file_tableau_protobuf_xml_proto_rawDescData = file_tableau_protobuf_xml_proto_rawDesc
+)
+
+func file_tableau_protobuf_xml_proto_rawDescGZIP() []byte {
+	file_tableau_protobuf_xml_proto_rawDescOnce.Do(func() {
+		file_tableau_protobuf_xml_proto_rawDescData = protoimpl.X.CompressGZIP(file_tableau_protobuf_xml_proto_rawDescData)
+	})
+	return file_tableau_protobuf_xml_proto_rawDescData
+}
+
+var file_tableau_protobuf_xml_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
+var file_tableau_protobuf_xml_proto_goTypes = []interface{}{
+	(*XMLBook)(nil),              // 0: tableau.XMLBook
+	(*XMLSheet)(nil),             // 1: tableau.XMLSheet
+	(*XMLNode)(nil),              // 2: tableau.XMLNode
+	nil,                          // 3: tableau.XMLBook.SheetMapEntry
+	nil,                          // 4: tableau.XMLSheet.MetaNodeMapEntry
+	nil,                          // 5: tableau.XMLSheet.DataNodeMapEntry
+	(*XMLSheet_NodeList)(nil),    // 6: tableau.XMLSheet.NodeList
+	nil,                          // 7: tableau.XMLNode.ChildMapEntry
+	(*XMLNode_IndexList)(nil),    // 8: tableau.XMLNode.IndexList
+	(*XMLNode_AttrMap)(nil),      // 9: tableau.XMLNode.AttrMap
+	nil,                          // 10: tableau.XMLNode.AttrMap.MapEntry
+	(*XMLNode_AttrMap_Attr)(nil), // 11: tableau.XMLNode.AttrMap.Attr
+}
+var file_tableau_protobuf_xml_proto_depIdxs = []int32{
+	1,  // 0: tableau.XMLBook.sheet_list:type_name -> tableau.XMLSheet
+	3,  // 1: tableau.XMLBook.sheet_map:type_name -> tableau.XMLBook.SheetMapEntry
+	2,  // 2: tableau.XMLSheet.meta:type_name -> tableau.XMLNode
+	2,  // 3: tableau.XMLSheet.data:type_name -> tableau.XMLNode
+	4,  // 4: tableau.XMLSheet.meta_node_map:type_name -> tableau.XMLSheet.MetaNodeMapEntry
+	5,  // 5: tableau.XMLSheet.data_node_map:type_name -> tableau.XMLSheet.DataNodeMapEntry
+	9,  // 6: tableau.XMLNode.attr_map:type_name -> tableau.XMLNode.AttrMap
+	2,  // 7: tableau.XMLNode.child_list:type_name -> tableau.XMLNode
+	7,  // 8: tableau.XMLNode.child_map:type_name -> tableau.XMLNode.ChildMapEntry
+	2,  // 9: tableau.XMLNode.parent:type_name -> tableau.XMLNode
+	2,  // 10: tableau.XMLSheet.MetaNodeMapEntry.value:type_name -> tableau.XMLNode
+	6,  // 11: tableau.XMLSheet.DataNodeMapEntry.value:type_name -> tableau.XMLSheet.NodeList
+	2,  // 12: tableau.XMLSheet.NodeList.nodes:type_name -> tableau.XMLNode
+	8,  // 13: tableau.XMLNode.ChildMapEntry.value:type_name -> tableau.XMLNode.IndexList
+	11, // 14: tableau.XMLNode.AttrMap.list:type_name -> tableau.XMLNode.AttrMap.Attr
+	10, // 15: tableau.XMLNode.AttrMap.map:type_name -> tableau.XMLNode.AttrMap.MapEntry
+	16, // [16:16] is the sub-list for method output_type
+	16, // [16:16] is the sub-list for method input_type
+	16, // [16:16] is the sub-list for extension type_name
+	16, // [16:16] is the sub-list for extension extendee
+	0,  // [0:16] is the sub-list for field type_name
+}
+
+func init() { file_tableau_protobuf_xml_proto_init() }
+func file_tableau_protobuf_xml_proto_init() {
+	if File_tableau_protobuf_xml_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_tableau_protobuf_xml_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLBook); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tableau_protobuf_xml_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLSheet); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tableau_protobuf_xml_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLNode); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tableau_protobuf_xml_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLSheet_NodeList); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tableau_protobuf_xml_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLNode_IndexList); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tableau_protobuf_xml_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLNode_AttrMap); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tableau_protobuf_xml_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*XMLNode_AttrMap_Attr); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_tableau_protobuf_xml_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   12,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_tableau_protobuf_xml_proto_goTypes,
+		DependencyIndexes: file_tableau_protobuf_xml_proto_depIdxs,
+		MessageInfos:      file_tableau_protobuf_xml_proto_msgTypes,
+	}.Build()
+	File_tableau_protobuf_xml_proto = out.File
+	file_tableau_protobuf_xml_proto_rawDesc = nil
+	file_tableau_protobuf_xml_proto_goTypes = nil
+	file_tableau_protobuf_xml_proto_depIdxs = nil
+}


Test patch:
diff --git a/internal/importer/xml_test.go b/internal/importer/xml_test.go
index 76801a7..b4aa172 100644
--- a/internal/importer/xml_test.go
+++ b/internal/importer/xml_test.go
@@ -1,81 +1,222 @@
 package importer
 
 import (
+	"reflect"
+	"strings"
 	"testing"
+
+	"github.com/antchfx/xmlquery"
+	"github.com/tableauio/tableau/proto/tableaupb"
 )
 
-func Test_isMetaBeginning(t *testing.T) {
+func Test_escapeAttrs(t *testing.T) {
 	type args struct {
-		s string
+		doc string
 	}
 	tests := []struct {
 		name string
 		args args
-		want bool
+		want string
 	}{
 		// TODO: Add test cases.
 		{
 			name: "standard",
 			args: args{
-				s: "<!-- @TABLEAU\n",
+				doc: `
+<Conf>
+    <Server Type="map<enum<.ServerType>, Server>" Value="int32"/>
+</Conf>
+`,
 			},
-			want: true,
+			want: `
+<Conf>
+    <Server Type="map&lt;enum&lt;.ServerType&gt;, Server&gt;" Value="int32"/>
+</Conf>
+`,
+		},
+		{
+			name: "FeatureToggle",
+			args: args{
+				doc: `
+<Conf>
+	<Client EnvID="map<uint32,Client>">
+		<Toggle ID="map<enum<.ToggleType>, Toggle>" WorldID="uint32"/>
+	</Client>
+</Conf>
+`,
+			},
+			want: `
+<Conf>
+	<Client EnvID="map&lt;uint32,Client&gt;">
+		<Toggle ID="map&lt;enum&lt;.ToggleType&gt;, Toggle&gt;" WorldID="uint32"/>
+	</Client>
+</Conf>
+`,
 		},
 		{
-			name: "with spaces",
+			name: "Prop",
 			args: args{
-				s: "   <!--      @TABLEAU           \n",
+				doc: `
+<Conf>
+	<Client ID="map<uint32, Client>|{unique:true range:"1,~"}" OpenTime="datetime|{default:"2022-01-23 15:40:00"}"/>
+</Conf>
+`,
+			},
+			want: `
+<Conf>
+	<Client ID="map&lt;uint32, Client&gt;|{unique:true range:&#34;1,~&#34;}" OpenTime="datetime|{default:&#34;2022-01-23 15:40:00&#34;}"/>
+</Conf>
+`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := escapeAttrs(tt.args.doc); got != tt.want {
+				t.Errorf("escapeAttrs() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func FindMetaNode(xmlSheet *tableaupb.XMLSheet, path string) *tableaupb.XMLNode {
+	if node, ok := xmlSheet.MetaNodeMap[path]; ok {
+		return node
+	}
+	return nil
+}
+
+func Test_isRepeated(t *testing.T) {
+	doc := `
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- @TABLEAU -->
+<MatchCfg Open="true">
+	<TeamRatingWeight>
+		<Weight Num="1">
+			<Param Value="100"/>
+		</Weight>
+		<Weight Num="2">
+			<Param Value="30"/>
+			<Param Value="70"/>
+		</Weight>
+	</TeamRatingWeight>
+</MatchCfg>
+`
+	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	xmlMeta, _, _ := readXMLFile(root, nil)
+	sheet1 := getXMLSheet(xmlMeta, "MatchCfg")
+	node1 := FindMetaNode(sheet1, "MatchCfg/TeamRatingWeight/Weight")
+	node2 := FindMetaNode(sheet1, "MatchCfg/TeamRatingWeight/Weight/Param")
+	node3 := FindMetaNode(sheet1, "MatchCfg/TeamRatingWeight")
+	node4 := FindMetaNode(sheet1, "MatchCfg")
+	type args struct {
+		xmlSheet *tableaupb.XMLSheet
+		curr     *tableaupb.XMLNode
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		// TODO: Add test cases.
+		{
+			name: "node1",
+			args: args{
+				xmlSheet: sheet1,
+				curr:     node1,
 			},
 			want: true,
 		},
 		{
-			name: "none space",
+			name: "node2",
 			args: args{
-				s: "<!--@TABLEAU\n",
+				xmlSheet: sheet1,
+				curr:     node2,
 			},
 			want: true,
 		},
 		{
-			name: "non-space heading",
+			name: "node3",
 			args: args{
-				s: "test <!-- @TABLEAU\n",
+				xmlSheet: sheet1,
+				curr:     node3,
 			},
 			want: false,
 		},
 		{
-			name: "non-space tailing",
+			name: "sheet attr",
 			args: args{
-				s: "<!-- @TABLEAU <conf>\n",
+				xmlSheet: sheet1,
+				curr:     node4,
 			},
 			want: false,
 		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := isRepeated(tt.args.xmlSheet, tt.args.curr); got != tt.want {
+				t.Errorf("isRepeated() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_matchAttr(t *testing.T) {
+	type args struct {
+		s string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		// TODO: Add test cases.
 		{
-			name: "empty metaSheet with more spaces",
+			name: "scalar type",
 			args: args{
-				s: "<!-- @TABLEAU -->\n",
+				s: `<AAA bb="bool" cc="int64" dd="enum<.EnumType>" >`,
+			},
+			want: []string{
+				`bb="bool"`, `bb`, `bool`, ``,
 			},
-			want: true,
 		},
+		// TODO: Add test cases.
 		{
-			name: "empty metaSheet with less spaces",
+			name: "Prop",
 			args: args{
-				s: " <!--@TABLEAU -->   \n",
+				s: `<Client OpenTime="datetime|{default:"2022-01-23 15:40:00"}" CloseTime="datetime|{default:"2022-01-23 15:40:00"}"/>`,
+			},
+			want: []string{
+				`OpenTime="datetime|{default:"2022-01-23 15:40:00"}"`,
+				`OpenTime`, `datetime`, `|{default:"2022-01-23 15:40:00"}`,
 			},
-			want: true,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := isMetaBeginning(tt.args.s); got != tt.want {
-				t.Errorf("isMetaBeginning() = %v, want %v", got, tt.want)
+			if got := matchAttr(tt.args.s); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("matchAttr() = %v, want %v", got, tt.want)
 			}
 		})
 	}
 }
 
-func Test_isMetaEnding(t *testing.T) {
+func Test_isFirstChild(t *testing.T) {
+	doc := `
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- @TABLEAU
+<Server>
+    <MapConf>
+        <Weight Num="map&lt;uint32,Weight&gt;"/>
+    </MapConf>
+</Server>
+-->
+`
+	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	xmlMeta, _, _ := readXMLFile(root, nil)
+	sheet1 := getXMLSheet(xmlMeta, "Server")
+	node1 := FindMetaNode(sheet1, "Server/MapConf/Weight")
 	type args struct {
-		s string
+		curr *tableaupb.XMLNode
 	}
 	tests := []struct {
 		name string
@@ -84,178 +225,304 @@ func Test_isMetaEnding(t *testing.T) {
 	}{
 		// TODO: Add test cases.
 		{
-			name: "standard",
+			name: "Weight",
 			args: args{
-				s: "-->\n",
+				curr: node1,
 			},
 			want: true,
 		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := isFirstChild(tt.args.curr); got != tt.want {
+				t.Errorf("isFirstChild() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_fixNodeType(t *testing.T) {
+	doc := `
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- @TABLEAU
+<MatchCfg open="true">
+	<MatchMode MissionType="map&lt;enum&lt;.MissionType&gt;,MatchMode&gt;">
+		<MatchAI IsOpen="bool" PlayerOnlyOneCamp="bool">
+			<AI Type="[AI]&lt;enum&lt;.ENMAIWarmType&gt;&gt;" IsOpen="bool" MinTime="duration" MaxTime="duration" />
+		</MatchAI>
+    </MatchMode>
+	<MapConf Param="[]int64">
+		<Test>
+        	<Weight Num="map&lt;uint32,Weight&gt;"/>
+		</Test>
+    </MapConf>
+	<Client EnvID="map&lt;uint32,Client&gt;">
+		<Toggle ID="map&lt;enum&lt;.ToggleType&gt;, Toggle&gt;" WorldID="uint32"/>
+	</Client>
+</MatchCfg>
+-->
+
+<MatchCfg>
+	<StructConf>
+		<Weight Num="1">
+			<Param Value="100"/>
+		</Weight>
+		<Test Value="1"/>
+	</StructConf>
+
+	<ListConf>
+        <Weight Num="1">
+            <Param Value="100"/>
+        </Weight>
+        <Weight Num="2">
+            <Param Value="30"/>
+            <Param Value="70"/>
+        </Weight>
+    </ListConf>
+</MatchCfg>
+`
+	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	xmlMeta, _, _ := readXMLFile(root, nil)
+	sheet1 := getXMLSheet(xmlMeta, "MatchCfg")
+	node1 := FindMetaNode(sheet1, "MatchCfg/MatchMode/MatchAI/AI")
+	node2 := FindMetaNode(sheet1, "MatchCfg/MapConf/Test/Weight")
+	node3 := FindMetaNode(sheet1, "MatchCfg/StructConf/Weight")
+	node4 := FindMetaNode(sheet1, "MatchCfg/ListConf/Weight")
+	node5 := FindMetaNode(sheet1, "MatchCfg/ListConf/Weight/Param")
+	node6 := FindMetaNode(sheet1, "MatchCfg/Client/Toggle")
+	node7 := FindMetaNode(sheet1, "MatchCfg")
+	node8 := FindMetaNode(sheet1, "MatchCfg/StructConf/Test")
+	node9 := FindMetaNode(sheet1, "MatchCfg/MapConf")
+	type args struct {
+		xmlSheet *tableaupb.XMLSheet
+		curr     *tableaupb.XMLNode
+		oriType  string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		// TODO: Add test cases.
 		{
-			name: "more spaces",
+			name: "MatchCfg/MatchMode/MatchAI/AI",
 			args: args{
-				s: "  --> \n",
+				xmlSheet: sheet1,
+				curr:     node1,
+				oriType:  `[AI]<enum<.ENMAIWarmType>>`,
 			},
-			want: true,
+			want: `[AI]<enum<.ENMAIWarmType>>`,
 		},
 		{
-			name: "empty metaSheet with more spaces",
+			name: "MatchCfg/MapConf/Test/Weight",
 			args: args{
-				s: "<!-- @TABLEAU -->\n",
+				xmlSheet: sheet1,
+				curr:     node2,
+				oriType:  `map<uint32,Weight>`,
 			},
-			want: true,
+			want: `{Test}map<uint32,Weight>`,
 		},
 		{
-			name: "empty metaSheet with less spaces",
+			name: "MatchCfg/StructConf/Weight",
 			args: args{
-				s: " <!--@TABLEAU -->   \n",
+				xmlSheet: sheet1,
+				curr:     node3,
+				oriType:  `int32`,
 			},
-			want: true,
+			want: `{StructConf}{Weight}int32`,
 		},
 		{
-			name: "non-space characters heading",
+			name: "MatchCfg/ListConf/Weight",
 			args: args{
-				s: "</Conf> -->\n",
+				xmlSheet: sheet1,
+				curr:     node4,
+				oriType:  `int32`,
 			},
-			want: false,
+			want: `{ListConf}[Weight]<int32>`,
 		},
 		{
-			name: "non-space characters tailing",
+			name: "MatchCfg/ListConf/Weight/Param",
 			args: args{
-				s: " --> <Conf> \n",
+				xmlSheet: sheet1,
+				curr:     node5,
+				oriType:  `int32`,
 			},
-			want: false,
+			want: `[Param]<int32>`,
+		},
+		{
+			name: "MatchCfg/Client/Toggle",
+			args: args{
+				xmlSheet: sheet1,
+				curr:     node6,
+				oriType:  `map<enum<.ToggleType>, Toggle>`,
+			},
+			want: `map<enum<.ToggleType>, Toggle>`,
+		},
+		{
+			name: "MatchCfg",
+			args: args{
+				xmlSheet: sheet1,
+				curr:     node7,
+				oriType:  `bool`,
+			},
+			want: `bool`,
+		},
+		{
+			name: "MatchCfg/StructConf/Test",
+			args: args{
+				xmlSheet: sheet1,
+				curr:     node8,
+				oriType:  `int32`,
+			},
+			want: `{Test}int32`,
+		},
+		{
+			name: "MatchCfg/MapConf@Param",
+			args: args{
+				xmlSheet: sheet1,
+				curr:     node9,
+				oriType:  `[]int64`,
+			},
+			want: `{MapConf}[]<int64>`,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := isMetaEnding(tt.args.s); got != tt.want {
-				t.Errorf("isMetaEnding() = %v, want %v", got, tt.want)
+			if got := fixNodeType(tt.args.xmlSheet, tt.args.curr, tt.args.oriType); got != tt.want {
+				t.Errorf("fixNodeType() = %v, want %v", got, tt.want)
 			}
 		})
 	}
 }
 
-func Test_getMetaDoc(t *testing.T) {
+func Test_getNodePath(t *testing.T) {
+	metaDoc := `
+<?xml version='1.0' encoding='UTF-8'?>
+<MatchCfg open="true">
+	<MapConf>
+        <Weight Num="map&lt;uint32,Weight&gt;"/>
+    </MapConf>
+</MatchCfg>
+`
+	doc := `
+<?xml version='1.0' encoding='UTF-8'?>
+<MatchCfg>
+	<StructConf>
+		<Weight Num="1">
+			<Param Value="100"/>
+		</Weight>
+		<Test Value="1"/>
+	</StructConf>
+</MatchCfg>
+`
+	metaRoot, _ := xmlquery.Parse(strings.NewReader(metaDoc))
+	node1 := xmlquery.FindOne(metaRoot, "MatchCfg/MapConf/Weight")
+	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	node2 := xmlquery.FindOne(root, "MatchCfg/StructConf/Test")
 	type args struct {
-		doc string
+		curr *xmlquery.Node
 	}
 	tests := []struct {
-		name        string
-		args        args
-		wantMetaDoc string
-		wantErr     bool
+		name     string
+		args     args
+		wantRoot *xmlquery.Node
+		wantPath string
 	}{
 		// TODO: Add test cases.
 		{
-			name: "standard",
+			name: "MatchCfg/MapConf/Weight",
 			args: args{
-				doc: `
-<!-- @TABLEAU
-<Conf>
-    <Server Value="int32"/>
-</Conf>
--->
-
-<Conf>
-    <Server Value="100"/>
-</Conf>
-`,
-			},
-			wantMetaDoc: `<Conf>
-    <Server Value="int32"/>
-</Conf>
-`,
-			wantErr: false,
-		},
-		{
-			name: "empty metaSheet",
-			args: args{
-				doc: `
-<!--@TABLEAU -->
-
-<Conf>
-    <Server Value="100"/>
-</Conf>
-`,
+				curr: node1,
 			},
-			wantMetaDoc: ``,
-			wantErr:     false,
+			wantRoot: metaRoot,
+			wantPath: "MatchCfg/MapConf/Weight",
 		},
 		{
-			name: "none metaSheet",
-			args: args{
-				doc: `
-
-<Conf>
-    <Server Value="100"/>
-</Conf>
-`,
-			},
-			wantMetaDoc: ``,
-			wantErr:     true,
-		},
-		{
-			name: "standard",
+			name: "MatchCfg/StructConf/Test",
 			args: args{
-				doc: `
-<!-- @TABLEAU <Conf>
-    <Server Value="int32"/>
-</Conf>
--->
-
-<Conf>
-    <Server Value="100"/>
-</Conf>
-`,
+				curr: node2,
 			},
-			wantMetaDoc: ``,
-			wantErr:     true,
+			wantRoot: root,
+			wantPath: "MatchCfg/StructConf/Test",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			gotMetaDoc, err := getMetaDoc(tt.args.doc)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("getMetaDoc() error = %v, wantErr %v", err, tt.wantErr)
-				return
+			gotRoot, gotPath := getNodePath(tt.args.curr)
+			if !reflect.DeepEqual(gotRoot, tt.wantRoot) {
+				t.Errorf("getNodePath() gotRoot = %v, want %v", gotRoot, tt.wantRoot)
 			}
-			if gotMetaDoc != tt.wantMetaDoc {
-				t.Errorf("getMetaDoc() = %v, want %v", gotMetaDoc, tt.wantMetaDoc)
+			if gotPath != tt.wantPath {
+				t.Errorf("getNodePath() gotPath = %v, want %v", gotPath, tt.wantPath)
 			}
 		})
 	}
 }
 
-func Test_escapeAttrs(t *testing.T) {
+func Test_isCrossCell(t *testing.T) {
 	type args struct {
-		doc string
+		t string
 	}
 	tests := []struct {
 		name string
 		args args
-		want string
+		want bool
 	}{
 		// TODO: Add test cases.
 		{
-			name: "standard",
+			name: "map<uint32, Item>",
 			args: args{
-				doc: `
-<Conf>
-    <Server Type="map<enum<.ServerType>, Server>" Value="int32"/>
-</Conf>
-`,
+				t: "map<uint32, Item>",
 			},
-			want: `
-<Conf>
-    <Server Type="map&lt;enum&lt;.ServerType&gt;, Server&gt;" Value="int32"/>
-</Conf>
-`,
+			want: true,
+		},
+		{
+			name: "map<uint32, enum<.RankType>>",
+			args: args{
+				t: "map<uint32, enum<.RankType>>",
+			},
+			want: false,
+		},
+		{
+			name: "[]int32",
+			args: args{
+				t: "[]int32",
+			},
+			want: false,
+		},		
+		{
+			name: "[.Item]uint32",
+			args: args{
+				t: "[.Item]uint32",
+			},
+			want: true,
+		},
+		{
+			name: "[RankConf]<uint32>",
+			args: args{
+				t: "[RankConf]<uint32>",
+			},
+			want: true,
+		},
+		{
+			name: "{int32 ID,string Name,string Desc}Prop",
+			args: args{
+				t: "{int32 ID,string Name,string Desc}Prop",
+			},
+			want: false,
+		},	
+		{
+			name: "{.Item}uint32",
+			args: args{
+				t: "{.Item}uint32",
+			},
+			want: true,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := escapeAttrs(tt.args.doc); got != tt.want {
-				t.Errorf("escapeAttrs() = %v, want %v", got, tt.want)
+			if got := isCrossCell(tt.args.t); got != tt.want {
+				t.Errorf("isCrossCell() = %v, want %v", got, tt.want)
 			}
 		})
 	}
diff --git a/test/dev/dynamic/tableau_test.go b/test/dev/dynamic/tableau_test.go
index b1efa75..93e9baf 100644
--- a/test/dev/dynamic/tableau_test.go
+++ b/test/dev/dynamic/tableau_test.go
@@ -38,6 +38,7 @@ func Test_GenProto(t *testing.T) {
 					},
 					Subdirs: []string{
 						// "xml/match",
+						// "xml/_test",
 					},
 				},
 			},
@@ -55,7 +56,7 @@ func Test_GenProto(t *testing.T) {
 		),
 		options.Log(
 			&options.LogOption{
-				Level: "INFO",
+				Level: "DEBUG",
 				Mode:  "FULL",
 			},
 		),
diff --git a/test/dev/proto/feature_toggle_conf.proto b/test/dev/proto/feature_toggle_conf.proto
index 200de72..e654f79 100644
--- a/test/dev/proto/feature_toggle_conf.proto
+++ b/test/dev/proto/feature_toggle_conf.proto
@@ -23,14 +23,13 @@ message ToggleCfg {
       protoconf.FeatureToggleMacroType id = 1 [(tableau.field) = {name:"Id"}];
       uint32 world_id = 2 [(tableau.field) = {name:"WorldID"}];
       uint32 zone_id = 3 [(tableau.field) = {name:"ZoneID"}];
-      google.protobuf.Timestamp open_time = 4 [(tableau.field) = {name:"OpenTime"}];
-      google.protobuf.Timestamp close_time = 5 [(tableau.field) = {name:"CloseTime"}];
-      int64 sys_key_1 = 6 [(tableau.field) = {name:"SysKey1"}];
-      int64 sys_key_2 = 7 [(tableau.field) = {name:"SysKey2"}];
+      google.protobuf.Timestamp open_time = 4 [(tableau.field) = {name:"OpenTime" prop:{default:"2022-01-23 15:40:00"}}];
+      google.protobuf.Timestamp close_time = 5 [(tableau.field) = {name:"CloseTime" prop:{default:"2022-01-23 15:40:00"}}];
+      int32 open_rate = 6 [(tableau.field) = {name:"OpenRate"}];
+      string desc = 7 [(tableau.field) = {name:"Desc"}];
       bool notify_closed = 8 [(tableau.field) = {name:"NotifyClosed"}];
       string name = 9 [(tableau.field) = {name:"Name"}];
-      int32 open_rate = 10 [(tableau.field) = {name:"OpenRate"}];
-      string desc = 11 [(tableau.field) = {name:"Desc"}];
+      repeated int64 sys_key_list = 10 [(tableau.field) = {name:"SysKey" layout:LAYOUT_HORIZONTAL}];
     }
   }
 }
diff --git a/test/dev/proto/multi_sheet_conf.proto b/test/dev/proto/multi_sheet_conf.proto
index bdc37fd..d008133 100644
--- a/test/dev/proto/multi_sheet_conf.proto
+++ b/test/dev/proto/multi_sheet_conf.proto
@@ -12,9 +12,12 @@ option (tableau.workbook) = {name:"xml/MultiSheet.xml"};
 message Server {
   option (tableau.worksheet) = {name:"Server" namerow:1 typerow:2 noterow:3 datarow:4 nameline:1 typeline:1 nested:true};
 
-  MapConf map_conf = 1 [(tableau.field) = {name:"MapConf"}];
+  string desc = 1 [(tableau.field) = {name:"Desc"}];
+  MapConf map_conf = 2 [(tableau.field) = {name:"MapConf"}];
   message MapConf {
-    repeated Weight weight_list = 1 [(tableau.field) = {name:"Weight" key:"Num" layout:LAYOUT_VERTICAL}];
+    repeated int64 param_list = 1 [(tableau.field) = {name:"Param" layout:LAYOUT_INCELL}];
+    map<int32, int32> type_num_map = 2 [(tableau.field) = {name:"TypeNum" layout:LAYOUT_INCELL}];
+    map<int32, Weight> weight_map = 3 [(tableau.field) = {name:"Weight" key:"Num" layout:LAYOUT_VERTICAL}];
     message Weight {
       int32 num = 1 [(tableau.field) = {name:"Num"}];
       repeated Param param_list = 2 [(tableau.field) = {name:"Param" key:"Value" layout:LAYOUT_VERTICAL}];
@@ -23,7 +26,6 @@ message Server {
       }
     }
   }
-  string desc = 2 [(tableau.field) = {name:"Desc"}];
   repeated Node node_list = 3 [(tableau.field) = {name:"Node" key:"Addr" layout:LAYOUT_VERTICAL}];
   message Node {
     string addr = 1 [(tableau.field) = {name:"Addr"}];
diff --git a/test/dev/testdata/xml/Client.xml b/test/dev/testdata/xml/Client.xml
index aa1c5be..7962f37 100644
--- a/test/dev/testdata/xml/Client.xml
+++ b/test/dev/testdata/xml/Client.xml
@@ -1,3 +1,4 @@
-<ClientCfg>
-    <Version Min="1.0.0.1" Max="1.0.0.3"/>
+<?xml version='1.0' encoding='UTF-8'?>
+<ClientCfg>
+    <Version Min="1.0.0.1" Max="1.0.0.3"/>
 </ClientCfg>
\ No newline at end of file
diff --git a/test/dev/testdata/xml/FeatureToggle.xml b/test/dev/testdata/xml/FeatureToggle.xml
index 4484730..8d211aa 100644
--- a/test/dev/testdata/xml/FeatureToggle.xml
+++ b/test/dev/testdata/xml/FeatureToggle.xml
@@ -1,9 +1,8 @@
 <?xml version='1.0' encoding='UTF-8'?>
 <!-- @TABLEAU
 <ToggleCfg>
-<FeatureToggle EnvID="map<uint32,FeatureToggle>">
-<Toggle ID="map<enum<.FeatureToggleMacroType>, Toggle>" WorldID="uint32" ZoneID="uint32" OpenTime="datetime|2022-01-23 15:40:00" CloseTime="datetime|2022-01-23 15:40:00" SysKey1="int64" SysKey2="int64" NotifyClosed="bool"/>
-
+    <FeatureToggle EnvID="map<uint32,FeatureToggle>">
+        <Toggle ID="map<enum<.FeatureToggleMacroType>, Toggle>" WorldID="uint32" ZoneID="uint32" OpenTime="datetime|{default:"2022-01-23 15:40:00"}" CloseTime="datetime|{default:"2022-01-23 15:40:00"}" SysKey1="int64" SysKey2="int64" NotifyClosed="bool"/>
     </FeatureToggle>
 </ToggleCfg>
 -->
diff --git a/test/dev/testdata/xml/MultiSheet.xml b/test/dev/testdata/xml/MultiSheet.xml
index d56dd49..9362773 100644
--- a/test/dev/testdata/xml/MultiSheet.xml
+++ b/test/dev/testdata/xml/MultiSheet.xml
@@ -1,7 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
 <!-- @TABLEAU
 <Server>
-    <MapConf>
-        <Weight Num="map<uint32,Weight>"/>
+    <MapConf Param="[]int64" TypeNum="map<int32, int32>">
+        <Weight Num="map<int32,Weight>"/>
     </MapConf>
 </Server>
 <Client/>
@@ -27,7 +28,7 @@
         </Weight>
     </ListConf>
 
-    <MapConf>
+    <MapConf Param="1,2,3" TypeNum="1:1,2:2">
         <Weight Num="1">
             <Param Value="100"/>
         </Weight>
@@ -46,4 +47,4 @@
     <Trash Name="Banana"/>
     <Trash Name="Apple"/>
     <Trash Name="Bottle"/>
-</TrashBin>
\ No newline at end of file
+</TrashBin>
diff --git a/test/dev/testdata/xml/match/Match.xml b/test/dev/testdata/xml/match/Match.xml
index c54d326..0e60588 100644
--- a/test/dev/testdata/xml/match/Match.xml
+++ b/test/dev/testdata/xml/match/Match.xml
@@ -1,3 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
 <!-- @TABLEAU
 <MatchCfg>
     <ConfirmTimeOutPunish Time="duration" />
diff --git a/test/dev/testdata/xml/match/SimpleMatch.xml b/test/dev/testdata/xml/match/SimpleMatch.xml
index c712066..4569431 100644
--- a/test/dev/testdata/xml/match/SimpleMatch.xml
+++ b/test/dev/testdata/xml/match/SimpleMatch.xml
@@ -1,3 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
 <!-- @TABLEAU -->
 
 <SimpleMatchConf>