Repository: tableauio/tableau
Pull request #124 (2024-08-13 03:38:45)

protogen(xml): improve XML importer v2
1. use `github.com/subchen/go-xmldom` instead of `github.com/antchfx/xmlquery` to parse xml content.
2. better support for `incell list` and `incell map` in xml attribute or xml text, remove support for `keyed list`.
3. support `empty struct` in both xml and yaml.
4. support `predefined` struct in xml.
5. only parse metasheet in xml protogen, and only parse content in xml confgen.
6. support `default` property in both xml and yaml.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tableauio/tableau/pull/124?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
Attention: Patch coverage is `55.55556%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 67.05%. Comparing base [(`e7bf16f`)](https://app.codecov.io/gh/tableauio/tableau/commit/e7bf16f54de0fc114532a028d6d7b74877eb58e9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) to head [(`a3d2e3f`)](https://app.codecov.io/gh/tableauio/tableau/commit/a3d2e3f01ee357fbe0f7088b821ec6d7558885f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).

| [Files](https://app.codecov.io/gh/tableauio/tableau/pull/124?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) | Patch % | Lines |
|---|---|---|
| [internal/importer/xml.go](https://app.codecov.io/gh/tableauio/tableau/pull/124?src=pr&el=tree&filepath=internal%2Fimporter%2Fxml.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvaW1wb3J0ZXIveG1sLmdv) | 55.55% | [3 Missing and 1 partial :warning: ](https://app.codecov.io/gh/tableauio/tableau/pull/124?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #124      +/-   ##
==========================================
- Coverage   67.11%   67.05%   -0.07%     
==========================================
  Files          44       44              
  Lines        5598     5548      -50     
==========================================
- Hits         3757     3720      -37     
+ Misses       1354     1344      -10     
+ Partials      487      484       -3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tableauio/tableau/pull/124?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).


Patch:
diff --git a/go.mod b/go.mod
index 01fdc81..2feeee6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/tableauio/tableau
 go 1.18
 
 require (
-	github.com/antchfx/xmlquery v1.3.15
 	github.com/davecgh/go-spew v1.1.1
 	github.com/emirpasic/gods v1.18.1
 	github.com/iancoleman/strcase v0.2.0
@@ -13,6 +12,7 @@ require (
 	github.com/rogpeppe/go-internal v1.10.0
 	github.com/spf13/cobra v1.7.0
 	github.com/stretchr/testify v1.8.2
+	github.com/subchen/go-xmldom v1.1.2
 	github.com/xuri/excelize/v2 v2.6.1
 	go.uber.org/multierr v1.8.0
 	go.uber.org/zap v1.24.0
@@ -25,7 +25,6 @@ require (
 
 require (
 	github.com/antchfx/xpath v1.2.3 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index d78d285..836da29 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/antchfx/xmlquery v1.3.15 h1:aJConNMi1sMha5G8YJoAIF5P+H+qG1L73bSItWHo8Tw=
-github.com/antchfx/xmlquery v1.3.15/go.mod h1:zMDv5tIGjOxY/JCNNinnle7V/EwthZ5IT8eeCGJKRWA=
+github.com/antchfx/xpath v0.0.0-20170515025933-1f3266e77307/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
 github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
 github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
@@ -19,8 +18,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -92,13 +89,14 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/subchen/go-xmldom v1.1.2 h1:7evI2YqfYYOnuj+PBwyaOZZYjl3iWq35P6KfBUw9jeU=
+github.com/subchen/go-xmldom v1.1.2/go.mod h1:6Pg/HuX5/T4Jlj0IPJF1sRxKVoI/rrKP6LIMge9d5/8=
 github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
 github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
 github.com/xuri/excelize/v2 v2.6.1 h1:ICBdtw803rmhLN3zfvyEGH3cwSmZv+kde7LhTDT659k=
 github.com/xuri/excelize/v2 v2.6.1/go.mod h1:tL+0m6DNwSXj/sILHbQTYsLi9IF4TW59H2EF3Yrx1AU=
 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
@@ -108,7 +106,6 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
 go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c=
 golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -117,19 +114,14 @@ golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeap
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -137,7 +129,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -147,20 +138,14 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -168,9 +153,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/internal/confgen/document_parser.go b/internal/confgen/document_parser.go
index 11bd4e3..4c3dae9 100644
--- a/internal/confgen/document_parser.go
+++ b/internal/confgen/document_parser.go
@@ -56,6 +56,13 @@ func (sp *documentParser) parseMessage(msg protoreflect.Message, node *book.Node
 				}
 			} else {
 				fieldNode = node.FindChild(field.opts.Name)
+				if fieldNode == nil && xproto.GetFieldDefaultValue(fd) != "" {
+					// if this field has a default value, use virtual node
+					fieldNode = &book.Node{
+						Name:  node.Name,
+						Value: node.Value,
+					}
+				}
 				if fieldNode == nil {
 					if sp.parser.IsFieldOptional(field) {
 						// field not found and is optional, just return nil.
@@ -129,7 +136,30 @@ func (sp *documentParser) parseMapField(field *Field, msg protoreflect.Message,
 	} else {
 		if valueFd.Kind() == protoreflect.MessageKind {
 			for _, elemNode := range node.Children {
-				newMapKey, keyPresent, err := sp.parser.parseMapKey(field, reflectMap, elemNode.Name)
+				var keyData string
+				switch node.Kind {
+				case book.MapNode:
+					keyData = elemNode.Name
+					// auto add virtual key node
+					keyNode := &book.Node{
+						Kind:     book.ScalarNode,
+						Name:     field.opts.Key,
+						Value:    elemNode.Name,
+						Children: nil,
+						NamePos:  node.NamePos,
+						ValuePos: node.ValuePos,
+					}
+					elemNode.Children = append(elemNode.Children, keyNode)
+				case book.ListNode:
+					keyNode := elemNode.FindChild(field.opts.Key)
+					if keyNode == nil {
+						return false, xerrors.WrapKV(xerrors.E2018(field.opts.Key), elemNode.DebugKV()...)
+					}
+					keyData = keyNode.Value
+				default:
+					return false, xerrors.ErrorKV("should not reach here", node.DebugKV()...)
+				}
+				newMapKey, keyPresent, err := sp.parser.parseMapKey(field, reflectMap, keyData)
 				if err != nil {
 					return false, xerrors.WithMessageKV(err, elemNode.DebugKV()...)
 				}
@@ -139,16 +169,6 @@ func (sp *documentParser) parseMapField(field *Field, msg protoreflect.Message,
 				} else {
 					newMapValue = reflectMap.NewValue()
 				}
-				// auto add virtual key node
-				keyNode := &book.Node{
-					Kind:     book.ScalarNode,
-					Name:     field.opts.Key,
-					Value:    elemNode.Name,
-					Children: nil,
-					NamePos:  node.NamePos,
-					ValuePos: node.ValuePos,
-				}
-				elemNode.Children = append(elemNode.Children, keyNode)
 				valuePresent, err := sp.parseMessage(newMapValue.Message(), elemNode)
 				if err != nil {
 					return false, xerrors.WithMessageKV(err, elemNode.DebugKV()...)
@@ -354,6 +374,12 @@ func (sp *documentParser) parseStructField(field *Field, msg protoreflect.Messag
 		}
 		return present, nil
 	} else {
+		if node.Kind == book.ListNode {
+			if len(node.Children) != 1 {
+				return false, xerrors.ErrorKV("list node of struct must have and only have one child", node.DebugKV()...)
+			}
+			node = node.Children[0]
+		}
 		present, err := sp.parseMessage(structValue.Message(), node)
 		if err != nil {
 			return false, xerrors.WithMessageKV(err, node.DebugKV()...)
diff --git a/internal/importer/importer.go b/internal/importer/importer.go
index 1113615..278fb94 100644
--- a/internal/importer/importer.go
+++ b/internal/importer/importer.go
@@ -44,7 +44,7 @@ func New(filename string, setters ...Option) (Importer, error) {
 	case format.CSV:
 		return NewCSVImporter(filename, opts.Sheets, opts.Parser, opts.Mode, opts.Cloned)
 	case format.XML:
-		return NewXMLImporter(filename, opts.Sheets, opts.Parser, opts.Mode, opts.Cloned, opts.PrimaryBookName)
+		return NewXMLImporter(filename, opts.Sheets, opts.Parser, opts.Mode, opts.Cloned)
 	case format.YAML:
 		return NewYAMLImporter(filename, opts.Sheets, opts.Parser, opts.Mode, opts.Cloned)
 	default:
diff --git a/internal/importer/xml.go b/internal/importer/xml.go
index 03304f7..39f1c4d 100644
--- a/internal/importer/xml.go
+++ b/internal/importer/xml.go
@@ -5,32 +5,33 @@ import (
 	"bytes"
 	"encoding/xml"
 	"fmt"
+	"io"
+	"log"
 	"os"
 	"path/filepath"
 	"regexp"
-	"strconv"
 	"strings"
 
-	"github.com/antchfx/xmlquery"
 	"github.com/pkg/errors"
+	"github.com/subchen/go-xmldom"
 	"github.com/tableauio/tableau/internal/importer/book"
 	"github.com/tableauio/tableau/internal/types"
-	"github.com/tableauio/tableau/log"
-	"github.com/tableauio/tableau/proto/tableaupb"
 )
 
 type XMLImporter struct {
 	*book.Book
 }
 
-var attrRegexp *regexp.Regexp
-var tagRegexp *regexp.Regexp
-var scalarListRegexp *regexp.Regexp
-var metasheetRegexp *regexp.Regexp
+var (
+	attrRegexp      *regexp.Regexp
+	tagRegexp       *regexp.Regexp
+	metasheetRegexp *regexp.Regexp
+)
 
 const (
 	xmlProlog             = `<?xml version='1.0' encoding='UTF-8'?>`
 	atTableauDisplacement = `ATABLEAU`
+	atTypeDisplacement    = "ATYPE"
 	ungreedyPropGroup     = `(\|\{[^\{\}]+\})?`                       // e.g.: |{default:"100"}
 	metasheetItemBlock    = `<Item(\s+\S+\s*=\s*("\S+"|'\S+'))+\s*/>` // e.g.: <Item Sheet="XXXConf" Sep="|"/>
 	sheetBlock            = `<%v(>(.*\n)*</%v>|\s*/>)`                // e.g.: <XXXConf>...</XXXConf>
@@ -39,8 +40,6 @@ const (
 func init() {
 	attrRegexp = regexp.MustCompile(`\s*=\s*("|')` + types.TypeGroup + ungreedyPropGroup + `("|')`) // e.g.: = "int32|{range:"1,~"}"
 	tagRegexp = regexp.MustCompile(`>` + types.TypeGroup + ungreedyPropGroup + `</`)                // e.g.: >int32|{range:"1,~"}</
-	scalarListRegexp = regexp.MustCompile(`([A-Za-z_]+)([0-9]+)`)                                   // e.g.: Para1, Para2, Para3, ...
-
 	// metasheet regexp, e.g.:
 	// <!--
 	// <@TABLEAU>
@@ -54,817 +53,276 @@ func init() {
 	metasheetRegexp = regexp.MustCompile(fmt.Sprintf(`<!--\s+(<%v(>(\s+`+metasheetItemBlock+`\s+)*</%v>|\s*/>)(.*\n)+?)-->\s*\n`, book.MetasheetName, book.MetasheetName))
 }
 
-// TODO: options
-func NewXMLImporter(filename string, sheets []string, parser book.SheetParser, mode ImporterMode, cloned bool, primaryBookName string) (*XMLImporter, error) {
-	newBook, err := parseXML(filename, sheets, parser, mode, cloned, primaryBookName)
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to parse xml:%s", filename)
-	}
-	if newBook == nil {
-		log.Debugf("xml:%s parsed to an empty book", filename)
-		bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
-		return &XMLImporter{
-			Book: book.NewBook(bookName, filename, nil),
-		}, nil
+func NewXMLImporter(filename string, sheets []string, parser book.SheetParser, mode ImporterMode, cloned bool) (*XMLImporter, error) {
+	var book *book.Book
+	var err error
+	if mode == Protogen {
+		book, err = readXMLBookWithOnlySchemaSheet(filename, parser)
+		if err != nil {
+			return nil, errors.WithMessagef(err, "failed to read csv book: %s", filename)
+		}
+		if err := book.ParseMetaAndPurge(); err != nil {
+			return nil, errors.WithMessage(err, "failed to parse metasheet")
+		}
+	} else {
+		book, err = readXMLBook(filename, parser)
+		if err != nil {
+			return nil, errors.WithMessagef(err, "failed to read csv book: %s", filename)
+		}
 	}
 
-	// log.Debugf("book: %+v", newBook)
-
+	// log.Debugf("book: %+v", book)
 	return &XMLImporter{
-		Book: newBook,
+		Book: book,
 	}, 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
-	}
-	scanner := bufio.NewScanner(strings.NewReader(matches[0]))
-	emptyLines := ""
-	for scanner.Scan() {
-		emptyLines += "\n"
-	}
-	if err := scanner.Err(); err != nil {
-		log.Panicf("scanner err:%v", err)
-		return "", rawXML
-	}
-	content = strings.ReplaceAll(rawXML, matches[0], emptyLines)
-	metasheet = xmlProlog + "\n" + escapeAttrs(strings.ReplaceAll(matches[1], book.MetasheetName, atTableauDisplacement))
-	return metasheet, content
-}
+func readXMLBook(filename string, parser book.SheetParser) (*book.Book, error) {
+	bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
+	newBook := book.NewBook(bookName, filename, parser)
 
-// 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, mode ImporterMode, cloned bool, primaryBookName string) (*book.Book, error) {
-	log.Debugf("xml:%s|cloned:%v|primaryBookName:%s", filename, cloned, primaryBookName)
-	buf, err := os.ReadFile(filename)
+	content, err := os.ReadFile(filename)
 	if err != nil {
 		return nil, err
 	}
-
-	metasheet, content := splitRawXML(string(buf))
-	// use primary book's metasheet if cloned
-	if cloned {
-		primaryBookBuf, err := os.ReadFile(primaryBookName)
-		if err != nil {
-			return nil, err
-		}
-		metasheet, _ = splitRawXML(string(primaryBookBuf))
-	}
-
-	// The first pass
-	bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
-	newBook := book.NewBook(bookName, filename, parser)
-	xmlMeta, err := readXMLFile(metasheet, content, newBook, mode)
+	rawDocs, err := extractRawXMLDocuments(string(content))
 	if err != nil {
 		return nil, err
 	}
-
-	for _, xmlSheet := range xmlMeta.SheetList {
-		sheetName := xmlSheet.Meta.Name
-		// The second pass
-		if err := preprocessMeta(xmlSheet, xmlSheet.Meta); err != nil {
-			return nil, errors.Wrapf(err, "failed to preprocessMeta for sheet: %s", sheetName)
-		}
-		if err := preprocessData(xmlSheet, xmlSheet.Data); err != nil {
-			return nil, errors.Wrapf(err, "failed to preprocessData for sheet: %s", sheetName)
-		}
-		// The third pass
-		metaSheet, dataSheet, err := genSheet(xmlSheet)
+	for _, rawDoc := range rawDocs {
+		doc, err := xmldom.ParseXML(rawDoc)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to genSheet for sheet: %s", sheetName)
+			return nil, err
 		}
-		newBook.AddSheet(metaSheet)
-		newBook.AddSheet(dataSheet)
-	}
-
-	// parse meta sheet
-	if parser != nil {
-		if err := newBook.ParseMetaAndPurge(); err != nil {
-			return nil, errors.WithMessage(err, "failed to parse metasheet")
+		sheet, err := parseXMLSheet(doc, UnknownMode)
+		if err != nil {
+			return nil, errors.WithMessagef(err, "file: %s", filename)
 		}
+		newBook.AddSheet(sheet)
 	}
-
-	if len(sheetNames) > 0 {
-		newBook.Squeeze(sheetNames)
-	}
-
 	return newBook, nil
 }
 
-// --------------------------------------------- THE FIRST PASS ------------------------------------ //
-// The first pass simply reads 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(metasheet, content string, newBook *book.Book, mode ImporterMode) (*tableaupb.XMLBook, error) {
-	xmlMeta := &tableaupb.XMLBook{
-		SheetMap: make(map[string]int32),
-	}
-	// sheetName -> {colName -> val}
-	metasheetMap := make(map[string]map[string]string)
-	// 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")
-			}
-			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)
-		}
-	}
+func readXMLBookWithOnlySchemaSheet(filename string, parser book.SheetParser) (*book.Book, error) {
+	bookName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
+	newBook := book.NewBook(bookName, filename, parser)
 
-	if mode == Protogen {
-		// 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], "")
-		}
+	content, err := os.ReadFile(filename)
+	if err != nil {
+		return nil, err
 	}
-
-	// parse data content
-	dataRoot, err := xmlquery.Parse(strings.NewReader(content))
+	metasheet := splitXMLMetasheet(string(content))
+	rawDocs, err := extractRawXMLDocuments(metasheet)
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to parse XML: %s", content)
+		return nil, err
 	}
