import React from 'react'
import includes from 'lodash/includes'
import isReactComponent from '../utils/is-react-component'
import typeOf from 'type-of'
import memoize from '../utils/memoize'
import { Record } from 'immutable'
/**
* Checks that the schema can perform, ordered by performance.
*
* @type {Object}
*/
const CHECKS = {
kind(object, value) {
if (object.kind != value) return object.kind
},
type(object, value) {
if (object.type != value) return object.type
},
isVoid(object, value) {
if (object.isVoid != value) return object.isVoid
},
minChildren(object, value) {
if (object.nodes.size < value) return object.nodes.size
},
maxChildren(object, value) {
if (object.nodes.size > value) return object.nodes.size
},
kinds(object, value) {
if (!includes(value, object.kind)) return object.kind
},
types(object, value) {
if (!includes(value, object.type)) return object.type
},
minLength(object, value) {
const { length } = object
if (length < value) return length
},
maxLength(object, value) {
const { length } = object
if (length > value) return length
},
text(object, value) {
const { text } = object
switch (typeOf(value)) {
case 'function': if (value(text)) return text
case 'regexp': if (!text.match(value)) return text
default: if (text != value) return text
}
},
anyOf(object, value) {
const { nodes } = object
if (!nodes) return
const invalids = nodes.filterNot((child) => {
return value.some(match => match(child))
})
if (invalids.size) return invalids
},
noneOf(object, value) {
const { nodes } = object
if (!nodes) return
const invalids = nodes.filterNot((child) => {
return value.every(match => !match(child))
})
if (invalids.size) return invalids
},
exactlyOf(object, value) {
const { nodes } = object
if (!nodes) return
if (nodes.size != value.length) return nodes
const invalids = nodes.filterNot((child, i) => {
const match = value[i]
if (!match) return false
return match(child)
})
if (invalids.size) return invalids
},
}
/**
* Default properties.
*
* @type {Object}
*/
const DEFAULTS = {
rules: [],
}
/**
* Schema.
*
* @type {Record}
*/
class Schema extends new Record(DEFAULTS) {
/**
* Create a new `Schema` with `properties`.
*
* @param {Object} properties
* @return {Schema} mark
*/
static create(properties = {}) {
if (properties instanceof Schema) return properties
return new Schema(normalizeProperties(properties))
}
/**
* Get the kind.
*
* @return {String} kind
*/
get kind() {
return 'schema'
}
/**
* Return the renderer for an `object`.
*
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Component || Void}
*/
__getComponent(object) {
const match = this.rules.find(rule => rule.render && rule.match(object))
if (!match) return
return match.render
}
/**
* Return the decorators for an `object`.
*
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Array}
*/
__getDecorators(object) {
return this.rules
.filter(rule => rule.decorate && rule.match(object))
.map((rule) => {
return (text) => {
return rule.decorate(text, object)
}
})
}
/**
* Validate an `object` against the schema, returning the failing rule and
* value if the object is invalid, or void if it's valid.
*
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Object || Void}
*/
__validate(object) {
let value
const match = this.rules.find((rule) => {
if (!rule.validate) return
if (!rule.match(object)) return
value = rule.validate(object)
return value
})
if (!value) return
return {
rule: match,
value,
}
}
}
/**
* Normalize the `properties` of a schema.
*
* @param {Object} properties
* @return {Object}
*/
function normalizeProperties(properties) {
let { rules = [], nodes, marks } = properties
if (nodes) {
const array = normalizeNodes(nodes)
rules = rules.concat(array)
}
if (marks) {
const array = normalizeMarks(marks)
rules = rules.concat(array)
}
return { rules }
}
/**
* Normalize a `nodes` shorthand argument.
*
* @param {Object} nodes
* @return {Array}
*/
function normalizeNodes(nodes) {
const rules = []
for (const key in nodes) {
let rule = nodes[key]
if (typeOf(rule) == 'function' || isReactComponent(rule)) {
rule = { render: rule }
}
rule.match = (object) => {
return (
(object.kind == 'block' || object.kind == 'inline') &&
object.type == key
)
}
rules.push(rule)
}
return rules
}
/**
* Normalize a `marks` shorthand argument.
*
* @param {Object} marks
* @return {Array}
*/
function normalizeMarks(marks) {
const rules = []
for (const key in marks) {
let rule = marks[key]
if (!rule.render && !rule.decorator && !rule.validate) {
rule = { render: rule }
}
rule.render = normalizeMarkComponent(rule.render)
rule.match = object => object.kind == 'mark' && object.type == key
rules.push(rule)
}
return rules
}
/**
* Normalize a mark `render` property.
*
* @param {Component || Function || Object || String} render
* @return {Component}
*/
function normalizeMarkComponent(render) {
if (isReactComponent(render)) return render
switch (typeOf(render)) {
case 'function':
return render
case 'object':
return props => {props.children}
case 'string':
return props => {props.children}
}
}
/**
* Export.
*
* @type {Record}
*/
export default Schema