Source: parser/parsedHedTag.js

  1. import { IssueError } from '../issues/issues'
  2. import ParsedHedSubstring from './parsedHedSubstring'
  3. import { SchemaValueTag } from '../schema/entries'
  4. import TagConverter from './tagConverter'
  5. import { ReservedChecker } from './reservedChecker'
  6. const TWO_LEVEL_TAGS = new Set(['Definition', 'Def', 'Def-expand'])
  7. const allowedRegEx = /^[^{},]*$/
  8. /**
  9. * A parsed HED tag.
  10. */
  11. export default class ParsedHedTag extends ParsedHedSubstring {
  12. /**
  13. * The formatted canonical version of the HED tag.
  14. * @type {string}
  15. */
  16. formattedTag
  17. /**
  18. * The canonical form of the HED tag.
  19. * @type {string}
  20. */
  21. canonicalTag
  22. /**
  23. * The HED schema this tag belongs to.
  24. * @type {Schema}
  25. */
  26. schema
  27. /**
  28. * The schema's representation of this tag.
  29. *
  30. * @type {SchemaTag}
  31. * @private
  32. */
  33. _schemaTag
  34. /**
  35. * The remaining part of the tag after the portion actually in the schema.
  36. * @type {string}
  37. * @private
  38. */
  39. _remainder
  40. /**
  41. * The value of the tag, if any.
  42. * @type {string}
  43. * @private
  44. */
  45. _value
  46. /**
  47. * If definition, this is the second value, otherwise empty string.
  48. * @type {string}
  49. * @private
  50. */
  51. _splitValue
  52. /**
  53. * The units if any.
  54. * @type {string}
  55. * @private
  56. */
  57. _units
  58. /**
  59. * Constructor.
  60. *
  61. * @param {TagSpec} tagSpec The token for this tag.
  62. * @param {Schemas} hedSchemas The collection of HED schemas.
  63. * @param {string} hedString The original HED string.
  64. * @throws {IssueError} If tag conversion or parsing fails.
  65. */
  66. constructor(tagSpec, hedSchemas, hedString) {
  67. super(tagSpec.tag, tagSpec.bounds) // Sets originalTag and originalBounds
  68. this._convertTag(hedSchemas, hedString, tagSpec)
  69. this._normalized = this.format(false) // Sets various forms of the tag.
  70. }
  71. /**
  72. * Convert this tag to its various forms
  73. *
  74. * @param {Schemas} hedSchemas The collection of HED schemas.
  75. * @param {string} hedString The original HED string.
  76. * @param {TagSpec} tagSpec The token for this tag.
  77. * @throws {IssueError} If tag conversion or parsing fails.
  78. */
  79. _convertTag(hedSchemas, hedString, tagSpec) {
  80. const schemaName = tagSpec.library
  81. this.schema = hedSchemas.getSchema(schemaName)
  82. if (this.schema === undefined) {
  83. if (schemaName !== '') {
  84. IssueError.generateAndThrow('unmatchedLibrarySchema', {
  85. tag: this.originalTag,
  86. library: schemaName,
  87. })
  88. } else {
  89. IssueError.generateAndThrow('unmatchedBaseSchema', {
  90. tag: this.originalTag,
  91. })
  92. }
  93. }
  94. const [schemaTag, remainder] = new TagConverter(tagSpec, hedSchemas).convert()
  95. this._schemaTag = schemaTag
  96. this._remainder = remainder
  97. this.canonicalTag = this._schemaTag.longExtend(remainder)
  98. this.formattedTag = this.canonicalTag.toLowerCase()
  99. this._handleRemainder(schemaTag, remainder)
  100. }
  101. /**
  102. * Handle the remainder portion for value tag (converter handles others).
  103. *
  104. * @param {SchemaTag} schemaTag - The part of the tag that is in the schema.
  105. * @param {string} remainder - the leftover part.
  106. * @throws {IssueError} If parsing the remainder section fails.
  107. * @private
  108. */
  109. _handleRemainder(schemaTag, remainder) {
  110. if (!(schemaTag instanceof SchemaValueTag)) {
  111. return
  112. }
  113. // Check that there is a value if required
  114. const reserved = ReservedChecker.getInstance()
  115. if ((schemaTag.hasAttribute('requireChild') || reserved.requireValueTags.has(schemaTag.name)) && remainder === '') {
  116. IssueError.generateAndThrow('valueRequired', { tag: this.originalTag })
  117. }
  118. // Check if this could have a two-level value
  119. const [value, rest] = this._getSplitValue(remainder)
  120. this._splitValue = rest
  121. // Resolve the units and check
  122. const [actualUnit, actualUnitString, actualValueString] = this._separateUnits(schemaTag, value)
  123. this._units = actualUnitString
  124. this._value = actualValueString
  125. if (actualUnit === null && actualUnitString !== null) {
  126. IssueError.generateAndThrow('unitClassInvalidUnit', { tag: this.originalTag })
  127. }
  128. if (!this.checkValue(actualValueString)) {
  129. IssueError.generateAndThrow('invalidValue', { tag: this.originalTag })
  130. }
  131. }
  132. /**
  133. * Separate the remainder of the tag into three parts.
  134. *
  135. * @param {SchemaTag} schemaTag - The part of the tag that is in the schema.
  136. * @param {string} remainder - The leftover part.
  137. * @returns {Array} - [SchemaUnit, string, string] representing the actual Unit, the unit string and the value string.
  138. * @throws {IssueError} - If parsing the remainder section fails.
  139. */
  140. _separateUnits(schemaTag, remainder) {
  141. const unitClasses = schemaTag.unitClasses
  142. let actualUnit = null
  143. let actualUnitString = null
  144. let actualValueString = remainder // If no unit class, the remainder is the value
  145. for (let i = 0; i < unitClasses.length; i++) {
  146. ;[actualUnit, actualUnitString, actualValueString] = unitClasses[i].extractUnit(remainder)
  147. if (actualUnit !== null) {
  148. break // found the unit
  149. }
  150. }
  151. return [actualUnit, actualUnitString, actualValueString]
  152. }
  153. /**
  154. * Handle reserved three-level tags.
  155. * @param {string} remainder - The remainder of the tag string after schema tag.
  156. */
  157. _getSplitValue(remainder) {
  158. if (!TWO_LEVEL_TAGS.has(this.schemaTag.name)) {
  159. return [remainder, null]
  160. }
  161. const [first, ...rest] = remainder.split('/')
  162. return [first, rest.join('/')]
  163. }
  164. /**
  165. * Nicely format this tag.
  166. *
  167. * @param {boolean} long - Whether the tags should be in long form.
  168. * @returns {string} - The nicely formatted version of this tag.
  169. */
  170. format(long = true) {
  171. let tagName
  172. if (long) {
  173. tagName = this._schemaTag?.longExtend(this._remainder)
  174. } else {
  175. tagName = this._schemaTag?.extend(this._remainder)
  176. }
  177. if (tagName === undefined) {
  178. tagName = this.originalTag
  179. }
  180. if (this.schema?.prefix) {
  181. return this.schema.prefix + ':' + tagName
  182. } else {
  183. return tagName
  184. }
  185. }
  186. /**
  187. * Return the normalized version of this tag.
  188. * @returns {string} - The normalized version of this tag.
  189. */
  190. get normalized() {
  191. return this._normalized
  192. }
  193. /**
  194. * Override of {@link Object.prototype.toString}.
  195. *
  196. * @returns {string} The original form of this HED tag.
  197. */
  198. toString() {
  199. if (this.schema?.prefix) {
  200. return this.schema.prefix + ':' + this.originalTag
  201. } else {
  202. return this.originalTag
  203. }
  204. }
  205. /**
  206. * Determine whether this tag has a given attribute.
  207. *
  208. * @param {string} attribute An attribute name.
  209. * @returns {boolean} Whether this tag has the named attribute.
  210. */
  211. hasAttribute(attribute) {
  212. return this.schemaTag.hasAttribute(attribute)
  213. }
  214. /**
  215. * Determine if this HED tag is equivalent to another HED tag.
  216. *
  217. * Note: HED tags are deemed equivalent if they have the same schema and normalized tag string.
  218. *
  219. * @param {ParsedHedTag} other - A HED tag to compare with this one.
  220. * @returns {boolean} Whether {@link other} True, if other is equivalent to this HED tag.
  221. */
  222. equivalent(other) {
  223. return other instanceof ParsedHedTag && this.formattedTag === other.formattedTag && this.schema === other.schema
  224. }
  225. /**
  226. * Get the schema tag object for this tag.
  227. *
  228. * @returns {SchemaTag} The schema tag object for this tag.
  229. */
  230. get schemaTag() {
  231. if (this._schemaTag instanceof SchemaValueTag) {
  232. return this._schemaTag.parent
  233. } else {
  234. return this._schemaTag
  235. }
  236. }
  237. /**
  238. * Indicates whether the tag is deprecated
  239. * @returns {boolean}
  240. */
  241. get isDeprecated() {
  242. return this.schemaTag.hasAttribute('deprecatedFrom')
  243. }
  244. /**
  245. * Indicates whether the tag is deprecated
  246. * @returns {boolean}
  247. */
  248. get isExtended() {
  249. return !this.takesValueTag && this._remainder !== ''
  250. }
  251. /**
  252. * Get the schema tag object for this tag's value-taking form.
  253. *
  254. * @returns {SchemaValueTag} The schema tag object for this tag's value-taking form.
  255. */
  256. get takesValueTag() {
  257. if (this._schemaTag instanceof SchemaValueTag) {
  258. return this._schemaTag
  259. }
  260. return undefined
  261. }
  262. /**
  263. * Checks if this HED tag has the {@code takesValue} attribute.
  264. *
  265. * @returns {boolean} Whether this HED tag has the {@code takesValue} attribute.
  266. */
  267. get takesValue() {
  268. return this.takesValueTag !== undefined
  269. }
  270. /**
  271. * Checks if this HED tag has the {@code unitClass} attribute.
  272. *
  273. * @returns {boolean} Whether this HED tag has the {@code unitClass} attribute.
  274. */
  275. get hasUnitClass() {
  276. if (!this.takesValueTag) {
  277. return false
  278. }
  279. return this.takesValueTag.hasUnitClasses
  280. }
  281. /**
  282. * Get the unit classes for this HED tag.
  283. *
  284. * @returns {SchemaUnitClass[]} The unit classes for this HED tag.
  285. */
  286. get unitClasses() {
  287. if (this.hasUnitClass) {
  288. return this.takesValueTag.unitClasses
  289. }
  290. return []
  291. }
  292. /**
  293. * Check if value is a valid value for this tag.
  294. *
  295. * @param {string} value - The value to be checked.
  296. * @returns {boolean} The result of check -- false if not a valid value.
  297. */
  298. checkValue(value) {
  299. if (!this.takesValue) {
  300. return false
  301. }
  302. if (value === '#') {
  303. // Placeholders work
  304. return true
  305. }
  306. const valueAttributeNames = this._schemaTag.valueAttributeNames
  307. const valueClassNames = valueAttributeNames?.get('valueClass')
  308. if (!valueClassNames) {
  309. // No specified value classes
  310. return allowedRegEx.test(value)
  311. }
  312. const entryManager = this.schema.entries.valueClasses
  313. for (let i = 0; i < valueClassNames.length; i++) {
  314. if (entryManager.getEntry(valueClassNames[i]).validateValue(value)) return true
  315. }
  316. return false
  317. }
  318. }