import { IssueError } from '../issues/issues'
import { getTagSlashIndices } from '../utils/hedStrings'
import { ReservedChecker } from './reservedChecker'
/**
* Converter from a tag specification to a schema-based tag object.
*/
export default class TagConverter {
/**
* A parsed tag token.
* @type {TagSpec}
*/
tagSpec
/**
* The tag string to convert.
* @type {string}
*/
tagString
/**
* The tag string split by slashes.
* @type {string[]}
*/
tagLevels
/**
* The indices of the tag string's slashes.
* @type {number[]}
*/
tagSlashes
/**
* A HED schema collection.
* @type {Schemas}
*/
hedSchemas
/**
* The entry manager for the tags in the active schema.
* @type {SchemaEntryManager<SchemaTag>}
*/
tagMapping
/**
* The converted tag in the schema.
* @type {SchemaTag}
*/
schemaTag
/**
* The remainder (e.g. value, extension) of the tag string.
* @type {string}
*/
remainder
/**
* Constructor.
*
* @param {TagSpec} tagSpec The tag specification to convert.
* @param {Schemas} hedSchemas The HED schema collection.
*/
constructor(tagSpec, hedSchemas) {
this.hedSchemas = hedSchemas
this.tagMapping = hedSchemas.getSchema(tagSpec.library).entries.tags
this.tagSpec = tagSpec
this.tagString = tagSpec.tag
this.tagLevels = this.tagString.split('/')
this.tagSlashes = getTagSlashIndices(this.tagString)
this.remainder = undefined
this.special = ReservedChecker.getInstance()
}
/**
* Retrieve the {@link SchemaTag} object for a tag specification.
*
* @returns {Array} - [SchemaTag, string] representing schema's corresponding tag object and the remainder of the tag string.
* @throws {IssueError} If tag conversion.
*/
convert() {
let parentTag = undefined
for (let tagLevelIndex = 0; tagLevelIndex < this.tagLevels.length; tagLevelIndex++) {
if (parentTag?.valueTag) {
// It is a value tag
this._setSchemaTag(parentTag.valueTag, tagLevelIndex)
return [this.schemaTag, this.remainder]
}
const childTag = this._validateChildTag(parentTag, tagLevelIndex)
if (childTag === undefined) {
// It is an extended tag and the rest is undefined
this._setSchemaTag(parentTag, tagLevelIndex)
}
parentTag = childTag
}
this._setSchemaTag(parentTag, this.tagLevels.length + 1) // Fix the ending
return [this.schemaTag, this.remainder]
}
_validateChildTag(parentTag, tagLevelIndex) {
const childTag = this._getSchemaTag(tagLevelIndex)
if (childTag === undefined) {
// This is an extended tag
if (tagLevelIndex === 0) {
// Top level tags can't be extensions
IssueError.generateAndThrow('invalidTag', { tag: this.tagString })
}
if (
parentTag !== undefined &&
(!parentTag.hasAttribute('extensionAllowed') || this.special.noExtensionTags.has(parentTag.name))
) {
IssueError.generateAndThrow('invalidExtension', {
tag: this.tagLevels[tagLevelIndex],
parentTag: this.tagLevels.slice(0, tagLevelIndex).join('/'),
})
}
this._checkExtensions(tagLevelIndex)
return childTag
}
if (tagLevelIndex > 0 && (childTag.parent === undefined || childTag.parent !== parentTag)) {
IssueError.generateAndThrow('invalidParentNode', {
tag: this.tagLevels[tagLevelIndex],
parentTag: this.tagLevels.slice(0, tagLevelIndex).join('/'),
})
}
return childTag
}
_checkExtensions(tagLevelIndex) {
// A non-tag has been detected --- from here on must be non-tags.
this._checkNameClass(tagLevelIndex) // This is an extension
for (let index = tagLevelIndex + 1; index < this.tagLevels.length; index++) {
const child = this._getSchemaTag(index)
if (child !== undefined) {
// A schema tag showed up after a non-schema tag
IssueError.generateAndThrow('invalidParentNode', {
tag: child.name,
parentTag: this.tagLevels.slice(0, index).join('/'),
})
}
this._checkNameClass(index)
}
}
_getSchemaTag(tagLevelIndex) {
const tagLevel = this.tagLevels[tagLevelIndex].toLowerCase()
return this.tagMapping.getEntry(tagLevel)
}
_setSchemaTag(schemaTag, remainderStartLevelIndex) {
if (this.schemaTag !== undefined) {
return
}
this.schemaTag = schemaTag
this.remainder = this.tagLevels.slice(remainderStartLevelIndex).join('/')
if (this.schemaTag?.hasAttribute('requireChild') && !this.remainder) {
IssueError.generateAndThrow('childRequired', { tag: this.tagString })
}
}
_checkNameClass(index) {
// Check whether the tagLevel is a valid name class
const valueClasses = this.hedSchemas.getSchema(this.tagSpec.library).entries.valueClasses
if (!valueClasses._definitions.get('nameClass').validateValue(this.tagLevels[index])) {
IssueError.generateAndThrow('invalidExtension', {
tag: this.tagLevels[index],
parentTag: this.tagLevels.slice(0, index).join('/'),
})
}
}
}