Skip to content

Commit

Permalink
Merge pull request #1484 from mischnic/ast-json
Browse files Browse the repository at this point in the history
fromJSON and toJSON
  • Loading branch information
ai committed Dec 5, 2020
2 parents 6b55ab9 + ab24158 commit 64971b0
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 30 deletions.
5 changes: 5 additions & 0 deletions lib/fromJSON.d.ts
@@ -0,0 +1,5 @@
import { JSONHydrator } from './postcss.js'

declare const fromJSON: JSONHydrator

export default fromJSON
47 changes: 47 additions & 0 deletions lib/fromJSON.js
@@ -0,0 +1,47 @@
'use strict'

let Declaration = require('./declaration')
let Comment = require('./comment')
let AtRule = require('./at-rule')
let Root = require('./root')
let Rule = require('./rule')
let Input = require('./input')
let PreviousMap = require('./previous-map')

function fromJSON (json) {
let defaults = { ...json }
if (json.nodes) {
defaults.nodes = json.nodes.map(n => fromJSON(n))
}
if (json.type === 'root') {
if (defaults.source) {
defaults.source = { ...defaults.source }
if (defaults.source.input) {
defaults.source.input = {
...defaults.source.input,
__proto__: Input.prototype
}
if (defaults.source.input.map) {
defaults.source.input.map = {
...defaults.source.input.map,
__proto__: PreviousMap.prototype
}
}
}
}
return new Root(defaults)
} else if (json.type === 'decl') {
return new Declaration(defaults)
} else if (json.type === 'rule') {
return new Rule(defaults)
} else if (json.type === 'comment') {
return new Comment(defaults)
} else if (json.type === 'atrule') {
return new AtRule(defaults)
} else {
throw new Error('Unknown node type: ' + json.type)
}
}

module.exports = fromJSON
fromJSON.default = fromJSON
84 changes: 54 additions & 30 deletions lib/input.js
Expand Up @@ -8,6 +8,8 @@ let terminalHighlight = require('./terminal-highlight')
let CssSyntaxError = require('./css-syntax-error')
let PreviousMap = require('./previous-map')

let fromOffsetCache = Symbol('Input fromOffset cache')

