Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Considerably faster than [Bublé](https://gitlab.com/Rich-Harris/buble) (up to 5×), [Escodegen](https://github.com/estools/escodegen) (up to 10×), [Babel](https://github.com/babel/babel) (up to 50×), [UglifyJS](https://github.com/mishoo/UglifyJS2) (up to 125×), and [Prettier](https://github.com/prettier/prettier) (up to 380×).
- Supports source map generation with [Source Map](https://github.com/mozilla/source-map#sourcemapgenerator).
- Supports comment generation with [Astravel](https://github.com/davidbonnet/astravel).
- Optionally supports JSX.
- No dependencies and small footprint (≈ 16 KB minified, ≈ 4 KB gziped).

Checkout the [live demo](http://david.bonnet.cc/astring/demo/) showing Astring in action.
Expand Down Expand Up @@ -109,6 +110,11 @@ The `options` are:

Base generator that can be used to [extend Astring](#extending).

### `JSX: object`

Handlers for JSX nodes. Can be combined with `GENERATOR` and/or your own
generators to support JSX.

### `EXPRESSIONS_PRECEDENCE: object`

Mapping of node types and their precedence level to let the generator know when to use parentheses.
Expand Down Expand Up @@ -189,6 +195,27 @@ var formattedCode = generate(ast, {
console.log(map.toString())
```

### Supporting JSX

```javascript
import { Parser } from 'acorn'
import acornJsx from 'acorn-jsx'
import { generate, GENERATOR, JSX } from './dist/astring.js'

// Parse with acorn:
const acorn = Parser.extend(acornJsx())

var ast = acorn.parse('console.log(<h1>Hello, world!</h1>)', {
ecmaVersion: 6,
sourceType: 'module',
})

// Serialize with astring:
var code = generate(ast, { generator: { ...GENERATOR, ...JSX } })

console.log(code)
```

### Using writable streams

This example for [Node](http://nodejs.org) shows how to use writable streams to get the rendered code.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "./dist/astring.js",
"module": "./src/astring.js",
"types": "./astring.d.ts",
"sideEffects": false,
"bin": {
"astring": "bin/astring"
},
Expand Down
136 changes: 136 additions & 0 deletions src/astring.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@ function formatVariableDeclaration(state, node) {
}
}

// Make sure that character references don’t pop up.
// For example, the text `&copy;` should stay that way, and not turn into `©`.
// We could encode all `&` (easy but verbose) or look for actual valid
// references (complex but cleanest output).
// Looking for the 2nd character gives us a middle ground.
// The `#` is for (decimal and hexadecimal) numeric references, the letters
// are for the named references.
function encodeJsx(value) {
return value.replace(/&(?=[#a-z])/gi, '&amp;')
}

let ForInStatement,
FunctionDeclaration,
RestElement,
Expand Down Expand Up @@ -992,6 +1003,131 @@ export const GENERATOR = {
},
}

export const JSX = {
// `attr`
// `attr="something"`
// `attr={1}`
JSXAttribute(node, state) {
this[node.name.type](node.name, state)

if (node.value !== undefined && node.value !== null) {
state.write('=')

// Encode double quotes in attribute values.
if (node.value.type === 'Literal') {
state.write(
'"' +
encodeJsx(String(node.value.value)).replace(/"/g, '&quot;') +
'"',
node,
)
} else {
this[node.value.type](node.value, state)
}
}
},
// `</div>`
JSXClosingElement(node, state) {
state.write('</')
this[node.name.type](node.name, state)
state.write('>')
},
// `</>`
JSXClosingFragment(node, state) {
state.write('</>', node)
},
// `<div />`
// `<div></div>`
JSXElement(node, state) {
let index = -1

this[node.openingElement.type](node.openingElement, state)

if (node.children) {
while (++index < node.children.length) {
this[node.children[index].type](node.children[index], state)
}
}

if (node.closingElement) {
this[node.closingElement.type](node.closingElement, state)
}
},
// `{}` (always in a `JSXExpressionContainer`, which does the curlies)
JSXEmptyExpression() {},
// `{expression}`
JSXExpressionContainer(node, state) {
state.write('{')
this[node.expression.type](node.expression, state)
state.write('}')
},
// `<></>`
JSXFragment(node, state) {
let index = -1

this[node.openingFragment.type](node.openingElement, state)

if (node.children) {
while (++index < node.children.length) {
this[node.children[index].type](node.children[index], state)
}
}

this[node.closingFragment.type](node.closingElement, state)
},
// `div`
JSXIdentifier(node, state) {
state.write(node.name, node)
},
// `member.expression`
JSXMemberExpression(node, state) {
this[node.object.type](node.object, state)
state.write('.')
this[node.property.type](node.property, state)
},
// `ns:name`
JSXNamespacedName(node, state) {
this[node.namespace.type](node.namespace, state)
state.write(':')
this[node.name.type](node.name, state)
},
// `<div>`
JSXOpeningElement(node, state) {
let index = -1

state.write('<')
this[node.name.type](node.name, state)

if (node.attributes) {
while (++index < node.attributes.length) {
state.write(' ')
this[node.attributes[index].type](node.attributes[index], state)
}
}

state.write(node.selfClosing ? ' />' : '>')
},
// `<>`
JSXOpeningFragment(node, state) {
state.write('<>', node)
},
// `{...argument}`
JSXSpreadAttribute(node, state) {
state.write('{')
this.SpreadElement(node, state)
state.write('}')
},
// `!`
JSXText(node, state) {
state.write(
encodeJsx(node.value).replace(/<|\{/g, function ($0) {
return $0 === '<' ? '&lt;' : '&#123;'
}),
node,
)
},
}

const EMPTY_OBJECT = {}

/*
Expand Down
2 changes: 2 additions & 0 deletions src/tests/astring.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { pick } from 'lodash'
import { generate } from '../astring'
import { readFile } from './tools'

import './jsx'

const FIXTURES_FOLDER = path.join(__dirname, 'fixtures')

const ecmaVersion = 12
Expand Down
Loading