-	for n := dataRoot.FirstChild; n != nil; n = n.NextSibling {
-		if n.Type != xmlquery.ElementNode {
-			continue
+	for _, rawDoc := range rawDocs {
+		doc, err := xmldom.ParseXML(rawDoc)
+		if err != nil {
+			return nil, err
 		}
-		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)
+		sheet, err := parseXMLSheet(doc, Protogen)
+		if err != nil {
+			return nil, errors.WithMessagef(err, "file: %s", filename)
 		}
+		newBook.AddSheet(sheet)
 	}
-
-	return xmlMeta, nil
+	return newBook, nil
 }
 
-// genMetasheet generates metasheet according to `
-//
-//	<@TABLEAU>
-//	    <Item Sheet="XXXConf" />
-//	</@TABLEAU>`
-func genMetasheet(tableauNode *xmlquery.Node) (map[string]map[string]string, *book.Sheet, error) {
-	root := &book.Node{
-		Kind: book.MapNode,
-		Children: []*book.Node{
-			{
-				Kind:  book.MapNode,
-				Name:  book.KeywordSheet,
-				Value: book.MetasheetName,
-			},
-		},
-	}
-	// sheetName -> {colName -> val}
-	metasheetMap := make(map[string]map[string]string)
-	for n := tableauNode.FirstChild; n != nil; n = n.NextSibling {
-		if n.Type != xmlquery.ElementNode {
-			continue
-		}
-		var children []*book.Node
-		sheetMap := make(map[string]string)
-		var sheetName string
-		for _, attr := range n.Attr {
-			name := attr.Name.Local
-			value := attr.Value
-			children = append(children, &book.Node{
-				Name:  name,
-				Value: value,
-			})
-			sheetMap[name] = value
-
-			if name == "Sheet" {
-				sheetName = value
-			}
-		}
-		if sheetName == "" {
-			return metasheetMap, nil, errors.Errorf("field `Sheet` not specified in metasheet @TABLEAU")
-		}
-		metasheetMap[sheetName] = sheetMap
-		sheetNode := &book.Node{
-			Kind:     book.MapNode,
-			Name:     sheetName,
-			Children: children,
+func parseXMLSheet(doc *xmldom.Document, mode ImporterMode) (*book.Sheet, error) {
+	name := doc.Root.Name
+	if mode == Protogen {
+		if name == atTableauDisplacement {
+			return parseXMLMetaSheet(doc)
 		}
-		root.Children = append(root.Children, sheetNode)
+		name = "@" + name
 	}
-	doc := &book.Node{
-		Kind:     book.DocumentNode,
-		Name:     book.MetasheetName,
-		Children: []*book.Node{root},
-	}
-	sheet := book.NewDocumentSheet(book.MetasheetName, doc)
-	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,
+	bnode := &book.Node{}
+	bnode.Kind = book.MapNode
+	bnode.Children = append(bnode.Children, &book.Node{
+		Name:  book.SheetKey,
+		Value: name,
 	})
-}
-
-// 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 ""
+	rootNode := &book.Node{}
+	if err := parseXMLNode(doc.Root, rootNode, mode); err != nil {
+		return nil, errors.Wrapf(err, "parse xml node failed")
 	}
-	for n := n.FirstChild; n != nil; n = n.NextSibling {
-		if n.Type == xmlquery.TextNode {
-			return strings.TrimSpace(n.Data)
-		}
+	if structNode := rootNode.FindChild(book.KeywordStruct); structNode != nil {
+		// used for protogen
+		bnode.Children = append(bnode.Children, structNode.Children...)
+	} else {
+		// used for confgen
+		bnode.Children = append(bnode.Children, rootNode.Children...)
 	}
-	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)
-	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)
-		}
-		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)
-			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)
-		}
-	}
-	return nil
+	sheet := book.NewDocumentSheet(
+		name,
+		&book.Node{
+			Name:     name,
+			Kind:     book.DocumentNode,
+			Children: []*book.Node{bnode},
+		},
+	)
+	return sheet, nil
 }
 
