Repository: tableauio/tableau
Pull request #25 (2022-10-12 13:59:23)

feat(xml): support tag inner text

codecov[bot]: # [Codecov](https://codecov.io/gh/tableauio/tableau/pull/25?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
> Merging [#25](https://codecov.io/gh/tableauio/tableau/pull/25?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) (14192fb) into [master](https://codecov.io/gh/tableauio/tableau/commit/5445cf74d7595a5ae5825aa7a640ae11e4347e8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) (5445cf7) will **increase** coverage by `0.15%`.
> The diff coverage is `72.00%`.

```diff
@@            Coverage Diff             @@
##           master      #25      +/-   ##
==========================================
+ Coverage   18.50%   18.66%   +0.15%     
==========================================
  Files          19       19              
  Lines        2853     2872      +19     
==========================================
+ Hits          528      536       +8     
- Misses       2282     2290       +8     
- Partials       43       46       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `18.66% <72.00%> (+0.15%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tableauio/tableau/pull/25?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) | Coverage Δ | |
|---|---|---|
| [internal/importer/xml.go](https://codecov.io/gh/tableauio/tableau/pull/25/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvaW1wb3J0ZXIveG1sLmdv) | `50.00% <72.00%> (-0.32%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio)


Patch:
diff --git a/internal/importer/book/book.go b/internal/importer/book/book.go
index e242018..eb4d1eb 100644
--- a/internal/importer/book/book.go
+++ b/internal/importer/book/book.go
@@ -39,11 +39,34 @@ func (b *Book) Filename() string {
 	return b.filename
 }
 
-// BookNames returns this book's name.
+// BookName returns this book's name.
 func (b *Book) BookName() string {
 	return b.name
 }
 
+// Metabook returns the metadata of this book.
+func (b *Book) Metabook() *tableaupb.Metabook {
+	return b.meta
+}
+
+// GetSheets returns all sheets in order in the book.
+func (b *Book) GetSheets() []*Sheet {
+	var sheets []*Sheet
+	for _, sheetName := range b.sheetNames {
+		sheet := b.GetSheet(sheetName)
+		if sheet == nil {
+			panic("sheet not found" + sheetName)
+		}
+		sheets = append(sheets, sheet)
+	}
+	return sheets
+}
+
+// GetSheet returns a Sheet of the specified sheet name.
+func (b *Book) GetSheet(name string) *Sheet {
+	return b.sheets[name]
+}
+
 // AddSheet adds a sheet to the book and keep the sheet order.
 func (b *Book) AddSheet(sheet *Sheet) {
 	b.sheets[sheet.Name] = sheet
@@ -68,24 +91,6 @@ func (b *Book) delSheetName(sheetName string) {
 	}
 }
 
-// GetSheets returns all sheets in order in the book.
-func (b *Book) GetSheets() []*Sheet {
-	var sheets []*Sheet
-	for _, sheetName := range b.sheetNames {
-		sheet := b.GetSheet(sheetName)
-		if sheet == nil {
-			panic("sheet not found" + sheetName)
-		}
-		sheets = append(sheets, sheet)
-	}
-	return sheets
-}
-
-// GetSheet returns a Sheet of the specified sheet name.
-func (b *Book) GetSheet(name string) *Sheet {
-	return b.sheets[name]
-}
-
 // Squeeze keeps only the inputed sheet names and removes other sheets from the book.
 func (b *Book) Squeeze(sheetNames []string) {
 	sheetNameMap := map[string]bool{} // sheet name -> keep or not (bool)
@@ -93,7 +98,7 @@ func (b *Book) Squeeze(sheetNames []string) {
 		sheetNameMap[sheetName] = true
 	}
 
-	// NOTE(wenchgy): must deep-copy the sheetNames, as we will delete 
+	// NOTE(wenchgy): must deep-copy the sheetNames, as we will delete
 	// the elements when looping the slice at the same time.
 	deeplyCopiedSheetNames := make([]string, len(b.sheetNames))
 	copy(deeplyCopiedSheetNames, b.sheetNames)
@@ -145,7 +150,7 @@ func (b *Book) ParseMeta() error {
 		keepedSheetNames = append(keepedSheetNames, sheetName)
 		sheet.Meta = sheetMeta
 	}
-	// NOTE: only keep the sheets that are specified in meta
+	// NOTE: only keep the sheets that are specified in metasheet
 	b.Squeeze(keepedSheetNames)
 	log.Debugf("squeezed: %s#%s: %+v", b.Filename(), MetasheetName, keepedSheetNames)
 	for sheetName := range b.sheets {
diff --git a/internal/importer/book/sheet.go b/internal/importer/book/sheet.go
index dbcc8f3..9f92fc7 100644
--- a/internal/importer/book/sheet.go
+++ b/internal/importer/book/sheet.go
@@ -26,7 +26,7 @@ type Sheet struct {
 	MaxRow int
 	MaxCol int
 
-	Rows [][]string // 2D array of string.
+	Rows [][]string // 2D array of strings.
 
 	Meta *tableaupb.Metasheet
 }
diff --git a/internal/importer/importer.go b/internal/importer/importer.go
index f7346b9..9b7b5ff 100644
--- a/internal/importer/importer.go
+++ b/internal/importer/importer.go
@@ -8,6 +8,7 @@ import (
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/importer/book"
 	"github.com/tableauio/tableau/log"
+	"github.com/tableauio/tableau/proto/tableaupb"
 )
 
 type Importer interface {
@@ -21,6 +22,8 @@ type Importer interface {
 	// 	- CSV: recognizes pattern: "<BookName>#<SheetName>.csv", and returns "<BookName>".
 	// 	- XML: the base filename without file extension.
 	BookName() string
+	// Metabook returns the metadata of the book.
+	Metabook() *tableaupb.Metabook
 	// GetSheets returns all sheets in order of the book.
 	GetSheets() []*book.Sheet
 	// GetSheet returns a Sheet of the specified sheet name.
diff --git a/internal/importer/xml.go b/internal/importer/xml.go
index 2ff588f..27dde7e 100644
--- a/internal/importer/xml.go
+++ b/internal/importer/xml.go
@@ -25,25 +25,22 @@ type XMLImporter struct {
 	*book.Book
 }
 
-type NoNeedParseError struct {
-}
-
-func (e *NoNeedParseError) Error() string {
-	return fmt.Sprintf("`%s` not found", book.MetasheetName)
-}
-
 var attrRegexp *regexp.Regexp
 var scalarListRegexp *regexp.Regexp
+var metasheetRegexp *regexp.Regexp
 
 const (
 	xmlProlog             = `<?xml version='1.0' encoding='UTF-8'?>`
 	ungreedyPropGroup     = `(\|\{[^\{\}]+\})?`
 	atTableauDisplacement = `ATABLEAU`
+	metasheetItemBlock    = `(\s+<Item(\s+\S+\s*=\s*"\S+")+\s*/>\s+)*`
+	sheetBlock            = `<%v(>(.*\n)*</%v>|\s*/>)`
 )
 
 func init() {
 	attrRegexp = regexp.MustCompile(`([0-9A-Za-z_]+)="` + types.TypeGroup + ungreedyPropGroup + `"`)
 	scalarListRegexp = regexp.MustCompile(`([A-Za-z_]+)([0-9]+)`)
+	metasheetRegexp = regexp.MustCompile(fmt.Sprintf(`<!--\s+(<%v(>`+metasheetItemBlock+`</%v>|\s*/>)(.*\n)+?)-->`, book.MetasheetName, book.MetasheetName))
 }
 
 // TODO: options
@@ -66,39 +63,38 @@ func NewXMLImporter(filename string, sheets []string, parser book.SheetParser) (
 	}, nil
 }
 
+// splitRawXML splits the raw xml into metasheet and content (which is the xml data)
+func splitRawXML(rawXML string) (metasheet, content string) {
+	matches := matchMetasheet(rawXML)
+	if len(matches) < 2 {
+		return "", rawXML
+	}
+	content = strings.ReplaceAll(rawXML, matches[0], "")
+	metasheet = xmlProlog + "\n" + escapeAttrs(strings.ReplaceAll(matches[1], book.MetasheetName, atTableauDisplacement))
+	return metasheet, content
+}
+
 // parseXML parse sheets in the XML file named `filename` and return a book with multiple sheets
 // in TABLEAU grammar which can be exported to protobuf by excel parser.
 func parseXML(filename string, sheetNames []string, parser book.SheetParser) (*book.Book, error) {
 	log.Debugf("xml: %s", filename)
 	buf, err := os.ReadFile(filename)
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to ReadFile %s", filename)
+		return nil, err
 	}
 	// pre check if exists `@TABLEAU`
-	existed, err := regexp.Match(book.MetasheetName, buf)
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to Match %s", book.MetasheetName)
-	}
-	if !existed {
+	metasheet, content := splitRawXML(string(buf))
+	if metasheet == "" {
+		log.Debugf("xml:%s no need parse: %s not found", filename, book.MetasheetName)
 		return nil, nil
 	}
-	root, err := xmlquery.Parse(strings.NewReader(string(buf)))
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to parse xml:%s", filename)
-	}
 
 	// The first pass
 	bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
 	newBook := book.NewBook(bookName, filename, parser)
-	xmlMeta, err := readXMLFile(root, newBook)
+	xmlMeta, err := readXMLFile(metasheet, content, newBook)
 	if err != nil {
-		switch e := err.(type) {
-		case *NoNeedParseError:
-			log.Debugf("xml:%s no need parse: %s not found", filename, book.MetasheetName)
-			return nil, nil
-		default:
-			return nil, e
-		}
+		return nil, err
 	}
 
 	for _, xmlSheet := range xmlMeta.SheetList {
@@ -134,101 +130,171 @@ func parseXML(filename string, sheetNames []string, parser book.SheetParser) (*b
 // 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, newBook *book.Book) (*tableaupb.XMLBook, error) {
+func readXMLFile(metasheet, content string, newBook *book.Book) (*tableaupb.XMLBook, error) {
 	xmlMeta := &tableaupb.XMLBook{
 		SheetMap: make(map[string]int32),
 	}
-	foundMetaSheetName := false
+	// sheetName -> {colName -> val}
 	metasheetMap := make(map[string]map[string]string)
-	for n := root.FirstChild; n != nil; n = n.NextSibling {
-		switch n.Type {
-		case xmlquery.CommentNode:
-			if !strings.Contains(n.Data, book.MetasheetName) {
-				continue
-			}
-			metaStr := xmlProlog + escapeAttrs(strings.ReplaceAll(n.Data, book.MetasheetName, atTableauDisplacement))
-			metaRoot, err := xmlquery.Parse(strings.NewReader(metaStr))
-			if err != nil {
-				return 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
-				}
-				if n.Data == atTableauDisplacement {
-					foundMetaSheetName = true
-					nameRow := book.MetasheetOptions().Namerow - 1
-					dataRow := book.MetasheetOptions().Datarow - 1
-					set := treeset.NewWithStringComparator()
-					for n := n.FirstChild; n != nil; n = n.NextSibling {
-						if n.Type != xmlquery.ElementNode {
-							continue
-						}
-						sheetMap := make(map[string]string)
-						for _, attr := range n.Attr {
-							sheetMap[attr.Name.Local] = attr.Value
-						}
-						sheetMap["Nameline"] = "1"
-						sheetMap["Typeline"] = "1"
-						sheetMap["Nested"] = "true"
-						sheetName, ok := sheetMap["Sheet"]
-						if !ok {
-							return nil, fmt.Errorf("@TABLEAU not specified sheetName by keyword `Sheet`")
-						}
-						metasheetMap[sheetName] = sheetMap
-						for k := range sheetMap {
-							set.Add(k)
-						}
-					}
-					rows := make([][]string, len(metasheetMap)+int(dataRow))
-					for _, k := range set.Values() {
-						rows[nameRow] = append(rows[nameRow], k.(string))
-					}
-					for _, sheet := range metasheetMap {
-						for _, k := range set.Values() {
-							if v, ok := sheet[k.(string)]; ok {
-								rows[dataRow] = append(rows[dataRow], v)
-							} else {
-								rows[dataRow] = append(rows[dataRow], "")
-							}
-						}
-						dataRow++
-					}
-					sheet := book.NewSheet(book.MetasheetName, rows)
-					newBook.AddSheet(sheet)
-					continue
-				}
-				sheetName := n.Data
-				xmlSheet := getXMLSheet(xmlMeta, sheetName)
-				if err := parseMetaNode(n, xmlSheet); err != nil {
-					return nil, errors.Wrapf(err, "failed to parseMetaNode for sheet:%s", sheetName)
-				}
-			}
-		case xmlquery.ElementNode:
-			sheet, ok := metasheetMap[n.Data]
-			// metasheet not empty and sheet not explicitly declared
-			if len(metasheetMap) != 0 && !ok {
-				log.Debugf("sheet not set in @TABLEAU, skilpped|sheetName:%v", n.Data)
-				continue
-			}
-			if template, ok := sheet["Template"]; ok && template == "true" {
-				continue
-			}
-			sheetName := n.Data
-			xmlSheet := getXMLSheet(xmlMeta, sheetName)
-			if err := parseDataNode(n, xmlSheet); err != nil {
-				return nil, errors.Wrapf(err, "failed to parseDataNode for sheet:%s", sheetName)
+	// parse metasheet
+	metaRoot, err := xmlquery.Parse(strings.NewReader(metasheet))
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to parse @TABLEAU string: %s", metasheet)
+	}
+	for n := metaRoot.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type != xmlquery.ElementNode {
+			continue
+		}
+		// <@TABLEAU>...</@TABLEAU>
+		if n.Data == atTableauDisplacement {
+			var sheet *book.Sheet
+			if metasheetMap, sheet, err = genMetasheet(n); err != nil {
+				return nil, errors.Wrapf(err, "failed to generate metasheet")
 			}
-		default:
+			newBook.AddSheet(sheet)
+			continue
+		}
+		sheetName := n.Data
+		xmlSheet := getXMLSheet(xmlMeta, sheetName)
+		if err := parseMetaNode(n, xmlSheet); err != nil {
+			return nil, errors.Wrapf(err, "failed to parseMetaNode for sheet:%s", sheetName)
+		}
+	}
+	
+	// strip template sheets
+	for sheet, colMap := range metasheetMap {
+		if template, ok := colMap["Template"]; !ok || template != "true" {
+			continue
 		}
+		matches := matchSheetBlock(content, sheet)
+		if len(matches) == 0 {
+			continue
+		}
+		content = strings.ReplaceAll(content, matches[0], "")
+	}
+
+	// parse data content
+	dataRoot, err := xmlquery.Parse(strings.NewReader(content))
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to parse @TABLEAU string: %s", content)
 	}
-	if !foundMetaSheetName {
-		return nil, &NoNeedParseError{}
+	for n := dataRoot.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type != xmlquery.ElementNode {
+			continue
+		}
+		sheetName := n.Data
+		xmlSheet := getXMLSheet(xmlMeta, sheetName)
+		if err := parseDataNode(n, xmlSheet); err != nil {
+			return nil, errors.Wrapf(err, "failed to parseDataNode for sheet:%s", sheetName)
+		}
 	}
 
 	return xmlMeta, nil
 }
 
+// genMetasheet generates metasheet according to `
+//   <@TABLEAU>
+//       <Item Sheet="XXXConf" />
+//   </@TABLEAU>`
+func genMetasheet(tableauNode *xmlquery.Node) (map[string]map[string]string, *book.Sheet, error) {
+	// sheetName -> {colName -> val}
+	metasheetMap := make(map[string]map[string]string)
+	nameRow := book.MetasheetOptions().Namerow - 1
+	dataRow := book.MetasheetOptions().Datarow - 1
+	set := treeset.NewWithStringComparator()
+	for n := tableauNode.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type != xmlquery.ElementNode {
+			continue
+		}
+		sheetMap := make(map[string]string)
+		for _, attr := range n.Attr {
+			sheetMap[attr.Name.Local] = attr.Value
+		}
+		sheetMap["Nested"] = "true"
+		// param in `config.yaml` may not be one
+		sheetMap["Nameline"] = "1"
+		sheetMap["Typeline"] = "1"
+		sheetName, ok := sheetMap["Sheet"]
+		if !ok {
+			return metasheetMap, nil, fmt.Errorf("@TABLEAU not specified sheetName by keyword `Sheet`")
+		}
+		metasheetMap[sheetName] = sheetMap
+		for k := range sheetMap {
+			set.Add(k)
+		}
+	}
+	rows := make([][]string, len(metasheetMap)+int(dataRow))
+	for _, k := range set.Values() {
+		rows[nameRow] = append(rows[nameRow], k.(string))
+	}
+	for _, sheet := range metasheetMap {
+		for _, k := range set.Values() {
+			if v, ok := sheet[k.(string)]; ok {
+				rows[dataRow] = append(rows[dataRow], v)
+			} else {
+				rows[dataRow] = append(rows[dataRow], "")
+			}
+		}
+		dataRow++
+	}
+	sheet := book.NewSheet(book.MetasheetName, rows)
+	return metasheetMap, sheet, nil
+}
+
+// addMetaNodeAttr adds an attribute to MetaNode AttrMap if not exists and otherwise replaces the attribute value
+func addMetaNodeAttr(attrMap *tableaupb.XMLNode_AttrMap, name, val string) {
+	if idx, ok := attrMap.Map[name]; !ok {
+		attrMap.Map[name] = int32(len(attrMap.List))
+		attrMap.List = append(attrMap.List, &tableaupb.XMLNode_AttrMap_Attr{
+			Name:  name,
+			Value: val,
+		})
+	} else {
+		// replace attribute value by metaSheet
+		metaAttr := attrMap.List[idx]
+		metaAttr.Value = val
+	}
+}
+
+// addDataNodeAttr adds an attribute to DataNode AttrMap
+func addDataNodeAttr(metaMap, dataMap *tableaupb.XMLNode_AttrMap, name, val string) {
+	if _, ok := metaMap.Map[name]; !ok {
+		metaMap.Map[name] = int32(len(metaMap.List))
+		metaMap.List = append(metaMap.List, &tableaupb.XMLNode_AttrMap_Attr{
+			Name:  name,
+			Value: inferType(val),
+		})
+	}
+	dataMap.Map[name] = int32(len(dataMap.List))
+	dataMap.List = append(dataMap.List, &tableaupb.XMLNode_AttrMap_Attr{
+		Name:  name,
+		Value: val,
+	})
+}
+
+// hasChild check if xml node has any child element node
+func hasChild(n *xmlquery.Node) bool {
+	for n := n.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type == xmlquery.ElementNode {
+			return true
+		}
+	}
+	return false
+}
+
+// getTextContent get the text node from xml node
+func getTextContent(n *xmlquery.Node) string {
+	if hasChild(n) {
+		return ""
+	}
+	for n := n.FirstChild; n != nil; n = n.NextSibling {
+		if n.Type == xmlquery.TextNode {
+			return strings.TrimSpace(n.Data)
+		}
+	}
+	return ""
+}
+
 // parseMetaNode parse xml node `curr` and construct the meta tree in `xmlSheet`.
 func parseMetaNode(curr *xmlquery.Node, xmlSheet *tableaupb.XMLSheet) error {
 	_, path := getNodePath(curr)
@@ -239,22 +305,18 @@ func parseMetaNode(curr *xmlquery.Node, xmlSheet *tableaupb.XMLSheet) error {
 		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 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
-		}
+		addMetaNodeAttr(meta.AttrMap, attrName, t)
 	}
 	for n := curr.FirstChild; n != nil; n = n.NextSibling {
 		if n.Type != xmlquery.ElementNode {
 			continue
 		}
+		// e.g.: <MaxNum>int32</MaxNum>
+		if innerText := getTextContent(n); innerText != "" {
+			attrName := n.Data
+			addMetaNodeAttr(meta.AttrMap, attrName, innerText)
+			continue
+		}
 		childName := n.Data
 		if _, ok := meta.ChildMap[childName]; !ok {
 			newNode := newNode(childName, meta)
@@ -279,24 +341,18 @@ func parseDataNode(curr *xmlquery.Node, xmlSheet *tableaupb.XMLSheet) error {
 	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,
-		})
+		addDataNodeAttr(meta.AttrMap, data.AttrMap, attrName, attr.Value)
 	}
 	for n := curr.FirstChild; n != nil; n = n.NextSibling {
 		if n.Type != xmlquery.ElementNode {
 			continue
 		}
+		// e.g.: <MaxNum>100</MaxNum>
+		if innerText := getTextContent(n); innerText != "" {
+			attrName := n.Data
+			addDataNodeAttr(meta.AttrMap, data.AttrMap, attrName, innerText)
+			continue
+		}
 		childName := n.Data
 		dataChild := newNode(childName, data)
 		registerDataNode(xmlSheet, dataChild)
@@ -381,6 +437,15 @@ func matchScalarList(s string) []string {
 	return scalarListRegexp.FindStringSubmatch(s)
 }
 
+func matchMetasheet(s string) []string {
+	return metasheetRegexp.FindStringSubmatch(s)
+}
+
+func matchSheetBlock(xml, sheetName string) []string {
+	sheetRegexp := regexp.MustCompile(fmt.Sprintf(sheetBlock, sheetName, sheetName))
+	return sheetRegexp.FindStringSubmatch(xml)
+}
+
 func newOrderedAttrMap() *tableaupb.XMLNode_AttrMap {
 	return &tableaupb.XMLNode_AttrMap{
 		Map: make(map[string]int32),
diff --git a/proto/tableau/protobuf/metabook.proto b/proto/tableau/protobuf/metabook.proto
index b1bc497..9e88764 100644
--- a/proto/tableau/protobuf/metabook.proto
+++ b/proto/tableau/protobuf/metabook.proto
@@ -67,5 +67,5 @@ message Metasheet {
   // C++:
   // - const std::vector<const STRUCT_TYPE*>& Find<IndexName>(INDEX_TYPE index) const;
   // - const STRUCT_TYPE* FindFirst<IndexName>(INDEX_TYPE index);
-  repeated string index = 51 [(tableau.field) = { name: "Index" optional: true layout: LAYOUT_INCELL }];  
-}
\ No newline at end of file
+  repeated string index = 51 [(tableau.field) = { name: "Index" optional: true layout: LAYOUT_INCELL }];
+}
diff --git a/tableau.go b/tableau.go
index 9120794..08953a2 100644
--- a/tableau.go
+++ b/tableau.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tableauio/tableau/internal/xlsxgen"
 	"github.com/tableauio/tableau/log"
 	"github.com/tableauio/tableau/options"
+	"github.com/tableauio/tableau/proto/tableaupb"
 )
 
 // Generate can convert Excel/CSV/XML files to protoconf files and
@@ -55,25 +56,6 @@ func GenConf(protoPackage, indir, outdir string, setters ...options.Option) erro
 	return g.Generate()
 }
 
-// Proto2Excel converts protoconf files to excel files (with tableau header).
-func Proto2Excel(protoPackage, indir, outdir string) {
-	g := xlsxgen.Generator{
-		ProtoPackage: protoPackage,
-		InputDir:     indir,
-		OutputDir:    outdir,
-	}
-	g.Generate()
-}
-
-// ParseMeta parses the metasheet "@TABLEAU" in a workbook.
-func ParseMeta(indir, relWorkbookPath string) (importer.Importer, error) {
-	parser := confgen.NewSheetParser(protogen.TableauProtoPackage, "", book.MetasheetOptions())
-	return importer.New(
-		filepath.Join(indir, relWorkbookPath),
-		importer.Parser(parser),
-	)
-}
-
 // NewProtoGenerator creates a new proto generator.
 func NewProtoGenerator(protoPackage, indir, outdir string, options ...options.Option) *protogen.Generator {
 	return protogen.NewGenerator(protoPackage, indir, outdir, options...)
@@ -108,8 +90,30 @@ func GetVersionInfo() *VersionInfo {
 	}
 }
 
-// SetLang sets the default language. 
+// SetLang sets the default language.
 // E.g: en, zh.
-func SetLang(lang string) error{
+func SetLang(lang string) error {
 	return localizer.SetLang(lang)
 }
+
+// Proto2Excel converts protoconf files to excel files (with tableau header).
+// TODO: fully usable generation of excel templates.
+func Proto2Excel(protoPackage, indir, outdir string) {
+	g := xlsxgen.Generator{
+		ProtoPackage: protoPackage,
+		InputDir:     indir,
+		OutputDir:    outdir,
+	}
+	g.Generate()
+}
+
+// ParseMetabook parses and returns metadata of the specified workbook.
+func ParseMetabook(indir, relWorkbookPath string) (*tableaupb.Metabook, error) {
+	workbookPath := filepath.Join(indir, relWorkbookPath)
+	parser := confgen.NewSheetParser(protogen.TableauProtoPackage, "", book.MetasheetOptions())
+	imp, err := importer.New(workbookPath, importer.Parser(parser))
+	if err != nil {
+		return nil, err
+	}
+	return imp.Metabook(), nil
+}


Test patch:
diff --git a/internal/importer/xml_test.go b/internal/importer/xml_test.go
index 8edbc5a..2e77d91 100644
--- a/internal/importer/xml_test.go
+++ b/internal/importer/xml_test.go
@@ -1,6 +1,7 @@
 package importer
 
 import (
+	"fmt"
 	"reflect"
 	"strings"
 	"testing"
@@ -19,7 +20,6 @@ func Test_escapeAttrs(t *testing.T) {
 		args args
 		want string
 	}{
-		// TODO: Add test cases.
 		{
 			name: "standard",
 			args: args{
@@ -105,9 +105,9 @@ func Test_isRepeated(t *testing.T) {
 	</TeamRatingWeight>
 </MatchCfg>
 `
-	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	metasheet, content := splitRawXML(doc)
 	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(root, newBook)
+	xmlMeta, _ := readXMLFile(metasheet, content, newBook)
 	sheet1 := getXMLSheet(xmlMeta, "MatchCfg")
 	node1 := FindMetaNode(sheet1, "MatchCfg/TeamRatingWeight/Weight")
 	node2 := FindMetaNode(sheet1, "MatchCfg/TeamRatingWeight/Weight/Param")
@@ -122,7 +122,6 @@ func Test_isRepeated(t *testing.T) {
 		args args
 		want bool
 	}{
-		// TODO: Add test cases.
 		{
 			name: "node1",
 			args: args{
@@ -174,7 +173,6 @@ func Test_matchAttr(t *testing.T) {
 		args args
 		want []string
 	}{
-		// TODO: Add test cases.
 		{
 			name: "scalar type",
 			args: args{
@@ -184,7 +182,6 @@ func Test_matchAttr(t *testing.T) {
 				`bb="bool"`, `bb`, `bool`, ``,
 			},
 		},
-		// TODO: Add test cases.
 		{
 			name: "Prop",
 			args: args{
@@ -220,9 +217,9 @@ func Test_isFirstChild(t *testing.T) {
 </Server>
 -->
 `
-	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	metasheet, content := splitRawXML(doc)
 	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(root, newBook)
+	xmlMeta, _ := readXMLFile(metasheet, content, newBook)
 	sheet1 := getXMLSheet(xmlMeta, "Server")
 	node1 := FindMetaNode(sheet1, "Server/MapConf/Weight")
 	type args struct {
@@ -233,7 +230,6 @@ func Test_isFirstChild(t *testing.T) {
 		args args
 		want bool
 	}{
-		// TODO: Add test cases.
 		{
 			name: "Weight",
 			args: args{
@@ -305,9 +301,9 @@ func Test_fixNodeType(t *testing.T) {
 	</Broadcast>
 </MatchCfg>
 `
-	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	metasheet, content := splitRawXML(doc)
 	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(root, newBook)
+	xmlMeta, _ := readXMLFile(metasheet, content, newBook)
 	sheet1 := getXMLSheet(xmlMeta, "MatchCfg")
 	node1 := FindMetaNode(sheet1, "MatchCfg/MatchMode/MatchAI/AI")
 	node2 := FindMetaNode(sheet1, "MatchCfg/MapConf/Test/Weight")
@@ -329,7 +325,6 @@ func Test_fixNodeType(t *testing.T) {
 		args args
 		want string
 	}{
-		// TODO: Add test cases.
 		{
 			name: "MatchCfg/MatchMode/MatchAI/AI",
 			args: args{
@@ -463,7 +458,6 @@ func Test_getNodePath(t *testing.T) {
 		wantRoot *xmlquery.Node
 		wantPath string
 	}{
-		// TODO: Add test cases.
 		{
 			name: "MatchCfg/MapConf/Weight",
 			args: args{
@@ -503,7 +497,6 @@ func Test_isCrossCell(t *testing.T) {
 		args args
 		want bool
 	}{
-		// TODO: Add test cases.
 		{
 			name: "map<uint32, Item>",
 			args: args{
@@ -524,7 +517,7 @@ func Test_isCrossCell(t *testing.T) {
 				t: "[]int32",
 			},
 			want: false,
-		},		
+		},
 		{
 			name: "[.Item]uint32",
 			args: args{
@@ -545,7 +538,7 @@ func Test_isCrossCell(t *testing.T) {
 				t: "{int32 ID,string Name,string Desc}Prop",
 			},
 			want: false,
-		},	
+		},
 		{
 			name: "{.Item}uint32",
 			args: args{
@@ -562,3 +555,562 @@ func Test_isCrossCell(t *testing.T) {
 		})
 	}
 }
+
+func Test_genMetasheet(t *testing.T) {
+	metaDoc := fmt.Sprintf(`
+<?xml version='1.0' encoding='UTF-8'?>
+<%v>
+	<Item Sheet="ServerConf" Sep="|" />
+</%v>
+`, atTableauDisplacement, atTableauDisplacement)
+
+	metaRoot, _ := xmlquery.Parse(strings.NewReader(metaDoc))
+	node := xmlquery.FindOne(metaRoot, atTableauDisplacement)
+
+	type args struct {
+		tableauNode *xmlquery.Node
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string]map[string]string
+		want1   *book.Sheet
+		wantErr bool
+	}{
+		{
+			name: "Common Case",
+			args: args{
+				tableauNode: node,
+			},
+			want: map[string]map[string]string{
+				"ServerConf": {
+					"Nested": "true",
+					"Sheet":  "ServerConf",
+					"Sep":    "|",
+					"Nameline": "1",
+					"Typeline": "1",
+				},
+			},
+			want1: &book.Sheet{
+				Name:   book.MetasheetName,
+				MaxRow: 2,
+				MaxCol: 5,
+				Rows: [][]string{
+					{
+						"Nameline",
+						"Nested",
+						"Sep",
+						"Sheet",
+						"Typeline",
+					},
+					{
+						"1",
+						"true",
+						"|",
+						"ServerConf",
+						"1",
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, got1, err := genMetasheet(tt.args.tableauNode)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("genMetasheet() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("genMetasheet() got = %v, want %v", got, tt.want)
+			}
+			if !reflect.DeepEqual(got1, tt.want1) {
+				t.Errorf("genMetasheet() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
+}
+
+func Test_addMetaNodeAttr(t *testing.T) {
+	type args struct {
+		attrMap *tableaupb.XMLNode_AttrMap
+		name    string
+		val     string
+	}
+	tests := []struct {
+		name string
+		args args
+	}{
+		{
+			name: "Common Case",
+			args: args{
+				attrMap: newOrderedAttrMap(),
+				name:    "Num",
+				val:     "int32",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			addMetaNodeAttr(tt.args.attrMap, tt.args.name, tt.args.val)
+		})
+	}
+}
+
+func Test_addDataNodeAttr(t *testing.T) {
+	type args struct {
+		metaMap *tableaupb.XMLNode_AttrMap
+		dataMap *tableaupb.XMLNode_AttrMap
+		name    string
+		val     string
+	}
+	tests := []struct {
+		name string
+		args args
+	}{
+		{
+			name: "Common Case",
+			args: args{
+				metaMap: newOrderedAttrMap(),
+				dataMap: newOrderedAttrMap(),
+				name:    "Num",
+				val:     "100",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			addDataNodeAttr(tt.args.metaMap, tt.args.dataMap, tt.args.name, tt.args.val)
+		})
+	}
+}
+
+func Test_readXMLFile(t *testing.T) {
+	// 	doc := `
+	// <?xml version='1.0' encoding='UTF-8'?>
+	// <!--
+	// <@TABLEAU>
+	// 	<Item Sheet="Server" />
+	// </@TABLEAU>
+
+	// <Server>
+	// 	<Weight Num="map&lt;uint32,Weight&gt;"/>
+	// </Server>
+	// -->
+
+	// <Server>
+	// 	<Weight Num="1"/>
+	// 	<Weight Num="2"/>
+	// </Server>
+	// `
+	// 		root, _ := xmlquery.Parse(strings.NewReader(doc))
+	// 		newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
+
+	type args struct {
+		metasheet, content string
+		newBook *book.Book
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *tableaupb.XMLBook
+		wantErr bool
+	}{
+		// TODO:
+		// {
+		// 	name: "MapConf",
+		// 	args: args{
+		// 		root: root,
+		// 		newBook: newBook,
+		// 	},
+		// 	want: &tableaupb.XMLBook{
+		// 		SheetMap: map[string]int32{
+		// 			"Server": 0,
+		// 		},
+		// 		SheetList: []*tableaupb.XMLSheet{
+		// 			{
+		// 				Meta: &tableaupb.XMLNode{
+		// 					Name: "Server",
+		// 					Path: "Server",
+		// 					AttrMap: newOrderedAttrMap(),
+		// 					ChildMap: map[string]*tableaupb.XMLNode_IndexList{
+		// 						"Weight": {
+		// 							Indexes: []int32{
+		// 								0,
+		// 							},
+		// 						},
+		// 					},
+		// 					ChildList: []*tableaupb.XMLNode{
+		// 						{
+		// 							Name: "Weight",
+		// 							AttrMap: &tableaupb.XMLNode_AttrMap{
+		// 								Map: map[string]int32{
+		// 									"Num": 0,
+		// 								},
+		// 								List: []*tableaupb.XMLNode_AttrMap_Attr{
+		// 									{
+		// 										Name: "Num",
+		// 										Value: "map<uint32,Weight>",
+		// 									},
+		// 								},
+		// 							},
+		// 						},
+		// 					},
+		// 				},
+		// 			},
+		// 		},
+		// 	},
+		// },
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := readXMLFile(tt.args.metasheet, tt.args.content, tt.args.newBook)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("readXMLFile() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("readXMLFile() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_hasChild(t *testing.T) {
+	doc := `
+<Conf>
+	<Client Open="true">
+		<Num>100</Num>	
+		<Item ID="123" />
+	</Client>
+	
+	<Server>
+		<!-- Comments -->
+	
+	</Server>
+</Conf>
+`
+	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	node1 := xmlquery.FindOne(root, "Conf/Client")
+	node2 := xmlquery.FindOne(root, "Conf/Client/Num")
+	node3 := xmlquery.FindOne(root, "Conf/Client/Item")
+	node4 := xmlquery.FindOne(root, "Conf/Server")
+
+	type args struct {
+		n *xmlquery.Node
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "Conf/Client",
+			args: args{
+				n: node1,
+			},
+			want: true,
+		},
+		{
+			name: "Conf/Client/Num",
+			args: args{
+				n: node2,
+			},
+			want: false,
+		},
+		{
+			name: "Conf/Client/Item",
+			args: args{
+				n: node3,
+			},
+			want: false,
+		},
+		{
+			name: "Conf/Server",
+			args: args{
+				n: node4,
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := hasChild(tt.args.n); got != tt.want {
+				t.Errorf("hasChild() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_getTextContent(t *testing.T) {
+	doc := `
+<Conf>
+	<Client Open="true">
+		<Num>100</Num>	
+		<Item ID="123" />
+	</Client>
+	
+	<Server>
+		<!-- Comments -->
+	
+	</Server>
+</Conf>
+`
+	root, _ := xmlquery.Parse(strings.NewReader(doc))
+	node1 := xmlquery.FindOne(root, "Conf/Client")
+	node2 := xmlquery.FindOne(root, "Conf/Client/Num")
+	node3 := xmlquery.FindOne(root, "Conf/Client/Item")
+	node4 := xmlquery.FindOne(root, "Conf/Server")
+
+	type args struct {
+		n *xmlquery.Node
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "Conf/Client",
+			args: args{
+				n: node1,
+			},
+			want: "",
+		},
+		{
+			name: "Conf/Client/Num",
+			args: args{
+				n: node2,
+			},
+			want: "100",
+		},
+		{
+			name: "Conf/Client/Item",
+			args: args{
+				n: node3,
+			},
+			want: "",
+		},
+		{
+			name: "Conf/Server",
+			args: args{
+				n: node4,
+			},
+			want: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := getTextContent(tt.args.n); got != tt.want {
+				t.Errorf("getTextContent() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_matchMetasheet(t *testing.T) {
+	doc := `
+<?xml version='1.0' encoding='UTF-8'?>
+<!--
+<@TABLEAU>
+	<Item Sheet="Server" />
+</@TABLEAU>
+
+<Server>
+	<Weight Num="map<uint32, Weight>"/>
+</Server>
+-->
+
+<!--
+		Comments
+		Comments
+		Comments	
+-->
+
+<Server>
+	<Weight Num="1"/>
+	<Weight Num="2"/>
+</Server>
+`
+	type args struct {
+		s string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		{
+			name: "Whole document",
+			args: args{
+				s: doc,
+			},
+			want: []string{
+				`<!--
+<@TABLEAU>
+	<Item Sheet="Server" />
+</@TABLEAU>
+
+<Server>
+	<Weight Num="map<uint32, Weight>"/>
+</Server>
+-->`,
+				`<@TABLEAU>
+	<Item Sheet="Server" />
+</@TABLEAU>
+
+<Server>
+	<Weight Num="map<uint32, Weight>"/>
+</Server>
+`,
+				`>
+	<Item Sheet="Server" />
+</@TABLEAU>`,
+				`
+	<Item Sheet="Server" />
+`,
+				` Sheet="Server"`,
+				`</Server>
+`,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := matchMetasheet(tt.args.s); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("matchMetasheet() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_splitRawXML(t *testing.T) {
+	doc := `<?xml version='1.0' encoding='UTF-8'?>
+<!--
+<@TABLEAU>
+	<Item Sheet="Server" />
+</@TABLEAU>
+
+<Server>
+	<Weight Num="map<uint32, Weight>"/>
+</Server>
+-->
+
+<Server>
+	<Weight Num="1"/>
+	<Weight Num="2"/>
+</Server>`
+
+	type args struct {
+		rawXML string
+	}
+	tests := []struct {
+		name          string
+		args          args
+		wantMetasheet string
+		wantContent   string
+	}{
+		{
+			name: "Whole document",
+			args: args{
+				rawXML: doc,
+			},
+			wantMetasheet: `<?xml version='1.0' encoding='UTF-8'?>
+<ATABLEAU>
+	<Item Sheet="Server" />
+</ATABLEAU>
+
+<Server>
+	<Weight Num="map&lt;uint32, Weight&gt;"/>
+</Server>
+`,
+			wantContent: `<?xml version='1.0' encoding='UTF-8'?>
+
+
+<Server>
+	<Weight Num="1"/>
+	<Weight Num="2"/>
+</Server>`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotMetasheet, gotContent := splitRawXML(tt.args.rawXML)
+			if gotMetasheet != tt.wantMetasheet {
+				t.Errorf("splitRawXML() gotMetasheet = %v, want %v", gotMetasheet, tt.wantMetasheet)
+			}
+			if gotContent != tt.wantContent {
+				t.Errorf("splitRawXML() gotContent = %v, want %v", gotContent, tt.wantContent)
+			}
+		})
+	}
+}
+
+func Test_matchSheetBlock(t *testing.T) {
+	doc := `<?xml version='1.0' encoding='UTF-8'?>
+
+<Server>
+	<Weight Num="1"/>
+	{{ if a == 1 }}
+	<Weight Num="2">
+	{{ else }}
+	<Weight Num="3">
+	{{ endif }}
+		<Param value="1" />
+	</Weight>
+</Server>
+
+<Client>
+	<Weight Num="1"/>
+</Client>`
+	
+	type args struct {
+		xml       string
+		sheetName string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		{
+			name: "General",
+			args: args{
+				xml: doc,
+				sheetName: "Server",
+			},
+			want: []string{
+`<Server>
+	<Weight Num="1"/>
+	{{ if a == 1 }}
+	<Weight Num="2">
+	{{ else }}
+	<Weight Num="3">
+	{{ endif }}
+		<Param value="1" />
+	</Weight>
+</Server>`,
+`>
+	<Weight Num="1"/>
+	{{ if a == 1 }}
+	<Weight Num="2">
+	{{ else }}
+	<Weight Num="3">
+	{{ endif }}
+		<Param value="1" />
+	</Weight>
+</Server>`,
+`	</Weight>
+`,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := matchSheetBlock(tt.args.xml, tt.args.sheetName); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("matchSheetBlock() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/test/functest/conf/MetasheetConf.json b/test/functest/conf/MetasheetConf.json
index a6d61b2..ec74a67 100644
--- a/test/functest/conf/MetasheetConf.json
+++ b/test/functest/conf/MetasheetConf.json
@@ -1,6 +1,7 @@
 {
-    "sepConf": {
-        "numberList": [
+    "maxNum":  100,
+    "sepConf":  {
+        "numberList":  [
             1,
             2,
             3
diff --git a/test/functest/proto/xml__metasheet__metasheet.proto b/test/functest/proto/xml__metasheet__metasheet.proto
index 31991d1..4b6d385 100644
--- a/test/functest/proto/xml__metasheet__metasheet.proto
+++ b/test/functest/proto/xml__metasheet__metasheet.proto
@@ -12,7 +12,8 @@ option (tableau.workbook) = {name:"xml/metasheet/Metasheet.xml"};
 message MetasheetConf {
   option (tableau.worksheet) = {name:"MetasheetConf" namerow:1 typerow:2 noterow:3 datarow:4 nameline:1 typeline:1 nested:true sep:";"};
 
-  SepConf sep_conf = 1 [(tableau.field) = {name:"SepConf"}];
+  int32 max_num = 1 [(tableau.field) = {name:"MaxNum"}];
+  SepConf sep_conf = 2 [(tableau.field) = {name:"SepConf"}];
   message SepConf {
     repeated int32 number_list = 1 [(tableau.field) = {name:"number" layout:LAYOUT_INCELL}];
   }
diff --git a/test/functest/testdata/xml/metasheet/Metasheet.xml b/test/functest/testdata/xml/metasheet/Metasheet.xml
index c697c12..b11d369 100644
--- a/test/functest/testdata/xml/metasheet/Metasheet.xml
+++ b/test/functest/testdata/xml/metasheet/Metasheet.xml
@@ -7,6 +7,7 @@
 
 <MetasheetConf>
     <SepConf number="[]int32" />
+    <MaxNum>int32</MaxNum>
 </MetasheetConf>
 
 <TemplateConf>
@@ -16,6 +17,9 @@
 
 <MetasheetConf>
     <SepConf number="1;2;3" />
+    
+    <!-- Test -->
+    <MaxNum>100</MaxNum>
 </MetasheetConf>
 
 <TemplateConf>