223 lines
6.0 KiB
JavaScript
223 lines
6.0 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const semver = require('semver')
|
||
|
|
||
|
const pkg = require('../package.json')
|
||
|
const constants = require('./constants')
|
||
|
const object = require('./complexValues/object')
|
||
|
const lineBuilder = require('./lineBuilder')
|
||
|
const formatUtils = require('./formatUtils')
|
||
|
const itemDescriptor = require('./metaDescriptors/item')
|
||
|
const propertyDescriptor = require('./metaDescriptors/property')
|
||
|
const stringDescriptor = require('./primitiveValues/string')
|
||
|
const recursorUtils = require('./recursorUtils')
|
||
|
const themeUtils = require('./themeUtils')
|
||
|
|
||
|
const API_VERSION = 1
|
||
|
const CONCORDANCE_VERSION = pkg.version
|
||
|
|
||
|
const descriptorRegistry = new Map()
|
||
|
const registry = new Map()
|
||
|
|
||
|
class PluginError extends Error {
|
||
|
constructor (message, plugin) {
|
||
|
super(message)
|
||
|
this.name = 'PluginError'
|
||
|
this.plugin = plugin
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class PluginTypeError extends TypeError {
|
||
|
constructor (message, plugin) {
|
||
|
super(message)
|
||
|
this.name = 'PluginTypeError'
|
||
|
this.plugin = plugin
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class UnsupportedApiError extends PluginError {
|
||
|
constructor (plugin) {
|
||
|
super('Plugin requires an unsupported API version', plugin)
|
||
|
this.name = 'UnsupportedApiError'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class UnsupportedError extends PluginError {
|
||
|
constructor (plugin) {
|
||
|
super('Plugin does not support this version of Concordance', plugin)
|
||
|
this.name = 'UnsupportedError'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DuplicateDescriptorTagError extends PluginError {
|
||
|
constructor (tag, plugin) {
|
||
|
super(`Could not add descriptor: tag ${String(tag)} has already been registered`, plugin)
|
||
|
this.name = 'DuplicateDescriptorTagError'
|
||
|
this.tag = tag
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DuplicateDescriptorIdError extends PluginError {
|
||
|
constructor (id, plugin) {
|
||
|
const printed = typeof id === 'number'
|
||
|
? `0x${id.toString(16).toUpperCase()}`
|
||
|
: String(id)
|
||
|
super(`Could not add descriptor: id ${printed} has already been registered`, plugin)
|
||
|
this.name = 'DuplicateDescriptorIdError'
|
||
|
this.id = id
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function verify (plugin) {
|
||
|
if (typeof plugin.name !== 'string' || !plugin.name) {
|
||
|
throw new PluginTypeError('Plugin must have a `name`', plugin)
|
||
|
}
|
||
|
|
||
|
if (plugin.apiVersion !== API_VERSION) {
|
||
|
throw new UnsupportedApiError(plugin)
|
||
|
}
|
||
|
|
||
|
if ('minimalConcordanceVersion' in plugin) {
|
||
|
if (!semver.valid(plugin.minimalConcordanceVersion)) {
|
||
|
throw new PluginTypeError('If specified, `minimalConcordanceVersion` must be a valid SemVer version', plugin)
|
||
|
}
|
||
|
|
||
|
const range = `>=${plugin.minimalConcordanceVersion}`
|
||
|
if (!semver.satisfies(CONCORDANCE_VERSION, range)) {
|
||
|
throw new UnsupportedError(plugin)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Selectively expose descriptor tags.
|
||
|
const publicDescriptorTags = Object.freeze({
|
||
|
complexItem: itemDescriptor.complexTag,
|
||
|
primitiveItem: itemDescriptor.primitiveTag,
|
||
|
primitiveProperty: propertyDescriptor.primitiveTag,
|
||
|
string: stringDescriptor.tag
|
||
|
})
|
||
|
|
||
|
// Don't expose `setDefaultGutter()`.
|
||
|
const publicLineBuilder = Object.freeze({
|
||
|
buffer: lineBuilder.buffer,
|
||
|
first: lineBuilder.first,
|
||
|
last: lineBuilder.last,
|
||
|
line: lineBuilder.line,
|
||
|
single: lineBuilder.single,
|
||
|
actual: Object.freeze({
|
||
|
buffer: lineBuilder.actual.buffer,
|
||
|
first: lineBuilder.actual.first,
|
||
|
last: lineBuilder.actual.last,
|
||
|
line: lineBuilder.actual.line,
|
||
|
single: lineBuilder.actual.single
|
||
|
}),
|
||
|
expected: Object.freeze({
|
||
|
buffer: lineBuilder.expected.buffer,
|
||
|
first: lineBuilder.expected.first,
|
||
|
last: lineBuilder.expected.last,
|
||
|
line: lineBuilder.expected.line,
|
||
|
single: lineBuilder.expected.single
|
||
|
})
|
||
|
})
|
||
|
|
||
|
function modifyTheme (descriptor, modifier) {
|
||
|
themeUtils.addModifier(descriptor, modifier)
|
||
|
return descriptor
|
||
|
}
|
||
|
|
||
|
function add (plugin) {
|
||
|
verify(plugin)
|
||
|
|
||
|
const name = plugin.name
|
||
|
if (registry.has(name)) return registry.get(name)
|
||
|
|
||
|
const id2deserialize = new Map()
|
||
|
const tag2id = new Map()
|
||
|
const addDescriptor = (id, tag, deserialize) => {
|
||
|
if (id2deserialize.has(id)) throw new DuplicateDescriptorIdError(id, plugin)
|
||
|
if (descriptorRegistry.has(tag) || tag2id.has(tag)) throw new DuplicateDescriptorTagError(tag, plugin)
|
||
|
|
||
|
id2deserialize.set(id, deserialize)
|
||
|
tag2id.set(tag, id)
|
||
|
}
|
||
|
|
||
|
const tryDescribeValue = plugin.register({
|
||
|
// Concordance makes assumptions about when AMBIGUOUS occurs. Do not expose
|
||
|
// it to plugins.
|
||
|
UNEQUAL: constants.UNEQUAL,
|
||
|
SHALLOW_EQUAL: constants.SHALLOW_EQUAL,
|
||
|
DEEP_EQUAL: constants.DEEP_EQUAL,
|
||
|
|
||
|
ObjectValue: object.ObjectValue,
|
||
|
DescribedMixin: object.DescribedMixin,
|
||
|
DeserializedMixin: object.DeserializedMixin,
|
||
|
|
||
|
addDescriptor,
|
||
|
applyThemeModifiers: themeUtils.applyModifiers,
|
||
|
descriptorTags: publicDescriptorTags,
|
||
|
lineBuilder: publicLineBuilder,
|
||
|
mapRecursor: recursorUtils.map,
|
||
|
modifyTheme,
|
||
|
wrapFromTheme: formatUtils.wrap
|
||
|
})
|
||
|
|
||
|
const registered = {
|
||
|
id2deserialize,
|
||
|
serializerVersion: plugin.serializerVersion,
|
||
|
name,
|
||
|
tag2id,
|
||
|
theme: plugin.theme || {},
|
||
|
tryDescribeValue
|
||
|
}
|
||
|
|
||
|
registry.set(name, registered)
|
||
|
for (const tag of tag2id.keys()) {
|
||
|
descriptorRegistry.set(tag, registered)
|
||
|
}
|
||
|
|
||
|
return registered
|
||
|
}
|
||
|
exports.add = add
|
||
|
|
||
|
function getDeserializers (plugins) {
|
||
|
return plugins.map(plugin => {
|
||
|
const registered = add(plugin)
|
||
|
return {
|
||
|
id2deserialize: registered.id2deserialize,
|
||
|
name: registered.name,
|
||
|
serializerVersion: registered.serializerVersion
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
exports.getDeserializers = getDeserializers
|
||
|
|
||
|
function getThemes (plugins) {
|
||
|
return plugins.map(plugin => {
|
||
|
const registered = add(plugin)
|
||
|
return {
|
||
|
name: registered.name,
|
||
|
theme: registered.theme
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
exports.getThemes = getThemes
|
||
|
|
||
|
function getTryDescribeValues (plugins) {
|
||
|
return plugins.map(plugin => add(plugin).tryDescribeValue)
|
||
|
}
|
||
|
exports.getTryDescribeValues = getTryDescribeValues
|
||
|
|
||
|
function resolveDescriptorRef (tag) {
|
||
|
if (!descriptorRegistry.has(tag)) return null
|
||
|
|
||
|
const registered = descriptorRegistry.get(tag)
|
||
|
return {
|
||
|
id: registered.tag2id.get(tag),
|
||
|
name: registered.name,
|
||
|
serialization: {
|
||
|
serializerVersion: registered.serializerVersion
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.resolveDescriptorRef = resolveDescriptorRef
|