emitter.go raw

   1  package jsbackend
   2  
   3  import (
   4  	"fmt"
   5  	"strings"
   6  )
   7  
   8  // Emitter builds JavaScript source code with proper indentation.
   9  type Emitter struct {
  10  	buf    strings.Builder
  11  	indent int
  12  	// Source file being emitted (for source map generation later).
  13  	sourceFile string
  14  }
  15  
  16  // NewEmitter creates a new JS code emitter.
  17  func NewEmitter() *Emitter {
  18  	return &Emitter{}
  19  }
  20  
  21  // Indent increases the indentation level.
  22  func (e *Emitter) Indent() {
  23  	e.indent++
  24  }
  25  
  26  // Dedent decreases the indentation level.
  27  func (e *Emitter) Dedent() {
  28  	if e.indent > 0 {
  29  		e.indent--
  30  	}
  31  }
  32  
  33  // Line emits a line of code with proper indentation.
  34  func (e *Emitter) Line(format string, args ...interface{}) {
  35  	for i := 0; i < e.indent; i++ {
  36  		e.buf.WriteString("  ")
  37  	}
  38  	fmt.Fprintf(&e.buf, format, args...)
  39  	e.buf.WriteByte('\n')
  40  }
  41  
  42  // Raw emits raw text without indentation or newline.
  43  func (e *Emitter) Raw(s string) {
  44  	e.buf.WriteString(s)
  45  }
  46  
  47  // Newline emits an empty line.
  48  func (e *Emitter) Newline() {
  49  	e.buf.WriteByte('\n')
  50  }
  51  
  52  // Comment emits a JS comment.
  53  func (e *Emitter) Comment(format string, args ...interface{}) {
  54  	for i := 0; i < e.indent; i++ {
  55  		e.buf.WriteString("  ")
  56  	}
  57  	e.buf.WriteString("// ")
  58  	fmt.Fprintf(&e.buf, format, args...)
  59  	e.buf.WriteByte('\n')
  60  }
  61  
  62  // Block opens a braced block: { with optional prefix.
  63  func (e *Emitter) Block(format string, args ...interface{}) {
  64  	for i := 0; i < e.indent; i++ {
  65  		e.buf.WriteString("  ")
  66  	}
  67  	if format != "" {
  68  		fmt.Fprintf(&e.buf, format, args...)
  69  		e.buf.WriteString(" {\n")
  70  	} else {
  71  		e.buf.WriteString("{\n")
  72  	}
  73  	e.indent++
  74  }
  75  
  76  // EndBlock closes a braced block: }
  77  func (e *Emitter) EndBlock() {
  78  	e.indent--
  79  	for i := 0; i < e.indent; i++ {
  80  		e.buf.WriteString("  ")
  81  	}
  82  	e.buf.WriteString("}\n")
  83  }
  84  
  85  // EndBlockSuffix closes a braced block with a suffix (e.g., "} else {").
  86  func (e *Emitter) EndBlockSuffix(suffix string) {
  87  	e.indent--
  88  	for i := 0; i < e.indent; i++ {
  89  		e.buf.WriteString("  ")
  90  	}
  91  	fmt.Fprintf(&e.buf, "} %s\n", suffix)
  92  }
  93  
  94  // String returns the generated JavaScript source code.
  95  func (e *Emitter) String() string {
  96  	return e.buf.String()
  97  }
  98  
  99  // Reset clears the emitter buffer.
 100  func (e *Emitter) Reset() {
 101  	e.buf.Reset()
 102  	e.indent = 0
 103  }
 104  
 105  // Len returns the current buffer length.
 106  func (e *Emitter) Len() int {
 107  	return e.buf.Len()
 108  }
 109  
 110  // ImportStatement emits an ES module import.
 111  func (e *Emitter) ImportStatement(names string, from string) {
 112  	e.Line("import %s from '%s';", names, from)
 113  }
 114  
 115  // ImportAll emits import * as X from '...'.
 116  func (e *Emitter) ImportAll(alias string, from string) {
 117  	e.Line("import * as %s from '%s';", alias, from)
 118  }
 119  
 120  // ExportFunction emits an exported async or sync function declaration.
 121  func (e *Emitter) ExportFunction(name string, params string, async bool) {
 122  	prefix := ""
 123  	if async {
 124  		prefix = "async "
 125  	}
 126  	e.Block("export %sfunction %s(%s)", prefix, name, params)
 127  }
 128  
 129  // Function emits a function declaration.
 130  func (e *Emitter) Function(name string, params string, async bool) {
 131  	prefix := ""
 132  	if async {
 133  		prefix = "async "
 134  	}
 135  	e.Block("%sfunction %s(%s)", prefix, name, params)
 136  }
 137  
 138  // JsString returns a properly escaped JavaScript string literal.
 139  func JsString(s string) string {
 140  	var b strings.Builder
 141  	b.WriteByte('\'')
 142  	for _, r := range s {
 143  		switch r {
 144  		case '\'':
 145  			b.WriteString("\\'")
 146  		case '\\':
 147  			b.WriteString("\\\\")
 148  		case '\n':
 149  			b.WriteString("\\n")
 150  		case '\r':
 151  			b.WriteString("\\r")
 152  		case '\t':
 153  			b.WriteString("\\t")
 154  		case '\x00':
 155  			b.WriteString("\\0")
 156  		default:
 157  			if r < 0x20 {
 158  				fmt.Fprintf(&b, "\\x%02x", r)
 159  			} else {
 160  				b.WriteRune(r)
 161  			}
 162  		}
 163  	}
 164  	b.WriteByte('\'')
 165  	return b.String()
 166  }
 167  
 168  // JsIdentifier converts a Go identifier to a valid JS identifier.
 169  // Go allows unicode identifiers that JS also allows, but we need to handle
 170  // cases like package paths becoming module names.
 171  func JsIdentifier(name string) string {
 172  	name = strings.ReplaceAll(name, "#", "$")
 173  	name = strings.ReplaceAll(name, "/", "$")
 174  	name = strings.ReplaceAll(name, ".", "$")
 175  	name = strings.ReplaceAll(name, "-", "_")
 176  	if isJsReserved(name) {
 177  		return "$" + name
 178  	}
 179  	return name
 180  }
 181  
 182  // isJsReserved checks if a name is a JS reserved word.
 183  func isJsReserved(name string) bool {
 184  	switch name {
 185  	case "break", "case", "catch", "continue", "debugger", "default",
 186  		"delete", "do", "else", "finally", "for", "function", "if",
 187  		"in", "instanceof", "new", "return", "switch", "this",
 188  		"throw", "try", "typeof", "var", "void", "while", "with",
 189  		"class", "const", "enum", "export", "extends", "import",
 190  		"super", "implements", "interface", "let", "package",
 191  		"private", "protected", "public", "static", "yield",
 192  		"await", "async":
 193  		return true
 194  	}
 195  	return false
 196  }
 197  
 198  // PackageModuleName converts a Go package import path to a JS module filename.
 199  func PackageModuleName(pkgPath string) string {
 200  	name := strings.ReplaceAll(pkgPath, "/", "_")
 201  	name = strings.ReplaceAll(name, ".", "_")
 202  	name = strings.ReplaceAll(name, "-", "_")
 203  	return name + ".mjs"
 204  }
 205