1133 lines
38 KiB
Go
1133 lines
38 KiB
Go
package jsxtransforms
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers"
|
|
"github.com/dlclark/regexp2"
|
|
)
|
|
|
|
type JSXTransformer struct {
|
|
transformers.Transformer
|
|
compilerOptions *core.CompilerOptions
|
|
emitResolver printer.EmitResolver
|
|
|
|
importSpecifier string
|
|
filenameDeclaration *ast.Node
|
|
utilizedImplicitRuntimeImports map[string]map[string]*ast.Node
|
|
inJsxChild bool
|
|
|
|
currentSourceFile *ast.SourceFile
|
|
}
|
|
|
|
func NewJSXTransformer(opts *transformers.TransformOptions) *transformers.Transformer {
|
|
compilerOptions := opts.CompilerOptions
|
|
emitContext := opts.Context
|
|
tx := &JSXTransformer{
|
|
compilerOptions: compilerOptions,
|
|
emitResolver: opts.EmitResolver,
|
|
}
|
|
return tx.NewTransformer(tx.visit, emitContext)
|
|
}
|
|
|
|
func (tx *JSXTransformer) getCurrentFileNameExpression() *ast.Node {
|
|
if tx.filenameDeclaration != nil {
|
|
return tx.filenameDeclaration.AsVariableDeclaration().Name()
|
|
}
|
|
d := tx.Factory().NewVariableDeclaration(
|
|
tx.Factory().NewUniqueNameEx("_jsxFileName", printer.AutoGenerateOptions{
|
|
Flags: printer.GeneratedIdentifierFlagsOptimistic | printer.GeneratedIdentifierFlagsFileLevel,
|
|
}),
|
|
nil,
|
|
nil,
|
|
tx.Factory().NewStringLiteral(tx.currentSourceFile.FileName()),
|
|
)
|
|
tx.filenameDeclaration = d
|
|
return d.AsVariableDeclaration().Name()
|
|
}
|
|
|
|
func (tx *JSXTransformer) getJsxFactoryCalleePrimitive(isStaticChildren bool) string {
|
|
if tx.compilerOptions.Jsx == core.JsxEmitReactJSXDev {
|
|
return "jsxDEV"
|
|
}
|
|
if isStaticChildren {
|
|
return "jsxs"
|
|
}
|
|
return "jsx"
|
|
}
|
|
|
|
func (tx *JSXTransformer) getJsxFactoryCallee(isStaticChildren bool) *ast.Node {
|
|
t := tx.getJsxFactoryCalleePrimitive(isStaticChildren)
|
|
return tx.getImplicitImportForName(t)
|
|
}
|
|
|
|
func (tx *JSXTransformer) getImplicitJsxFragmentReference() *ast.Node {
|
|
return tx.getImplicitImportForName("Fragment")
|
|
}
|
|
|
|
func (tx *JSXTransformer) getImplicitImportForName(name string) *ast.Node {
|
|
importSource := tx.importSpecifier
|
|
if name != "createElement" {
|
|
importSource = ast.GetJSXRuntimeImport(importSource, tx.compilerOptions)
|
|
}
|
|
existing, ok := tx.utilizedImplicitRuntimeImports[importSource]
|
|
if ok {
|
|
elem, ok := existing[name]
|
|
if ok {
|
|
return elem.AsImportSpecifier().Name()
|
|
}
|
|
} else {
|
|
tx.utilizedImplicitRuntimeImports[importSource] = make(map[string]*ast.Node)
|
|
}
|
|
|
|
generatedName := tx.Factory().NewUniqueNameEx("_"+name, printer.AutoGenerateOptions{
|
|
Flags: printer.GeneratedIdentifierFlagsOptimistic | printer.GeneratedIdentifierFlagsFileLevel | printer.GeneratedIdentifierFlagsAllowNameSubstitution,
|
|
})
|
|
specifier := tx.Factory().NewImportSpecifier(false, tx.Factory().NewIdentifier(name), generatedName)
|
|
tx.emitResolver.SetReferencedImportDeclaration(generatedName, specifier)
|
|
tx.utilizedImplicitRuntimeImports[importSource][name] = specifier
|
|
return specifier.Name()
|
|
}
|
|
|
|
func (tx *JSXTransformer) setInChild(v bool) {
|
|
tx.inJsxChild = v
|
|
}
|
|
|
|
func (tx *JSXTransformer) visit(node *ast.Node) *ast.Node {
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
if node.SubtreeFacts()&ast.SubtreeContainsJsx == 0 {
|
|
return node
|
|
}
|
|
switch node.Kind {
|
|
case ast.KindSourceFile:
|
|
tx.setInChild(false)
|
|
return tx.visitSourceFile(node.AsSourceFile())
|
|
case ast.KindJsxElement:
|
|
return tx.visitJsxElement(node.AsJsxElement())
|
|
case ast.KindJsxSelfClosingElement:
|
|
return tx.visitJsxSelfClosingElement(node.AsJsxSelfClosingElement())
|
|
case ast.KindJsxFragment:
|
|
return tx.visitJsxFragment(node.AsJsxFragment())
|
|
case ast.KindJsxOpeningElement:
|
|
panic("JsxOpeningElement should not be visited, handled in visitJsxElement")
|
|
case ast.KindJsxOpeningFragment:
|
|
panic("JsxOpeningFragment should not be visited, handled in visitJsxFragment")
|
|
case ast.KindJsxText:
|
|
tx.setInChild(false)
|
|
return tx.visitJsxText(node.AsJsxText())
|
|
case ast.KindJsxExpression:
|
|
tx.setInChild(false)
|
|
return tx.visitJsxExpression(node.AsJsxExpression())
|
|
}
|
|
tx.setInChild(false)
|
|
return tx.Visitor().VisitEachChild(node) // by default, do nothing
|
|
}
|
|
|
|
/**
|
|
* The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread
|
|
*/
|
|
func hasKeyAfterPropsSpread(node *ast.Node) bool {
|
|
spread := false
|
|
opener := node
|
|
if node.Kind == ast.KindJsxElement {
|
|
opener = node.AsJsxElement().OpeningElement
|
|
} // otherwise self-closing
|
|
for _, elem := range opener.Attributes().Properties() {
|
|
if ast.IsJsxSpreadAttribute(elem) && (!ast.IsObjectLiteralExpression(elem.Expression()) || core.Some(elem.Expression().Properties(), ast.IsSpreadAssignment)) {
|
|
spread = true
|
|
} else if spread && ast.IsJsxAttribute(elem) && ast.IsIdentifier(elem.Name()) && elem.Name().AsIdentifier().Text == "key" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (tx *JSXTransformer) shouldUseCreateElement(node *ast.Node) bool {
|
|
return len(tx.importSpecifier) == 0 || hasKeyAfterPropsSpread(node)
|
|
}
|
|
|
|
func insertStatementAfterPrologue[T any](to []*ast.Node, statement *ast.Node, isPrologueDirective func(callee T, node *ast.Node) bool, callee T) []*ast.Node {
|
|
if statement == nil {
|
|
return to
|
|
}
|
|
statementIdx := 0
|
|
// skip all prologue directives to insert at the correct position
|
|
for ; statementIdx < len(to); statementIdx++ {
|
|
if !isPrologueDirective(callee, to[statementIdx]) {
|
|
break
|
|
}
|
|
}
|
|
return slices.Insert(to, statementIdx, statement)
|
|
}
|
|
|
|
func (tx *JSXTransformer) isAnyPrologueDirective(node *ast.Node) bool {
|
|
return ast.IsPrologueDirective(node) || (tx.EmitContext().EmitFlags(node)&printer.EFCustomPrologue != 0)
|
|
}
|
|
|
|
func (tx *JSXTransformer) insertStatementAfterCustomPrologue(to []*ast.Node, statement *ast.Node) []*ast.Node {
|
|
return insertStatementAfterPrologue(to, statement, (*JSXTransformer).isAnyPrologueDirective, tx)
|
|
}
|
|
|
|
func sortByImportDeclarationSource(a *ast.Node, b *ast.Node) int {
|
|
return stringutil.CompareStringsCaseSensitive(a.AsImportDeclaration().ModuleSpecifier.AsStringLiteral().Text, b.AsImportDeclaration().ModuleSpecifier.AsStringLiteral().Text)
|
|
}
|
|
|
|
func getSpecifierOfRequireCall(s *ast.Node) string {
|
|
return s.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration().Initializer.AsCallExpression().Arguments.Nodes[0].AsStringLiteral().Text
|
|
}
|
|
|
|
func sortByRequireSource(a *ast.Node, b *ast.Node) int {
|
|
return stringutil.CompareStringsCaseSensitive(getSpecifierOfRequireCall(a), getSpecifierOfRequireCall(b))
|
|
}
|
|
|
|
func sortImportSpecifiers(a *ast.Node, b *ast.Node) int {
|
|
res := stringutil.CompareStringsCaseSensitive(a.AsImportSpecifier().PropertyName.Text(), b.AsImportSpecifier().PropertyName.Text())
|
|
if res != 0 {
|
|
return res
|
|
}
|
|
return stringutil.CompareStringsCaseSensitive(a.AsImportSpecifier().Name().AsIdentifier().Text, b.AsImportSpecifier().Name().AsIdentifier().Text)
|
|
}
|
|
|
|
func getSortedSpecifiers(m map[string]*ast.Node) []*ast.Node {
|
|
res := slices.Collect(maps.Values(m))
|
|
slices.SortFunc(res, sortImportSpecifiers)
|
|
return res
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitSourceFile(file *ast.SourceFile) *ast.Node {
|
|
if file.IsDeclarationFile {
|
|
return file.AsNode()
|
|
}
|
|
|
|
tx.currentSourceFile = file
|
|
tx.importSpecifier = ast.GetJSXImplicitImportBase(tx.compilerOptions, file)
|
|
tx.filenameDeclaration = nil
|
|
tx.utilizedImplicitRuntimeImports = make(map[string]map[string]*ast.Node)
|
|
|
|
visited := tx.Visitor().VisitEachChild(file.AsNode())
|
|
tx.EmitContext().AddEmitHelper(visited.AsNode(), tx.EmitContext().ReadEmitHelpers()...)
|
|
statements := visited.Statements()
|
|
statementsUpdated := false
|
|
if tx.filenameDeclaration != nil {
|
|
statements = tx.insertStatementAfterCustomPrologue(statements, tx.Factory().NewVariableStatement(nil, tx.Factory().NewVariableDeclarationList(
|
|
ast.NodeFlagsConst,
|
|
tx.Factory().NewNodeList([]*ast.Node{tx.filenameDeclaration}),
|
|
)))
|
|
statementsUpdated = true
|
|
}
|
|
|
|
if len(tx.utilizedImplicitRuntimeImports) > 0 {
|
|
// A key difference from strada is that these imports are sorted in corsa, rather than appearing in a use-defined order
|
|
if ast.IsExternalModule(file) {
|
|
statementsUpdated = true
|
|
newStatements := make([]*ast.Node, 0, len(tx.utilizedImplicitRuntimeImports))
|
|
for importSource, importSpecifiersMap := range tx.utilizedImplicitRuntimeImports {
|
|
s := tx.Factory().NewImportDeclaration(
|
|
nil,
|
|
tx.Factory().NewImportClause(ast.KindUnknown, nil, tx.Factory().NewNamedImports(tx.Factory().NewNodeList(getSortedSpecifiers(importSpecifiersMap)))),
|
|
tx.Factory().NewStringLiteral(importSource),
|
|
nil,
|
|
)
|
|
ast.SetParentInChildren(s)
|
|
newStatements = append(newStatements, s)
|
|
|
|
}
|
|
slices.SortFunc(newStatements, sortByImportDeclarationSource)
|
|
for _, e := range newStatements {
|
|
statements = tx.insertStatementAfterCustomPrologue(statements, e)
|
|
}
|
|
} else if ast.IsExternalOrCommonJSModule(file) {
|
|
statementsUpdated = true
|
|
newStatements := make([]*ast.Node, 0, len(tx.utilizedImplicitRuntimeImports))
|
|
for importSource, importSpecifiersMap := range tx.utilizedImplicitRuntimeImports {
|
|
sorted := getSortedSpecifiers(importSpecifiersMap)
|
|
asBindingElems := make([]*ast.Node, 0, len(sorted))
|
|
for _, elem := range sorted {
|
|
asBindingElems = append(asBindingElems, tx.Factory().NewBindingElement(nil, elem.AsImportSpecifier().PropertyName, elem.AsImportSpecifier().Name(), nil))
|
|
}
|
|
s := tx.Factory().NewVariableStatement(nil, tx.Factory().NewVariableDeclarationList(ast.NodeFlagsConst, tx.Factory().NewNodeList([]*ast.Node{tx.Factory().NewVariableDeclaration(
|
|
tx.Factory().NewBindingPattern(ast.KindObjectBindingPattern, tx.Factory().NewNodeList(asBindingElems)),
|
|
nil,
|
|
nil,
|
|
tx.Factory().NewCallExpression(tx.Factory().NewIdentifier("require"), nil, nil, tx.Factory().NewNodeList([]*ast.Node{tx.Factory().NewStringLiteral(importSource)}), ast.NodeFlagsNone),
|
|
)})))
|
|
ast.SetParentInChildren(s)
|
|
newStatements = append(newStatements, s)
|
|
}
|
|
slices.SortFunc(newStatements, sortByRequireSource)
|
|
for _, e := range newStatements {
|
|
statements = tx.insertStatementAfterCustomPrologue(statements, e)
|
|
}
|
|
} else {
|
|
// Do nothing (script file) - consider an error in the checker?
|
|
}
|
|
}
|
|
|
|
if statementsUpdated {
|
|
visited = tx.Factory().UpdateSourceFile(file, tx.Factory().NewNodeList(statements), file.EndOfFileToken)
|
|
}
|
|
|
|
tx.currentSourceFile = nil
|
|
tx.importSpecifier = ""
|
|
tx.filenameDeclaration = nil
|
|
tx.utilizedImplicitRuntimeImports = nil
|
|
|
|
return visited
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxElement(element *ast.JsxElement) *ast.Node {
|
|
tagTransform := (*JSXTransformer).visitJsxOpeningLikeElementJSX
|
|
if tx.shouldUseCreateElement(element.AsNode()) {
|
|
tagTransform = (*JSXTransformer).visitJsxOpeningLikeElementCreateElement
|
|
}
|
|
return tagTransform(tx, element.OpeningElement, element.Children, element.AsNode())
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxSelfClosingElement(element *ast.JsxSelfClosingElement) *ast.Node {
|
|
tagTransform := (*JSXTransformer).visitJsxOpeningLikeElementJSX
|
|
if tx.shouldUseCreateElement(element.AsNode()) {
|
|
tagTransform = (*JSXTransformer).visitJsxOpeningLikeElementCreateElement
|
|
}
|
|
return tagTransform(tx, element.AsNode(), nil, element.AsNode())
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxFragment(fragment *ast.JsxFragment) *ast.Node {
|
|
tagTransform := (*JSXTransformer).visitJsxOpeningFragmentJSX
|
|
if len(tx.importSpecifier) == 0 {
|
|
tagTransform = (*JSXTransformer).visitJsxOpeningFragmentCreateElement
|
|
}
|
|
return tagTransform(tx, fragment.OpeningFragment.AsJsxOpeningFragment(), fragment.Children, fragment.AsNode())
|
|
}
|
|
|
|
func (tx *JSXTransformer) convertJsxChildrenToChildrenPropObject(children []*ast.JsxChild) *ast.Node {
|
|
prop := tx.convertJsxChildrenToChildrenPropAssignment(children)
|
|
if prop == nil {
|
|
return nil
|
|
}
|
|
return tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{prop}), false)
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxChildToExpression(node *ast.Node) *ast.Node {
|
|
tx.setInChild(true)
|
|
defer tx.setInChild(false)
|
|
return tx.Visitor().Visit(node)
|
|
}
|
|
|
|
func (tx *JSXTransformer) convertJsxChildrenToChildrenPropAssignment(children []*ast.JsxChild) *ast.Node {
|
|
nonWhitespceChildren := ast.GetSemanticJsxChildren(children)
|
|
if len(nonWhitespceChildren) == 1 && (nonWhitespceChildren[0].Kind != ast.KindJsxExpression || nonWhitespceChildren[0].AsJsxExpression().DotDotDotToken == nil) {
|
|
result := tx.transformJsxChildToExpression(nonWhitespceChildren[0])
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
return tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("children"), nil, nil, result)
|
|
}
|
|
results := make([]*ast.Node, 0, len(nonWhitespceChildren))
|
|
for _, child := range nonWhitespceChildren {
|
|
res := tx.transformJsxChildToExpression(child)
|
|
if res == nil {
|
|
continue
|
|
}
|
|
results = append(results, res)
|
|
}
|
|
if len(results) == 0 {
|
|
return nil
|
|
}
|
|
return tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("children"), nil, nil, tx.Factory().NewArrayLiteralExpression(tx.Factory().NewNodeList(results), false))
|
|
}
|
|
|
|
func (tx *JSXTransformer) getTagName(node *ast.Node) *ast.Node {
|
|
if node.Kind == ast.KindJsxElement {
|
|
return tx.getTagName(node.AsJsxElement().OpeningElement)
|
|
} else if ast.IsJsxOpeningLikeElement(node) {
|
|
tagName := node.TagName()
|
|
if ast.IsIdentifier(tagName) && scanner.IsIntrinsicJsxName(tagName.Text()) {
|
|
return tx.Factory().NewStringLiteral(tagName.Text())
|
|
} else if ast.IsJsxNamespacedName(tagName) {
|
|
return tx.Factory().NewStringLiteral(tagName.AsJsxNamespacedName().Namespace.Text() + ":" + tagName.AsJsxNamespacedName().Name().Text())
|
|
} else {
|
|
return createExpressionFromEntityName(tx.Factory(), tagName)
|
|
}
|
|
} else {
|
|
panic("unhandled node kind passed to getTagName: " + node.Kind.String())
|
|
}
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxOpeningLikeElementJSX(element *ast.Node, children *ast.NodeList, location *ast.Node) *ast.Node {
|
|
tagName := tx.getTagName(element)
|
|
var childrenProp *ast.Node
|
|
if children != nil && len(children.Nodes) > 0 {
|
|
childrenProp = tx.convertJsxChildrenToChildrenPropAssignment(children.Nodes)
|
|
}
|
|
var keyAttr *ast.Node
|
|
attrs := element.Attributes().AsJsxAttributes().Properties.Nodes
|
|
for i, p := range attrs {
|
|
if p.Kind == ast.KindJsxAttribute && p.AsJsxAttribute().Name() != nil && ast.IsIdentifier(p.AsJsxAttribute().Name()) && p.AsJsxAttribute().Name().AsIdentifier().Text == "key" {
|
|
keyAttr = p
|
|
attrs = slices.Clone(attrs)
|
|
attrs = slices.Delete(attrs, i, i+1)
|
|
break
|
|
}
|
|
}
|
|
var object *ast.Node
|
|
if len(attrs) > 0 {
|
|
object = tx.transformJsxAttributesToObjectProps(attrs, childrenProp)
|
|
} else {
|
|
objectChildren := []*ast.Node{}
|
|
if childrenProp != nil {
|
|
objectChildren = append(objectChildren, childrenProp)
|
|
}
|
|
object = tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(objectChildren), false) // When there are no attributes, React wants {}
|
|
}
|
|
return tx.visitJsxOpeningLikeElementOrFragmentJSX(
|
|
tagName,
|
|
object,
|
|
keyAttr,
|
|
children,
|
|
location,
|
|
)
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxAttributesToObjectProps(attrs []*ast.Node, childrenProp *ast.Node) *ast.Node {
|
|
target := tx.compilerOptions.GetEmitScriptTarget()
|
|
if target >= core.ScriptTargetES2018 {
|
|
// target has object spreads, can keep as-is
|
|
return tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(tx.transformJsxAttributesToProps(attrs, childrenProp)), false)
|
|
}
|
|
return tx.transformJsxAttributesToExpression(attrs, childrenProp)
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxAttributesToExpression(attrs []*ast.Node, childrenProp *ast.Node) *ast.Node {
|
|
expressions := make([]*ast.Expression, 0, 2)
|
|
properties := make([]*ast.ObjectLiteralElement, 0, len(attrs))
|
|
|
|
for _, attr := range attrs {
|
|
if ast.IsJsxSpreadAttribute(attr) {
|
|
// as an optimization we try to flatten the first level of spread inline object
|
|
// as if its props would be passed as JSX attributes
|
|
if ast.IsObjectLiteralExpression(attr.Expression()) && !hasProto(attr.Expression().AsObjectLiteralExpression()) {
|
|
for _, prop := range attr.Expression().Properties() {
|
|
if ast.IsSpreadAssignment(prop) {
|
|
expressions, properties = tx.combinePropertiesIntoNewExpression(expressions, properties)
|
|
expressions = append(expressions, tx.Visitor().Visit(prop.Expression()))
|
|
continue
|
|
}
|
|
properties = append(properties, tx.Visitor().Visit(prop))
|
|
}
|
|
continue
|
|
}
|
|
expressions, properties = tx.combinePropertiesIntoNewExpression(expressions, properties)
|
|
expressions = append(expressions, tx.Visitor().Visit(attr.Expression()))
|
|
continue
|
|
}
|
|
properties = append(properties, tx.transformJsxAttributeToObjectLiteralElement(attr.AsJsxAttribute()))
|
|
}
|
|
|
|
if childrenProp != nil {
|
|
properties = append(properties, childrenProp)
|
|
}
|
|
|
|
expressions, _ = tx.combinePropertiesIntoNewExpression(expressions, properties)
|
|
|
|
if len(expressions) > 0 && !ast.IsObjectLiteralExpression(expressions[0]) {
|
|
// We must always emit at least one object literal before a spread attribute
|
|
// as the JSX always factory expects a fresh object, so we need to make a copy here
|
|
// we also avoid mutating an external reference by doing this (first expression is used as assign's target)
|
|
expressions = append([]*ast.Expression{tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{}), false)}, expressions...)
|
|
}
|
|
|
|
if len(expressions) == 1 {
|
|
return expressions[0]
|
|
}
|
|
return tx.Factory().NewAssignHelper(expressions, tx.compilerOptions.GetEmitScriptTarget())
|
|
}
|
|
|
|
func (tx *JSXTransformer) combinePropertiesIntoNewExpression(expressions []*ast.Expression, props []*ast.ObjectLiteralElement) ([]*ast.Expression, []*ast.ObjectLiteralElement) {
|
|
if len(props) == 0 {
|
|
return expressions, props
|
|
}
|
|
newObj := tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(props), false)
|
|
expressions = append(expressions, newObj)
|
|
return expressions, nil
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxAttributesToProps(attrs []*ast.Node, childrenProp *ast.Node) []*ast.Node {
|
|
props := make([]*ast.Node, 0, len(attrs))
|
|
for _, attr := range attrs {
|
|
if attr.Kind == ast.KindJsxSpreadAttribute {
|
|
res := tx.transformJsxSpreadAttributesToProps(attr.AsJsxSpreadAttribute())
|
|
props = append(props, res...)
|
|
} else {
|
|
props = append(props, tx.transformJsxAttributeToObjectLiteralElement(attr.AsJsxAttribute()))
|
|
}
|
|
}
|
|
if childrenProp != nil {
|
|
props = append(props, childrenProp)
|
|
}
|
|
return props
|
|
}
|
|
|
|
func hasProto(obj *ast.ObjectLiteralExpression) bool {
|
|
for _, p := range obj.Properties.Nodes {
|
|
if ast.IsPropertyAssignment(p) && (ast.IsStringLiteral(p.Name()) || ast.IsIdentifier(p.Name())) && p.Name().Text() == "__proto__" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxSpreadAttributesToProps(node *ast.JsxSpreadAttribute) []*ast.Node {
|
|
if ast.IsObjectLiteralExpression(node.Expression) && !hasProto(node.Expression.AsObjectLiteralExpression()) {
|
|
res, _ := tx.Visitor().VisitSlice(node.Expression.Properties())
|
|
return res
|
|
}
|
|
return []*ast.Node{tx.Factory().NewSpreadAssignment(tx.Visitor().Visit(node.Expression))}
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxAttributeToObjectLiteralElement(node *ast.JsxAttribute) *ast.Node {
|
|
name := tx.getAttributeName(node)
|
|
expression := tx.transformJsxAttributeInitializer(node.Initializer)
|
|
return tx.Factory().NewPropertyAssignment(nil, name, nil, nil, expression)
|
|
}
|
|
|
|
/**
|
|
* Emit an attribute name, which is quoted if it needs to be quoted. Because
|
|
* these emit into an object literal property name, we don't need to be worried
|
|
* about keywords, just non-identifier characters
|
|
*/
|
|
func (tx *JSXTransformer) getAttributeName(node *ast.JsxAttribute) *ast.Node {
|
|
name := node.Name()
|
|
if ast.IsIdentifier(name) {
|
|
text := name.Text()
|
|
if scanner.IsIdentifierText(text, core.LanguageVariantStandard) {
|
|
return name
|
|
}
|
|
return tx.Factory().NewStringLiteral(text)
|
|
}
|
|
// must be jsx namespace
|
|
return tx.Factory().NewStringLiteral(name.AsJsxNamespacedName().Namespace.Text() + ":" + name.AsJsxNamespacedName().Name().Text())
|
|
}
|
|
|
|
func (tx *JSXTransformer) transformJsxAttributeInitializer(node *ast.Node) *ast.Node {
|
|
if node == nil {
|
|
return tx.Factory().NewTrueExpression()
|
|
}
|
|
if node.Kind == ast.KindStringLiteral {
|
|
// Always recreate the literal to escape any escape sequences or newlines which may be in the original jsx string and which
|
|
// Need to be escaped to be handled correctly in a normal string
|
|
res := tx.Factory().NewStringLiteral(decodeEntities(node.Text()))
|
|
res.Loc = node.Loc
|
|
return res
|
|
}
|
|
if node.Kind == ast.KindJsxExpression {
|
|
if node.AsJsxExpression().Expression == nil {
|
|
return tx.Factory().NewTrueExpression()
|
|
}
|
|
return tx.Visitor().Visit(node.AsJsxExpression().Expression)
|
|
}
|
|
if ast.IsJsxElement(node) || ast.IsJsxSelfClosingElement(node) || ast.IsJsxFragment(node) {
|
|
tx.setInChild(false)
|
|
return tx.Visitor().Visit(node)
|
|
}
|
|
panic("Unhandled node kind found in jsx initializer: " + node.Kind.String())
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxOpeningLikeElementOrFragmentJSX(
|
|
tagName *ast.Expression,
|
|
object *ast.Expression,
|
|
keyAttr *ast.Node,
|
|
children *ast.NodeList,
|
|
location *ast.Node,
|
|
) *ast.Node {
|
|
var nonWhitespaceChildren []*ast.Node
|
|
if children != nil {
|
|
nonWhitespaceChildren = ast.GetSemanticJsxChildren(children.Nodes)
|
|
}
|
|
isStaticChildren := len(nonWhitespaceChildren) > 1 || (len(nonWhitespaceChildren) == 1 && ast.IsJsxExpression(nonWhitespaceChildren[0]) && nonWhitespaceChildren[0].AsJsxExpression().DotDotDotToken != nil)
|
|
args := make([]*ast.Node, 0, 3)
|
|
args = append(args, tagName, object)
|
|
// function jsx(type, config, maybeKey) {}
|
|
// "maybeKey" is optional. It is acceptable to use "_jsx" without a third argument
|
|
if keyAttr != nil {
|
|
args = append(args, tx.transformJsxAttributeInitializer(keyAttr.Initializer()))
|
|
}
|
|
|
|
if tx.compilerOptions.Jsx == core.JsxEmitReactJSXDev {
|
|
originalFile := tx.EmitContext().Original(tx.currentSourceFile.AsNode())
|
|
if originalFile != nil && ast.IsSourceFile(originalFile) {
|
|
// "maybeKey" has to be replaced with "void 0" to not break the jsxDEV signature
|
|
if keyAttr == nil {
|
|
args = append(args, tx.Factory().NewVoidZeroExpression())
|
|
}
|
|
// isStaticChildren development flag
|
|
if isStaticChildren {
|
|
args = append(args, tx.Factory().NewTrueExpression())
|
|
} else {
|
|
args = append(args, tx.Factory().NewFalseExpression())
|
|
}
|
|
// __source development flag
|
|
line, col := scanner.GetECMALineAndCharacterOfPosition(originalFile.AsSourceFile(), location.Pos())
|
|
args = append(args, tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{
|
|
tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("fileName"), nil, nil, tx.getCurrentFileNameExpression()),
|
|
tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("lineNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(line+1), 10))),
|
|
tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("columnNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(col+1), 10))),
|
|
}), false))
|
|
// __self development flag
|
|
args = append(args, tx.Factory().NewThisExpression())
|
|
}
|
|
}
|
|
|
|
element := tx.Factory().NewCallExpression(tx.getJsxFactoryCallee(isStaticChildren), nil, nil, tx.Factory().NewNodeList(args), ast.NodeFlagsNone)
|
|
element.Loc = location.Loc
|
|
|
|
if tx.inJsxChild {
|
|
tx.EmitContext().AddEmitFlags(element, printer.EFStartOnNewLine)
|
|
}
|
|
|
|
return element
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxOpeningFragmentJSX(fragment *ast.JsxOpeningFragment, children *ast.NodeList, location *ast.Node) *ast.Node {
|
|
var childrenProps *ast.Expression
|
|
if children != nil && len(children.Nodes) > 0 {
|
|
result := tx.convertJsxChildrenToChildrenPropObject(children.Nodes)
|
|
if result != nil {
|
|
childrenProps = result
|
|
}
|
|
}
|
|
if childrenProps == nil {
|
|
childrenProps = tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{}), false)
|
|
}
|
|
return tx.visitJsxOpeningLikeElementOrFragmentJSX(
|
|
tx.getImplicitJsxFragmentReference(),
|
|
childrenProps,
|
|
nil,
|
|
children,
|
|
location,
|
|
)
|
|
}
|
|
|
|
func (tx *JSXTransformer) createReactNamespace(reactNamespace string, parent *ast.Node) *ast.Node {
|
|
// To ensure the emit resolver can properly resolve the namespace, we need to
|
|
// treat this identifier as if it were a source tree node by clearing the `Synthesized`
|
|
// flag and setting a parent node. TODO: Is this still true? The emit resolver is supposed to be
|
|
// hardened aginast this, so long as the node retains original node pointers back to a parsed node
|
|
if len(reactNamespace) == 0 {
|
|
reactNamespace = "React"
|
|
}
|
|
react := tx.Factory().NewIdentifier(reactNamespace)
|
|
react.Flags &= ^ast.NodeFlagsSynthesized
|
|
|
|
// Set the parent that is in parse tree
|
|
// this makes sure that parent chain is intact for checker to traverse complete scope tree
|
|
react.Parent = tx.EmitContext().ParseNode(parent)
|
|
return react
|
|
}
|
|
|
|
func (tx *JSXTransformer) createJsxFactoryExpressionFromEntityName(e *ast.Node, parent *ast.Node) *ast.Node {
|
|
if ast.IsQualifiedName(e) {
|
|
left := tx.createJsxFactoryExpressionFromEntityName(e.AsQualifiedName().Left, parent)
|
|
right := tx.Factory().NewIdentifier(e.AsQualifiedName().Right.Text())
|
|
return tx.Factory().NewPropertyAccessExpression(left, nil, right, ast.NodeFlagsNone)
|
|
}
|
|
return tx.createReactNamespace(e.AsIdentifier().Text, parent)
|
|
}
|
|
|
|
func (tx *JSXTransformer) createJsxPsuedoFactoryExpression(parent *ast.Node, e *ast.Node, target string) *ast.Node {
|
|
if e != nil {
|
|
return tx.createJsxFactoryExpressionFromEntityName(e, parent)
|
|
}
|
|
return tx.Factory().NewPropertyAccessExpression(
|
|
tx.createReactNamespace(tx.compilerOptions.ReactNamespace, parent),
|
|
nil,
|
|
tx.Factory().NewIdentifier(target),
|
|
ast.NodeFlagsNone,
|
|
)
|
|
}
|
|
|
|
func (tx *JSXTransformer) createJsxFactoryExpression(parent *ast.Node) *ast.Node {
|
|
e := tx.emitResolver.GetJsxFactoryEntity(tx.currentSourceFile.AsNode())
|
|
return tx.createJsxPsuedoFactoryExpression(parent, e, "createElement")
|
|
}
|
|
|
|
func (tx *JSXTransformer) createJsxFragmentFactoryExpression(parent *ast.Node) *ast.Node {
|
|
e := tx.emitResolver.GetJsxFragmentFactoryEntity(tx.currentSourceFile.AsNode())
|
|
return tx.createJsxPsuedoFactoryExpression(parent, e, "Fragment")
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxOpeningLikeElementCreateElement(element *ast.Node, children *ast.NodeList, location *ast.Node) *ast.Node {
|
|
tagName := tx.getTagName(element)
|
|
attrs := element.Attributes().Properties()
|
|
var objectProperties *ast.Expression
|
|
if len(attrs) > 0 {
|
|
objectProperties = tx.transformJsxAttributesToObjectProps(attrs, nil)
|
|
} else {
|
|
objectProperties = tx.Factory().NewKeywordExpression(ast.KindNullKeyword) // When there are no attributes, React wants "null"
|
|
}
|
|
|
|
var callee *ast.Expression
|
|
if len(tx.importSpecifier) == 0 {
|
|
callee = tx.createJsxFactoryExpression(element)
|
|
} else {
|
|
callee = tx.getImplicitImportForName("createElement")
|
|
}
|
|
|
|
var newChildren []*ast.Node
|
|
if children != nil && len(children.Nodes) > 0 {
|
|
for _, c := range children.Nodes {
|
|
res := tx.transformJsxChildToExpression(c)
|
|
if res != nil {
|
|
if len(children.Nodes) > 1 {
|
|
tx.EmitContext().AddEmitFlags(res, printer.EFStartOnNewLine)
|
|
}
|
|
newChildren = append(newChildren, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
args := make([]*ast.Expression, 0, len(newChildren)+2)
|
|
args = append(args, tagName)
|
|
args = append(args, objectProperties)
|
|
args = append(args, newChildren...)
|
|
|
|
result := tx.Factory().NewCallExpression(
|
|
callee,
|
|
nil,
|
|
nil,
|
|
tx.Factory().NewNodeList(args),
|
|
ast.NodeFlagsNone,
|
|
)
|
|
result.Loc = location.Loc
|
|
|
|
if tx.inJsxChild {
|
|
tx.EmitContext().AddEmitFlags(result, printer.EFStartOnNewLine)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxOpeningFragmentCreateElement(fragment *ast.JsxOpeningFragment, children *ast.NodeList, location *ast.Node) *ast.Node {
|
|
tagName := tx.createJsxFragmentFactoryExpression(fragment.AsNode())
|
|
callee := tx.createJsxFactoryExpression(fragment.AsNode())
|
|
|
|
var newChildren []*ast.Node
|
|
if children != nil && len(children.Nodes) > 0 {
|
|
for _, c := range children.Nodes {
|
|
res := tx.transformJsxChildToExpression(c)
|
|
if res != nil {
|
|
if len(children.Nodes) > 1 {
|
|
tx.EmitContext().AddEmitFlags(res, printer.EFStartOnNewLine)
|
|
}
|
|
newChildren = append(newChildren, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
args := make([]*ast.Expression, 0, len(newChildren)+2)
|
|
args = append(args, tagName)
|
|
args = append(args, tx.Factory().NewKeywordExpression(ast.KindNullKeyword))
|
|
args = append(args, newChildren...)
|
|
|
|
result := tx.Factory().NewCallExpression(
|
|
callee,
|
|
nil,
|
|
nil,
|
|
tx.Factory().NewNodeList(args),
|
|
ast.NodeFlagsNone,
|
|
)
|
|
result.Loc = location.Loc
|
|
|
|
if tx.inJsxChild {
|
|
tx.EmitContext().AddEmitFlags(result, printer.EFStartOnNewLine)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxText(text *ast.JsxText) *ast.Node {
|
|
fixed := fixupWhitespaceAndDecodeEntities(text.Text)
|
|
if len(fixed) == 0 {
|
|
return nil
|
|
}
|
|
return tx.Factory().NewStringLiteral(fixed)
|
|
}
|
|
|
|
func addLineOfJsxText(b *strings.Builder, trimmedLine string, isInitial bool) {
|
|
// We do not escape the string here as that is handled by the printer
|
|
// when it emits the literal. We do, however, need to decode JSX entities.
|
|
decoded := decodeEntities(trimmedLine)
|
|
if !isInitial {
|
|
b.WriteString(" ")
|
|
}
|
|
b.WriteString(decoded)
|
|
}
|
|
|
|
/**
|
|
* JSX trims whitespace at the end and beginning of lines, except that the
|
|
* start/end of a tag is considered a start/end of a line only if that line is
|
|
* on the same line as the closing tag. See examples in
|
|
* tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
|
|
* See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
|
|
*
|
|
* An equivalent algorithm would be:
|
|
* - If there is only one line, return it.
|
|
* - If there is only whitespace (but multiple lines), return `undefined`.
|
|
* - Split the text into lines.
|
|
* - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
|
|
* - Decode entities on each line (individually).
|
|
* - Remove empty lines and join the rest with " ".
|
|
*/
|
|
func fixupWhitespaceAndDecodeEntities(text string) string {
|
|
acc := &strings.Builder{}
|
|
initial := true
|
|
// First non-whitespace character on this line.
|
|
firstNonWhitespace := 0
|
|
// End byte position of the last non-whitespace character on this line.
|
|
lastNonWhitespaceEnd := -1
|
|
// These initial values are special because the first line is:
|
|
// firstNonWhitespace = 0 to indicate that we want leading whitespace,
|
|
// but lastNonWhitespaceEnd = -1 as a special flag to indicate that we *don't* include the line if it's all whitespace.
|
|
for i := 0; i < len(text); i++ {
|
|
c, size := utf8.DecodeRuneInString(text[i:])
|
|
if stringutil.IsLineBreak(c) {
|
|
// If we've seen any non-whitespace characters on this line, add the 'trim' of the line.
|
|
// (lastNonWhitespaceEnd === -1 is a special flag to detect whether the first line is all whitespace.)
|
|
if firstNonWhitespace != -1 && lastNonWhitespaceEnd != -1 {
|
|
addLineOfJsxText(acc, text[firstNonWhitespace:lastNonWhitespaceEnd+1], initial)
|
|
initial = false
|
|
}
|
|
|
|
// Reset firstNonWhitespace for the next line.
|
|
// Don't bother to reset lastNonWhitespaceEnd because we ignore it if firstNonWhitespace = -1.
|
|
firstNonWhitespace = -1
|
|
} else if !stringutil.IsWhiteSpaceSingleLine(c) {
|
|
lastNonWhitespaceEnd = i + size - 1 // Store the end byte position of the character
|
|
if firstNonWhitespace == -1 {
|
|
firstNonWhitespace = i
|
|
}
|
|
}
|
|
|
|
if size > 1 {
|
|
i += (size - 1)
|
|
}
|
|
}
|
|
|
|
if firstNonWhitespace != -1 {
|
|
// Last line had a non-whitespace character. Emit the 'trimLeft', meaning keep trailing whitespace.
|
|
addLineOfJsxText(acc, text[firstNonWhitespace:], initial)
|
|
}
|
|
return acc.String()
|
|
}
|
|
|
|
func (tx *JSXTransformer) visitJsxExpression(expression *ast.JsxExpression) *ast.Node {
|
|
e := tx.Visitor().Visit(expression.Expression)
|
|
if expression.DotDotDotToken != nil {
|
|
return tx.Factory().NewSpreadElement(e)
|
|
}
|
|
return e
|
|
}
|
|
|
|
var htmlEntityMatcher = regexp2.MustCompile(`&((#((\d+)|x([\da-fA-F]+)))|(\w+));`, regexp2.ECMAScript)
|
|
|
|
func htmlEntityReplacer(m regexp2.Match) string {
|
|
decimal := m.GroupByNumber(4)
|
|
if decimal != nil && decimal.Capture.String() != "" {
|
|
parsed, err := strconv.ParseInt(decimal.Capture.String(), 10, 32)
|
|
if err == nil {
|
|
return string(rune(parsed))
|
|
}
|
|
}
|
|
hex := m.GroupByNumber(5)
|
|
if hex != nil && hex.Capture.String() != "" {
|
|
parsed, err := strconv.ParseInt(hex.Capture.String(), 16, 32)
|
|
if err == nil {
|
|
return string(rune(parsed))
|
|
}
|
|
}
|
|
word := m.GroupByNumber(6)
|
|
if word != nil && word.Capture.String() != "" {
|
|
res, ok := entities[word.Capture.String()]
|
|
if ok {
|
|
return string(res)
|
|
}
|
|
}
|
|
return m.String()
|
|
}
|
|
|
|
/**
|
|
* Replace entities like " ", "{", and "�" with the characters they encode.
|
|
* See https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
|
|
*/
|
|
func decodeEntities(text string) string {
|
|
res, err := htmlEntityMatcher.ReplaceFunc(text, htmlEntityReplacer, -1, -1)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
return res
|
|
}
|
|
|
|
var entities = map[string]rune{
|
|
"quot": 0x0022,
|
|
"amp": 0x0026,
|
|
"apos": 0x0027,
|
|
"lt": 0x003C,
|
|
"gt": 0x003E,
|
|
"nbsp": 0x00A0,
|
|
"iexcl": 0x00A1,
|
|
"cent": 0x00A2,
|
|
"pound": 0x00A3,
|
|
"curren": 0x00A4,
|
|
"yen": 0x00A5,
|
|
"brvbar": 0x00A6,
|
|
"sect": 0x00A7,
|
|
"uml": 0x00A8,
|
|
"copy": 0x00A9,
|
|
"ordf": 0x00AA,
|
|
"laquo": 0x00AB,
|
|
"not": 0x00AC,
|
|
"shy": 0x00AD,
|
|
"reg": 0x00AE,
|
|
"macr": 0x00AF,
|
|
"deg": 0x00B0,
|
|
"plusmn": 0x00B1,
|
|
"sup2": 0x00B2,
|
|
"sup3": 0x00B3,
|
|
"acute": 0x00B4,
|
|
"micro": 0x00B5,
|
|
"para": 0x00B6,
|
|
"middot": 0x00B7,
|
|
"cedil": 0x00B8,
|
|
"sup1": 0x00B9,
|
|
"ordm": 0x00BA,
|
|
"raquo": 0x00BB,
|
|
"frac14": 0x00BC,
|
|
"frac12": 0x00BD,
|
|
"frac34": 0x00BE,
|
|
"iquest": 0x00BF,
|
|
"Agrave": 0x00C0,
|
|
"Aacute": 0x00C1,
|
|
"Acirc": 0x00C2,
|
|
"Atilde": 0x00C3,
|
|
"Auml": 0x00C4,
|
|
"Aring": 0x00C5,
|
|
"AElig": 0x00C6,
|
|
"Ccedil": 0x00C7,
|
|
"Egrave": 0x00C8,
|
|
"Eacute": 0x00C9,
|
|
"Ecirc": 0x00CA,
|
|
"Euml": 0x00CB,
|
|
"Igrave": 0x00CC,
|
|
"Iacute": 0x00CD,
|
|
"Icirc": 0x00CE,
|
|
"Iuml": 0x00CF,
|
|
"ETH": 0x00D0,
|
|
"Ntilde": 0x00D1,
|
|
"Ograve": 0x00D2,
|
|
"Oacute": 0x00D3,
|
|
"Ocirc": 0x00D4,
|
|
"Otilde": 0x00D5,
|
|
"Ouml": 0x00D6,
|
|
"times": 0x00D7,
|
|
"Oslash": 0x00D8,
|
|
"Ugrave": 0x00D9,
|
|
"Uacute": 0x00DA,
|
|
"Ucirc": 0x00DB,
|
|
"Uuml": 0x00DC,
|
|
"Yacute": 0x00DD,
|
|
"THORN": 0x00DE,
|
|
"szlig": 0x00DF,
|
|
"agrave": 0x00E0,
|
|
"aacute": 0x00E1,
|
|
"acirc": 0x00E2,
|
|
"atilde": 0x00E3,
|
|
"auml": 0x00E4,
|
|
"aring": 0x00E5,
|
|
"aelig": 0x00E6,
|
|
"ccedil": 0x00E7,
|
|
"egrave": 0x00E8,
|
|
"eacute": 0x00E9,
|
|
"ecirc": 0x00EA,
|
|
"euml": 0x00EB,
|
|
"igrave": 0x00EC,
|
|
"iacute": 0x00ED,
|
|
"icirc": 0x00EE,
|
|
"iuml": 0x00EF,
|
|
"eth": 0x00F0,
|
|
"ntilde": 0x00F1,
|
|
"ograve": 0x00F2,
|
|
"oacute": 0x00F3,
|
|
"ocirc": 0x00F4,
|
|
"otilde": 0x00F5,
|
|
"ouml": 0x00F6,
|
|
"divide": 0x00F7,
|
|
"oslash": 0x00F8,
|
|
"ugrave": 0x00F9,
|
|
"uacute": 0x00FA,
|
|
"ucirc": 0x00FB,
|
|
"uuml": 0x00FC,
|
|
"yacute": 0x00FD,
|
|
"thorn": 0x00FE,
|
|
"yuml": 0x00FF,
|
|
"OElig": 0x0152,
|
|
"oelig": 0x0153,
|
|
"Scaron": 0x0160,
|
|
"scaron": 0x0161,
|
|
"Yuml": 0x0178,
|
|
"fnof": 0x0192,
|
|
"circ": 0x02C6,
|
|
"tilde": 0x02DC,
|
|
"Alpha": 0x0391,
|
|
"Beta": 0x0392,
|
|
"Gamma": 0x0393,
|
|
"Delta": 0x0394,
|
|
"Epsilon": 0x0395,
|
|
"Zeta": 0x0396,
|
|
"Eta": 0x0397,
|
|
"Theta": 0x0398,
|
|
"Iota": 0x0399,
|
|
"Kappa": 0x039A,
|
|
"Lambda": 0x039B,
|
|
"Mu": 0x039C,
|
|
"Nu": 0x039D,
|
|
"Xi": 0x039E,
|
|
"Omicron": 0x039F,
|
|
"Pi": 0x03A0,
|
|
"Rho": 0x03A1,
|
|
"Sigma": 0x03A3,
|
|
"Tau": 0x03A4,
|
|
"Upsilon": 0x03A5,
|
|
"Phi": 0x03A6,
|
|
"Chi": 0x03A7,
|
|
"Psi": 0x03A8,
|
|
"Omega": 0x03A9,
|
|
"alpha": 0x03B1,
|
|
"beta": 0x03B2,
|
|
"gamma": 0x03B3,
|
|
"delta": 0x03B4,
|
|
"epsilon": 0x03B5,
|
|
"zeta": 0x03B6,
|
|
"eta": 0x03B7,
|
|
"theta": 0x03B8,
|
|
"iota": 0x03B9,
|
|
"kappa": 0x03BA,
|
|
"lambda": 0x03BB,
|
|
"mu": 0x03BC,
|
|
"nu": 0x03BD,
|
|
"xi": 0x03BE,
|
|
"omicron": 0x03BF,
|
|
"pi": 0x03C0,
|
|
"rho": 0x03C1,
|
|
"sigmaf": 0x03C2,
|
|
"sigma": 0x03C3,
|
|
"tau": 0x03C4,
|
|
"upsilon": 0x03C5,
|
|
"phi": 0x03C6,
|
|
"chi": 0x03C7,
|
|
"psi": 0x03C8,
|
|
"omega": 0x03C9,
|
|
"thetasym": 0x03D1,
|
|
"upsih": 0x03D2,
|
|
"piv": 0x03D6,
|
|
"ensp": 0x2002,
|
|
"emsp": 0x2003,
|
|
"thinsp": 0x2009,
|
|
"zwnj": 0x200C,
|
|
"zwj": 0x200D,
|
|
"lrm": 0x200E,
|
|
"rlm": 0x200F,
|
|
"ndash": 0x2013,
|
|
"mdash": 0x2014,
|
|
"lsquo": 0x2018,
|
|
"rsquo": 0x2019,
|
|
"sbquo": 0x201A,
|
|
"ldquo": 0x201C,
|
|
"rdquo": 0x201D,
|
|
"bdquo": 0x201E,
|
|
"dagger": 0x2020,
|
|
"Dagger": 0x2021,
|
|
"bull": 0x2022,
|
|
"hellip": 0x2026,
|
|
"permil": 0x2030,
|
|
"prime": 0x2032,
|
|
"Prime": 0x2033,
|
|
"lsaquo": 0x2039,
|
|
"rsaquo": 0x203A,
|
|
"oline": 0x203E,
|
|
"frasl": 0x2044,
|
|
"euro": 0x20AC,
|
|
"image": 0x2111,
|
|
"weierp": 0x2118,
|
|
"real": 0x211C,
|
|
"trade": 0x2122,
|
|
"alefsym": 0x2135,
|
|
"larr": 0x2190,
|
|
"uarr": 0x2191,
|
|
"rarr": 0x2192,
|
|
"darr": 0x2193,
|
|
"harr": 0x2194,
|
|
"crarr": 0x21B5,
|
|
"lArr": 0x21D0,
|
|
"uArr": 0x21D1,
|
|
"rArr": 0x21D2,
|
|
"dArr": 0x21D3,
|
|
"hArr": 0x21D4,
|
|
"forall": 0x2200,
|
|
"part": 0x2202,
|
|
"exist": 0x2203,
|
|
"empty": 0x2205,
|
|
"nabla": 0x2207,
|
|
"isin": 0x2208,
|
|
"notin": 0x2209,
|
|
"ni": 0x220B,
|
|
"prod": 0x220F,
|
|
"sum": 0x2211,
|
|
"minus": 0x2212,
|
|
"lowast": 0x2217,
|
|
"radic": 0x221A,
|
|
"prop": 0x221D,
|
|
"infin": 0x221E,
|
|
"ang": 0x2220,
|
|
"and": 0x2227,
|
|
"or": 0x2228,
|
|
"cap": 0x2229,
|
|
"cup": 0x222A,
|
|
"int": 0x222B,
|
|
"there4": 0x2234,
|
|
"sim": 0x223C,
|
|
"cong": 0x2245,
|
|
"asymp": 0x2248,
|
|
"ne": 0x2260,
|
|
"equiv": 0x2261,
|
|
"le": 0x2264,
|
|
"ge": 0x2265,
|
|
"sub": 0x2282,
|
|
"sup": 0x2283,
|
|
"nsub": 0x2284,
|
|
"sube": 0x2286,
|
|
"supe": 0x2287,
|
|
"oplus": 0x2295,
|
|
"otimes": 0x2297,
|
|
"perp": 0x22A5,
|
|
"sdot": 0x22C5,
|
|
"lceil": 0x2308,
|
|
"rceil": 0x2309,
|
|
"lfloor": 0x230A,
|
|
"rfloor": 0x230B,
|
|
"lang": 0x2329,
|
|
"rang": 0x232A,
|
|
"loz": 0x25CA,
|
|
"spades": 0x2660,
|
|
"clubs": 0x2663,
|
|
"hearts": 0x2665,
|
|
"diams": 0x2666,
|
|
}
|