class Input {
constructor (css, opts = {}) {
if (
Expand Down Expand Up @@ -49,42 +51,48 @@ class Input {
}

fromOffset (offset) {
let lines = this.css.split('\n')
let lineToIndex = new Array(lines.length)
let prevIndex = 0
let lastLine, lineToIndex
if (this[fromOffsetCache] == null) {
let lines = this.css.split('\n')
lineToIndex = new Array(lines.length)
let prevIndex = 0

for (let i = 0, l = lines.length; i < l; i++) {
lineToIndex[i] = prevIndex
prevIndex += lines[i].length + 1
}

for (let i = 0, l = lines.length; i < l; i++) {
lineToIndex[i] = prevIndex
prevIndex += lines[i].length + 1
lastLine = lineToIndex[lineToIndex.length - 1]
this[fromOffsetCache] = {
lastLine,
lineToIndex
}
} else {
;({ lastLine, lineToIndex } = this[fromOffsetCache])
}

let lastLine = lineToIndex[lineToIndex.length - 1]

this.fromOffset = index => {
let min = 0
if (index >= lastLine) {
min = lineToIndex.length - 1
} else {
let max = lineToIndex.length - 2
let mid
while (min < max) {
mid = min + ((max - min) >> 1)
if (index < lineToIndex[mid]) {
max = mid - 1
} else if (index >= lineToIndex[mid + 1]) {
min = mid + 1
} else {
min = mid
break
}
let min = 0
if (offset >= lastLine) {
min = lineToIndex.length - 1
} else {
let max = lineToIndex.length - 2
let mid
while (min < max) {
mid = min + ((max - min) >> 1)
if (offset < lineToIndex[mid]) {
max = mid - 1
} else if (offset >= lineToIndex[mid + 1]) {
min = mid + 1
} else {
min = mid
break
}
}
return {
line: min + 1,
col: index - lineToIndex[min] + 1
}
}
return this.fromOffset(offset)
return {
line: min + 1,
col: offset - lineToIndex[min] + 1
}
}

error (message, line, column, opts = {}) {
Expand Down Expand Up @@ -168,6 +176,22 @@ class Input {
get from () {
return this.file || this.id
}

toJSON () {
let json = {}
for (let name of ['hasBOM', 'css', 'file', 'id']) {
if (this[name] != null) {
json[name] = this[name]
}
}
if (this.map) {
json.map = { ...this.map }
if (json.map.consumerCache) {
json.map.consumerCache = undefined
}
}
return json
}
}

module.exports = Input
Expand Down
5 changes: 5 additions & 0 deletions lib/node.js
Expand Up @@ -187,6 +187,11 @@ class Node {
})
} else if (typeof value === 'object' && value.toJSON) {
fixed[name] = value.toJSON()
} else if (this.type === 'root' && name === 'source') {
fixed[name] = {
input: value.input.toJSON(),
start: value.start
}
} else {
fixed[name] = value
}
Expand Down
16 changes: 16 additions & 0 deletions lib/postcss.d.ts
Expand Up @@ -213,6 +213,10 @@ export interface Stringifier {
(node: AnyNode, builder: Builder): void
}

export interface JSONHydrator {
(data: object): Node
}

export interface Syntax {
/**
* Function to generate AST by string.
Expand Down Expand Up @@ -352,6 +356,17 @@ export interface Postcss {
*/
parse: Parser

/**
* Rehydrate a JSON AST (from Node#toJSON) back into the corresponding node.
*
* ```js
* const json = root.toJSON();
* // ...
* const rehydrated = postcss.fromJSON(json);
* ```
*/
fromJSON: JSONHydrator

/**
* Contains the `list` module.
*/
Expand Down Expand Up @@ -412,6 +427,7 @@ export interface Postcss {

export const stringify: Stringifier
export const parse: Parser
export const fromJSON: JSONHydrator

export const comment: Postcss['comment']
export const atRule: Postcss['atRule']
Expand Down
2 changes: 2 additions & 0 deletions lib/postcss.js
Expand Up @@ -6,6 +6,7 @@ let LazyResult = require('./lazy-result')
let Container = require('./container')
let Processor = require('./processor')
let stringify = require('./stringify')
let fromJSON = require('./fromJSON')
let Warning = require('./warning')
let Comment = require('./comment')
let AtRule = require('./at-rule')
Expand Down Expand Up @@ -62,6 +63,7 @@ postcss.plugin = function plugin (name, initializer) {

postcss.stringify = stringify
postcss.parse = parse
postcss.fromJSON = fromJSON
postcss.list = list

postcss.comment = defaults => new Comment(defaults)
Expand Down
1 change: 1 addition & 0 deletions lib/postcss.mjs
Expand Up @@ -5,6 +5,7 @@ export default postcss
export const stringify = postcss.stringify
export const plugin = postcss.plugin
export const parse = postcss.parse
export const fromJSON = postcss.fromJSON
export const list = postcss.list

export const comment = postcss.comment
Expand Down
33 changes: 33 additions & 0 deletions test/fromJSON.test.ts
@@ -0,0 +1,33 @@
import v8 from 'v8'

import postcss, { Root } from '../lib/postcss.js'

it('rehydrates a JSON AST', () => {
let cssWithMap = postcss().process(
'.foo { color: red; font-size: 12pt; } /* abc */ @media (width: 60em) { }',
{
from: undefined,
map: {
inline: true
},
stringifier: postcss.stringify
}
).css

let root = postcss.parse(cssWithMap)

let json = root.toJSON()
let serialized = v8.serialize(json)
let deserialized = v8.deserialize(serialized)
let rehydrated = postcss.fromJSON(deserialized) as Root

rehydrated.nodes[0].remove()

expect(rehydrated.nodes).toHaveLength(3)
})

it('throws when rehydrating an invalid JSON AST', () => {
expect(() => {
postcss.fromJSON({ type: 'not-a-node-type' })
}).toThrow('Unknown node type: not-a-node-type')
})

0 comments on commit 64971b0

Please sign in to comment.