diff --git a/spec_tests/jsonTests.spec.js b/spec_tests/jsonTests.spec.js index 1a96f64a..9aedf2ca 100644 --- a/spec_tests/jsonTests.spec.js +++ b/spec_tests/jsonTests.spec.js @@ -1,8 +1,7 @@ import * as fs from 'node:fs' import path from 'node:path' -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import { BidsHedIssue } from '../src/bids/types/issues' diff --git a/src/bids/datasetParser.ts b/src/bids/datasetParser.ts index 9c9f2c8a..ab6443b2 100644 --- a/src/bids/datasetParser.ts +++ b/src/bids/datasetParser.ts @@ -13,11 +13,11 @@ import fsp from 'node:fs/promises' import path from 'node:path' -import { organizePaths } from '../utils/paths' -import { IssueError } from '../issues/issues' import { buildBidsSchemas } from './schema' import { type BidsJsonFile } from './types/json' +import { IssueError } from '../issues/issues' import { type HedSchemas } from '../schema/containers' +import { organizePaths } from '../utils/paths' type SchemaBuilder = (datasetDescription: BidsJsonFile) => Promise @@ -46,7 +46,7 @@ export abstract class BidsFileAccessor { /** * The HED schema builder function. */ - protected readonly schemaBuilder: SchemaBuilder + readonly schemaBuilder: SchemaBuilder /** * BIDS suffixes. @@ -64,7 +64,7 @@ export abstract class BidsFileAccessor { /** * BIDS special directories. */ - private static readonly SPECIAL_DIRS: string[] = ['phenotype', 'stimuli'] + static readonly SPECIAL_DIRS: string[] = ['phenotype', 'stimuli'] /** * Constructs a BidsFileAccessor. diff --git a/src/bids/types/dataset.ts b/src/bids/types/dataset.ts index ecad091d..003c888a 100644 --- a/src/bids/types/dataset.ts +++ b/src/bids/types/dataset.ts @@ -6,15 +6,15 @@ import path from 'node:path' import { BidsFileAccessor } from '../datasetParser' -import { BidsSidecar } from './json' +import { BidsHedIssue } from './issues' +import { BidsJsonFile, BidsSidecar } from './json' import { BidsTsvFile } from './tsv' import { generateIssue, IssueError } from '../../issues/issues' -import { getMergedSidecarData, organizedPathsGenerator } from '../../utils/paths' -import { BidsHedIssue } from './issues' import { type HedSchemas } from '../../schema/containers' +import { getMergedSidecarData, organizedPathsGenerator } from '../../utils/paths' -type BidsFileAccessorConstructor = { - create(datasetRootDirectory: string | object): Promise +type BidsFileAccessorConstructor = { + create(datasetRootDirectory: string | object): Promise> } /** @@ -52,7 +52,7 @@ type BidsFileAccessorConstructor = { * @property {HedSchemas} hedSchemas The HED schemas used to validate this dataset. * @property {BidsFileAccessor} fileAccessor The BIDS file accessor. */ -export class BidsDataset { +export class BidsDataset { /** * Map of BIDS sidecar files that contain HED annotations. * The keys are relative paths and the values are BidsSidecar objects. @@ -72,7 +72,7 @@ export class BidsDataset { /** * The BIDS file accessor. */ - public fileAccessor: BidsFileAccessor + public fileAccessor: BidsFileAccessor /** * Constructor for a BIDS dataset. @@ -81,7 +81,7 @@ export class BidsDataset { * @throws {IssueError} If accessor is not an instance of BidsFileAccessor. * @see BidsDataset.create */ - private constructor(accessor: BidsFileAccessor) { + private constructor(accessor: BidsFileAccessor) { if (!(accessor instanceof BidsFileAccessor)) { IssueError.generateAndThrowInternalError('BidsDataset constructor requires an instance of BidsFileAccessor') } @@ -102,15 +102,15 @@ export class BidsDataset { * @param fileAccessorClass The BidsFileAccessor class to use for accessing files. * @returns A Promise that resolves to a two-element array containing the BidsDataset instance (or null if creation failed) and an array of issues. */ - static async create( + static async create( rootOrFiles: string | object, - fileAccessorClass: BidsFileAccessorConstructor, - ): Promise<[BidsDataset | null, BidsHedIssue[]]> { + fileAccessorClass: BidsFileAccessorConstructor, + ): Promise<[BidsDataset | null, BidsHedIssue[]]> { let dataset = null const issues: BidsHedIssue[] = [] try { const accessor = await fileAccessorClass.create(rootOrFiles) - dataset = new BidsDataset(accessor) + dataset = new BidsDataset(accessor) const schemaIssues = await dataset.setHedSchemas() issues.push(...schemaIssues) const sidecarIssues = await dataset.setSidecars() @@ -134,21 +134,20 @@ export class BidsDataset { * @throws {IssueError} If `dataset_description.json` is missing or contains an invalid HED specification. */ async setHedSchemas(): Promise { + const datasetDescriptionFileName = 'dataset_description.json' let description try { - const descriptionContentString = await this.fileAccessor.getFileContent('dataset_description.json') + const descriptionContentString = await this.fileAccessor.getFileContent(datasetDescriptionFileName) if (descriptionContentString === null || typeof descriptionContentString === 'undefined') { - IssueError.generateAndThrow('missingSchemaSpecification', { file: 'dataset_description.json' }) - } - description = { - jsonData: JSON.parse(descriptionContentString), + IssueError.generateAndThrow('missingSchemaSpecification', { file: datasetDescriptionFileName }) } + description = new BidsJsonFile(datasetDescriptionFileName, {}, JSON.parse(descriptionContentString)) } catch (e) { if (e instanceof IssueError) { throw e } - IssueError.generateAndThrow('missingSchemaSpecification', { file: 'dataset_description.json' }) + IssueError.generateAndThrow('missingSchemaSpecification', { file: datasetDescriptionFileName }) } try { diff --git a/src/bids/types/json.ts b/src/bids/types/json.ts index 1ee73594..0b278149 100644 --- a/src/bids/types/json.ts +++ b/src/bids/types/json.ts @@ -12,7 +12,7 @@ import BidsHedSidecarValidator from '../validator/sidecarValidator' import { IssueError, addIssueParameters, type Issue } from '../../issues/issues' import { DefinitionManager, Definition } from '../../parser/definitionManager' import { type HedSchemas } from '../../schema/containers' -import { type ParsedHedColumnSplice } from '../../parser/parsedHedColumnSplice' +import type ParsedHedColumnSplice from '../../parser/parsedHedColumnSplice' const ILLEGAL_SIDECAR_KEYS = new Set(['hed', 'n/a']) diff --git a/src/parser/parseUtils.js b/src/parser/parseUtils.ts similarity index 52% rename from src/parser/parseUtils.js rename to src/parser/parseUtils.ts index f96a9c74..0795a873 100644 --- a/src/parser/parseUtils.js +++ b/src/parser/parseUtils.ts @@ -1,43 +1,45 @@ /** This module holds utilities for parsing HED strings. * @module parser/parseUtils */ + import ParsedHedTag from './parsedHedTag' +import { type Constructor } from '../utils/types' /** - * Extract the items of a specified subtype from a list of ParsedHedSubstring - * @param {ParsedHedSubstring[]} items - Objects to be filtered by class type. - * @param {Class} classType - The class type to filter by. - * @returns {ParsedHedSubstring[]} - A list of objects of the specified subclass of ParsedHedSubstring + * Extract the items of a specified subtype from a list of ParsedHedSubstring. + * + * @param items - Objects to be filtered by class type. + * @param classType - The class type to filter by. + * @returns A list of objects of the specified subclass of ParsedHedSubstring. */ -export function filterByClass(items, classType) { - return items && items.length ? items.filter((item) => item instanceof classType) : [] +export function filterByClass(items: any[], classType: Constructor): C[] { + return items?.filter((item) => item instanceof classType) ?? [] } /** * Extract the ParsedHedTag tags with a specified tag name - * @param {ParsedHedTag[]} tags - to be filtered by name - * @param {string} tagName - name of the tag to filter by - * @returns {ParsedHedTag[]} + * + * @param tags - to be filtered by name + * @param tagName - name of the tag to filter by + * @returns A list of tags with the name {} */ -export function filterByTagName(tags, tagName) { - if (!tags) { - return [] - } - return tags.filter((tag) => tag instanceof ParsedHedTag && tag.schemaTag?.name === tagName) +export function filterByTagName(tags: ParsedHedTag[], tagName: string): ParsedHedTag[] { + return tags?.filter((tag) => tag instanceof ParsedHedTag && tag.schemaTag?.name === tagName) ?? [] } /** * Extract the ParsedHedTag tags with a specified tag name. - * @param {Map} tagMap - The Map of parsed HED tags for extraction (must be defined). - * @param {string[]} tagNames - The names to use as keys for the filter. - * @returns {ParsedHedTag[]} - A list of temporal tags. + * + * @param tagMap - The Map of parsed HED tags for extraction (must be defined). + * @param tagNames - The names to use as keys for the filter. + * @returns A list of temporal tags. */ -export function filterTagMapByNames(tagMap, tagNames) { +export function filterTagMapByNames(tagMap: Map, tagNames: string[]): ParsedHedTag[] { if (!tagNames || tagMap.size === 0) { return [] } - const keys = [...tagNames].filter((name) => tagMap.has(name)) + const keys = tagNames.filter((name) => tagMap.has(name)) if (keys.length === 0) { return [] } @@ -47,42 +49,50 @@ export function filterTagMapByNames(tagMap, tagNames) { /** * Convert a list of ParsedHedTag objects into a comma-separated string of their string representations. - * @param {ParsedHedTag[]} tagList - The HED tags whose string representations should be put in a comma-separated list. - * @returns {string} A comma separated list of original tag names for tags in tagList. + * + * @param tagList - The HED tags whose string representations should be put in a comma-separated list. + * @returns A comma separated list of original tag names for tags in tagList. */ -export function getTagListString(tagList) { +export function getTagListString(tagList: ParsedHedTag[]): string { return tagList.map((tag) => tag.toString()).join(', ') } /** * Create a map of the ParsedHedTags by type. - * @param {ParsedHedTag[]} tagList - The HED tags to be categorized. - * @param {Set} tagNames - The tag names to use as categories. - * @returns {Map} - A map (string --> ParsedHedTag) of tag name to a list of tags with that name. + * + * @param tagList - The HED tags to be categorized. + * @param tagNames - The tag names to use as categories. + * @returns A map of tag name to a list of tags with that name. */ -export function categorizeTagsByName(tagList, tagNames = null) { +export function categorizeTagsByName( + tagList: ParsedHedTag[], + tagNames: Set | null = null, +): Map { // Initialize the map with keys from tagNames and an "other" key - const resultMap = new Map() + const resultMap = new Map() // Iterate through A and categorize - tagList.forEach((tag) => { + for (const tag of tagList) { if (!tagNames || tagNames.has(tag.schemaTag.name)) { - const tagList = resultMap.get(tag.schemaTag.name) || [] + const tagList = resultMap.get(tag.schemaTag.name) ?? [] tagList.push(tag) resultMap.set(tag.schemaTag.name, tagList) // Add to matching key list } - }) + } + return resultMap } /** - * Return a list of duplicate strings. - * @param { string[] } itemList - A list of strings to look for duplicates in. - * @returns {string[]} - A list of unique duplicate strings (multiple copies not repeated). + * Return a list of duplicates. + * + * @param itemList - A list of items in which to look for duplicates. + * @returns A list of unique duplicates (multiple copies not repeated). */ -export function getDuplicates(itemList) { - const checkSet = new Set() - const dupSet = new Set() +export function getDuplicates(itemList: T[]): T[] { + const checkSet = new Set() + const dupSet = new Set() + for (const item of itemList) { if (!checkSet.has(item)) { checkSet.add(item) @@ -90,16 +100,17 @@ export function getDuplicates(itemList) { dupSet.add(item) } } + return [...dupSet] } /** - * lean up a string and remove redundant commas and parentheses. - * @param {string} stringIn - The input string to be cleaned up. - * @return {string} - The cleaned-up string with redundant commas and parentheses removed. + * Clean up a string and remove redundant commas and parentheses. * + * @param stringIn - The input string to be cleaned up. + * @return The cleaned-up string with redundant commas and parentheses removed. */ -export function cleanupEmpties(stringIn) { +export function cleanupEmpties(stringIn: string): string { const leadingCommaRegEx = /^\s*,+/g // Remove leading commas const trailingCommaRegEx = /,\s*$/g // Remove trailing commas const innerCommaRegEx = /,\s*,+/g // Collapse multiple commas inside @@ -108,7 +119,7 @@ export function cleanupEmpties(stringIn) { const trailingInnerCommaRegEx = /[\s,]+\)/g // Remove trailing commas and spaces inside parentheses let result = stringIn - let previousResult + let previousResult: string do { previousResult = result @@ -131,6 +142,7 @@ export function cleanupEmpties(stringIn) { // Step 5: Remove trailing commas inside parentheses result = result.replace(trailingInnerCommaRegEx, ')') } while (result !== previousResult) // Keep looping until no more changes + result = result.replace(/\(\s*,+/g, '(') return result.trim() } diff --git a/src/parser/parsedHedColumnSplice.js b/src/parser/parsedHedColumnSplice.ts similarity index 56% rename from src/parser/parsedHedColumnSplice.js rename to src/parser/parsedHedColumnSplice.ts index 87a86c92..1c842240 100644 --- a/src/parser/parsedHedColumnSplice.js +++ b/src/parser/parsedHedColumnSplice.ts @@ -3,8 +3,6 @@ */ import ParsedHedSubstring from './parsedHedSubstring' -import ParsedHedGroup from './parsedHedGroup' -import ParsedHedString from './parsedHedString' /** * A template for an inline column splice in a HED string. @@ -12,54 +10,48 @@ import ParsedHedString from './parsedHedString' * @see {@link ParsedHedString} * @see {@link ParsedHedGroup} */ -export class ParsedHedColumnSplice extends ParsedHedSubstring { +export default class ParsedHedColumnSplice extends ParsedHedSubstring { /** * The normalized string representation of this column splice. - * @type {string} - * @private */ - _normalized + private readonly _normalized: string /** * Constructor. * - * @param {string} columnName The token for this tag. - * @param {number[]} bounds The collection of HED schemas. + * @param columnName The name of the referenced column. + * @param bounds The bounds of the column splice. */ - constructor(columnName, bounds) { + public constructor(columnName: string, bounds: [number, number]) { super(columnName, bounds) // Sets originalTag and originalBounds this._normalized = this.format(false) // Sets various forms of the tag. } /** * Get the normalized version of the object. - * - * @returns {string} */ - get normalized() { + public get normalized(): string { return this._normalized } /** * Nicely format this column splice template. * - * @param {boolean} long Whether the tags should be in long form. - * @returns {string} The formatted column splice template. + * @param long Whether the tags should be in long form. + * @returns The formatted column splice template. */ // eslint-disable-next-line no-unused-vars - format(long = true) { + public format(long: boolean = true): string { return '{' + this.originalTag + '}' } /** * Determine if this column splice is equivalent to another. * - * @param {ParsedHedColumnSplice} other The other column splice. - * @returns {boolean} Whether the two column splices are equivalent. + * @param other The other column splice. + * @returns Whether the two column splices are equivalent. */ - equivalent(other) { + public equivalent(other: unknown): boolean { return other instanceof ParsedHedColumnSplice && this.originalTag === other.originalTag } } - -export default ParsedHedColumnSplice diff --git a/src/parser/parsedHedGroup.js b/src/parser/parsedHedGroup.ts similarity index 60% rename from src/parser/parsedHedGroup.js rename to src/parser/parsedHedGroup.ts index 0fe19bf6..9aac6f4a 100644 --- a/src/parser/parsedHedGroup.js +++ b/src/parser/parsedHedGroup.ts @@ -1,13 +1,15 @@ /** This module holds the class for representing a HED group. * @module parser/parsedHedGroup */ + import differenceWith from 'lodash/differenceWith' -import { IssueError } from '../issues/issues' + +import ParsedHedColumnSplice from './parsedHedColumnSplice' import ParsedHedSubstring from './parsedHedSubstring' import ParsedHedTag from './parsedHedTag' -import ParsedHedColumnSplice from './parsedHedColumnSplice' import { ReservedChecker } from './reservedChecker' -import { filterByClass, categorizeTagsByName, getDuplicates, filterByTagName } from './parseUtils' +import { categorizeTagsByName, filterByClass, filterByTagName, getDuplicates } from './parseUtils' +import { IssueError } from '../issues/issues' /** * A parsed HED tag group. @@ -15,83 +17,82 @@ import { filterByClass, categorizeTagsByName, getDuplicates, filterByTagName } f export default class ParsedHedGroup extends ParsedHedSubstring { /** * The parsed HED tags, groups, or splices in the HED tag group at the top level. - * @type {import('./parsedHedSubstring.js').default[]} */ - tags + readonly tags: ParsedHedSubstring[] /** * The top-level parsed HED tags in this string. - * @type {ParsedHedTag[]} */ - topTags + readonly topTags: ParsedHedTag[] /** * The top-level parsed HED groups in this string. - * @type {ParsedHedGroup[]} */ - topGroups + readonly topGroups: ParsedHedGroup[] /** - * The top-level column splices in this string - * @type {import('./parsedHedColumnSplice.js').ParsedHedColumnSplice[]} + * The top-level column splices in this string. */ - topSplices + readonly topSplices: ParsedHedColumnSplice[] /** * All the parsed HED tags in this string. - * @type {ParsedHedTag[]} */ - allTags + readonly allTags: ParsedHedTag[] /** * Reserved HED group tags. This only covers top group tags in the group. - * @type {Map} */ - reservedTags + readonly reservedTags: Map /** * The top-level child subgroups containing Def-expand tags. - * @type {ParsedHedGroup[]} */ - defExpandChildren + readonly defExpandChildren: ParsedHedGroup[] + + /** + * The top-level Def tags. + */ + readonly defTags: ParsedHedTag[] /** - * The top-level Def tags - * @type {ParsedHedTag[]} + * The top-level Def-expand tags. */ - defTags + readonly defExpandTags: ParsedHedTag[] /** - * The top-level Def-expand tags - * @type {ParsedHedTag[]} + * The top-level Definition tags. */ - defExpandTags + readonly definitionTags: ParsedHedTag[] /** * True if this group has a Definition tag at the top level. - * @type {boolean} */ - isDefinitionGroup + readonly isDefinitionGroup: boolean /** * The total number of top-level Def tags and top-level Def-expand groups. - * @type {Number} */ - defCount + readonly defCount: number /** * The unique top-level tag requiring a Def or Def-expand group, if any. - * @type {import('./parsedHedTag.js').default[] | null} */ - requiresDefTag + readonly requiresDefTag: ParsedHedTag[] | null + + /** + * The normalized string representation of this column splice. + */ + #normalized: string /** * Constructor. - * @param {import('./parsedHedSubstring.js').default[]} parsedHedTags The parsed HED tags, groups or column splices in the HED tag group. - * @param {string} hedString The original HED string. - * @param {number[]} originalBounds The bounds of the HED tag in the original HED string. + * + * @param parsedHedTags The parsed HED tags, groups or column splices in the HED tag group. + * @param hedString The original HED string. + * @param originalBounds The bounds of the HED tag in the original HED string. */ - constructor(parsedHedTags, hedString, originalBounds) { + public constructor(parsedHedTags: ParsedHedSubstring[], hedString: string, originalBounds: [number, number]) { const originalTag = hedString.substring(originalBounds[0], originalBounds[1]) super(originalTag, originalBounds) this.tags = parsedHedTags @@ -99,25 +100,9 @@ export default class ParsedHedGroup extends ParsedHedSubstring { this.topTags = filterByClass(parsedHedTags, ParsedHedTag) this.topSplices = filterByClass(parsedHedTags, ParsedHedColumnSplice) this.allTags = this._getAllTags() - this._normalized = undefined - this._initializeGroups() - } - - /** - * Recursively create a list of all the tags in this group. - * @returns {ParsedHedTag[]} - * @private - */ - _getAllTags() { - const subgroupTags = this.topGroups.flatMap((tagGroup) => tagGroup.allTags) - return this.topTags.concat(subgroupTags) - } + this.#normalized = undefined - /** - * Sets information about the reserved tags, particularly definition-related tags in this group. - * @private - */ - _initializeGroups() { + // Initialize groups. const reserved = ReservedChecker.getInstance() this.reservedTags = categorizeTagsByName(this.topTags, reserved.reservedNames) this.defExpandTags = this._filterTopTagsByTagName('Def-expand') @@ -132,25 +117,32 @@ export default class ParsedHedGroup extends ParsedHedSubstring { } /** - * Filter top tags by tag name. + * Recursively create a list of all the tags in this group. * - * @param {string} tagName - The schemaTag name to filter by. - * @returns {import('./parsedHedTag.js').default[]} An array of top-level tags with the given name. - * @private + * @returns A list of all the tags in this group. + */ + private _getAllTags(): ParsedHedTag[] { + const subgroupTags = this.topGroups.flatMap((tagGroup) => tagGroup.allTags) + return this.topTags.concat(subgroupTags) + } + + /** + * Filter top tags by tag name. * + * @param tagName The schemaTag name to filter by. + * @returns An array of top-level tags with the given name. */ - _filterTopTagsByTagName(tagName) { - return this.topTags.filter((tag) => tag.schemaTag._name === tagName) + private _filterTopTagsByTagName(tagName: string): ParsedHedTag[] { + return this.topTags.filter((tag) => tag.schemaTag.name === tagName) } /** * Filter top subgroups that include a tag at the top-level of the group. * - * @param {string} tagName - The schemaTag name to filter by. - * @returns {ParsedHedGroup[]} Array of subgroups containing the specified tag. - * @private + * @param tagName The schemaTag name to filter by. + * @returns Array of subgroups containing the specified tag. */ - _filterSubgroupsByTagName(tagName) { + private _filterSubgroupsByTagName(tagName: string): ParsedHedGroup[] { return Array.from(this.topLevelGroupIterator()).filter((subgroup) => subgroup.topTags.some((tag) => tag.schemaTag.name === tagName), ) @@ -159,24 +151,24 @@ export default class ParsedHedGroup extends ParsedHedSubstring { /** * Nicely format this tag group. * - * @param {boolean} long Whether the tags should be in long form. - * @returns {string} The formatted tag group. + * @param long Whether the tags should be in long form. + * @returns The formatted tag group. */ - format(long = true) { + public format(long: boolean = true): string { return '(' + this.tags.map((substring) => substring.format(long)).join(', ') + ')' } /** * Determine if this group is equivalent to another. * - * @param {ParsedHedGroup} other The other group. - * @returns {boolean} Whether the two groups are equivalent. + * @param other The other group. + * @returns Whether the two groups are equivalent. */ - equivalent(other) { + public equivalent(other: unknown): boolean { if (!(other instanceof ParsedHedGroup)) { return false } - const equivalence = (ours, theirs) => ours.equivalent(theirs) + const equivalence = (ours: ParsedHedGroup, theirs: ParsedHedGroup) => ours.equivalent(theirs) return ( differenceWith(this.tags, other.tags, equivalence).length === 0 && differenceWith(other.tags, this.tags, equivalence).length === 0 @@ -184,18 +176,19 @@ export default class ParsedHedGroup extends ParsedHedSubstring { } /** - * Return a normalized string representation - * @returns {string} The normalized string representation of this group. + * Return a normalized string representation. + * + * @returns The normalized string representation of this group. */ - get normalized() { - if (this._normalized) { - return this._normalized + public get normalized(): string { + if (this.#normalized) { + return this.#normalized } // Recursively normalize each item in the group const normalizedItems = this.tags.map((item) => item.normalized) // Sort normalized items to ensure order independence - const sortedNormalizedItems = normalizedItems.sort() + const sortedNormalizedItems = normalizedItems.toSorted((a, b) => a.localeCompare(b)) const duplicates = getDuplicates(sortedNormalizedItems) if (duplicates.length > 0) { @@ -204,26 +197,26 @@ export default class ParsedHedGroup extends ParsedHedSubstring { string: this.originalTag, }) } - this._normalized = '(' + sortedNormalizedItems.join(',') + ')' - // Return the normalized group as a string - return `(${sortedNormalizedItems.join(',')})` // Using curly braces to indicate unordered group + this.#normalized = '(' + sortedNormalizedItems.join(',') + ')' + return this.#normalized } /** * Override of {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString | Object.prototype.toString}. * - * @returns {string} The original string for this group. + * @returns The original string for this group. */ - toString() { + public toString(): string { return this.originalTag } /** * Iterator over the ParsedHedGroup objects in this HED tag group. - * @param {string | null} tagName - The name of the tag whose groups are to be iterated over or null if all tags. - * @yields {ParsedHedGroup} This object and the ParsedHedGroup objects belonging to this tag group. + * + * @param tagName The name of the tag whose groups are to be iterated over or null if all tags. + * @yields This object and the ParsedHedGroup objects belonging to this tag group. */ - *subParsedGroupIterator(tagName = null) { + public *subParsedGroupIterator(tagName: string | null = null): Generator { if (!tagName || filterByTagName(this.topTags, tagName)) { yield this } @@ -237,9 +230,9 @@ export default class ParsedHedGroup extends ParsedHedSubstring { /** * Iterator over the parsed HED tags in this HED tag group. * - * @yields {import('./parsedHedTag.js').default} This tag group's HED tags. + * @yields This tag group's HED tags. */ - *tagIterator() { + public *tagIterator(): Generator { for (const innerTag of this.tags) { if (innerTag instanceof ParsedHedTag) { yield innerTag @@ -252,9 +245,9 @@ export default class ParsedHedGroup extends ParsedHedSubstring { /** * Iterator over the parsed HED column splices in this HED tag group. * - * @yields {import('./parsedHedColumnSplice.js').ParsedHedColumnSplice} This tag group's HED column splices. + * @yields This tag group's HED column splices. */ - *columnSpliceIterator() { + public *columnSpliceIterator(): Generator { for (const innerTag of this.tags) { if (innerTag instanceof ParsedHedColumnSplice) { yield innerTag @@ -267,9 +260,9 @@ export default class ParsedHedGroup extends ParsedHedSubstring { /** * Iterator over the top-level parsed HED groups in this HED tag group. * - * @yields {ParsedHedGroup} This tag group's top-level HED groups. + * @yields This tag group's top-level HED groups. */ - *topLevelGroupIterator() { + public *topLevelGroupIterator(): Generator { for (const innerTag of this.tags) { if (innerTag instanceof ParsedHedGroup) { yield innerTag diff --git a/src/parser/parsedHedString.js b/src/parser/parsedHedString.ts similarity index 66% rename from src/parser/parsedHedString.js rename to src/parser/parsedHedString.ts index d26b1bb4..2c4720a3 100644 --- a/src/parser/parsedHedString.js +++ b/src/parser/parsedHedString.ts @@ -1,70 +1,69 @@ /** This module holds the class representing a HED string. * @module parser/parsedHedString */ -import ParsedHedTag from './parsedHedTag' -import ParsedHedGroup from './parsedHedGroup' + import ParsedHedColumnSplice from './parsedHedColumnSplice' +import ParsedHedGroup from './parsedHedGroup' +import type ParsedHedSubstring from './parsedHedSubstring' +import ParsedHedTag from './parsedHedTag' import { filterByClass, getDuplicates } from './parseUtils' import { IssueError } from '../issues/issues' /** * A parsed HED string. */ -export class ParsedHedString { +export default class ParsedHedString { /** * The original HED string. - * @type {string} */ - hedString + readonly hedString: string /** * The parsed substring data in unfiltered form. - * @type {import('./parsedHedSubstring.js').default[]} */ - parseTree + readonly parseTree: ParsedHedSubstring[] /** * The tag groups in the string (top-level). - * @type {ParsedHedGroup[]} */ - tagGroups + readonly tagGroups: ParsedHedGroup[] /** * All the top-level tags in the string. - * @type {ParsedHedTag[]} */ - topLevelTags + readonly topLevelTags: ParsedHedTag[] /** - * All the tags in the string at all levels - * @type {ParsedHedTag[]} + * All the tags in the string at all levels. */ - tags + readonly tags: ParsedHedTag[] /** * All the column splices in the string at all levels. - * @type {ParsedHedColumnSplice[]} */ - columnSplices + readonly columnSplices: ParsedHedColumnSplice[] /** - * The tags in the top-level tag groups in the string, split into arrays. - * @type {ParsedHedTag[][]} + * The tags in the top-level tag groups in the string. */ - topLevelGroupTags + readonly topLevelGroupTags: ParsedHedTag[] /** * The top-level definition tag groups in the string. - * @type {import('./parsedHedGroup.js').default[]} */ - definitions + readonly definitions: ParsedHedGroup[] + + /** + * The normalized string representation of this column splice. + */ + readonly normalized: string /** * Constructor. - * @param {string} hedString The original HED string. - * @param {import('./parsedHedSubstring.js').default[]} parsedTags The nested list of parsed HED tags, groups, and column splices. + * @param hedString The original HED string. + * @param parsedTags The nested list of parsed HED tags, groups, and column splices. */ - constructor(hedString, parsedTags) { + public constructor(hedString: string, parsedTags: ParsedHedSubstring[]) { this.hedString = hedString this.parseTree = parsedTags this.tagGroups = filterByClass(parsedTags, ParsedHedGroup) @@ -77,8 +76,7 @@ export class ParsedHedString { const subgroupColumnSplices = this.tagGroups.flatMap((tagGroup) => Array.from(tagGroup.columnSpliceIterator())) this.columnSplices = topLevelColumnSplices.concat(subgroupColumnSplices) - //this.topLevelGroupTags = this.tagGroups.map((tagGroup) => filterByClass(tagGroup.tags, ParsedHedTag)) - this.topLevelGroupTags = this.tagGroups.flatMap((tagGroup) => filterByClass(tagGroup.tags, ParsedHedTag)) + this.topLevelGroupTags = this.tagGroups.flatMap((tagGroup) => tagGroup.topTags) this.definitions = this.tagGroups.filter((group) => group.isDefinitionGroup) this.normalized = this._getNormalized() } @@ -86,25 +84,24 @@ export class ParsedHedString { /** * Nicely format this HED string. (Doesn't allow column splices). * - * @param {boolean} long Whether the tags should be in long form. - * @returns {string} The formatted HED string. + * @param long Whether the tags should be in long form. + * @returns The formatted HED string. */ - format(long = true) { + public format(long: boolean = true): string { return this.parseTree.map((substring) => substring.format(long)).join(', ') } /** * Return a normalized string representation. * - * @returns {string} The normalized HED string. - * @private + * @returns The normalized HED string. */ - _getNormalized() { + private _getNormalized(): string { // This is an implicit recursion as the items have the same call. const normalizedItems = this.parseTree.map((item) => item.normalized) // Sort normalized items to ensure order independence - const sortedNormalizedItems = normalizedItems.sort() + const sortedNormalizedItems = normalizedItems.toSorted((a, b) => a.localeCompare(b)) const duplicates = getDuplicates(sortedNormalizedItems) if (duplicates.length > 0) { IssueError.generateAndThrow('duplicateTag', { tags: '[' + duplicates.join('],[') + ']', string: this.hedString }) @@ -116,11 +113,9 @@ export class ParsedHedString { /** * Override of {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString | Object.prototype.toString}. * - * @returns {string} The original HED string. + * @returns The original HED string. */ - toString() { + public toString(): string { return this.hedString } } - -export default ParsedHedString diff --git a/src/parser/parsedHedSubstring.js b/src/parser/parsedHedSubstring.js deleted file mode 100644 index e7ee9c9b..00000000 --- a/src/parser/parsedHedSubstring.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * A parsed HED substring. - * @module parser/arsedHedSubstring - */ -export class ParsedHedSubstring { - /** - * The original pre-parsed version of the HED tag. - * @type {string} - */ - originalTag - /** - * The bounds of the HED tag in the original HED string. - * @type {int[]} - */ - originalBounds - - /** - * Constructor. - * @param {string} originalTag The original HED tag. - * @param {number[]} originalBounds The bounds of the HED tag in the original HED string. - */ - constructor(originalTag, originalBounds) { - this.originalTag = originalTag - this.originalBounds = originalBounds - } - - /** - * Nicely format this substring. This is left blank for the subclasses to override. - * - * This is left blank for the subclasses to override. - * - * @param {boolean} long - Whether the tags should be in long form. - * @returns {string} - * @abstract - */ - format(long = true) {} - - /** - * Get the normalized version of the object. - * - * @returns {string} - * @abstract - */ - get normalized() { - return '' - } - - /** - * Override of {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString | Object.prototype.toString}. - * - * @returns {string} The original form of this HED substring. - */ - toString() { - return this.originalTag - } -} - -export default ParsedHedSubstring diff --git a/src/parser/parsedHedSubstring.ts b/src/parser/parsedHedSubstring.ts new file mode 100644 index 00000000..0430ad71 --- /dev/null +++ b/src/parser/parsedHedSubstring.ts @@ -0,0 +1,58 @@ +/** + * A parsed HED substring. + * @module parser/parsedHedSubstring + */ +export default abstract class ParsedHedSubstring { + /** + * The original pre-parsed version of the HED tag. + */ + originalTag: string + /** + * The bounds of the HED tag in the original HED string. + */ + originalBounds: [number, number] + + /** + * Constructor. + * + * @param originalTag The original HED tag. + * @param originalBounds The bounds of the HED tag in the original HED string. + */ + protected constructor(originalTag: string, originalBounds: [number, number]) { + this.originalTag = originalTag + this.originalBounds = originalBounds + } + + /** + * Nicely format this substring. + * + * @param long Whether the tags should be in long form. + * @returns A nicely formatted version of this substring. + * @abstract + */ + public abstract format(long: boolean): string + + /** + * Get the normalized version of the object. + * + * @returns The normalized version of this substring. + */ + public abstract get normalized(): string + + /** + * Determine if this substring is equivalent to another. + * + * @param other The other substring. + * @returns Whether the two substrings are equivalent. + */ + public abstract equivalent(other: unknown): boolean + + /** + * Override of {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString Object.prototype.toString}. + * + * @returns The original form of this HED substring. + */ + public toString(): string { + return this.originalTag + } +} diff --git a/src/parser/parsedHedTag.js b/src/parser/parsedHedTag.ts similarity index 68% rename from src/parser/parsedHedTag.js rename to src/parser/parsedHedTag.ts index 3e118196..fdfb7d5b 100644 --- a/src/parser/parsedHedTag.js +++ b/src/parser/parsedHedTag.ts @@ -1,11 +1,14 @@ /** This module holds the class representing a parsed HED tag. * @module parser/parsedHedTag */ -import { IssueError } from '../issues/issues' + import ParsedHedSubstring from './parsedHedSubstring' -import { SchemaValueTag } from '../schema/entries' -import TagConverter from './tagConverter' import { ReservedChecker } from './reservedChecker' +import TagConverter from './tagConverter' +import { type TagSpec } from './tokenizer' +import { IssueError } from '../issues/issues' +import { type HedSchema, type HedSchemas } from '../schema/containers' +import { type SchemaTag, type SchemaUnit, type SchemaUnitClass, SchemaValueTag } from '../schema/entries' const TWO_LEVEL_TAGS = new Set(['Definition', 'Def', 'Def-expand']) const allowedRegEx = /^[^{},]*$/ @@ -16,82 +19,70 @@ const allowedRegEx = /^[^{},]*$/ export default class ParsedHedTag extends ParsedHedSubstring { /** * The formatted canonical version of the HED tag. - * @type {string} */ - formattedTag + formattedTag: string /** * The canonical form of the HED tag. - * @type {string} */ - canonicalTag + canonicalTag: string /** * The HED schema this tag belongs to. - * @type {HedSchema} */ + schema: HedSchema - schema /** * The schema's representation of this tag. - * - * @type {SchemaTag} - * @private */ - - _schemaTag + private _schemaTag: SchemaTag /** * The remaining part of the tag after the portion actually in the schema. - * @type {string} - * @private */ - _remainder + private _remainder: string /** * The value of the tag, if any. - * @type {string} - * @private */ - _value + private _value: string /** * If definition, this is the second value, otherwise empty string. - * @type {string} - * @private */ - _splitValue + private _splitValue: string /** * The units if any. - * @type {string} - * @private */ - _units + private _units: string + + /** + * The normalized string representation of this column splice. + */ + readonly #normalized: string /** * Constructor. * - * @param {TagSpec} tagSpec The token for this tag. - * @param {HedSchemas} hedSchemas The collection of HED schemas. - * @param {string} hedString The original HED string. + * @param tagSpec The token for this tag. + * @param hedSchemas The collection of HED schemas. * @throws {IssueError} If tag conversion or parsing fails. */ - constructor(tagSpec, hedSchemas, hedString) { + public constructor(tagSpec: TagSpec, hedSchemas: HedSchemas) { super(tagSpec.tag, tagSpec.bounds) // Sets originalTag and originalBounds - this._convertTag(hedSchemas, hedString, tagSpec) - this._normalized = this.format(false) // Sets various forms of the tag. + this._convertTag(hedSchemas, tagSpec) + this.#normalized = this.format(false) // Sets various forms of the tag. } /** * Convert this tag to its various forms * - * @param {HedSchemas} hedSchemas The collection of HED schemas. - * @param {string} hedString The original HED string. - * @param {TagSpec} tagSpec The token for this tag. + * @param hedSchemas The collection of HED schemas. + * @param tagSpec The token for this tag. * @throws {IssueError} If tag conversion or parsing fails. */ - _convertTag(hedSchemas, hedString, tagSpec) { + private _convertTag(hedSchemas: HedSchemas, tagSpec: TagSpec): void { const schemaName = tagSpec.library this.schema = hedSchemas.getSchema(schemaName) if (this.schema === undefined) { @@ -118,12 +109,11 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Handle the remainder portion for value tag (converter handles others). * - * @param {SchemaTag} schemaTag - The part of the tag that is in the schema. - * @param {string} remainder - the leftover part. + * @param schemaTag The part of the tag that is in the schema. + * @param remainder The leftover part. * @throws {IssueError} If parsing the remainder section fails. - * @private */ - _handleRemainder(schemaTag, remainder) { + private _handleRemainder(schemaTag: SchemaTag, remainder: string): void { if (!(schemaTag instanceof SchemaValueTag)) { return } @@ -153,12 +143,12 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Separate the remainder of the tag into three parts. * - * @param {SchemaTag} schemaTag - The part of the tag that is in the schema. - * @param {string} remainder - The leftover part. - * @returns {Array} - [SchemaUnit, string, string] representing the actual Unit, the unit string and the value string. - * @throws {IssueError} - If parsing the remainder section fails. + * @param schemaTag The part of the tag that is in the schema. + * @param remainder The leftover part. + * @returns A tuple representing the actual Unit, the unit string and the value string. + * @throws {IssueError} If parsing the remainder section fails. */ - _separateUnits(schemaTag, remainder) { + private _separateUnits(schemaTag: SchemaTag, remainder: string): [SchemaUnit, string, string] { const unitClasses = schemaTag.unitClasses let actualUnit = null let actualUnitString = null @@ -174,9 +164,10 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Handle reserved three-level tags. - * @param {string} remainder - The remainder of the tag string after schema tag. + * + * @param remainder The remainder of the tag string after schema tag. */ - _getSplitValue(remainder) { + private _getSplitValue(remainder: string): [string, string | null] { if (!TWO_LEVEL_TAGS.has(this.schemaTag.name)) { return [remainder, null] } @@ -187,10 +178,10 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Nicely format this tag. * - * @param {boolean} long - Whether the tags should be in long form. - * @returns {string} - The nicely formatted version of this tag. + * @param long Whether the tags should be in long form. + * @returns The nicely formatted version of this tag. */ - format(long = true) { + public format(long: boolean = true): string { let tagName if (long) { tagName = this._schemaTag?.longExtend(this._remainder) @@ -209,18 +200,19 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Return the normalized version of this tag. - * @returns {string} - The normalized version of this tag. + * + * @returns The normalized version of this tag. */ - get normalized() { - return this._normalized + public get normalized(): string { + return this.#normalized } /** * Override of {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString | Object.prototype.toString}. * - * @returns {string} The original form of this HED tag. + * @returns The original form of this HED tag. */ - toString() { + public toString(): string { if (this.schema?.prefix) { return this.schema.prefix + ':' + this.originalTag } else { @@ -231,31 +223,33 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Determine whether this tag has a given attribute. * - * @param {string} attribute An attribute name. - * @returns {boolean} Whether this tag has the named attribute. + * @param attribute An attribute name. + * @returns Whether this tag has the named attribute. */ - hasAttribute(attribute) { + public hasAttribute(attribute: string): boolean { return this.schemaTag.hasAttribute(attribute) } /** * Determine if this HED tag is equivalent to another HED tag. * - * Note: HED tags are deemed equivalent if they have the same schema and normalized tag string. + * @remarks + * + * HED tags are deemed equivalent if they have the same schema and normalized tag string. * - * @param {ParsedHedTag} other A HED tag to compare with this one. - * @returns {boolean} Whether the other tag is equivalent to this HED tag. + * @param other A HED tag to compare with this one. + * @returns Whether the other tag is equivalent to this HED tag. */ - equivalent(other) { + public equivalent(other: unknown): boolean { return other instanceof ParsedHedTag && this.formattedTag === other.formattedTag && this.schema === other.schema } /** * Get the schema tag object for this tag. * - * @returns {SchemaTag} The schema tag object for this tag. + * @returns The schema tag object for this tag. */ - get schemaTag() { + public get schemaTag(): SchemaTag { if (this._schemaTag instanceof SchemaValueTag) { return this._schemaTag.parent } else { @@ -264,27 +258,25 @@ export default class ParsedHedTag extends ParsedHedSubstring { } /** - * Indicates whether the tag is deprecated - * @returns {boolean} + * Indicates whether the tag is deprecated. */ - get isDeprecated() { + public get isDeprecated(): boolean { return this.schemaTag.hasAttribute('deprecatedFrom') } /** - * Indicates whether the tag is deprecated - * @returns {boolean} + * Indicates whether the tag is extended. */ - get isExtended() { + public get isExtended(): boolean { return !this.takesValueTag && this._remainder !== '' } /** * Get the schema tag object for this tag's value-taking form. * - * @returns {SchemaValueTag} The schema tag object for this tag's value-taking form. + * @returns The schema tag object for this tag's value-taking form. */ - get takesValueTag() { + public get takesValueTag(): SchemaValueTag | undefined { if (this._schemaTag instanceof SchemaValueTag) { return this._schemaTag } @@ -294,27 +286,27 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Checks if this HED tag has the `takesValue` attribute. * - * @returns {boolean} Whether this HED tag has the `takesValue` attribute. + * @returns Whether this HED tag has the `takesValue` attribute. */ - takesValue() { + public takesValue(): boolean { return this.takesValueTag !== undefined } /** * Checks if this HED tag has the `unitClass` attribute. * - * @returns {boolean} Whether this HED tag has the `unitClass` attribute. + * @returns Whether this HED tag has the `unitClass` attribute. */ - isUnitClass() { + public hasUnitClass(): boolean { return this.hasAttribute('unitClass') } /** * Get the unit classes for this HED tag. * - * @returns {SchemaUnitClass[]} The unit classes for this HED tag. + * @returns The unit classes for this HED tag. */ - get unitClasses() { + public get unitClasses(): SchemaUnitClass[] { if (this.hasUnitClass) { return this.takesValueTag.unitClasses } @@ -324,10 +316,10 @@ export default class ParsedHedTag extends ParsedHedSubstring { /** * Check if value is a valid value for this tag. * - * @param {string} value - The value to be checked. - * @returns {string} An empty string if value is value, otherwise an indication of failure. + * @param value The value to be checked. + * @returns An empty string if value is value, otherwise an indication of failure. */ - checkValue(value) { + public checkValue(value: string): string { if (!this.takesValue) { return `Tag "${this.schemaTag.name}" does not take a value but has value "${value}"` } diff --git a/src/parser/reservedChecker.js b/src/parser/reservedChecker.js index 04abbbd0..0eda24de 100644 --- a/src/parser/reservedChecker.js +++ b/src/parser/reservedChecker.js @@ -94,7 +94,7 @@ export class ReservedChecker { */ checkTagGroupLevels(hedString, fullValidation) { const issues = [] - const topGroupTags = hedString.topLevelGroupTags.flat() + const topGroupTags = hedString.topLevelGroupTags // Check for top-level violations because tag is deep hedString.tags.forEach((tag) => { diff --git a/src/parser/splitter.js b/src/parser/splitter.js deleted file mode 100644 index 702ee830..00000000 --- a/src/parser/splitter.js +++ /dev/null @@ -1,130 +0,0 @@ -/** This module holds the classes for basic splitting of HED strings. - * @module parser/splitter - */ -import ParsedHedTag from './parsedHedTag' -import ParsedHedColumnSplice from './parsedHedColumnSplice' -import ParsedHedGroup from './parsedHedGroup' -import { recursiveMap } from '../utils/array' -import { HedStringTokenizer, ColumnSpliceSpec, TagSpec } from './tokenizer' -import { generateIssue, IssueError } from '../issues/issues' - -export default class HedStringSplitter { - /** - * The HED string being split. - * @type {string} - */ - hedString - - /** - * The collection of HED schemas. - * @type {HedSchemas} - */ - hedSchemas - - /** - * Any issues found. - * @type {Issue[]} - */ - issues - - /** - * Constructor. - * - * @param {string} hedString The HED string to be split and parsed. - * @param {HedSchemas} hedSchemas The collection of HED schemas. - */ - constructor(hedString, hedSchemas) { - this.hedString = hedString - this.hedSchemas = hedSchemas - this.issues = [] - } - - /** - * Split and parse a HED string into tags and groups. - * - * @returns {Array} - [ParsedHedSubstring[], Issue[]] representing the parsed HED string data and any issues found. - */ - splitHedString() { - if (this.hedString === null || this.hedString === undefined || typeof this.hedString !== 'string') { - return [null, [generateIssue('invalidTagString', {})]] - } - if (this.hedString.length === 0) { - return [[], []] - } - const [tagSpecs, groupBounds, issues] = new HedStringTokenizer(this.hedString).tokenize() - if (issues.length > 0) { - return [null, issues] - } - const [parsedTags, parsingIssues] = this._createParsedTags(tagSpecs, groupBounds) - return [parsedTags, parsingIssues] - } - - /** - * Create parsed HED tags and groups from specifications. - * - * @param {TagSpec[]} tagSpecs The tag specifications. - * @param {GroupSpec} groupSpecs The group specifications. - * @returns {Array} - [ParsedHedSubstring[], Issue[]] representing the parsed HED tags and any issues found. - */ - _createParsedTags(tagSpecs, groupSpecs) { - // Create tags from specifications - this.issues = [] - const parsedTags = recursiveMap(tagSpecs, (tagSpec) => this._createParsedTag(tagSpec)) - - // Create groups from the parsed tags - const parsedTagsWithGroups = this._createParsedGroups(parsedTags, groupSpecs.children) - return [parsedTagsWithGroups, this.issues] - } - - _createParsedTag(tagSpec) { - if (tagSpec instanceof TagSpec) { - try { - return new ParsedHedTag(tagSpec, this.hedSchemas, this.hedString) - } catch (issueError) { - this.issues.push(this._handleIssueError(issueError)) - return null - } - } else if (tagSpec instanceof ColumnSpliceSpec) { - return new ParsedHedColumnSplice(tagSpec.columnName, tagSpec.bounds) - } - } - - /** - * Handle any issue encountered during tag parsing. - * - * @param {Error|IssueError} issueError The error encountered. - */ - _handleIssueError(issueError) { - if (issueError instanceof IssueError) { - return issueError.issue - } else if (issueError instanceof Error) { - return generateIssue('internalError', { message: issueError.message }) - } - } - - /** - * Create parsed HED groups from parsed tags and group specifications. - * - * @param {ParsedHedTag[]} tags The parsed HED tags. - * @param {GroupSpec[]} groupSpecs The group specifications. - * @returns {ParsedHedGroup[]} The parsed HED groups. - */ - _createParsedGroups(tags, groupSpecs) { - const tagGroups = [] - let index = 0 - - for (const tag of tags) { - if (Array.isArray(tag)) { - const groupSpec = groupSpecs[index] - tagGroups.push( - new ParsedHedGroup(this._createParsedGroups(tag, groupSpec.children), this.hedString, groupSpec.bounds), - ) - index++ - } else if (tag !== null) { - tagGroups.push(tag) - } - } - - return tagGroups - } -} diff --git a/src/parser/splitter.ts b/src/parser/splitter.ts new file mode 100644 index 00000000..644238f0 --- /dev/null +++ b/src/parser/splitter.ts @@ -0,0 +1,159 @@ +/** This module holds the classes for basic splitting of HED strings. + * @module parser/splitter + */ + +import ParsedHedColumnSplice from './parsedHedColumnSplice' +import ParsedHedGroup from './parsedHedGroup' +import type ParsedHedSubstring from './parsedHedSubstring' +import ParsedHedTag from './parsedHedTag' +import { ColumnSpliceSpec, type GroupSpec, HedStringTokenizer, TagSpec } from './tokenizer' +import { generateIssue, type Issue, IssueError } from '../issues/issues' +import { type HedSchemas } from '../schema/containers' +import { recursiveMap } from '../utils/array' +import { type RecursiveArray } from '../utils/types' + +export default class HedStringSplitter { + /** + * The HED string being split. + */ + private readonly hedString: string + + /** + * The collection of HED schemas. + */ + private readonly hedSchemas: HedSchemas + + /** + * Any issues found. + */ + private readonly issues: Issue[] + + /** + * The parsed HED substrings. + */ + private parsedTags: ParsedHedSubstring[] | null + + /** + * Constructor. + * + * @param hedString - The HED string to be split and parsed. + * @param hedSchemas - The collection of HED schemas. + */ + public constructor(hedString: string, hedSchemas: HedSchemas) { + this.hedString = hedString + this.hedSchemas = hedSchemas + this.issues = [] + this.parsedTags = undefined + } + + /** + * Split and parse a HED string into tags and groups. + * + * This method is idempotent. If called repeatedly, it will simply return the already-parsed substrings and issues. + * + * @returns A tuple representing the parsed HED string data and any issues found. + */ + public splitHedString(): [ParsedHedSubstring[], Issue[]] { + if (this.parsedTags === undefined) { + this._splitHedString() + } + return [this.parsedTags, this.issues] + } + + /** + * Split and parse a HED string into tags and groups. + * + * @returns A tuple representing the parsed HED string data and any issues found. + */ + private _splitHedString(): void { + if (typeof this.hedString !== 'string') { + this.parsedTags = null + this.issues.push(generateIssue('invalidTagString', {})) + return + } + if (this.hedString === '') { + this.parsedTags = [] + return + } + const [tagSpecs, groupBounds, issues] = new HedStringTokenizer(this.hedString).tokenize() + if (issues.length > 0) { + this.parsedTags = null + this.issues.push(...issues) + return + } + this.parsedTags = this._createParsedTags(tagSpecs, groupBounds) + } + + /** + * Create parsed HED tags and groups from specifications. + * + * @param tagSpecs - The tag specifications. + * @param groupSpecs - The group specifications. + * @returns A tuple representing the parsed HED tags and any issues found. + */ + private _createParsedTags(tagSpecs: TagSpec[], groupSpecs: GroupSpec): ParsedHedSubstring[] { + // Create tags from specifications + const parsedTags = recursiveMap(tagSpecs, (tagSpec) => this._createParsedTag(tagSpec)) + + // Create groups from the parsed tags + return this._createParsedGroups(parsedTags, groupSpecs.children) + } + + /** + * Create a parsed HED tag or column splice from a specification. + * + * @param tagSpec - The tag or column splice specification. + * @returns The parsed tag or column splice spec, or null if the tag parsing generated an error. + */ + private _createParsedTag(tagSpec: TagSpec | ColumnSpliceSpec): ParsedHedTag | ParsedHedColumnSplice | null { + if (tagSpec instanceof TagSpec) { + try { + return new ParsedHedTag(tagSpec, this.hedSchemas) + } catch (issueError) { + this.issues.push(this._handleIssueError(issueError)) + return null + } + } else if (tagSpec instanceof ColumnSpliceSpec) { + return new ParsedHedColumnSplice(tagSpec.columnName, tagSpec.bounds) + } + } + + /** + * Handle any issue encountered during tag parsing. + * + * @param issueError - The error encountered. + */ + private _handleIssueError(issueError: Error): Issue { + if (issueError instanceof IssueError) { + return issueError.issue + } else if (issueError instanceof Error) { + return generateIssue('internalError', { message: issueError.message }) + } + } + + /** + * Create parsed HED groups from parsed tags and group specifications. + * + * @param tags - The parsed HED tags. + * @param groupSpecs - The group specifications. + * @returns The parsed HED groups. + */ + private _createParsedGroups(tags: RecursiveArray, groupSpecs: GroupSpec[]): ParsedHedSubstring[] { + const tagGroups: ParsedHedSubstring[] = [] + let index = 0 + + for (const tag of tags) { + if (Array.isArray(tag)) { + const groupSpec = groupSpecs[index] + tagGroups.push( + new ParsedHedGroup(this._createParsedGroups(tag, groupSpec.children), this.hedString, groupSpec.bounds), + ) + index++ + } else if (tag !== null) { + tagGroups.push(tag) + } + } + + return tagGroups + } +} diff --git a/src/parser/tokenizer.js b/src/parser/tokenizer.js index b25861b7..01e717a5 100644 --- a/src/parser/tokenizer.js +++ b/src/parser/tokenizer.js @@ -1,6 +1,7 @@ /** This module holds a class for tokenizing HED strings. * @module parser/tokenizer */ + import { unicodeName } from 'unicode-name' import { generateIssue } from '../issues/issues' @@ -43,7 +44,7 @@ for (let i = 0x7f; i <= 0x9f; i++) { export class SubstringSpec { /** * The starting and ending bounds of the substring. - * @type {number[]} + * @type {[number, number]} */ bounds diff --git a/src/schema/specs.ts b/src/schema/specs.ts index 1c1ff6b1..ed8a4a4f 100644 --- a/src/schema/specs.ts +++ b/src/schema/specs.ts @@ -1,6 +1,7 @@ /** This module holds the specification classes for HED schemas. * @module schema/specs */ + import castArray from 'lodash/castArray' import semver from 'semver' @@ -176,13 +177,20 @@ export class SchemasSpec { * @returns A schemas specification object containing parsed schema specifications. * @throws {IssueError} If any schema specification is invalid. */ - public static parseVersionSpecs(versionSpecs: string | string[]): SchemasSpec { + public static parseVersionSpecs(versionSpecs: unknown): SchemasSpec { const schemasSpec = new SchemasSpec() - const processVersion = castArray(versionSpecs) - if (processVersion.length === 0) { + let processedVersionSpecs: string[] + if (typeof versionSpecs === 'string') { + processedVersionSpecs = castArray(versionSpecs) + } else if (!Array.isArray(versionSpecs) || !versionSpecs.every((spec) => typeof spec === 'string')) { + IssueError.generateAndThrow('invalidSchemaSpecification', { spec: versionSpecs }) + } else { + processedVersionSpecs = versionSpecs + } + if (processedVersionSpecs.length === 0) { IssueError.generateAndThrow('missingSchemaSpecification') } - for (const schemaVersion of processVersion) { + for (const schemaVersion of processedVersionSpecs) { const schemaSpec = SchemaSpec.parseVersionSpec(schemaVersion) schemasSpec.addSchemaSpec(schemaSpec) } diff --git a/src/utils/array.ts b/src/utils/array.ts index ddb567b1..4eed4143 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -3,27 +3,31 @@ * @module */ -type NestedArray = T | NestedArray[] +import { type RecursiveArray } from './types' /** * Apply a function recursively to an array. * - * @param array The array to map. - * @param fn The function to apply. + * @param array - The array to map. + * @param fn - The function to apply. * @returns The mapped array. */ -export function recursiveMap(array: NestedArray, fn: (element: T) => U): NestedArray { - if (Array.isArray(array)) { - return array.map((element) => recursiveMap(element, fn)) - } else { - return fn(array) +export function recursiveMap(array: RecursiveArray, fn: (element: T) => U): RecursiveArray { + const mappedArray: RecursiveArray = [] + for (const element of array) { + if (Array.isArray(element)) { + mappedArray.push(recursiveMap(element, fn)) + } else { + mappedArray.push(fn(element)) + } } + return mappedArray } /** * Generate an iterator over the pairwise combinations of an array. * - * @param array The array to combine. + * @param array - The array to combine. * @returns A generator which iterates over the list of combinations as tuples. */ export function* iteratePairwiseCombinations(array: T[]): Generator<[T, T]> { @@ -34,7 +38,8 @@ export function* iteratePairwiseCombinations(array: T[]): Generator<[T, T]> { /** * Type guard for an ordered pair of numbers (e.g. bounds). * - * @param value A possible ordered pair of numbers. + * @param value - A possible ordered pair of numbers. + * @returns Whether the value is an ordered pair of number. */ export function isNumberPair(value: unknown): value is [number, number] { return Array.isArray(value) && value.length === 2 && value.every((bound) => typeof bound === 'number') diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 00000000..c1b88609 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,16 @@ +/** + * General utility types. + * @module utils/types + */ + +/** + * A generic constructor type. + */ +export type Constructor = { + new (...args: any[]): Type +} + +/** + * A generic recursive array type. + */ +export interface RecursiveArray extends Array> {} diff --git a/tests/jsonTests/bidsTests.spec.js b/tests/jsonTests/bidsTests.spec.js index f6e540b7..b7446b90 100644 --- a/tests/jsonTests/bidsTests.spec.js +++ b/tests/jsonTests/bidsTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import path from 'node:path' import { buildSchemas } from '../../src/schema/init' diff --git a/tests/jsonTests/definitionManagerCreationTests.spec.js b/tests/jsonTests/definitionManagerCreationTests.spec.js index 9af4419e..81010b61 100644 --- a/tests/jsonTests/definitionManagerCreationTests.spec.js +++ b/tests/jsonTests/definitionManagerCreationTests.spec.js @@ -1,7 +1,6 @@ import path from 'node:path' -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import { buildSchemas } from '../../src/schema/init' diff --git a/tests/jsonTests/definitionManagerValidationTests.spec.js b/tests/jsonTests/definitionManagerValidationTests.spec.js index 906c0f50..2a7d9f0f 100644 --- a/tests/jsonTests/definitionManagerValidationTests.spec.js +++ b/tests/jsonTests/definitionManagerValidationTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import path from 'node:path' diff --git a/tests/jsonTests/normalizerTests.spec.js b/tests/jsonTests/normalizerTests.spec.js index c708ab4f..21af85b6 100644 --- a/tests/jsonTests/normalizerTests.spec.js +++ b/tests/jsonTests/normalizerTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import path from 'node:path' diff --git a/tests/jsonTests/schemaBuildTests.spec.js b/tests/jsonTests/schemaBuildTests.spec.js index 6745b16e..b3bc527f 100644 --- a/tests/jsonTests/schemaBuildTests.spec.js +++ b/tests/jsonTests/schemaBuildTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import { buildBidsSchemas } from '../../src/bids/schema' diff --git a/tests/jsonTests/schemaSpecTests.spec.js b/tests/jsonTests/schemaSpecTests.spec.js index 29a6bf0d..fe6a4ac4 100644 --- a/tests/jsonTests/schemaSpecTests.spec.js +++ b/tests/jsonTests/schemaSpecTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import { shouldRun } from '../testHelpers/testUtilities' diff --git a/tests/jsonTests/splitterTests.spec.js b/tests/jsonTests/splitterTests.spec.js index efa05b01..0e66c873 100644 --- a/tests/jsonTests/splitterTests.spec.js +++ b/tests/jsonTests/splitterTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import path from 'node:path' diff --git a/tests/jsonTests/stringParserTests.spec.js b/tests/jsonTests/stringParserTests.spec.js index cb09f8ea..74f50031 100644 --- a/tests/jsonTests/stringParserTests.spec.js +++ b/tests/jsonTests/stringParserTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll, it } from '@jest/globals' import path from 'node:path' diff --git a/tests/jsonTests/tagParserTests.spec.js b/tests/jsonTests/tagParserTests.spec.js index 409d1d31..c99fad1c 100644 --- a/tests/jsonTests/tagParserTests.spec.js +++ b/tests/jsonTests/tagParserTests.spec.js @@ -1,15 +1,16 @@ -import chai from 'chai' -const assert = chai.assert +import path from 'node:path' + import { beforeAll, describe, afterAll } from '@jest/globals' +import { assert } from 'chai' import ParsedHedTag from '../../src/parser/parsedHedTag' -import { shouldRun } from '../testHelpers/testUtilities' -import { parsedHedTagTests } from '../jsonTestData/tagParserTests.data' import { SchemaSpec, SchemasSpec } from '../../src/schema/specs' -import path from 'node:path' import { buildSchemas } from '../../src/schema/init' import { SchemaValueTag } from '../../src/schema/entries' +import { shouldRun } from '../testHelpers/testUtilities' +import { parsedHedTagTests } from '../jsonTestData/tagParserTests.data' + // Ability to select individual tests to run const skipMap = new Map() const runAll = true @@ -38,7 +39,7 @@ describe('TagSpec converter tests using JSON tests', () => { let issue = null let tag = null try { - tag = new ParsedHedTag(test.tagSpec, thisSchema, test.fullString) + tag = new ParsedHedTag(test.tagSpec, thisSchema) } catch (error) { issue = error.issue } diff --git a/tests/jsonTests/tokenizerTests.spec.js b/tests/jsonTests/tokenizerTests.spec.js index d738dcc7..7183181b 100644 --- a/tests/jsonTests/tokenizerTests.spec.js +++ b/tests/jsonTests/tokenizerTests.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, afterAll } from '@jest/globals' import { HedStringTokenizer } from '../../src/parser/tokenizer' diff --git a/tests/otherTests/bidsIssues.spec.js b/tests/otherTests/bidsIssues.spec.js index c63192c5..7b9ce2bc 100644 --- a/tests/otherTests/bidsIssues.spec.js +++ b/tests/otherTests/bidsIssues.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { describe, test } from '@jest/globals' import { BidsHedIssue } from '../../src/bids/types/issues' import { Issue } from '../../src/issues/issues' diff --git a/tests/otherTests/hed.spec.js b/tests/otherTests/hed.spec.js index 8bb349bf..5a2b4483 100644 --- a/tests/otherTests/hed.spec.js +++ b/tests/otherTests/hed.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { describe, it } from '@jest/globals' import * as hed from '../../src/utils/hedStrings' diff --git a/tests/otherTests/regexTests.spec.js b/tests/otherTests/regexTests.spec.js index ec2c5571..93eb0e43 100644 --- a/tests/otherTests/regexTests.spec.js +++ b/tests/otherTests/regexTests.spec.js @@ -1,6 +1,6 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { describe, it } from '@jest/globals' + import { cleanupEmpties } from '../../src/parser/parseUtils' describe('Tokenizer validation using JSON tests', () => { diff --git a/tests/otherTests/schema.spec.js b/tests/otherTests/schema.spec.js index 9584ea9c..257d2a64 100644 --- a/tests/otherTests/schema.spec.js +++ b/tests/otherTests/schema.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { beforeAll, describe, it } from '@jest/globals' import { generateIssue, IssueError } from '../../src/issues/issues' diff --git a/tests/otherTests/string.spec.js b/tests/otherTests/string.spec.js index 55c1a32e..ca597230 100644 --- a/tests/otherTests/string.spec.js +++ b/tests/otherTests/string.spec.js @@ -1,5 +1,4 @@ -import chai from 'chai' -const assert = chai.assert +import { assert } from 'chai' import { describe, it } from '@jest/globals' import * as stringUtils from '../../src/utils/string'