import pluralize from 'pluralize'
pluralize.addUncountableRule('hertz')
import { IssueError } from '../issues/issues'
import Memoizer from '../utils/memoizer'
/**
* SchemaEntries class
*/
export class SchemaEntries extends Memoizer {
/**
* The schema's properties.
* @type {SchemaEntryManager<SchemaProperty>}
*/
properties
/**
* The schema's attributes.
* @type {SchemaEntryManager<SchemaAttribute>}
*/
attributes
/**
* The schema's value classes.
* @type {SchemaEntryManager<SchemaValueClass>}
*/
valueClasses
/**
* The schema's unit classes.
* @type {SchemaEntryManager<SchemaUnitClass>}
*/
unitClasses
/**
* The schema's unit modifiers.
* @type {SchemaEntryManager<SchemaUnitModifier>}
*/
unitModifiers
/**
* The schema's tags.
* @type {SchemaEntryManager<SchemaTag>}
*/
tags
/**
* Constructor.
* @param {SchemaParser} schemaParser A constructed schema parser.
*/
constructor(schemaParser) {
super()
this.properties = new SchemaEntryManager(schemaParser.properties)
this.attributes = new SchemaEntryManager(schemaParser.attributes)
this.valueClasses = schemaParser.valueClasses
this.unitClasses = schemaParser.unitClasses
this.unitModifiers = schemaParser.unitModifiers
this.tags = schemaParser.tags
}
}
/**
* A manager of {@link SchemaEntry} objects.
*
* @template T
*/
export class SchemaEntryManager extends Memoizer {
/**
* The definitions managed by this entry manager.
* @type {Map<string, T>}
*/
_definitions
/**
* Constructor.
*
* @param {Map<string, T>} definitions A map of schema entry definitions.
*/
constructor(definitions) {
super()
this._definitions = definitions
}
/**
* Iterator over the entry manager's entries.
*
* @template T
* @returns {IterableIterator} - [string, T]
*/
[Symbol.iterator]() {
return this._definitions.entries()
}
/**
* Iterator over the entry manager's keys.
*
* @returns {IterableIterator} - [string]
*/
keys() {
return this._definitions.keys()
}
/**
* Iterator over the entry manager's keys.
*
* @returns {IterableIterator} - [T]
*/
values() {
return this._definitions.values()
}
/**
* Determine whether the entry with the given name exists.
*
* @param {string} name The name of the entry.
* @return {boolean} Whether the entry exists.
*/
hasEntry(name) {
return this._definitions.has(name)
}
/**
* Get the entry with the given name.
*
* @param {string} name - The name of the entry to retrieve.
* @returns {T} - The entry with that name.
*/
getEntry(name) {
return this._definitions.get(name)
}
/**
* Get a collection of entries with the given boolean attribute.
*
* @param {string} booleanAttributeName - The name of boolean attribute to filter on.
* @returns {Map} - string->T representing a collection of entries with that attribute.
*/
getEntriesWithBooleanAttribute(booleanAttributeName) {
return this._memoize(booleanAttributeName, () => {
return this.filter(([, v]) => {
return v.hasBooleanAttribute(booleanAttributeName)
})
})
}
/**
* Filter the map underlying this manager.
*
* @param {function} fn - ([string, T]): boolean specifying the filtering function.
* @returns {Map} - string->T representing a collection of entries with that attribute.
*/
filter(fn) {
return SchemaEntryManager._filterDefinitionMap(this._definitions, fn)
}
/**
* Filter a definition map.
*
* @template T
* @param {Map<string, T>} definitionMap The definition map.
* @param {function} fn - ([string, T]):boolean specifying the filtering function.
* @returns {Map} - string->T representing the filtered definitions.
* @protected
*/
static _filterDefinitionMap(definitionMap, fn) {
const pairArray = Array.from(definitionMap.entries())
return new Map(pairArray.filter((entry) => fn(entry)))
}
/**
* The number of entries in this collection.
*
* @returns {number} The number of entries in this collection.
*/
get length() {
return this._definitions.size
}
}
/**
* SchemaEntry class
*/
export class SchemaEntry extends Memoizer {
/**
* The name of this schema entry.
* @type {string}
*/
_name
constructor(name) {
super()
this._name = name
}
/**
* The name of this schema entry.
* @returns {string}
*/
get name() {
return this._name
}
/**
* Whether this schema entry has this attribute (by name).
*
* This method is a stub to be overridden in {@link SchemaEntryWithAttributes}.
*
* @param {string} attributeName The attribute to check for.
* @returns {boolean} Whether this schema entry has this attribute.
*/
// eslint-disable-next-line no-unused-vars
hasBooleanAttribute(attributeName) {
return false
}
}
// TODO: Switch back to class constant once upstream bug is fixed.
const categoryProperty = 'categoryProperty'
const typeProperty = 'typeProperty'
const roleProperty = 'roleProperty'
/**
* A schema property.
*/
export class SchemaProperty extends SchemaEntry {
/**
* The type of the property.
* @type {string}
*/
_propertyType
constructor(name, propertyType) {
super(name)
this._propertyType = propertyType
}
/**
* Whether this property describes a schema category.
* @returns {boolean}
*/
get isCategoryProperty() {
return this._propertyType === categoryProperty
}
/**
* Whether this property describes a data type.
* @returns {boolean}
*/
get isTypeProperty() {
return this._propertyType === typeProperty
}
/**
* Whether this property describes a role.
* @returns {boolean}
*/
get isRoleProperty() {
return this._propertyType === roleProperty
}
}
// Pseudo-properties
// TODO: Switch back to class constant once upstream bug is fixed.
export const nodeProperty = new SchemaProperty('nodeProperty', categoryProperty)
export const schemaAttributeProperty = new SchemaProperty('schemaAttributeProperty', categoryProperty)
const stringProperty = new SchemaProperty('stringProperty', typeProperty)
/**
* A schema attribute.
*/
export class SchemaAttribute extends SchemaEntry {
/**
* The categories of elements this schema attribute applies to.
* @type {Set<SchemaProperty>}
*/
_categoryProperties
/**
* The data type of this schema attribute.
* @type {SchemaProperty}
*/
_typeProperty
/**
* The set of role properties for this schema attribute.
* @type {Set<SchemaProperty>}
*/
_roleProperties
/**
* Constructor.
*
* @param {string} name The name of the schema attribute.
* @param {SchemaProperty[]} properties The properties assigned to this schema attribute.
*/
constructor(name, properties) {
super(name, new Set(), new Map())
// Parse properties
const categoryProperties = properties.filter((property) => property?.isCategoryProperty)
this._categoryProperties = categoryProperties.length === 0 ? new Set([nodeProperty]) : new Set(categoryProperties)
const typeProperties = properties.filter((property) => property?.isTypeProperty)
this._typeProperty = typeProperties.length === 0 ? stringProperty : typeProperties[0]
this._roleProperties = new Set(properties.filter((property) => property?.isRoleProperty))
}
/**
* The categories of elements this schema attribute applies to.
* @returns {Set<SchemaProperty>|SchemaProperty|undefined}
*/
get categoryProperty() {
switch (this._categoryProperties.size) {
case 0:
return undefined
case 1:
return Array.from(this._categoryProperties)[0]
default:
return this._categoryProperties
}
}
/**
* The data type property of this schema attribute.
* @returns {SchemaProperty}
*/
get typeProperty() {
return this._typeProperty
}
/**
* The set of role properties for this schema attribute.
* @returns {Set<SchemaProperty>}
*/
get roleProperties() {
return new Set(this._roleProperties)
}
}
/**
* SchemaEntryWithAttributes class
*/
export class SchemaEntryWithAttributes extends SchemaEntry {
/**
* The set of boolean attributes this schema entry has.
* @type {Set<SchemaAttribute>}
*/
booleanAttributes
/**
* The collection of value attributes this schema entry has.
* @type {Map<SchemaAttribute, *>}
*/
valueAttributes
/**
* The set of boolean attribute names this schema entry has.
* @type {Set<string>}
*/
booleanAttributeNames
/**
* The collection of value attribute names this schema entry has.
* @type {Map<string, *>}
*/
valueAttributeNames
constructor(name, booleanAttributes, valueAttributes) {
super(name)
this.booleanAttributes = booleanAttributes
this.valueAttributes = valueAttributes
this._parseAttributeNames()
}
/**
* Create aliases of the attribute collections keyed on their names.
*
* @private
*/
_parseAttributeNames() {
this.booleanAttributeNames = new Set()
for (const attribute of this.booleanAttributes) {
this.booleanAttributeNames.add(attribute.name)
}
this.valueAttributeNames = new Map()
for (const [attributeName, value] of this.valueAttributes) {
this.valueAttributeNames.set(attributeName.name, value)
}
}
/**
* Whether this schema entry has this attribute (by name).
* @param {string} attributeName The attribute to check for.
* @returns {boolean} Whether this schema entry has this attribute.
*/
hasAttribute(attributeName) {
return this.booleanAttributeNames.has(attributeName) || this.valueAttributeNames.has(attributeName)
}
/**
* Whether this schema entry has this boolean attribute (by name).
* @param {string} attributeName The attribute to check for.
* @returns {boolean} Whether this schema entry has this attribute.
*/
hasBooleanAttribute(attributeName) {
return this.booleanAttributeNames.has(attributeName)
}
/**
* Retrieve the value of a value attribute (by name) on this schema entry.
* @param {string} attributeName The attribute whose value should be returned.
* @param {boolean} alwaysReturnArray Whether to return a singleton array instead of a scalar value.
* @returns {*} The value of the attribute.
*/
getAttributeValue(attributeName, alwaysReturnArray = false) {
return SchemaEntryWithAttributes._getMapArrayValue(this.valueAttributeNames, attributeName, alwaysReturnArray)
}
/**
* Return a map value, with a scalar being returned in lieu of a singleton array if alwaysReturnArray is false.
*
* @template K,V
* @param {Map<K,V>} map The map to search.
* @param {K} key A key in the map.
* @param {boolean} alwaysReturnArray Whether to return a singleton array instead of a scalar value.
* @returns {V|V[]} The value for the key in the passed map.
* @private
*/
static _getMapArrayValue(map, key, alwaysReturnArray) {
const value = map.get(key)
if (!alwaysReturnArray && Array.isArray(value) && value.length === 1) {
return value[0]
} else {
return value
}
}
}
/**
* SchemaUnit class
*/
export class SchemaUnit extends SchemaEntryWithAttributes {
/**
* The legal derivatives of this unit.
* @type {string[]}
*/
_derivativeUnits
/**
* Constructor.
*
* @param {string} name The name of the unit.
* @param {Set<SchemaAttribute>} booleanAttributes This unit's boolean attributes.
* @param {Map<SchemaAttribute, *>} valueAttributes This unit's key-value attributes.
* @param {SchemaEntryManager<SchemaUnitModifier>} unitModifiers The collection of unit modifiers.
*/
constructor(name, booleanAttributes, valueAttributes, unitModifiers) {
super(name, booleanAttributes, valueAttributes)
this._derivativeUnits = [name]
if (!this.isSIUnit) {
this._pushPluralUnit()
return
}
if (this.isUnitSymbol) {
const SIUnitSymbolModifiers = unitModifiers.getEntriesWithBooleanAttribute('SIUnitSymbolModifier')
for (const modifierName of SIUnitSymbolModifiers.keys()) {
this._derivativeUnits.push(modifierName + name)
}
} else {
const SIUnitModifiers = unitModifiers.getEntriesWithBooleanAttribute('SIUnitModifier')
const pluralUnit = this._pushPluralUnit()
for (const modifierName of SIUnitModifiers.keys()) {
this._derivativeUnits.push(modifierName + name, modifierName + pluralUnit)
}
}
}
_pushPluralUnit() {
if (!this.isUnitSymbol) {
const pluralUnit = pluralize.plural(this._name)
this._derivativeUnits.push(pluralUnit)
return pluralUnit
}
return null
}
*derivativeUnits() {
for (const unit of this._derivativeUnits) {
yield unit
}
}
get isPrefixUnit() {
return this.hasAttribute('unitPrefix')
}
get isSIUnit() {
return this.hasAttribute('SIUnit')
}
get isUnitSymbol() {
return this.hasAttribute('unitSymbol')
}
/**
* Determine if a value has this unit.
*
* @param {string} value -- either the whole value or the part after a blank (if not a prefix unit)
* @returns {boolean} Whether the value has these units.
*/
validateUnit(value) {
if (value == null || value === '') {
return false
}
if (this.isPrefixUnit) {
return value.startsWith(this.name)
}
for (const dUnit of this.derivativeUnits()) {
if (value === dUnit) {
return true
}
}
return false
}
}
/**
* SchemaUnitClass class
*/
export class SchemaUnitClass extends SchemaEntryWithAttributes {
/**
* The units for this unit class.
* @type {Map<string, SchemaUnit>}
*/
_units
/**
* Constructor.
*
* @param {string} name The name of this unit class.
* @param {Set<SchemaAttribute>} booleanAttributes The boolean attributes for this unit class.
* @param {Map<SchemaAttribute, *>} valueAttributes The value attributes for this unit class.
* @param {Map<string, SchemaUnit>} units The units for this unit class.
* @constructor
*/
constructor(name, booleanAttributes, valueAttributes, units) {
super(name, booleanAttributes, valueAttributes)
this._units = units
}
/**
* Get the units for this unit class.
* @returns {Map<string, SchemaUnit>}
*/
get units() {
return new Map(this._units)
}
/**
* Get the default unit for this unit class.
* @returns {SchemaUnit}
*/
get defaultUnit() {
return this._units.get(this.getAttributeValue('defaultUnits'))
}
/**
* Extracts the Unit class and remainder
* @returns {Array} [SchemaUnit, string, string] containing unit class, unit string, and value string
*/
extractUnit(value) {
let actualUnit = null // The Unit class of the value
let actualValueString = null // The actual value part of the value
let actualUnitString = null
let lastPart = null
let firstPart = null
const index = value.indexOf(' ')
if (index !== -1) {
lastPart = value.slice(index + 1)
firstPart = value.slice(0, index)
} else {
// no blank -- there are no units
return [null, null, value]
}
actualValueString = firstPart
actualUnitString = lastPart
for (const unit of this._units.values()) {
if (!unit.isPrefixUnit && unit.validateUnit(lastPart)) {
// Checking if it is non-prefixed unit
actualValueString = firstPart
actualUnitString = lastPart
actualUnit = unit
break
} else if (!unit.isPrefixUnit) {
continue
}
if (unit.validateUnit(firstPart)) {
actualUnit = unit
actualValueString = value.substring(unit.name.length + 1)
actualUnitString = unit.name
break
}
// If it got here, can only be a prefix Unit
}
return [actualUnit, actualUnitString, actualValueString]
}
}
/**
* SchemaUnitModifier class
*/
export class SchemaUnitModifier extends SchemaEntryWithAttributes {
constructor(name, booleanAttributes, valueAttributes) {
super(name, booleanAttributes, valueAttributes)
}
}
/**
* SchemaValueClass class
*/
export class SchemaValueClass extends SchemaEntryWithAttributes {
/**
* The character class-based regular expression.
* @type {RegExp}
* @private
*/
_charClassRegex
/**
* The "word form"-based regular expression.
* @type {RegExp}
* @private
*/
_wordRegex
/**
* Constructor.
*
* @param {string} name The name of this value class.
* @param {Set<SchemaAttribute>} booleanAttributes The boolean attributes for this value class.
* @param {Map<SchemaAttribute, *>} valueAttributes The value attributes for this value class.
* @param {RegExp} charClassRegex The character class-based regular expression for this value class.
* @param {RegExp} wordRegex The "word form"-based regular expression for this value class.
*/
constructor(name, booleanAttributes, valueAttributes, charClassRegex, wordRegex) {
super(name, booleanAttributes, valueAttributes)
this._charClassRegex = charClassRegex
this._wordRegex = wordRegex
}
/**
* Determine if a value is valid according to this value class.
*
* @param {string} value A HED value.
* @returns {boolean} Whether the value conforms to this value class.
*/
validateValue(value) {
return this._wordRegex.test(value) && this._charClassRegex.test(value)
}
}
/**
* A tag in a HED schema.
*/
export class SchemaTag extends SchemaEntryWithAttributes {
/**
* This tag's parent tag.
* @type {SchemaTag}
* @private
*/
_parent
/**
* This tag's unit classes.
* @type {SchemaUnitClass[]}
* @private
*/
_unitClasses
/**
* This tag's value-classes
* @type {SchemaValueClass[]}
* @private
*/
_valueClasses
/**
* This tag's value-taking child.
* @type {SchemaValueTag}
* @private
*/
_valueTag
/**
* Constructor.
*
* @param {string} name The name of this tag.
* @param {Set<SchemaAttribute>} booleanAttributes The boolean attributes for this tag.
* @param {Map<SchemaAttribute, *>} valueAttributes The value attributes for this tag.
* @param {SchemaUnitClass[]} unitClasses The unit classes for this tag.
* @param {SchemaValueClass[]} valueClasses The value classes for this tag.
* @constructor
*/
constructor(name, booleanAttributes, valueAttributes, unitClasses, valueClasses) {
super(name, booleanAttributes, valueAttributes)
this._unitClasses = unitClasses ?? []
this._valueClasses = valueClasses ?? []
}
/**
* This tag's unit classes.
* @type {SchemaUnitClass[]}
*/
get unitClasses() {
return this._unitClasses.slice() // The slice prevents modification
}
/**
* Whether this tag has any unit classes.
* @returns {boolean}
*/
get hasUnitClasses() {
return this._unitClasses.length !== 0
}
/**
* This tag's value classes.
* @type {SchemaValueClass[]}
*/
get valueClasses() {
return this._valueClasses.slice()
}
/**
* This tag's value-taking child tag.
* @returns {SchemaValueTag}
*/
get valueTag() {
return this._valueTag
}
/**
* Set the tag's value-taking child tag.
* @param {SchemaValueTag} newValueTag The new value-taking child tag.
*/
set valueTag(newValueTag) {
if (!this._isPrivateFieldSet(this._valueTag, 'value tag')) {
this._valueTag = newValueTag
}
}
/**
* This tag's parent tag.
* @type {SchemaTag}
*/
get parent() {
return this._parent
}
/**
* Set the tag's parent tag.
* @param {SchemaTag} newParent The new parent tag.
*/
set parent(newParent) {
if (!this._isPrivateFieldSet(this._parent, 'parent')) {
this._parent = newParent
}
}
/**
* Throw an error if a private field is already set.
*
* @param {*} field The field being set.
* @param {string} fieldName The name of the field (for error reporting).
* @return {boolean} Whether the field is set (never returns true).
* @throws {IssueError} If the field is already set.
* @private
*/
_isPrivateFieldSet(field, fieldName) {
if (field !== undefined) {
IssueError.generateAndThrowInternalError(
`Attempted to set ${fieldName} for schema tag ${this.longName} when it already has one.`,
)
}
return false
}
/**
* Return all of this tag's ancestors.
* @returns {Array}
*/
get ancestors() {
return this._memoize('ancestors', () => {
if (this.parent) {
return [this.parent, ...this.parent.ancestors]
}
return []
})
}
/**
* This tag's long name.
* @returns {string}
*/
get longName() {
const nameParts = this.ancestors.map((parentTag) => parentTag.name)
nameParts.reverse().push(this.name)
return nameParts.join('/')
}
/**
* Extend this tag's short name.
*
* @param {string} extension The extension.
* @returns {string} The extended short string.
*/
extend(extension) {
if (extension) {
return this.name + '/' + extension
} else {
return this.name
}
}
/**
* Extend this tag's long name.
*
* @param {string} extension The extension.
* @returns {string} The extended long string.
*/
longExtend(extension) {
if (extension) {
return this.longName + '/' + extension
} else {
return this.longName
}
}
}
/**
* A value-taking tag in a HED schema.
*/
export class SchemaValueTag extends SchemaTag {
/**
* This tag's long name.
* @returns {string}
*/
get longName() {
const nameParts = this.ancestors.map((parentTag) => parentTag.name)
nameParts.reverse().push('#')
return nameParts.join('/')
}
/**
* Extend this tag's short name.
*
* @param {string} extension The extension.
* @returns {string} The extended short string.
*/
extend(extension) {
return this.parent.extend(extension)
}
/**
* Extend this tag's long name.
*
* @param {string} extension The extension.
* @returns {string} The extended long string.
*/
longExtend(extension) {
return this.parent.longExtend(extension)
}
}