Repository: goplus/gop
Pull request #2302 (2025-04-23 05:40:04)

ast, parser: DomainTextLitEx


Patch:
diff --git a/ast/ast_gop.go b/ast/ast_gop.go
index 8703c101..afa29c47 100644
--- a/ast/ast_gop.go
+++ b/ast/ast_gop.go
@@ -62,12 +62,22 @@ func (*OverloadFuncDecl) declNode() {}
 // A DomainTextLit node represents a domain-specific text literal.
 // https://github.com/goplus/gop/issues/2143
 //
-//	tpl`...`
+//	domainTag`...`
+//	domainTag`> arg1, arg2, ...
+//	  ...
+//	`
 type DomainTextLit struct {
 	Domain   *Ident    // domain name
 	ValuePos token.Pos // literal position
 	Value    string    // literal string; e.g. `\m\n\o`
-	Extra    any       // *ast.StringLitEx or *gop/tpl/ast.File, optional
+	Extra    any       // *DomainTextLitEx or *gop/tpl/ast.File, optional
+}
+
+// DomainTextLitEx represents extra information for domain text literal.
+type DomainTextLitEx struct {
+	Args   []Expr    // domain text arguments; or nil
+	RawPos token.Pos // position of the first character of the raw string
+	Raw    string    // raw string without backquote
 }
 
 // Pos returns position of first character belonging to the node.
diff --git a/parser/parser.go b/parser/parser.go
index 0d92069e..2cf617fd 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1725,6 +1725,31 @@ func (p *parser) stringLitExpr(parts []any, off, end token.Pos) []any {
 	return parts
 }
 
+func (p *parser) domainTextLitEx(off, end token.Pos) *ast.DomainTextLitEx {
+	file := p.file
+	base := file.Base()
+	src := p.scanner.CodeTo(int(end) - base)
+
+	var args []ast.Expr
+	var sp parser
+	sp.initSub(file, src, int(off)-base, 0)
+
+	for {
+		expr := sp.parseRHS()
+		args = append(args, expr)
+		if sp.tok != token.COMMA {
+			break
+		}
+		sp.next()
+	}
+	sp.expect(token.SEMICOLON)
+	return &ast.DomainTextLitEx{
+		Args:   args,
+		RawPos: sp.pos,
+		Raw:    string(src[int(sp.pos)-base:]),
+	}
+}
+
 func (p *parser) tplLit(off, end token.Pos) any {
 	file := p.file
 	base := file.Base()
@@ -1760,6 +1785,8 @@ func (p *parser) parseOperand(lhs, allowTuple, allowCmd bool) (x ast.Expr, isTup
 			var extra any
 			if ident.Name == "tpl" {
 				extra = p.tplLit(pos+1, pos+token.Pos(len(lit))-1)
+			} else if strings.HasPrefix(lit, "`< ") { // domainTag`< ...`
+				extra = p.domainTextLitEx(pos+3, pos+token.Pos(len(lit))-1)
 			}
 			x = &ast.DomainTextLit{
 				Domain:   ident,


Test patch:
diff --git a/parser/_testdata/domainhuh/huh.gop b/parser/_testdata/domainhuh/huh.gop
new file mode 100644
index 00000000..2bfcdb77
--- /dev/null
+++ b/parser/_testdata/domainhuh/huh.gop
@@ -0,0 +1,12 @@
+form := huh`< &ret, 10
+<form>
+	<group>
+		<select id="Burger" title="Choose your burger">
+			<option value="classic" title="Charmburger Classic"/>
+			<option value="chickwich" title="Chickwich"/>
+			<option value="fishburger" title="Fishburger"/>
+			<option value="charmpossible" title="Charmpossible™ Burger"/>
+		</select>
+	</group>
+</form>
+`
diff --git a/parser/_testdata/domainhuh/parser.expect b/parser/_testdata/domainhuh/parser.expect
new file mode 100644
index 00000000..9323c3cf
--- /dev/null
+++ b/parser/_testdata/domainhuh/parser.expect
@@ -0,0 +1,57 @@
+package main
+
+file huh.gop
+noEntrypoint
+ast.FuncDecl:
+  Name:
+    ast.Ident:
+      Name: main
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.AssignStmt:
+          Lhs:
+            ast.Ident:
+              Name: form
+          Tok: :=
+          Rhs:
+            ast.DomainTextLit:
+              Domain:
+                ast.Ident:
+                  Name: huh
+              Value: `< &ret, 10
+<form>
+	<group>
+		<select id="Burger" title="Choose your burger">
+			<option value="classic" title="Charmburger Classic"/>
+			<option value="chickwich" title="Chickwich"/>
+			<option value="fishburger" title="Fishburger"/>
+			<option value="charmpossible" title="Charmpossible™ Burger"/>
+		</select>
+	</group>
+</form>
+`
+                Extra: args=2
+                  ast.UnaryExpr:
+                    Op: &
+                    X:
+                      ast.Ident:
+                        Name: ret
+                  ast.BasicLit:
+                    Kind: INT
+                    Value: 10
+                  <form>
+	<group>
+		<select id="Burger" title="Choose your burger">
+			<option value="classic" title="Charmburger Classic"/>
+			<option value="chickwich" title="Chickwich"/>
+			<option value="fishburger" title="Fishburger"/>
+			<option value="charmpossible" title="Charmpossible™ Burger"/>
+		</select>
+	</group>
+</form>
+
diff --git a/parser/parserdir_test.go b/parser/parserdir_test.go
index 697a2129..58febdde 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -116,11 +116,8 @@ func testFrom(t *testing.T, pkgDir, sel string, exclude Mode) {
 		t.Fatal("ParseDir failed:", err, reflect.TypeOf(err), len(pkgs))
 	}
 	for _, pkg := range pkgs {
-		b, err := os.ReadFile(pkgDir + "/parser.expect")
-		if err != nil {
-			t.Fatal("Parsing", pkgDir, "-", err)
-		}
-		parsertest.Expect(t, pkg, string(b))
+		b, _ := os.ReadFile(pkgDir + "/parser.expect")
+		parsertest.ExpectEx(t, pkgDir+"/result.txt", pkg, b)
 		return
 	}
 }
diff --git a/parser/parsertest/parsertest.go b/parser/parsertest/parsertest.go
index d3bc6d1d..f8c1dee9 100644
--- a/parser/parsertest/parsertest.go
+++ b/parser/parsertest/parsertest.go
@@ -30,6 +30,7 @@ import (
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
 	tpltoken "github.com/goplus/gop/tpl/token"
+	"github.com/qiniu/x/test"
 )
 
 func sortedKeys(m any) []string {
@@ -102,6 +103,13 @@ func FprintNode(w io.Writer, lead string, v any, prefix, indent string) {
 					FprintNode(w, "", part, prefix, indent)
 				}
 			}
+		} else if lit, ok := v.(*ast.DomainTextLitEx); ok {
+			fmt.Fprintf(w, "%sExtra: args=%d\n", prefix, len(lit.Args))
+			prefix += indent
+			for _, arg := range lit.Args {
+				FprintNode(w, "", arg, prefix, indent)
+			}
+			fmt.Fprintf(w, "%s%v\n", prefix, lit.Raw)
 		} else {
 			log.Panicln("FprintNode unexpected type:", t)
 		}
@@ -137,6 +145,14 @@ func Expect(t *testing.T, pkg *ast.Package, expected string) {
 	}
 }
 
+func ExpectEx(t *testing.T, outfile string, pkg *ast.Package, expected []byte) {
+	b := bytes.NewBuffer(nil)
+	Fprint(b, pkg)
+	if test.Diff(t, outfile, b.Bytes(), expected) {
+		t.Fatal("gop.Parser: unexpect result")
+	}
+}
+
 // ExpectNode asserts a Go+ AST node equals output or not.
 func ExpectNode(t *testing.T, node any, expected string) {
 	b := bytes.NewBuffer(nil)