cuelang.org/go@v0.10.1/encoding/protobuf/parse.go (about) 1 // Copyright 2019 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package protobuf 16 17 import ( 18 "bytes" 19 "fmt" 20 "os" 21 "path" 22 "path/filepath" 23 "strconv" 24 "strings" 25 "text/scanner" 26 "unicode" 27 28 "github.com/emicklei/proto" 29 30 "cuelang.org/go/cue/ast" 31 "cuelang.org/go/cue/ast/astutil" 32 "cuelang.org/go/cue/errors" 33 "cuelang.org/go/cue/literal" 34 "cuelang.org/go/cue/parser" 35 "cuelang.org/go/cue/token" 36 "cuelang.org/go/internal/source" 37 ) 38 39 func (s *Extractor) parse(filename string, src interface{}) (p *protoConverter, err error) { 40 if filename == "" { 41 return nil, errors.Newf(token.NoPos, "empty filename") 42 } 43 if r, ok := s.fileCache[filename]; ok { 44 return r.p, r.err 45 } 46 defer func() { 47 s.fileCache[filename] = result{p, err} 48 }() 49 50 b, err := source.ReadAll(filename, src) 51 if err != nil { 52 return nil, err 53 } 54 55 parser := proto.NewParser(bytes.NewReader(b)) 56 if filename != "" { 57 parser.Filename(filename) 58 } 59 d, err := parser.Parse() 60 if err != nil { 61 return nil, errors.Newf(token.NoPos, "protobuf: %v", err) 62 } 63 64 tfile := token.NewFile(filename, -1, len(b)) 65 tfile.SetLinesForContent(b) 66 67 p = &protoConverter{ 68 id: filename, 69 state: s, 70 tfile: tfile, 71 imported: map[string]bool{}, 72 symbols: map[string]bool{}, 73 } 74 75 defer func() { 76 switch x := recover().(type) { 77 case nil: 78 case protoError: 79 err = &protobufError{ 80 path: p.path, 81 pos: p.toCUEPos(x.pos), 82 err: x.error, 83 } 84 default: 85 panic(x) 86 } 87 }() 88 89 p.file = &ast.File{Filename: filename} 90 91 p.addNames(d.Elements) 92 93 // Parse package definitions. 94 for _, e := range d.Elements { 95 switch x := e.(type) { 96 case *proto.Package: 97 p.protoPkg = x.Name 98 case *proto.Option: 99 if x.Name == "go_package" { 100 str, err := strconv.Unquote(x.Constant.SourceRepresentation()) 101 if err != nil { 102 failf(x.Position, "unquoting package filed: %v", err) 103 } 104 split := strings.Split(str, ";") 105 switch { 106 case strings.Contains(split[0], "."): 107 p.cuePkgPath = split[0] 108 switch len(split) { 109 case 1: 110 p.shortPkgName = path.Base(str) 111 case 2: 112 p.shortPkgName = split[1] 113 default: 114 failf(x.Position, "unexpected ';' in %q", str) 115 } 116 117 case len(split) == 1: 118 p.shortPkgName = split[0] 119 120 default: 121 failf(x.Position, "malformed go_package clause %s", str) 122 } 123 // name.AddComment(comment(x.Comment, true)) 124 // name.AddComment(comment(x.InlineComment, false)) 125 } 126 } 127 } 128 129 if name := p.shortName(); name != "" { 130 p.file.Decls = append(p.file.Decls, &ast.Package{Name: ast.NewIdent(name)}) 131 } 132 133 for _, e := range d.Elements { 134 switch x := e.(type) { 135 case *proto.Import: 136 if err := p.doImport(x); err != nil { 137 return nil, err 138 } 139 } 140 } 141 142 for _, e := range d.Elements { 143 p.topElement(e) 144 } 145 146 err = astutil.Sanitize(p.file) 147 148 return p, err 149 } 150 151 // A protoConverter converts a proto definition to CUE. Proto files map to 152 // CUE files one to one. 153 type protoConverter struct { 154 state *Extractor 155 tfile *token.File 156 157 proto3 bool 158 159 id string 160 protoPkg string 161 shortPkgName string 162 cuePkgPath string 163 164 file *ast.File 165 current *ast.StructLit 166 167 imported map[string]bool 168 169 path []string 170 scope []map[string]mapping // for symbols resolution within package. 171 symbols map[string]bool // symbols provided by package 172 } 173 174 type mapping struct { 175 cue func() ast.Expr // needs to be a new copy as position changes 176 pkg *protoConverter 177 } 178 179 func (p *protoConverter) qualifiedImportPath() string { 180 s := p.importPath() 181 if short := p.shortPkgName; short != "" && short != path.Base(s) { 182 s += ":" + short 183 } 184 return s 185 } 186 187 func (p *protoConverter) importPath() string { 188 if p.cuePkgPath == "" && p.protoPkg != "" { 189 dir := strings.Replace(p.protoPkg, ".", "/", -1) 190 p.cuePkgPath = path.Join("googleapis.com", dir) 191 } 192 return p.cuePkgPath 193 } 194 195 func (p *protoConverter) shortName() string { 196 if p.state.pkgName != "" { 197 return p.state.pkgName 198 } 199 if p.shortPkgName == "" && p.protoPkg != "" { 200 split := strings.Split(p.protoPkg, ".") 201 p.shortPkgName = split[len(split)-1] 202 } 203 return p.shortPkgName 204 } 205 206 func (p *protoConverter) toCUEPos(pos scanner.Position) token.Pos { 207 return p.tfile.Pos(pos.Offset, 0) 208 } 209 210 func (p *protoConverter) addRef(pos scanner.Position, name string, cue func() ast.Expr) { 211 top := p.scope[len(p.scope)-1] 212 if _, ok := top[name]; ok { 213 failf(pos, "entity %q already defined", name) 214 } 215 top[name] = mapping{cue: cue} 216 } 217 218 func (p *protoConverter) addNames(elems []proto.Visitee) { 219 p.scope = append(p.scope, map[string]mapping{}) 220 for _, e := range elems { 221 var pos scanner.Position 222 var name string 223 switch x := e.(type) { 224 case *proto.Message: 225 if x.IsExtend { 226 continue 227 } 228 name = x.Name 229 pos = x.Position 230 case *proto.Enum: 231 name = x.Name 232 pos = x.Position 233 case *proto.NormalField: 234 name = x.Name 235 pos = x.Position 236 case *proto.MapField: 237 name = x.Name 238 pos = x.Position 239 case *proto.Oneof: 240 name = x.Name 241 pos = x.Position 242 default: 243 continue 244 } 245 sym := strings.Join(append(p.path, name), ".") 246 p.symbols[sym] = true 247 p.addRef(pos, name, func() ast.Expr { return ast.NewIdent("#" + name) }) 248 } 249 } 250 251 func (p *protoConverter) popNames() { 252 p.scope = p.scope[:len(p.scope)-1] 253 } 254 255 func (p *protoConverter) resolve(pos scanner.Position, name string, options []*proto.Option) ast.Expr { 256 if expr := protoToCUE(name, options); expr != nil { 257 ast.SetPos(expr, p.toCUEPos(pos)) 258 return expr 259 } 260 if strings.HasPrefix(name, ".") { 261 return p.resolveTopScope(pos, name[1:], options) 262 } 263 for i := len(p.scope) - 1; i > 0; i-- { 264 if m, ok := p.scope[i][name]; ok { 265 return m.cue() 266 } 267 } 268 expr := p.resolveTopScope(pos, name, options) 269 return expr 270 } 271 272 func (p *protoConverter) resolveTopScope(pos scanner.Position, name string, options []*proto.Option) ast.Expr { 273 for i := 0; i < len(name); i++ { 274 k := strings.IndexByte(name[i:], '.') 275 i += k 276 if k == -1 { 277 i = len(name) 278 } 279 if m, ok := p.scope[0][name[:i]]; ok { 280 if m.pkg != nil { 281 p.imported[m.pkg.qualifiedImportPath()] = true 282 } 283 expr := m.cue() 284 for i < len(name) { 285 name = name[i+1:] 286 if i = strings.IndexByte(name, '.'); i == -1 { 287 i = len(name) 288 } 289 expr = ast.NewSel(expr, "#"+name[:i]) 290 } 291 ast.SetPos(expr, p.toCUEPos(pos)) 292 return expr 293 } 294 } 295 failf(pos, "name %q not found", name) 296 return nil 297 } 298 299 func (p *protoConverter) doImport(v *proto.Import) error { 300 if v.Filename == "cue/cue.proto" { 301 return nil 302 } 303 304 filename := "" 305 for _, p := range p.state.paths { 306 name := filepath.Join(p, v.Filename) 307 _, err := os.Stat(name) 308 if err != nil { 309 continue 310 } 311 filename = name 312 break 313 } 314 315 if filename == "" { 316 err := errors.Newf(p.toCUEPos(v.Position), "could not find import %q", v.Filename) 317 p.state.addErr(err) 318 return err 319 } 320 321 if !p.mapBuiltinPackage(v.Position, v.Filename, filename == "") { 322 return nil 323 } 324 325 imp, err := p.state.parse(filename, nil) 326 if err != nil { 327 fail(v.Position, err) 328 } 329 330 pkgNamespace := strings.Split(imp.protoPkg, ".") 331 curNamespace := strings.Split(p.protoPkg, ".") 332 for { 333 for k := range imp.symbols { 334 ref := k 335 if len(pkgNamespace) > 0 { 336 ref = strings.Join(append(pkgNamespace, k), ".") 337 } 338 if _, ok := p.scope[0][ref]; !ok { 339 pkg := imp 340 a := toCue(k) 341 342 var f func() ast.Expr 343 344 if imp.qualifiedImportPath() == p.qualifiedImportPath() { 345 pkg = nil 346 f = func() ast.Expr { return ast.NewIdent(a[0]) } 347 } else { 348 f = func() ast.Expr { 349 ident := &ast.Ident{ 350 Name: imp.shortName(), 351 Node: ast.NewImport(nil, imp.qualifiedImportPath()), 352 } 353 return ast.NewSel(ident, a[0]) 354 } 355 } 356 p.scope[0][ref] = mapping{f, pkg} 357 } 358 } 359 if len(pkgNamespace) == 0 { 360 break 361 } 362 if len(curNamespace) == 0 || pkgNamespace[0] != curNamespace[0] { 363 break 364 } 365 pkgNamespace = pkgNamespace[1:] 366 curNamespace = curNamespace[1:] 367 } 368 return nil 369 } 370 371 // TODO: this doesn't work. Do something more principled. 372 func toCue(name string) []string { 373 a := strings.Split(name, ".") 374 for i, s := range a { 375 a[i] = "#" + s 376 } 377 return a 378 } 379 380 func (p *protoConverter) stringLit(pos scanner.Position, s string) *ast.BasicLit { 381 return &ast.BasicLit{ 382 ValuePos: p.toCUEPos(pos), 383 Kind: token.STRING, 384 Value: literal.String.Quote(s)} 385 } 386 387 func (p *protoConverter) ident(pos scanner.Position, name string) *ast.Ident { 388 return &ast.Ident{NamePos: p.toCUEPos(pos), Name: labelName(name)} 389 } 390 391 func (p *protoConverter) ref(pos scanner.Position) *ast.Ident { 392 name := "#" + p.path[len(p.path)-1] 393 return &ast.Ident{NamePos: p.toCUEPos(pos), Name: name} 394 } 395 396 func (p *protoConverter) subref(pos scanner.Position, name string) *ast.Ident { 397 return &ast.Ident{ 398 NamePos: p.toCUEPos(pos), 399 Name: "#" + name, 400 } 401 } 402 403 func (p *protoConverter) addTag(f *ast.Field, body string) { 404 tag := "@protobuf(" + body + ")" 405 f.Attrs = append(f.Attrs, &ast.Attribute{Text: tag}) 406 } 407 408 func (p *protoConverter) topElement(v proto.Visitee) { 409 switch x := v.(type) { 410 case *proto.Syntax: 411 p.proto3 = x.Value == "proto3" 412 413 case *proto.Comment: 414 addComments(p.file, 0, x, nil) 415 416 case *proto.Enum: 417 p.enum(x) 418 419 case *proto.Package: 420 if doc := x.Doc(); doc != nil { 421 addComments(p.file, 0, doc, nil) 422 } 423 424 case *proto.Message: 425 p.message(x) 426 427 case *proto.Option: 428 case *proto.Import: 429 // already handled. 430 431 case *proto.Service: 432 // TODO: handle services. 433 434 case *proto.Extensions, *proto.Reserved: 435 // no need to handle 436 437 default: 438 failf(scanner.Position{}, "unsupported type %T", x) 439 } 440 } 441 442 func (p *protoConverter) message(v *proto.Message) { 443 if v.IsExtend { 444 // TODO: we are not handling extensions as for now. 445 return 446 } 447 448 defer func(saved []string) { p.path = saved }(p.path) 449 p.path = append(p.path, v.Name) 450 451 p.addNames(v.Elements) 452 defer p.popNames() 453 454 // TODO: handle IsExtend/ proto2 455 456 s := &ast.StructLit{ 457 Lbrace: p.toCUEPos(v.Position), 458 // TODO: set proto file position. 459 Rbrace: token.Newline.Pos(), 460 } 461 462 ref := p.ref(v.Position) 463 if v.Comment == nil { 464 ref.NamePos = newSection 465 } 466 f := &ast.Field{Label: ref, Value: s} 467 addComments(f, 1, v.Comment, nil) 468 469 p.addDecl(f) 470 defer func(current *ast.StructLit) { 471 p.current = current 472 }(p.current) 473 p.current = s 474 475 for i, e := range v.Elements { 476 p.messageField(s, i, e) 477 } 478 } 479 480 func (p *protoConverter) addDecl(d ast.Decl) { 481 if p.current == nil { 482 p.file.Decls = append(p.file.Decls, d) 483 } else { 484 p.current.Elts = append(p.current.Elts, d) 485 } 486 } 487 488 func (p *protoConverter) messageField(s *ast.StructLit, i int, v proto.Visitee) { 489 switch x := v.(type) { 490 case *proto.Comment: 491 s.Elts = append(s.Elts, comment(x, true)) 492 493 case *proto.NormalField: 494 f := p.parseField(s, i, x.Field) 495 496 if x.Repeated { 497 f.Value = &ast.ListLit{ 498 Lbrack: p.toCUEPos(x.Position), 499 Elts: []ast.Expr{&ast.Ellipsis{Type: f.Value}}, 500 } 501 } 502 503 case *proto.MapField: 504 defer func(saved []string) { p.path = saved }(p.path) 505 p.path = append(p.path, x.Name) 506 507 f := &ast.Field{} 508 509 // All keys are converted to strings. 510 // TODO: support integer keys. 511 f.Label = ast.NewList(ast.NewIdent("string")) 512 f.Value = p.resolve(x.Position, x.Type, x.Options) 513 514 name := p.ident(x.Position, x.Name) 515 f = &ast.Field{ 516 Label: name, 517 Value: ast.NewStruct(f), 518 } 519 addComments(f, i, x.Comment, x.InlineComment) 520 521 o := optionParser{message: s, field: f} 522 o.tags = fmt.Sprintf(`%d,map[%s]%s`, x.Sequence, x.KeyType, x.Type) 523 if x.Name != name.Name { 524 o.tags += "," + x.Name 525 } 526 s.Elts = append(s.Elts, f) 527 o.parse(x.Options) 528 p.addTag(f, o.tags) 529 530 if !o.required { 531 f.Optional = token.NoSpace.Pos() 532 } 533 534 case *proto.Enum: 535 p.enum(x) 536 537 case *proto.Message: 538 p.message(x) 539 540 case *proto.Oneof: 541 p.oneOf(x) 542 543 case *proto.Extensions, *proto.Reserved: 544 // no need to handle 545 546 case *proto.Option: 547 opt := fmt.Sprintf("@protobuf(option %s=%s)", x.Name, x.Constant.Source) 548 attr := &ast.Attribute{ 549 At: p.toCUEPos(x.Position), 550 Text: opt, 551 } 552 addComments(attr, i, x.Doc(), x.InlineComment) 553 s.Elts = append(s.Elts, attr) 554 555 default: 556 failf(scanner.Position{}, "unsupported field type %T", v) 557 } 558 } 559 560 // enum converts a proto enum definition to CUE. 561 // 562 // An enum will generate two top-level definitions: 563 // 564 // Enum: 565 // "Value1" | 566 // "Value2" | 567 // "Value3" 568 // 569 // and 570 // 571 // Enum_value: { 572 // "Value1": 0 573 // "Value2": 1 574 // } 575 // 576 // Enums are always defined at the top level. The name of a nested enum 577 // will be prefixed with the name of its parent and an underscore. 578 func (p *protoConverter) enum(x *proto.Enum) { 579 580 if len(x.Elements) == 0 { 581 failf(x.Position, "empty enum") 582 } 583 584 name := p.subref(x.Position, x.Name) 585 586 defer func(saved []string) { p.path = saved }(p.path) 587 p.path = append(p.path, x.Name) 588 589 p.addNames(x.Elements) 590 591 if len(p.path) == 0 { 592 defer func() { p.path = p.path[:0] }() 593 p.path = append(p.path, x.Name) 594 } 595 596 // Top-level enum entry. 597 enum := &ast.Field{Label: name} 598 addComments(enum, 1, x.Comment, nil) 599 if p.current != nil && len(p.current.Elts) > 0 { 600 ast.SetRelPos(enum, token.NewSection) 601 } 602 603 // Top-level enum values entry. 604 valueName := ast.NewIdent(name.Name + "_value") 605 valueName.NamePos = newSection 606 valueMap := &ast.StructLit{} 607 d := &ast.Field{Label: valueName, Value: valueMap} 608 // addComments(valueMap, 1, x.Comment, nil) 609 610 if strings.Contains(name.Name, "google") { 611 panic(name.Name) 612 } 613 p.addDecl(enum) 614 615 numEnums := 0 616 for _, v := range x.Elements { 617 if _, ok := v.(*proto.EnumField); ok { 618 numEnums++ 619 } 620 } 621 622 lastSingle := false 623 624 firstSpace := token.NewSection 625 626 // The line comments for an enum field need to attach after the '|', which 627 // is only known at the next iteration. 628 var lastComment *proto.Comment 629 for i, v := range x.Elements { 630 switch y := v.(type) { 631 case *proto.EnumField: 632 // Add enum value to map 633 intValue := ast.NewLit(token.INT, strconv.Itoa(y.Integer)) 634 f := &ast.Field{ 635 Label: p.stringLit(y.Position, y.Name), 636 Value: intValue, 637 } 638 valueMap.Elts = append(valueMap.Elts, f) 639 640 var e ast.Expr 641 switch p.state.enumMode { 642 case "int": 643 e = ast.NewIdent("#" + y.Name) 644 ast.SetRelPos(e, token.Newline) 645 646 f := &ast.Field{ 647 Label: ast.NewIdent("#" + y.Name), 648 Value: intValue, 649 } 650 ast.SetRelPos(f, firstSpace) 651 firstSpace = token.Newline 652 addComments(f, 0, y.Comment, y.InlineComment) 653 p.addDecl(f) 654 655 case "", "json": 656 // add to enum disjunction 657 value := p.stringLit(y.Position, y.Name) 658 embed := &ast.EmbedDecl{Expr: value} 659 ast.SetRelPos(embed, token.Blank) 660 field := &ast.Field{Label: ast.NewIdent("#enumValue"), Value: intValue} 661 st := &ast.StructLit{ 662 Lbrace: token.Blank.Pos(), 663 Elts: []ast.Decl{embed, field}, 664 } 665 666 addComments(embed, 0, y.Comment, y.InlineComment) 667 if y.Comment == nil && y.InlineComment == nil { 668 ast.SetRelPos(field, token.Blank) 669 ast.SetRelPos(field.Label, token.Blank) 670 st.Rbrace = token.Blank.Pos() 671 if i > 0 && lastSingle { 672 st.Lbrace = token.Newline.Pos() 673 } 674 lastSingle = true 675 } else { 676 lastSingle = false 677 } 678 e = st 679 680 default: 681 p.state.errs = errors.Append(p.state.errs, 682 errors.Newf(token.NoPos, "unknown enum mode %q", p.state.enumMode)) 683 return 684 } 685 686 if enum.Value != nil { 687 e = &ast.BinaryExpr{X: enum.Value, Op: token.OR, Y: e} 688 } 689 enum.Value = e 690 691 // a := fmt.Sprintf("@protobuf(enum,name=%s)", y.Name) 692 // f.Attrs = append(f.Attrs, &ast.Attribute{Text: a}) 693 } 694 } 695 p.addDecl(d) 696 addComments(enum.Value, 1, nil, lastComment) 697 } 698 699 // oneOf converts a Proto OneOf field to CUE. Note that Protobuf defines 700 // a oneOf to be at most one of the fields. Rather than making each field 701 // optional, we define oneOfs as all required fields, but add one more 702 // disjunction allowing no fields. This makes it easier to constrain the 703 // result to include at least one of the values. 704 func (p *protoConverter) oneOf(x *proto.Oneof) { 705 s := ast.NewStruct() 706 ast.SetRelPos(s, token.Newline) 707 embed := &ast.EmbedDecl{Expr: s} 708 embed.AddComment(comment(x.Comment, true)) 709 710 p.addDecl(embed) 711 712 newStruct := func() { 713 s = &ast.StructLit{ 714 // TODO: make this the default in the formatter. 715 Rbrace: token.Newline.Pos(), 716 } 717 embed.Expr = ast.NewBinExpr(token.OR, embed.Expr, s) 718 } 719 for _, v := range x.Elements { 720 switch x := v.(type) { 721 case *proto.OneOfField: 722 newStruct() 723 oneOf := p.parseField(s, 0, x.Field) 724 oneOf.Optional = token.NoPos 725 726 case *proto.Comment: 727 cg := comment(x, false) 728 ast.SetRelPos(cg, token.NewSection) 729 s.Elts = append(s.Elts, cg) 730 731 default: 732 newStruct() 733 p.messageField(s, 1, v) 734 } 735 736 } 737 } 738 739 func (p *protoConverter) parseField(s *ast.StructLit, i int, x *proto.Field) *ast.Field { 740 defer func(saved []string) { p.path = saved }(p.path) 741 p.path = append(p.path, x.Name) 742 743 f := &ast.Field{} 744 addComments(f, i, x.Comment, x.InlineComment) 745 746 name := p.ident(x.Position, x.Name) 747 f.Label = name 748 typ := p.resolve(x.Position, x.Type, x.Options) 749 f.Value = typ 750 s.Elts = append(s.Elts, f) 751 752 o := optionParser{message: s, field: f} 753 754 // body of @protobuf tag: sequence,type[,name=<name>][,...] 755 o.tags += fmt.Sprintf("%v,%s", x.Sequence, x.Type) 756 if x.Name != name.Name { 757 o.tags += ",name=" + x.Name 758 } 759 o.parse(x.Options) 760 p.addTag(f, o.tags) 761 762 if !o.required { 763 f.Optional = token.NoSpace.Pos() 764 } 765 return f 766 } 767 768 type optionParser struct { 769 message *ast.StructLit 770 field *ast.Field 771 required bool 772 tags string 773 } 774 775 func (p *optionParser) parse(options []*proto.Option) { 776 777 // TODO: handle options 778 // - translate options to tags 779 // - interpret CUE options. 780 for _, o := range options { 781 switch o.Name { 782 case "(cue.opt).required": 783 p.required = true 784 // TODO: Dropping comments. Maybe add a dummy tag? 785 786 case "(cue.val)": 787 // TODO: set filename and base offset. 788 expr, err := parser.ParseExpr("", o.Constant.Source) 789 if err != nil { 790 failf(o.Position, "invalid cue.val value: %v", err) 791 } 792 // Any further checks will be done at the end. 793 constraint := &ast.Field{Label: p.field.Label, Value: expr} 794 addComments(constraint, 1, o.Comment, o.InlineComment) 795 p.message.Elts = append(p.message.Elts, constraint) 796 if !p.required { 797 constraint.Optional = token.NoSpace.Pos() 798 } 799 case "(google.api.field_behavior)": 800 if o.Constant.Source == "REQUIRED" { 801 p.required = true 802 } 803 default: 804 // TODO: dropping comments. Maybe add dummy tag? 805 806 // TODO: should CUE support nested attributes? 807 source := o.Constant.SourceRepresentation() 808 p.tags += "," 809 switch source { 810 case "true": 811 p.tags += quoteOption(o.Name) 812 default: 813 p.tags += quoteOption(o.Name + "=" + source) 814 } 815 } 816 } 817 } 818 819 func quoteOption(s string) string { 820 needQuote := false 821 for _, r := range s { 822 if !unicode.In(r, unicode.L, unicode.N) { 823 needQuote = true 824 break 825 } 826 } 827 if !needQuote { 828 return s 829 } 830 if !strings.ContainsAny(s, `"\`) { 831 return literal.String.Quote(s) 832 } 833 esc := `\#` 834 for strings.Contains(s, esc) { 835 esc += "#" 836 } 837 return esc[1:] + `"` + s + `"` + esc[1:] 838 }