-// 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
-		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)
+func parseXMLMetaSheet(doc *xmldom.Document) (*book.Sheet, error) {
+	bnode := &book.Node{}
+	bnode.Kind = book.MapNode
+	bnode.Children = append(bnode.Children, &book.Node{
+		Name:  book.SheetKey,
+		Value: book.MetasheetName,
+	})
+	for _, child := range doc.Root.Children {
+		if child.Name != "Item" {
 			continue
 		}
-		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)
-		}
-		if err := parseDataNode(n, xmlSheet); err != nil {
-			return errors.Wrapf(err, "failed to parseDataNode for %s@%s", childName, meta.Name)
+		subNode := &book.Node{
+			Kind: book.MapNode,
 		}
-		if list, ok := data.ChildMap[childName]; !ok {
-			data.ChildMap[childName] = &tableaupb.XMLNode_IndexList{
-				Indexes: []int32{int32(len(data.ChildList))},
+		for _, attribute := range child.Attributes {
+			if attribute.Name == "Sheet" {
+				subNode.Name = attribute.Value
+			} else {
+				subNode.Children = append(subNode.Children, &book.Node{
+					Name:  attribute.Name,
+					Value: attribute.Value,
+				})
 			}
-		} else {
-			list.Indexes = append(list.Indexes, int32(len(data.ChildList)))
-		}
-		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\"", typeBuf.String(), propBuf.String())
-	})
-	escapedDoc = tagRegexp.ReplaceAllStringFunc(escapedDoc, func(s string) string {
-		matches := matchTag(s)
-		var typeBuf, propBuf bytes.Buffer
-		xml.EscapeText(&typeBuf, []byte(matches[1]))
-		xml.EscapeText(&propBuf, []byte(matches[2]))
-		return fmt.Sprintf(">%s%s</", 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)
-
-		}
-	}
-	return root, path
-}
-
-// 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 matchTag(s string) []string {
-	return tagRegexp.FindStringSubmatch(s)
-}
-
-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),
-	}
-}
-
-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 {
-		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 preprocesses 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 preprocessMeta(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 preprocessMeta
-	for _, child := range node.ChildList {
-		if err := preprocessMeta(xmlSheet, child); err != nil {
-			return errors.Wrapf(err, "failed to preprocessMeta node:%s", child.Name)
-		}
-	}
-	return nil
-}
-
-func preprocessData(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode) error {
-	path := node.Path
-	// read type info from meta node map
-	meta, ok := xmlSheet.MetaNodeMap[path]
-	if !ok {
-		return errors.Errorf("Node[%s] has no meta definition", path)
-	}
-	for _, attr := range meta.AttrMap.List {
-		mustFirst := isCrossCell(attr.Value)
-		if mustFirst {
-			swapAttr(node.AttrMap, int(node.AttrMap.Map[attr.Name]), 0)
-			break
-		}
-	}
-
-	// recursively preprocessData
-	for _, child := range node.ChildList {
-		if err := preprocessData(xmlSheet, child); err != nil {
-			return errors.Wrapf(err, "failed to preprocessData node:%s", child.Name)
 		}
+		bnode.Children = append(bnode.Children, subNode)
 	}
-	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])
+	sheet := book.NewDocumentSheet(
+		book.MetasheetName,
+		&book.Node{
+			Name:     book.MetasheetName,
+			Kind:     book.DocumentNode,
+			Children: []*book.Node{bnode},
+		},
+	)
+	return sheet, nil
+}
+
+func parseXMLNode(node *xmldom.Node, bnode *book.Node, mode ImporterMode) error {
+	switch mode {
+	case Protogen:
+		bnode.Kind = book.MapNode
+		bnode.Name = node.Name
+		if typeAttr := node.GetAttribute(atTypeDisplacement); typeAttr != nil {
+			fmt.Println(node.Name, typeAttr.Value)
+			// predefined struct
+			if len(node.Attributes) != 1 || len(node.Children) != 0 || node.Text != "" {
+				return errors.Errorf("predefined struct should not have children, text, or other attributes|name: %s", node.Name)
+			}
+			bnode.Kind = book.ScalarNode
+			bnode.Value = typeAttr.Value
+			return nil
+		}
+		// NOTE: curBNode may be pointed to one subnode when needed
+		curBNode := bnode
+		for i, attr := range node.Attributes {
+			var err error
+			curBNode, err = parseXMLAttribute(curBNode, attr.Name, attr.Value, i == 0)
 			if err != nil {
-				log.Errorf("strconv.Atoi failed|attr:%s|num:%s|err:%s", attr.Name, matches[2], err)
-				continue
+				return errors.WithMessagef(err, "parse xml attribute failed")
 			}
-			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 i == 1 {
-			attr := attrMap.List[index]
-			attr.Value = fmt.Sprintf("[]%s", attr.Value)
+		// generate struct even if encounter empty node
+		if len(node.Attributes) == 0 {
+			curBNode = &book.Node{
+				Kind: book.MapNode,
+				Name: book.KeywordStruct,
+			}
+			bnode.Children = append(bnode.Children, &book.Node{
+				Name:  book.KeywordType,
+				Value: fmt.Sprintf("{%s}", node.Name),
+			}, curBNode)
 		}
-		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
-//
-// NOTE: list and keyedlist auto-deduction not supported temporarily
-func fixNodeType(xmlSheet *tableaupb.XMLSheet, curr *tableaupb.XMLNode, oriType string) (t string) {
-	t = oriType
-	// 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) {
+		for _, child := range node.Children {
+			if child.Text != "" {
+				// child with text is regarded as an attribute
+				if len(child.Attributes) != 0 || len(child.Children) != 0 {
+					return errors.Errorf("node contains text so attributes and children must be empty|name: %s", child.Name)
+				}
+				_, err := parseXMLAttribute(curBNode, child.Name, child.Text, false)
+				if err != nil {
+					return errors.WithMessagef(err, "parse xml text-only child failed")
+				}
 				continue
 			}
-		} else {
-			// not the first attr or not the first child, fix ok
-			if len(n.AttrMap.List) > 0 || !isFirstChild(c) {
-				break
+			subNode := &book.Node{}
+			if err := parseXMLNode(child, subNode, mode); err != nil {
+				return errors.Wrapf(err, "parse xml node failed")
 			}
+			curBNode.Children = append(curBNode.Children, subNode)
 		}
-		if isRepeated(xmlSheet, n) {
-			if n == curr {
-				t = fmt.Sprintf("[%s]<%s>", n.Name, t)
-			} else {
-				t = fmt.Sprintf("[%s]%s", n.Name, t)
+	default:
+		if node.Text != "" {
+			if len(node.Attributes) != 0 || len(node.Children) != 0 {
+				return errors.Errorf("node contains text so attributes and children must be empty|name: %s", node.Name)
 			}
-		} else {
-			t = fmt.Sprintf("{%s}%s", n.Name, t)
+			bnode.Name = node.Name
+			bnode.Value = node.Text
+			break
 		}
-	}
-	return t
-}
-
-// 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
+		bnode.Kind = book.MapNode
+		bnode.Name = node.Name
+		for _, attr := range node.Attributes {
+			subNode := &book.Node{
+				Name:  attr.Name,
+				Value: attr.Value,
 			}
+			bnode.Children = append(bnode.Children, subNode)
 		}
-	}
-	return false
-}
-
-// isCrossCell check if type string `t` is a cross-cell type.
-func isCrossCell(t string) bool {
-	if types.IsMap(t) { // map case
-		desc := types.MatchMap(t)
-		return !(types.IsScalarType(desc.ValueType) || types.IsEnum(desc.ValueType))
-	} else if types.IsList(t) { // list case
-		desc := types.MatchList(t)
-		return desc.ElemType != ""
-	} else if types.IsKeyedList(t) { // keyed-list case
-		desc := types.MatchKeyedList(t)
-		return desc.ElemType != ""
-	} else if types.IsStruct(t) { // struct case
-		desc := types.MatchStruct(t)
-		return types.IsScalarType(desc.ColumnType) || types.IsEnum(desc.ColumnType)
-	}
-	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
-}
-
-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 be furtherly processed by `protogen`.
-func genSheet(xmlSheet *tableaupb.XMLSheet) (metaSheet *book.Sheet, dataSheet *book.Sheet, err error) {
-	sheetName := fmt.Sprintf("@%s", xmlSheet.Meta.Name)
-	root := &book.Node{Name: sheetName}
-	// fill meta nodes
-	if err := fillMetaNode(xmlSheet, xmlSheet.Meta, root); err != nil {
-		return nil, nil, errors.Wrapf(err, "failed to fillMetaNode for sheet: %s", sheetName)
-	}
-	metaSheet = book.NewDocumentSheet(sheetName, &book.Node{
-		Kind:     book.DocumentNode,
-		Name:     sheetName,
-		Children: []*book.Node{root},
-	})
-
-	sheetName = xmlSheet.Meta.Name
-	root = &book.Node{Name: sheetName}
-	// fill data nodes
-	if err := fillDataNode(xmlSheet, xmlSheet.Data, root); err != nil {
-		return nil, nil, errors.Wrapf(err, "failed to fillDataNode for sheet: %s", sheetName)
-	}
-	dataSheet = book.NewDocumentSheet(sheetName, &book.Node{
-		Kind:     book.DocumentNode,
-		Name:     sheetName,
-		Children: []*book.Node{root},
-	})
-	return metaSheet, dataSheet, nil
-}
-
-// fillMetaNode recursively read data from `node` and fill them to the data rows of `metaSheet`.
-func fillMetaNode(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode, bnode *book.Node) error {
-	path := node.Path
-	meta, ok := xmlSheet.MetaNodeMap[path]
-	if !ok {
-		return errors.Errorf("Node[%s] has no meta definition", path)
-	}
-	bnode.Kind = book.MapNode
-	if meta.Parent == nil {
-		// generate `@sheet: TableName` when level = 0
-		bnode.Children = append(bnode.Children, &book.Node{
-			Name:  book.KeywordSheet,
-			Value: bnode.Name,
-		})
-	}
-	// NOTE: curBNode may be pointed to one subnode when needed
-	curBNode := bnode
-	for i, attr := range node.AttrMap.List {
-		if desc := types.MatchMap(attr.Value); desc != nil {
-			curBNode = &book.Node{
-				Kind: book.MapNode,
-				Name: book.KeywordStruct,
-				Children: []*book.Node{
-					{
-						Name:  book.KeywordKey,
-						Value: attr.Name,
-					},
-					{
-						Name:  book.KeywordKeyname,
-						Value: attr.Name,
-					},
-				},
+		for _, child := range node.Children {
+			subNode := &book.Node{}
+			if err := parseXMLNode(child, subNode, mode); err != nil {
+				return errors.Wrapf(err, "parse xml node failed")
 			}
-			bnode.Children = append(bnode.Children, &book.Node{
-				Name:  book.KeywordType,
-				Value: attr.Value,
-			}, &book.Node{
-				Name:  book.KeywordVariable,
-				Value: fmt.Sprintf("%sMap", node.Name),
-			}, curBNode)
-		} else if desc := types.MatchKeyedList(attr.Value); desc != nil {
-			// treat keyed list as normal list
-			curBNode = &book.Node{
-				Kind: book.MapNode,
-				Name: book.KeywordStruct,
-				Children: []*book.Node{
-					{
-						Name:  book.KeywordKey,
-						Value: attr.Name,
-					},
-					{
-						Name:  attr.Name,
-						Value: desc.ColumnType,
-					},
-				},
-			}
-			bnode.Children = append(bnode.Children, &book.Node{
-				Name:  book.KeywordType,
-				Value: fmt.Sprintf("[%s]", node.Name),
-			}, &book.Node{
-				Name:  book.KeywordVariable,
-				Value: fmt.Sprintf("%sList", node.Name),
-			}, curBNode)
-		} else if desc := types.MatchList(attr.Value); desc != nil {
-			if desc.ElemType != "" {
-				// struct list
-				curBNode = &book.Node{
-					Kind: book.MapNode,
-					Name: book.KeywordStruct,
+			if subNode.Kind == book.ScalarNode {
+				bnode.Children = append(bnode.Children, subNode)
+			} else if existingBnode := bnode.FindChild(subNode.Name); existingBnode == nil {
+				bnode.Children = append(bnode.Children, &book.Node{
+					Kind: book.ListNode,
+					Name: subNode.Name,
 					Children: []*book.Node{
 						{
-							Name:  attr.Name,
-							Value: desc.ColumnType,
+							Kind:     book.MapNode,
+							Children: subNode.Children,
 						},
 					},
-				}
-				bnode.Children = append(bnode.Children, &book.Node{
-					Name:  book.KeywordType,
-					Value: fmt.Sprintf("[%s]", node.Name),
-				}, &book.Node{
-					Name:  book.KeywordVariable,
-					Value: fmt.Sprintf("%sList", node.Name),
-				}, curBNode)
+				})
 			} else {
-				// incell list
+				subNode.Name = ""
+				existingBnode.Children = append(existingBnode.Children, subNode)
+			}
+		}
+	}
+	return nil
+}
+
+func parseXMLAttribute(bnode *book.Node, attrName, attrValue string, isFirstAttr bool) (*book.Node, error) {
+	curBNode := bnode
+	if desc := types.MatchMap(attrValue); desc != nil {
+		valueDesc := types.ParseTypeDescriptor(desc.ValueType)
+		switch valueDesc.Kind {
+		case types.ScalarKind, types.EnumKind:
+			// incell map
+			if isFirstAttr {
 				curBNode = &book.Node{
 					Kind: book.MapNode,
 					Name: book.KeywordStruct,
 					Children: []*book.Node{
 						{
 							Kind: book.MapNode,
-							Name: attr.Name,
+							Name: attrName,
 							Children: []*book.Node{
 								{
 									Name:  book.KeywordType,
-									Value: fmt.Sprintf("[%s]", desc.ColumnType),
-								},
-								{
-									Name:  book.KeywordVariable,
-									Value: fmt.Sprintf("%sList", attr.Name),
+									Value: attrValue,
 								},
 								{
 									Name:  book.KeywordIncell,
@@ -876,187 +334,255 @@ func fillMetaNode(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode, bnode *
 				}
 				bnode.Children = append(bnode.Children, &book.Node{
 					Name:  book.KeywordType,
-					Value: fmt.Sprintf("{%s}", node.Name),
+					Value: fmt.Sprintf("{%s}", bnode.Name),
 				}, curBNode)
+				return curBNode, nil
 			}
-		} else if desc := types.MatchStruct(attr.Value); desc != nil {
-			curBNode = &book.Node{
+			bnode.Children = append(bnode.Children, &book.Node{
 				Kind: book.MapNode,
-				Name: book.KeywordStruct,
+				Name: attrName,
 				Children: []*book.Node{
 					{
-						Name:  attr.Name,
-						Value: desc.ColumnType,
+						Name:  book.KeywordType,
+						Value: attrValue,
+					},
+					{
+						Name:  book.KeywordIncell,
+						Value: "true",
 					},
 				},
+			})
+			return bnode, nil
+		default:
+			if !isFirstAttr {
+				return nil, errors.Errorf("vertical map not supported on non-first attributes")
 			}
-			bnode.Children = append(bnode.Children, &book.Node{
-				Name:  book.KeywordType,
-				Value: fmt.Sprintf("{%s}", node.Name),
-			}, curBNode)
-		} else if i == 0 && meta.Parent != nil {
-			// generate struct when first encounter scalar attribute
+			// vertical map
 			curBNode = &book.Node{
 				Kind: book.MapNode,
 				Name: book.KeywordStruct,
 				Children: []*book.Node{
 					{
-						Name:  attr.Name,
-						Value: attr.Value,
+						Name:  book.KeywordKey,
+						Value: attrName,
+					},
+					{
+						Name:  book.KeywordKeyname,
+						Value: attrName,
 					},
 				},
 			}
 			bnode.Children = append(bnode.Children, &book.Node{
 				Name:  book.KeywordType,
-				Value: fmt.Sprintf("{%s}", node.Name),
+				Value: attrValue,
+			}, &book.Node{
+				Name:  book.KeywordVariable,
+				Value: fmt.Sprintf("%sMap", bnode.Name),
 			}, curBNode)
-		} else {
-			curBNode.Children = append(curBNode.Children, &book.Node{
-				Name:  attr.Name,
-				Value: attr.Value,
-			})
-		}
-	}
-	// generate struct even if encounter empty node (level>=1)
-	if meta.Parent != nil && len(node.AttrMap.List) == 0 {
-		curBNode = &book.Node{
-			Kind: book.MapNode,
-			Name: book.KeywordStruct,
-		}
-		bnode.Children = append(bnode.Children, &book.Node{
-			Name:  book.KeywordType,
-			Value: fmt.Sprintf("{%s}", node.Name),
-		}, curBNode)
-	}
-	// iterate over child nodes
-	nodeMap := make(map[string]*book.Node)
-	for _, child := range node.ChildList {
-		tagName := child.Name
-		if subNode, existed := nodeMap[tagName]; existed {
-			if err := fillMetaNode(xmlSheet, child, subNode); err != nil {
-				return errors.Wrapf(err, "failed to fillMetaNode for %s@%s", tagName, node.Name)
-			}
-		} else {
-			subNode := &book.Node{
-				Name: tagName,
-			}
-			curBNode.Children = append(curBNode.Children, subNode)
-			if err := fillMetaNode(xmlSheet, child, subNode); err != nil {
-				return errors.Wrapf(err, "failed to fillMetaNode for %s@%s", tagName, node.Name)
-			}
-			nodeMap[tagName] = subNode
+			return curBNode, nil
 		}
-	}
-
-	return nil
-}
-
-// fillDataNode recursively read data from `node` and fill them to the data rows of `metaSheet`.
-func fillDataNode(xmlSheet *tableaupb.XMLSheet, node *tableaupb.XMLNode, bnode *book.Node) error {
-	path := node.Path
-	// read type info from meta node map
-	meta, ok := xmlSheet.MetaNodeMap[path]
-	if !ok {
-		return errors.Errorf("Node[%s] has no meta definition", path)
-	}
-	bnode.Kind = book.MapNode
-	if meta.Parent == nil {
-		// generate `@sheet: TableName` when level = 0
-		bnode.Children = append(bnode.Children, &book.Node{
-			Name:  book.KeywordSheet,
-			Value: bnode.Name,
-		})
-	}
-	// NOTE: curBNode may be pointed to one subnode when needed
-	curBNode := bnode
-	for _, attr := range node.AttrMap.List {
-		typeAttr := meta.AttrMap.List[meta.AttrMap.Map[attr.Name]].Value
-		if desc := types.MatchMap(typeAttr); desc != nil {
+	} else if desc := types.MatchList(attrValue); desc != nil {
+		if desc.ElemType != "" {
+			// struct list
 			curBNode = &book.Node{
 				Kind: book.MapNode,
-				Name: attr.Value,
+				Name: book.KeywordStruct,
 				Children: []*book.Node{
 					{
-						Name:  attr.Name,
-						Value: attr.Value,
+						Name:  attrName,
+						Value: desc.ColumnType,
 					},
 				},
 			}
-			bnode.Children = append(bnode.Children, curBNode)
-		} else if desc := types.MatchKeyedList(typeAttr); desc != nil {
-			//treat keyed list as normal list
+			if isFirstAttr {
+				bnode.Children = append(bnode.Children, &book.Node{
+					Name:  book.KeywordType,
+					Value: fmt.Sprintf("[%s]", bnode.Name),
+				}, &book.Node{
+					Name:  book.KeywordVariable,
+					Value: fmt.Sprintf("%sList", bnode.Name),
+				}, curBNode)
+				return curBNode, nil
+			} else {
+				bnode.Children = append(bnode.Children, curBNode.Children[0])
+				return bnode, nil
+			}
+		} else {
+			// incell list
 			curBNode = &book.Node{
 				Kind: book.MapNode,
+				Name: book.KeywordStruct,
 				Children: []*book.Node{
 					{
-						Name:  attr.Name,
-						Value: attr.Value,
+						Kind: book.MapNode,
+						Name: attrName,
+						Children: []*book.Node{
+							{
+								Name:  book.KeywordType,
+								Value: fmt.Sprintf("[%s]", desc.ColumnType),
+							},
+							{
+								Name:  book.KeywordVariable,
+								Value: fmt.Sprintf("%sList", attrName),
+							},
+							{
+								Name:  book.KeywordIncell,
+								Value: "true",
+							},
+						},
 					},
 				},
 			}
-			bnode.Kind = book.ListNode
-			bnode.Children = append(bnode.Children, curBNode)
-		} else if desc := types.MatchList(typeAttr); desc != nil {
-			if desc.ElemType != "" {
-				// struct list
-				curBNode = &book.Node{
-					Kind: book.MapNode,
-					Children: []*book.Node{
-						{
-							Name:  attr.Name,
-							Value: attr.Value,
-						},
-					},
-				}
-				bnode.Kind = book.ListNode
-				bnode.Children = append(bnode.Children, curBNode)
+			if isFirstAttr {
+				bnode.Children = append(bnode.Children, &book.Node{
+					Name:  book.KeywordType,
+					Value: fmt.Sprintf("{%s}", bnode.Name),
+				}, curBNode)
+				return curBNode, nil
 			} else {
-				// incell list
-				curBNode.Children = append(curBNode.Children, &book.Node{
-					Name:  attr.Name,
-					Value: attr.Value,
-				})
+				bnode.Children = append(bnode.Children, curBNode.Children[0])
+				return bnode, nil
 			}
-		} else if desc := types.MatchStruct(typeAttr); desc != nil {
+		}
+	} else if desc := types.MatchStruct(attrValue); desc != nil {
+		curBNode = &book.Node{
+			Kind: book.MapNode,
+			Name: book.KeywordStruct,
+			Children: []*book.Node{
+				{
+					Name:  attrName,
+					Value: desc.ColumnType,
+				},
+			},
+		}
+		if isFirstAttr {
 			bnode.Children = append(bnode.Children, &book.Node{
-				Name:  attr.Name,
-				Value: desc.ColumnType,
-			})
+				Name:  book.KeywordType,
+				Value: fmt.Sprintf("{%s}", bnode.Name),
+			}, curBNode)
+			return curBNode, nil
 		} else {
-			curBNode.Children = append(curBNode.Children, &book.Node{
-				Name:  attr.Name,
-				Value: attr.Value,
-			})
+			bnode.Children = append(bnode.Children, curBNode)
+			return bnode, nil
 		}
-	}
-	// iterate over child nodes
-	nodeMap := make(map[string]*book.Node)
-	for _, child := range node.ChildList {
-		tagName := child.Name
-		if subNode, existed := nodeMap[tagName]; existed {
-			if err := fillDataNode(xmlSheet, child, subNode); err != nil {
-				return errors.Wrapf(err, "failed to fillDataNode for %s@%s", tagName, node.Name)
-			}
-		} else {
-			subNode := &book.Node{
-				Name: tagName,
-			}
-			curBNode.Children = append(curBNode.Children, subNode)
-			if err := fillDataNode(xmlSheet, child, subNode); err != nil {
-				return errors.Wrapf(err, "failed to fillDataNode for %s@%s", tagName, node.Name)
-			}
-			nodeMap[tagName] = subNode
+	} else if isFirstAttr {
+		// generate struct when first encounter scalar attribute
+		curBNode = &book.Node{
+			Kind: book.MapNode,
+			Name: book.KeywordStruct,
+			Children: []*book.Node{
+				{
+					Name:  attrName,
+					Value: attrValue,
+				},
+			},
 		}
+		bnode.Children = append(bnode.Children, &book.Node{
+			Name:  book.KeywordType,
+			Value: fmt.Sprintf("{%s}", bnode.Name),
+		}, curBNode)
+		return curBNode, nil
+	} else {
+		bnode.Children = append(bnode.Children, &book.Node{
+			Name:  attrName,
+			Value: attrValue,
+		})
+		return bnode, nil
 	}
+}
 
-	return nil
+// splitXMLMetasheet splits metasheet from xml notes
+func splitXMLMetasheet(content string) string {
+	matches := metasheetRegexp.FindStringSubmatch(content)
+	if len(matches) < 2 {
+		return ""
+	}
+	scanner := bufio.NewScanner(strings.NewReader(matches[0]))
+	emptyLines := ""
+	for scanner.Scan() {
+		emptyLines += "\n"
+	}
+	if err := scanner.Err(); err != nil {
+		log.Panicf("scanner err:%v", err)
+		return ""
+	}
+	metasheet := matches[1]
+	metasheet = strings.ReplaceAll(metasheet, book.MetasheetName, atTableauDisplacement)
+	metasheet = strings.ReplaceAll(metasheet, book.KeywordType, atTypeDisplacement)
+	metasheet = escapeAttrs(metasheet)
+	metasheet = xmlProlog + "\n" + metasheet
+	return metasheet
 }
 
-func newPrefix(prefix, curNode, sheetName string) string {
-	// sheet name should not occur in the prefix
-	if curNode != sheetName {
-		return prefix + curNode
-	} else {
-		return prefix
+// 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 := attrRegexp.FindStringSubmatch(s)
+		var typeBuf, propBuf bytes.Buffer
+		xml.EscapeText(&typeBuf, []byte(matches[2]))
+		xml.EscapeText(&propBuf, []byte(matches[3]))
+		return fmt.Sprintf("=\"%s%s\"", typeBuf.String(), propBuf.String())
+	})
+	escapedDoc = tagRegexp.ReplaceAllStringFunc(escapedDoc, func(s string) string {
+		matches := tagRegexp.FindStringSubmatch(s)
+		var typeBuf, propBuf bytes.Buffer
+		xml.EscapeText(&typeBuf, []byte(matches[1]))
+		xml.EscapeText(&propBuf, []byte(matches[2]))
+		return fmt.Sprintf(">%s%s</", typeBuf.String(), propBuf.String())
+	})
+	return escapedDoc
+}
+
+// extractRawYAMLDocuments extracts raw XML into separate documents.
+func extractRawXMLDocuments(content string) ([]string, error) {
+	var results []string
+	reader := strings.NewReader(content)
+	decoder := xml.NewDecoder(reader)
+	var buffer bytes.Buffer
+	var encoder = xml.NewEncoder(&buffer)
+	depth := 0
+	for {
+		t, err := decoder.Token()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, fmt.Errorf("error reading token: %v", err)
+		}
+		switch tok := t.(type) {
+		case xml.StartElement:
+			depth++
+			if err := encoder.EncodeToken(tok); err != nil {
+				return nil, fmt.Errorf("error encoding %T token: %v", tok, err)
+			}
+		case xml.EndElement:
+			depth--
+			if err := encoder.EncodeToken(tok); err != nil {
+				return nil, fmt.Errorf("error encoding %T token: %v", tok, err)
+			}
+			if depth == 0 {
+				if err := encoder.Flush(); err != nil {
+					return nil, fmt.Errorf("error flushing encoder: %v", err)
+				}
+				results = append(results, buffer.String())
+				buffer.Reset()
+				encoder = xml.NewEncoder(&buffer)
+			}
+		case xml.CharData:
+			if err := encoder.EncodeToken(tok); err != nil {
+				return nil, fmt.Errorf("error encoding %T token: %v", tok, err)
+			}
+		}
 	}
+	return results, nil
 }
diff --git a/internal/localizer/i18n/config/ecode/en.yaml b/internal/localizer/i18n/config/ecode/en.yaml
index f9a3a24..9afb145 100644
--- a/internal/localizer/i18n/config/ecode/en.yaml
+++ b/internal/localizer/i18n/config/ecode/en.yaml
@@ -84,6 +84,10 @@ E2017:
   desc: map contains multiple empty keys
   text: '"{{.MapType}}" contains multiple empty keys'
   help: ensure there is at most one empty key in map, or assign them explicitly if you need multiple zero-value keys
+E2018:
+  desc: map key not exist
+  text: 'map key field "{{.KeyName}}" not exist'
+  help: ensure the key field is set in elements of map
 # [3000, 3999]: importer error
 E3000:
   desc: no workbook file found about sheet specifier
diff --git a/internal/localizer/i18n/config/ecode/zh.yaml b/internal/localizer/i18n/config/ecode/zh.yaml
index 7670fe9..abe8765 100644
--- a/internal/localizer/i18n/config/ecode/zh.yaml
+++ b/internal/localizer/i18n/config/ecode/zh.yaml
@@ -84,6 +84,10 @@ E2017:
   desc: map contains multiple empty keys
   text: '"{{.MapType}}" 存在多个空值key'
   help: 请确保map中最多仅有一个空值key，如果确实需要多个key为零值，请显式填充
+E2018:
+  desc: map key not exist
+  text: 'map的key字段 "{{.KeyName}}" 不存在'
+  help: 请确保map的每个元素中都填写了key字段
 # [3000, 3999]: importer error
 E3000:
   desc: no workbook file found about sheet specifier
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 3b7ae83..6f65402 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -295,28 +295,36 @@ func (x *sheetExporter) exportField(depth int, tagid int, field *tableaupb.Field
 			x.Imports[typeInfo.ParentFilename] = true
 		}
 	} else {
-		if field.Fields != nil {
+		nestedMsgName := prefix + "." + typeName
+		switch {
+		case field.Fields != nil:
 			// iff field is a map or list and message type is not imported.
-			nestedMsgName := prefix + "." + typeName
 			if isSameFieldMessageType(field, x.nestedMessages[nestedMsgName]) {
 				// if the nested message is the same as the previous one,
 				// just use the previous one, and don't generate a new one.
 				return nil
 			}
+		case !types.IsScalarType(typeName):
+			if _, ok := x.nestedMessages[nestedMsgName]; ok {
+				// if the nested message has the same name with the previous one,
+				// just use the previous one, and don't generate a new one.
+				return nil
+			}
+		default:
+			return nil
+		}
+		// bookkeeping this nested msessage, so we can check if we can reuse it later.
+		x.nestedMessages[nestedMsgName] = field
 
-			// bookkeeping this nested msessage, so we can check if we can reuse it later.
-			x.nestedMessages[nestedMsgName] = field
-
-			// x.g.P("")
-			x.g.P(printer.Indent(depth), "message ", typeName, " {")
-			for i, f := range field.Fields {
-				tagid := i + 1
-				if err := x.exportField(depth+1, tagid, f, nestedMsgName); err != nil {
-					return err
-				}
+		// x.g.P("")
+		x.g.P(printer.Indent(depth), "message ", typeName, " {")
+		for i, f := range field.Fields {
+			tagid := i + 1
+			if err := x.exportField(depth+1, tagid, f, nestedMsgName); err != nil {
+				return err
 			}
-			x.g.P(printer.Indent(depth), "}")
 		}
+		x.g.P(printer.Indent(depth), "}")
 	}
 	return nil
 }
diff --git a/internal/protogen/parser.go b/internal/protogen/parser.go
index 8385e79..c755d0f 100644
--- a/internal/protogen/parser.go
+++ b/internal/protogen/parser.go
@@ -869,34 +869,7 @@ func parseTypeDescriptor(typeInfos *xproto.TypeInfos, rawType string) (*types.De
 			return nil, xerrors.Errorf("predefined type not found: %s", rawType)
 		}
 	}
-	switch rawType {
-	case "datetime", "date":
-		return &types.Descriptor{
-			Name:       types.WellKnownMessageTimestamp,
-			FullName:   types.WellKnownMessageTimestamp,
-			Predefined: true,
-			Kind:       types.ScalarKind,
-		}, nil
-	case "time", "duration":
-		return &types.Descriptor{
-			Name:       types.WellKnownMessageDuration,
-			FullName:   types.WellKnownMessageDuration,
-			Predefined: true,
-			Kind:       types.ScalarKind,
-		}, nil
-	default:
-		desc := &types.Descriptor{
-			Name:       rawType,
-			FullName:   rawType,
-			Predefined: false,
-		}
-		if types.IsScalarType(desc.Name) {
-			desc.Kind = types.ScalarKind
-		} else {
-			desc.Kind = types.MessageKind
-		}
-		return desc, nil
-	}
+	return types.ParseTypeDescriptor(rawType), nil
 }
 
 // parseIncellStruct parses incell struct type definition. For example:
diff --git a/internal/types/types.go b/internal/types/types.go
index c480e4b..d3f892e 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -380,3 +380,36 @@ type Descriptor struct {
 	Predefined bool
 	Kind       Kind
 }
+
+func ParseTypeDescriptor(rawType string) *Descriptor {
+	switch rawType {
+	case "datetime", "date":
+		return &Descriptor{
+			Name:       WellKnownMessageTimestamp,
+			FullName:   WellKnownMessageTimestamp,
+			Predefined: true,
+			Kind:       ScalarKind,
+		}
+	case "time", "duration":
+		return &Descriptor{
+			Name:       WellKnownMessageDuration,
+			FullName:   WellKnownMessageDuration,
+			Predefined: true,
+			Kind:       ScalarKind,
+		}
+	default:
+		desc := &Descriptor{
+			Name:       rawType,
+			FullName:   rawType,
+			Predefined: false,
+		}
+		if IsScalarType(desc.Name) {
+			desc.Kind = ScalarKind
+		} else if MatchEnum(rawType) != nil {
+			desc.Kind = EnumKind
+		} else {
+			desc.Kind = MessageKind
+		}
+		return desc
+	}
+}
diff --git a/internal/xproto/value.go b/internal/xproto/value.go
index 14063b2..53c2504 100644
--- a/internal/xproto/value.go
+++ b/internal/xproto/value.go
@@ -49,7 +49,7 @@ func init() {
 	DefaultDurationValue = pref.ValueOfMessage(du.ProtoReflect())
 }
 
-func getFieldDefaultValue(fd pref.FieldDescriptor) string {
+func GetFieldDefaultValue(fd pref.FieldDescriptor) string {
 	opts := fd.Options().(*descriptorpb.FieldOptions)
 	fieldOpts := proto.GetExtension(opts, tableaupb.E_Field).(*tableaupb.FieldOptions)
 	if fieldOpts != nil && fieldOpts.Prop != nil {
@@ -82,7 +82,7 @@ func ParseFieldValue(fd pref.FieldDescriptor, rawValue string, locationName stri
 	}
 
 	value := strings.TrimSpace(rawValue)
-	defaultValue := getFieldDefaultValue(fd)
+	defaultValue := GetFieldDefaultValue(fd)
 	if value == "" {
 		value = strings.TrimSpace(defaultValue)
 	}
diff --git a/proto/tableau/protobuf/xmlbook.proto b/proto/tableau/protobuf/xmlbook.proto
deleted file mode 100644
index c9d9316..0000000
--- a/proto/tableau/protobuf/xmlbook.proto
+++ /dev/null
@@ -1,64 +0,0 @@
-// Protoconf - Tableau's data interchange format
-// https://tableauio.github.io/
-
-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;  // sheet name -> index
-}
-
-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;   // path -> XMLNode
-  map<string, NodeList> data_node_map = 4;  // path -> XMLNode list
-
-  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;  // child name -> index list of child
-
-  // 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;  // attribute name -> index
-
-    message Attr {
-      string name = 1;
-      string value = 2;
-    }
-  }
-}
diff --git a/proto/tableaupb/xmlbook.pb.go b/proto/tableaupb/xmlbook.pb.go
deleted file mode 100644
index bf93b78..0000000
--- a/proto/tableaupb/xmlbook.pb.go
+++ /dev/null
@@ -1,704 +0,0 @@
-// Protoconf - Tableau's data interchange format
-// https://tableauio.github.io/
-
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.21.12
-// source: tableau/protobuf/xmlbook.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"` // sheet name -> index
-}
-
-func (x *XMLBook) Reset() {
-	*x = XMLBook{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_tableau_protobuf_xmlbook_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_xmlbook_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_xmlbook_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"` // path -> XMLNode
-	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"` // path -> XMLNode list
-}
-
-func (x *XMLSheet) Reset() {
-	*x = XMLSheet{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_tableau_protobuf_xmlbook_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_xmlbook_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_xmlbook_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"` // child name -> index list of child
-	// 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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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"` // attribute name -> index
-}
-
-func (x *XMLNode_AttrMap) Reset() {
-	*x = XMLNode_AttrMap{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_tableau_protobuf_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_proto protoreflect.FileDescriptor
-
-var file_tableau_protobuf_xmlbook_proto_rawDesc = []byte{
-	0x0a, 0x1e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
-	0x75, 0x66, 0x2f, 0x78, 0x6d, 0x6c, 0x62, 0x6f, 0x6f, 0x6b, 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_xmlbook_proto_rawDescOnce sync.Once
-	file_tableau_protobuf_xmlbook_proto_rawDescData = file_tableau_protobuf_xmlbook_proto_rawDesc
-)
-
-func file_tableau_protobuf_xmlbook_proto_rawDescGZIP() []byte {
-	file_tableau_protobuf_xmlbook_proto_rawDescOnce.Do(func() {
-		file_tableau_protobuf_xmlbook_proto_rawDescData = protoimpl.X.CompressGZIP(file_tableau_protobuf_xmlbook_proto_rawDescData)
-	})
-	return file_tableau_protobuf_xmlbook_proto_rawDescData
-}
-
-var file_tableau_protobuf_xmlbook_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
-var file_tableau_protobuf_xmlbook_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_xmlbook_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_xmlbook_proto_init() }
-func file_tableau_protobuf_xmlbook_proto_init() {
-	if File_tableau_protobuf_xmlbook_proto != nil {
-		return
-	}
-	if !protoimpl.UnsafeEnabled {
-		file_tableau_protobuf_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_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_xmlbook_proto_rawDesc,
-			NumEnums:      0,
-			NumMessages:   12,
-			NumExtensions: 0,
-			NumServices:   0,
-		},
-		GoTypes:           file_tableau_protobuf_xmlbook_proto_goTypes,
-		DependencyIndexes: file_tableau_protobuf_xmlbook_proto_depIdxs,
-		MessageInfos:      file_tableau_protobuf_xmlbook_proto_msgTypes,
-	}.Build()
-	File_tableau_protobuf_xmlbook_proto = out.File
-	file_tableau_protobuf_xmlbook_proto_rawDesc = nil
-	file_tableau_protobuf_xmlbook_proto_goTypes = nil
-	file_tableau_protobuf_xmlbook_proto_depIdxs = nil
-}
diff --git a/xerrors/ecode.go b/xerrors/ecode.go
index 0443177..a53771d 100644
--- a/xerrors/ecode.go
+++ b/xerrors/ecode.go
@@ -182,6 +182,13 @@ func E2017(mapType string) error {
 	})
 }
 
+// E2018: map key not exist.
+func E2018(keyName string) error {
+	return renderEcode("E2018", map[string]any{
+		"KeyName": keyName,
+	})
+}
+
 // E3000: no workbook file found about sheet specifier.
 func E3000(sheetSpecifier, pattern string) error {
 	return renderEcode("E3000", map[string]any{


Test patch:
diff --git a/internal/importer/testdata/Test.xml b/internal/importer/testdata/Test.xml
index 092270c..e1216b4 100644
--- a/internal/importer/testdata/Test.xml
+++ b/internal/importer/testdata/Test.xml
@@ -5,7 +5,12 @@
 </@TABLEAU>
 
 <Item>
-    <Item Id="map<uint32,Item>">
-    </Item>
+    <PreEntry Name="map<string, PreEntry>" Cost="[]int32" />
+    <Entry Id="map<uint32, Entry>" Value="int32" IncellMap="map<int32, string>" />
+    <Incell Map="map<string, int32>" />
+    <IncellMap>map<int32, int32></IncellMap>
+    <CrossCell Value="[CrossCell]int32" Name="string" Cost="[]int32" />
+    <Incell Value="[]int32" Cost="[]int32" />
+    <EnumIncell>[]enum<.FruitType></EnumIncell>
 </Item>
 -->
diff --git a/internal/importer/xml_test.go b/internal/importer/xml_test.go
index 1f03e40..9168f00 100644
--- a/internal/importer/xml_test.go
+++ b/internal/importer/xml_test.go
@@ -2,41 +2,92 @@ package importer
 
 import (
 	"fmt"
-	"reflect"
-	"strings"
 	"testing"
 
-	"github.com/antchfx/xmlquery"
 	"github.com/stretchr/testify/require"
+	"github.com/subchen/go-xmldom"
 	"github.com/tableauio/tableau/internal/importer/book"
-	"github.com/tableauio/tableau/log"
-	"github.com/tableauio/tableau/proto/tableaupb"
 )
 
-const (
-	matchDoc = `
-<?xml version='1.0' encoding='UTF-8'?>
-<!--
+func Test_inspectXMLNode(t *testing.T) {
+	// your byte array
+	data := []byte(`
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  
 <@TABLEAU>
-	<Item Sheet="MatchCfg" />
+  <Item Sheet="RankConf"/>
 </@TABLEAU>
-
-<MatchCfg open="bool">
-	<Broadcast Id="[Broadcast]int32" Priority="int32">
-		<BroadcastTime BeginTime="datetime" EndTime="datetime" Gap="duration" />
-		<Content txt="string"/>
-	</Broadcast>
-</MatchCfg>
+<RankConf>
+  <RankItem ID="map<uint32, RankItem>" Score="int32" Name="string"/>
+  <MaxScore>int32</MaxScore>
+</RankConf>
 -->
 
-<MatchCfg>
-	<Broadcast Id="2" Priority="2">
-		<BroadcastTime BeginTime="2016-11-15 19:30:10" EndTime="2022-04-21 23:29:59" Gap="60s" />
-		<Content txt="每分钟发送一次测试"/>
-	</Broadcast>
-</MatchCfg>
-`
-)
+<RankConf>
+  <RankItem ID="1" Score="100" Name="Tony"/>
+  <RankItem ID="2" Score="99" Name="Eric"/>
+  <RankItem ID="3" Score="98" Name="David"/>
+  <RankItem ID="4" Score="98" Name="Jenny"/>
+  <MaxScore>100</MaxScore>
+</RankConf>
+`)
+	// protogen
+	metasheet := splitXMLMetasheet(string(data))
+	rawDocs, err := extractRawXMLDocuments(metasheet)
+	require.NoError(t, err)
+	for _, rawDoc := range rawDocs {
+		doc, err := xmldom.ParseXML(rawDoc)
+		require.NoError(t, err)
+		sheet, err := parseXMLSheet(doc, Protogen)
+		require.NoError(t, err)
+		fmt.Println(sheet.String())
+	}
+	// confgen
+	rawDocs, err = extractRawXMLDocuments(string(data))
+	require.NoError(t, err)
+	for _, rawDoc := range rawDocs {
+		doc, err := xmldom.ParseXML(rawDoc)
+		require.NoError(t, err)
+		sheet, err := parseXMLSheet(doc, Confgen)
+		require.NoError(t, err)
+		fmt.Println(sheet.String())
+	}
+}
+
+func TestNewXMLImporter(t *testing.T) {
+	type args struct {
+		filename   string
+		sheetNames []string
+		parser     book.SheetParser
+		mode       ImporterMode
+		cloned     bool
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *XMLImporter
+		wantErr bool
+	}{
+		{
+			name: "Test.xml",
+			args: args{
+				filename: "testdata/Test.xml",
+				mode:     Protogen,
+				parser:   &TestSheetParser{},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := NewXMLImporter(tt.args.filename, tt.args.sheetNames, tt.args.parser, tt.args.mode, tt.args.cloned)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("NewYAMLImporter() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			fmt.Println(got.String())
+		})
+	}
+}
 
 func Test_escapeAttrs(t *testing.T) {
 	type args struct {
@@ -135,1358 +186,3 @@ func Test_escapeAttrs(t *testing.T) {
 		})
 	}
 }
-
-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>
-`
-	metasheet, content := splitRawXML(doc)
-	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(metasheet, content, newBook, Protogen)
-	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
-	}{
-		{
-			name: "node1",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node1,
-			},
-			want: true,
-		},
-		{
-			name: "node2",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node2,
-			},
-			want: true,
-		},
-		{
-			name: "node3",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node3,
-			},
-			want: false,
-		},
-		{
-			name: "sheet attr",
-			args: args{
-				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
-	}{
-		{
-			name: "scalar type",
-			args: args{
-				s: `<AAA bb = "bool" cc = "int64" dd = "enum<.EnumType>" >`,
-			},
-			want: []string{
-				` = "bool"`, `"`, `bool`, ``, `"`,
-			},
-		},
-		{
-			name: "Prop",
-			args: args{
-				s: `<Client OpenTime="datetime|{default:"2022-01-23 15:40:00"}" CloseTime="datetime|{default:"2022-01-23 15:40:00"}"/>`,
-			},
-			want: []string{
-				`="datetime|{default:"2022-01-23 15:40:00"}"`,
-				`"`, `datetime`, `|{default:"2022-01-23 15:40:00"}`, `"`,
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := matchAttr(tt.args.s); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("matchAttr() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func Test_isFirstChild(t *testing.T) {
-	doc := `
-<?xml version='1.0' encoding='UTF-8'?>
-<!--
-<@TABLEAU>
-	<Item Sheet="Server" />
-</@TABLEAU>
-
-<Server>
-    <MapConf>
-        <Weight Num="map&lt;uint32,Weight&gt;"/>
-    </MapConf>
-</Server>
--->
-`
-	metasheet, content := splitRawXML(doc)
-	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(metasheet, content, newBook, Protogen)
-	sheet1 := getXMLSheet(xmlMeta, "Server")
-	node1 := FindMetaNode(sheet1, "Server/MapConf/Weight")
-	type args struct {
-		curr *tableaupb.XMLNode
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "Weight",
-			args: args{
-				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>
-	<Item Sheet="MatchCfg" />
-</@TABLEAU>
-
-<MatchCfg open="bool">
-	<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>
-	
-	<Broadcast Id="[Broadcast]int32" Priority="int32">
-		<BroadcastTime BeginTime="datetime" EndTime="datetime" Gap="duration" />
-		<Content Txt="string"/>
-	</Broadcast>
-</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>
-	
-	<Broadcast Id="2" Priority="2">
-		<BroadcastTime BeginTime="2016-11-15 19:30:10" EndTime="2022-04-21 23:29:59" Gap="60s" />
-		<Content txt="每分钟发送一次测试"/>
-	</Broadcast>
-</MatchCfg>
-`
-	metasheet, content := splitRawXML(doc)
-	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(metasheet, content, newBook, Protogen)
-	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")
-	node10 := FindMetaNode(sheet1, "MatchCfg/Broadcast/Content")
-	type args struct {
-		xmlSheet *tableaupb.XMLSheet
-		curr     *tableaupb.XMLNode
-		oriType  string
-	}
-	tests := []struct {
-		name string
-		args args
-		want string
-	}{
-		{
-			name: "MatchCfg/MatchMode/MatchAI/AI",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node1,
-				oriType:  `[AI]<enum<.ENMAIWarmType>>`,
-			},
-			want: `[AI]<enum<.ENMAIWarmType>>`,
-		},
-		{
-			name: "MatchCfg/MapConf/Test/Weight",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node2,
-				oriType:  `map<uint32,Weight>`,
-			},
-			want: `{Test}map<uint32,Weight>`,
-		},
-		{
-			name: "MatchCfg/StructConf/Weight",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node3,
-				oriType:  `int32`,
-			},
-			want: `{StructConf}{Weight}int32`,
-		},
-		{
-			name: "MatchCfg/ListConf/Weight",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node4,
-				oriType:  `int32`,
-			},
-			want: `{ListConf}[Weight]<int32>`,
-		},
-		{
-			name: "MatchCfg/ListConf/Weight/Param",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node5,
-				oriType:  `int32`,
-			},
-			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`,
-		},
-		{
-			name: "MatchCfg/Broadcast/Content@txt",
-			args: args{
-				xmlSheet: sheet1,
-				curr:     node10,
-				oriType:  `string`,
-			},
-			want: `{Content}string`,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			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_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 {
-		curr *xmlquery.Node
-	}
-	tests := []struct {
-		name     string
-		args     args
-		wantRoot *xmlquery.Node
-		wantPath string
-	}{
-		{
-			name: "MatchCfg/MapConf/Weight",
-			args: args{
-				curr: node1,
-			},
-			wantRoot: metaRoot,
-			wantPath: "MatchCfg/MapConf/Weight",
-		},
-		{
-			name: "MatchCfg/StructConf/Test",
-			args: args{
-				curr: node2,
-			},
-			wantRoot: root,
-			wantPath: "MatchCfg/StructConf/Test",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			gotRoot, gotPath := getNodePath(tt.args.curr)
-			if !reflect.DeepEqual(gotRoot, tt.wantRoot) {
-				t.Errorf("getNodePath() gotRoot = %v, want %v", gotRoot, tt.wantRoot)
-			}
-			if gotPath != tt.wantPath {
-				t.Errorf("getNodePath() gotPath = %v, want %v", gotPath, tt.wantPath)
-			}
-		})
-	}
-}
-
-func Test_isCrossCell(t *testing.T) {
-	type args struct {
-		t string
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "map<uint32, Item>",
-			args: args{
-				t: "map<uint32, Item>",
-			},
-			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 := isCrossCell(tt.args.t); got != tt.want {
-				t.Errorf("isCrossCell() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-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": {
-					"Sheet": "ServerConf",
-					"Sep":   "|",
-				},
-			},
-			want1: &book.Sheet{
-				Name: book.MetasheetName,
-				Document: &book.Node{
-					Kind: book.DocumentNode,
-					Name: book.MetasheetName,
-					Children: []*book.Node{
-						{
-							Kind: book.MapNode,
-							Children: []*book.Node{
-								{
-									Kind:  book.MapNode,
-									Name:  book.KeywordSheet,
-									Value: book.MetasheetName,
-								},
-								{
-									Kind: book.MapNode,
-									Name: "ServerConf",
-									Children: []*book.Node{
-										{
-											Name:  "Sheet",
-											Value: "ServerConf",
-										},
-										{
-											Name:  "Sep",
-											Value: "|",
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			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)
-			}
-			require.Equalf(t, got1, tt.want1, "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, Protogen)
-			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"`,
-				`</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)
-			}
-		})
-	}
-}
-
-func Test_matchTag(t *testing.T) {
-	type args struct {
-		s string
-	}
-	tests := []struct {
-		name string
-		args args
-		want []string
-	}{
-		{
-			name: "SimpleTag",
-			args: args{
-				s: `<ClientID>enum<.ClientIDType></ClientID>`,
-			},
-			want: []string{
-				`>enum<.ClientIDType></`,
-				`enum<.ClientIDType>`,
-				``,
-			},
-		},
-		{
-			name: "ComplexTag",
-			args: args{
-				s: `<ClientID>map<uint32, Client>|{unique:true range:"1,~"}</ClientID>`,
-			},
-			want: []string{
-				`>map<uint32, Client>|{unique:true range:"1,~"}</`,
-				`map<uint32, Client>`,
-				`|{unique:true range:"1,~"}`,
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := matchTag(tt.args.s); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("matchTag() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func TestNewXMLImporter(t *testing.T) {
-	type args struct {
-		filename        string
-		sheets          []string
-		parser          book.SheetParser
-		mode            ImporterMode
-		cloned          bool
-		primaryBookName string
-	}
-	tests := []struct {
-		name    string
-		args    args
-		wantErr bool
-	}{
-		{
-			name: "test",
-			args: args{
-				filename: "testdata/Test.xml",
-				sheets: []string{
-					"Item",
-				},
-				parser:          nil,
-				mode:            Protogen,
-				cloned:          false,
-				primaryBookName: "",
-			},
-			wantErr: false,
-		},
-	}
-	log.Init(&log.Options{
-		Level: "DEBUG",
-	})
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := NewXMLImporter(tt.args.filename, tt.args.sheets, tt.args.parser, tt.args.mode, tt.args.cloned, tt.args.primaryBookName)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("NewXMLImporter() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			for _, sheet := range tt.args.sheets {
-				if got.Book.GetSheet(sheet) == nil {
-					t.Errorf("NewXMLImporter() GetSheet = nil, want %v", sheet)
-					return
-				}
-			}
-		})
-	}
-}
-
-func Test_genSheet(t *testing.T) {
-	metasheet, content := splitRawXML(matchDoc)
-	newBook := book.NewBook(`Test.xml`, `Test.xml`, nil)
-	xmlMeta, _ := readXMLFile(metasheet, content, newBook, Protogen)
-	xmlSheet := xmlMeta.SheetList[0]
-	if err := preprocessMeta(xmlSheet, xmlSheet.Meta); err != nil {
-		return
-	}
-	if err := preprocessData(xmlSheet, xmlSheet.Meta); err != nil {
-		return
-	}
-	type args struct {
-		xmlSheet *tableaupb.XMLSheet
-	}
-	tests := []struct {
-		name          string
-		args          args
-		wantMetaSheet *book.Sheet
-		wantDataSheet *book.Sheet
-		wantErr       bool
-	}{
-		{
-			name: "test",
-			args: args{
-				xmlSheet: xmlSheet,
-			},
-			wantMetaSheet: &book.Sheet{
-				Name: "@MatchCfg",
-				Document: &book.Node{
-					Kind: book.DocumentNode,
-					Name: "@MatchCfg",
-					Children: []*book.Node{
-						{
-							Kind: book.MapNode,
-							Name: "@MatchCfg",
-							Children: []*book.Node{
-								{
-									Name:  book.KeywordSheet,
-									Value: "@MatchCfg",
-								},
-								{
-									Name:  "open",
-									Value: "bool",
-								},
-								{
-									Kind: book.MapNode,
-									Name: "Broadcast",
-									Children: []*book.Node{
-										{
-											Name:  book.KeywordType,
-											Value: "[Broadcast]",
-										},
-										{
-											Name:  book.KeywordVariable,
-											Value: "BroadcastList",
-										},
-										{
-											Kind: book.MapNode,
-											Name: book.KeywordStruct,
-											Children: []*book.Node{
-												{
-													Name:  "Id",
-													Value: "int32",
-												},
-												{
-													Name:  "Priority",
-													Value: "int32",
-												},
-												{
-													Kind: book.MapNode,
-													Name: "BroadcastTime",
-													Children: []*book.Node{
-														{
-															Name:  book.KeywordType,
-															Value: "{BroadcastTime}",
-														},
-														{
-															Kind: book.MapNode,
-															Name: book.KeywordStruct,
-															Children: []*book.Node{
-																{
-																	Name:  "BeginTime",
-																	Value: "datetime",
-																},
-																{
-																	Name:  "EndTime",
-																	Value: "datetime",
-																},
-																{
-																	Name:  "Gap",
-																	Value: "duration",
-																},
-															},
-														},
-													},
-												},
-												{
-													Kind: book.MapNode,
-													Name: "Content",
-													Children: []*book.Node{
-														{
-															Name:  book.KeywordType,
-															Value: "{Content}",
-														},
-														{
-															Kind: book.MapNode,
-															Name: book.KeywordStruct,
-															Children: []*book.Node{
-																{
-																	Name:  "txt",
-																	Value: "string",
-																},
-															},
-														},
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			wantDataSheet: &book.Sheet{
-				Name: "MatchCfg",
-				Document: &book.Node{
-					Kind: book.DocumentNode,
-					Name: "MatchCfg",
-					Children: []*book.Node{
-						{
-							Kind: book.MapNode,
-							Name: "MatchCfg",
-							Children: []*book.Node{
-								{
-									Name:  book.KeywordSheet,
-									Value: "MatchCfg",
-								},
-								{
-									Kind: book.ListNode,
-									Name: "Broadcast",
-									Children: []*book.Node{
-										{
-											Kind: book.MapNode,
-											Children: []*book.Node{
-												{
-													Name:  "Id",
-													Value: "2",
-												},
-												{
-													Name:  "Priority",
-													Value: "2",
-												},
-												{
-													Kind: book.MapNode,
-													Name: "BroadcastTime",
-													Children: []*book.Node{
-														{
-															Name:  "BeginTime",
-															Value: "2016-11-15 19:30:10",
-														},
-														{
-															Name:  "EndTime",
-															Value: "2022-04-21 23:29:59",
-														},
-														{
-															Name:  "Gap",
-															Value: "60s",
-														},
-													},
-												},
-												{
-													Kind: book.MapNode,
-													Name: "Content",
-													Children: []*book.Node{
-														{
-															Name:  "txt",
-															Value: "每分钟发送一次测试",
-														},
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			wantErr: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			gotMetaSheet, gotDataSheet, err := genSheet(tt.args.xmlSheet)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("genSheet() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(gotMetaSheet, tt.wantMetaSheet) {
-				t.Errorf("genSheet() gotMetaSheet = %v, want %v", gotMetaSheet, tt.wantMetaSheet)
-			}
-			if !reflect.DeepEqual(gotDataSheet, tt.wantDataSheet) {
-				t.Errorf("genSheet() gotDataSheet = %v, want %v", gotDataSheet, tt.wantDataSheet)
-			}
-		})
-	}
-}
diff --git a/internal/protogen/exporter_test.go b/internal/protogen/exporter_test.go
index 68e145c..3b166a6 100644
--- a/internal/protogen/exporter_test.go
+++ b/internal/protogen/exporter_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
@@ -310,10 +311,12 @@ func Test_sheetExporter_exportStruct(t *testing.T) {
 					Fields: []*tableaupb.Field{
 						{Name: "id", Type: "uint32", FullType: "uint32", Options: &tableaupb.FieldOptions{Name: "ID"}},
 						{Name: "num", Type: "int32", FullType: "int32", Options: &tableaupb.FieldOptions{Name: "Num"}},
-						{Name: "fruit_type", Type: "FruitType", FullType: "protoconf.FruitType", Options: &tableaupb.FieldOptions{Name: "FruitType"}},
+						{Name: "fruit_type", Type: "FruitType", FullType: "protoconf.FruitType", Predefined: true, Options: &tableaupb.FieldOptions{Name: "FruitType"}},
 					},
 				},
-				g: NewGeneratedBuf(),
+				g:              NewGeneratedBuf(),
+				typeInfos:      &xproto.TypeInfos{},
+				nestedMessages: make(map[string]*tableaupb.Field),
 			},
 			want: `// Generated from sheet: StructTaskReward.
 message TaskReward {
@@ -357,13 +360,16 @@ func Test_sheetExporter_exportUnion(t *testing.T) {
 								{Name: "id", Type: "uint32", FullType: "uint32", Options: &tableaupb.FieldOptions{Name: "ID"}},
 								{Name: "damage", Type: "int64", FullType: "int64", Options: &tableaupb.FieldOptions{Name: "Damage"}},
 								{Name: "type_list", Type: "repeated", FullType: "repeated protoconf.FruitType",
-									ListEntry: &tableaupb.Field_ListEntry{ElemType: "FruitType", ElemFullType: "protoconf.FruitType"},
-									Options:   &tableaupb.FieldOptions{Name: "Type", Layout: tableaupb.Layout_LAYOUT_INCELL}},
+									ListEntry:  &tableaupb.Field_ListEntry{ElemType: "FruitType", ElemFullType: "protoconf.FruitType"},
+									Predefined: true,
+									Options:    &tableaupb.FieldOptions{Name: "Type", Layout: tableaupb.Layout_LAYOUT_INCELL}},
 							},
 						},
 					},
 				},
-				g: NewGeneratedBuf(),
+				g:              NewGeneratedBuf(),
+				typeInfos:      &xproto.TypeInfos{},
+				nestedMessages: make(map[string]*tableaupb.Field),
 			},
 			want: `// Generated from sheet: UnionTaskTarget.
 message TaskTarget {
diff --git a/internal/types/types_test.go b/internal/types/types_test.go
index 5bc552d..e5586ba 100644
--- a/internal/types/types_test.go
+++ b/internal/types/types_test.go
@@ -516,3 +516,44 @@ func TestIsScalarType(t *testing.T) {
 		})
 	}
 }
+
+func TestParseTypeDescriptor(t *testing.T) {
+	tests := []struct {
+		name string
+		args string
+		want Kind
+	}{
+		{
+			name: "int32",
+			args: "int32",
+			want: ScalarKind,
+		},
+		{
+			name: "datetime",
+			args: "datetime",
+			want: ScalarKind,
+		},
+		{
+			name: "duration",
+			args: "duration",
+			want: ScalarKind,
+		},
+		{
+			name: "Item",
+			args: "Item",
+			want: MessageKind,
+		},
+		{
+			name: "enum<.FruitType>",
+			args: "enum<.FruitType>",
+			want: EnumKind,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ParseTypeDescriptor(tt.args); got.Kind != tt.want {
+				t.Errorf("IsScalarType() = %v, want %v", got.Kind, tt.want)
+			}
+		})
+	}
+}
diff --git a/test/functest/conf/ListConf.json b/test/functest/conf/ListConf.json
index 2824c92..9756c49 100644
--- a/test/functest/conf/ListConf.json
+++ b/test/functest/conf/ListConf.json
@@ -4,19 +4,40 @@
             "crossCellList": [
                 {
                     "value": 1,
-                    "name": "Apple"
+                    "name": "Apple",
+                    "costList": [
+                        1,
+                        2,
+                        3
+                    ]
                 },
                 {
                     "value": 2,
-                    "name": "Banana"
+                    "name": "Banana",
+                    "costList": [
+                        4,
+                        5,
+                        6
+                    ]
                 },
                 {
                     "value": 3,
-                    "name": "Orange"
+                    "name": "Orange",
+                    "costList": [
+                        7,
+                        8,
+                        9
+                    ]
                 },
                 {
                     "value": 1,
-                    "name": "Grape"
+                    "name": "Grape",
+                    "costList": [
+                        10,
+                        11,
+                        12,
+                        13
+                    ]
                 }
             ]
         }
diff --git a/test/functest/conf/ListConf2.json b/test/functest/conf/ListConf2.json
index b1a9435..c1fa07a 100644
--- a/test/functest/conf/ListConf2.json
+++ b/test/functest/conf/ListConf2.json
@@ -1,19 +1,25 @@
 {
     "parent": {
         "parent2": {
-            "enumIncellList": [
-                "FRUIT_TYPE_APPLE",
-                "FRUIT_TYPE_ORANGE",
-                "FRUIT_TYPE_BANANA"
-            ],
             "incell": {
                 "valueList": [
                     1,
                     2,
                     3,
                     1
+                ],
+                "costList": [
+                    1,
+                    2,
+                    3,
+                    2
                 ]
-            }
+            },
+            "enumIncellList": [
+                "FRUIT_TYPE_APPLE",
+                "FRUIT_TYPE_ORANGE",
+                "FRUIT_TYPE_BANANA"
+            ]
         }
     }
 }
\ No newline at end of file
diff --git a/test/functest/conf/MapConf.json b/test/functest/conf/MapConf.json
index 0f741bc..749bc1e 100644
--- a/test/functest/conf/MapConf.json
+++ b/test/functest/conf/MapConf.json
@@ -2,24 +2,158 @@
     "open": true,
     "preEntryMap": {
         "1": {
-            "name": "1"
+            "name": "1",
+            "costList": [
+                1,
+                2,
+                3
+            ]
         },
         "2": {
-            "name": "2"
+            "name": "2",
+            "costList": [
+                4,
+                5,
+                6
+            ]
         }
     },
     "entryMap": {
         "1": {
             "id": 1,
-            "value": 1
+            "value": 1,
+            "incellMap": {
+                "1": "first",
+                "2": "second"
+            }
         },
         "2": {
             "id": 2,
-            "value": 2
+            "value": 2,
+            "incellMap": {}
         },
         "3": {
             "id": 3,
-            "value": 3
+            "value": 1,
+            "incellMap": {
+                "3": "third"
+            }
+        }
+    },
+    "incell": {
+        "map": {
+            "Jerry": 2,
+            "Tom": 1
+        }
+    },
+    "incellMap": {
+        "1": 1,
+        "2": 10,
+        "3": 100
+    },
+    "keyEnumMap": {
+        "1": {
+            "key": "FRUIT_TYPE_APPLE",
+            "value": 1
+        },
+        "4": {
+            "key": "FRUIT_TYPE_BANANA",
+            "value": 2
+        }
+    },
+    "valueEnumMap": {
+        "1": "FRUIT_TYPE_BANANA",
+        "2": "FRUIT_TYPE_ORANGE"
+    },
+    "kvEnumMap": {
+        "1": {
+            "key": "FRUIT_TYPE_APPLE",
+            "value": "FRUIT_TYPE_BANANA"
+        },
+        "4": {
+            "key": "FRUIT_TYPE_BANANA",
+            "value": "FRUIT_TYPE_ORANGE"
+        }
+    },
+    "mealMap": {
+        "breakfast": {
+            "id": "breakfast",
+            "time": "900s",
+            "meatMap": {
+                "Egg": {
+                    "name": "Egg",
+                    "weight": "100g"
+                }
+            },
+            "vegetableInfo": {
+                "vegetableMap": {
+                    "Cauliflower": {
+                        "name": "Cauliflower",
+                        "num": 2
+                    },
+                    "Cucumber": {
+                        "name": "Cucumber",
+                        "num": 1
+                    }
+                }
+            },
+            "fruitInfo": {
+                "fruitMap": {
+                    "Apple": {
+                        "name": "Apple",
+                        "desc": "sour"
+                    }
+                }
+            }
+        },
+        "dinner": {
+            "id": "dinner",
+            "time": "1800s",
+            "meatMap": {
+                "Beef": {
+                    "name": "Beef",
+                    "weight": "200g"
+                }
+            },
+            "vegetableInfo": null,
+            "fruitInfo": {
+                "fruitMap": {
+                    "Banana": {
+                        "name": "Banana",
+                        "desc": ""
+                    }
+                }
+            }
+        },
+        "lunch": {
+            "id": "lunch",
+            "time": "1200s",
+            "meatMap": {
+                "Chicken": {
+                    "name": "Chicken",
+                    "weight": "150g"
+                },
+                "Fish": {
+                    "name": "Fish",
+                    "weight": "100g"
+                }
+            },
+            "vegetableInfo": {
+                "vegetableMap": {
+                    "Spinach": {
+                        "name": "Spinach",
+                        "num": 1
+                    }
+                }
+            },
+            "fruitInfo": {
+                "fruitMap": {
+                    "Mongo": {
+                        "name": "Mongo",
+                        "desc": "sweet"
+                    }
+                }
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/test/functest/conf/XMLStructConf.json b/test/functest/conf/XMLStructConf.json
new file mode 100644
index 0000000..f70b1ae
--- /dev/null
+++ b/test/functest/conf/XMLStructConf.json
@@ -0,0 +1,25 @@
+{
+    "item": {
+        "id": 1,
+        "name": "Apple"
+    },
+    "fruitShopData": {
+        "fruitMap": {
+            "apple": {
+                "name": "apple",
+                "flavorList": [
+                    "FRUIT_FLAVOR_SOUR",
+                    "FRUIT_FLAVOR_SWEET"
+                ]
+            },
+            "banana": {
+                "name": "banana",
+                "flavorList": [
+                    "FRUIT_FLAVOR_FRAGRANT",
+                    "FRUIT_FLAVOR_SWEET"
+                ]
+            }
+        }
+    },
+    "empty": null
+}
\ No newline at end of file
diff --git a/test/functest/conf/XMLStructConf2.json b/test/functest/conf/XMLStructConf2.json
new file mode 100644
index 0000000..7ecdff9
--- /dev/null
+++ b/test/functest/conf/XMLStructConf2.json
@@ -0,0 +1,5 @@
+{
+    "node": {
+        "desc": "Multiple root nodes are supported."
+    }
+}
\ No newline at end of file
diff --git a/test/functest/conf/YamlStructConf.json b/test/functest/conf/YamlStructConf.json
index 1992cca..b4156ee 100644
--- a/test/functest/conf/YamlStructConf.json
+++ b/test/functest/conf/YamlStructConf.json
@@ -28,5 +28,6 @@
     "predefinedIncellStruct": {
         "id": 4,
         "num": 40
-    }
+    },
+    "emptyStruct": null
 }
\ No newline at end of file
diff --git a/test/functest/proto/common/common.proto b/test/functest/proto/common/common.proto
index d832023..444482a 100644
--- a/test/functest/proto/common/common.proto
+++ b/test/functest/proto/common/common.proto
@@ -36,3 +36,11 @@ message Transform {
   Vector3 rotation = 2;
   Vector3 scale = 3;
 }
+
+message FruitInfo {
+  map<string, Fruit> fruit_map = 1 [(tableau.field) = { name: "Fruit" key: "Name" }];
+  message Fruit {
+    string name = 1 [(tableau.field) = { name: "Name" }];
+    repeated FruitFlavor flavor_list = 2 [(tableau.field) = { name: "Flavor" layout: LAYOUT_INCELL }];
+  }
+}
\ No newline at end of file
diff --git a/test/functest/proto/xml__list__list.proto b/test/functest/proto/xml__list__list.proto
index 4b7d51e..98aa98c 100644
--- a/test/functest/proto/xml__list__list.proto
+++ b/test/functest/proto/xml__list__list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
+// Code generated by tableau (protogen v0.6.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
@@ -22,6 +22,7 @@ message ListConf {
       message CrossCell {
         int32 value = 1 [(tableau.field) = {name:"Value"}];
         string name = 2 [(tableau.field) = {name:"Name"}];
+        repeated int32 cost_list = 3 [(tableau.field) = {name:"Cost" layout:LAYOUT_INCELL}];
       }
     }
   }
@@ -34,11 +35,12 @@ message ListConf2 {
   message Parent {
     Parent2 parent_2 = 1 [(tableau.field) = {name:"Parent2"}];
     message Parent2 {
-      repeated protoconf.FruitType enum_incell_list = 1 [(tableau.field) = {name:"EnumIncell" layout:LAYOUT_INCELL}];
-      Incell incell = 2 [(tableau.field) = {name:"Incell"}];
+      Incell incell = 1 [(tableau.field) = {name:"Incell"}];
       message Incell {
         repeated int32 value_list = 1 [(tableau.field) = {name:"Value" layout:LAYOUT_INCELL}];
+        repeated int32 cost_list = 2 [(tableau.field) = {name:"Cost" layout:LAYOUT_INCELL}];
       }
+      repeated protoconf.FruitType enum_incell_list = 2 [(tableau.field) = {name:"EnumIncell" layout:LAYOUT_INCELL}];
     }
   }
 }
diff --git a/test/functest/proto/xml__map__map.proto b/test/functest/proto/xml__map__map.proto
index a7de091..ab92025 100644
--- a/test/functest/proto/xml__map__map.proto
+++ b/test/functest/proto/xml__map__map.proto
@@ -1,10 +1,12 @@
-// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
+// Code generated by tableau (protogen v0.6.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
 
 package protoconf;
 
+import "common/common.proto";
+import "google/protobuf/duration.proto";
 import "tableau/protobuf/tableau.proto";
 
 option go_package = "github.com/tableauio/tableau/test/functest/protoconf";
@@ -17,10 +19,54 @@ message MapConf {
   map<string, PreEntry> pre_entry_map = 2 [(tableau.field) = {name:"PreEntry" key:"Name"}];
   message PreEntry {
     string name = 1 [(tableau.field) = {name:"Name"}];
+    repeated int32 cost_list = 2 [(tableau.field) = {name:"Cost" layout:LAYOUT_INCELL}];
   }
   map<uint32, Entry> entry_map = 3 [(tableau.field) = {name:"Entry" key:"Id"}];
   message Entry {
     uint32 id = 1 [(tableau.field) = {name:"Id"}];
-    int32 value = 2 [(tableau.field) = {name:"Value"}];
+    int32 value = 2 [(tableau.field) = {name:"Value" prop:{default:"1"}}];
+    map<int32, string> incell_map = 3 [(tableau.field) = {name:"IncellMap" layout:LAYOUT_INCELL}];
+  }
+  Incell incell = 4 [(tableau.field) = {name:"Incell"}];
+  message Incell {
+    map<string, int32> map = 1 [(tableau.field) = {name:"Map" layout:LAYOUT_INCELL}];
+  }
+  map<int32, int32> incell_map = 5 [(tableau.field) = {name:"IncellMap" layout:LAYOUT_INCELL}];
+  map<int32, KeyEnumMapValue> key_enum_map = 6 [(tableau.field) = {name:"KeyEnumMap" key:"@key" layout:LAYOUT_INCELL span:SPAN_INNER_CELL}];
+  message KeyEnumMapValue {
+    protoconf.FruitType key = 1 [(tableau.field) = {name:"@key"}];
+    int32 value = 2 [(tableau.field) = {name:"@value"}];
+  }
+  map<int32, protoconf.FruitType> value_enum_map = 7 [(tableau.field) = {name:"ValueEnumMap" layout:LAYOUT_INCELL}];
+  map<int32, KvEnumMapValue> kv_enum_map = 8 [(tableau.field) = {name:"KvEnumMap" key:"@key" layout:LAYOUT_INCELL span:SPAN_INNER_CELL}];
+  message KvEnumMapValue {
+    protoconf.FruitType key = 1 [(tableau.field) = {name:"@key"}];
+    protoconf.FruitType value = 2 [(tableau.field) = {name:"@value"}];
+  }
+  map<string, Meal> meal_map = 9 [(tableau.field) = {name:"Meal" key:"Id"}];
+  message Meal {
+    string id = 1 [(tableau.field) = {name:"Id"}];
+    google.protobuf.Duration time = 2 [(tableau.field) = {name:"Time" prop:{default:"30m"}}];
+    map<string, Meat> meat_map = 3 [(tableau.field) = {name:"Meat" key:"Name"}];
+    message Meat {
+      string name = 1 [(tableau.field) = {name:"Name"}];
+      string weight = 2 [(tableau.field) = {name:"Weight"}];
+    }
+    VegetableInfo vegetable_info = 4 [(tableau.field) = {name:"VegetableInfo"}];
+    message VegetableInfo {
+      map<string, Vegetable> vegetable_map = 1 [(tableau.field) = {name:"Vegetable" key:"Name" prop:{optional:true}}];
+      message Vegetable {
+        string name = 1 [(tableau.field) = {name:"Name" prop:{optional:true}}];
+        int32 num = 2 [(tableau.field) = {name:"Num"}];
+      }
+    }
+    FruitInfo fruit_info = 5 [(tableau.field) = {name:"FruitInfo"}];
+    message FruitInfo {
+      map<string, Fruit> fruit_map = 1 [(tableau.field) = {name:"Fruit" key:"Name"}];
+      message Fruit {
+        string name = 1 [(tableau.field) = {name:"Name"}];
+        string desc = 2 [(tableau.field) = {name:"Desc" prop:{optional:true}}];
+      }
+    }
   }
 }
diff --git a/test/functest/proto/xml__metasheet__metasheet.proto b/test/functest/proto/xml__metasheet__metasheet.proto
index cfed1a3..1a2ca24 100644
--- a/test/functest/proto/xml__metasheet__metasheet.proto
+++ b/test/functest/proto/xml__metasheet__metasheet.proto
@@ -13,11 +13,11 @@ option (tableau.workbook) = {name:"xml/metasheet/Metasheet.xml"};
 message MetasheetConf {
   option (tableau.worksheet) = {name:"MetasheetConf" sep:";"};
 
-  int32 max_num = 1 [(tableau.field) = {name:"MaxNum"}];
-  SepConf sep_conf = 2 [(tableau.field) = {name:"SepConf"}];
+  SepConf sep_conf = 1 [(tableau.field) = {name:"SepConf"}];
   message SepConf {
     repeated int32 number_list = 1 [(tableau.field) = {name:"number" layout:LAYOUT_INCELL}];
   }
+  int32 max_num = 2 [(tableau.field) = {name:"MaxNum"}];
 }
 
 message TemplateConf {
diff --git a/test/functest/proto/xml__struct.proto b/test/functest/proto/xml__struct.proto
new file mode 100644
index 0000000..6085f89
--- /dev/null
+++ b/test/functest/proto/xml__struct.proto
@@ -0,0 +1,35 @@
+// Code generated by tableau (protogen v0.6.0). DO NOT EDIT.
+// clang-format off
+
+syntax = "proto3";
+
+package protoconf;
+
+import "common/common.proto";
+import "tableau/protobuf/tableau.proto";
+
+option go_package = "github.com/tableauio/tableau/test/functest/protoconf";
+option (tableau.workbook) = {name:"xml/Struct.xml"};
+
+message XMLStructConf {
+  option (tableau.worksheet) = {name:"XMLStructConf"};
+
+  Item item = 1 [(tableau.field) = {name:"Item"}];
+  message Item {
+    uint32 id = 1 [(tableau.field) = {name:"ID"}];
+    string name = 2 [(tableau.field) = {name:"Name"}];
+  }
+  protoconf.FruitInfo fruit_shop_data = 2 [(tableau.field) = {name:"FruitShopData"}];
+  Empty empty = 3 [(tableau.field) = {name:"Empty"}];
+  message Empty {
+  }
+}
+
+message XMLStructConf2 {
+  option (tableau.worksheet) = {name:"XMLStructConf2"};
+
+  Node node = 1 [(tableau.field) = {name:"Node"}];
+  message Node {
+    string desc = 1 [(tableau.field) = {name:"desc"}];
+  }
+}
diff --git a/test/functest/proto/yaml__struct.proto b/test/functest/proto/yaml__struct.proto
index 5323619..8d14029 100644
--- a/test/functest/proto/yaml__struct.proto
+++ b/test/functest/proto/yaml__struct.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
+// Code generated by tableau (protogen v0.6.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
@@ -40,4 +40,7 @@ message YamlStructConf {
     int32 num = 2 [(tableau.field) = {name:"Num"}];
   }
   protoconf.Item predefined_incell_struct = 7 [(tableau.field) = {name:"PredefinedIncellStruct" span:SPAN_INNER_CELL}];
+  Empty empty_struct = 8 [(tableau.field) = {name:"EmptyStruct"}];
+  message Empty {
+  }
 }
diff --git a/test/functest/testdata/xml/Struct.xml b/test/functest/testdata/xml/Struct.xml
new file mode 100644
index 0000000..67daf03
--- /dev/null
+++ b/test/functest/testdata/xml/Struct.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+<@TABLEAU>
+    <Item Sheet="XMLStructConf" />
+    <Item Sheet="XMLStructConf2" />
+</@TABLEAU>
+
+<XMLStructConf>
+    <Item ID="uint32" Name="string" />
+    <FruitShopData @type="{.FruitInfo}" />
+    <Empty>
+    </Empty>
+</XMLStructConf>
+
+<XMLStructConf2>
+    <Node desc="string" />
+</XMLStructConf2>
+-->
+
+<XMLStructConf>
+    <Item ID="1" Name="Apple" VerboseAttr="A kind of fruit."/>
+    <VerboseElement ID="10" Num="100" />
+    <FruitShopData>
+        <Fruit Name="apple" Flavor="Sour,Sweet"/>
+        <Fruit Name="banana" Flavor="Fragrant,Sweet"/>
+    </FruitShopData>
+    <Empty>
+    </Empty>
+</XMLStructConf>
+
+<XMLStructConf2>
+    <Node desc="Multiple root nodes are supported."/>
+</XMLStructConf2>
\ No newline at end of file
diff --git a/test/functest/testdata/xml/list/List.xml b/test/functest/testdata/xml/list/List.xml
index 695a70a..0cdcfee 100644
--- a/test/functest/testdata/xml/list/List.xml
+++ b/test/functest/testdata/xml/list/List.xml
@@ -8,7 +8,7 @@
 <ListConf>
     <Parent>
         <Parent2>
-            <CrossCell Value="[CrossCell]int32" Name="string" />
+            <CrossCell Value="[CrossCell]int32" Name="string" Cost="[]int32" />
         </Parent2>
     </Parent>
 </ListConf>
@@ -16,7 +16,7 @@
 <ListConf2>
     <Parent>
         <Parent2>
-            <Incell Value="[]int32" />
+            <Incell Value="[]int32" Cost="[]int32" />
             <EnumIncell>[]enum<.FruitType></EnumIncell>
         </Parent2>
     </Parent>
@@ -26,10 +26,10 @@
 <ListConf>
     <Parent>
         <Parent2>
-            <CrossCell Value="1" Name="Apple" />
-            <CrossCell Value="2" Name="Banana" />
-            <CrossCell Value="3" Name="Orange" />
-            <CrossCell Value="1" Name="Grape" />    
+            <CrossCell Value="1" Name="Apple" Cost="1,2,3" />
+            <CrossCell Value="2" Name="Banana" Cost="4,5,6" />
+            <CrossCell Value="3" Name="Orange" Cost="7,8,9" />
+            <CrossCell Value="1" Name="Grape" Cost="10,11,12,13" />
         </Parent2>
     </Parent>
 </ListConf>
@@ -37,7 +37,7 @@
 <ListConf2>
     <Parent>
         <Parent2>
-            <Incell Value="1,2,3,1" />
+            <Incell Value="1,2,3,1" Cost="1,2,3,2" />
             <EnumIncell>Apple, Orange, Banana</EnumIncell>
         </Parent2>
     </Parent>
diff --git a/test/functest/testdata/xml/map/Map.xml b/test/functest/testdata/xml/map/Map.xml
index 552f67c..8fcead4 100644
--- a/test/functest/testdata/xml/map/Map.xml
+++ b/test/functest/testdata/xml/map/Map.xml
@@ -5,15 +5,63 @@
 </@TABLEAU>
 
 <MapConf Open="bool">
-    <PreEntry Name="map<string, PreEntry>" />
-    <Entry Id="map<uint32, Entry>" Value="int32" />
+    <PreEntry Name="map<string, PreEntry>" Cost="[]int32" />
+    <Entry Id="map<uint32, Entry>" Value="int32|{default:"1"}" IncellMap="map<int32, string>" />
+    <Incell Map="map<string, int32>" />
+    <IncellMap>map<int32, int32></IncellMap>
+    <KeyEnumMap>map<enum<.FruitType>, int32></KeyEnumMap>
+    <ValueEnumMap>map<int32, enum<.FruitType>></ValueEnumMap>
+    <KvEnumMap>map<enum<.FruitType>, enum<.FruitType>></KvEnumMap>
+
+    <Meal Id="map<string, Meal>" Time="duration|{default:"30m"}">
+        <Meat Name="map<string, Meat>" Weight="string" />
+        <VegetableInfo>
+            <Vegetable Name="map<string, Vegetable>|{optional:true}" Num="int32" />
+        </VegetableInfo>
+        <FruitInfo>
+            <Fruit Name="map<string, Fruit>" Desc="string|{optional:true}" />
+        </FruitInfo>
+    </Meal>
 </MapConf>
 -->
 
 <MapConf Open="true">
-    <PreEntry Name="1" />
-    <PreEntry Name="2" />
-    <Entry Id="1" Value="1" />
-    <Entry Id="2" Value="2" />
-    <Entry Id="3" Value="3" />
+    <PreEntry Name="1" Cost="1,2,3" />
+    <PreEntry Name="2" Cost="4,5,6" />
+    <Entry Id="1" Value="1" IncellMap="1:first,2:second" />
+    <Entry Id="2" Value="2" IncellMap=""/>
+    <Entry Id="3" IncellMap="3:third" />
+    <Incell Map="Tom:1,Jerry:2" />
+    <IncellMap>1:1,2:10,3:100</IncellMap>
+    <KeyEnumMap>Apple:1,Banana:2</KeyEnumMap>
+    <ValueEnumMap>1:Banana,2:Orange</ValueEnumMap>
+    <KvEnumMap>Apple:Banana,Banana:Orange</KvEnumMap>
+
+    <Meal Id="breakfast" Time="15m">
+        <Meat Name="Egg" Weight="100g" />
+        <VegetableInfo>
+            <Vegetable Name="Cauliflower" Num="2" />
+            <Vegetable Name="Cucumber" Num="1" />
+        </VegetableInfo>
+        <FruitInfo>
+            <Fruit Name="Apple" Desc="sour" />
+        </FruitInfo>
+    </Meal>
+    <Meal Id="lunch" Time="20m">
+        <Meat Name="Chicken" Weight="150g" />
+        <Meat Name="Fish" Weight="100g" />
+        <VegetableInfo>
+            <Vegetable Name="Spinach" Num="1" />
+        </VegetableInfo>
+        <FruitInfo>
+            <Fruit Name="Mongo" Desc="sweet" />
+        </FruitInfo>
+    </Meal>
+    <Meal Id="dinner">
+        <Meat Name="Beef" Weight="200g" />
+        <VegetableInfo />
+        <FruitInfo>
+            <Fruit Name="Banana" />
+        </FruitInfo>
+    </Meal>
 </MapConf>
diff --git a/test/functest/testdata/xml/metasheet/Merger.xml b/test/functest/testdata/xml/metasheet/Merger.xml
index 3a459b6..d200bab 100644
--- a/test/functest/testdata/xml/metasheet/Merger.xml
+++ b/test/functest/testdata/xml/metasheet/Merger.xml
@@ -17,3 +17,6 @@
     </Fuben>
 </MergerConf>
 -->
+
+<MergerConf>
+</MergerConf>
\ No newline at end of file
diff --git a/test/functest/testdata/yaml/Struct.yaml b/test/functest/testdata/yaml/Struct.yaml
index 9f963a7..261308e 100644
--- a/test/functest/testdata/yaml/Struct.yaml
+++ b/test/functest/testdata/yaml/Struct.yaml
@@ -24,12 +24,13 @@ IncellStruct2: "{uint32 ID, int32 Num}InplaceIncellItem"
 PredefinedIncellStruct:
   "@type": "{.Item}"
   "@incell": true
-
+EmptyStruct:
+  "@type": "{Empty}"
 ---
 "@sheet": YamlStructConf
 StrictStruct:
-    ID: 1
-    Name: apple
+  ID: 1
+  Name: apple
 Struct:
   ID: 1
   StartTime: 2024-10-01 10:10:10
@@ -44,3 +45,4 @@ PredefinedStruct:
 IncellStruct: "2, 20"
 IncellStruct2: "3, 30"
 PredefinedIncellStruct: "4, 40"
+EmptyStruct: