Repository: hed-standard/hed-python (language: python)

Issue:
Add commas to allowed desc characters



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 642e7b581..364493e94 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,14 @@
 # HEDTools - Python
-HED (Hierarchical Event Descriptors) is a framework for systematically describing both laboratory and real-world events. HED tags are comma-separated path strings. The goal of HED is to describe precisely the nature of the events of interest occurring in an experiment using a common language. HED, itself, is platform-independent and data-neutral. Most people will simply annotate their events by creating a spreadsheet that associates HED tags with event codes or the events themselves. If you have such a spreadsheet, you can use the HED Online Validator (http://visual.cs.utsa.edu/hed/) to validate your spreadsheet without downloading any tools.
+HED (Hierarchical Event Descriptors) is a framework for systematically describing both laboratory and real-world events. HED tags are comma-separated path strings. The goal of HED is to describe precisely the nature of the events of interest occurring in an experiment using a common language. HED, itself, is platform-independent and data-neutral. 
 
-### Python modules
- - hedconversion   converts a HED schema to an XML file to use in validation (python 3)
- - hedemail        implements the web hook that emails updates of HED schema to other tools
- - hedvalidation   main python code for validating hed tags
- - hedweb          contains the code to deploy the python validator as a docker module
-
-### How to validate a spreadsheet from python
-The hed_files_examples.py file in hedvalidation/examples shows how to validate a HED spreadsheet from a python program.
+Most people will simply annotate their events by creating a spreadsheet that associates HED tags with event codes or the events themselves. If you have such a spreadsheet, you can use the HED Online Validator currently available at [https://hedtags.ucsd.edu/hed](https://hedtags.ucsd.edu/hed) to validate your spreadsheet without downloading any tools.
 
-### Calling the validation functions from python
-The hed_string_examples.py file in hedvalidation/examples shows how to call python HED validator functions to validate a HED string.
+### Python modules
+ - hedtools        contains all of the validation and translation tools for hed tags and hed schema.
+ - hedweb          contains the code to deploy the validators and other HED tools as a web application running in a docker module.
+ 
+The HEDTools require python 3.7 or greater.
 
+### Calling HED tools directly from a python program
 
+The `examples` directory in hedtools gives runnable examples illustrating how to call various HED tools directly from python.
diff --git a/hedtools/examples/data/both_types_events_errors.json b/hedtools/examples/data/both_types_events_errors.json
index 86a0740ec..7edf29603 100644
--- a/hedtools/examples/data/both_types_events_errors.json
+++ b/hedtools/examples/data/both_types_events_errors.json
@@ -7,7 +7,7 @@
           "stop": "A blue square is displayed to indicate stopping"
        },
        "HED": {
-          "go": "Experimental-stimulus, Label/RedSquare,Description/A red square is displayed to indicate starting,Visual, Item/Object/2D Shape/Square,Attribute/Visual/Color/Red, Attribute/Visual/Rendering type/Screen,Attribute/Location/Screen/Center",
+          "go": "Experimental-stimulu, Label/RedSquare,Description/A red square is displayed to indicate starting,Visual, Item/Object/2D Shape/Square,Attribute/Visual/Color/Red, Attribute/Visual/Rendering type/Screen,Attribute/Location/Screen/Center",
           "stop": "Experimental-stimulus, Label/BlueSquare,Description/A blue square is displayed to indicate stopping,Visual, Item/Object/2D Shape/Square,Attribute/Visual/Color/Blue, Attribute/Visual/Rendering type/Screen,Attribute/Location/Screen/Center"
        }
    },
@@ -20,11 +20,17 @@
    "stim_file": {
        "LongName": "Stimulus file name",
        "Description": "Relative path of the stimulus image file",
-       "HED": {"THING":  "Attribute/File/#" }
+       "HED": {"THING":  "Attribute/File/#, Experimental-stimulu" }
    },
   "debug_col": {
        "LongName": "Stimulus file name",
        "Description": "Relative path of the stimulus image file",
-       "HED": "Attribute/Response start delay/1 seconds, Action/Button press"
+       "HED": "Attribute/Response start delay/1 seconds, Experimental-stimulu"
+   },
+     "stim_file2": {
+       "LongName": "Stimulus file name",
+       "Description": "Relative path of the stimulus image file",
+       "HED": {"THING":  "Attribute/File/1 seconds, Experimental-stimulu",
+               "THING2":  "Attribute/File/1 second, Experimental-stimulu"}
    }
 }
\ No newline at end of file
diff --git a/hedtools/examples/run_check_for_duplicate_tags.py b/hedtools/examples/run_check_for_duplicate_tags.py
deleted file mode 100644
index 8f2537dc5..000000000
--- a/hedtools/examples/run_check_for_duplicate_tags.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Simplified example code for running xml2_wiki
-
-from shutil import move
-from hed.schema import duplicate_nodes
-
-if __name__ == '__main__':
-    local_hed_xml = "data/HED7.1.1.xml"
-    output_location, errors = duplicate_nodes.check_for_duplicate_tags(local_hed_xml)
-    move(output_location, "duplicate_tags_in_HEDXML.txt")
diff --git a/hedtools/examples/run_hed_def_mapper_example.py b/hedtools/examples/run_hed_def_mapper_example.py
index e536788ac..8a498453e 100644
--- a/hedtools/examples/run_hed_def_mapper_example.py
+++ b/hedtools/examples/run_hed_def_mapper_example.py
@@ -3,7 +3,7 @@
 """
 import os
 from hed.util.event_file_input import EventFileInput
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 from hed.validator.hed_validator import HedValidator
 
 if __name__ == '__main__':
@@ -11,19 +11,19 @@
     example_data_path = 'data'   # path to example data
     hed3_tags_single_sheet = os.path.join(example_data_path, 'hed_tag_def_example.xlsx')
 
-    hed_dict = HedDictionary(local_hed_file)
+    hed_dict = HedSchema(local_hed_file)
     prefixed_needed_tag_columns = {2: 'Event/Label/', 3: 'Event/Description/'}
     input_file = EventFileInput(hed3_tags_single_sheet, json_def_files="data/both_types_events_def_example.json",
                                 tag_columns=[4], column_prefix_dictionary=prefixed_needed_tag_columns,
                                 worksheet_name='LKT Events',
-                                hed_dictionary=hed_dict)
+                                hed_schema=hed_dict)
 
-    validation_issues = input_file.validate_file_sidecars(hed_dictionary=hed_dict)
+    validation_issues = input_file.validate_file_sidecars(hed_schema=hed_dict)
     if validation_issues:
         print("There should be no errors with the sidecar.  This will likely cause other errors.")
         for issue in validation_issues:
             print(f"{issue['code']}: {issue['message']}")
-    validator = HedValidator(input_file, hed_dictionary=hed_dict)
+    validator = HedValidator(input_file, hed_schema=hed_dict)
     print(validator.get_printable_issue_string(
         "Normal hed string errors"))
 
diff --git a/hedtools/examples/run_json_validation_and_conversion.py b/hedtools/examples/run_json_validation_and_conversion.py
index 13048c291..6abee0e1e 100644
--- a/hedtools/examples/run_json_validation_and_conversion.py
+++ b/hedtools/examples/run_json_validation_and_conversion.py
@@ -2,16 +2,16 @@
         opening a json file and converting each HED tag from short to long."""
 
 from hed.util.column_def_group import ColumnDefGroup
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 from hed.tools.tag_format import TagFormat
 
 local_hed_xml = "data/HED8.0.0-alpha.1.xml"
-hed_dictionary = HedDictionary(local_hed_xml)
+hed_schema = HedSchema(local_hed_xml)
 json_filename = "data/both_types_events_errors.json"
 
 json_file = ColumnDefGroup(json_filename)
 # Print all the errors from the json file
-errors = json_file.validate_entries(hed_dictionary)
+errors = json_file.validate_entries(hed_schema)
 for error in errors.values():
     for sub_error in error:
         print(sub_error["message"])
diff --git a/hedtools/examples/run_validate_schema.py b/hedtools/examples/run_validate_schema.py
new file mode 100644
index 000000000..f492baaed
--- /dev/null
+++ b/hedtools/examples/run_validate_schema.py
@@ -0,0 +1,16 @@
+# Example of validating a given schema XML file.
+
+from hed.schema import schema_validator
+
+if __name__ == '__main__':
+    # This old schema should produce many issues, including many duplicate terms
+    local_hed_xml = "data/HED7.1.1.xml"
+    issues = schema_validator.validate_schema(local_hed_xml)
+    for issue in issues:
+        print(issue['message'])
+
+    # this should produce fairly minimal issues.
+    local_hed_xml = "data/HED8.0.0-alpha.1.xml"
+    issues = schema_validator.validate_schema(local_hed_xml)
+    for issue in issues:
+        print(issue['message'])
diff --git a/hedtools/examples/run_wiki2xml.py b/hedtools/examples/run_wiki2xml.py
index 899c932fe..549efc418 100644
--- a/hedtools/examples/run_wiki2xml.py
+++ b/hedtools/examples/run_wiki2xml.py
@@ -1,9 +1,12 @@
 """Example for how to convert from mediawiki format to xml format"""
 from shutil import move
-from hed.schema import wiki2xml, constants
+from hed.schema import wiki2xml
 
 if __name__ == '__main__':
-    local_hed_file = None
     hed_wiki_url = 'https://raw.githubusercontent.com/hed-standard/hed-specification/master/HED-generation3-schema.mediawiki'
+    local_hed_file = None
     xml_location, errors = wiki2xml.convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=local_hed_file)
-    move(xml_location, "output.xml")
\ No newline at end of file
+    for issue in errors:
+        print(issue['message'])
+    if xml_location:
+        move(xml_location, "output.xml")
\ No newline at end of file
diff --git a/hedtools/examples/run_xml2wiki.py b/hedtools/examples/run_xml2wiki.py
index 1119e26ed..fa28adbed 100644
--- a/hedtools/examples/run_xml2wiki.py
+++ b/hedtools/examples/run_xml2wiki.py
@@ -1,9 +1,12 @@
 """Example for how to convert from xml format to mediawiki format"""
 from shutil import move
-from hed.schema import xml2wiki, constants
+from hed.schema import xml2wiki
 
 if __name__ == '__main__':
     hed_xml_url = 'https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED7.1.1.xml'
     local_xml_file = None
-    xml_location, errors = xml2wiki.convert_hed_xml_2_wiki(hed_xml_url, local_xml_file)
-    move(xml_location, "output.mediawiki")
+    mediawiki_location, errors = xml2wiki.convert_hed_xml_2_wiki(hed_xml_url, local_xml_file)
+    for issue in errors:
+        print(issue['message'])
+    if mediawiki_location:
+        move(mediawiki_location, "HED7.1.1.mediawiki")
diff --git a/hedtools/hed/schema/duplicate_nodes.py b/hedtools/hed/schema/duplicate_nodes.py
deleted file mode 100644
index 143c1a446..000000000
--- a/hedtools/hed/schema/duplicate_nodes.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from hed.util.file_util import write_text_iter_to_file
-from hed.util.hed_dictionary import HedDictionary
-
-
-def check_for_duplicate_tags(local_xml_file):
-    """
-    Checks if a source XML file has duplicate tags.
-
-    If there are no duplicates, returns None.
-    If there are any duplicates, points to a file containing the formatted result.
-
-    Parameters
-    ----------
-    local_xml_file: string
-        path to local XML file to validate
-    Returns
-    -------
-    output_filename: str or None
-        location of text file containing list of duplicates
-    errors: list
-        list of errors in checking for duplicates    
-    """
-    hed_dict = HedDictionary(local_xml_file)
-    dupe_tag_file = None
-    if hed_dict.has_duplicate_tags():
-        dupe_tag_file = write_text_iter_to_file(hed_dict.dupe_tag_iter(True))
-    for line in hed_dict.dupe_tag_iter(True):
-        print(line)
-
-    errors = []
-    return dupe_tag_file, errors
diff --git a/hedtools/hed/schema/parsewiki.py b/hedtools/hed/schema/parsewiki.py
index 4c6ea1e92..c500eb4c5 100644
--- a/hedtools/hed/schema/parsewiki.py
+++ b/hedtools/hed/schema/parsewiki.py
@@ -4,6 +4,9 @@
 
 from xml.etree.ElementTree import Element, SubElement
 from hed.schema import parsetag, constants
+from hed.util.exceptions import SchemaFileError
+from hed.util.error_types import SchemaErrors
+
 import os
 
 ATTRIBUTE_DEFINITION_STRING = '\'\'\'Attribute Definitions:'
@@ -128,7 +131,7 @@ def hed_wiki_2_xml_tree(wiki_file_path):
 
     Parameters
     ----------
-    wiki_file_path: string
+    wiki_file_path: str
         The location of the HED wiki file.
 
     Returns
@@ -137,7 +140,7 @@ def hed_wiki_2_xml_tree(wiki_file_path):
         A XML element containing the entire HED.
     """
     hed_node.clear()
-    if os.path.exists(wiki_file_path):
+    try:
         with open(wiki_file_path, 'r', encoding='utf-8', errors='replace') as wiki_file:
             line = wiki_file.readline()
             while line:
@@ -152,6 +155,8 @@ def hed_wiki_2_xml_tree(wiki_file_path):
                     add_tags(wiki_file)
                     break
                 line = wiki_file.readline()
+    except FileNotFoundError as e:
+        raise SchemaFileError(SchemaErrors.FILE_NOT_FOUND, e.strerror, wiki_file_path)
     return hed_node
 
 
diff --git a/hedtools/hed/schema/schema_validator.py b/hedtools/hed/schema/schema_validator.py
new file mode 100644
index 000000000..51f34f4b4
--- /dev/null
+++ b/hedtools/hed/schema/schema_validator.py
@@ -0,0 +1,106 @@
+from hed.util.hed_schema import HedSchema
+from hed.util.error_reporter import format_schema_error, format_schema_warning, push_error_context, pop_error_context
+from hed.util.error_types import SchemaErrors, SchemaWarnings, ErrorContext
+from hed.util.exceptions import SchemaFileError
+
+ALLOWED_TAG_CHARS = "-"
+ALLOWED_DESC_CHARS = "-_:;,./()+ ^"
+
+
+def validate_schema(hed_xml_file, also_check_for_warnings=True):
+    """
+        Does validation of schema and returns a list of errors and warnings.
+
+    Parameters
+    ----------
+    hed_xml_file : str
+        filepath to a HED XML file to validate
+    also_check_for_warnings : bool, default True
+        If True, also checks for formatting issues like invalid characters, capitalization, etc.
+    Returns
+    -------
+    issue_list : [{}]
+        A list of all warnings and errors found in the file.
+    """
+    issues_list = []
+    try:
+        hed_dict = HedSchema(hed_xml_file)
+    except SchemaFileError as e:
+        return e.format_error_message()
+
+    push_error_context(ErrorContext.FILE_NAME, hed_xml_file)
+    if hed_dict.has_duplicate_tags():
+        duplicate_dict = hed_dict.find_duplicate_tags()
+        for tag_name, long_org_tags in duplicate_dict.items():
+            issues_list += format_schema_error(SchemaErrors.DUPLICATE_TERMS, tag_name, duplicate_tag_list=long_org_tags)
+
+    if also_check_for_warnings:
+        hed_terms = hed_dict.get_all_terms()
+        for hed_term in hed_terms:
+            issues_list += validate_schema_term(hed_term)
+
+        tag_descs = hed_dict.get_all_descriptions()
+        for tag_name, desc in tag_descs.items():
+            issues_list += validate_schema_description(tag_name, desc)
+
+    pop_error_context()
+    return issues_list
+
+
+def validate_schema_term(hed_term):
+    """
+    Takes a single term(ie short tag) and checks capitalization and illegal characters.
+
+    Parameters
+    ----------
+    hed_term : str
+        A single hed term
+    Returns
+    -------
+    issue_list: [{}]
+        A list of all formatting issues found in the term
+    """
+    issues_list = []
+    if hed_term == "#":
+        return issues_list
+
+    for i, char in enumerate(hed_term):
+        if i == 0 and not (char.isdigit() or char.isupper()):
+            issues_list += format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, hed_term,
+                                                 error_index=i, problem_char=char)
+            continue
+        if char in ALLOWED_TAG_CHARS:
+            continue
+        if char.isalnum():
+            continue
+        issues_list += format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_TAG, hed_term,
+                                             error_index=i, problem_char=char)
+    return issues_list
+
+
+def validate_schema_description(tag_name, hed_description):
+    """
+    Takes a single term description and returns a list of warnings and errors in it.
+
+    Parameters
+    ----------
+    tag_name : str
+        A single hed tag - not validated here, just used for error messages
+    hed_description: str
+        The description string to validate
+    Returns
+    -------
+    issue_list: [{}]
+        A list of all formatting issues found in the description
+    """
+    issues_list = []
+    # Blank description is fine
+    if not hed_description:
+        return issues_list
+    for i, char in enumerate(hed_description):
+        if char.isalnum():
+            continue
+        if char in ALLOWED_DESC_CHARS:
+            continue
+        issues_list += format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, tag_name, hed_description, i, char)
+    return issues_list
\ No newline at end of file
diff --git a/hedtools/hed/schema/wiki2xml.py b/hedtools/hed/schema/wiki2xml.py
index 6b116be8b..2cd232f69 100644
--- a/hedtools/hed/schema/wiki2xml.py
+++ b/hedtools/hed/schema/wiki2xml.py
@@ -3,30 +3,42 @@
 """
 from hed.util import file_util
 from hed.schema import parsewiki
+from hed.schema.schema_validator import validate_schema
+from hed.util.exceptions import SchemaFileError
 
 
-def convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=None):
+def convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=None, check_for_issues=True):
     """Converts the HED wiki into a XML file.
 
     Parameters
     ----------
-        hed_wiki_url: string
-            url pointing to the .mediawiki file to use
-        local_wiki_file: string
-            local wiki file to use(overrides hed_wiki_url)
+    hed_wiki_url: string or None
+        url pointing to the .mediawiki file to use
+    local_wiki_file: str or None
+        local wiki file to use(overrides hed_wiki_url)
+    check_for_issues : bool
+        After conversion checks for warnings like capitalization or invalid characters.
     Returns
     -------
-    dictionary
-        Contains xml, wiki, and version info.
+    xml_filename: str
+        Location of output XML file, None on complete failure
+    issues_list: [{}]
+        returns a list of error/warning dictionaries
     """
     if local_wiki_file is None:
         local_wiki_file = file_util.url_to_file(hed_wiki_url)
 
-    hed_xml_file_location = _create_hed_xml_file(local_wiki_file)
+    try:
+        hed_xml_file_location = _create_hed_xml_file(local_wiki_file)
+    except SchemaFileError as e:
+        return None, e.format_error_message()
 
-    # Placeholder
-    errors = []
-    return hed_xml_file_location, errors
+    issue_list = []
+    if check_for_issues:
+        warnings = validate_schema(hed_xml_file_location)
+        issue_list += warnings
+
+    return hed_xml_file_location, issue_list
 
 
 def _create_hed_xml_file(hed_wiki_file_path):
@@ -39,8 +51,8 @@ def _create_hed_xml_file(hed_wiki_file_path):
 
     Returns
     -------
-    tuple
-        A tuple containing the path to the HED XML file and a HED XML tree.
+    filename: str
+        Path to hed XML file
     """
     hed_xml_tree = parsewiki.hed_wiki_2_xml_tree(hed_wiki_file_path)
     hed_xml_file_location = file_util.write_xml_tree_2_xml_file(hed_xml_tree)
diff --git a/hedtools/hed/schema/xml2wiki.py b/hedtools/hed/schema/xml2wiki.py
index 82bca302f..fa7db4a4e 100644
--- a/hedtools/hed/schema/xml2wiki.py
+++ b/hedtools/hed/schema/xml2wiki.py
@@ -2,8 +2,9 @@
 
 from hed.schema import constants
 from hed.util import file_util
-from hed.util.hed_dictionary import HedDictionary
-
+from hed.util.hed_schema import HedSchema
+from hed.util.exceptions import SchemaFileError
+from hed.schema.schema_validator import validate_schema
 
 class MainParseMode(Enum):
     MainTags = 1
@@ -184,27 +185,39 @@ def get_attribs_from_root_hed_node(elem):
         return final_attrib_string
 
 
-def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None):
+def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None, check_for_issues=True):
     """Converts the local HED xml file into a wikimedia file
 
     Parameters
     ----------
-        hed_xml_url: string
-            url pointing to the .xml file to use
-        local_xml_file: string
-            filepath to local xml hed schema(overrides hed_xml_url)
+    hed_xml_url: str or None
+        url pointing to the .xml file to use
+    local_xml_file: str or None
+        filepath to local xml hed schema(overrides hed_xml_url)
+    check_for_issues : bool
+        After conversion checks for warnings like capitalization or invalid characters.
     Returns
     -------
-    dictionary
-        Contains xml, wiki, and version info.
+    mediawiki_filename: str
+        Location of output mediawiki file, None on complete failure
+    issues_list: [{}]
+        returns a list of error/warning dictionaries
     """
     if local_xml_file is None:
         local_xml_file = file_util.url_to_file(hed_xml_url)
 
-    hed_xml_tree = HedDictionary.parse_hed_xml_file(local_xml_file)
+    try:
+        hed_xml_tree = HedSchema.parse_hed_xml_file(local_xml_file)
+    except SchemaFileError as e:
+        return None, e.format_error_message()
+
     xml2wiki = HEDXml2Wiki()
     output_strings = xml2wiki.process_tree(hed_xml_tree)
     local_mediawiki_file = file_util.write_strings_to_file(output_strings, ".mediawiki")
 
-    errors = []
-    return local_mediawiki_file, errors
+    issue_list = []
+    if check_for_issues:
+        warnings = validate_schema(local_xml_file)
+        issue_list += warnings
+
+    return local_mediawiki_file, issue_list
diff --git a/hedtools/hed/tools/map_tag_versions.py b/hedtools/hed/tools/map_tag_versions.py
index 60a37ac7a..6e5955311 100644
--- a/hedtools/hed/tools/map_tag_versions.py
+++ b/hedtools/hed/tools/map_tag_versions.py
@@ -1,7 +1,7 @@
 import re
 import copy
 from hed.util.hed_string_util import split_hed_string, remove_slashes_and_spaces
-from hed.util.hed_dictionary import HedDictionary, HedKey
+from hed.util.hed_schema import HedSchema, HedKey
 
 # Todo: Review this file once we have more stable test cases for it.
 
@@ -42,9 +42,9 @@ def read_version_map(version_map_filename, left_hed_schema=None, right_hed_schem
                      return_errors=False):
     left_dict = right_dict = None
     if left_hed_schema:
-        left_dict = HedDictionary(left_hed_schema)
+        left_dict = HedSchema(left_hed_schema)
     if right_hed_schema:
-        right_dict = HedDictionary(right_hed_schema)
+        right_dict = HedSchema(right_hed_schema)
     mapping_dict = {}
     error_list = []
 
diff --git a/hedtools/hed/tools/tag_format.py b/hedtools/hed/tools/tag_format.py
index fb1c5695b..179ada83b 100644
--- a/hedtools/hed/tools/tag_format.py
+++ b/hedtools/hed/tools/tag_format.py
@@ -1,7 +1,7 @@
 from hed.util import error_reporter
-from hed.util.error_types import SchemaErrors
+from hed.util.error_types import SchemaErrors, ErrorContext
 from hed.util import hed_string_util
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 
 
 class TagFormat:
@@ -9,19 +9,19 @@ class TagFormat:
     Class to convert hed3 tags between short and long form.
     """
 
-    def __init__(self, hed_xml_file=None, hed_dictionary=None):
+    def __init__(self, hed_xml_file=None, hed_schema=None):
         """
 
         Parameters
         ----------
         hed_xml_file : str
-            hed xml schema filepath to create HedDictionary from
-        hed_dictionary : HedDictionary, default None
+            hed xml schema filepath to create HedSchema from
+        hed_schema : HedSchema, default None
              Used in place of hed_xml_file
         """
-        if hed_dictionary is None:
-            hed_dictionary = HedDictionary(hed_xml_file)
-        self._short_tag_mapping = hed_dictionary.short_tag_mapping
+        if hed_schema is None:
+            hed_schema = HedSchema(hed_xml_file)
+        self._short_tag_mapping = hed_schema.short_tag_mapping
 
     def convert_hed_string_to_short(self, hed_string):
         """
@@ -297,14 +297,13 @@ def _convert_file(input_file, conversion_function):
         error_list = []
         for row_number, row_hed_string, column_to_hed_tags_dictionary in input_file:
             for column_number in column_to_hed_tags_dictionary:
+                error_reporter.push_error_context(ErrorContext.COLUMN, row_number, column_context=column_number)
                 old_text = column_to_hed_tags_dictionary[column_number]
                 new_text, errors = conversion_function(old_text)
                 input_file.set_cell(row_number, column_number, new_text,
                                     include_column_prefix_if_exist=False)
 
-                for error in errors:
-                    error_reporter.add_row_and_column(error, row_number, column_number)
-                    error_list.append(error)
+                error_reporter.pop_error_context()
 
         return input_file, error_list
 
diff --git a/hedtools/hed/util/column_def_group.py b/hedtools/hed/util/column_def_group.py
index 195804449..46bdf1636 100644
--- a/hedtools/hed/util/column_def_group.py
+++ b/hedtools/hed/util/column_def_group.py
@@ -1,7 +1,7 @@
 import json
 from hed.util.column_definition import ColumnDef
-from hed.util.error_types import SidecarErrors
-from hed.util.error_reporter import format_sidecar_error
+from hed.util.error_types import SidecarErrors, ErrorContext
+from hed.util.error_reporter import format_sidecar_error, push_error_context, pop_error_context
 
 
 class ColumnDefGroup:
@@ -51,7 +51,6 @@ def add_json_file_defs(self, json_filename):
                 loaded_defs = json.load(fp)
                 for col_def, col_dict in loaded_defs.items():
                     self._add_single_col_type(col_def, col_dict)
-            self.save_as_json(json_filename + "test_json_out.json")
 
         except FileNotFoundError:
             self._validation_issues[json_filename] = format_sidecar_error(SidecarErrors.INVALID_FILENAME,
@@ -183,13 +182,16 @@ def get_validation_issues(self):
         """
         return self._validation_issues
 
-    def validate_entries(self, hed_dictionary=None):
-        """Validate the column entries, and also hed strings in the column entries if a hed_dictionary is passed.
+    def validate_entries(self, hed_schema=None, display_filename=None):
+        """Validate the column entries, and also hed strings in the column entries if a hed_schema is passed.
 
         Parameters
         ----------
-        hed_dictionary : HedDictionary, optional
+        hed_schema : HedSchema, optional
             The dictionary to use to validate individual hed strings.
+        display_filename: str
+            If present, it will display errors as coming from this filename instead of the actual source.
+            Useful for temporary files and similar.
         Returns
         -------
         validation_issues: [{}]
@@ -199,14 +201,16 @@ def validate_entries(self, hed_dictionary=None):
         # If we already have an issue we either already validated, or have a file io error
         if not self._validation_issues:
             key_validation_issues = {}
+            if not display_filename:
+                display_filename = self._json_filename
+            push_error_context(ErrorContext.FILE_NAME, display_filename, False)
             for column_entry in self:
-                col_validation_issues = column_entry.validate_column_entry(hed_dictionary)
+                push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_entry.column_name)
+                col_validation_issues = column_entry.validate_column_entry(hed_schema)
                 if col_validation_issues:
-                    if self._json_filename:
-                        col_validation_issues = format_sidecar_error(SidecarErrors.SIDECAR_FILE_NAME,
-                                                                     filename=self._json_filename) \
-                                                + col_validation_issues
                     key_validation_issues[column_entry.column_name] = col_validation_issues
 
+                pop_error_context()
             self._validation_issues = key_validation_issues
+            pop_error_context(False)
         return self._validation_issues
diff --git a/hedtools/hed/util/column_definition.py b/hedtools/hed/util/column_definition.py
index bc705ac42..d8aa19def 100644
--- a/hedtools/hed/util/column_definition.py
+++ b/hedtools/hed/util/column_definition.py
@@ -1,7 +1,7 @@
 from enum import Enum
 from hed.util.hed_string_util import split_hed_string, split_hed_string_return_strings
-from hed.util.error_types import SidecarErrors
-from hed.util.error_reporter import format_sidecar_error
+from hed.util.error_types import SidecarErrors, ErrorContext
+from hed.util.error_reporter import format_sidecar_error, push_error_context, pop_error_context
 from hed.validator.tag_validator import TagValidator
 
 
@@ -281,13 +281,13 @@ def _detect_column_def_type(dict_for_entry):
 
         return ColumnType.Value
 
-    def validate_column_entry(self, hed_dictionary=None):
+    def validate_column_entry(self, hed_schema=None):
         """
         Finds all validation issues in this column.
 
         Parameters
         ----------
-        hed_dictionary : HedDictionary, optional
+        hed_schema : HedSchema, optional
             The dictionary to use to validate hed_strings.  If absent, will only validate column syntax.
 
         Returns
@@ -295,11 +295,13 @@ def validate_column_entry(self, hed_dictionary=None):
         column_issues: [{}]
         """
         col_validation_issues = []
-        # Hed string validation can only be done with a hed_dictionary
-        if hed_dictionary and self.hed_string_iter():
-            tag_validator = TagValidator(hed_dictionary, check_for_warnings=True, run_semantic_validation=True,
+        # Hed string validation can only be done with a hed_schema
+        if hed_schema and self.hed_string_iter():
+            tag_validator = TagValidator(hed_schema, check_for_warnings=True, run_semantic_validation=True,
                                          allow_numbers_to_be_pound_sign=True)
             for hed_string, position in self.hed_string_iter(include_position=True):
+                push_error_context(ErrorContext.SIDECAR_CUE_NAME, position)
+                push_error_context(ErrorContext.SIDECAR_HED_STRING, hed_string, False)
                 new_col_validation_issues = tag_validator.run_hed_string_validators(hed_string)
                 for hed_tag in split_hed_string_return_strings(hed_string):
                     hed_tag_lower = hed_tag.lower()
@@ -307,10 +309,13 @@ def validate_column_entry(self, hed_dictionary=None):
                     if tag_issues:
                         new_col_validation_issues += tag_issues
                 if new_col_validation_issues:
-                    new_col_validation_issues = format_sidecar_error(SidecarErrors.SIDECAR_HED_STRING,
-                                                                     hed_string=hed_string,
-                                                                     position=position) + new_col_validation_issues
                     col_validation_issues += new_col_validation_issues
+                if self.column_type == ColumnType.Categorical:
+                    if hed_string.count("#") != 0:
+                        col_validation_issues += format_sidecar_error(SidecarErrors.TOO_MANY_POUND_SIGNS,
+                                                                      pound_sign_count=hed_string.count("#"))
+                pop_error_context(False)
+                pop_error_context()
 
         if self.column_type is None:
             col_validation_issues += format_sidecar_error(SidecarErrors.UNKNOWN_COLUMN_TYPE,
@@ -341,12 +346,12 @@ def validate_column_entry(self, hed_dictionary=None):
                     col_validation_issues += format_sidecar_error(SidecarErrors.TOO_FEW_CATEGORIES,
                                                                   category_count=len(raw_hed_dict))
 
-                for key, value in raw_hed_dict.items():
-                    if value.count("#") != 0:
-                        col_validation_issues += format_sidecar_error(SidecarErrors.TOO_MANY_POUND_SIGNS,
-                                                                      pound_sign_count=value.count("#"))
-        if col_validation_issues:
-            col_validation_issues = format_sidecar_error(SidecarErrors.SIDECAR_COLUMN_NAME,
-                                                         column_name=self.column_name) \
-                                    + col_validation_issues
+                # Handle this error here instead of above if we don't have a dictionary for hed_string validation
+                if hed_schema is None:
+                    for key, value in raw_hed_dict.items():
+                        if value.count("#") != 0:
+                            col_validation_issues += format_sidecar_error(SidecarErrors.TOO_MANY_POUND_SIGNS,
+                                                                          pound_sign_count=value.count("#"))
+
+
         return col_validation_issues
diff --git a/hedtools/hed/util/column_mapper.py b/hedtools/hed/util/column_mapper.py
index 4eb500503..562e493a4 100644
--- a/hedtools/hed/util/column_mapper.py
+++ b/hedtools/hed/util/column_mapper.py
@@ -9,7 +9,7 @@ class ColumnMapper:
         Private Functions and variables column and row indexing starts at 0.
         Public functions and variables indexing starts at 1(or 2 if has column names)"""
     def __init__(self, json_def_files=None, tag_columns=None, column_prefix_dictionary=None,
-                 hed_dictionary=None, attribute_columns=None, definition_mapper=None):
+                 hed_schema=None, attribute_columns=None, definition_mapper=None):
         """Constructor for ColumnMapper
 
         Parameters
@@ -25,7 +25,7 @@ def __init__(self, json_def_files=None, tag_columns=None, column_prefix_dictiona
             4: 'Event/Label/', 5: 'Event/Category/'} The third column contains tags that need Event/Description/
             prepended to them, the fourth column contains tags that need Event/Label/ prepended to them,
             and the fifth column contains tags that needs Event/Category/ prepended to them.
-        hed_dictionary: HedDictionary
+        hed_schema: HedSchema
             Used to create a TagValidator, which is then used to validate the entries in value and category entries.
         attribute_columns: str or int or [str] or [int]
              A list of column names or numbers to treat as attributes.
@@ -44,7 +44,7 @@ def __init__(self, json_def_files=None, tag_columns=None, column_prefix_dictiona
         self._def_mapper = definition_mapper
 
         self._na_patterns = ["n/a", "nan"]
-        self._hed_dictionary = hed_dictionary
+        self._hed_schema = hed_schema
 
         if json_def_files:
             self.add_json_file_defs(json_def_files)
diff --git a/hedtools/hed/util/def_mapper.py b/hedtools/hed/util/def_mapper.py
index 826729cf0..ac48b6db2 100644
--- a/hedtools/hed/util/def_mapper.py
+++ b/hedtools/hed/util/def_mapper.py
@@ -15,24 +15,24 @@ class DefinitionMapper:
     DEF_KEY = "definition"
     ORG_KEY = "organizational"
 
-    def __init__(self, hed_inputs=None, hed_dictionary=None):
+    def __init__(self, hed_inputs=None, hed_schema=None):
         """
 
         Parameters
         ----------
         hed_inputs : [] or str or BaseFileInput or ColumnDefGroup
             List input doesn't need to all be the same type.
-        hed_dictionary : HedDictionary, optional
+        hed_schema : HedSchema, optional
             Used to determine where definition tags are in the schema.  This is technically optional, but
             only short form definition tags will work if this is absent.
         """
         self._defs = {}
-        self._short_tag_mapping = hed_dictionary.short_tag_mapping
+        self._short_tag_mapping = hed_schema.short_tag_mapping
 
-        if hed_dictionary:
-            self._def_tag_versions = hed_dictionary.get_all_forms_of_tag(self.DEF_KEY)
-            self._org_tag_versions = hed_dictionary.get_all_forms_of_tag(self.ORG_KEY)
-            self._label_tag_versions = hed_dictionary.get_all_forms_of_tag(self.DLABEL_KEY)
+        if hed_schema:
+            self._def_tag_versions = hed_schema.get_all_forms_of_tag(self.DEF_KEY)
+            self._org_tag_versions = hed_schema.get_all_forms_of_tag(self.ORG_KEY)
+            self._label_tag_versions = hed_schema.get_all_forms_of_tag(self.DLABEL_KEY)
             if not self._label_tag_versions:
                 self._label_tag_versions = [self.DLABEL_KEY + "/"]
         else:
diff --git a/hedtools/hed/util/error_reporter.py b/hedtools/hed/util/error_reporter.py
index c6ef1a499..012b3d0fa 100644
--- a/hedtools/hed/util/error_reporter.py
+++ b/hedtools/hed/util/error_reporter.py
@@ -1,13 +1,126 @@
 """
 This module is used to report errors found in the validation.
 
+You can scope the formatted errors with calls to push_error_context and pop_error_context.
 """
 
-from hed.util.error_types import ValidationErrors, ValidationWarnings, SchemaErrors, SidecarErrors
+from hed.util.error_types import ValidationErrors, ValidationWarnings, SchemaErrors, \
+    SidecarErrors, SchemaWarnings, ErrorContext
 
-# Todo: Update doc strings in this file
+# Number of tabs to insert at the start of each line.  Incremented when you push a new error_context.
+current_tab_depth = 0
+# A list of strings containing the current error context scope such as row or column number.
+# Starts from index 0 being the largest(eg file usually) scope.
+error_context_stack = []
+# A copy of the last error_context_stack used to actually format an error.
+# This is used to allow us to only display the context once, regardless of how many errors there are.
+last_used_error_context = []
 
-def format_val_error(error_type, error_row=1, error_column=1, hed_string='', tag='', tag_prefix='', previous_tag='',
+
+def push_error_context(context_type, context, increment_depth_after=True, column_context=None):
+    """
+    Pushes a new error context to the end of the stack to narrow down error scope.
+
+    Parameters
+    ----------
+    context_type : str
+        This should be a value from ErrorContext representing the type of scope.
+    context : str or int
+        The main value for the context_type.  eg for ErrorContext.FILE_NAME this would be the actual filename.
+    increment_depth_after : bool
+        If True, this adds an extra tab to any subsequent errors in the scope.
+    column_context : str or int
+        This is only used by ErrorContext.COLUMN, where context is the row and column_context is the column number.
+    Returns
+    -------
+    """
+    global current_tab_depth
+    default_tabbing = "\t" * current_tab_depth
+
+    error_types = {
+        ErrorContext.FILE_NAME: f"Errors in file '{context}'",
+        ErrorContext.SIDECAR_COLUMN_NAME: f"Column '{context}':",
+        ErrorContext.SIDECAR_CUE_NAME: f"Cue: {context}",
+        ErrorContext.SIDECAR_HED_STRING: f"hed_string: {context}",
+        ErrorContext.ROW: f'Issues in row {context}:',
+        ErrorContext.COLUMN: f'Issues in row {context} column {column_context}:'
+    }
+
+    default_context = 'ERROR: Unknown error context'
+    context_message = error_types.get(context_type, default_context)
+
+    error_context_stack.append(f"{default_tabbing}{context_message}")
+    if increment_depth_after:
+        current_tab_depth += 1
+
+
+def pop_error_context(decrement_depth_after=True):
+    """
+    Removes the last scope from the error context.
+
+    Parameters
+    ----------
+    decrement_depth_after : bool
+        This should always match the value passed to push_error_context.
+
+    Returns
+    -------
+    """
+    global current_tab_depth
+    if decrement_depth_after:
+        current_tab_depth -= 1
+    error_context_stack.pop()
+
+
+def reset_error_context():
+    """Reset all error context information to defaults
+
+    This function should not be needed with proper usage."""
+    global current_tab_depth
+    current_tab_depth = 0
+    global error_context_stack
+    error_context_stack = []
+    global last_used_error_context
+    last_used_error_context = []
+
+
+def _add_context_to_errors(error_object):
+    """
+    Takes an error object and adds relevant context around it, such as row number, or column name.
+
+    Parameters
+    ----------
+    error_object : {}
+        Generated error containing at least a code and message entry.
+
+    Returns
+    -------
+    error_object_list: [{}]
+        The passed in error with any needed context strings added to the start.
+    """
+    global last_used_error_context
+
+    # The context has not changed, no need to add new context text
+    if last_used_error_context == error_context_stack:
+        return [error_object]
+
+    error_context = []
+    for i, context in enumerate(error_context_stack):
+        if len(last_used_error_context) > i:
+            last_drawn = last_used_error_context[i]
+            # Was drawn, and hasn't changed.
+            if last_drawn == context:
+                continue
+
+        context_object = {'code': 'context', 'message': context}
+        error_context.append(context_object)
+
+    last_used_error_context = error_context_stack.copy()
+
+    return error_context + [error_object]
+
+
+def format_val_error(error_type, hed_string='', tag='', tag_prefix='', previous_tag='',
                      character='', index=0, unit_class_units='', file_name='', opening_parentheses_count=0,
                      closing_parentheses_count=0):
     """Reports the abc error based on the type of error.
@@ -16,10 +129,6 @@ def format_val_error(error_type, error_row=1, error_column=1, hed_string='', tag
     ----------
     error_type: str
         The type of abc error.
-    error_row: int
-        The row number that the error occurred on.
-    error_column: int
-        The column number that the error occurred on.
     hed_string: str
         The full HED string in which the error occurred
     tag: str
@@ -47,57 +156,82 @@ def format_val_error(error_type, error_row=1, error_column=1, hed_string='', tag
         of warning.
 
     """
-
+    default_tabbing = "\t" * current_tab_depth
+    error_prefix = default_tabbing + "ERROR: "
     error_types = {
-        ValidationErrors.ROW: 'Issues in row %s:' % str(error_row),
-        ValidationErrors.COLUMN: 'Issues in row %s column %s:' % (str(error_row), str(error_column)),
-        ValidationErrors.INVALID_FILENAME: '\tInvalid file name - "%s"' % file_name,
-        ValidationErrors.PARENTHESES: '\tERROR: Number of opening and closing parentheses are unequal. %s opening parentheses. %s '
-                                      'closing parentheses' % (opening_parentheses_count, closing_parentheses_count),
-        ValidationErrors.INVALID_CHARACTER: '\tERROR: Invalid character "%s" at index %s of string "%s"'
-                                            % (character, index, hed_string),
-        ValidationErrors.COMMA_MISSING: '\tERROR: Comma missing after - "%s"' % tag,
-        ValidationErrors.INVALID_COMMA: '\tERROR: Either "%s" contains a comma when it should not or "%s" is not a valid '
-                                        'tag ' % (previous_tag, tag),
-        ValidationErrors.DUPLICATE: '\tERROR: Duplicate tag - "%s"' % tag,
-        ValidationErrors.REQUIRE_CHILD: '\tERROR: Descendant tag required - "%s"' % tag,
-        ValidationErrors.EXTRA_TILDE: '\tERROR: Too many tildes - group "%s"' % tag,
-        ValidationErrors.MULTIPLE_UNIQUE: '\tERROR: Multiple unique tags with prefix - "%s"' % tag_prefix,
-        ValidationErrors.UNIT_CLASS_INVALID_UNIT: '\tERROR: Invalid unit - "%s" valid units are "%s"' % (tag, unit_class_units),
-        ValidationErrors.INVALID_TAG: '\tERROR: Invalid tag - "%s"' % tag,
-        ValidationErrors.EXTRA_DELIMITER: '\tERROR: Extra delimiter "%s" at index %s of string "%s"'
-                                          % (character, index, hed_string),
+        ValidationErrors.INVALID_FILENAME: f'Invalid file name - "{file_name}"',
+        ValidationErrors.PARENTHESES: f'{error_prefix}Number of opening and closing parentheses are unequal. '
+                                      f'{opening_parentheses_count} opening parentheses. {closing_parentheses_count} '
+                                      'closing parentheses',
+        ValidationErrors.INVALID_CHARACTER: f'{error_prefix}Invalid character "{character}" at index {index} of string "{hed_string}"',
+        ValidationErrors.COMMA_MISSING: f'{error_prefix}Comma missing after - "{tag}"',
+        ValidationErrors.INVALID_COMMA: f'{error_prefix}Either "{previous_tag}" contains a comma when it should not or "{tag}" is not a valid '
+                                        'tag ',
+        ValidationErrors.DUPLICATE: f'{error_prefix}Duplicate tag - "{tag}"',
+        ValidationErrors.REQUIRE_CHILD: f'{error_prefix}Descendant tag required - "{tag}"',
+        ValidationErrors.EXTRA_TILDE: f'{error_prefix}Too many tildes - group "{tag}"',
+        ValidationErrors.MULTIPLE_UNIQUE: f'{error_prefix}Multiple unique tags with prefix - "{tag_prefix}"',
+        ValidationErrors.UNIT_CLASS_INVALID_UNIT: f'{error_prefix}Invalid unit - "{tag}" valid units are "{unit_class_units}"',
+        ValidationErrors.INVALID_TAG: f'{error_prefix}Invalid tag - "{tag}"',
+        ValidationErrors.EXTRA_DELIMITER: f'{error_prefix}Extra delimiter "{character}" at index {index} of string "{hed_string}"',
     }
     default_error_message = 'ERROR: Unknown error'
     error_message = error_types.get(error_type, default_error_message)
 
     error_object = {'code': error_type, 'message': error_message}
-    return [error_object]
+    error_object_list = _add_context_to_errors(error_object)
+    return error_object_list
 
 
 def format_sidecar_error(error_type, filename="", column_name="", given_type="", expected_type="", pound_sign_count=0,
-                         category_count=0, hed_string="", position=None):
-    ERROR_PREFIX = "\tERROR: "
+                         category_count=0):
+    """
+    Formats a json sidecar error to human readable
+
+    Parameters
+    ----------
+    error_type : str
+        This should be a value from SidecarErrors
+    filename : str
+        The filename this error is from
+    column_name : str
+        The column name this error is from
+    given_type : str
+        A str representing the type of the variable there is an error with
+    expected_type : str
+        A str representing the expected type of the variable there is an error with
+    pound_sign_count : int
+        The number of pound signs found in the string.
+    category_count : int
+        The number of categories found in this column.
+
+    Returns
+    -------
+    issue_list: [{}]
+        A list containing a single dictionary with the warning type and warning message related to a particular type
+        of warning.
+    """
+    default_tabbing = "\t" * current_tab_depth
+    error_prefix = default_tabbing + "ERROR: "
     error_types = {
-        SidecarErrors.SIDECAR_FILE_NAME: f"Errors in file '{filename}'",
-        SidecarErrors.SIDECAR_COLUMN_NAME: f"Errors in column '{column_name}':",
-        SidecarErrors.SIDECAR_HED_STRING: f"Errors in hed string at position {position}: {hed_string}",
-        SidecarErrors.INVALID_FILENAME: f"ERROR: File does not exist or cannot be opened. '{filename}'",
-        SidecarErrors.CANNOT_PARSE_JSON: f"ERROR: Json file cannot be parsed. '{filename}'",
-        SidecarErrors.BLANK_HED_STRING: f"{ERROR_PREFIX}No HED string found for Value or Category column.",
-        SidecarErrors.WRONG_HED_DATA_TYPE: f"{ERROR_PREFIX}Invalid HED string datatype sidecar. Should be '{expected_type}', but got '{given_type}'",
-        SidecarErrors.INVALID_NUMBER_POUND_SIGNS: f"{ERROR_PREFIX}There should be exactly one # character in a sidecar string. Found {pound_sign_count}",
-        SidecarErrors.TOO_MANY_POUND_SIGNS: f"{ERROR_PREFIX}There should be no # characters in a category sidecar string. Found {pound_sign_count}",
-        SidecarErrors.TOO_FEW_CATEGORIES: f"{ERROR_PREFIX}A category column should have at least two keys. Found {category_count}",
-        SidecarErrors.UNKNOWN_COLUMN_TYPE: f"{ERROR_PREFIX}Could not automatically identify column '{column_name}' type from file. "
+        SidecarErrors.INVALID_FILENAME: f"{error_prefix}File does not exist or cannot be opened. '{filename}'",
+        SidecarErrors.CANNOT_PARSE_JSON: f"{error_prefix}Json file cannot be parsed. '{filename}'",
+        SidecarErrors.BLANK_HED_STRING: f"{error_prefix}No HED string found for Value or Category column.",
+        SidecarErrors.WRONG_HED_DATA_TYPE: f"{error_prefix}Invalid HED string datatype sidecar. Should be '{expected_type}', but got '{given_type}'",
+        SidecarErrors.INVALID_NUMBER_POUND_SIGNS: f"{error_prefix}There should be exactly one # character in a sidecar string. Found {pound_sign_count}",
+        SidecarErrors.TOO_MANY_POUND_SIGNS: f"{error_prefix}There should be no # characters in a category sidecar string. Found {pound_sign_count}",
+        SidecarErrors.TOO_FEW_CATEGORIES: f"{error_prefix}A category column should have at least two cues. Found {category_count}",
+        SidecarErrors.UNKNOWN_COLUMN_TYPE: f"{error_prefix}Could not automatically identify column '{column_name}' type from file. "
                                           f"Most likely the column definition in question needs a # sign to replace a number somewhere."
     }
 
-    default_error_message = f'\tERROR: Unknown error {error_type}'
+    default_error_message = f'{error_prefix}Unknown error {error_type}'
     error_message = error_types.get(error_type, default_error_message)
 
     error_object = {'code': error_type, 'message': error_message}
-    return [error_object]
+
+    error_object_list = _add_context_to_errors(error_object)
+    return error_object_list
 
 
 def format_val_warning(warning_type, tag='', default_unit='', tag_prefix=''):
@@ -120,40 +254,70 @@ def format_val_warning(warning_type, tag='', default_unit='', tag_prefix=''):
         of warning.
 
     """
+    default_tabbing = "\t" * current_tab_depth
+    warning_prefix = default_tabbing + "WARNING: "
+
     warning_types = {
-        ValidationWarnings.CAPITALIZATION: '\tWARNING: First word not capitalized or camel case - "%s"' % tag,
-        ValidationWarnings.REQUIRED_PREFIX_MISSING: '\tWARNING: Tag with prefix "%s" is required' % tag_prefix,
-        ValidationWarnings.UNIT_CLASS_DEFAULT_USED: '\tWARNING: No unit specified. Using "%s" as the default - "%s"' % (
-        default_unit, tag)
+        ValidationWarnings.CAPITALIZATION: f'{warning_prefix}First word not capitalized or camel case - "{tag}"',
+        ValidationWarnings.REQUIRED_PREFIX_MISSING: f'{warning_prefix}Tag with prefix "{tag_prefix}" is required',
+        ValidationWarnings.UNIT_CLASS_DEFAULT_USED: f'{warning_prefix}No unit specified. Using "{default_unit}" as the default - "{tag}"'
     }
     default_warning_message = 'WARNING: Unknown warning'
     warning_message = warning_types.get(warning_type, default_warning_message)
 
     warning_object = {'code': warning_type, 'message': warning_message}
-    return [warning_object]
+    warning_object_list = _add_context_to_errors(warning_object)
+    return warning_object_list
 
 
-SCHEMA_ERROR_PREFIX = "ERROR: "
-
+def reformat_schema_error(error, hed_string, offset):
+    """
+    Updates a schema error from hed_tag indexing to hed_string indexing.
+    
+    Parameters
+    ----------
+    error : {}
+        An error returned from format_schema_error
+    hed_string : str
+        The full hed_string the error is from.
+    offset : int
+        The index into hed_string where the reported error hed_tag started
+    Returns
+    -------
+    {}
+        The same error with index changed if it was a schema error.
+    """
+    # Bail early if this isn't a schema error
+    if "start_index" not in error:
+        return error
 
-def reformat_schema_error(error, source_string, offset):
-    """Updates a schema error from hed_tag indexing to hed_string indexing."""
     error_type = error["code"]
     error_index = error["start_index"]
     error_index_end = error["end_index"]
     expected_parent_tag = error["expected_parent_tag"]
 
-    reformatted_error = format_schema_error(error_type, source_string, error_index + offset, error_index_end + offset, expected_parent_tag)
+    reformatted_error = format_schema_error(error_type, hed_string, error_index + offset, error_index_end + offset, expected_parent_tag)
     return reformatted_error
 
 
-def format_schema_error(error_type, hed_tag, error_index=0, error_index_end=None, expected_parent_tag=None):
+def format_schema_error(error_type, hed_tag, error_index=0, error_index_end=None, expected_parent_tag=None,
+                        duplicate_tag_list=()):
     """Reports the abc error based on the type of error.
 
     Parameters
     ----------
     error_type: str
         The type of abc error.
+    hed_tag: str
+        The hed tag with a problem
+    error_index: int
+        The index where the error starts
+    error_index_end: int
+        The index where the error stops
+    expected_parent_tag: str
+        The expected full path of the tag from the schema.
+    duplicate_tag_list: []
+        A list of all all possible parents for this tag, if there is more than one.
     Returns
     -------
     issue_list: [{}]
@@ -166,15 +330,21 @@ def format_schema_error(error_type, hed_tag, error_index=0, error_index_end=None
 
     problem_tag = hed_tag[error_index: error_index_end]
 
+    default_tabbing = "\t" * current_tab_depth
+    error_prefix = f"{default_tabbing}ERROR: "
+
+    tag_join_delimiter = f"\n\t{default_tabbing}"
     error_types = {
-        SchemaErrors.INVALID_PARENT_NODE: f"{SCHEMA_ERROR_PREFIX}'{problem_tag}' appears as '{expected_parent_tag}' and cannot be used "
+        SchemaErrors.INVALID_PARENT_NODE: f"{error_prefix}'{problem_tag}' appears as '{expected_parent_tag}' and cannot be used "
                                           f"as an extension.  {error_index}, {error_index_end}",
-        SchemaErrors.NO_VALID_TAG_FOUND: f"{SCHEMA_ERROR_PREFIX}'{problem_tag}' is not a valid base hed tag.  {error_index}, {error_index_end} ",
-        SchemaErrors.EMPTY_TAG_FOUND: f"{SCHEMA_ERROR_PREFIX}Empty tag cannot be converted.",
-        SchemaErrors.INVALID_SCHEMA: f"{SCHEMA_ERROR_PREFIX}Source hed schema is invalid as it contains duplicate tags.  "
-                                     f"Please fix if you wish to be abe to convert tags. {error_index}, {error_index_end}"
+        SchemaErrors.NO_VALID_TAG_FOUND: f"{error_prefix}'{problem_tag}' is not a valid base hed tag.  {error_index}, {error_index_end} ",
+        SchemaErrors.EMPTY_TAG_FOUND: f"{error_prefix}Empty tag cannot be converted.",
+        SchemaErrors.INVALID_SCHEMA: f"{error_prefix}Source hed schema is invalid as it contains duplicate tags.  "
+                                     f"Please fix if you wish to be abe to convert tags. {error_index}, {error_index_end}",
+        SchemaErrors.DUPLICATE_TERMS: f"{error_prefix}Term(Short Tag) '{hed_tag}' used {len(duplicate_tag_list)} places in schema as: {tag_join_delimiter}"
+                                      f"{tag_join_delimiter.join(duplicate_tag_list)}"
     }
-    default_error_message = f'{SCHEMA_ERROR_PREFIX}Internal Error'
+    default_error_message = f'{error_prefix}Internal Error'
     error_message = error_types.get(error_type, default_error_message)
 
     error_object = {'code': error_type,
@@ -184,18 +354,51 @@ def format_schema_error(error_type, hed_tag, error_index=0, error_index_end=None
                     'end_index': error_index_end,
                     'expected_parent_tag': expected_parent_tag}
 
-    # Debug printing
-    # print(f"{hed_string}")
-    # print(error_message)
+    error_object_list = _add_context_to_errors(error_object)
+    return error_object_list
+
+
+def format_schema_warning(error_type, hed_tag, hed_desc=None, error_index=0, problem_char=None):
+    """Reports the abc warning based on the type of error.
+
+    Parameters
+    ----------
+    error_type: str
+        The type of abc error.
+    hed_tag: str
+        The hed tag this error is from
+    hed_desc: str
+        The description this error is from
+    error_index: int
+        The position of the character with a problem
+    problem_char: char
+        The invalid character
+    Returns
+    -------
+    issue_list: [{}]
+        A list containing a single dictionary with the warning type and warning message related to a particular type
+        of warning.
+
+    """
+    problem_tag = hed_tag
 
-    return [error_object]
+    default_tabbing = "\t" * current_tab_depth
+    warning_prefix = f"{default_tabbing}WARNING: "
+    error_types = {
+        SchemaWarnings.INVALID_CHARACTERS_IN_DESC: f"{warning_prefix}Invalid character '{problem_char}' in desc "
+                                                   f"for '{problem_tag}' at position {error_index}.  '{hed_desc}",
+        SchemaWarnings.INVALID_CHARACTERS_IN_TAG: f"{warning_prefix}Invalid character '{problem_char}' in tag "
+                                                   f"'{problem_tag}' at position {error_index}.",
+        SchemaWarnings.INVALID_CAPITALIZATION: f"{warning_prefix}First character just be a capital letter or number.  "
+                                               f"Found character '{problem_char}' in tag "
+                                               f"'{problem_tag}' at position {error_index}.",
+    }
+    default_warning_message = f'{warning_prefix}Internal Error'
+    warning_message = error_types.get(error_type, default_warning_message)
 
+    error_object = {'code': error_type,
+                    'message': warning_message,
+                    'source_tag': hed_tag}
 
-def add_row_and_column(error_object, row, col):
-    old_error_msg = error_object['message']
-    if old_error_msg.startswith(SCHEMA_ERROR_PREFIX):
-        old_trimmed_msg = old_error_msg[len(SCHEMA_ERROR_PREFIX):]
-        new_error_msg = f"ERROR on {row}, {col}: {old_trimmed_msg}"
-        error_object["message"] = new_error_msg
-    error_object["row_number"] = row
-    error_object["column_number"] = col
+    error_object_list = _add_context_to_errors(error_object)
+    return error_object_list
diff --git a/hedtools/hed/util/error_types.py b/hedtools/hed/util/error_types.py
index 46fd925c1..3a8fd1408 100644
--- a/hedtools/hed/util/error_types.py
+++ b/hedtools/hed/util/error_types.py
@@ -1,8 +1,16 @@
 
-class ValidationErrors:
-    # General validation errors
+class ErrorContext:
+    """Indicates the context this error took place in, each error potentially having multiple contexts"""
+    FILE_NAME = 'filename'
+    SIDECAR_COLUMN_NAME = 'sidecarColumnName'
+    SIDECAR_CUE_NAME = 'sidecarCueName'
+    SIDECAR_HED_STRING = 'sidecarHedString'
     ROW = 'row'
     COLUMN = 'column'
+
+
+class ValidationErrors:
+    # General validation errors
     INVALID_FILENAME = 'invalidFileName'
     UNIT_CLASS_INVALID_UNIT = 'unitClassInvalidUnit'
     EXTRA_DELIMITER = 'extraDelimiter'
@@ -24,10 +32,7 @@ class ValidationWarnings:
 
 
 class SidecarErrors:
-    # These are for json sidecar validation errors(sidecars can also produce most normal errors)
-    SIDECAR_FILE_NAME = 'sidecarFilename'
-    SIDECAR_COLUMN_NAME = 'sidecarColumnName'
-    SIDECAR_HED_STRING = 'sidecarHedString'
+    # These are for json sidecar validation errors(sidecars can also produce most normal validation errors)
     INVALID_FILENAME = 'sidecarInvalidFilename'
     CANNOT_PARSE_JSON = 'cannotParseJson'
     BLANK_HED_STRING = 'blankValueString'
@@ -46,3 +51,10 @@ class SchemaErrors:
     EMPTY_TAG_FOUND = 'emptyTag'
     FILE_NOT_FOUND = 'fileNotFound'
     CANNOT_PARSE_XML = 'cannotParseXML'
+    DUPLICATE_TERMS = 'duplicateTerms'
+
+
+class SchemaWarnings:
+    INVALID_CHARACTERS_IN_DESC = "invalidCharDesc"
+    INVALID_CHARACTERS_IN_TAG = "invalidCharTag"
+    INVALID_CAPITALIZATION = 'invalidCaps'
diff --git a/hedtools/hed/util/event_file_input.py b/hedtools/hed/util/event_file_input.py
index 1cf39547a..7efb9a86b 100644
--- a/hedtools/hed/util/event_file_input.py
+++ b/hedtools/hed/util/event_file_input.py
@@ -10,7 +10,7 @@ class EventFileInput(BaseFileInput):
     def __init__(self, filename, worksheet_name=None, tag_columns=None,
                  has_column_names=True, column_prefix_dictionary=None,
                  json_def_files=None, attribute_columns=None,
-                 hed_dictionary=None):
+                 hed_schema=None):
         """Constructor for the EventFileInput class.
 
         Parameters
@@ -35,7 +35,7 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
         attribute_columns: str or int or [str] or [int]
             A list of column names or numbers to treat as attributes.
             Default: ["duration", "onset"]
-        hed_dictionary: HedDictionary
+        hed_schema: HedSchema
            Used by column mapper to do (optional) hed string validation, and also to gather definition tags correctly.
         """
         if tag_columns is None:
@@ -47,10 +47,10 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
 
         self.column_group_defs = ColumnDefGroup.load_multiple_json_files(json_def_files)
 
-        def_mapper = DefinitionMapper(self.column_group_defs, hed_dictionary=hed_dictionary)
+        def_mapper = DefinitionMapper(self.column_group_defs, hed_schema=hed_schema)
         new_mapper = ColumnMapper(json_def_files=self.column_group_defs, tag_columns=tag_columns,
                                   column_prefix_dictionary=column_prefix_dictionary,
-                                  hed_dictionary=hed_dictionary, attribute_columns=attribute_columns,
+                                  hed_schema=hed_schema, attribute_columns=attribute_columns,
                                   definition_mapper=def_mapper)
 
         super().__init__(filename, worksheet_name, has_column_names, new_mapper)
@@ -59,13 +59,13 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
             raise ValueError("You are attempting to open a bids style file with no column headers provided.\n"
                              "This is probably not intended.")
 
-    def validate_file_sidecars(self, hed_dictionary=None):
+    def validate_file_sidecars(self, hed_schema=None):
         """
         Validates all column definitions and column definition hed strings.
 
         Parameters
         ----------
-        hed_dictionary : HedDictionary, optional
+        hed_schema : HedSchema, optional
             Also semantically validates hed strings if present.
         Returns
         -------
@@ -74,7 +74,7 @@ def validate_file_sidecars(self, hed_dictionary=None):
         """
         validation_issues = []
         for column_def_group in self.column_group_defs:
-            new_issue_dict = column_def_group.validate_entries(hed_dictionary=hed_dictionary)
+            new_issue_dict = column_def_group.validate_entries(hed_schema=hed_schema)
             for name, issues in new_issue_dict.items():
                 validation_issues += issues
 
diff --git a/hedtools/hed/util/file_util.py b/hedtools/hed/util/file_util.py
index 08f691c19..9304ab060 100644
--- a/hedtools/hed/util/file_util.py
+++ b/hedtools/hed/util/file_util.py
@@ -9,102 +9,148 @@
 NO_VERSION_INFO_STRING = "No version info found"
 
 
-def url_to_file(resource_url):
-    """Write data from a URL resource into a file. Data is decoded as unicode.
+def delete_file_if_it_exist(file_path):
+    """Deletes a other if it exist.
 
     Parameters
     ----------
-    resource_url: string
-        The URL to the resource.
+    file_path: str
+        The path the file.
 
     Returns
     -------
-    string: The local temporary filename for downloaded file
+    bool
+        True if the file exists and was deleted.
     """
-    url_request = urllib.request.urlopen(resource_url)
-    suffix = get_file_extension(resource_url)
-    url_data = str(url_request.read(), 'utf-8')
-    with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, mode='w', encoding='utf-8') as opened_file:
-        opened_file.write(url_data)
-        return opened_file.name
+    if os.path.isfile(file_path):
+        os.remove(file_path)
+        return True
+    return False
 
 
 def get_file_extension(file_name_or_path):
-    """Get the other extension from the specified filename. This can be the full path or just the name of the other.
+    """Get the other extension from the specified filename. This can be the full path or just the name.
 
        Parameters
        ----------
-       file_name_or_path: string
-           The name or full path of a other.
+       file_name_or_path: str
+           The name or full path of a file.
 
        Returns
        -------
-       string
+       str
            The extension of the other.
        """
     return os.path.splitext(file_name_or_path)[1]
 
 
-def write_strings_to_file(output_strings, extension=None):
-    with tempfile.NamedTemporaryFile(suffix=extension, delete=False, mode='w', encoding='utf-8') as opened_file:
-        for string in output_strings:
-            opened_file.write(string)
-            opened_file.write('\n')
-        return opened_file.name
+def get_version_from_xml(hed_xml_tree):
+    """Gets version info from root node if present
+
+       Parameters
+       ----------
+       hed_xml_tree: Element
+           The root node of an XML tree.
 
+       Returns
+       -------
+       str
+           The version of the HED schema.
 
-def _xml_element_2_str(elem):
-    """Converts an XML element to an XML string.
+    """
+
+    if hed_xml_tree is None:
+        return NO_VERSION_INFO_STRING
+
+    try:
+        return hed_xml_tree.attrib['version']
+    except KeyError or AttributeError:
+        return NO_VERSION_INFO_STRING
+
+
+def url_to_file(resource_url):
+    """Write data from a URL resource into a file. Data is decoded as unicode.
 
     Parameters
     ----------
-    elem: Element
-        An XML element.
+    resource_url: str
+        The URL to the resource.
 
     Returns
     -------
-    string
-        A XML string representing the XML element.
+    str
+        The local temporary filename for downloaded file
+    """
+    url_request = urllib.request.urlopen(resource_url)
+    suffix = get_file_extension(resource_url)
+    url_data = str(url_request.read(), 'utf-8')
+    with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, mode='w', encoding='utf-8') as opened_file:
+        opened_file.write(url_data)
+        return opened_file.name
+
 
+def write_errors_to_file(issues, extension=".txt"):
     """
-    rough_string = ElementTree.tostring(elem, method='xml')
-    reparsed = minidom.parseString(rough_string)
-    return reparsed.toprettyxml(indent="   ")
+    Writes an array of issue dictionaries to a temporary file.
 
+    Parameters
+    ----------
+    issues: list
+        List of 2-element dictionaries containing code and message keys.
+    extension: str
+        Desired file extension.
 
-def write_xml_tree_2_xml_file(xml_tree, extension=".xml"):
-    """Writes a XML element tree object into a XML file.
+    Returns
+    -------
+    str
+        The name of the temporary file
+    """
+    with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as error_file:
+        for line in issues:
+            error_file.write(f"{line['code']}: {line['message']}\n")
+        return error_file.name
+
+
+def write_strings_to_file(output_strings, extension=None):
+    """
+    Writes a list of output strings to a temporary file and returns the open file.
 
     Parameters
     ----------
-    xml_tree: Element
-        A element representing an XML file.
-    extension: string
-        The file extension to use for temporary file.
+    output_strings: list
+        List of strings to output one per line.
+    extension: str
+        Desired file extension.
 
     Returns
     -------
+        file
+            Opened temporary file
+
 
     """
-    with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as hed_xml_file:
-        xml_string = _xml_element_2_str(xml_tree)
-        hed_xml_file.write(xml_string)
-        return hed_xml_file.name
+    with tempfile.NamedTemporaryFile(suffix=extension, delete=False, mode='w', encoding='utf-8') as opened_file:
+        for string in output_strings:
+            opened_file.write(string)
+            opened_file.write('\n')
+        return opened_file.name
 
 
 def write_text_iter_to_file(text_iter, extension=".txt"):
-    """Writes a text file based on an iterator.
+    """
+    Writes a text file based on an iterator.
 
     Parameters
     ----------
     text_iter: text iterator
-        Iterates over the lines you wish to write to a file.  Adds newlines.
-    extension: string
+        Iterates over values and writes to temporary file one per line.  Adds newlines.
+    extension: str
         Desired file extension.
 
     Returns
     -------
-
+        str
+            Name of the temporary file.
     """
     with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as hed_xml_file:
         for line in text_iter:
@@ -112,31 +158,40 @@ def write_text_iter_to_file(text_iter, extension=".txt"):
         return hed_xml_file.name
 
 
-def get_version_from_xml(hed_xml_tree):
-    """Gets version info from root node if present"""
-    if hed_xml_tree is None:
-        return NO_VERSION_INFO_STRING
+def write_xml_tree_2_xml_file(xml_tree, extension=".xml"):
+    """Writes a XML element tree object into a XML file.
 
-    try:
-        return hed_xml_tree.attrib['version']
-    except KeyError or AttributeError:
-        return NO_VERSION_INFO_STRING
+    Parameters
+    ----------
+    xml_tree: Element
+        A element representing an XML file.
+    extension: string
+        The file extension to use for temporary file.
 
+    Returns
+    -------
 
-def delete_file_if_it_exist(file_path):
-    """Deletes a other if it exist.
+    """
+    with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as hed_xml_file:
+        xml_string = _xml_element_2_str(xml_tree)
+        hed_xml_file.write(xml_string)
+        return hed_xml_file.name
+
+
+def _xml_element_2_str(elem):
+    """Converts an XML element to an XML string.
 
     Parameters
     ----------
-    file_path: string
-        The path to a other.
+    elem: Element
+        An XML element.
 
     Returns
     -------
-    boolean
-        True if the other exist and was deleted.
+    str
+        A XML string representing the XML element.
+
     """
-    if os.path.isfile(file_path):
-        os.remove(file_path)
-        return True
-    return False
+    rough_string = ElementTree.tostring(elem, method='xml')
+    reparsed = minidom.parseString(rough_string)
+    return reparsed.toprettyxml(indent="   ")
diff --git a/hedtools/hed/util/hed_dictionary.py b/hedtools/hed/util/hed_schema.py
similarity index 93%
rename from hedtools/hed/util/hed_dictionary.py
rename to hedtools/hed/util/hed_schema.py
index d752a9a74..d0c328bc4 100644
--- a/hedtools/hed/util/hed_dictionary.py
+++ b/hedtools/hed/util/hed_schema.py
@@ -1,5 +1,5 @@
 """
-This module contains the Hed_Dictionary class which encapsulates all HED tags, tag attributes, unit classes, and
+This module contains the HedSchema class which encapsulates all HED tags, tag attributes, unit classes, and
 unit class attributes in a dictionary.
 
 The dictionary is a dictionary of dictionaries. The dictionary names are
@@ -42,7 +42,7 @@ class HedKey:
     ShortTags = 'shortTags'
 
 
-class HedDictionary:
+class HedSchema:
     TAG_DICTIONARY_KEYS = [HedKey.Default, HedKey.ExtensionAllowed, HedKey.IsNumeric, HedKey.Position,
                            HedKey.PredicateType, HedKey.Recommended, HedKey.RequiredPrefix, HedKey.RequireChild,
                            HedKey.AllTags, HedKey.TakesValue, HedKey.Unique, HedKey.UnitClass]
@@ -61,7 +61,7 @@ class HedDictionary:
     VERSION_ATTRIBUTE = 'version'
 
     def __init__(self, hed_xml_file_path):
-        """Constructor for the Hed_Dictionary class.
+        """Constructor for the HedSchema class.
 
         Parameters
         ----------
@@ -70,8 +70,8 @@ def __init__(self, hed_xml_file_path):
 
         Returns
         -------
-        HedDictionary
-            A Hed_Dictionary object.
+        HedSchema
+            A HedSchema object.
 
         """
         self.no_duplicate_tags = True
@@ -200,7 +200,7 @@ def _populate_tag_dictionaries(self):
             A dictionary of dictionaries that has been populated with dictionaries associated with tag attributes.
 
         """
-        for dict_key in HedDictionary.TAG_DICTIONARY_KEYS:
+        for dict_key in HedSchema.TAG_DICTIONARY_KEYS:
             tags, tag_elements = self.get_tags_by_attribute(dict_key)
             if HedKey.ExtensionAllowed == dict_key:
                 child_tags = self._get_all_child_tags(tag_elements)
@@ -292,11 +292,14 @@ def dupe_tag_iter(self, return_detailed_info=False):
             A list of long tags that have duplicates, with optional descriptive short tag lines.
         """
         duplicate_dict = self.find_duplicate_tags()
+        prefix_string = ""
+        if return_detailed_info:
+            prefix_string = "\t"
         for tag_name in duplicate_dict:
             if return_detailed_info:
                 yield f"Duplicate tag found {tag_name} - {len(duplicate_dict[tag_name])} versions:"
             for tag_entry in duplicate_dict[tag_name]:
-                yield f"\t{tag_entry}"
+                yield f"{prefix_string}{tag_entry}"
 
     def _populate_short_tag_dict(self):
         """
@@ -401,7 +404,7 @@ def _populate_unit_class_default_unit_dictionary(self, unit_class_elements):
             default_unit = unit_class_element.get(self.DEFAULT_UNITS_FOR_TYPE_ATTRIBUTE)
             if default_unit is None:
                 self.dictionaries[HedKey.DefaultUnits][unit_class_element_name] = \
-                    unit_class_element.attrib[HedDictionary.DEFAULT_UNIT_FOR_OLD_UNIT_CLASS_ATTRIBUTE]
+                    unit_class_element.attrib[HedSchema.DEFAULT_UNIT_FOR_OLD_UNIT_CLASS_ATTRIBUTE]
             else:
                 self.dictionaries[HedKey.DefaultUnits][unit_class_element_name] = default_unit
 
@@ -613,6 +616,39 @@ def get_all_tags(self, tag_element_name='node'):
     #     tag_elements = self.root_element.xpath('.//%s[@%s]' % (element_name, attribute_name))
     #     return tag_elements
 
+    def get_all_descriptions(self):
+        """
+        Gather all description nodes from the xml file and pair them with their tag name.
+
+        Returns
+        -------
+        {str: str}:
+            tag name : description
+        """
+        elements = self._get_elements_by_name("description")
+        parents = [self._get_parent_tag_name(element) for element in elements]
+        final_dict = {parent: element.text for parent, element in zip(parents, elements)}
+        return final_dict
+
+    def get_all_terms(self):
+        """
+        Gets a single copy of all hed terms from the schema, for hed2 or hed3 compatible.
+
+        Returns
+        -------
+        term_list: [str]
+            A list of all terms(short tags) from the schema.
+        """
+        final_list = []
+        if not self.has_duplicate_tags():
+            for lower_tag, org_tag in self.short_tag_mapping.items():
+                final_list.append(org_tag.split('/')[-1])
+        # Fallback for hed2 style schema validation
+        else:
+            for lower_tag, org_tag in self.dictionaries[HedKey.AllTags].items():
+                final_list.append(org_tag.split('/')[-1])
+        return final_list
+
     def _get_elements_by_name(self, element_name='node', parent_element=None):
         """Gets the elements that have a specific element name.
 
@@ -699,5 +735,5 @@ def get_hed_xml_version(hed_xml_file_path):
             The version number of the HED XML file.
 
         """
-        root_node = HedDictionary.parse_hed_xml_file(hed_xml_file_path)
-        return root_node.attrib[HedDictionary.VERSION_ATTRIBUTE]
+        root_node = HedSchema.parse_hed_xml_file(hed_xml_file_path)
+        return root_node.attrib[HedSchema.VERSION_ATTRIBUTE]
diff --git a/hedtools/hed/validator/hed_validator.py b/hedtools/hed/validator/hed_validator.py
index 6ffcd9a53..d3d2eb752 100644
--- a/hedtools/hed/validator/hed_validator.py
+++ b/hedtools/hed/validator/hed_validator.py
@@ -4,10 +4,11 @@
 the get_validation_issues() function.
 
 """
-from hed.util.error_types import ValidationErrors
+from hed.util.error_types import ValidationErrors, ErrorContext
 from hed.util import hed_cache
 from hed.util import error_reporter
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.error_reporter import push_error_context, pop_error_context
+from hed.util.hed_schema import HedSchema
 from hed.util.hed_string_delimiter import HedStringDelimiter
 from hed.validator.tag_validator import TagValidator
 from hed.util.hed_file_input import BaseFileInput
@@ -17,7 +18,7 @@
 class HedValidator:
     def __init__(self, hed_input, check_for_warnings=False, run_semantic_validation=True,
                  hed_xml_file='', xml_version_number=None,
-                 hed_dictionary=None):
+                 hed_schema=None):
         """Constructor for the HedValidator class.
 
         Parameters
@@ -34,8 +35,8 @@ def __init__(self, hed_input, check_for_warnings=False, run_semantic_validation=
         xml_version_number: str
             HED version format string. Expected format: 'X.Y.Z'  Only applies if hed_xml_file is empty,
                 or does not point to a specific xml file.
-        hed_dictionary: HedDictionary
-            Name of already prepared HedDictionary to use.  This overrides hed_xml_file and xml_version_number.
+        hed_schema: HedSchema
+            Name of already prepared HedSchema to use.  This overrides hed_xml_file and xml_version_number.
         Returns
         -------
         HedValidator object
@@ -48,16 +49,16 @@ def __init__(self, hed_input, check_for_warnings=False, run_semantic_validation=
         self._tag_validator = None
         if run_semantic_validation:
             try:
-                if hed_dictionary is None:
-                    self._hed_dictionary = self._get_hed_dictionary(hed_xml_file,
-                                                                    get_specific_version=xml_version_number)
+                if hed_schema is None:
+                    self._hed_schema = self._get_hed_schema(hed_xml_file,
+                                                            get_specific_version=xml_version_number)
                 else:
-                    self._hed_dictionary = hed_dictionary
-                self._tag_validator = TagValidator(hed_dictionary=self._hed_dictionary,
+                    self._hed_schema = hed_schema
+                self._tag_validator = TagValidator(hed_schema=self._hed_schema,
                                                    check_for_warnings=check_for_warnings,
                                                    run_semantic_validation=True)
             except SchemaFileError as e:
-                self._hed_dictionary = None
+                self._hed_schema = None
                 self._validation_issues += e.format_error_message()
 
         # Fall back to syntax validation if we don't have a tag validator at this point
@@ -83,9 +84,9 @@ def get_tag_validator(self):
         return self._tag_validator
 
     @staticmethod
-    def _get_hed_dictionary(hed_xml_file, get_specific_version=None):
+    def _get_hed_schema(hed_xml_file, get_specific_version=None):
         """
-        Gets a HEDDictionary object based on the hed xml file specified. If no HED file is specified then the latest
+        Gets a HedSchema object based on the hed xml file specified. If no HED file is specified then the latest
            file will be retrieved.
 
         Parameters
@@ -97,16 +98,16 @@ def _get_hed_dictionary(hed_xml_file, get_specific_version=None):
                 or does not point to a specific xml file.
         Returns
         -------
-        HedDictionary
-            A HedDictionary object.
+        HedSchema
+            A HedSchema object.
 
         """
         # If we're not asking for a specific file, for ease of use cache the ones from github.
         if hed_xml_file is None and get_specific_version is None:
             hed_cache.cache_all_hed_xml_versions()
         final_hed_xml_file = hed_cache.get_local_file(hed_xml_file, get_specific_version)
-        hed_dictionary = HedDictionary(final_hed_xml_file)
-        return hed_dictionary
+        hed_schema = HedSchema(final_hed_xml_file)
+        return hed_schema
 
     def _validate_hed_input(self):
         """Validates the HED tags in a string or a file.
@@ -196,11 +197,12 @@ def _append_row_validation_issues_if_found(self, validation_issues, row_number,
 
          """
         if row_hed_string:
+            push_error_context(ErrorContext.ROW, row_number)
             hed_string_delimiter = HedStringDelimiter(row_hed_string)
             row_validation_issues = self._validate_top_level_in_hed_string(hed_string_delimiter)
             row_validation_issues += self._validate_tag_levels_in_hed_string(hed_string_delimiter)
-            if row_validation_issues:
-                validation_issues += HedValidator.generate_row_issue_message(row_number) + row_validation_issues
+            validation_issues += row_validation_issues
+            pop_error_context()
         return validation_issues
 
     def _append_column_validation_issues_if_found(self, validation_issues, row_number, column_to_hed_tags_dictionary):
@@ -222,11 +224,10 @@ def _append_column_validation_issues_if_found(self, validation_issues, row_numbe
          """
         if column_to_hed_tags_dictionary:
             for column_number in column_to_hed_tags_dictionary.keys():
+                push_error_context(ErrorContext.COLUMN, row_number, column_context=column_number)
                 column_hed_string = column_to_hed_tags_dictionary[column_number]
-                column_validation_issues = self.validate_column_hed_string(column_hed_string)
-                if column_validation_issues:
-                    validation_issues += HedValidator.generate_column_issue_message(row_number, column_number) + \
-                                         column_validation_issues
+                validation_issues += self.validate_column_hed_string(column_hed_string)
+                pop_error_context()
         return validation_issues
 
     def validate_column_hed_string(self, column_hed_string):
@@ -413,39 +414,3 @@ def get_previous_original_and_formatted_tag(original_and_formatted_tags, loop_in
             previous_original_tag = original_and_formatted_tags[loop_index - 1][0]
             previous_formatted_tag = original_and_formatted_tags[loop_index - 1][1]
         return previous_original_tag, previous_formatted_tag
-
-    @staticmethod
-    def generate_row_issue_message(row_number):
-        """Generates a row issue message that is associated with a particular row in a spreadsheet.
-
-         Parameters
-         ----------
-         row_number: int
-            The row number that the issue is associated with.
-         Returns
-         -------
-         issue_list: []
-             A singleton list containing the row issue message.
-
-         """
-        return error_reporter.format_val_error(ValidationErrors.ROW, error_row=row_number)
-
-    @staticmethod
-    def generate_column_issue_message(row_number, column_number):
-        """Generates a column issue message that is associated with a particular column in a spreadsheet.
-
-         Parameters
-         ----------
-         row_number: int
-            The row number that the issue is associated with.
-         column_number: int
-            The column number that the issue is associated with.
-
-         Returns
-         -------
-         []
-             A singleton list containing the column issue message.
-
-         """
-        return error_reporter.format_val_error(ValidationErrors.COLUMN,
-                                               error_row=row_number, error_column=column_number)
diff --git a/hedtools/hed/validator/tag_validator.py b/hedtools/hed/validator/tag_validator.py
index 32a5e3a5e..11b9df88e 100644
--- a/hedtools/hed/validator/tag_validator.py
+++ b/hedtools/hed/validator/tag_validator.py
@@ -9,7 +9,7 @@
 
 from hed.util.error_types import ValidationErrors, ValidationWarnings
 from hed.util import error_reporter
-from hed.util.hed_dictionary import HedDictionary, HedKey
+from hed.util.hed_schema import HedSchema, HedKey
 
 pluralize = inflect.engine()
 pluralize.defnoun("hertz", "hertz")
@@ -29,14 +29,14 @@ class TagValidator:
     DATE_TIME_UNIT_CLASS = 'dateTime'
     TIME_UNIT_CLASS = 'time'
 
-    def __init__(self, hed_dictionary=None, check_for_warnings=False, run_semantic_validation=True,
+    def __init__(self, hed_schema=None, check_for_warnings=False, run_semantic_validation=True,
                  allow_numbers_to_be_pound_sign=False):
         """Constructor for the Tag_Validator class.
 
         Parameters
         ----------
-        hed_dictionary: HedDictionary
-            A Hed_Dictionary object.
+        hed_schema: HedSchema
+            A HedSchema object.
         allow_numbers_to_be_pound_sign: bool
             If true, considers # equal to a number for validation purposes.  This is so it can validate templates.
         Returns
@@ -45,11 +45,11 @@ def __init__(self, hed_dictionary=None, check_for_warnings=False, run_semantic_v
             A Tag_Validator object.
 
         """
-        self._hed_dictionary = hed_dictionary
-        if hed_dictionary:
-            self._hed_dictionary_dictionaries = hed_dictionary.get_dictionaries()
+        self._hed_schema = hed_schema
+        if hed_schema:
+            self._hed_schema_dictionaries = hed_schema.get_dictionaries()
         else:
-            self._hed_dictionary_dictionaries = None
+            self._hed_schema_dictionaries = None
         self._check_for_warnings = check_for_warnings
         self._issue_count = 0
         self._error_count = 0
@@ -249,7 +249,7 @@ def tag_exist_in_schema(self, original_tag, formatted_tag, previous_original_tag
         """
         validation_issues = []
         is_extension_tag = self.is_extension_allowed_tag(formatted_tag)
-        if self._hed_dictionary_dictionaries[HedKey.AllTags].get(formatted_tag) or \
+        if self._hed_schema_dictionaries[HedKey.AllTags].get(formatted_tag) or \
                 self.tag_takes_value(formatted_tag) or formatted_tag == TagValidator.TILDE:
             return validation_issues
 
@@ -276,7 +276,7 @@ def tag_exists_in_schema(self, formatted_tag):
             True if the tag is a valid HED tag. False, if otherwise.
 
         """
-        return self._hed_dictionary_dictionaries[HedKey.AllTags].get(formatted_tag) is not None
+        return self._hed_schema_dictionaries[HedKey.AllTags].get(formatted_tag) is not None
 
     def check_capitalization(self, original_tag, formatted_tag):
         """Reports a validation warning if the tag isn't correctly capitalized.
@@ -323,8 +323,8 @@ def is_extension_allowed_tag(self, formatted_tag):
         tag_slash_indices = self.get_tag_slash_indices(formatted_tag)
         for tag_slash_index in tag_slash_indices:
             tag_substring = self.get_tag_substring_by_end_index(formatted_tag, tag_slash_index)
-            if self._hed_dictionary.tag_has_attribute(tag_substring,
-                                                      HedKey.ExtensionAllowed):
+            if self._hed_schema.tag_has_attribute(tag_substring,
+                                                  HedKey.ExtensionAllowed):
                 return True
         return False
 
@@ -341,11 +341,11 @@ def tag_takes_value(self, formatted_tag):
             True if the tag has the 'takesValue' attribute. False, if otherwise.
 
         """
-        if not self._hed_dictionary:
+        if not self._hed_schema:
             return False
         takes_value_tag = self.replace_tag_name_with_pound(formatted_tag)
-        return self._hed_dictionary.tag_has_attribute(takes_value_tag,
-                                                      HedKey.TakesValue)
+        return self._hed_schema.tag_has_attribute(takes_value_tag,
+                                                  HedKey.TakesValue)
 
     def is_unit_class_tag(self, formatted_tag):
         """Checks to see if the tag has the 'unitClass' attribute.
@@ -361,10 +361,10 @@ def is_unit_class_tag(self, formatted_tag):
 
         """
         takes_value_tag = self.replace_tag_name_with_pound(formatted_tag)
-        if not self._hed_dictionary.has_unit_classes:
+        if not self._hed_schema.has_unit_classes:
             return False
-        return self._hed_dictionary.tag_has_attribute(takes_value_tag,
-                                                      HedKey.UnitClass)
+        return self._hed_schema.tag_has_attribute(takes_value_tag,
+                                                  HedKey.UnitClass)
 
     def replace_tag_name_with_pound(self, formatted_tag):
         """Replaces the tag name with the pound sign.
@@ -407,17 +407,17 @@ def check_if_tag_unit_class_units_are_valid(self, original_tag, formatted_tag):
             original_tag_unit_value = self.get_tag_name(original_tag)
             tag_unit_class_units = tuple(self.get_tag_unit_class_units(formatted_tag))
             if (TagValidator.DATE_TIME_UNIT_CLASS in
-                    self._hed_dictionary_dictionaries[HedKey.Units]):
+                    self._hed_schema_dictionaries[HedKey.Units]):
                 if (TagValidator.DATE_TIME_UNIT_CLASS in tag_unit_classes
                         and TagValidator.is_clock_face_time(formatted_tag_unit_value)):
                     return validation_issues
             if (TagValidator.CLOCK_TIME_UNIT_CLASS in
-                    self._hed_dictionary_dictionaries[HedKey.Units]):
+                    self._hed_schema_dictionaries[HedKey.Units]):
                 if (TagValidator.CLOCK_TIME_UNIT_CLASS in tag_unit_classes
                         and TagValidator.is_clock_face_time(formatted_tag_unit_value)):
                     return validation_issues
             elif (TagValidator.TIME_UNIT_CLASS in
-                  self._hed_dictionary_dictionaries[HedKey.Units]):
+                  self._hed_schema_dictionaries[HedKey.Units]):
                 if (TagValidator.TIME_UNIT_CLASS in tag_unit_classes
                         and TagValidator.is_clock_face_time(formatted_tag_unit_value)):
                     return validation_issues
@@ -446,8 +446,8 @@ def get_valid_unit_plural(self, unit):
             list of plural units
         """
         derivative_units = [unit]
-        if self._hed_dictionary.has_unit_modifiers and \
-                self._hed_dictionary_dictionaries[HedKey.UnitSymbol].get(unit) is None:
+        if self._hed_schema.has_unit_modifiers and \
+                self._hed_schema_dictionaries[HedKey.UnitSymbol].get(unit) is None:
             derivative_units.append(pluralize.plural(unit))
         return derivative_units
 
@@ -477,13 +477,13 @@ def _strip_off_units_if_valid(self, unit_value, unit, is_unit_symbol):
             found_unit = True
             stripped_value = str(unit_value)[0:-len(unit)].strip()
 
-        if found_unit and self._hed_dictionary.has_unit_modifiers:
+        if found_unit and self._hed_schema.has_unit_modifiers:
             if is_unit_symbol:
                 modifier_key = HedKey.SIUnitSymbolModifier
             else:
                 modifier_key = HedKey.SIUnitModifier
 
-            for unit_modifier in self._hed_dictionary_dictionaries[modifier_key]:
+            for unit_modifier in self._hed_schema_dictionaries[modifier_key]:
                 if stripped_value.startswith(unit_modifier):
                     stripped_value = stripped_value[len(unit_modifier):].strip()
                 elif stripped_value.endswith(unit_modifier):
@@ -512,8 +512,8 @@ def validate_units(self, original_tag_unit_value, formatted_tag_unit_value, tag_
         for unit in tag_unit_class_units:
             derivative_units = self.get_valid_unit_plural(unit)
             for derivative_unit in derivative_units:
-                if self._hed_dictionary.has_unit_modifiers and \
-                        self._hed_dictionary_dictionaries[HedKey.UnitSymbol].get(unit):
+                if self._hed_schema.has_unit_modifiers and \
+                        self._hed_schema_dictionaries[HedKey.UnitSymbol].get(unit):
                     found_unit, stripped_value = self._strip_off_units_if_valid(original_tag_unit_value,
                                                                                 derivative_unit,
                                                                                 True)
@@ -588,7 +588,7 @@ def get_tag_unit_classes(self, formatted_tag):
         unit_classes = []
         unit_class_tag = self.replace_tag_name_with_pound(formatted_tag)
         if self.is_unit_class_tag(formatted_tag):
-            unit_classes = self._hed_dictionary_dictionaries[HedKey.UnitClass][unit_class_tag]
+            unit_classes = self._hed_schema_dictionaries[HedKey.UnitClass][unit_class_tag]
             unit_classes = unit_classes.split(',')
         return unit_classes
 
@@ -609,12 +609,12 @@ def get_tag_unit_class_units(self, formatted_tag):
         units = []
         unit_class_tag = self.replace_tag_name_with_pound(formatted_tag)
         if self.is_unit_class_tag(formatted_tag):
-            unit_classes = self._hed_dictionary_dictionaries[HedKey.UnitClass][unit_class_tag]
+            unit_classes = self._hed_schema_dictionaries[HedKey.UnitClass][unit_class_tag]
             unit_classes = unit_classes.split(',')
 
             for unit_class in unit_classes:
                 try:
-                    units += (self._hed_dictionary_dictionaries[HedKey.Units][unit_class])
+                    units += (self._hed_schema_dictionaries[HedKey.Units][unit_class])
                 except Exception:
                     continue
         return units
@@ -636,15 +636,15 @@ def get_unit_class_default_unit(self, formatted_tag):
         default_unit = ''
         unit_class_tag = self.replace_tag_name_with_pound(formatted_tag)
         if self.is_unit_class_tag(formatted_tag):
-            has_default_attribute = self._hed_dictionary.tag_has_attribute(unit_class_tag,
-                                                                           HedKey.Default)
+            has_default_attribute = self._hed_schema.tag_has_attribute(unit_class_tag,
+                                                                       HedKey.Default)
             if has_default_attribute:
-                default_unit = self._hed_dictionary_dictionaries[HedKey.Default][unit_class_tag]
-            elif unit_class_tag in self._hed_dictionary_dictionaries[HedKey.UnitClass]:
+                default_unit = self._hed_schema_dictionaries[HedKey.Default][unit_class_tag]
+            elif unit_class_tag in self._hed_schema_dictionaries[HedKey.UnitClass]:
                 unit_classes = \
-                    self._hed_dictionary_dictionaries[HedKey.UnitClass][unit_class_tag].split(',')
+                    self._hed_schema_dictionaries[HedKey.UnitClass][unit_class_tag].split(',')
                 first_unit_class = unit_classes[0]
-                default_unit = self._hed_dictionary_dictionaries[HedKey.DefaultUnits][
+                default_unit = self._hed_schema_dictionaries[HedKey.DefaultUnits][
                     first_unit_class]
         return default_unit
 
@@ -685,7 +685,7 @@ def check_if_tag_requires_child(self, original_tag, formatted_tag):
 
         """
         validation_issues = []
-        if self._hed_dictionary_dictionaries[HedKey.RequireChild].get(formatted_tag):
+        if self._hed_schema_dictionaries[HedKey.RequireChild].get(formatted_tag):
             validation_issues += error_reporter.format_val_error(ValidationErrors.REQUIRE_CHILD,
                                                                  tag=original_tag)
             self._increment_issue_count()
@@ -705,10 +705,10 @@ def check_for_required_tags(self, formatted_top_level_tags):
 
         """
         validation_issues = []
-        required_tag_prefixes = self._hed_dictionary_dictionaries[HedKey.RequiredPrefix]
+        required_tag_prefixes = self._hed_schema_dictionaries[HedKey.RequiredPrefix]
         for required_tag_prefix in required_tag_prefixes:
             capitalized_required_tag_prefix = \
-                self._hed_dictionary_dictionaries[HedKey.RequiredPrefix][required_tag_prefix]
+                self._hed_schema_dictionaries[HedKey.RequiredPrefix][required_tag_prefix]
             if sum([x.startswith(required_tag_prefix) for x in formatted_top_level_tags]) < 1:
                 validation_issues += error_reporter.format_val_warning(
                     ValidationWarnings.REQUIRED_PREFIX_MISSING,
@@ -732,13 +732,13 @@ def check_if_multiple_unique_tags_exist(self, original_tag_list, formatted_tag_l
 
         """
         validation_issues = []
-        unique_tag_prefixes = self._hed_dictionary_dictionaries[HedKey.Unique]
+        unique_tag_prefixes = self._hed_schema_dictionaries[HedKey.Unique]
         for unique_tag_prefix in unique_tag_prefixes:
             unique_tag_prefix_bool_mask = [x.startswith(unique_tag_prefix) for x in formatted_tag_list]
             if sum(unique_tag_prefix_bool_mask) > 1:
                 validation_issues += error_reporter.format_val_error(
                     ValidationErrors.MULTIPLE_UNIQUE,
-                    tag_prefix=self._hed_dictionary_dictionaries[HedKey.Unique][unique_tag_prefix])
+                    tag_prefix=self._hed_schema_dictionaries[HedKey.Unique][unique_tag_prefix])
                 self._increment_issue_count()
         return validation_issues
 
@@ -755,7 +755,7 @@ def tag_has_unique_prefix(self, tag):
             True if the tag starts with a unique prefix. False if otherwise.
 
         """
-        unique_tag_prefixes = self._hed_dictionary_dictionaries[HedKey.Unique]
+        unique_tag_prefixes = self._hed_schema_dictionaries[HedKey.Unique]
         for unique_tag_prefix in unique_tag_prefixes:
             if tag.lower().startswith(unique_tag_prefix):
                 return True
diff --git a/hedweb/hed/web/constants/common_constants.py b/hedweb/hed/web/constants/common_constants.py
index 6d6816963..ce9b1dcaf 100644
--- a/hedweb/hed/web/constants/common_constants.py
+++ b/hedweb/hed/web/constants/common_constants.py
@@ -1,13 +1,17 @@
 CHECK_FOR_WARNINGS = 'check-for-warnings'
 COLUMN_NAMES = 'column-names'
 COLUMNS_NAMES_TABLE = 'columns-names-table'
+HAS_COLUMN_NAMES = 'has-column-names'
 COLUMN_POSTFIX = '-column'
 COLUMN_PREFIX_DICTIONARY = 'column-prefix-dictionary'
-HAS_COLUMN_NAMES = 'has-column-names'
+TAG_COLUMNS = 'tag-columns'
+TAG_COLUMN_INDICES = 'tag-column-indices'
+
 DOWNLOAD_FILE = 'download-file'
-DICTIONARY_FILE = 'dictionary-file'
+JSON_FILE = 'json-file'
+JSON_PATH = 'json-path'
 ERROR_COUNT = 'error-count'
-HAS_COLUMN_NAMES = 'has-column-names'
+
 HED_DISPLAY_NAME = 'hed-display-name'
 HED_MAJOR_VERSIONS = 'hed-major-versions'
 HED_OTHER_VERSION = 'hed-other-version'
@@ -24,12 +28,13 @@
 SPREADSHEET_DISPLAY_NAME = 'spreadsheet-display-name'
 SPREADSHEET_FILE = 'spreadsheet-file'
 SPREADSHEET_PATH = 'spreadsheet-path'
-TAG_COLUMNS = 'tag-columns'
-TAG_COLUMN_INDICES = 'tag-column-indices'
+
 WARNING_COUNT = 'warning-count'
+
 WORKSHEET_NAME = 'worksheet-name'
 WORKSHEET_NAMES = 'worksheet-names'
 
+
 # Schema-specific constants
 SCHEMA_FORM = 'schema-form'
 SCHEMA_CONVERSION_SUBMIT = 'schema-conversion-submit'
diff --git a/hedweb/hed/web/constants/route_constants.py b/hedweb/hed/web/constants/route_constants.py
index 3187eb370..824f43c16 100644
--- a/hedweb/hed/web/constants/route_constants.py
+++ b/hedweb/hed/web/constants/route_constants.py
@@ -12,7 +12,7 @@
 HED_VERSION_ROUTE = '/get-hed-version'
 SCHEMA_ROUTE = '/schema'
 SCHEMA_CONVERSION_SUBMIT_ROUTE = '/schema-conversion-submit'
-SCHEMA_DUPLICATE_TAG_SUBMIT_ROUTE = '/schema-duplicate-tag-submit'
+SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE = '/schema-compliance-check-submit'
 SPREADSHEET_COLUMN_INFO_ROUTE = '/get-spreadsheet-columns-info'
 SPREADSHEET_VALIDATION_ROUTE = '/spreadsheet-validation'
 SPREADSHEET_VALIDATION_SUBMIT_ROUTE = '/spreadsheet-validation-submit'
diff --git a/hedweb/hed/web/dictionary.py b/hedweb/hed/web/dictionary.py
index 72983bc34..45ff7f6b9 100644
--- a/hedweb/hed/web/dictionary.py
+++ b/hedweb/hed/web/dictionary.py
@@ -7,7 +7,8 @@
 
 from hed.util.file_util import get_file_extension, delete_file_if_it_exist
 from hed.validator.hed_validator import HedValidator
-from hed.util.hed_file_input import HedFileInput
+from hed.util.column_def_group import ColumnDefGroup
+from hed.util.hed_schema import HedSchema
 
 from hed.web.constants import common_constants, error_constants, file_constants
 from hed.web import web_utils
@@ -31,76 +32,34 @@ def generate_arguments_from_validation_form(form_request_object):
     """
     hed_file_path, hed_display_name = web_utils.get_hed_path_from_pull_down(form_request_object)
     uploaded_file_name, original_file_name = \
-        web_utils.get_uploaded_file_path_from_form(form_request_object, common_constants.SPREADSHEET_FILE,
-                                                   file_constants.SPREADSHEET_FILE_EXTENSIONS)
-
-    validation_input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
-                                  common_constants.HED_DISPLAY_NAME: hed_display_name,
-                                  common_constants.SPREADSHEET_PATH: uploaded_file_name,
-                                  common_constants.SPREADSHEET_FILE: original_file_name}
-    validation_input_arguments[common_constants.TAG_COLUMNS] = \
-        web_utils.convert_number_str_to_list(form_request_object.form[common_constants.TAG_COLUMNS])
-    validation_input_arguments[common_constants.COLUMN_PREFIX_DICTIONARY] = \
-        utils.get_specific_tag_columns_from_form(form_request_object)
-    validation_input_arguments[common_constants.WORKSHEET_NAME] = \
-        utils.get_optional_form_field(form_request_object, common_constants.WORKSHEET_NAME,
-                                      common_constants.STRING)
-    validation_input_arguments[common_constants.HAS_COLUMN_NAMES] = utils.get_optional_form_field(
-        form_request_object, common_constants.HAS_COLUMN_NAMES, common_constants.BOOLEAN)
-    validation_input_arguments[common_constants.CHECK_FOR_WARNINGS] = utils.get_optional_form_field(
+        web_utils.get_uploaded_file_path_from_form(form_request_object, common_constants.JSON_FILE,
+                                                   file_constants.DICTIONARY_FILE_EXTENSIONS)
+
+    input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
+                       common_constants.HED_DISPLAY_NAME: hed_display_name,
+                       common_constants.JSON_PATH: uploaded_file_name,
+                       common_constants.JSON_FILE: original_file_name}
+    input_arguments[common_constants.CHECK_FOR_WARNINGS] = utils.get_optional_form_field(
         form_request_object, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
-    return validation_input_arguments
+    return input_arguments
 
 
-def generate_spreadsheet_validation_filename(spreadsheet_filename, worksheet_name=''):
-    """Generates a filename for the attachment that will contain the spreadsheet validation issues.
+def generate_dictionary_validation_filename(dictionary_filename):
+    """Generates a filename for the attachment that will contain the dictionary validation issues.
 
     Parameters
     ----------
-    spreadsheet_filename: string
-        The name of the spreadsheet other.
-    worksheet_name: string
-        The name of the spreadsheet worksheet.
+    dictionary_filename: string
+        The name of the dictionary file.
+
     Returns
     -------
     string
         The name of the attachment other containing the validation issues.
     """
-    if worksheet_name:
-        return common_constants.VALIDATION_OUTPUT_FILE_PREFIX + \
-               secure_filename(spreadsheet_filename).rsplit('.')[0] + '_' + \
-               secure_filename(worksheet_name) + file_constants.TEXT_EXTENSION
-    return common_constants.VALIDATION_OUTPUT_FILE_PREFIX + secure_filename(spreadsheet_filename).rsplit('.')[
-        0] + file_constants.TEXT_EXTENSION
-
 
-# def get_uploaded_file_paths_from_forms(form_request_object):
-#     """Gets the other paths of the uploaded files in the form.
-#
-#     Parameters
-#     ----------
-#     form_request_object: Request object
-#         A Request object containing user data from the validation form.
-#
-#     Returns
-#     -------
-#     tuple
-#         A tuple containing the other paths. The two other paths are for the spreadsheet and a optional HED XML other.
-#     """
-#     spreadsheet_file_path = ''
-#     hed_file_path = ''
-#     if common_constants.SPREADSHEET_FILE in form_request_object.files and \
-#         web_utils.file_extension_is_valid(form_request_object.files[common_constants.SPREADSHEET_FILE].filename,
-#                                           file_constants.SPREADSHEET_FILE_EXTENSIONS):
-#         spreadsheet_file_path = web_utils.save_file_to_upload_folder(
-#             form_request_object.files[common_constants.SPREADSHEET_FILE])
-#     if common_constants.HED_XML_FILE in form_request_object.files and \
-#             web_utils.file_extension_is_valid(form_request_object.files[common_constants.HED_XML_FILE].filename,
-#                                              [file_constants.SCHEMA_XML_EXTENSION]):
-#         hed_file_path = web_utils.save_file_to_upload_folder(form_request_object.files[common_constants.HED_XML_FILE])
-#         hed_file_path = web_utils.get_hed_path_from_form(form_request_object, hed_file_path)
-#     elif
-#     return spreadsheet_file_path, hed_file_path
+    return common_constants.VALIDATION_OUTPUT_FILE_PREFIX + secure_filename(dictionary_filename).rsplit('.')[
+        0] + file_constants.TEXT_EXTENSION
 
 
 def report_eeg_events_validation_status(request):
@@ -149,7 +108,7 @@ def report_eeg_events_validation_status(request):
     return validation_status
 
 
-def report_spreadsheet_validation_status(form_request_object):
+def report_dictionary_validation_status(form_request_object):
     """Reports the spreadsheet validation status.
 
     Parameters
@@ -166,56 +125,56 @@ def report_spreadsheet_validation_status(form_request_object):
     input_arguments = []
     try:
         input_arguments = generate_arguments_from_validation_form(form_request_object)
-        hed_validator = validate_spreadsheet(input_arguments)
-        validation_issues = hed_validator.get_validation_issues()
+        def_group = validate_dictionary(input_arguments)
+        validation_issues = def_group.get_validation_issues()
         if validation_issues:
-            issue_file = save_issues_to_upload_folder(input_arguments[common_constants.SPREADSHEET_FILE],
-                                                      validation_issues,
-                                                      input_arguments[common_constants.WORKSHEET_NAME])
+            issue_file = save_issues_to_upload_folder(input_arguments[common_constants.JSON_FILE],
+                                                      validation_issues)
             download_response = web_utils.generate_download_file_response(issue_file)
             if isinstance(download_response, str):
                 return web_utils.handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
             return download_response
     except HTTPError:
-            return error_constants.NO_URL_CONNECTION_ERROR
+        return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
         return error_constants.INVALID_URL_ERROR
     except Exception as e:
         return "Unexpected processing error: " + str(e)
     finally:
-        delete_file_if_it_exist(input_arguments[common_constants.SPREADSHEET_PATH])
+        delete_file_if_it_exist(input_arguments[common_constants.JSON_PATH])
         # delete_file_if_it_exist(input_arguments[common_constants.HED_XML_FILE])
     return ""
 
 
-def save_issues_to_upload_folder(spreadsheet_filename, validation_issues, worksheet_name=''):
+def save_issues_to_upload_folder(dictionary_filename, validation_issues, file_mode='w'):
     """Saves the validation issues found to a other in the upload folder.
 
     Parameters
     ----------
-    spreadsheet_filename: string
-        The name to the spreadsheet.
-    worksheet_name: string
-        The name of the spreadsheet worksheet.
-    validation_issues: string
+    dictionary_filename: str
+        The name to the dictionary.
+    validation_issues: dictionary
         A string containing the validation issues.
-
+    file_mode: str
+        Either 'w' to create a new file and write or 'a' to create a new file if necessary and append to end.
     Returns
     -------
     string
         The name of the validation output other.
 
     """
-    validation_issues_filename = generate_spreadsheet_validation_filename(spreadsheet_filename, worksheet_name)
+    validation_issues_filename = generate_dictionary_validation_filename(dictionary_filename)
     validation_issues_file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], validation_issues_filename)
-    with open(validation_issues_file_path, 'w', encoding='utf-8') as validation_issues_file:
-        for val_issue in validation_issues:
-            validation_issues_file.write(val_issue['message'] + "\n")
+    with open(validation_issues_file_path, file_mode, encoding='utf-8') as validation_issues_file:
+        for val_issue in validation_issues.values():
+            for issue in val_issue:
+                validation_issues_file.write(issue['message'] + "\n")
+    validation_issues_file.close()
     return validation_issues_file_path
 
 
-def validate_spreadsheet(validation_arguments):
-    """Validates the spreadsheet.
+def validate_dictionary(validation_arguments):
+    """Validates the dictionary.
 
     Parameters
     ----------
@@ -224,17 +183,11 @@ def validate_spreadsheet(validation_arguments):
 
     Returns
     -------
-    HedValidator object
-        A HedValidator object containing the validation results.
+    ColumnDefGroup
+        Contains a representation of the JSON dictionary and validation issues.
     """
 
-    file_input_object = HedFileInput(validation_arguments[common_constants.SPREADSHEET_PATH],
-                                     worksheet_name=validation_arguments[common_constants.WORKSHEET_NAME],
-                                     tag_columns=validation_arguments[common_constants.TAG_COLUMNS],
-                                     has_column_names=validation_arguments[common_constants.HAS_COLUMN_NAMES],
-                                     column_prefix_dictionary=validation_arguments[
-                                         common_constants.COLUMN_PREFIX_DICTIONARY])
-
-    return HedValidator(file_input_object,
-                        check_for_warnings=validation_arguments[common_constants.CHECK_FOR_WARNINGS],
-                        hed_xml_file=validation_arguments[common_constants.HED_XML_FILE])
+    json_dictionary = ColumnDefGroup(validation_arguments[common_constants.JSON_PATH])
+    hed_schema = HedSchema(validation_arguments[common_constants.HED_XML_FILE])
+    json_dictionary.validate_entries(hed_schema)
+    return json_dictionary
diff --git a/hedweb/hed/web/routes.py b/hedweb/hed/web/routes.py
index c0814412b..e490ea88d 100644
--- a/hedweb/hed/web/routes.py
+++ b/hedweb/hed/web/routes.py
@@ -6,9 +6,7 @@
 import hed.util.file_util
 from hed.util import hed_cache
 
-from hed.web import utils
-from hed.web import spreadsheet
-from hed.web import schema
+from hed.web import dictionary, spreadsheet, schema, utils
 from hed.web.constants import blueprint_constants, common_constants, error_constants, page_constants, route_constants
 from hed.web.web_utils import delete_file_if_it_exist, find_hed_version_in_uploaded_file, save_file_to_upload_folder, \
     generate_download_file_response, handle_http_error
@@ -74,20 +72,19 @@ def get_dictionary_validation_results():
         500 error message is returned.
     """
 
-    return "Dictionary validation is not yet implemented"
-    # validation_response = spreadsheet.report_spreadsheet_validation_status(request)
-    # # Success
-    # if isinstance(validation_response, Response):
-    #     return validation_response
-    # if isinstance(validation_response, str):
-    #     if validation_response:
-    #         return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_response, as_text=True)
-    #     else:
-    #         return ""
+    validation_response = dictionary.report_dictionary_validation_status(request)
+    # Success
+    if isinstance(validation_response, Response):
+        return validation_response
+    if isinstance(validation_response, str):
+        if validation_response:
+            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_response, as_text=True)
+        else:
+            return ""
 
 
-@route_blueprint.route(route_constants.SCHEMA_DUPLICATE_TAG_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
-def get_duplicate_tag_results():
+@route_blueprint.route(route_constants.SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_schema_compliance_check_results():
     """Check the HED specification in the form after submission and return an attachment other containing the output.
 
     Parameters
@@ -99,7 +96,7 @@ def get_duplicate_tag_results():
         A serialized JSON string containing the hed specification to check. If the conversion fails then a
         500 error message is returned.
     """
-    comparison_response = schema.run_schema_duplicate_tag_detection(request)
+    comparison_response = schema.run_schema_compliance_check(request)
     # Success
     if isinstance(comparison_response, Response):
         return comparison_response
diff --git a/hedweb/hed/web/schema.py b/hedweb/hed/web/schema.py
index 2fb4845d0..f11ed58ec 100644
--- a/hedweb/hed/web/schema.py
+++ b/hedweb/hed/web/schema.py
@@ -4,68 +4,18 @@
 from urllib.parse import urlparse
 from flask import current_app, Response
 
-from hed.schema import xml2wiki, wiki2xml
-from hed.util.hed_dictionary import HedDictionary
-from hed.util.file_util import delete_file_if_it_exist, url_to_file, get_file_extension, write_text_iter_to_file
+from hed.schema import xml2wiki, wiki2xml, schema_validator
+from hed.util.file_util import delete_file_if_it_exist, url_to_file, get_file_extension, write_errors_to_file
 from hed.util.exceptions import SchemaFileError
 
-from hed.web.web_utils import check_if_option_in_form, file_extension_is_valid, handle_http_error, \
-    save_file_to_upload_folder, generate_download_file_response
+from hed.web.web_utils import file_extension_is_valid, form_has_file, form_has_option, form_has_url, \
+    handle_http_error, save_file_to_upload_folder, generate_download_file_response
 
 from hed.web.constants import common_constants, error_constants, file_constants
 
 app_config = current_app.config
 
 
-def form_has_hed_file(form_request_object, valid_extensions):
-    """Checks to see if a HED file is present in a request object from schema form.
-
-    Parameters
-    ----------
-    form_request_object: Request object
-        A Request object containing user data from the schema form.
-    valid_extensions: list of str
-        List of valid extensions
-
-    Returns
-    -------
-    boolean
-        True if a HED file is present in a request object from the schema form.
-
-    """
-    if check_if_option_in_form(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
-                               common_constants.SCHEMA_FILE_OPTION) and \
-        common_constants.SCHEMA_FILE in form_request_object.files and \
-            file_extension_is_valid(form_request_object.files[common_constants.SCHEMA_FILE].filename, valid_extensions):
-        return True
-    else:
-        return False
-
-
-def form_has_hed_url(form_request_object, valid_extensions):
-    """Checks to see if a HED URL is present in a request object from schema form.
-
-    Parameters
-    ----------
-    form_request_object: Request object
-        A Request object containing user data from the schema form.
-    valid_extensions: list of str
-        List of valid extensions
-
-    Returns
-    -------
-    boolean
-        True if a HED URL is present in a request object from the schema form.
-
-    """
-    if not check_if_option_in_form(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
-                                   common_constants.SCHEMA_URL_OPTION) or \
-            common_constants.SCHEMA_URL not in form_request_object.values:
-        return False
-    parsed_url = urlparse(form_request_object.values.get(common_constants.SCHEMA_URL))
-    return file_extension_is_valid(parsed_url.path, valid_extensions)
-
-
 def generate_input_from_schema_form(form_request_object):
     """Gets the conversion function input arguments from a request object associated with the conversion form.
 
@@ -81,11 +31,15 @@ def generate_input_from_schema_form(form_request_object):
     """
     arguments = {}
    
-    if form_has_hed_file(form_request_object, file_constants.SCHEMA_EXTENSIONS):
+    if form_has_option(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
+                       common_constants.SCHEMA_FILE_OPTION) and \
+            form_has_file(form_request_object, common_constants.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
         schema_file = form_request_object.files[common_constants.SCHEMA_FILE]
         arguments[common_constants.SCHEMA_PATH] = save_file_to_upload_folder(schema_file)
         arguments[common_constants.SCHEMA_DISPLAY_NAME] = schema_file.filename
-    elif form_has_hed_url(form_request_object, file_constants.SCHEMA_EXTENSIONS):
+    elif form_has_option(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
+                         common_constants.SCHEMA_URL_OPTION) and \
+            form_has_url(form_request_object, common_constants.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
         schema_url = form_request_object.values[common_constants.SCHEMA_URL]
         arguments[common_constants.SCHEMA_PATH] = url_to_file(schema_url)
         url_parsed = urlparse(schema_url)
@@ -110,7 +64,7 @@ def get_schema_conversion(schema_local_path):
             conversion_function = wiki2xml.convert_hed_wiki_2_xml
         else:
             raise ValueError(f"Invalid extension type: {input_extension}")
-        converted_schema_path, errors = conversion_function(None, schema_local_path)
+        converted_schema_path, errors = conversion_function(None, schema_local_path, check_for_issues=False)
     except ValueError as error:
         errors = format(error)
         converted_schema_path = ''
@@ -118,29 +72,46 @@ def get_schema_conversion(schema_local_path):
     return converted_schema_path, errors
 
 
-# def get_uploaded_file_paths_from_schema_form(form_request_object):
-#     """Gets the other paths of the uploaded files in the form.
-# 
-#     Parameters
-#     ----------
-#     form_request_object: Request object
-#         A Request object containing user data from the schema form.
-# 
-#     Returns
-#     -------
-#     string
-#         The local file path, if exists.
-#     """
-#     schema_local_path = ''
-#     hed_present_in_form = check_if_option_in_form(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
-#                                                   common_constants.SCHEMA_FILE_OPTION) and \
-#         common_constants.SCHEMA_FILE in form_request_object.files
-#     if hed_present_in_form and file_extension_is_valid(
-#             form_request_object.files[common_constants.SCHEMA_FILE].filename, file_constants.SCHEMA_EXTENSIONS):
-#         schema_local_path = save_file_to_upload_folder(form_request_object.files[common_constants.SCHEMA_FILE])
-#     elif form_has_hed_url(form_request_object):
-#         schema_local_path = url_to_file(form_request_object.values[common_constants.SCHEMA_URL])
-#     return schema_local_path
+def run_schema_compliance_check(form_request_object):
+    """Run tag comparison(map_schema from converter)
+
+    returns: Response or string.
+        Empty string is success, but nothing to download.
+        Non empty string is an error
+        Response is a download success.
+    """
+    hed_file_path = ''
+    try:
+        conversion_input_arguments = generate_input_from_schema_form(form_request_object)
+        hed_file_path = conversion_input_arguments.get(common_constants.SCHEMA_PATH, '')
+        if hed_file_path and hed_file_path.endswith(".mediawiki"):
+            new_file_path, errors = get_schema_conversion(hed_file_path)
+            if new_file_path:
+                delete_file_if_it_exist(hed_file_path)
+                hed_file_path = new_file_path
+
+        if not hed_file_path or not file_extension_is_valid(hed_file_path, [file_constants.SCHEMA_XML_EXTENSION]):
+            return f"Invalid file name {hed_file_path}"
+        issues = schema_validator.validate_schema(hed_file_path)
+
+        if issues:
+            #issue_file = write_text_iter_to_file(issues)
+            issue_file = write_errors_to_file(issues, extension=".txt")
+            download_response = generate_download_file_response(issue_file, f"HED-3G format issues for: {hed_file_path}")
+            if isinstance(download_response, str):
+                return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
+            return download_response
+    except HTTPError:
+        return error_constants.NO_URL_CONNECTION_ERROR
+    except URLError:
+        return error_constants.INVALID_URL_ERROR
+    except SchemaFileError as e:
+        return e.message
+    except Exception as e:
+        return "Unexpected processing error: " + str(e)
+    finally:
+        delete_file_if_it_exist(hed_file_path)
+    return ""
 
 
 def run_schema_conversion(form_request_object):
@@ -175,43 +146,3 @@ def run_schema_conversion(form_request_object):
         return traceback.format_exc()
     finally:
         delete_file_if_it_exist(hed_file_path)
-
-
-def run_schema_duplicate_tag_detection(form_request_object):
-    """Run tag comparison(map_schema from converter)
-
-    returns: Response or string.
-        Empty string is success, but nothing to download.
-        Non empty string is an error
-        Response is a download success.
-    """
-    hed_file_path = ''
-    try:
-        conversion_input_arguments = generate_input_from_schema_form(form_request_object)
-        hed_file_path = conversion_input_arguments.get(common_constants.SCHEMA_PATH, '')
-        if hed_file_path and hed_file_path.endswith(".mediawiki"):
-            new_file_path, errors = get_schema_conversion(hed_file_path)
-            if new_file_path:
-                delete_file_if_it_exist(hed_file_path)
-                hed_file_path = new_file_path
-
-        if not hed_file_path or not file_extension_is_valid(hed_file_path, [file_constants.SCHEMA_XML_EXTENSION]):
-            return f"Invalid file name {hed_file_path}"
-        hed_dict = HedDictionary(hed_file_path)
-        if hed_dict.has_duplicate_tags():
-            dup_tag_file = write_text_iter_to_file(hed_dict.dupe_tag_iter(True))
-            download_response = generate_download_file_response(dup_tag_file, f"Duplicate tags for: {hed_file_path}")
-            if isinstance(download_response, str):
-                return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-            return download_response
-    except HTTPError:
-        return error_constants.NO_URL_CONNECTION_ERROR
-    except URLError:
-        return error_constants.INVALID_URL_ERROR
-    except SchemaFileError as e:
-        return e.message
-    except Exception as e:
-        return "Unexpected processing error: " + str(e)
-    finally:
-        delete_file_if_it_exist(hed_file_path)
-    return ""
diff --git a/hedweb/hed/web/spreadsheet.py b/hedweb/hed/web/spreadsheet.py
index 72983bc34..ca1e604d5 100644
--- a/hedweb/hed/web/spreadsheet.py
+++ b/hedweb/hed/web/spreadsheet.py
@@ -74,35 +74,6 @@ def generate_spreadsheet_validation_filename(spreadsheet_filename, worksheet_nam
         0] + file_constants.TEXT_EXTENSION
 
 
-# def get_uploaded_file_paths_from_forms(form_request_object):
-#     """Gets the other paths of the uploaded files in the form.
-#
-#     Parameters
-#     ----------
-#     form_request_object: Request object
-#         A Request object containing user data from the validation form.
-#
-#     Returns
-#     -------
-#     tuple
-#         A tuple containing the other paths. The two other paths are for the spreadsheet and a optional HED XML other.
-#     """
-#     spreadsheet_file_path = ''
-#     hed_file_path = ''
-#     if common_constants.SPREADSHEET_FILE in form_request_object.files and \
-#         web_utils.file_extension_is_valid(form_request_object.files[common_constants.SPREADSHEET_FILE].filename,
-#                                           file_constants.SPREADSHEET_FILE_EXTENSIONS):
-#         spreadsheet_file_path = web_utils.save_file_to_upload_folder(
-#             form_request_object.files[common_constants.SPREADSHEET_FILE])
-#     if common_constants.HED_XML_FILE in form_request_object.files and \
-#             web_utils.file_extension_is_valid(form_request_object.files[common_constants.HED_XML_FILE].filename,
-#                                              [file_constants.SCHEMA_XML_EXTENSION]):
-#         hed_file_path = web_utils.save_file_to_upload_folder(form_request_object.files[common_constants.HED_XML_FILE])
-#         hed_file_path = web_utils.get_hed_path_from_form(form_request_object, hed_file_path)
-#     elif
-#     return spreadsheet_file_path, hed_file_path
-
-
 def report_eeg_events_validation_status(request):
     """Reports validation status of hed strings associated with EEG events
        received from EEGLAB plugin HEDTools
@@ -177,7 +148,7 @@ def report_spreadsheet_validation_status(form_request_object):
                 return web_utils.handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
             return download_response
     except HTTPError:
-            return error_constants.NO_URL_CONNECTION_ERROR
+        return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
         return error_constants.INVALID_URL_ERROR
     except Exception as e:
@@ -211,6 +182,7 @@ def save_issues_to_upload_folder(spreadsheet_filename, validation_issues, worksh
     with open(validation_issues_file_path, 'w', encoding='utf-8') as validation_issues_file:
         for val_issue in validation_issues:
             validation_issues_file.write(val_issue['message'] + "\n")
+    validation_issues_file.close()
     return validation_issues_file_path
 
 
diff --git a/hedweb/hed/web/static/css/dictionary-form-helper.css b/hedweb/hed/web/static/css/dictionary-form.css
similarity index 100%
rename from hedweb/hed/web/static/css/dictionary-form-helper.css
rename to hedweb/hed/web/static/css/dictionary-form.css
diff --git a/hedweb/hed/web/static/css/multi-file-input.css b/hedweb/hed/web/static/css/multi-file-input.css
new file mode 100644
index 000000000..80d6601a7
--- /dev/null
+++ b/hedweb/hed/web/static/css/multi-file-input.css
@@ -0,0 +1,253 @@
+/*!
+ * @copyright Copyright &copy; Kartik Visweswaran, Krajee.com, 2014 - 2015
+ * @package bootstrap-fileinput
+ * @version 4.3.1
+ *
+ * File input styling for Bootstrap 3.0
+ * Built for Yii Framework 2.0
+ * Author: Kartik Visweswaran
+ * Year: 2015
+ * For more Yii related demos visit http://demos.krajee.com
+ */
+.file-loading {
+    top: 0;
+    right: 0;
+    width: 25px;
+    height: 25px;
+    font-size: 999px;
+    text-align: right;
+    color: #fff;
+    background: transparent url('../img/loading.gif') top left no-repeat;
+    border: none;
+}
+
+.file-object {
+    margin: 0 0 -5px 0;
+    padding: 0;
+}
+
+.btn-file {
+    position: relative;
+    overflow: hidden;
+}
+
+.btn-file input[type=file] {
+    position: absolute;
+    top: 0;
+    right: 0;
+    min-width: 100%;
+    min-height: 100%;
+    text-align: right;
+    opacity: 0;
+    background: none repeat scroll 0 0 transparent;
+    cursor: inherit;
+    display: block;
+}
+
+.file-caption-name {
+    display: inline-block;
+    overflow: hidden;
+    height: 20px;
+    word-break: break-all;
+}
+
+.input-group-lg .file-caption-name {
+    height: 25px;
+}
+
+.file-preview-detail-modal {
+    text-align: left;
+}
+
+.file-error-message {
+    color: #a94442;
+    background-color: #f2dede;
+    margin: 5px;
+    border: 1px solid #ebccd1;
+    border-radius: 4px;
+    padding: 15px;
+}
+
+.file-error-message pre, .file-error-message ul {
+    margin: 0;
+    text-align: left;
+}
+
+.file-error-message pre {
+    margin: 5px 0;
+}
+
+.file-caption-disabled {
+    background-color: #EEEEEE;
+    cursor: not-allowed;
+    opacity: 1;
+}
+
+.file-preview {
+    border-radius: 5px;
+    border: 1px solid #ddd;
+    padding: 5px;
+    width: 100%;
+    margin-bottom: 5px;
+}
+
+.file-preview-frame {
+    display: table;
+    margin: 8px;
+    height: 160px;
+    border: 1px solid #ddd;
+    box-shadow: 1px 1px 5px 0 #a2958a;
+    padding: 6px;
+    float: left;
+    text-align: center;
+    vertical-align: middle;
+}
+
+.file-preview-frame:not(.file-preview-error):hover {
+    box-shadow: 3px 3px 5px 0 #333;
+}
+
+.file-preview-image {
+    height: 160px;
+    vertical-align: middle;
+}
+
+.file-preview-text {
+    text-align: left;
+    width: 160px;
+    margin-bottom: 2px;
+    color: #428bca;
+    background: #fff;
+    overflow-x: hidden;
+}
+
+.file-preview-other {
+    display: table-cell;
+    text-align: center;
+    vertical-align: middle;
+    width: 160px;
+    height: 160px;
+    border: 2px solid #999;
+    border-radius: 30px;
+}
+
+.file-preview-other:hover {
+    opacity: 0.8;
+}
+
+.file-actions, .file-other-error {
+    text-align: left;
+}
+
+.file-icon-lg {
+    font-size: 1.2em;
+}
+
+.file-icon-2x {
+    font-size: 2.4em;
+}
+
+.file-icon-4x {
+    font-size: 4.8em;
+}
+
+.file-input-new .file-preview, .file-input-new .close, .file-input-new .glyphicon-file,
+.file-input-new .fileinput-remove-button, .file-input-new .fileinput-upload-button,
+.file-input-ajax-new .fileinput-remove-button, .file-input-ajax-new .fileinput-upload-button {
+    display: none;
+}
+
+.file-thumb-loading {
+    background: transparent url('../img/loading.gif') no-repeat scroll center center content-box !important;
+}
+
+.file-actions {
+    margin-top: 15px;
+}
+
+.file-footer-buttons {
+    float: right;
+}
+
+.file-upload-indicator {
+    padding-top: 2px;
+    cursor: default;
+    opacity: 0.8;
+    width: 60%;
+}
+
+.file-upload-indicator:hover {
+    font-weight: bold;
+    opacity: 1;
+}
+
+.file-footer-caption {
+    display: block;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 160px;
+    text-align: center;
+    padding-top: 4px;
+    font-size: 11px;
+    color: #777;
+    margin: 5px auto 10px auto;
+}
+
+.file-preview-error {
+    opacity: 0.65;
+    box-shadow: none;
+}
+
+.file-preview-frame:not(.file-preview-error) .file-footer-caption:hover {
+    color: #000;
+}
+
+.file-drop-zone {
+    border: 1px dashed #aaa;
+    border-radius: 4px;
+    height: 100%;
+    text-align: center;
+    vertical-align: middle;
+    margin: 12px 15px 12px 12px;
+    padding: 5px;
+}
+
+.file-drop-zone-title {
+    color: #aaa;
+    font-size: 40px;
+    padding: 85px 10px;
+}
+
+.file-highlighted {
+    border: 2px dashed #999 !important;
+    background-color: #f0f0f0;
+}
+
+.file-uploading {
+    background: url('../img/loading-sm.gif') no-repeat center bottom 10px;
+    opacity: 0.65;
+}
+
+.file-thumb-progress .progress, .file-thumb-progress .progress-bar {
+    height: 10px;
+    font-size: 9px;
+    line-height: 10px;
+}
+
+.file-thumbnail-footer {
+    position: relative;
+}
+
+.file-thumb-progress {
+    position: absolute;
+    top: 22px;
+    left: 0;
+    right: 0;
+}
+
+/* IE 10 fix */
+.btn-file ::-ms-browse {
+    width:100%;
+    height:100%;
+}
\ No newline at end of file
diff --git a/hedweb/hed/web/static/css/schema-form-helper.css b/hedweb/hed/web/static/css/schema-form.css
similarity index 83%
rename from hedweb/hed/web/static/css/schema-form-helper.css
rename to hedweb/hed/web/static/css/schema-form.css
index d113ab394..7a2056f07 100644
--- a/hedweb/hed/web/static/css/schema-form-helper.css
+++ b/hedweb/hed/web/static/css/schema-form.css
@@ -11,17 +11,24 @@
     margin-left: 1rem;
 }
 
-.form-group input[type="text"], .form-group input[type="file"], .form-group select, .form-group button {
+.form-group input[type="text"], .form-group input[type="file"], .form-group select {
     height: 2.35rem;
     font-size: 18px;
     margin-left: 0.6rem !important;
 }
 
 .form-group button {
-    width: 10rem;
+    height: 2.35rem;
+    width: 15rem;
+    font-size: 18px;
+    margin-left: 0.6rem !important;
+}
+
+.form-group button[id="schema-conversion-submit"]{
+    width: 15rem;
 }
 
-.form-group button[id="schema-conversion-submit"], .form-group button[id="schema-duplicate-tag-submit"]{
+.form-group button[id="schema-check-submit"]{
     width: 15rem;
 }
 
diff --git a/hedweb/hed/web/static/css/spreadsheet-form-helper.css b/hedweb/hed/web/static/css/spreadsheet-form.css
similarity index 100%
rename from hedweb/hed/web/static/css/spreadsheet-form-helper.css
rename to hedweb/hed/web/static/css/spreadsheet-form.css
diff --git a/hedweb/hed/web/templates/additional-examples.html b/hedweb/hed/web/templates/additional-examples.html
index f55ee5c53..619f5ea8d 100644
--- a/hedweb/hed/web/templates/additional-examples.html
+++ b/hedweb/hed/web/templates/additional-examples.html
@@ -3,7 +3,7 @@
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/doc.css') }}">
 {% endblock %}
 {% block content %}
-    <h1>Using the HED Schema to create tags</h1>
+    <h1>Using the HED Schema to create tags (Under revision)</h1>
     <div id="table_of_contents">
         <h2>Contents</h2>
         <ul id="outer_ul">
diff --git a/hedweb/hed/web/templates/common-errors.html b/hedweb/hed/web/templates/common-errors.html
index f9f9c82c8..4d9e5b233 100644
--- a/hedweb/hed/web/templates/common-errors.html
+++ b/hedweb/hed/web/templates/common-errors.html
@@ -3,7 +3,7 @@
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/doc.css') }}">
 {% endblock %}
 {% block content %}
-    <h1>Examples of common errors and fixes</h1>
+    <h1>Examples of common errors and fixes (under revision)</h1>
     <div id="table_of_contents">
         <h2>Contents</h2>
         <ul id="outer_ul">
diff --git a/hedweb/hed/web/templates/dictionary-form-helper.html b/hedweb/hed/web/templates/dictionary-form-helper.html
deleted file mode 100644
index 449150dbf..000000000
--- a/hedweb/hed/web/templates/dictionary-form-helper.html
+++ /dev/null
@@ -1,50 +0,0 @@
-{% macro create_dictionary_validation_form() %}
-    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/spreadsheet-form-helper.css') }}">
-    <form id="dictionary-form" method="POST" enctype="multipart/form-data">
-        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
-
-        <div class="form-group">
-            <label for="dictionary-file">Select JSON-formatted dictionaries:</label><br>
-            <label class="btn-group file-browse-group dictionary-btn-group">
-                <button for="dictionary-file">Browse ...</button>
-                <label id="dictionary-display-name"></label>
-                <input type="file" name="dictionary-file" id="dictionary-file"/>
-            </label>
-            <p class="flash" id="dictionary-flash"></p>
-        </div>
-
-        <div class="form-group">
-            <label for="hed-version">Choose HED version:</label><br>
-            <select name="hed-version" id="hed-version"></select><br>
-        </div>
-
-        <div class="form-group" id="hed-other-version">
-            <label class="secondary-label" for="hed-xml-file">Upload HED version:</label>
-            <label class="btn-group file-browse-group">
-                <button for="hed-xml-file">Browse ...</button>
-                <label id="hed-display-name"></label>
-                <input type="file" name="hed-xml-file" id="hed-xml-file"/>
-            </label>
-            <p class="flash" id="hed-flash"></p>
-        </div>
-
-        <div class="form-group">
-            <label>Specify additional options:</label><br>
-            <div class="checkbox-group">
-                <div class="inline-field">
-                    <input type="checkbox" name="check-for-warnings" id="check-for-warnings">
-                    <label for="check-for-warnings">Include warnings in output file</label>
-                </div>
-            </div>
-        </div>
-
-        <div class="form-group">
-            <button id="dictionary-validation-submit" type="button">Validate</button>
-            <p class="flash" id="dictionary-validation-submit-flash"></p>
-        </div>
-    </form>
-    <script type="module">
-        {%  include 'js/form-helpers.js' %}
-        {%  include 'js/dictionary-form-helper.js' %}
-    </script>
-{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/dictionary.html b/hedweb/hed/web/templates/dictionary.html
index 766fb5fad..67ba295e5 100644
--- a/hedweb/hed/web/templates/dictionary.html
+++ b/hedweb/hed/web/templates/dictionary.html
@@ -1,6 +1,35 @@
 {% extends "layout.html" %}
-{% from "dictionary-form-helper.html" import create_dictionary_validation_form %}
+{% from "json-input.html" import create_json_input %}
+{% from "hed-pulldown.html" import create_hed_pulldown %}
+
 {% block content %}
-    <h2>Validate JSON dictionary containing HED tags (in progress)</h2>
-    {{ create_dictionary_validation_form() }}
+    <h2>Validate JSON dictionary containing HED tags</h2>
+        <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dictionary-form.css') }}">
+    <form id="dictionary-form" method="POST" enctype="multipart/form-data">
+        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+
+        {{ create_json_input() }}
+        {{ create_hed_pulldown() }}
+
+        <div class="form-group">
+            <label>Specify additional options:</label><br>
+            <div class="checkbox-group">
+                <div class="inline-field">
+                    <input type="checkbox" name="check-for-warnings" id="check-for-warnings">
+                    <label for="check-for-warnings">Include warnings in output file</label>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <button id="dictionary-validation-submit" type="button">Validate</button>
+            <p class="flash" id="dictionary-validation-submit-flash"></p>
+        </div>
+    </form>
+    <script type="module">
+        {%  include 'js/form-helpers.js' %}
+        {%  include 'js/hed-pulldown.js' %}
+        {%  include 'js/json-input.js' %}
+        {%  include 'js/dictionary-form.js' %}
+    </script>
 {% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/spreadsheet-form-helper.html b/hedweb/hed/web/templates/events.html
similarity index 71%
rename from hedweb/hed/web/templates/spreadsheet-form-helper.html
rename to hedweb/hed/web/templates/events.html
index 78541f0c9..ef2311cf7 100644
--- a/hedweb/hed/web/templates/spreadsheet-form-helper.html
+++ b/hedweb/hed/web/templates/events.html
@@ -1,10 +1,13 @@
-{% macro create_spreadsheet_validation_form() %}
-    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/spreadsheet-form-helper.css') }}">
+{% extends "layout.html" %}
+{% from "hed-pulldown.html" import create_hed_pulldown %}
+{% block content %}
+    <h2>Validate spreadsheet containing HED tags</h2>
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/spreadsheet-form.css') }}">
     <form id="spreadsheet-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
         <div class="form-group">
-            <label for="spreadsheet-file">Select spreadsheet (Excel and tab-separated):</label><br>
+            <label for="spreadsheet-file">Select spreadsheet (Excel or tab-separated):</label><br>
             <label class="btn-group file-browse-group spreadsheet-btn-group">
                 <button for="spreadsheet-file">Browse ...</button>
                 <label id="spreadsheet-display-name"></label>
@@ -20,12 +23,12 @@
         </div>
 
         <div class="form-group" id="column-names">
-            <label class="secondary-label" for="columns-names-table">Spreadsheet columns</label>
+            <label class="secondary-label" for="columns-names-table">Worksheet columns</label>
             <table id="columns-names-table"></table>
         </div>
 
         <div class="form-group">
-            <label>Columns corresponding to specific tags (if available):</label><br>
+            <label>Special non-prefixed tag columns (if available):</label><br>
             <div class="textbox-group">
                 <label for="category-column">Category</label>
                 <input type="text" name="category-column" id="category-column">
@@ -36,33 +39,20 @@
                 <label for="long-column">Long name</label>
                 <input type="text" name="long-column" id="long-column">
             </div>
-            <label class="secondary-label" for="tag-columns">Other columns containing tags
+            <label class="secondary-label" for="tag-columns">Columns containing HED strings
                 (comma-separated):</label><br>
             <input class="textbox-group" type="text" name="tag-columns" id="tag-columns">
             <p class="flash" id="tag-columns-flash"></p>
         </div>
 
-        <div class="form-group">
-            <label for="hed-version">Choose HED version:</label><br>
-            <select name="hed-version" id="hed-version"></select><br>
-        </div>
-
-        <div class="form-group" id="hed-other-version">
-            <label class="secondary-label" for="hed-xml-file">Upload HED version:</label>
-            <label class="btn-group file-browse-group">
-                <button for="hed-xml-file">Browse ...</button>
-                <label id="hed-display-name"></label>
-                <input type="file" name="hed-xml-file" id="hed-xml-file"/>
-            </label>
-            <p class="flash" id="hed-flash"></p>
-        </div>
+        {{ create_hed_pulldown() }}
 
         <div class="form-group">
             <label>Specify additional options:</label><br>
             <div class="checkbox-group">
                 <div class="inline-field">
                     <input type="checkbox" name="has-column-names" id="has-column-names">
-                    <label for="has-column-names">Spreadsheet has column names</label>
+                    <label for="has-column-names">Worksheet has column names</label>
                 </div>
                 <div class="inline-field">
                     <input type="checkbox" name="check-for-warnings" id="check-for-warnings">
@@ -78,6 +68,8 @@
     </form>
     <script type="module">
         {%  include 'js/form-helpers.js' %}
-        {%  include 'js/spreadsheet-form-helper.js' %}
+        {%  include 'js/hed-pulldown.js' %}
+        {%  include 'js/spreadsheet-form.js' %}
     </script>
-{% endmacro %}
\ No newline at end of file
+
+{% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/hed-pulldown.html b/hedweb/hed/web/templates/hed-pulldown.html
new file mode 100644
index 000000000..b7471c307
--- /dev/null
+++ b/hedweb/hed/web/templates/hed-pulldown.html
@@ -0,0 +1,16 @@
+{% macro create_hed_pulldown() %}
+        <div class="form-group">
+            <label for="hed-version">Choose HED version:</label><br>
+            <select name="hed-version" id="hed-version"></select><br>
+        </div>
+
+        <div class="form-group" id="hed-other-version">
+            <label class="secondary-label" for="hed-xml-file">Upload HED version:</label>
+            <label class="btn-group file-browse-group">
+                <button for="hed-xml-file">Browse ...</button>
+                <label id="hed-display-name"></label>
+                <input type="file" name="hed-xml-file" id="hed-xml-file"/>
+            </label>
+            <p class="flash" id="hed-flash"></p>
+        </div>
+{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/js/dictionary-form-helper.js b/hedweb/hed/web/templates/js/dictionary-form-helper.js
deleted file mode 100644
index 69b374caf..000000000
--- a/hedweb/hed/web/templates/js/dictionary-form-helper.js
+++ /dev/null
@@ -1,273 +0,0 @@
-const DICTIONARY_FILE_EXTENSIONS = ['json'];
-const XML_FILE_EXTENSIONS = ['xml'];
-const HED_OTHER_VERSION_OPTION = 'Other';
-
-$(document).ready(function () {
-    prepareDictionaryValidationForm();
-});
-
-/**
- * Event handler function when the HED version drop-down menu changes. If Other is selected the file browser
- * underneath it will appear. If another option is selected then it will disappear.
- */
-$('#hed-version').change(function () {
-    if ($(this).val() === HED_OTHER_VERSION_OPTION) {
-        $('#hed-other-version').show();
-    } else {
-        hideOtherHEDVersionFileUpload()
-    }
-    flashMessageOnScreen('', 'success', 'hed-flash');
-});
-
-/**
- * Checks if the HED file uploaded has a valid extension.
- */
-$('#hed-xml-file').change(function () {
-    let hedSchema = $('#hed-xml-file');
-    let hedPath = hedSchema.val();
-    let hedFile = hedSchema[0].files[0];
-    if (fileHasValidExtension(hedPath, XML_FILE_EXTENSIONS)) {
-        getVersionFromHEDFile(hedFile);
-        updateHEDFileLabel(hedPath);
-    } else {
-        flashMessageOnScreen('Please upload a valid HED file (.xml)', 'error',
-            'hed-flash')
-    }
-});
-
-/**
- * Dictionary event handler function. Checks if the file uploaded has a valid dictionary extension.
- */
-$('#dictionary-file').change(function () {
-    let dictionary = $('#dictionary-file');
-    let dictionaryPath = dictionary.val();
-    resetFlashMessages();
-    if (cancelWasPressedInChromeFileUpload(dictionaryPath)) {
-        resetForm();
-    }
-    else if (fileHasValidExtension(dictionaryPath, DICTIONARY_FILE_EXTENSIONS)) {
-        updateDictionaryFileLabel(dictionaryPath);
-    } else {
-        resetForm();
-        flashMessageOnScreen('Please upload a JSON dictionary (.json)',
-            'error', 'dictionary-flash');
-    }
-});
-
-/**
- * Submits the form if the tag columns textbox is valid.
- */
-$('#dictionary-validation-submit').click(function () {
-    if (dictionaryIsSpecified() && hedSpecifiedWhenOtherIsSelected()) {
-        submitForm();
-    }
-});
-
-/**
- * Clears the dictionary file label.
- */
-function clearDictionaryFileLabel() {
-    $('#dictionary-display-name').text('');
-}
-
-
-/**
- * Checks to see if a dictionary is empty
- * @param {Array} dictionary - A dictionary
- * @returns {boolean} - True if the dictionary is empty. False, if otherwise.
- */
-function dictionaryIsEmpty(dictionary) {
-    return Object.keys(dictionary).length === 0;
-}
-
-
-/**
- * Gets the HED versions that are in the HED version drop-down menu.
- */
-function getHEDVersions() {
-    $.ajax({
-            type: 'GET',
-            url: "{{url_for('route_blueprint.get_major_hed_versions')}}",
-            contentType: false,
-            processData: false,
-            dataType: 'json',
-            success: function (hedInfo) {
-                populateHEDVersionsDropdown(hedInfo['hed-major-versions']);
-            },
-            error: function (jqXHR) {
-                console.log(jqXHR.responseJSON.message);
-                flashMessageOnScreen('Major HED versions could not be retrieved. Please provide your own.',
-                    'error', 'dictionary-flash');
-            }
-        }
-    );
-}
-
-/**
- * Gets the version from the HED file that the user uploaded.
- * @param {Object} hedXMLFile - A HED XML file.
- */
-function getVersionFromHEDFile(hedXMLFile) {
-    let formData = new FormData();
-    formData.append('hed-xml-file', hedXMLFile);
-    $.ajax({
-        type: 'POST',
-        url: "{{ url_for('route_blueprint.get_hed_version_in_file')}}",
-        data: formData,
-        contentType: false,
-        processData: false,
-        dataType: 'json',
-        success: function (hedInfo) {
-            resetFlashMessages();
-            flashMessageOnScreen('Using HED version ' + hedInfo['hed-version'],
-                'success', 'hed-flash');
-        },
-        error: function (jqXHR) {
-            console.log(jqXHR.responseJSON.message);
-            flashMessageOnScreen('Could not get version number from HED XML file.',
-                'error', 'hed-flash');
-        }
-    });
-}
-
-
-/**
- * Checks to see if a HED XML file is specified when the HED drop-down is set to "Other".
- */
-function hedSpecifiedWhenOtherIsSelected() {
-    let hedFile = $('#hed-xml-file');
-    let hedFileIsEmpty = hedFile[0].files.length === 0;
-    if ($('#hed-version').val() === HED_OTHER_VERSION_OPTION && hedFileIsEmpty) {
-        flashMessageOnScreen('HED version is not specified.', 'error', 'hed-flash');
-        return false;
-    }
-    return true;
-}
-
-/**
- * Hides the HED XML file upload.
- */
-function hideOtherHEDVersionFileUpload() {
-    $('#hed-display-name').text('');
-    $('#hed-other-version').hide();
-}
-
-
-/**
- * Populates the HED version drop-down menu.
- * @param {Array} hedVersions - An array containing the HED versions.
- */
-function populateHEDVersionsDropdown(hedVersions) {
-    let hedVersionDropdown = $('#hed-version');
-    hedVersionDropdown.append('<option value=' + hedVersions[0] + '>' + hedVersions[0] + ' (Latest)</option>');
-    for (let i = 1; i < hedVersions.length; i++) {
-        hedVersionDropdown.append('<option value=' + hedVersions[i] + '>' + hedVersions[i] + '</option>');
-    }
-    hedVersionDropdown.append('<option value=' + HED_OTHER_VERSION_OPTION + '>' + HED_OTHER_VERSION_OPTION +
-        '</option>');
-}
-
-
-/**
- * Prepare the validation form after the page is ready. The form will be reset to handle page refresh and
- * components will be hidden and populated.
- */
-function prepareDictionaryValidationForm() {
-    resetForm();
-    getHEDVersions()
-    hideOtherHEDVersionFileUpload();
-}
-
-/**
- * Resets the flash messages that aren't related to the form submission.
- */
-function resetFlashMessages() {
-    flashMessageOnScreen('', 'success', 'dictionary-flash');
-    flashMessageOnScreen('', 'success', 'hed-flash');
-    flashMessageOnScreen('', 'success', 'dictionary-validation-submit-flash');
-}
-
-/**
- * Resets the fields in the form.
- */
-function resetForm() {
-    $('#dictionary-form')[0].reset();
-    clearDictionaryFileLabel();
-    hideOtherHEDVersionFileUpload()
-}
-
-/**
- * Checks to see if a dictionary has been specified.
- */
-function dictionaryIsSpecified() {
-    let dictionaryFile = $('#dictionary-file');
-    if (dictionaryFile[0].files.length === 0) {
-        flashMessageOnScreen('Dictionary is not specified.', 'error',
-            'dictionary-flash');
-        return false;
-    }
-    return true;
-}
-
-/**
- * Submit the form and return the validation results. If there are issues then they are returned in an attachment
- * file.
- */
-function submitForm() {
-    let dictionaryForm = document.getElementById("dictionary-form");
-    let formData = new FormData(dictionaryForm);
-    let prefix = 'validation_issues';
-
-    let dictionaryFile = $('#dictionary-file')[0].files[0].name;
-    let display_name = convertToResultsName(dictionaryFile, prefix)
-    resetFlashMessages();
-    flashMessageOnScreen('Dictionary is being validated ...', 'success',
-        'dictionary-validation-submit-flash')
-    $.ajax({
-            type: 'POST',
-            url: "{{url_for('route_blueprint.get_dictionary_validation_results')}}",
-            data: formData,
-            contentType: false,
-            processData: false,
-            dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success',
-                          'dictionary-validation-submit-flash');
-                      triggerDownloadBlob(downloaded_file, display_name);
-                  } else {
-                      flashMessageOnScreen('No validation errors found.', 'success',
-                          'dictionary-validation-submit-flash');
-                  }
-            },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'dictionary-validation-submit-flash');
-                } else {
-                    flashMessageOnScreen('Dictionary could not be processed',
-                        'error','dictionary-validation-submit-flash');
-                }
-            }
-        }
-    )
-}
-
-/**
- * Updates the HED file label.
- * @param {String} hedPath - The path to the HED XML file.
- */
-function updateHEDFileLabel(hedPath) {
-    let hedFilename = hedPath.split('\\').pop();
-    $('#hed-display-name').text(hedFilename);
-}
-
-/**
- * Updates the dictionary file label.
- * @param {String} dictionaryPath - The path to the dictionary.
- */
-function updateDictionaryFileLabel(dictionaryPath) {
-    let dictionaryFilename = dictionaryPath.split('\\').pop();
-    $('#dictionary-display-name').text(dictionaryFilename);
-}
diff --git a/hedweb/hed/web/templates/js/dictionary-form.js b/hedweb/hed/web/templates/js/dictionary-form.js
new file mode 100644
index 000000000..a8547bc58
--- /dev/null
+++ b/hedweb/hed/web/templates/js/dictionary-form.js
@@ -0,0 +1,98 @@
+const DICTIONARY_FILE_EXTENSIONS = ['json'];
+
+$(function () {
+    prepareDictionaryValidationForm();
+});
+
+
+/**
+ * Submits the form if the tag columns textbox is valid.
+ */
+$('#dictionary-validation-submit').on('click', function () {
+    if (jsonIsSpecified() && hedSpecifiedWhenOtherIsSelected()) {
+        submitForm();
+    }
+});
+
+
+/**
+ * Prepare the validation form after the page is ready. The form will be reset to handle page refresh and
+ * components will be hidden and populated.
+ */
+function prepareDictionaryValidationForm() {
+    resetDictionaryForm();
+    getHEDVersions()
+    hideOtherHEDVersionFileUpload();
+}
+
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ */
+function resetFlashMessages() {
+    clearJsonFlashMessage();
+    clearHEDFlashMessage();
+    flashMessageOnScreen('', 'success', 'dictionary-validation-submit-flash');
+}
+
+/**
+ * Resets the fields in the form.
+ */
+function resetDictionaryForm() {
+    $('#dictionary-form')[0].reset();
+    clearJsonFileLabel();
+    hideOtherHEDVersionFileUpload()
+}
+
+/**
+ * Submit the form and return the validation results. If there are issues then they are returned in an attachment
+ * file.
+ */
+function submitForm() {
+    let dictionaryForm = document.getElementById("dictionary-form");
+    let formData = new FormData(dictionaryForm);
+
+    let dictionaryFile = getJsonFileLabel();
+    let display_name = convertToResultsName(dictionaryFile, 'validation_issues')
+    resetFlashMessages();
+    flashMessageOnScreen('Dictionary is being validated ...', 'success',
+        'dictionary-validation-submit-flash')
+    $.ajax({
+            type: 'POST',
+            url: "{{url_for('route_blueprint.get_dictionary_validation_results')}}",
+            data: formData,
+            contentType: false,
+            processData: false,
+            dataType: 'text',
+            success: function (downloaded_file) {
+                  if (downloaded_file) {
+                      flashMessageOnScreen('', 'success',
+                          'dictionary-validation-submit-flash');
+                      triggerDownloadBlob(downloaded_file, display_name);
+                  } else {
+                      flashMessageOnScreen('No validation errors found.', 'success',
+                          'dictionary-validation-submit-flash');
+                  }
+            },
+            error: function (download_response) {
+                console.log(download_response.responseText);
+                if (download_response.responseText.length < 100) {
+                    flashMessageOnScreen(download_response.responseText, 'error',
+                        'dictionary-validation-submit-flash');
+                } else {
+                    flashMessageOnScreen('Dictionary could not be processed',
+                        'error','dictionary-validation-submit-flash');
+                }
+            }
+        }
+    )
+}
+
+
+/**
+ * Updates the dictionary file label.
+ * @param {String} dictionaryPath - The path to the dictionary.
+ */
+function updateDictionaryFileLabel(dictionaryPath) {
+    let dictionaryFilename = dictionaryPath.split('\\').pop();
+    $('#dictionary-display-name').text(dictionaryFilename);
+}
diff --git a/hedweb/hed/web/templates/js/form-helpers.js b/hedweb/hed/web/templates/js/form-helpers.js
index 10c91e0a4..f0f0b13f6 100644
--- a/hedweb/hed/web/templates/js/form-helpers.js
+++ b/hedweb/hed/web/templates/js/form-helpers.js
@@ -101,3 +101,12 @@ export function triggerDownloadBlob(download_text_blob, display_name) {
     document.body.appendChild(link);
     link.click();
 }
+
+/**
+ * Updates the dictionary file label.
+ * @param {String} filePath - The path to the dictionary.
+ */
+export function updateFileLabel(filePath, displayName) {
+    let filename = filePath.split('\\').pop();
+    $(displayName).text(filename);
+}
diff --git a/hedweb/hed/web/templates/js/hed-pulldown.js b/hedweb/hed/web/templates/js/hed-pulldown.js
new file mode 100644
index 000000000..6ef855bd3
--- /dev/null
+++ b/hedweb/hed/web/templates/js/hed-pulldown.js
@@ -0,0 +1,125 @@
+const XML_FILE_EXTENSIONS = ['xml'];
+const HED_OTHER_VERSION_OPTION = 'Other';
+
+/**
+ * Event handler function when the HED version drop-down menu changes. If Other is selected the file browser
+ * underneath it will appear. If another option is selected then it will disappear.
+ */
+$('#hed-version').on('change',function () {
+    if ($(this).val() === HED_OTHER_VERSION_OPTION) {
+        $('#hed-other-version').show();
+    } else {
+        hideOtherHEDVersionFileUpload()
+    }
+    flashMessageOnScreen('', 'success', 'hed-flash');
+});
+
+/**
+ * Checks if the HED file uploaded has a valid extension.
+ */
+$('#hed-xml-file').on('change', function () {
+    let hedSchema = $('#hed-xml-file');
+    let hedPath = hedSchema.val();
+    let hedFile = hedSchema[0].files[0];
+    if (fileHasValidExtension(hedPath, XML_FILE_EXTENSIONS)) {
+        getVersionFromHEDFile(hedFile);
+        updateFileLabel(hedPath, '#hed-display-name');
+    } else {
+        flashMessageOnScreen('Please upload a valid HED file (.xml)', 'error',
+            'hed-flash')
+    }
+})
+
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ */
+function clearHEDFlashMessage() {
+    flashMessageOnScreen('', 'success', 'hed-flash');
+}
+
+
+/**
+ * Gets the HED versions that are in the HED version drop-down menu.
+ */
+function getHEDVersions() {
+    $.ajax({
+            type: 'GET',
+            url: "{{url_for('route_blueprint.get_major_hed_versions')}}",
+            contentType: false,
+            processData: false,
+            dataType: 'json',
+            success: function (hedInfo) {
+                populateHEDVersionsDropdown(hedInfo['hed-major-versions']);
+            },
+            error: function (jqXHR) {
+                console.log(jqXHR.responseJSON.message);
+                flashMessageOnScreen('Major HED versions could not be retrieved. Please provide your own.',
+                    'error', 'dictionary-flash');
+            }
+        }
+    );
+}
+
+/**
+ * Gets the version from the HED file that the user uploaded.
+ * @param {Object} hedXMLFile - A HED XML file.
+ */
+function getVersionFromHEDFile(hedXMLFile) {
+    let formData = new FormData();
+    formData.append('hed-xml-file', hedXMLFile);
+    $.ajax({
+        type: 'POST',
+        url: "{{ url_for('route_blueprint.get_hed_version_in_file')}}",
+        data: formData,
+        contentType: false,
+        processData: false,
+        dataType: 'json',
+        success: function (hedInfo) {
+            resetFlashMessages();
+            flashMessageOnScreen('Using HED version ' + hedInfo['hed-version'],
+                'success', 'hed-flash');
+        },
+        error: function (jqXHR) {
+            console.log(jqXHR.responseJSON.message);
+            flashMessageOnScreen('Could not get version number from HED XML file.',
+                'error', 'hed-flash');
+        }
+    });
+}
+
+
+/**
+ * Checks to see if a HED XML file is specified when the HED drop-down is set to "Other".
+ */
+function hedSpecifiedWhenOtherIsSelected() {
+    let hedFile = $('#hed-xml-file');
+    let hedFileIsEmpty = hedFile[0].files.length === 0;
+    if ($('#hed-version').val() === HED_OTHER_VERSION_OPTION && hedFileIsEmpty) {
+        flashMessageOnScreen('HED version is not specified.', 'error', 'hed-flash');
+        return false;
+    }
+    return true;
+}
+
+/**
+ * Hides the HED XML file upload.
+ */
+function hideOtherHEDVersionFileUpload() {
+    $('#hed-display-name').text('');
+    $('#hed-other-version').hide();
+}
+
+
+/**
+ * Populates the HED version drop-down menu.
+ * @param {Array} hedVersions - An array containing the HED versions.
+ */
+function populateHEDVersionsDropdown(hedVersions) {
+    let hedVersionDropdown = $('#hed-version');
+    hedVersionDropdown.append('<option value=' + hedVersions[0] + '>' + hedVersions[0] + ' (Latest)</option>');
+    for (let i = 1; i < hedVersions.length; i++) {
+        hedVersionDropdown.append('<option value=' + hedVersions[i] + '>' + hedVersions[i] + '</option>');
+    }
+    hedVersionDropdown.append('<option value=' + HED_OTHER_VERSION_OPTION + '>' + HED_OTHER_VERSION_OPTION +
+        '</option>');
+}
diff --git a/hedweb/hed/web/templates/js/json-input.js b/hedweb/hed/web/templates/js/json-input.js
new file mode 100644
index 000000000..cddfbae39
--- /dev/null
+++ b/hedweb/hed/web/templates/js/json-input.js
@@ -0,0 +1,53 @@
+const JSON_FILE_EXTENSIONS = ['json'];
+
+/**
+ * Dictionary event handler function. Checks if the file uploaded has a valid dictionary extension.
+ */
+$('#json-file').on('change',function () {
+    let jsonPath = $('#json-file').val();
+    resetFlashMessages();
+    if (cancelWasPressedInChromeFileUpload(jsonPath)) {
+        resetForm();
+    }
+    else if (fileHasValidExtension(jsonPath, JSON_FILE_EXTENSIONS)) {
+        updateFileLabel(jsonPath, '#json-display-name');
+    } else {
+        resetForm();
+        flashMessageOnScreen('Please upload a JSON dictionary (.json)',
+            'error', 'json-flash');
+    }
+});
+
+
+/**
+ * Clears the dictionary file label.
+ */
+export function clearJsonFileLabel() {
+    $('#json-display-name').text('');
+}
+
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ */
+export function clearJsonFlashMessage() {
+    flashMessageOnScreen('', 'success', 'json-flash');
+}
+
+
+export function getJsonFileLabel() {
+    return $('#json-file')[0].files[0].name;
+}
+
+
+/**
+ * Checks to see if a dictionary has been specified.
+ */
+export function jsonIsSpecified() {
+    let jsonFile = $('#json-file');
+    if (jsonFile[0].files.length === 0) {
+        flashMessageOnScreen('JSON is not specified.', 'error',
+            'json-flash');
+        return false;
+    }
+    return true;
+}
diff --git a/hedweb/hed/web/templates/js/multi-file-input.js b/hedweb/hed/web/templates/js/multi-file-input.js
new file mode 100644
index 000000000..60ee07e37
--- /dev/null
+++ b/hedweb/hed/web/templates/js/multi-file-input.js
@@ -0,0 +1,2761 @@
+/*!
+ * @copyright Copyright &copy; Kartik Visweswaran, Krajee.com, 2014 - 2015
+ * @version 4.3.1
+ *
+ * File input styled for Bootstrap 3.0 that utilizes HTML5 File Input's advanced features including the FileReader API.
+ *
+ * The plugin drastically enhances the HTML file input to preview multiple files on the client before upload. In
+ * addition it provides the ability to preview content of images, text, videos, audio, html, flash and other objects.
+ * It also offers the ability to upload and delete files using AJAX, and add files in batches (i.e. preview, append,
+ * or remove before upload).
+ *
+ * Author: Kartik Visweswaran
+ * Copyright: 2015, Kartik Visweswaran, Krajee.com
+ * For more JQuery plugins visit http://plugins.krajee.com
+ * For more Yii related demos visit http://demos.krajee.com
+ */
+(function (factory) {
+    "use strict";
+    if (typeof define === 'function' && define.amd) { // jshint ignore:line
+        // AMD. Register as an anonymous module.
+        define(['jquery'], factory); // jshint ignore:line
+    } else { // noinspection JSUnresolvedVariable
+        if (typeof module === 'object' && module.exports) { // jshint ignore:line
+            // Node/CommonJS
+            // noinspection JSUnresolvedVariable
+            module.exports = factory(require('jquery')); // jshint ignore:line
+        } else {
+            // Browser globals
+            factory(window.jQuery);
+        }
+    }
+}(function ($) {
+    "use strict";
+
+    $.fn.fileinputLocales = {};
+
+    var NAMESPACE, objUrl, compare, isIE, isEdge, handler, previewCache, getNum, hasFileAPISupport, hasDragDropSupport,
+        hasFileUploadSupport, addCss, STYLE_SETTING, OBJECT_PARAMS, DEFAULT_PREVIEW, defaultFileActionSettings, tMain1,
+        tMain2, tPreview, tIcon, tClose, tCaption, tBtnDefault, tBtnLink, tBtnBrowse, tModal, tProgress, tFooter,
+        tActions, tActionDelete, tActionUpload, tZoom, tGeneric, tHtml, tImage, tText, tVideo, tAudio, tFlash, tObject,
+        tOther, defaultLayoutTemplates, defaultPreviewTemplates, defaultPreviewTypes, defaultPreviewSettings, FileInput,
+        defaultFileTypeSettings, isEmpty, isArray, isSet, getElement, uniqId, htmlEncode, replaceTags, cleanMemory;
+
+    NAMESPACE = '.fileinput';
+    //noinspection JSUnresolvedVariable
+    objUrl = window.URL || window.webkitURL;
+    compare = function (input, str, exact) {
+        return input !== undefined && (exact ? input === str : input.match(str));
+    };
+    isIE = function (ver) {
+        // check for IE versions < 11
+        if (navigator.appName !== 'Microsoft Internet Explorer') {
+            return false;
+        }
+        if (ver === 10) {
+            return new RegExp('msie\\s' + ver, 'i').test(navigator.userAgent);
+        }
+        var div = document.createElement("div"), status;
+        div.innerHTML = "<!--[if IE " + ver + "]> <i></i> <![endif]-->";
+        status = div.getElementsByTagName("i").length;
+        document.body.appendChild(div);
+        div.parentNode.removeChild(div);
+        return status;
+    };
+    isEdge = function () {
+        return new RegExp('Edge\/[0-9]+', 'i').test(navigator.userAgent);
+    };
+    handler = function ($el, event, callback, skipNS) {
+        var ev = skipNS ? event : event.split(' ').join(NAMESPACE + ' ') + NAMESPACE;
+        $el.off(ev).on(ev, callback);
+    };
+    previewCache = {
+        data: {},
+        init: function (obj) {
+            var content = obj.initialPreview, id = obj.id;
+            if (content.length > 0 && !isArray(content)) {
+                content = content.split(obj.initialPreviewDelimiter);
+            }
+            previewCache.data[id] = {
+                content: content,
+                config: obj.initialPreviewConfig,
+                tags: obj.initialPreviewThumbTags,
+                delimiter: obj.initialPreviewDelimiter,
+                template: obj.previewGenericTemplate,
+                msg: function (n) {
+                    return obj._getMsgSelected(n);
+                },
+                initId: obj.previewInitId,
+                footer: obj._getLayoutTemplate('footer').replace(/\{progress}/g, obj._renderThumbProgress()),
+                isDelete: obj.initialPreviewShowDelete,
+                caption: obj.initialCaption,
+                actions: function (showUpload, showDelete, disabled, url, key) {
+                    return obj._renderFileActions(showUpload, showDelete, disabled, url, key);
+                }
+            };
+        },
+        fetch: function (id) {
+            return previewCache.data[id].content.filter(function (n) {
+                return n !== null;
+            });
+        },
+        count: function (id, all) {
+            return !!previewCache.data[id] && !!previewCache.data[id].content ?
+                (all ? previewCache.data[id].content.length : previewCache.fetch(id).length) : 0;
+        },
+        get: function (id, i, isDisabled) {
+            var ind = 'init_' + i, data = previewCache.data[id], config = data.config[i],
+                previewId = data.initId + '-' + ind, out, $tmp, frameClass = ' file-preview-initial';
+            /** @namespace config.frameClass */
+            /** @namespace config.frameAttr */
+            isDisabled = isDisabled === undefined ? true : isDisabled;
+            if (data.content[i] === null) {
+                return '';
+            }
+            if (!isEmpty(config) && !isEmpty(config.frameClass)) {
+                frameClass += ' ' + config.frameClass;
+            }
+            out = data.template
+                .replace(/\{previewId}/g, previewId)
+                .replace(/\{frameClass}/g, frameClass)
+                .replace(/\{fileindex}/g, ind)
+                .replace(/\{content}/g, data.content[i])
+                .replace(/\{footer}/g, previewCache.footer(id, i, isDisabled));
+            if (data.tags.length && data.tags[i]) {
+                out = replaceTags(out, data.tags[i]);
+            }
+            if (!isEmpty(config) && !isEmpty(config.frameAttr)) {
+                $tmp = $(document.createElement('div')).html(out);
+                $tmp.find('.file-preview-initial').attr(config.frameAttr);
+                out = $tmp.html();
+                $tmp.remove();
+            }
+            return out;
+        },
+        add: function (id, content, config, tags, append) {
+            var data = $.extend(true, {}, previewCache.data[id]), index;
+            if (!isArray(content)) {
+                content = content.split(data.delimiter);
+            }
+            if (append) {
+                index = data.content.push(content) - 1;
+                data.config[index] = config;
+                data.tags[index] = tags;
+            } else {
+                index = content.length;
+                data.content = content;
+                data.config = config;
+                data.tags = tags;
+            }
+            previewCache.data[id] = data;
+            return index;
+        },
+        set: function (id, content, config, tags, append) {
+            var data = $.extend(true, {}, previewCache.data[id]), i, chk;
+            if (!content || !content.length) {
+                return;
+            }
+            if (!isArray(content)) {
+                content = content.split(data.delimiter);
+            }
+            chk = content.filter(function (n) {
+                return n !== null;
+            });
+            if (!chk.length) {
+                return;
+            }
+            if (data.content === undefined) {
+                data.content = [];
+            }
+            if (data.config === undefined) {
+                data.config = [];
+            }
+            if (data.tags === undefined) {
+                data.tags = [];
+            }
+            if (append) {
+                for (i = 0; i < content.length; i++) {
+                    if (content[i]) {
+                        data.content.push(content[i]);
+                    }
+                }
+                for (i = 0; i < config.length; i++) {
+                    if (config[i]) {
+                        data.config.push(config[i]);
+                    }
+                }
+                for (i = 0; i < tags.length; i++) {
+                    if (tags[i]) {
+                        data.tags.push(tags[i]);
+                    }
+                }
+            } else {
+                data.content = content;
+                data.config = config;
+                data.tags = tags;
+            }
+            previewCache.data[id] = data;
+        },
+        unset: function (id, index) {
+            var chk = previewCache.count(id);
+            if (!chk) {
+                return;
+            }
+            if (chk === 1) {
+                previewCache.data[id].content = [];
+                previewCache.data[id].config = [];
+                return;
+            }
+            previewCache.data[id].content[index] = null;
+            previewCache.data[id].config[index] = null;
+        },
+        out: function (id) {
+            var html = '', data = previewCache.data[id], caption, len = previewCache.count(id, true);
+            if (len === 0) {
+                return {content: '', caption: ''};
+            }
+            for (var i = 0; i < len; i++) {
+                html += previewCache.get(id, i);
+            }
+            caption = data.msg(previewCache.count(id));
+            return {content: html, caption: caption};
+        },
+        footer: function (id, i, isDisabled) {
+            var data = previewCache.data[id];
+            isDisabled = isDisabled === undefined ? true : isDisabled;
+            if (data.config.length === 0 || isEmpty(data.config[i])) {
+                return '';
+            }
+            var config = data.config[i],
+                caption = isSet('caption', config) ? config.caption : '',
+                width = isSet('width', config) ? config.width : 'auto',
+                url = isSet('url', config) ? config.url : false,
+                key = isSet('key', config) ? config.key : null,
+                disabled = (url === false) && isDisabled,
+                actions = data.isDelete ? data.actions(false, true, disabled, url, key) : '',
+                footer = data.footer.replace(/\{actions}/g, actions);
+            return footer.replace(/\{caption}/g, caption)
+                .replace(/\{width}/g, width)
+                .replace(/\{indicator}/g, '')
+                .replace(/\{indicatorTitle}/g, '');
+        }
+    };
+    getNum = function (num, def) {
+        def = def || 0;
+        if (typeof num === "number") {
+            return num;
+        }
+        if (typeof num === "string") {
+            num = parseFloat(num);
+        }
+        return isNaN(num) ? def : num;
+    };
+    hasFileAPISupport = function () {
+        return !!(window.File && window.FileReader);
+    };
+    hasDragDropSupport = function () {
+        var div = document.createElement('div');
+        /** @namespace div.draggable */
+        /** @namespace div.ondragstart */
+        /** @namespace div.ondrop */
+        return !isIE(9) && !isEdge() && // Fix for MS Edge drag & drop support bug
+            (div.draggable !== undefined || (div.ondragstart !== undefined && div.ondrop !== undefined));
+    };
+    hasFileUploadSupport = function () {
+        return hasFileAPISupport() && window.FormData;
+    };
+    addCss = function ($el, css) {
+        $el.removeClass(css).addClass(css);
+    };
+    STYLE_SETTING = 'style="width:{width};height:{height};"';
+    OBJECT_PARAMS = '      <param name="controller" value="true" />\n' +
+        '      <param name="allowFullScreen" value="true" />\n' +
+        '      <param name="allowScriptAccess" value="always" />\n' +
+        '      <param name="autoPlay" value="false" />\n' +
+        '      <param name="autoStart" value="false" />\n' +
+        '      <param name="quality" value="high" />\n';
+    DEFAULT_PREVIEW = '<div class="file-preview-other">\n' +
+        '   <span class="{previewFileIconClass}">{previewFileIcon}</span>\n' +
+        '</div>';
+    defaultFileActionSettings = {
+        removeIcon: '<i class="glyphicon glyphicon-trash text-danger"></i>',
+        removeClass: 'btn btn-xs btn-default',
+        removeTitle: 'Remove file',
+        uploadIcon: '<i class="glyphicon glyphicon-upload text-info"></i>',
+        uploadClass: 'btn btn-xs btn-default',
+        uploadTitle: 'Upload file',
+        indicatorNew: '<i class="glyphicon glyphicon-hand-down text-warning"></i>',
+        indicatorSuccess: '<i class="glyphicon glyphicon-ok-sign text-success"></i>',
+        indicatorError: '<i class="glyphicon glyphicon-exclamation-sign text-danger"></i>',
+        indicatorLoading: '<i class="glyphicon glyphicon-hand-up text-muted"></i>',
+        indicatorNewTitle: 'Not uploaded yet',
+        indicatorSuccessTitle: 'Uploaded',
+        indicatorErrorTitle: 'Upload Error',
+        indicatorLoadingTitle: 'Uploading ...'
+    };
+    tMain1 = '{preview}\n' +
+        '<div class="kv-upload-progress hide"></div>\n' +
+        '<div class="input-group {class}">\n' +
+        '   {caption}\n' +
+        '   <div class="input-group-btn">\n' +
+        '       {remove}\n' +
+        '       {cancel}\n' +
+        '       {upload}\n' +
+        '       {browse}\n' +
+        '   </div>\n' +
+        '</div>';
+    tMain2 = '{preview}\n<div class="kv-upload-progress hide"></div>\n{remove}\n{cancel}\n{upload}\n{browse}\n';
+    tPreview = '<div class="file-preview {class}">\n' +
+        '    {close}' +
+        '    <div class="{dropClass}">\n' +
+        '    <div class="file-preview-thumbnails">\n' +
+        '    </div>\n' +
+        '    <div class="clearfix"></div>' +
+        '    <div class="file-preview-status text-center text-success"></div>\n' +
+        '    <div class="kv-fileinput-error"></div>\n' +
+        '    </div>\n' +
+        '</div>';
+    tClose = '<div class="close fileinput-remove">&times;</div>\n';
+    tIcon = '<span class="glyphicon glyphicon-file kv-caption-icon"></span>';
+    tCaption = '<div tabindex="500" class="form-control file-caption {class}">\n' +
+        '   <div class="file-caption-name"></div>\n' +
+        '</div>\n';
+    //noinspection HtmlUnknownAttribute
+    tBtnDefault = '<button type="{type}" tabindex="500" title="{title}" class="{css}" {status}>{icon}{label}</button>';
+    tBtnLink = '<a href="{href}" tabindex="500" title="{title}" class="{css}" {status}>{icon}{label}</a>';
+    tBtnBrowse = '<div tabindex="500" class="{css}" {status}>{icon}{label}</div>';
+    tModal = '<div id="{id}" class="file-preview-detail-modal modal fade" tabindex="-1">\n' +
+        '  <div class="modal-dialog modal-lg">\n' +
+        '    <div class="modal-content">\n' +
+        '      <div class="modal-header">\n' +
+        '        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>\n' +
+        '        <h3 class="modal-title">{heading} <small>{title}</small></h3>\n' +
+        '      </div>\n' +
+        '      <div class="modal-body">\n' +
+        '           <pre>{body}</pre>\n' +
+        '      </div>\n' +
+        '    </div>\n' +
+        '  </div>\n' +
+        '</div>';
+    tProgress = '<div class="progress">\n' +
+        '    <div class="{class}" role="progressbar"' +
+        ' aria-valuenow="{percent}" aria-valuemin="0" aria-valuemax="100" style="width:{percent}%;">\n' +
+        '        {percent}%\n' +
+        '     </div>\n' +
+        '</div>';
+    tFooter = '<div class="file-thumbnail-footer">\n' +
+        '    <div class="file-footer-caption" title="{caption}">{caption}</div>\n' +
+        '    {progress} {actions}\n' +
+        '</div>';
+    tActions = '<div class="file-actions">\n' +
+        '    <div class="file-footer-buttons">\n' +
+        '        {upload}{delete}{other}' +
+        '    </div>\n' +
+        '    <div class="file-upload-indicator" title="{indicatorTitle}">{indicator}</div>\n' +
+        '    <div class="clearfix"></div>\n' +
+        '</div>';
+    tActionDelete = '<button type="button" class="kv-file-remove {removeClass}" ' +
+        'title="{removeTitle}" {dataUrl}{dataKey}>{removeIcon}</button>\n';
+    tActionUpload = '<button type="button" class="kv-file-upload {uploadClass}" title="{uploadTitle}">' +
+        '   {uploadIcon}\n</button>\n';
+    tZoom = '<button type="button" class="btn btn-default btn-xs btn-block" title="{zoomTitle}: {caption}" onclick="{dialog}">\n' +
+        '   {zoomInd}\n' +
+        '</button>\n';
+    tGeneric = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}">\n' +
+        '   {content}\n' +
+        '   {footer}\n' +
+        '</div>\n';
+    tHtml = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}">\n' +
+        '    <object class="file-object" data="{data}" type="{type}" width="{width}" height="{height}">\n' +
+        '       ' + DEFAULT_PREVIEW + '\n' +
+        '    </object>\n' +
+        '   {footer}\n' +
+        '</div>';
+    tImage = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}">\n' +
+        '   <img src="{data}" class="file-preview-image" title="{caption}" alt="{caption}" ' + STYLE_SETTING + '>\n' +
+        '   {footer}\n' +
+        '</div>\n';
+    tText = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}">\n' +
+        '   <pre class="file-preview-text" title="{caption}" ' + STYLE_SETTING + '>{data}</pre>\n' +
+        '   {zoom}\n' +
+        '   {footer}\n' +
+        '</div>';
+    tVideo = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}"' +
+        ' title="{caption}" ' + STYLE_SETTING + '>\n' +
+        '   <video width="{width}" height="{height}" controls>\n' +
+        '       <source src="{data}" type="{type}">\n' +
+        '       ' + DEFAULT_PREVIEW + '\n' +
+        '   </video>\n' +
+        '   {footer}\n' +
+        '</div>\n';
+    tAudio = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}"' +
+        ' title="{caption}" ' + STYLE_SETTING + '>\n' +
+        '   <audio controls>\n' +
+        '       <source src="' + '{data}' + '" type="{type}">\n' +
+        '       ' + DEFAULT_PREVIEW + '\n' +
+        '   </audio>\n' +
+        '   {footer}\n' +
+        '</div>';
+    tFlash = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}"' +
+        ' title="{caption}" ' + STYLE_SETTING + '>\n' +
+        '   <object class="file-object" type="application/x-shockwave-flash" width="{width}" height="{height}" data="{data}">\n' +
+        OBJECT_PARAMS + '       ' + DEFAULT_PREVIEW + '\n' +
+        '   </object>\n' +
+        '   {footer}\n' +
+        '</div>\n';
+    tObject = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}"' +
+        ' title="{caption}" ' + STYLE_SETTING + '>\n' +
+        '   <object class="file-object" data="{data}" type="{type}" width="{width}" height="{height}">\n' +
+        '       <param name="movie" value="{caption}" />\n' +
+        OBJECT_PARAMS + '         ' + DEFAULT_PREVIEW + '\n' +
+        '   </object>\n' +
+        '   {footer}\n' +
+        '</div>';
+    tOther = '<div class="file-preview-frame{frameClass}" id="{previewId}" data-fileindex="{fileindex}"' +
+        ' title="{caption}" ' + STYLE_SETTING + '>\n' +
+        '   <div class="file-preview-other-frame">\n' +
+        '   ' + DEFAULT_PREVIEW + '\n' +
+        '   </div>\n' +
+        '   <div class="file-preview-other-footer">{footer}</div>\n' +
+        '</div>';
+    defaultLayoutTemplates = {
+        main1: tMain1,
+        main2: tMain2,
+        preview: tPreview,
+        close: tClose,
+        zoom: tZoom,
+        icon: tIcon,
+        caption: tCaption,
+        modal: tModal,
+        progress: tProgress,
+        footer: tFooter,
+        actions: tActions,
+        actionDelete: tActionDelete,
+        actionUpload: tActionUpload,
+        btnDefault: tBtnDefault,
+        btnLink: tBtnLink,
+        btnBrowse: tBtnBrowse
+    };
+    defaultPreviewTemplates = {
+        generic: tGeneric,
+        html: tHtml,
+        image: tImage,
+        text: tText,
+        video: tVideo,
+        audio: tAudio,
+        flash: tFlash,
+        object: tObject,
+        other: tOther
+    };
+    defaultPreviewTypes = ['image', 'html', 'text', 'video', 'audio', 'flash', 'object'];
+    defaultPreviewSettings = {
+        image: {width: "auto", height: "160px"},
+        html: {width: "213px", height: "160px"},
+        text: {width: "160px", height: "136px"},
+        video: {width: "213px", height: "160px"},
+        audio: {width: "213px", height: "80px"},
+        flash: {width: "213px", height: "160px"},
+        object: {width: "160px", height: "160px"},
+        other: {width: "160px", height: "160px"}
+    };
+    defaultFileTypeSettings = {
+        image: function (vType, vName) {
+            return compare(vType, 'image.*') || compare(vName, /\.(gif|png|jpe?g)$/i);
+        },
+        html: function (vType, vName) {
+            return compare(vType, 'text/html') || compare(vName, /\.(htm|html)$/i);
+        },
+        text: function (vType, vName) {
+            return compare(vType, 'text.*') || compare(vType, /\.(xml|javascript)$/i) ||
+                compare(vName, /\.(txt|md|csv|nfo|ini|json|php|js|css)$/i);
+        },
+        video: function (vType, vName) {
+            return compare(vType, 'video.*') && (compare(vType, /(ogg|mp4|mp?g|webm|3gp)$/i) ||
+                compare(vName, /\.(og?|mp4|webm|mp?g|3gp)$/i));
+        },
+        audio: function (vType, vName) {
+            return compare(vType, 'audio.*') && (compare(vType, /(ogg|mp3|mp?g|wav)$/i) ||
+                compare(vName, /\.(og?|mp3|mp?g|wav)$/i));
+        },
+        flash: function (vType, vName) {
+            return compare(vType, 'application/x-shockwave-flash', true) || compare(vName, /\.(swf)$/i);
+        },
+        object: function (vType, vName) {
+            return compare(vType, 'application/pdf', true) || compare(vName, /\.(pdf)$/i);
+        },
+        other: function () {
+            return true;
+        }
+    };
+    isEmpty = function (value, trim) {
+        return value === undefined || value === null || value.length === 0 || (trim && $.trim(value) === '');
+    };
+    isArray = function (a) {
+        return Array.isArray(a) || Object.prototype.toString.call(a) === '[object Array]';
+    };
+    isSet = function (needle, haystack) {
+        return (typeof haystack === 'object' && needle in haystack);
+    };
+    getElement = function (options, param, value) {
+        return (isEmpty(options) || isEmpty(options[param])) ? value : $(options[param]);
+    };
+    uniqId = function () {
+        return Math.round(new Date().getTime() + (Math.random() * 100));
+    };
+    htmlEncode = function (str) {
+        return str.replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/"/g, '&quot;')
+            .replace(/'/g, '&apos;');
+    };
+    replaceTags = function (str, tags) {
+        var out = str;
+        if (!tags) {
+            return out;
+        }
+        $.each(tags, function (key, value) {
+            if (typeof value === "function") {
+                value = value();
+            }
+            out = out.split(key).join(value);
+        });
+        return out;
+    };
+    cleanMemory = function ($thumb) {
+        var data = $thumb.is('img') ? $thumb.attr('src') : $thumb.find('source').attr('src');
+        /** @namespace objUrl.revokeObjectURL */
+        objUrl.revokeObjectURL(data);
+    };
+    FileInput = function (element, options) {
+        var self = this;
+        self.$element = $(element);
+        if (!self._validate()) {
+            return;
+        }
+        self.isPreviewable = hasFileAPISupport();
+        self.isIE9 = isIE(9);
+        self.isIE10 = isIE(10);
+        if (self.isPreviewable || self.isIE9) {
+            self._init(options);
+            self._listen();
+        } else {
+            self.$element.removeClass('file-loading');
+        }
+    };
+
+    FileInput.prototype = {
+        constructor: FileInput,
+        _init: function (options) {
+            var self = this, $el = self.$element, t;
+            $.each(options, function (key, value) {
+                switch (key) {
+                    case 'minFileCount':
+                    case 'maxFileCount':
+                    case 'maxFileSize':
+                        self[key] = getNum(value);
+                        break;
+                    default:
+                        self[key] = value;
+                        break;
+                }
+            });
+            self.fileInputCleared = false;
+            self.fileBatchCompleted = true;
+            if (!self.isPreviewable) {
+                self.showPreview = false;
+            }
+            self.uploadFileAttr = !isEmpty($el.attr('name')) ? $el.attr('name') : 'file_data';
+            self.reader = null;
+            self.formdata = {};
+            self.clearStack();
+            self.uploadCount = 0;
+            self.uploadStatus = {};
+            self.uploadLog = [];
+            self.uploadAsyncCount = 0;
+            self.loadedImages = [];
+            self.totalImagesCount = 0;
+            self.ajaxRequests = [];
+            self.isError = false;
+            self.ajaxAborted = false;
+            self.cancelling = false;
+            t = self._getLayoutTemplate('progress');
+            self.progressTemplate = t.replace('{class}', self.progressClass);
+            self.progressCompleteTemplate = t.replace('{class}', self.progressCompleteClass);
+            self.progressErrorTemplate = t.replace('{class}', self.progressErrorClass);
+            self.dropZoneEnabled = hasDragDropSupport() && self.dropZoneEnabled;
+            self.isDisabled = self.$element.attr('disabled') || self.$element.attr('readonly');
+            self.isUploadable = hasFileUploadSupport() && !isEmpty(self.uploadUrl);
+            self.slug = typeof options.slugCallback === "function" ? options.slugCallback : self._slugDefault;
+            self.mainTemplate = self.showCaption ? self._getLayoutTemplate('main1') : self._getLayoutTemplate('main2');
+            self.captionTemplate = self._getLayoutTemplate('caption');
+            self.previewGenericTemplate = self._getPreviewTemplate('generic');
+            if (self.resizeImage && (self.maxImageWidth || self.maxImageHeight)) {
+                self.imageCanvas = document.createElement('canvas');
+                self.imageCanvasContext = self.imageCanvas.getContext('2d');
+            }
+            if (isEmpty(self.$element.attr('id'))) {
+                self.$element.attr('id', uniqId());
+            }
+            if (self.$container === undefined) {
+                self.$container = self._createContainer();
+            } else {
+                self._refreshContainer();
+            }
+            self.$dropZone = self.$container.find('.file-drop-zone');
+            self.$progress = self.$container.find('.kv-upload-progress');
+            self.$btnUpload = self.$container.find('.fileinput-upload');
+            self.$captionContainer = getElement(options, 'elCaptionContainer', self.$container.find('.file-caption'));
+            self.$caption = getElement(options, 'elCaptionText', self.$container.find('.file-caption-name'));
+            self.$previewContainer = getElement(options, 'elPreviewContainer', self.$container.find('.file-preview'));
+            self.$preview = getElement(options, 'elPreviewImage', self.$container.find('.file-preview-thumbnails'));
+            self.$previewStatus = getElement(options, 'elPreviewStatus', self.$container.find('.file-preview-status'));
+            self.$errorContainer = getElement(options, 'elErrorContainer',
+                self.$previewContainer.find('.kv-fileinput-error'));
+            if (!isEmpty(self.msgErrorClass)) {
+                addCss(self.$errorContainer, self.msgErrorClass);
+            }
+            self.$errorContainer.hide();
+            self.fileActionSettings = $.extend(true, defaultFileActionSettings, options.fileActionSettings);
+            self.previewInitId = "preview-" + uniqId();
+            self.id = self.$element.attr('id');
+            previewCache.init(self);
+            self._initPreview(true);
+            self._initPreviewDeletes();
+            self.options = options;
+            self._setFileDropZoneTitle();
+            self.$element.removeClass('file-loading');
+            if (self.$element.attr('disabled')) {
+                self.disable();
+            }
+        },
+        _validate: function () {
+            var self = this, $exception;
+            if (self.$element.attr('type') === 'file') {
+                return true;
+            }
+            $exception = '<div class="help-block alert alert-warning">' +
+                '<h4>Invalid Input Type</h4>' +
+                'You must set an input <code>type = file</code> for <b>bootstrap-fileinput</b> plugin to initialize.' +
+                '</div>';
+            self.$element.after($exception);
+            return false;
+        },
+        _errorsExist: function () {
+            var self = this, $err;
+            if (self.$errorContainer.find('li').length) {
+                return true;
+            }
+            $err = $(document.createElement('div')).html(self.$errorContainer.html());
+            $err.find('span.kv-error-close').remove();
+            $err.find('ul').remove();
+            return $.trim($err.text()).length ? true : false;
+        },
+        _errorHandler: function (evt, caption) {
+            var self = this, err = evt.target.error;
+            /** @namespace err.NOT_FOUND_ERR */
+            /** @namespace err.SECURITY_ERR */
+            /** @namespace err.NOT_READABLE_ERR */
+            if (err.code === err.NOT_FOUND_ERR) {
+                self._showError(self.msgFileNotFound.replace('{name}', caption));
+            } else if (err.code === err.SECURITY_ERR) {
+                self._showError(self.msgFileSecured.replace('{name}', caption));
+            } else if (err.code === err.NOT_READABLE_ERR) {
+                self._showError(self.msgFileNotReadable.replace('{name}', caption));
+            } else if (err.code === err.ABORT_ERR) {
+                self._showError(self.msgFilePreviewAborted.replace('{name}', caption));
+            } else {
+                self._showError(self.msgFilePreviewError.replace('{name}', caption));
+            }
+        },
+        _addError: function (msg) {
+            var self = this, $error = self.$errorContainer;
+            if (msg && $error.length) {
+                $error.html(self.errorCloseButton + msg);
+                handler($error.find('.kv-error-close'), 'click', function () {
+                    $error.fadeOut('slow');
+                });
+            }
+        },
+        _resetErrors: function (fade) {
+            var self = this, $error = self.$errorContainer;
+            self.isError = false;
+            self.$container.removeClass('has-error');
+            $error.html('');
+            if (fade) {
+                $error.fadeOut('slow');
+            } else {
+                $error.hide();
+            }
+        },
+        _showFolderError: function (folders) {
+            var self = this, $error = self.$errorContainer, msg;
+            if (!folders) {
+                return;
+            }
+            msg = self.msgFoldersNotAllowed.replace(/\{n}/g, folders);
+            self._addError(msg);
+            addCss(self.$container, 'has-error');
+            $error.fadeIn(800);
+            self._raise('filefoldererror', [folders, msg]);
+        },
+        _showUploadError: function (msg, params, event) {
+            var self = this, $error = self.$errorContainer, ev = event || 'fileuploaderror', e = params && params.id ?
+            '<li data-file-id="' + params.id + '">' + msg + '</li>' : '<li>' + msg + '</li>';
+            if ($error.find('ul').length === 0) {
+                self._addError('<ul>' + e + '</ul>');
+            } else {
+                $error.find('ul').append(e);
+            }
+            $error.fadeIn(800);
+            self._raise(ev, [params, msg]);
+            self.$container.removeClass('file-input-new');
+            addCss(self.$container, 'has-error');
+            return true;
+        },
+        _showError: function (msg, params, event) {
+            var self = this, $error = self.$errorContainer, ev = event || 'fileerror';
+            params = params || {};
+            params.reader = self.reader;
+            self._addError(msg);
+            $error.fadeIn(800);
+            self._raise(ev, [params, msg]);
+            if (!self.isUploadable) {
+                self._clearFileInput();
+            }
+            self.$container.removeClass('file-input-new');
+            addCss(self.$container, 'has-error');
+            self.$btnUpload.attr('disabled', true);
+            return true;
+        },
+        _noFilesError: function (params) {
+            var self = this, label = self.minFileCount > 1 ? self.filePlural : self.fileSingle,
+                msg = self.msgFilesTooLess.replace('{n}', self.minFileCount).replace('{files}', label),
+                $error = self.$errorContainer;
+            self._addError(msg);
+            self.isError = true;
+            self._updateFileDetails(0);
+            $error.fadeIn(800);
+            self._raise('fileerror', [params, msg]);
+            self._clearFileInput();
+            addCss(self.$container, 'has-error');
+        },
+        _parseError: function (jqXHR, errorThrown, fileName) {
+            /** @namespace jqXHR.responseJSON */
+            var self = this, errMsg = $.trim(errorThrown + ''),
+                dot = errMsg.slice(-1) === '.' ? '' : '.',
+                text = jqXHR.responseJSON !== undefined && jqXHR.responseJSON.error !== undefined ?
+                    jqXHR.responseJSON.error : jqXHR.responseText;
+            if (self.cancelling && self.msgUploadAborted) {
+                errMsg = self.msgUploadAborted;
+            }
+            if (self.showAjaxErrorDetails && text) {
+                text = $.trim(text.replace(/\n\s*\n/g, '\n'));
+                text = text.length > 0 ? '<pre>' + text + '</pre>' : '';
+                errMsg += dot + text;
+            } else {
+                errMsg += dot;
+            }
+            self.cancelling = false;
+            return fileName ? '<b>' + fileName + ': </b>' + errMsg : errMsg;
+        },
+        _parseFileType: function (file) {
+            var self = this, isValid, vType, cat, i;
+            for (i = 0; i < defaultPreviewTypes.length; i += 1) {
+                cat = defaultPreviewTypes[i];
+                isValid = isSet(cat, self.fileTypeSettings) ? self.fileTypeSettings[cat] : defaultFileTypeSettings[cat];
+                vType = isValid(file.type, file.name) ? cat : '';
+                if (!isEmpty(vType)) {
+                    return vType;
+                }
+            }
+            return 'other';
+        },
+        _parseFilePreviewIcon: function (content, fname) {
+            var self = this, ext, icn = self.previewFileIcon;
+            if (fname && fname.indexOf('.') > -1) {
+                ext = fname.split('.').pop();
+                if (self.previewFileIconSettings && self.previewFileIconSettings[ext]) {
+                    icn = self.previewFileIconSettings[ext];
+                }
+                if (self.previewFileExtSettings) {
+                    $.each(self.previewFileExtSettings, function (key, func) {
+                        if (self.previewFileIconSettings[key] && func(ext)) {
+                            icn = self.previewFileIconSettings[key];
+                        }
+                    });
+                }
+            }
+            if (content.indexOf('{previewFileIcon}') > -1) {
+                return content.replace(/\{previewFileIconClass}/g, self.previewFileIconClass).replace(
+                    /\{previewFileIcon}/g, icn);
+            }
+            return content;
+        },
+        _raise: function (event, params) {
+            var self = this, e = $.Event(event);
+            if (params !== undefined) {
+                self.$element.trigger(e, params);
+            } else {
+                self.$element.trigger(e);
+            }
+            if (e.isDefaultPrevented()) {
+                return false;
+            }
+            if (!e.result) {
+                return e.result;
+            }
+            switch (event) {
+                // ignore these events
+                case 'filebatchuploadcomplete':
+                case 'filebatchuploadsuccess':
+                case 'fileuploaded':
+                case 'fileclear':
+                case 'filecleared':
+                case 'filereset':
+                case 'fileerror':
+                case 'filefoldererror':
+                case 'fileuploaderror':
+                case 'filebatchuploaderror':
+                case 'filedeleteerror':
+                case 'filecustomerror':
+                case 'filesuccessremove':
+                    break;
+                // receive data response via `filecustomerror` event`
+                default:
+                    self.ajaxAborted = e.result;
+                    break;
+            }
+            return true;
+        },
+        _listen: function () {
+            var self = this, $el = self.$element, $form = $el.closest('form'), $cont = self.$container;
+            handler($el, 'change', $.proxy(self._change, self));
+            handler(self.$btnFile, 'click', $.proxy(self._browse, self));
+            handler($form, 'reset', $.proxy(self.reset, self));
+            handler($cont.find('.fileinput-remove:not([disabled])'), 'click', $.proxy(self.clear, self));
+            handler($cont.find('.fileinput-cancel'), 'click', $.proxy(self.cancel, self));
+            self._initDragDrop();
+            if (!self.isUploadable) {
+                handler($form, 'submit', $.proxy(self._submitForm, self));
+            }
+            handler(self.$container.find('.fileinput-upload'), 'click', $.proxy(self._uploadClick, self));
+        },
+        _initDragDrop: function () {
+            var self = this, $zone = self.$dropZone;
+            if (self.isUploadable && self.dropZoneEnabled && self.showPreview) {
+                handler($zone, 'dragenter dragover', $.proxy(self._zoneDragEnter, self));
+                handler($zone, 'dragleave', $.proxy(self._zoneDragLeave, self));
+                handler($zone, 'drop', $.proxy(self._zoneDrop, self));
+                handler($(document), 'dragenter dragover drop', self._zoneDragDropInit);
+            }
+        },
+        _zoneDragDropInit: function (e) {
+            e.stopPropagation();
+            e.preventDefault();
+        },
+        _zoneDragEnter: function (e) {
+            var self = this, hasFiles = $.inArray('Files', e.originalEvent.dataTransfer.types) > -1;
+            self._zoneDragDropInit(e);
+            if (self.isDisabled || !hasFiles) {
+                e.originalEvent.dataTransfer.effectAllowed = 'none';
+                e.originalEvent.dataTransfer.dropEffect = 'none';
+                return;
+            }
+            addCss(self.$dropZone, 'file-highlighted');
+        },
+        _zoneDragLeave: function (e) {
+            var self = this;
+            self._zoneDragDropInit(e);
+            if (self.isDisabled) {
+                return;
+            }
+            self.$dropZone.removeClass('file-highlighted');
+        },
+        _zoneDrop: function (e) {
+            var self = this;
+            e.preventDefault();
+            /** @namespace e.originalEvent.dataTransfer */
+            if (self.isDisabled || isEmpty(e.originalEvent.dataTransfer.files)) {
+                return;
+            }
+            self._change(e, 'dragdrop');
+            self.$dropZone.removeClass('file-highlighted');
+        },
+        _uploadClick: function (e) {
+            var self = this, $btn = self.$container.find('.fileinput-upload'), $form,
+                isEnabled = !$btn.hasClass('disabled') && isEmpty($btn.attr('disabled'));
+            if (e && e.isDefaultPrevented()) {
+                return;
+            }
+            if (!self.isUploadable) {
+                if (isEnabled && $btn.attr('type') !== 'submit') {
+                    $form = $btn.closest('form');
+                    // downgrade to normal form submit if possible
+                    if ($form.length) {
+                        $form.trigger('submit');
+                    }
+                    e.preventDefault();
+                }
+                return;
+            }
+            e.preventDefault();
+            if (isEnabled) {
+                self.upload();
+            }
+        },
+        _submitForm: function () {
+            var self = this, $el = self.$element, files = $el.get(0).files;
+            if (files && self.minFileCount > 0 && self._getFileCount(files.length) < self.minFileCount) {
+                self._noFilesError({});
+                return false;
+            }
+            return !self._abort({});
+        },
+        _clearPreview: function () {
+            var self = this, $thumbs = !self.showUploadedThumbs ? self.$preview.find('.file-preview-frame') :
+                self.$preview.find('.file-preview-frame:not(.file-preview-success)');
+            $thumbs.remove();
+            if (!self.$preview.find('.file-preview-frame').length || !self.showPreview) {
+                self._resetUpload();
+            }
+            self._validateDefaultPreview();
+        },
+        _initPreview: function (isInit) {
+            var self = this, cap = self.initialCaption || '', out;
+            if (!previewCache.count(self.id)) {
+                self._clearPreview();
+                if (isInit) {
+                    self._setCaption(cap);
+                } else {
+                    self._initCaption();
+                }
+                return;
+            }
+            out = previewCache.out(self.id);
+            cap = isInit && self.initialCaption ? self.initialCaption : out.caption;
+            self.$preview.html(out.content);
+            self._setCaption(cap);
+            if (!isEmpty(out.content)) {
+                self.$container.removeClass('file-input-new');
+            }
+        },
+        _initPreviewDeletes: function () {
+            var self = this, deleteExtraData = self.deleteExtraData || {},
+                resetProgress = function () {
+                    var hasFiles = self.isUploadable ? previewCache.count(self.id) : self.$element.get(0).files.length;
+                    if (self.$preview.find('.kv-file-remove').length === 0 && !hasFiles) {
+                        self.reset();
+                        self.initialCaption = '';
+                    }
+                };
+
+            self.$preview.find('.kv-file-remove').each(function () {
+                var $el = $(this), vUrl = $el.data('url') || self.deleteUrl, vKey = $el.data('key');
+                if (isEmpty(vUrl) || vKey === undefined) {
+                    return;
+                }
+                var $frame = $el.closest('.file-preview-frame'), cache = previewCache.data[self.id],
+                    settings, params, index = $frame.data('fileindex'), config, extraData;
+                index = parseInt(index.replace('init_', ''));
+                config = isEmpty(cache.config) && isEmpty(cache.config[index]) ? null : cache.config[index];
+                extraData = isEmpty(config) || isEmpty(config.extra) ? deleteExtraData : config.extra;
+                if (typeof extraData === "function") {
+                    extraData = extraData();
+                }
+                params = {id: $el.attr('id'), key: vKey, extra: extraData};
+                settings = $.extend(true, {}, {
+                    url: vUrl,
+                    type: 'POST',
+                    dataType: 'json',
+                    data: $.extend(true, {}, {key: vKey}, extraData),
+                    beforeSend: function (jqXHR) {
+                        self.ajaxAborted = false;
+                        self._raise('filepredelete', [vKey, jqXHR, extraData]);
+                        if (self.ajaxAborted) {
+                            jqXHR.abort();
+                        } else {
+                            addCss($frame, 'file-uploading');
+                            addCss($el, 'disabled');
+                        }
+                    },
+                    success: function (data, textStatus, jqXHR) {
+                        var n, cap;
+                        if (isEmpty(data) || isEmpty(data.error)) {
+                            previewCache.unset(self.id, index);
+                            n = previewCache.count(self.id);
+                            cap = n > 0 ? self._getMsgSelected(n) : '';
+                            self._raise('filedeleted', [vKey, jqXHR, extraData]);
+                            self._setCaption(cap);
+                        } else {
+                            params.jqXHR = jqXHR;
+                            params.response = data;
+                            self._showError(data.error, params, 'filedeleteerror');
+                            $frame.removeClass('file-uploading');
+                            $el.removeClass('disabled');
+                            resetProgress();
+                            return;
+                        }
+                        $frame.removeClass('file-uploading').addClass('file-deleted');
+                        $frame.fadeOut('slow', function () {
+                            self._clearObjects($frame);
+                            $frame.remove();
+                            resetProgress();
+                            if (!n && self.getFileStack().length === 0) {
+                                self._setCaption('');
+                                self.reset();
+                            }
+                        });
+                    },
+                    error: function (jqXHR, textStatus, errorThrown) {
+                        var errMsg = self._parseError(jqXHR, errorThrown);
+                        params.jqXHR = jqXHR;
+                        params.response = {};
+                        self._showError(errMsg, params, 'filedeleteerror');
+                        $frame.removeClass('file-uploading');
+                        resetProgress();
+                    }
+                }, self.ajaxDeleteSettings);
+                handler($el, 'click', function () {
+                    if (!self._validateMinCount()) {
+                        return false;
+                    }
+                    $.ajax(settings);
+                });
+            });
+        },
+        _clearObjects: function ($el) {
+            $el.find('video audio').each(function () {
+                this.pause();
+                $(this).remove();
+            });
+            $el.find('img object div').each(function () {
+                $(this).remove();
+            });
+        },
+        _clearFileInput: function () {
+            var self = this, $el = self.$element, $srcFrm, $tmpFrm, $tmpEl;
+            if (isEmpty($el.val())) {
+                return;
+            }
+            // Fix for IE ver < 11, that does not clear file inputs. Requires a sequence of steps to prevent IE
+            // crashing but still allow clearing of the file input.
+            if (self.isIE9 || self.isIE10) {
+                $srcFrm = $el.closest('form');
+                $tmpFrm = $(document.createElement('form'));
+                $tmpEl = $(document.createElement('div'));
+                $el.before($tmpEl);
+                if ($srcFrm.length) {
+                    $srcFrm.after($tmpFrm);
+                } else {
+                    $tmpEl.after($tmpFrm);
+                }
+                $tmpFrm.append($el).trigger('reset');
+                $tmpEl.before($el).remove();
+                $tmpFrm.remove();
+            } else { // normal input clear behavior for other sane browsers
+                $el.val('');
+            }
+            self.fileInputCleared = true;
+        },
+        _resetUpload: function () {
+            var self = this;
+            self.uploadCache = {content: [], config: [], tags: [], append: true};
+            self.uploadCount = 0;
+            self.uploadStatus = {};
+            self.uploadLog = [];
+            self.uploadAsyncCount = 0;
+            self.loadedImages = [];
+            self.totalImagesCount = 0;
+            self.$btnUpload.removeAttr('disabled');
+            self._setProgress(0);
+            addCss(self.$progress, 'hide');
+            self._resetErrors(false);
+            self.ajaxAborted = false;
+            self.ajaxRequests = [];
+            self._resetCanvas();
+        },
+        _resetCanvas: function () {
+            var self = this;
+            if (self.canvas && self.imageCanvasContext) {
+                self.imageCanvasContext.clearRect(0, 0, self.canvas.width, self.canvas.height);
+            }
+        },
+        _hasInitialPreview: function () {
+            var self = this;
+            return !self.overwriteInitial && previewCache.count(self.id);
+        },
+        _resetPreview: function () {
+            var self = this, out, cap;
+            if (previewCache.count(self.id)) {
+                out = previewCache.out(self.id);
+                self.$preview.html(out.content);
+                cap = self.initialCaption ? self.initialCaption : out.caption;
+                self._setCaption(cap);
+            } else {
+                self._clearPreview();
+                self._initCaption();
+            }
+        },
+        _clearDefaultPreview: function () {
+            var self = this;
+            self.$preview.find('.file-default-preview').remove();
+        },
+        _validateDefaultPreview: function () {
+            var self = this;
+            if (!self.showPreview || isEmpty(self.defaultPreviewContent)) {
+                return;
+            }
+            self.$preview.html('<div class="file-default-preview">' + self.defaultPreviewContent + '</div>');
+            self.$container.removeClass('file-input-new');
+        },
+        _resetPreviewThumbs: function (isAjax) {
+            var self = this, out;
+            if (isAjax) {
+                self._clearPreview();
+                self.clearStack();
+                return;
+            }
+            if (self._hasInitialPreview()) {
+                out = previewCache.out(self.id);
+                self.$preview.html(out.content);
+                self._setCaption(out.caption);
+                self._initPreviewDeletes();
+            } else {
+                self._clearPreview();
+            }
+        },
+        _getLayoutTemplate: function (t) {
+            var self = this,
+                template = isSet(t, self.layoutTemplates) ? self.layoutTemplates[t] : defaultLayoutTemplates[t];
+            if (isEmpty(self.customLayoutTags)) {
+                return template;
+            }
+            return replaceTags(template, self.customLayoutTags);
+        },
+        _getPreviewTemplate: function (t) {
+            var self = this,
+                template = isSet(t, self.previewTemplates) ? self.previewTemplates[t] : defaultPreviewTemplates[t];
+            if (isEmpty(self.customPreviewTags)) {
+                return template;
+            }
+            return replaceTags(template, self.customPreviewTags);
+        },
+        _getOutData: function (jqXHR, responseData, filesData) {
+            var self = this;
+            jqXHR = jqXHR || {};
+            responseData = responseData || {};
+            filesData = filesData || self.filestack.slice(0) || {};
+            return {
+                form: self.formdata,
+                files: filesData,
+                filenames: self.filenames,
+                extra: self._getExtraData(),
+                response: responseData,
+                reader: self.reader,
+                jqXHR: jqXHR
+            };
+        },
+        _getMsgSelected: function (n) {
+            var self = this, strFiles = n === 1 ? self.fileSingle : self.filePlural;
+            return self.msgSelected.replace('{n}', n).replace('{files}', strFiles);
+        },
+        _getThumbs: function (css) {
+            css = css || '';
+            return this.$preview.find('.file-preview-frame:not(.file-preview-initial)' + css);
+        },
+        _getExtraData: function (previewId, index) {
+            var self = this, data = self.uploadExtraData;
+            if (typeof self.uploadExtraData === "function") {
+                data = self.uploadExtraData(previewId, index);
+            }
+            return data;
+        },
+        _initXhr: function (xhrobj, previewId, fileCount) {
+            var self = this;
+            if (xhrobj.upload) {
+                xhrobj.upload.addEventListener('progress', function (event) {
+                    var pct = 0, position = event.loaded || event.position, total = event.total;
+                    /** @namespace event.lengthComputable */
+                    if (event.lengthComputable) {
+                        pct = Math.ceil(position / total * 100);
+                    }
+                    if (previewId) {
+                        self._setAsyncUploadStatus(previewId, pct, fileCount);
+                    } else {
+                        self._setProgress(Math.ceil(pct));
+                    }
+                }, false);
+            }
+            return xhrobj;
+        },
+        _ajaxSubmit: function (fnBefore, fnSuccess, fnComplete, fnError, previewId, index) {
+            var self = this, settings;
+            self._raise('filepreajax', [previewId, index]);
+            self._uploadExtra(previewId, index);
+            settings = $.extend(true, {}, {
+                xhr: function () {
+                    var xhrobj = $.ajaxSettings.xhr();
+                    return self._initXhr(xhrobj, previewId, self.getFileStack().length);
+                },
+                url: self.uploadUrl,
+                type: 'POST',
+                dataType: 'json',
+                data: self.formdata,
+                cache: false,
+                processData: false,
+                contentType: false,
+                beforeSend: fnBefore,
+                success: fnSuccess,
+                complete: fnComplete,
+                error: fnError
+            }, self.ajaxSettings);
+            self.ajaxRequests.push($.ajax(settings));
+        },
+        _initUploadSuccess: function (out, $thumb, allFiles) {
+            var self = this, append, data, index, $newThumb, content, config, tags, i;
+            if (!self.showPreview || typeof out !== 'object' || $.isEmptyObject(out)) {
+                return;
+            }
+            if (out.initialPreview !== undefined && out.initialPreview.length > 0) {
+                self.hasInitData = true;
+                content = out.initialPreview || [];
+                config = out.initialPreviewConfig || [];
+                tags = out.initialPreviewThumbTags || [];
+                append = out.append === undefined || out.append ? true : false;
+                self.overwriteInitial = false;
+                if ($thumb !== undefined) {
+                    if (!allFiles) {
+                        index = previewCache.add(self.id, content, config[0], tags[0], append);
+                        data = previewCache.get(self.id, index, false);
+                        $newThumb = $(data).hide();
+                        $thumb.after($newThumb).fadeOut('slow', function () {
+                            $newThumb.fadeIn('slow').css('display:inline-block');
+                            self._initPreviewDeletes();
+                            self._clearFileInput();
+                            $thumb.remove();
+                        });
+                    } else {
+                        i = $thumb.attr('data-fileindex');
+                        self.uploadCache.content[i] = content[0];
+                        self.uploadCache.config[i] = config[0];
+                        self.uploadCache.tags[i] = tags[0];
+                        self.uploadCache.append = append;
+                    }
+                } else {
+                    previewCache.set(self.id, content, config, tags, append);
+                    self._initPreview();
+                    self._initPreviewDeletes();
+                }
+            }
+        },
+        _initSuccessThumbs: function () {
+            var self = this;
+            if (!self.showPreview) {
+                return;
+            }
+            self._getThumbs('.file-preview-success').each(function () {
+                var $thumb = $(this), $remove = $thumb.find('.kv-file-remove');
+                $remove.removeAttr('disabled');
+                handler($remove, 'click', function () {
+                    var out = self._raise('filesuccessremove', [$thumb.attr('id'), $thumb.data('fileindex')]);
+                    cleanMemory($thumb);
+                    if (out === false) {
+                        return;
+                    }
+                    $thumb.fadeOut('slow', function () {
+                        $thumb.remove();
+                        if (!self.$preview.find('.file-preview-frame').length) {
+                            self.reset();
+                        }
+                    });
+                });
+            });
+        },
+        _checkAsyncComplete: function () {
+            var self = this, previewId, i;
+            for (i = 0; i < self.filestack.length; i++) {
+                if (self.filestack[i]) {
+                    previewId = self.previewInitId + "-" + i;
+                    if ($.inArray(previewId, self.uploadLog) === -1) {
+                        return false;
+                    }
+                }
+            }
+            return (self.uploadAsyncCount === self.uploadLog.length);
+        },
+        _uploadExtra: function (previewId, index) {
+            var self = this, data = self._getExtraData(previewId, index);
+            if (data.length === 0) {
+                return;
+            }
+            $.each(data, function (key, value) {
+                self.formdata.append(key, value);
+            });
+        },
+        _uploadSingle: function (i, files, allFiles) {
+            var self = this, total = self.getFileStack().length, formdata = new FormData(), outData,
+                previewId = self.previewInitId + "-" + i, $thumb, chkComplete, $btnUpload, $btnDelete,
+                hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData),
+                fnBefore, fnSuccess, fnComplete, fnError, updateUploadLog, params = {id: previewId, index: i};
+            self.formdata = formdata;
+            if (self.showPreview) {
+                $thumb = $('#' + previewId + ':not(.file-preview-initial)');
+                $btnUpload = $thumb.find('.kv-file-upload');
+                $btnDelete = $thumb.find('.kv-file-remove');
+                $('#' + previewId).find('.file-thumb-progress').removeClass('hide');
+            }
+            if (total === 0 || !hasPostData || ($btnUpload && $btnUpload.hasClass('disabled')) || self._abort(params)) {
+                return;
+            }
+            updateUploadLog = function (i, previewId) {
+                self.updateStack(i, undefined);
+                self.uploadLog.push(previewId);
+                if (self._checkAsyncComplete()) {
+                    self.fileBatchCompleted = true;
+                }
+            };
+            chkComplete = function () {
+                if (!self.fileBatchCompleted) {
+                    return;
+                }
+                setTimeout(function () {
+                    if (self.showPreview) {
+                        previewCache.set(
+                            self.id,
+                            self.uploadCache.content,
+                            self.uploadCache.config,
+                            self.uploadCache.tags,
+                            self.uploadCache.append
+                        );
+                        if (self.hasInitData) {
+                            self._initPreview();
+                            self._initPreviewDeletes();
+                        }
+                    }
+                    self.unlock();
+                    self._clearFileInput();
+                    self._raise('filebatchuploadcomplete', [self.filestack, self._getExtraData()]);
+                    self.uploadCount = 0;
+                    self.uploadStatus = {};
+                    self.uploadLog = [];
+                    self._setProgress(100);
+                }, 100);
+            };
+            fnBefore = function (jqXHR) {
+                outData = self._getOutData(jqXHR);
+                self.fileBatchCompleted = false;
+                if (self.showPreview) {
+                    if (!$thumb.hasClass('file-preview-success')) {
+                        self._setThumbStatus($thumb, 'Loading');
+                        addCss($thumb, 'file-uploading');
+                    }
+                    $btnUpload.attr('disabled', true);
+                    $btnDelete.attr('disabled', true);
+                }
+                if (!allFiles) {
+                    self.lock();
+                }
+                self._raise('filepreupload', [outData, previewId, i]);
+                $.extend(true, params, outData);
+                if (self._abort(params)) {
+                    jqXHR.abort();
+                    self._setProgressCancelled();
+                }
+            };
+            fnSuccess = function (data, textStatus, jqXHR) {
+                outData = self._getOutData(jqXHR, data);
+                $.extend(true, params, outData);
+                setTimeout(function () {
+                    if (isEmpty(data) || isEmpty(data.error)) {
+                        if (self.showPreview) {
+                            self._setThumbStatus($thumb, 'Success');
+                            $btnUpload.hide();
+                            self._initUploadSuccess(data, $thumb, allFiles);
+                        }
+                        self._raise('fileuploaded', [outData, previewId, i]);
+                        if (!allFiles) {
+                            self.updateStack(i, undefined);
+                        } else {
+                            updateUploadLog(i, previewId);
+                        }
+                    } else {
+                        self._showUploadError(data.error, params);
+                        self._setPreviewError($thumb, i);
+                        if (allFiles) {
+                            updateUploadLog(i, previewId);
+                        }
+                    }
+                }, 100);
+            };
+            fnComplete = function () {
+                setTimeout(function () {
+                    if (self.showPreview) {
+                        $btnUpload.removeAttr('disabled');
+                        $btnDelete.removeAttr('disabled');
+                        $thumb.removeClass('file-uploading');
+                    }
+                    if (!allFiles) {
+                        self.unlock(false);
+                        self._clearFileInput();
+                    } else {
+                        chkComplete();
+                    }
+                    self._initSuccessThumbs();
+                }, 100);
+            };
+            fnError = function (jqXHR, textStatus, errorThrown) {
+                var errMsg = self._parseError(jqXHR, errorThrown, (allFiles ? files[i].name : null));
+                setTimeout(function () {
+                    if (allFiles) {
+                        updateUploadLog(i, previewId);
+                    }
+                    self.uploadStatus[previewId] = 100;
+                    self._setPreviewError($thumb, i);
+                    $.extend(true, params, self._getOutData(jqXHR));
+                    self._showUploadError(errMsg, params);
+                }, 100);
+            };
+            formdata.append(self.uploadFileAttr, files[i], self.filenames[i]);
+            formdata.append('file_id', i);
+            self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError, previewId, i);
+        },
+        _uploadBatch: function () {
+            var self = this, files = self.filestack, total = files.length, params = {}, fnBefore, fnSuccess, fnError,
+                fnComplete, hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData),
+                setAllUploaded;
+            self.formdata = new FormData();
+            if (total === 0 || !hasPostData || self._abort(params)) {
+                return;
+            }
+            setAllUploaded = function () {
+                $.each(files, function (key) {
+                    self.updateStack(key, undefined);
+                });
+                self._clearFileInput();
+            };
+            fnBefore = function (jqXHR) {
+                self.lock();
+                var outData = self._getOutData(jqXHR);
+                if (self.showPreview) {
+                    self._getThumbs().each(function () {
+                        var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'),
+                            $btnDelete = $thumb.find('.kv-file-remove');
+                        if (!$thumb.hasClass('file-preview-success')) {
+                            self._setThumbStatus($thumb, 'Loading');
+                            addCss($thumb, 'file-uploading');
+                        }
+                        $btnUpload.attr('disabled', true);
+                        $btnDelete.attr('disabled', true);
+                    });
+                }
+                self._raise('filebatchpreupload', [outData]);
+                if (self._abort(outData)) {
+                    jqXHR.abort();
+                    self._setProgressCancelled();
+                }
+            };
+            fnSuccess = function (data, textStatus, jqXHR) {
+                /** @namespace data.errorkeys */
+                var outData = self._getOutData(jqXHR, data), $thumbs = self._getThumbs(), key = 0,
+                    keys = isEmpty(data) || isEmpty(data.errorkeys) ? [] : data.errorkeys;
+                if (isEmpty(data) || isEmpty(data.error)) {
+                    self._raise('filebatchuploadsuccess', [outData]);
+                    setAllUploaded();
+                    if (self.showPreview) {
+                        $thumbs.each(function () {
+                            var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload');
+                            $thumb.find('.kv-file-upload').hide();
+                            self._setThumbStatus($thumb, 'Success');
+                            $thumb.removeClass('file-uploading');
+                            $btnUpload.removeAttr('disabled');
+                        });
+                        self._initUploadSuccess(data);
+                    } else {
+                        self.reset();
+                    }
+                } else {
+                    if (self.showPreview) {
+                        $thumbs.each(function () {
+                            var $thumb = $(this), $btnDelete = $thumb.find('.kv-file-remove'),
+                                $btnUpload = $thumb.find('.kv-file-upload');
+                            $thumb.removeClass('file-uploading');
+                            $btnUpload.removeAttr('disabled');
+                            $btnDelete.removeAttr('disabled');
+                            if (keys.length === 0) {
+                                self._setPreviewError($thumb);
+                                return;
+                            }
+                            if ($.inArray(key, keys) !== -1) {
+                                self._setPreviewError($thumb);
+                            } else {
+                                $thumb.find('.kv-file-upload').hide();
+                                self._setThumbStatus($thumb, 'Success');
+                                self.updateStack(key, undefined);
+                            }
+                            key++;
+                        });
+                        self._initUploadSuccess(data);
+                    }
+                    self._showUploadError(data.error, outData, 'filebatchuploaderror');
+                }
+            };
+            fnComplete = function () {
+                self._setProgress(100);
+                self.unlock();
+                self._initSuccessThumbs();
+                self._clearFileInput();
+                self._raise('filebatchuploadcomplete', [self.filestack, self._getExtraData()]);
+            };
+            fnError = function (jqXHR, textStatus, errorThrown) {
+                var outData = self._getOutData(jqXHR), errMsg = self._parseError(jqXHR, errorThrown);
+                self._showUploadError(errMsg, outData, 'filebatchuploaderror');
+                self.uploadFileCount = total - 1;
+                if (!self.showPreview) {
+                    return;
+                }
+                self._getThumbs().each(function () {
+                    var $thumb = $(this), key = $thumb.attr('data-fileindex');
+                    $thumb.removeClass('file-uploading');
+                    if (self.filestack[key] !== undefined) {
+                        self._setPreviewError($thumb);
+                    }
+                });
+                self._getThumbs().removeClass('file-uploading');
+                self._getThumbs(' .kv-file-upload').removeAttr('disabled');
+                self._getThumbs(' .kv-file-delete').removeAttr('disabled');
+            };
+            $.each(files, function (key, data) {
+                if (!isEmpty(files[key])) {
+                    self.formdata.append(self.uploadFileAttr, data, self.filenames[key]);
+                }
+            });
+            self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError);
+        },
+        _uploadExtraOnly: function () {
+            var self = this, params = {}, fnBefore, fnSuccess, fnComplete, fnError;
+            self.formdata = new FormData();
+            if (self._abort(params)) {
+                return;
+            }
+            fnBefore = function (jqXHR) {
+                self.lock();
+                var outData = self._getOutData(jqXHR);
+                self._raise('filebatchpreupload', [outData]);
+                self._setProgress(50);
+                params.data = outData;
+                params.xhr = jqXHR;
+                if (self._abort(params)) {
+                    jqXHR.abort();
+                    self._setProgressCancelled();
+                }
+            };
+            fnSuccess = function (data, textStatus, jqXHR) {
+                var outData = self._getOutData(jqXHR, data);
+                if (isEmpty(data) || isEmpty(data.error)) {
+                    self._raise('filebatchuploadsuccess', [outData]);
+                    self._clearFileInput();
+                    self._initUploadSuccess(data);
+                } else {
+                    self._showUploadError(data.error, outData, 'filebatchuploaderror');
+                }
+            };
+            fnComplete = function () {
+                self._setProgress(100);
+                self.unlock();
+                self._clearFileInput();
+                self._raise('filebatchuploadcomplete', [self.filestack, self._getExtraData()]);
+            };
+            fnError = function (jqXHR, textStatus, errorThrown) {
+                var outData = self._getOutData(jqXHR), errMsg = self._parseError(jqXHR, errorThrown);
+                params.data = outData;
+                self._showUploadError(errMsg, outData, 'filebatchuploaderror');
+            };
+            self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError);
+        },
+        _initFileActions: function () {
+            var self = this;
+            if (!self.showPreview) {
+                return;
+            }
+            self.$preview.find('.kv-file-remove').each(function () {
+                var $el = $(this), $frame = $el.closest('.file-preview-frame'), hasError,
+                    id = $frame.attr('id'), ind = $frame.attr('data-fileindex'), n, cap, status;
+                handler($el, 'click', function () {
+                    status = self._raise('filepreremove', [id, ind]);
+                    if (status === false || !self._validateMinCount()) {
+                        return false;
+                    }
+                    hasError = $frame.hasClass('file-preview-error');
+                    cleanMemory($frame);
+                    $frame.fadeOut('slow', function () {
+                        self.updateStack(ind, undefined);
+                        self._clearObjects($frame);
+                        $frame.remove();
+                        if (id && hasError) {
+                            self.$errorContainer.find('li[data-file-id="' + id + '"]').fadeOut('fast', function () {
+                                $(this).remove();
+                                if (!self._errorsExist()) {
+                                    self._resetErrors();
+                                }
+                            });
+                        }
+                        var filestack = self.getFileStack(true), len = filestack.length, chk = previewCache.count(
+                            self.id),
+                            hasThumb = self.showPreview && self.$preview.find('.file-preview-frame').length;
+                        self._clearFileInput();
+                        if (len === 0 && chk === 0 && !hasThumb) {
+                            self.reset();
+                        } else {
+                            n = chk + len;
+                            cap = n > 1 ? self._getMsgSelected(n) : (filestack[0] ? self._getFileNames()[0] : '');
+                            self._setCaption(cap);
+                        }
+                        self._raise('fileremoved', [id, ind]);
+                    });
+                });
+            });
+            self.$preview.find('.kv-file-upload').each(function () {
+                var $el = $(this);
+                handler($el, 'click', function () {
+                    var $frame = $el.closest('.file-preview-frame'),
+                        ind = $frame.attr('data-fileindex');
+                    if (!$frame.hasClass('file-preview-error')) {
+                        self._uploadSingle(ind, self.filestack, false);
+                    }
+                });
+            });
+        },
+        _hideFileIcon: function () {
+            if (this.overwriteInitial) {
+                this.$captionContainer.find('.kv-caption-icon').hide();
+            }
+        },
+        _showFileIcon: function () {
+            this.$captionContainer.find('.kv-caption-icon').show();
+        },
+        _previewDefault: function (file, previewId, isDisabled) {
+            if (!this.showPreview) {
+                return;
+            }
+            var self = this, frameClass = '', fname = file ? file.name : '',
+                /** @namespace objUrl.createObjectURL */
+                data = objUrl.createObjectURL(file), ind = previewId.slice(previewId.lastIndexOf('-') + 1),
+                config = self.previewSettings.other || defaultPreviewSettings.other,
+                footer = self._renderFileFooter(file.name, config.width),
+                previewOtherTemplate = self._parseFilePreviewIcon(self._getPreviewTemplate('other'), fname);
+            if (isDisabled === true) {
+                if (!self.isUploadable) {
+                    footer += '<div class="file-other-error" title="' + self.fileActionSettings.indicatorErrorTitle +
+                        '">' + self.fileActionSettings.indicatorError + '</div>';
+                }
+            }
+            self._clearDefaultPreview();
+            self.$preview.append("\n" + previewOtherTemplate
+                    .replace(/\{previewId}/g, previewId)
+                    .replace(/\{frameClass}/g, frameClass)
+                    .replace(/\{fileindex}/g, ind)
+                    .replace(/\{caption}/g, self.slug(file.name))
+                    .replace(/\{width}/g, config.width)
+                    .replace(/\{height}/g, config.height)
+                    .replace(/\{type}/g, file.type)
+                    .replace(/\{data}/g, data)
+                    .replace(/\{footer}/g, footer));
+            if (isDisabled === true && self.isUploadable) {
+                self._setThumbStatus($('#' + previewId), 'Error');
+            }
+        },
+        _previewFile: function (i, file, theFile, previewId, data) {
+            if (!this.showPreview) {
+                return;
+            }
+            var self = this, cat = self._parseFileType(file), fname = file ? file.name : '', caption = self.slug(fname),
+                content, strText, types = self.allowedPreviewTypes, mimes = self.allowedPreviewMimeTypes,
+                tmplt = self._getPreviewTemplate(cat), chkTypes = types && types.indexOf(cat) >= 0, id,
+                config = isSet(cat, self.previewSettings) ? self.previewSettings[cat] : defaultPreviewSettings[cat],
+                chkMimes = mimes && mimes.indexOf(file.type) !== -1,
+                footer = self._renderFileFooter(caption, config.width), modal = '',
+                ind = previewId.slice(previewId.lastIndexOf('-') + 1);
+            if (chkTypes || chkMimes) {
+                tmplt = self._parseFilePreviewIcon(tmplt, fname.split('.').pop());
+                if (cat === 'text') {
+                    strText = htmlEncode(theFile.target.result);
+                    id = 'text-' + uniqId();
+                    content = tmplt.replace(/\{zoom}/g, self._getLayoutTemplate('zoom'));
+                    modal = self._getLayoutTemplate('modal').replace('{id}', id)
+                        .replace(/\{title}/g, caption)
+                        .replace(/\{body}/g, strText).replace(/\{heading}/g, self.msgZoomModalHeading);
+                    content = content.replace(/\{previewId}/g, previewId).replace(/\{caption}/g, caption)
+                            .replace(/\{width}/g, config.width).replace(/\{height}/g, config.height)
+                            .replace(/\{frameClass}/g, '').replace(/\{zoomInd}/g, self.zoomIndicator)
+                            .replace(/\{footer}/g, footer).replace(/\{fileindex}/g, ind)
+                            .replace(/\{type}/g, file.type).replace(/\{zoomTitle}/g, self.msgZoomTitle)
+                            .replace(/\{dialog}/g, "$('#" + id + "').modal('show')")
+                            .replace(/\{data}/g, strText) + modal;
+                } else {
+                    content = tmplt.replace(/\{previewId}/g, previewId).replace(/\{caption}/g, caption)
+                        .replace(/\{frameClass}/g, '').replace(/\{type}/g, file.type).replace(/\{fileindex}/g, ind)
+                        .replace(/\{width}/g, config.width).replace(/\{height}/g, config.height)
+                        .replace(/\{footer}/g, footer).replace(/\{data}/g, data);
+                }
+                self._clearDefaultPreview();
+                self.$preview.append("\n" + content);
+                self._validateImage(i, previewId, caption, file.type);
+            } else {
+                self._previewDefault(file, previewId);
+            }
+        },
+        _slugDefault: function (text) {
+            return isEmpty(text) ? '' : String(text).replace(/[\-\[\]\/\{}:;#%=\(\)\*\+\?\\\^\$\|<>&"']/g, '_');
+        },
+        _readFiles: function (files) {
+            this.reader = new FileReader();
+            var self = this, $el = self.$element, $preview = self.$preview, reader = self.reader,
+                $container = self.$previewContainer, $status = self.$previewStatus, msgLoading = self.msgLoading,
+                msgProgress = self.msgProgress, previewInitId = self.previewInitId, numFiles = files.length,
+                settings = self.fileTypeSettings, ctr = self.filestack.length, readFile,
+                throwError = function (msg, file, previewId, index) {
+                    var p1 = $.extend(true, {}, self._getOutData({}, {}, files), {id: previewId, index: index}),
+                        p2 = {id: previewId, index: index, file: file, files: files};
+                    self._previewDefault(file, previewId, true);
+                    if (self.isUploadable) {
+                        self.addToStack(undefined);
+                    }
+                    setTimeout(readFile(index + 1), 100);
+                    self._initFileActions();
+                    if (self.removeFromPreviewOnError) {
+                        $('#' + previewId).remove();
+                    }
+                    return self.isUploadable ? self._showUploadError(msg, p1) : self._showError(msg, p2);
+                };
+
+            self.loadedImages = [];
+            self.totalImagesCount = 0;
+
+            $.each(files, function (key, file) {
+                var func = self.fileTypeSettings.image || defaultFileTypeSettings.image;
+                if (func && func(file.type)) {
+                    self.totalImagesCount++;
+                }
+            });
+
+            readFile = function (i) {
+                if (isEmpty($el.attr('multiple'))) {
+                    numFiles = 1;
+                }
+                if (i >= numFiles) {
+                    if (self.isUploadable && self.filestack.length > 0) {
+                        self._raise('filebatchselected', [self.getFileStack()]);
+                    } else {
+                        self._raise('filebatchselected', [files]);
+                    }
+                    $container.removeClass('file-thumb-loading');
+                    $status.html('');
+                    return;
+                }
+                var node = ctr + i, previewId = previewInitId + "-" + node, isText, file = files[i],
+                    caption = self.slug(file.name), fileSize = (file.size || 0) / 1000, checkFile, fileExtExpr = '',
+                    previewData = objUrl.createObjectURL(file), fileCount = 0, j, msg, typ, chk,
+                    fileTypes = self.allowedFileTypes, strTypes = isEmpty(fileTypes) ? '' : fileTypes.join(', '),
+                    fileExt = self.allowedFileExtensions, strExt = isEmpty(fileExt) ? '' : fileExt.join(', ');
+                if (!isEmpty(fileExt)) {
+                    fileExtExpr = new RegExp('\\.(' + fileExt.join('|') + ')$', 'i');
+                }
+                fileSize = fileSize.toFixed(2);
+                if (self.maxFileSize > 0 && fileSize > self.maxFileSize) {
+                    msg = self.msgSizeTooLarge.replace('{name}', caption)
+                        .replace('{size}', fileSize)
+                        .replace('{maxSize}', self.maxFileSize);
+                    self.isError = throwError(msg, file, previewId, i);
+                    return;
+                }
+                if (!isEmpty(fileTypes) && isArray(fileTypes)) {
+                    for (j = 0; j < fileTypes.length; j += 1) {
+                        typ = fileTypes[j];
+                        checkFile = settings[typ];
+                        chk = (checkFile !== undefined && checkFile(file.type, caption));
+                        fileCount += isEmpty(chk) ? 0 : chk.length;
+                    }
+                    if (fileCount === 0) {
+                        msg = self.msgInvalidFileType.replace('{name}', caption).replace('{types}', strTypes);
+                        self.isError = throwError(msg, file, previewId, i);
+                        return;
+                    }
+                }
+                if (fileCount === 0 && !isEmpty(fileExt) && isArray(fileExt) && !isEmpty(fileExtExpr)) {
+                    chk = compare(caption, fileExtExpr);
+                    fileCount += isEmpty(chk) ? 0 : chk.length;
+                    if (fileCount === 0) {
+                        msg = self.msgInvalidFileExtension.replace('{name}', caption).replace('{extensions}',
+                            strExt);
+                        self.isError = throwError(msg, file, previewId, i);
+                        return;
+                    }
+                }
+                if (!self.showPreview) {
+                    self.addToStack(file);
+                    setTimeout(readFile(i + 1), 100);
+                    self._raise('fileloaded', [file, previewId, i, reader]);
+                    return;
+                }
+                if ($preview.length > 0 && FileReader !== undefined) {
+                    $status.html(msgLoading.replace('{index}', i + 1).replace('{files}', numFiles));
+                    $container.addClass('file-thumb-loading');
+                    reader.onerror = function (evt) {
+                        self._errorHandler(evt, caption);
+                    };
+                    reader.onload = function (theFile) {
+                        self._previewFile(i, file, theFile, previewId, previewData);
+                        self._initFileActions();
+                    };
+                    reader.onloadend = function () {
+                        msg = msgProgress
+                            .replace('{index}', i + 1).replace('{files}', numFiles)
+                            .replace('{percent}', 50).replace('{name}', caption);
+                        setTimeout(function () {
+                            $status.html(msg);
+                            self._updateFileDetails(numFiles);
+                            readFile(i + 1);
+                        }, 100);
+                        self._raise('fileloaded', [file, previewId, i, reader]);
+                    };
+                    reader.onprogress = function (data) {
+                        if (data.lengthComputable) {
+                            var fact = (data.loaded / data.total) * 100, progress = Math.ceil(fact);
+                            msg = msgProgress.replace('{index}', i + 1).replace('{files}', numFiles)
+                                .replace('{percent}', progress).replace('{name}', caption);
+                            setTimeout(function () {
+                                $status.html(msg);
+                            }, 100);
+                        }
+                    };
+                    isText = isSet('text', settings) ? settings.text : defaultFileTypeSettings.text;
+                    if (isText(file.type, caption)) {
+                        reader.readAsText(file, self.textEncoding);
+                    } else {
+                        reader.readAsArrayBuffer(file);
+                    }
+                } else {
+                    self._previewDefault(file, previewId);
+                    setTimeout(function () {
+                        readFile(i + 1);
+                        self._updateFileDetails(numFiles);
+                    }, 100);
+                    self._raise('fileloaded', [file, previewId, i, reader]);
+                }
+                self.addToStack(file);
+            };
+
+            readFile(0);
+            self._updateFileDetails(numFiles, false);
+        },
+        _updateFileDetails: function (numFiles) {
+            var self = this, $el = self.$element, fileStack = self.getFileStack(),
+                name = ($el[0].files[0] && $el[0].files[0].name) || (fileStack.length && fileStack[0].name) || '',
+                label = self.slug(name), n = self.isUploadable ? fileStack.length : numFiles,
+                nFiles = previewCache.count(self.id) + n, log = n > 1 ? self._getMsgSelected(nFiles) : label;
+            if (self.isError) {
+                self.$previewContainer.removeClass('file-thumb-loading');
+                self.$previewStatus.html('');
+                self.$captionContainer.find('.kv-caption-icon').hide();
+            } else {
+                self._showFileIcon();
+            }
+            self._setCaption(log, self.isError);
+            self.$container.removeClass('file-input-new file-input-ajax-new');
+            if (arguments.length === 1) {
+                self._raise('fileselect', [numFiles, label]);
+            }
+            if (previewCache.count(self.id)) {
+                self._initPreviewDeletes();
+            }
+        },
+        _setThumbStatus: function ($thumb, status) {
+            var self = this;
+            if (!self.showPreview) {
+                return;
+            }
+            var icon = 'indicator' + status, msg = icon + 'Title',
+                css = 'file-preview-' + status.toLowerCase(),
+                $indicator = $thumb.find('.file-upload-indicator'),
+                config = self.fileActionSettings;
+            $thumb.removeClass('file-preview-success file-preview-error file-preview-loading');
+            if (status === 'Error') {
+                $thumb.find('.kv-file-upload').attr('disabled', true);
+            }
+            $indicator.html(config[icon]);
+            $indicator.attr('title', config[msg]);
+            $thumb.addClass(css);
+        },
+        _setProgressCancelled: function () {
+            var self = this;
+            self._setProgress(100, self.$progress, self.msgCancelled);
+        },
+        _setProgress: function (p, $el, error) {
+            var self = this, pct = Math.min(p, 100), template = pct < 100 ? self.progressTemplate :
+                (error ? self.progressErrorTemplate : self.progressCompleteTemplate);
+            $el = $el || self.$progress;
+            if (!isEmpty(template)) {
+                $el.html(template.replace(/\{percent}/g, pct));
+                if (error) {
+                    $el.find('[role="progressbar"]').html(error);
+                }
+            }
+        },
+        _setFileDropZoneTitle: function () {
+            var self = this, $zone = self.$container.find('.file-drop-zone');
+            $zone.find('.' + self.dropZoneTitleClass).remove();
+            if (!self.isUploadable || !self.showPreview || $zone.length === 0 || self.getFileStack().length > 0 || !self.dropZoneEnabled) {
+                return;
+            }
+            if ($zone.find('.file-preview-frame').length === 0 && isEmpty(self.defaultPreviewContent)) {
+                $zone.prepend('<div class="' + self.dropZoneTitleClass + '">' + self.dropZoneTitle + '</div>');
+            }
+            self.$container.removeClass('file-input-new');
+            addCss(self.$container, 'file-input-ajax-new');
+        },
+        _setAsyncUploadStatus: function (previewId, pct, total) {
+            var self = this, sum = 0;
+            self._setProgress(pct, $('#' + previewId).find('.file-thumb-progress'));
+            self.uploadStatus[previewId] = pct;
+            $.each(self.uploadStatus, function (key, value) {
+                sum += value;
+            });
+            self._setProgress(Math.ceil(sum / total));
+
+        },
+        _validateMinCount: function () {
+            var self = this, len = self.isUploadable ? self.getFileStack().length : self.$element.get(0).files.length;
+            if (self.validateInitialCount && self.minFileCount > 0 && self._getFileCount(
+                    len - 1) < self.minFileCount) {
+                self._noFilesError({});
+                return false;
+            }
+            return true;
+        },
+        _getFileCount: function (fileCount) {
+            var self = this, addCount = 0;
+            if (self.validateInitialCount && !self.overwriteInitial) {
+                addCount = previewCache.count(self.id);
+                fileCount += addCount;
+            }
+            return fileCount;
+        },
+        _getFileName: function (file) {
+            return file && file.name ? this.slug(file.name) : undefined;
+        },
+        _getFileNames: function (skipNull) {
+            var self = this;
+            return self.filenames.filter(function (n) {
+                return (skipNull ? n !== undefined : n !== undefined && n !== null);
+            });
+        },
+        _setPreviewError: function ($thumb, i, val) {
+            var self = this;
+            if (i) {
+                self.updateStack(i, val);
+            }
+            if (self.removeFromPreviewOnError) {
+                $thumb.remove();
+            } else {
+                self._setThumbStatus($thumb, 'Error');
+            }
+        },
+        _checkDimensions: function (i, chk, $img, $thumb, fname, type, params) {
+            var self = this, msg, dim, tag = chk === 'Small' ? 'min' : 'max', limit = self[tag + 'Image' + type],
+                $imgEl, isValid;
+            if (isEmpty(limit) || !$img.length) {
+                return;
+            }
+            $imgEl = $img[0];
+            dim = (type === 'Width') ? $imgEl.naturalWidth || $imgEl.width : $imgEl.naturalHeight || $imgEl.height;
+            isValid = chk === 'Small' ? dim >= limit : dim <= limit;
+            if (isValid) {
+                return;
+            }
+            msg = self['msgImage' + type + chk].replace('{name}', fname).replace('{size}', limit);
+            self._showUploadError(msg, params);
+            self._setPreviewError($thumb, i, null);
+        },
+        _validateImage: function (i, previewId, fname, ftype) {
+            var self = this, $preview = self.$preview, params, w1, w2,
+                $thumb = $preview.find("#" + previewId), $img = $thumb.find('img');
+            fname = fname || 'Untitled';
+            if (!$img.length) {
+                return;
+            }
+            handler($img, 'load', function () {
+                w1 = $thumb.width();
+                w2 = $preview.width();
+                if (w1 > w2) {
+                    $img.css('width', '100%');
+                    $thumb.css('width', '97%');
+                }
+                params = {ind: i, id: previewId};
+                self._checkDimensions(i, 'Small', $img, $thumb, fname, 'Width', params);
+                self._checkDimensions(i, 'Small', $img, $thumb, fname, 'Height', params);
+                if (!self.resizeImage) {
+                    self._checkDimensions(i, 'Large', $img, $thumb, fname, 'Width', params);
+                    self._checkDimensions(i, 'Large', $img, $thumb, fname, 'Height', params);
+                }
+                self._raise('fileimageloaded', [previewId]);
+                self.loadedImages.push({ind: i, img: $img, thumb: $thumb, pid: previewId, typ: ftype});
+                self._validateAllImages();
+                objUrl.revokeObjectURL($img.attr('src'));
+            });
+        },
+        _validateAllImages: function () {
+            var self = this, i, config, $img, $thumb, pid, ind, params = {}, errFunc;
+            if (self.loadedImages.length !== self.totalImagesCount) {
+                return;
+            }
+            self._raise('fileimagesloaded');
+            if (!self.resizeImage) {
+                return;
+            }
+            errFunc = self.isUploadable ? self._showUploadError : self._showError;
+            for (i = 0; i < self.loadedImages.length; i++) {
+                config = self.loadedImages[i];
+                $img = config.img;
+                $thumb = config.thumb;
+                pid = config.pid;
+                ind = config.ind;
+                params = {id: pid, 'index': ind};
+                if (!self._getResizedImage($img[0], config.typ, pid, ind)) {
+                    errFunc(self.msgImageResizeError, params, 'fileimageresizeerror');
+                    self._setPreviewError($thumb, ind);
+                }
+            }
+            self._raise('fileimagesresized');
+        },
+        _getResizedImage: function (image, type, pid, ind) {
+            var self = this, width = image.naturalWidth, height = image.naturalHeight, ratio = 1,
+                maxWidth = self.maxImageWidth || width, maxHeight = self.maxImageHeight || height,
+                isValidImage = (width && height), chkWidth, chkHeight,
+                canvas = self.imageCanvas, context = self.imageCanvasContext;
+            if (!isValidImage) {
+                return false;
+            }
+            if (width === maxWidth && height === maxHeight) {
+                return true;
+            }
+            type = type || self.resizeDefaultImageType;
+            chkWidth = width > maxWidth;
+            chkHeight = height > maxHeight;
+            if (self.resizePreference === 'width') {
+                ratio = chkWidth ? maxWidth / width : (chkHeight ? maxHeight / height : 1);
+            } else {
+                ratio = chkHeight ? maxHeight / height : (chkWidth ? maxWidth / width : 1);
+            }
+            self._resetCanvas();
+            width *= ratio;
+            height *= ratio;
+            canvas.width = width;
+            canvas.height = height;
+            try {
+                context.drawImage(image, 0, 0, width, height);
+                canvas.toBlob(function (blob) {
+                    self._raise('fileimageresized', [pid, ind]);
+                    self.filestack[ind] = blob;
+                }, type, self.resizeQuality);
+                return true;
+            }
+            catch (err) {
+                return false;
+            }
+        },
+        _initBrowse: function ($container) {
+            var self = this;
+            self.$btnFile = $container.find('.btn-file');
+            self.$btnFile.append(self.$element);
+        },
+        _initCaption: function () {
+            var self = this, cap = self.initialCaption || '';
+            if (self.overwriteInitial || isEmpty(cap)) {
+                self.$caption.html('');
+                return false;
+            }
+            self._setCaption(cap);
+            return true;
+        },
+        _setCaption: function (content, isError) {
+            var self = this, title, out, n, cap, stack = self.getFileStack();
+            if (!self.$caption.length) {
+                return;
+            }
+            if (isError) {
+                title = $('<div>' + self.msgValidationError + '</div>').text();
+                n = stack.length;
+                if (n) {
+                    cap = n === 1 && stack[0] ? self._getFileNames()[0] : self._getMsgSelected(n);
+                } else {
+                    cap = self._getMsgSelected(self.msgNo);
+                }
+                out = '<span class="' + self.msgValidationErrorClass + '">' + self.msgValidationErrorIcon +
+                    (isEmpty(content) ? cap : content) + '</span>';
+            } else {
+                if (isEmpty(content)) {
+                    return;
+                }
+                title = $('<div>' + content + '</div>').text();
+                out = self._getLayoutTemplate('icon') + title;
+            }
+            self.$caption.html(out);
+            self.$caption.attr('title', title);
+            self.$captionContainer.find('.file-caption-ellipsis').attr('title', title);
+        },
+        _createContainer: function () {
+            var self = this,
+                $container = $(document.createElement("div"))
+                    .attr({"class": 'file-input file-input-new'})
+                    .html(self._renderMain());
+            self.$element.before($container);
+            self._initBrowse($container);
+            return $container;
+        },
+        _refreshContainer: function () {
+            var self = this, $container = self.$container;
+            $container.before(self.$element);
+            $container.html(self._renderMain());
+            self._initBrowse($container);
+        },
+        _renderMain: function () {
+            var self = this, dropCss = (self.isUploadable && self.dropZoneEnabled) ? ' file-drop-zone' : 'file-drop-disabled',
+                close = !self.showClose ? '' : self._getLayoutTemplate('close'),
+                preview = !self.showPreview ? '' : self._getLayoutTemplate('preview')
+                    .replace(/\{class}/g, self.previewClass)
+                    .replace(/\{dropClass}/g, dropCss),
+                css = self.isDisabled ? self.captionClass + ' file-caption-disabled' : self.captionClass,
+                caption = self.captionTemplate.replace(/\{class}/g, css + ' kv-fileinput-caption');
+            return self.mainTemplate.replace(/\{class}/g, self.mainClass)
+                .replace(/\{preview}/g, preview)
+                .replace(/\{close}/g, close)
+                .replace(/\{caption}/g, caption)
+                .replace(/\{upload}/g, self._renderButton('upload'))
+                .replace(/\{remove}/g, self._renderButton('remove'))
+                .replace(/\{cancel}/g, self._renderButton('cancel'))
+                .replace(/\{browse}/g, self._renderButton('browse'));
+        },
+        _renderButton: function (type) {
+            var self = this, tmplt = self._getLayoutTemplate('btnDefault'), css = self[type + 'Class'],
+                title = self[type + 'Title'], icon = self[type + 'Icon'], label = self[type + 'Label'],
+                status = self.isDisabled ? ' disabled' : '', btnType = 'button';
+            switch (type) {
+                case 'remove':
+                    if (!self.showRemove) {
+                        return '';
+                    }
+                    break;
+                case 'cancel':
+                    if (!self.showCancel) {
+                        return '';
+                    }
+                    css += ' hide';
+                    break;
+                case 'upload':
+                    if (!self.showUpload) {
+                        return '';
+                    }
+                    if (self.isUploadable && !self.isDisabled) {
+                        tmplt = self._getLayoutTemplate('btnLink').replace('{href}', self.uploadUrl);
+                    } else {
+                        btnType = 'submit';
+                    }
+                    break;
+                case 'browse':
+                    tmplt = self._getLayoutTemplate('btnBrowse');
+                    break;
+                default:
+                    return '';
+            }
+            css += type === 'browse' ? ' btn-file' : ' fileinput-' + type + ' fileinput-' + type + '-button';
+            if (!isEmpty(label)) {
+                label = ' <span class="' + self.buttonLabelClass + '">' + label + '</span>';
+            }
+            return tmplt.replace('{type}', btnType)
+                .replace('{css}', css)
+                .replace('{title}', title)
+                .replace('{status}', status)
+                .replace('{icon}', icon)
+                .replace('{label}', label);
+        },
+        _renderThumbProgress: function () {
+            return '<div class="file-thumb-progress hide">' + this.progressTemplate.replace(/\{percent}/g,
+                    '0') + '</div>';
+        },
+        _renderFileFooter: function (caption, width) {
+            var self = this, config = self.fileActionSettings, footer, out, template = self._getLayoutTemplate(
+                'footer');
+            if (self.isUploadable) {
+                footer = template.replace(/\{actions}/g, self._renderFileActions(true, true, false, false, false));
+                out = footer.replace(/\{caption}/g, caption)
+                    .replace(/\{width}/g, width)
+                    .replace(/\{progress}/g, self._renderThumbProgress())
+                    .replace(/\{indicator}/g, config.indicatorNew)
+                    .replace(/\{indicatorTitle}/g, config.indicatorNewTitle);
+            } else {
+                out = template.replace(/\{actions}/g, '')
+                    .replace(/\{caption}/g, caption)
+                    .replace(/\{progress}/g, '')
+                    .replace(/\{width}/g, width)
+                    .replace(/\{indicator}/g, '')
+                    .replace(/\{indicatorTitle}/g, '');
+            }
+            out = replaceTags(out, self.previewThumbTags);
+            return out;
+        },
+        _renderFileActions: function (showUpload, showDelete, disabled, url, key) {
+            if (!showUpload && !showDelete) {
+                return '';
+            }
+            var self = this,
+                vUrl = url === false ? '' : ' data-url="' + url + '"',
+                vKey = key === false ? '' : ' data-key="' + key + '"',
+                btnDelete = self._getLayoutTemplate('actionDelete'),
+                btnUpload = '',
+                template = self._getLayoutTemplate('actions'),
+                otherButtons = self.otherActionButtons.replace(/\{dataKey}/g, vKey),
+                config = self.fileActionSettings,
+                removeClass = disabled ? config.removeClass + ' disabled' : config.removeClass;
+            btnDelete = btnDelete
+                .replace(/\{removeClass}/g, removeClass)
+                .replace(/\{removeIcon}/g, config.removeIcon)
+                .replace(/\{removeTitle}/g, config.removeTitle)
+                .replace(/\{dataUrl}/g, vUrl)
+                .replace(/\{dataKey}/g, vKey);
+            if (showUpload) {
+                btnUpload = self._getLayoutTemplate('actionUpload')
+                    .replace(/\{uploadClass}/g, config.uploadClass)
+                    .replace(/\{uploadIcon}/g, config.uploadIcon)
+                    .replace(/\{uploadTitle}/g, config.uploadTitle);
+            }
+            return template
+                .replace(/\{delete}/g, btnDelete)
+                .replace(/\{upload}/g, btnUpload)
+                .replace(/\{other}/g, otherButtons);
+        },
+        _browse: function (e) {
+            var self = this;
+            self._raise('filebrowse');
+            if (e && e.isDefaultPrevented()) {
+                return;
+            }
+            if (self.isError && !self.isUploadable) {
+                self.clear();
+            }
+            self.$captionContainer.focus();
+        },
+        _change: function (e) {
+            var self = this, $el = self.$element;
+            if (!self.isUploadable && isEmpty($el.val()) && self.fileInputCleared) { // IE 11 fix
+                self.fileInputCleared = false;
+                return;
+            }
+            self.fileInputCleared = false;
+            var tfiles, msg, total, isDragDrop = arguments.length > 1, isAjaxUpload = self.isUploadable, i = 0, f, n, len,
+                files = isDragDrop ? e.originalEvent.dataTransfer.files : $el.get(0).files, ctr = self.filestack.length,
+                isSingleUpload = isEmpty($el.attr('multiple')), flagSingle = (isSingleUpload && ctr > 0), folders = 0,
+                throwError = function (mesg, file, previewId, index) {
+                    var p1 = $.extend(true, {}, self._getOutData({}, {}, files), {id: previewId, index: index}),
+                        p2 = {id: previewId, index: index, file: file, files: files};
+                    return self.isUploadable ? self._showUploadError(mesg, p1) : self._showError(mesg, p2);
+                };
+            self.reader = null;
+            self._resetUpload();
+            self._hideFileIcon();
+            if (self.isUploadable) {
+                self.$container.find('.file-drop-zone .' + self.dropZoneTitleClass).remove();
+            }
+            if (isDragDrop) {
+                tfiles = [];
+                while (files[i]) {
+                    f = files[i];
+                    if (!f.type && f.size % 4096 === 0) {
+                        folders++;
+                    } else {
+                        tfiles.push(f);
+                    }
+                    i++;
+                }
+            } else {
+                if (e.target.files === undefined) {
+                    tfiles = e.target && e.target.value ? [
+                        {name: e.target.value.replace(/^.+\\/, '')}
+                    ] : [];
+                } else {
+                    tfiles = e.target.files;
+                }
+            }
+            if (isEmpty(tfiles) || tfiles.length === 0) {
+                if (!isAjaxUpload) {
+                    self.clear();
+                }
+                self._showFolderError(folders);
+                self._raise('fileselectnone');
+                return;
+            }
+            self._resetErrors();
+            len = tfiles.length;
+            total = self._getFileCount(self.isUploadable ? (self.getFileStack().length + len) : len);
+            if (self.maxFileCount > 0 && total > self.maxFileCount) {
+                if (!self.autoReplace || len > self.maxFileCount) {
+                    n = (self.autoReplace && len > self.maxFileCount) ? len : total;
+                    msg = self.msgFilesTooMany.replace('{m}', self.maxFileCount).replace('{n}', n);
+                    self.isError = throwError(msg, null, null, null);
+                    self.$captionContainer.find('.kv-caption-icon').hide();
+                    self._setCaption('', true);
+                    self.$container.removeClass('file-input-new file-input-ajax-new');
+                    return;
+                }
+                if (total > self.maxFileCount) {
+                    self._resetPreviewThumbs(isAjaxUpload);
+                }
+            } else {
+                if (!isAjaxUpload || flagSingle) {
+                    self._resetPreviewThumbs(false);
+                    if (flagSingle) {
+                        self.clearStack();
+                    }
+                } else {
+                    if (isAjaxUpload && ctr === 0 && (!previewCache.count(self.id) || self.overwriteInitial)) {
+                        self._resetPreviewThumbs(true);
+                    }
+                }
+            }
+            if (self.isPreviewable) {
+                self._readFiles(tfiles);
+            } else {
+                self._updateFileDetails(1);
+            }
+            self._showFolderError(folders);
+        },
+        _abort: function (params) {
+            var self = this, data;
+            if (self.ajaxAborted && typeof self.ajaxAborted === "object" && self.ajaxAborted.message !== undefined) {
+                data = $.extend(true, {}, self._getOutData(), params);
+                data.abortData = self.ajaxAborted.data || {};
+                data.abortMessage = self.ajaxAborted.message;
+                self.cancel();
+                self._setProgress(100, self.$progress, self.msgCancelled);
+                self._showUploadError(self.ajaxAborted.message, data, 'filecustomerror');
+                return true;
+            }
+            return false;
+        },
+        _resetFileStack: function () {
+            var self = this, i = 0, newstack = [], newnames = [];
+            self._getThumbs().each(function () {
+                var $thumb = $(this), ind = $thumb.attr('data-fileindex'),
+                    file = self.filestack[ind];
+                if (ind === -1) {
+                    return;
+                }
+                if (file !== undefined) {
+                    newstack[i] = file;
+                    newnames[i] = self._getFileName(file);
+                    $thumb.attr({
+                        'id': self.previewInitId + '-' + i,
+                        'data-fileindex': i
+                    });
+                    i++;
+                } else {
+                    $thumb.attr({
+                        'id': 'uploaded-' + uniqId(),
+                        'data-fileindex': '-1'
+                    });
+                }
+            });
+            self.filestack = newstack;
+            self.filenames = newnames;
+        },
+        clearStack: function () {
+            var self = this;
+            self.filestack = [];
+            self.filenames = [];
+            return self.$element;
+        },
+        updateStack: function (i, file) {
+            var self = this;
+            self.filestack[i] = file;
+            self.filenames[i] = self._getFileName(file);
+            return self.$element;
+        },
+        addToStack: function (file) {
+            var self = this;
+            self.filestack.push(file);
+            self.filenames.push(self._getFileName(file));
+            return self.$element;
+        },
+        getFileStack: function (skipNull) {
+            var self = this;
+            return self.filestack.filter(function (n) {
+                return (skipNull ? n !== undefined : n !== undefined && n !== null);
+            });
+        },
+        lock: function () {
+            var self = this;
+            self._resetErrors();
+            self.disable();
+            if (self.showRemove) {
+                addCss(self.$container.find('.fileinput-remove'), 'hide');
+            }
+            if (self.showCancel) {
+                self.$container.find('.fileinput-cancel').removeClass('hide');
+            }
+            self._raise('filelock', [self.filestack, self._getExtraData()]);
+            return self.$element;
+        },
+        unlock: function (reset) {
+            var self = this;
+            if (reset === undefined) {
+                reset = true;
+            }
+            self.enable();
+            if (self.showCancel) {
+                addCss(self.$container.find('.fileinput-cancel'), 'hide');
+            }
+            if (self.showRemove) {
+                self.$container.find('.fileinput-remove').removeClass('hide');
+            }
+            if (reset) {
+                self._resetFileStack();
+            }
+            self._raise('fileunlock', [self.filestack, self._getExtraData()]);
+            return self.$element;
+        },
+        cancel: function () {
+            var self = this, xhr = self.ajaxRequests, len = xhr.length, i;
+            if (len > 0) {
+                for (i = 0; i < len; i += 1) {
+                    self.cancelling = true;
+                    xhr[i].abort();
+                }
+            }
+            self._setProgressCancelled();
+            self._getThumbs().each(function () {
+                var $thumb = $(this), ind = $thumb.attr('data-fileindex');
+                $thumb.removeClass('file-uploading');
+                if (self.filestack[ind] !== undefined) {
+                    $thumb.find('.kv-file-upload').removeClass('disabled').removeAttr('disabled');
+                    $thumb.find('.kv-file-remove').removeClass('disabled').removeAttr('disabled');
+                }
+                self.unlock();
+            });
+            return self.$element;
+        },
+        clear: function () {
+            var self = this, cap;
+            self.$btnUpload.removeAttr('disabled');
+            self._getThumbs().find('video,audio,img').each(function () {
+                cleanMemory($(this));
+            });
+            self._resetUpload();
+            self.clearStack();
+            self._clearFileInput();
+            self._resetErrors(true);
+            self._raise('fileclear');
+            if (self._hasInitialPreview()) {
+                self._showFileIcon();
+                self._resetPreview();
+                self._initPreviewDeletes();
+                self.$container.removeClass('file-input-new');
+            } else {
+                self._getThumbs().each(function () {
+                    self._clearObjects($(this));
+                });
+                if (self.isUploadable) {
+                    previewCache.data[self.id] = {};
+                }
+                self.$preview.html('');
+                cap = (!self.overwriteInitial && self.initialCaption.length > 0) ? self.initialCaption : '';
+                self._setCaption(cap);
+                self.$caption.attr('title', '');
+                addCss(self.$container, 'file-input-new');
+                self._validateDefaultPreview();
+            }
+            if (self.$container.find('.file-preview-frame').length === 0) {
+                if (!self._initCaption()) {
+                    self.$captionContainer.find('.kv-caption-icon').hide();
+                }
+            }
+            self._hideFileIcon();
+            self._raise('filecleared');
+            self.$captionContainer.focus();
+            self._setFileDropZoneTitle();
+            return self.$element;
+        },
+        reset: function () {
+            var self = this;
+            self._resetPreview();
+            self.$container.find('.fileinput-filename').text('');
+            self._raise('filereset');
+            addCss(self.$container, 'file-input-new');
+            if (self.$preview.find('.file-preview-frame').length || self.isUploadable && self.dropZoneEnabled) {
+                self.$container.removeClass('file-input-new');
+            }
+            self._setFileDropZoneTitle();
+            self.clearStack();
+            self.formdata = {};
+            return self.$element;
+        },
+        disable: function () {
+            var self = this;
+            self.isDisabled = true;
+            self._raise('filedisabled');
+            self.$element.attr('disabled', 'disabled');
+            self.$container.find(".kv-fileinput-caption").addClass("file-caption-disabled");
+            self.$container.find(".btn-file, .fileinput-remove, .fileinput-upload, .file-preview-frame button").attr(
+                "disabled",
+                true);
+            self._initDragDrop();
+            return self.$element;
+        },
+        enable: function () {
+            var self = this;
+            self.isDisabled = false;
+            self._raise('fileenabled');
+            self.$element.removeAttr('disabled');
+            self.$container.find(".kv-fileinput-caption").removeClass("file-caption-disabled");
+            self.$container.find(
+                ".btn-file, .fileinput-remove, .fileinput-upload, .file-preview-frame button").removeAttr("disabled");
+            self._initDragDrop();
+            return self.$element;
+        },
+        upload: function () {
+            var self = this, totLen = self.getFileStack().length, params = {},
+                i, outData, len, hasExtraData = !$.isEmptyObject(self._getExtraData());
+            if (self.minFileCount > 0 && self._getFileCount(totLen) < self.minFileCount) {
+                self._noFilesError(params);
+                return;
+            }
+            if (!self.isUploadable || self.isDisabled || (totLen === 0 && !hasExtraData)) {
+                return;
+            }
+            self._resetUpload();
+            self.$progress.removeClass('hide');
+            self.uploadCount = 0;
+            self.uploadStatus = {};
+            self.uploadLog = [];
+            self.lock();
+            self._setProgress(2);
+            if (totLen === 0 && hasExtraData) {
+                self._uploadExtraOnly();
+                return;
+            }
+            len = self.filestack.length;
+            self.hasInitData = false;
+            if (self.uploadAsync) {
+                outData = self._getOutData();
+                self._raise('filebatchpreupload', [outData]);
+                self.fileBatchCompleted = false;
+                self.uploadCache = {content: [], config: [], tags: [], append: true};
+                self.uploadAsyncCount = self.getFileStack().length;
+                for (i = 0; i < len; i++) {
+                    self.uploadCache.content[i] = null;
+                    self.uploadCache.config[i] = null;
+                    self.uploadCache.tags[i] = null;
+                }
+                for (i = 0; i < len; i++) {
+                    if (self.filestack[i] !== undefined) {
+                        self._uploadSingle(i, self.filestack, true);
+                    }
+                }
+                return;
+            }
+            self._uploadBatch();
+            return self.$element;
+        },
+        destroy: function () {
+            var self = this, $cont = self.$container;
+            $cont.find('.file-drop-zone').off();
+            self.$element.insertBefore($cont).off(NAMESPACE).removeData();
+            $cont.off().remove();
+            return self.$element;
+        },
+        refresh: function (options) {
+            var self = this, $el = self.$element;
+            options = options ? $.extend(true, {}, self.options, options) : self.options;
+            self.destroy();
+            $el.fileinput(options);
+            if ($el.val()) {
+                $el.trigger('change.fileinput');
+            }
+            return $el;
+        }
+    };
+
+    $.fn.fileinput = function (option) {
+        if (!hasFileAPISupport() && !isIE(9)) {
+            return;
+        }
+        var args = Array.apply(null, arguments), retvals = [];
+        args.shift();
+        this.each(function () {
+            var self = $(this), data = self.data('fileinput'), options = typeof option === 'object' && option,
+                lang = options.language || self.data('language') || 'en', loc = {}, opts;
+
+            if (!data) {
+                if (lang !== 'en' && !isEmpty($.fn.fileinputLocales[lang])) {
+                    loc = $.fn.fileinputLocales[lang];
+                }
+                opts = $.extend(true, {}, $.fn.fileinput.defaults, $.fn.fileinputLocales.en, loc, options, self.data());
+                data = new FileInput(this, opts);
+                self.data('fileinput', data);
+            }
+
+            if (typeof option === 'string') {
+                retvals.push(data[option].apply(data, args));
+            }
+        });
+        switch (retvals.length) {
+            case 0:
+                return this;
+            case 1:
+                return retvals[0];
+            default:
+                return retvals;
+        }
+    };
+
+    $.fn.fileinput.defaults = {
+        language: 'en',
+        showCaption: true,
+        showPreview: true,
+        showRemove: true,
+        showUpload: true,
+        showCancel: true,
+        showClose: true,
+        showUploadedThumbs: true,
+        autoReplace: false,
+        mainClass: '',
+        previewClass: '',
+        captionClass: '',
+        mainTemplate: null,
+        initialCaption: '',
+        initialPreview: [],
+        initialPreviewDelimiter: '*$$*',
+        initialPreviewConfig: [],
+        initialPreviewThumbTags: [],
+        previewThumbTags: {},
+        initialPreviewShowDelete: true,
+        removeFromPreviewOnError: false,
+        deleteUrl: '',
+        deleteExtraData: {},
+        overwriteInitial: true,
+        layoutTemplates: defaultLayoutTemplates,
+        previewTemplates: defaultPreviewTemplates,
+        allowedPreviewTypes: defaultPreviewTypes,
+        allowedPreviewMimeTypes: null,
+        allowedFileTypes: null,
+        allowedFileExtensions: null,
+        defaultPreviewContent: null,
+        customLayoutTags: {},
+        customPreviewTags: {},
+        previewSettings: defaultPreviewSettings,
+        fileTypeSettings: defaultFileTypeSettings,
+        previewFileIcon: '<i class="glyphicon glyphicon-file"></i>',
+        previewFileIconClass: 'file-icon-4x',
+        previewFileIconSettings: {},
+        previewFileExtSettings: {},
+        buttonLabelClass: 'hidden-xs',
+        browseIcon: '<i class="glyphicon glyphicon-folder-open"></i>&nbsp;',
+        browseClass: 'btn btn-primary',
+        removeIcon: '<i class="glyphicon glyphicon-trash"></i>',
+        removeClass: 'btn btn-default',
+        cancelIcon: '<i class="glyphicon glyphicon-ban-circle"></i>',
+        cancelClass: 'btn btn-default',
+        uploadIcon: '<i class="glyphicon glyphicon-upload"></i>',
+        uploadClass: 'btn btn-default',
+        uploadUrl: null,
+        uploadAsync: true,
+        uploadExtraData: {},
+        minImageWidth: null,
+        minImageHeight: null,
+        maxImageWidth: null,
+        maxImageHeight: null,
+        resizeImage: false,
+        resizePreference: 'width',
+        resizeQuality: 0.92,
+        resizeDefaultImageType: 'image/jpeg',
+        maxFileSize: 0,
+        minFileCount: 0,
+        maxFileCount: 0,
+        validateInitialCount: false,
+        msgValidationErrorClass: 'text-danger',
+        msgValidationErrorIcon: '<i class="glyphicon glyphicon-exclamation-sign"></i> ',
+        msgErrorClass: 'file-error-message',
+        progressThumbClass: "progress-bar progress-bar-success progress-bar-striped active",
+        progressClass: "progress-bar progress-bar-success progress-bar-striped active",
+        progressCompleteClass: "progress-bar progress-bar-success",
+        progressErrorClass: "progress-bar progress-bar-danger",
+        previewFileType: 'image',
+        zoomIndicator: '<i class="glyphicon glyphicon-zoom-in"></i>',
+        elCaptionContainer: null,
+        elCaptionText: null,
+        elPreviewContainer: null,
+        elPreviewImage: null,
+        elPreviewStatus: null,
+        elErrorContainer: null,
+        errorCloseButton: '<span class="close kv-error-close">&times;</span>',
+        slugCallback: null,
+        dropZoneEnabled: true,
+        dropZoneTitleClass: 'file-drop-zone-title',
+        fileActionSettings: {},
+        otherActionButtons: '',
+        textEncoding: 'UTF-8',
+        ajaxSettings: {},
+        ajaxDeleteSettings: {},
+        showAjaxErrorDetails: true
+    };
+
+    $.fn.fileinputLocales.en = {
+        fileSingle: 'file',
+        filePlural: 'files',
+        browseLabel: 'Browse &hellip;',
+        removeLabel: 'Remove',
+        removeTitle: 'Clear selected files',
+        cancelLabel: 'Cancel',
+        cancelTitle: 'Abort ongoing upload',
+        uploadLabel: 'Upload',
+        uploadTitle: 'Upload selected files',
+        msgNo: 'No',
+        msgCancelled: 'Cancelled',
+        msgZoomTitle: 'View details',
+        msgZoomModalHeading: 'Detailed Preview',
+        msgSizeTooLarge: 'File "{name}" (<b>{size} KB</b>) exceeds maximum allowed upload size of <b>{maxSize} KB</b>.',
+        msgFilesTooLess: 'You must select at least <b>{n}</b> {files} to upload.',
+        msgFilesTooMany: 'Number of files selected for upload <b>({n})</b> exceeds maximum allowed limit of <b>{m}</b>.',
+        msgFileNotFound: 'File "{name}" not found!',
+        msgFileSecured: 'Security restrictions prevent reading the file "{name}".',
+        msgFileNotReadable: 'File "{name}" is not readable.',
+        msgFilePreviewAborted: 'File preview aborted for "{name}".',
+        msgFilePreviewError: 'An error occurred while reading the file "{name}".',
+        msgInvalidFileType: 'Invalid type for file "{name}". Only "{types}" files are supported.',
+        msgInvalidFileExtension: 'Invalid extension for file "{name}". Only "{extensions}" files are supported.',
+        msgUploadAborted: 'The file upload was aborted',
+        msgValidationError: 'Validation Error',
+        msgLoading: 'Loading file {index} of {files} &hellip;',
+        msgProgress: 'Loading file {index} of {files} - {name} - {percent}% completed.',
+        msgSelected: '{n} {files} selected',
+        msgFoldersNotAllowed: 'Drag & drop files only! {n} folder(s) dropped were skipped.',
+        msgImageWidthSmall: 'Width of image file "{name}" must be at least {size} px.',
+        msgImageHeightSmall: 'Height of image file "{name}" must be at least {size} px.',
+        msgImageWidthLarge: 'Width of image file "{name}" cannot exceed {size} px.',
+        msgImageHeightLarge: 'Height of image file "{name}" cannot exceed {size} px.',
+        msgImageResizeError: 'Could not get the image dimensions to resize.',
+        msgImageResizeException: 'Error while resizing the image.<pre>{errors}</pre>',
+        dropZoneTitle: 'Drag & drop files here &hellip;'
+    };
+
+    $.fn.fileinput.Constructor = FileInput;
+
+    /**
+     * Convert automatically file inputs with class 'file' into a bootstrap fileinput control.
+     */
+    $(document).ready(function () {
+        var $input = $('input.file[type=file]');
+        if ($input.length) {
+            $input.fileinput();
+        }
+    });
+}));
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/js/schema-form-helper.js b/hedweb/hed/web/templates/js/schema-form.js
similarity index 74%
rename from hedweb/hed/web/templates/js/schema-form-helper.js
rename to hedweb/hed/web/templates/js/schema-form.js
index 7bfe2f3b2..b3dcafcaa 100644
--- a/hedweb/hed/web/templates/js/schema-form-helper.js
+++ b/hedweb/hed/web/templates/js/schema-form.js
@@ -1,23 +1,23 @@
 const SCHEMA_XML_EXTENSION = 'xml';
 const SCHEMA_MEDIAWIKI_EXTENSION = 'mediawiki';
-const DEFAULT_XML_URL = "https://raw.githubusercontent.com/hed-standard/hed-specification/HED-restructure/hedxml/HED7.1.1.xml";
+const DEFAULT_XML_URL = "https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED7.1.1.xml";
 
-$(document).ready(function () {
+$(function () {
     prepareSchemaForm();
 });
 
 /**
  * Checks if the HED file uploaded has a valid extension.
  */
-$('#schema-file').change(function () {
-    updateSchemaFileLabel($('#schema-file').val());
+$('#schema-file').on('change', function () {
+    updateFileLabel($('#schema-file').val(), '#schema-file-display-name');
     $('#schema-file-option').prop('checked', true);
     clearSchemaSubmitFlash()
     updateFormGui();
 });
 
-$('#schema-url').change(function () {
-    updateSchemaUrlLabel($('#schema-url').val());
+$('#schema-url').on('change', function () {
+    updateFileLabel($('#schema-url').val(), '#schema-url-display-name');
     $('#schema-url-option').prop('checked', true);
     clearSchemaSubmitFlash()
     updateFormGui();
@@ -38,20 +38,20 @@ $('#schema-conversion-submit').click(function () {
 /**
  * Submits the form for tag comparison if we have a valid file.
  */
-$('#schema-duplicate-tag-submit').click(function () {
+$('#schema-check-submit').on('click', function () {
    if (getSchemaFilename() === "") {
         flashMessageOnScreen('No valid source schema file.  See above.', 'error',
             'schema-submit-flash')
     } else {
-        submitSchemaDuplicateTagForm();
+        submitSchemaComplianceCheckForm();
     }
 });
 
-$('#schema-file-option').change(function () {
+$('#schema-file-option').on('change', function () {
     updateFormGui();
 });
 
-$('#schema-url-option').change(function () {
+$('#schema-url-option').on('change',function () {
     updateFormGui();
 });
 
@@ -73,37 +73,6 @@ function convertToOutputName(original_filename) {
     return basename + "." + new_extension
 }
 
-function convertToResultsName(original_filename) {
-    let file_parts = splitExt(original_filename);
-    let basename = file_parts[0]
-    return "duplicate_tags_" + basename + ".txt"
-}
-
-/**
- * Compares the file extension of the file at the specified path to an Array of accepted file extensions.
- * @param {String} filePath - A path to a file.
- * @param {Array} acceptedFileExtensions - An array of accepted file extensions.
- * @returns {boolean} - True if the file has an accepted file extension.
- */
-function fileHasValidExtension(filePath, acceptedFileExtensions) {
-    let fileExtension = filePath.split('.').pop();
-    if ($.inArray(fileExtension.toLowerCase(), acceptedFileExtensions) != -1) {
-        return true;
-    }
-    return false;
-}
-
-/**
- * Flash a message on the screen.
- * @param {String} message - The message that will be flashed on the screen.
- * @param {String} category - The category of the message. The categories are 'error', 'success', and 'other'.
- */
-function flashMessageOnScreen(message, category, flashMessageElementId) {
-    let flashMessage = document.getElementById(flashMessageElementId);
-    flashMessage.innerHTML = message;
-    setFlashMessageCategory(flashMessage, category);
-}
-
 function getSchemaFilename() {
     let checkRadio = document.querySelector('input[name="schema-upload-options"]:checked');
     if (checkRadio == null) {
@@ -166,23 +135,6 @@ function resetForm() {
     $('#schema-file-option').prop('checked', false);
 }
 
-/**
- * Flash a message on the screen.
- * @param {Object} flashMessage - The li element containing the flash message.
- * @param {String} category - The category of the message. The categories are 'error', 'success', and 'other'.
- */
-function setFlashMessageCategory(flashMessage, category) {
-    if ("error" === category) {
-        flashMessage.style.backgroundColor = 'lightcoral';
-    } else if ("success" === category) {
-        flashMessage.style.backgroundColor = 'palegreen';
-    } else if ("warning" === category) {
-        flashMessage.style.backgroundColor = 'darkorange';
-    } else {
-        flashMessage.style.backgroundColor = '#f0f0f5';
-    }
-}
-
 function splitExt(filename) {
     const index = filename.lastIndexOf('.');
     return (-1 !== index) ? [filename.substring(0, index), filename.substring(index + 1)] : [filename, ''];
@@ -224,16 +176,16 @@ function submitSchemaConversionForm() {
     ;
 }
 
-function submitSchemaDuplicateTagForm() {
+function submitSchemaComplianceCheckForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
-    let download_display_name = convertToResultsName(getSchemaFilename())
+    let download_display_name = convertToResultsName(getSchemaFilename(), 'HED3G_format_issues')
     resetFlashMessages(false);
-    flashMessageOnScreen('Searching for duplicates in schema...', 'success',
+    flashMessageOnScreen('Checking schema for HED-3G compliance...', 'success',
         'schema-submit-flash')
     $.ajax({
             type: 'POST',
-            url: "{{url_for('route_blueprint.get_duplicate_tag_results')}}",
+            url: "{{url_for('route_blueprint.get_schema_compliance_check_results')}}",
             data: formData,
             contentType: false,
             processData: false,
@@ -243,7 +195,7 @@ function submitSchemaDuplicateTagForm() {
                       flashMessageOnScreen('', 'success', 'schema-submit-flash');
                       triggerDownloadBlob(downloaded_file, download_display_name);
                   } else {
-                      flashMessageOnScreen('No duplicate tags found.', 'success',
+                      flashMessageOnScreen('No HED-3G compliance issues found.', 'success',
                           'schema-submit-flash');
                   }
             },
@@ -253,7 +205,7 @@ function submitSchemaDuplicateTagForm() {
                     flashMessageOnScreen(download_response.responseText, 'error',
                         'schema-submit-flash');
                 } else {
-                    flashMessageOnScreen('XML could not be processed',
+                    flashMessageOnScreen('Schema could not be processed',
                         'error','schema-submit-flash');
                 }
             }
@@ -262,19 +214,6 @@ function submitSchemaDuplicateTagForm() {
     ;
 }
 
-
-/**
- * Trigger the "save as" dialog for a text blob to save as a file with display name.
- */
-function triggerDownloadBlob(download_text_blob, display_name) {
-    const url = window.URL.createObjectURL(new Blob([download_text_blob]));
-    const link = document.createElement('a');
-    link.href = url;
-    link.setAttribute('download', display_name);
-    document.body.appendChild(link);
-    link.click();
-}
-
 function updateFormGui() {
      let filename = getSchemaFilename();
      let isXMLFilename = fileHasValidExtension(filename, [SCHEMA_XML_EXTENSION]);
diff --git a/hedweb/hed/web/templates/js/spreadsheet-form-helper.js b/hedweb/hed/web/templates/js/spreadsheet-form.js
similarity index 62%
rename from hedweb/hed/web/templates/js/spreadsheet-form-helper.js
rename to hedweb/hed/web/templates/js/spreadsheet-form.js
index 7dba79e60..22bfdda53 100644
--- a/hedweb/hed/web/templates/js/spreadsheet-form-helper.js
+++ b/hedweb/hed/web/templates/js/spreadsheet-form.js
@@ -1,45 +1,15 @@
 const EXCEL_FILE_EXTENSIONS = ['xlsx', 'xls'];
-const XML_FILE_EXTENSIONS = ['xml'];
 const TEXT_FILE_EXTENSIONS = ['tsv', 'txt'];
-const HED_OTHER_VERSION_OPTION = 'Other';
 
-$(document).ready(function () {
+$(function () {
     prepareSpreadsheetForm();
 });
 
-/**
- * Event handler function when the HED version drop-down menu changes. If Other is selected the file browser
- * underneath it will appear. If another option is selected then it will disappear.
- */
-$('#hed-version').change(function () {
-    if ($(this).val() === HED_OTHER_VERSION_OPTION) {
-        $('#hed-other-version').show();
-    } else {
-        hideOtherHEDVersionFileUpload()
-    }
-    flashMessageOnScreen('', 'success', 'hed-flash');
-});
-
-/**
- * Checks if the HED file uploaded has a valid extension.
- */
-$('#hed-xml-file').change(function () {
-    let hedSchema = $('#hed-xml-file');
-    let hedPath = hedSchema.val();
-    let hedFile = hedSchema[0].files[0];
-    if (fileHasValidExtension(hedPath, XML_FILE_EXTENSIONS)) {
-        getVersionFromHEDFile(hedFile);
-        updateHEDFileLabel(hedPath);
-    } else {
-        flashMessageOnScreen('Please upload a valid HED file (.xml)', 'error',
-            'hed-flash')
-    }
-});
 
 /**
  * Spreadsheet event handler function. Checks if the file uploaded has a valid spreadsheet extension.
  */
-$('#spreadsheet-file').change(function () {
+$('#spreadsheet-file').on('change', function () {
     let spreadsheet = $('#spreadsheet-file');
     let spreadsheetPath = spreadsheet.val();
     let spreadsheetFile = spreadsheet[0].files[0];
@@ -48,13 +18,13 @@ $('#spreadsheet-file').change(function () {
         resetForm();
     }
     else if (fileHasValidExtension(spreadsheetPath, EXCEL_FILE_EXTENSIONS)) {
-        updateSpreadsheetFileLabel(spreadsheetPath);
+        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
         getWorksheetsInfo(spreadsheetFile);
     }
     else if (fileHasValidExtension(spreadsheetPath, TEXT_FILE_EXTENSIONS)) {
-        updateSpreadsheetFileLabel(spreadsheetPath);
+        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
         clearWorksheetSelectbox();
-        getSpreadsheetColumnsInfo(spreadsheetFile, '');
+        getColumnsInfo(spreadsheetFile, '');
     } else {
         resetForm();
         flashMessageOnScreen('Please upload a excel or text spreadsheet (.xlsx, .xls, .tsv, .txt)',
@@ -65,7 +35,7 @@ $('#spreadsheet-file').change(function () {
 /**
  * Submits the form if the tag columns textbox is valid.
  */
-$('#spreadsheet-validation-submit').click(function () {
+$('#spreadsheet-validation-submit').on('click', function () {
     if (spreadsheetIsSpecified() && tagColumnsTextboxIsValid() && hedSpecifiedWhenOtherIsSelected()) {
         submitForm();
     }
@@ -75,11 +45,11 @@ $('#spreadsheet-validation-submit').click(function () {
  * Gets the information associated with the Excel worksheet that was newly selected. This information contains
  * the names of the columns and column indices that contain HED tags.
  */
-$('#worksheet-name').change(function () {
+$('#worksheet-name').on('change', function () {
     let spreadsheetFile = $('#spreadsheet-file')[0].files[0];
     let worksheetName = $('#worksheet-name option:selected').text();
     resetFlashMessages();
-    getSpreadsheetColumnsInfo(spreadsheetFile, worksheetName);
+    getColumnsInfo(spreadsheetFile, worksheetName);
 });
 
 /**
@@ -118,7 +88,7 @@ function dictionaryIsEmpty(dictionary) {
  * @param {Array} requiredTagColumnIndices - An array of indices of columns containing required tags
  * contain tags.
  */
-function flashSpreadsheetTagColumnCountMessage(tagColumnIndices, requiredTagColumnIndices) {
+function flashTagColumnCountMessage(tagColumnIndices, requiredTagColumnIndices) {
     let numberOfTagColumns = (tagColumnIndices.length + Object.keys(requiredTagColumnIndices).length).toString();
     if (numberOfTagColumns === '0') {
         flashMessageOnScreen('Warning: No tag column(s) found... Using the 2nd column', 'warning',
@@ -139,34 +109,12 @@ function flashWorksheetNumberMessage(worksheetNames) {
         'success', 'worksheet-flash');
 }
 
-/**
- * Gets the HED versions that are in the HED version drop-down menu.
- */
-function getHEDVersions() {
-    $.ajax({
-            type: 'GET',
-            url: "{{url_for('route_blueprint.get_major_hed_versions')}}",
-            contentType: false,
-            processData: false,
-            dataType: 'json',
-            success: function (hedInfo) {
-                populateHEDVersionsDropdown(hedInfo['hed-major-versions']);
-            },
-            error: function (jqXHR) {
-                console.log(jqXHR.responseJSON.message);
-                flashMessageOnScreen('Major HED versions could not be retrieved. Please provide your own.',
-                    'error', 'spreadsheet-flash');
-            }
-        }
-    );
-}
-
 /**
  * Gets the spreadsheet columns.
  * @param {Object} spreadsheetFile - A spreadsheet file.
  * @param {String} worksheetName - An Excel worksheet name.
  */
-function getSpreadsheetColumnsInfo(spreadsheetFile, worksheetName) {
+function getColumnsInfo(spreadsheetFile, worksheetName) {
     let formData = new FormData();
     formData.append('spreadsheet-file', spreadsheetFile);
     if (typeof worksheetName !== 'undefined') {
@@ -179,10 +127,10 @@ function getSpreadsheetColumnsInfo(spreadsheetFile, worksheetName) {
         contentType: false,
         processData: false,
         dataType: 'json',
-        success: function (spreadsheetColumnsInfo) {
-            setComponentsRelatedToSpreadsheetColumns(spreadsheetColumnsInfo);
-            flashSpreadsheetTagColumnCountMessage(spreadsheetColumnsInfo['tag-column-indices'],
-                spreadsheetColumnsInfo['required-tag-column-indices']);
+        success: function (columnsInfo) {
+            setComponentsRelatedToColumns(columnsInfo);
+            flashTagColumnCountMessage(columnsInfo['tag-column-indices'],
+                columnsInfo['required-tag-column-indices']);
         },
         error: function (jqXHR) {
             console.log(jqXHR.responseJSON.message);
@@ -192,33 +140,6 @@ function getSpreadsheetColumnsInfo(spreadsheetFile, worksheetName) {
     });
 }
 
-/**
- * Gets the version from the HED file that the user uploaded.
- * @param {Object} hedXMLFile - A HED XML file.
- */
-function getVersionFromHEDFile(hedXMLFile) {
-    let formData = new FormData();
-    formData.append('hed-xml-file', hedXMLFile);
-    $.ajax({
-        type: 'POST',
-        url: "{{ url_for('route_blueprint.get_hed_version_in_file')}}",
-        data: formData,
-        contentType: false,
-        processData: false,
-        dataType: 'json',
-        success: function (hedInfo) {
-            resetFlashMessages();
-            flashMessageOnScreen('Using HED version ' + hedInfo['hed-version'],
-                'success', 'hed-flash');
-        },
-        error: function (jqXHR) {
-            console.log(jqXHR.responseJSON.message);
-            flashMessageOnScreen('Could not get version number from HED XML file.',
-                'error', 'hed-flash');
-        }
-    });
-}
-
 /**
  * Gets information associated with the Excel workbook worksheets. This information contains the names of the
  * worksheets, the names of the columns in the first worksheet, and column indices that contain HED tags in the
@@ -236,9 +157,10 @@ function getWorksheetsInfo(workbookFile) {
         processData: false,
         dataType: 'json',
         success: function (worksheetsInfo) {
-            setComponentsRelatedToWorksheets(worksheetsInfo);
+            populateWorksheetSelectbox(worksheetsInfo['worksheet-names']);
+            setComponentsRelatedToColumns(worksheetsInfo);
             flashWorksheetNumberMessage(worksheetsInfo['worksheet-names']);
-            flashSpreadsheetTagColumnCountMessage(worksheetsInfo['tag-column-indices'],
+            flashTagColumnCountMessage(worksheetsInfo['tag-column-indices'],
                 worksheetsInfo['required-tag-column-indices']);
         },
         error: function (jqXHR) {
@@ -251,48 +173,29 @@ function getWorksheetsInfo(workbookFile) {
 }
 
 /**
- * Checks to see if a HED XML file is specified when the HED drop-down is set to "Other".
+ * Hides  columns section in the form.
  */
-function hedSpecifiedWhenOtherIsSelected() {
-    let hedFile = $('#hed-xml-file');
-    let hedFileIsEmpty = hedFile[0].files.length === 0;
-    if ($('#hed-version').val() === HED_OTHER_VERSION_OPTION && hedFileIsEmpty) {
-        flashMessageOnScreen('HED version is not specified.', 'error', 'hed-flash');
-        return false;
-    }
-    return true;
-}
-
-/**
- * Hides spreadsheet columns section in the form.
- */
-function hideSpreadsheetColumnNamesTable() {
+function hideColumnNamesTable() {
     $('#column-names').hide();
 }
 
-/**
- * Hides the HED XML file upload.
- */
-function hideOtherHEDVersionFileUpload() {
-    $('#hed-display-name').text('');
-    $('#hed-other-version').hide();
-}
-
 
 /**
- * Populates the HED version drop-down menu.
- * @param {Array} hedVersions - An array containing the HED versions.
+ * Populates a table containing the worksheet columns.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
  */
-function populateHEDVersionsDropdown(hedVersions) {
-    let hedVersionDropdown = $('#hed-version');
-    hedVersionDropdown.append('<option value=' + hedVersions[0] + '>' + hedVersions[0] + ' (Latest)</option>');
-    for (let i = 1; i < hedVersions.length; i++) {
-        hedVersionDropdown.append('<option value=' + hedVersions[i] + '>' + hedVersions[i] + '</option>');
+function populateColumnNamesTable(columnNames) {
+    let columnNamesTable = $('#columns-names-table');
+    let columnNamesRow = $('<tr/>');
+    let numberOfColumnNames = columnNames.length;
+    columnNamesTable.empty();
+    for (let i = 0; i < numberOfColumnNames; i++) {
+        columnNamesRow.append('<td>' + columnNames[i] + '</td>');
     }
-    hedVersionDropdown.append('<option value=' + HED_OTHER_VERSION_OPTION + '>' + HED_OTHER_VERSION_OPTION +
-        '</option>');
+    columnNamesTable.append(columnNamesRow);
 }
 
+
 /**
  * Populate the required tag column textboxes from the tag column indices found in the spreadsheet columns.
  * @param {object} requiredTagColumnIndices - A dictionary containing the required tag column indices found
@@ -304,21 +207,6 @@ function populateRequiredTagColumnTextboxes(requiredTagColumnIndices) {
     }
 }
 
-/**
- * Populates a table containing the spreadsheet columns.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- */
-function populateSpreadsheetColumnNamesTable(columnNames) {
-    let columnNamesTable = $('#columns-names-table');
-    let columnNamesRow = $('<tr/>');
-    let numberOfColumnNames = columnNames.length;
-    columnNamesTable.empty();
-    for (let i = 0; i < numberOfColumnNames; i++) {
-        columnNamesRow.append('<td>' + columnNames[i] + '</td>');
-    }
-    columnNamesTable.append(columnNamesRow);
-}
-
 /**
  * Populate the tag column textbox from the tag column indices found in the spreadsheet columns.
  * @param {Array} tagColumnIndices - An integer array of tag column indices found in the spreadsheet
@@ -348,7 +236,7 @@ function populateWorksheetSelectbox(worksheetNames) {
 function prepareSpreadsheetForm() {
     resetForm();
     getHEDVersions()
-    hideSpreadsheetColumnNamesTable();
+    hideColumnNamesTable();
     hideOtherHEDVersionFileUpload();
 }
 
@@ -370,70 +258,59 @@ function resetForm() {
     $('#spreadsheet-form')[0].reset();
     clearSpreadsheetFileLabel();
     clearWorksheetSelectbox();
-    hideSpreadsheetColumnNamesTable();
+    hideColumnNamesTable();
     hideOtherHEDVersionFileUpload()
 }
 
 /**
- * Sets the components related to the spreadsheet columns when they are not empty.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
+ * Sets the components related to Excel worksheet columns when they are all empty.
  */
-function setComponentsRelatedToNonEmptySpreadsheetColumnNames(columnNames) {
-    populateSpreadsheetColumnNamesTable(columnNames);
-    setHasColumnNamesCheckboxToTrue();
-    showSpreadsheetColumnNamesTable();
+function setComponentsRelatedToEmptyColumnNames() {
+    clearTagColumnTextboxes();
+    setHasColumnNamesCheckboxToFalse();
+    hideColumnNamesTable();
 }
 
 /**
  * Sets the components related to the spreadsheet tag column indices when they are empty.
  */
-function setComponentsRelatedToEmptySpreadsheetTagColumnIndices() {
+function setComponentsRelatedToEmptyTagColumnIndices() {
     $('#tag-columns').val('2');
 }
 
 /**
- * Sets the components related to Excel worksheet columns when they are all empty.
+ * Sets the components related to the spreadsheet columns when they are not empty.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
  */
-function setComponentsRelatedToEmptySpreadsheetColumnNames() {
-    clearTagColumnTextboxes();
-    setHasColumnNamesCheckboxToFalse();
-    hideSpreadsheetColumnNamesTable();
+function setComponentsRelatedToNonEmptyColumnNames(columnNames) {
+    populateColumnNamesTable(columnNames);
+    setHasColumnNamesCheckboxToTrue();
+    $('#column-names').show();
 }
 
 /**
  * Sets the components related to the Excel worksheet columns.
- * @param {JSON} spreadsheetColumnsInfo - A JSON object containing information related to the spreadsheet
+ * @param {JSON} columnsInfo - A JSON object containing information related to the spreadsheet
  * columns.
  * This information contains the names of the columns and column indices that contain HED tags.
  */
-function setComponentsRelatedToSpreadsheetColumns(spreadsheetColumnsInfo) {
+function setComponentsRelatedToColumns(columnsInfo) {
     clearTagColumnTextboxes();
-    if (spreadsheetColumnNamesAreEmpty(spreadsheetColumnsInfo['column-names'])) {
-        setComponentsRelatedToEmptySpreadsheetColumnNames();
+    if (columnNamesAreEmpty(columnsInfo['column-names'])) {
+        setComponentsRelatedToEmptyColumnNames();
     } else {
-        setComponentsRelatedToNonEmptySpreadsheetColumnNames(spreadsheetColumnsInfo['column-names']);
+        setComponentsRelatedToNonEmptyColumnNames(columnsInfo['column-names']);
     }
-    if (spreadsheetTagColumnsIndicesAreEmpty(spreadsheetColumnsInfo['tag-column-indices'])) {
-        setComponentsRelatedToEmptySpreadsheetTagColumnIndices();
+    if (tagColumnsIndicesAreEmpty(columnsInfo['tag-column-indices'])) {
+        setComponentsRelatedToEmptyTagColumnIndices();
     } else {
-        populateTagColumnsTextbox(spreadsheetColumnsInfo['tag-column-indices']);
+        populateTagColumnsTextbox(columnsInfo['tag-column-indices']);
     }
-    if (!dictionaryIsEmpty(spreadsheetColumnsInfo['required-tag-column-indices'])) {
-        populateRequiredTagColumnTextboxes(spreadsheetColumnsInfo['required-tag-column-indices']);
+    if (!dictionaryIsEmpty(columnsInfo['required-tag-column-indices'])) {
+        populateRequiredTagColumnTextboxes(columnsInfo['required-tag-column-indices']);
     }
 }
 
-/**
- * Sets components related to an Excel worksheet.
- * @param {JSON} worksheetsInfo - A JSON object containing information related to the Excel worksheet. This
- * information contains the names of the worksheets in a workbook, the names of the columns in the first worksheet,
- * and column indices that contain HED tags in the first worksheet.
- */
-function setComponentsRelatedToWorksheets(worksheetsInfo) {
-    populateWorksheetSelectbox(worksheetsInfo['worksheet-names']);
-    setComponentsRelatedToSpreadsheetColumns(worksheetsInfo);
-}
-
 /**
  * Sets the spreadsheet has column names checkbox to false.
  */
@@ -448,19 +325,12 @@ function setHasColumnNamesCheckboxToTrue() {
     $('#has-column-names').prop('checked', true);
 }
 
-/**
- * Shows spreadsheet columns section in the form.
- */
-function showSpreadsheetColumnNamesTable() {
-    $('#column-names').show();
-}
-
 /**
  * Checks to see if the spreadsheet columns are empty.
  * @param {Array} columnNames - An array containing the spreadsheet column names.
  * @returns {boolean} - True if the spreadsheet columns are all empty.
  */
-function spreadsheetColumnNamesAreEmpty(columnNames) {
+function columnNamesAreEmpty(columnNames) {
     let numberOfColumnNames = columnNames.length;
     for (let i = 0; i < numberOfColumnNames; i++) {
         if (!isEmptyStr(columnNames[i].trim())) {
@@ -484,12 +354,12 @@ function spreadsheetIsSpecified() {
 }
 
 /**
- * Checks to see if the spreadsheet tag column indices are empty.
+ * Checks to see if the worksheet tag column indices are empty.
  * @param {Array} tagColumnsIndices - An array containing the tag column indices based on the
  *                columns found in the spreadsheet.
  * @returns {boolean} - True if the spreadsheet tag column indices array is empty.
  */
-function spreadsheetTagColumnsIndicesAreEmpty(tagColumnsIndices) {
+function tagColumnsIndicesAreEmpty(tagColumnsIndices) {
     if (tagColumnsIndices.length > 0) {
         return false;
     }
@@ -563,22 +433,3 @@ function tagColumnsTextboxIsValid() {
     }
     return valid;
 }
-
-
-/**
- * Updates the HED file label.
- * @param {String} hedPath - The path to the HED XML file.
- */
-function updateHEDFileLabel(hedPath) {
-    let hedFilename = hedPath.split('\\').pop();
-    $('#hed-display-name').text(hedFilename);
-}
-
-/**
- * Updates the spreadsheet file label.
- * @param {String} spreadsheetPath - The path to the spreadsheet.
- */
-function updateSpreadsheetFileLabel(spreadsheetPath) {
-    let spreadsheetFilename = spreadsheetPath.split('\\').pop();
-    $('#spreadsheet-display-name').text(spreadsheetFilename);
-}
diff --git a/hedweb/hed/web/templates/json-input.html b/hedweb/hed/web/templates/json-input.html
new file mode 100644
index 000000000..1478d2d76
--- /dev/null
+++ b/hedweb/hed/web/templates/json-input.html
@@ -0,0 +1,11 @@
+{% macro create_json_input() %}
+        <div class="form-group">
+            <label for="json-file">Select JSON-formatted dictionary or side-car:</label><br>
+            <label class="btn-group file-browse-group json-btn-group">
+                <button for="json-file">Browse ...</button>
+                <label id="json-display-name"></label>
+                <input type="file" name="json-file" id="json-file" multiple/>
+            </label>
+            <p class="flash" id="json-flash"></p>
+        </div>
+{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/multi-file-input.html b/hedweb/hed/web/templates/multi-file-input.html
new file mode 100644
index 000000000..554f5204f
--- /dev/null
+++ b/hedweb/hed/web/templates/multi-file-input.html
@@ -0,0 +1,11 @@
+  <div class="form-group">
+      <label class="col-sm-3 control-label">
+          Attachment(s)
+          (Attach multiple files.)
+      </label>
+      <div class="col-sm-9">
+                            <span class="btn btn-default btn-file">
+                                <input id="input-2" name="input2[]" type="file" class="file" multiple data-show-upload="true" data-show-caption="true">
+                            </span>
+      </div>
+   </div>
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/schema-form-helper.html b/hedweb/hed/web/templates/schema-form-helper.html
deleted file mode 100644
index 1d270f4d1..000000000
--- a/hedweb/hed/web/templates/schema-form-helper.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% macro create_schema_form() %}
-    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/schema-form-helper.css') }}">
-    <form id="schema-form" method="POST" enctype="multipart/form-data">
-        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
-        <h3>Source file (.xml/.mediawiki): </h3>
-        <div class="form-group" id="schema-url-group">
-            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-url-option" value="schema-url-option">
-            <label class="secondary-label" for="schema-url-version">Enter HED schema URL:</label>
-            <label class="btn-group url-browse-group">
-                <label id="schema-url-display-name"></label>
-                <input type="url" name="schema-url" id="schema-url">
-            </label>
-            <p class="flash" id="schema-url-flash"></p>
-        </div>
-
-        <div class="form-group" id="schema-file-group">
-            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-file-option" value="schema-file-option">
-            <label class="secondary-label" for="schema-file">Upload HED schema file:</label>
-            <label class="btn-group file-browse-group">
-                <button for="schema-file">Browse ...</button>
-                <label id="schema-file-display-name"></label>
-                <input type="file" name="schema-file" id="schema-file"/>
-            </label>
-            <p class="flash" id="schema-file-flash"></p>
-        </div>
-
-        <h3>Actions (pick one): </h3>
-        <div class="form-group">
-            <button id="schema-conversion-submit" type="button">Convert schema format</button>
-        </div>
-
-        <div class="form-group">
-            <button id="schema-duplicate-tag-submit" type="button">Check for duplicate tags</button>
-            <p class="flash" id="schema-submit-flash"></p>
-        </div>
-
-    </form>
-    <script>
-        {%  include 'js/schema-form-helper.js' %}
-    </script>
-
-{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/schema.html b/hedweb/hed/web/templates/schema.html
index aff9badd8..42d83d5d7 100644
--- a/hedweb/hed/web/templates/schema.html
+++ b/hedweb/hed/web/templates/schema.html
@@ -1,6 +1,48 @@
 {% extends "layout.html" %}
-{% from "schema-form-helper.html" import create_schema_form %}
 {% block content %}
-    <h2>Process HED Schema</h2>
-    {{ create_schema_form() }}
+<h2>Process HED Schema</h2>
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/schema-form.css') }}">
+    <form id="schema-form" method="POST" enctype="multipart/form-data">
+        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+        <h3>Source file (.xml/.mediawiki): </h3>
+        <div class="form-group" id="schema-url-group">
+            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-url-option" value="schema-url-option">
+            <label class="secondary-label" for="schema-url-version">Enter HED schema URL:</label>
+            <label class="btn-group url-browse-group">
+                <label id="schema-url-display-name"></label>
+                <input type="url" name="schema-url" id="schema-url">
+            </label>
+            <p class="flash" id="schema-url-flash"></p>
+        </div>
+
+        <div class="form-group" id="schema-file-group">
+            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-file-option" value="schema-file-option">
+            <label class="secondary-label" for="schema-file">Upload HED schema file:</label>
+            <label class="btn-group file-browse-group">
+                <button for="schema-file">Browse ...</button>
+                <label id="schema-file-display-name"></label>
+                <input type="file" name="schema-file" id="schema-file"/>
+            </label>
+            <p class="flash" id="schema-file-flash"></p>
+        </div>
+
+        <h3>Actions (pick one): </h3>
+        <div class="form-group">
+            <button id="schema-conversion-submit" type="button">Convert schema format</button>
+        </div>
+
+        <div class="form-group">
+            <button id="schema-check-submit" type="button">Check HED-3G format</button>
+        </div>
+
+        <div class="form-group">
+            <p class="flash" id="schema-submit-flash"></p>
+        </div>
+
+    </form>
+    <script type="module">
+        {%  include 'js/form-helpers.js' %}
+        {%  include 'js/schema-form.js' %}
+    </script>
+
 {% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/spreadsheet.html b/hedweb/hed/web/templates/spreadsheet.html
index 8cc8b1b0f..ef2311cf7 100644
--- a/hedweb/hed/web/templates/spreadsheet.html
+++ b/hedweb/hed/web/templates/spreadsheet.html
@@ -1,7 +1,75 @@
 {% extends "layout.html" %}
-{% from "spreadsheet-form-helper.html" import create_spreadsheet_validation_form %}
+{% from "hed-pulldown.html" import create_hed_pulldown %}
 {% block content %}
     <h2>Validate spreadsheet containing HED tags</h2>
-    {{ create_spreadsheet_validation_form() }}
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/spreadsheet-form.css') }}">
+    <form id="spreadsheet-form" method="POST" enctype="multipart/form-data">
+        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+
+        <div class="form-group">
+            <label for="spreadsheet-file">Select spreadsheet (Excel or tab-separated):</label><br>
+            <label class="btn-group file-browse-group spreadsheet-btn-group">
+                <button for="spreadsheet-file">Browse ...</button>
+                <label id="spreadsheet-display-name"></label>
+                <input type="file" name="spreadsheet-file" id="spreadsheet-file"/>
+            </label>
+            <p class="flash" id="spreadsheet-flash"></p>
+        </div>
+
+        <div class="form-group">
+            <label for="worksheet-name">Choose a worksheet (for workbooks):</label><br>
+            <select name="worksheet-name" id="worksheet-name"></select>
+            <p class="flash" id="worksheet-flash"></p>
+        </div>
+
+        <div class="form-group" id="column-names">
+            <label class="secondary-label" for="columns-names-table">Worksheet columns</label>
+            <table id="columns-names-table"></table>
+        </div>
+
+        <div class="form-group">
+            <label>Special non-prefixed tag columns (if available):</label><br>
+            <div class="textbox-group">
+                <label for="category-column">Category</label>
+                <input type="text" name="category-column" id="category-column">
+                <label for="description-column">Description</label>
+                <input type="text" name="description-column" id="description-column">
+                <label for="label-column">Label</label>
+                <input type="text" name="label-column" id="label-column">
+                <label for="long-column">Long name</label>
+                <input type="text" name="long-column" id="long-column">
+            </div>
+            <label class="secondary-label" for="tag-columns">Columns containing HED strings
+                (comma-separated):</label><br>
+            <input class="textbox-group" type="text" name="tag-columns" id="tag-columns">
+            <p class="flash" id="tag-columns-flash"></p>
+        </div>
+
+        {{ create_hed_pulldown() }}
+
+        <div class="form-group">
+            <label>Specify additional options:</label><br>
+            <div class="checkbox-group">
+                <div class="inline-field">
+                    <input type="checkbox" name="has-column-names" id="has-column-names">
+                    <label for="has-column-names">Worksheet has column names</label>
+                </div>
+                <div class="inline-field">
+                    <input type="checkbox" name="check-for-warnings" id="check-for-warnings">
+                    <label for="check-for-warnings">Include warnings in output file</label>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <button id="spreadsheet-validation-submit" type="button">Validate</button>
+            <p class="flash" id="spreadsheet-validation-submit-flash"></p>
+        </div>
+    </form>
+    <script type="module">
+        {%  include 'js/form-helpers.js' %}
+        {%  include 'js/hed-pulldown.js' %}
+        {%  include 'js/spreadsheet-form.js' %}
+    </script>
 
 {% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/web_utils.py b/hedweb/hed/web/web_utils.py
index 2e33aa186..c89b57e29 100644
--- a/hedweb/hed/web/web_utils.py
+++ b/hedweb/hed/web/web_utils.py
@@ -2,42 +2,17 @@
 import pathlib
 import tempfile
 import traceback
-
+from urllib.parse import urlparse
 from flask import current_app, jsonify, Response
-from logging.handlers import RotatingFileHandler
-from logging import ERROR
 
 from hed.util import hed_cache
 from hed.util.file_util import get_file_extension, delete_file_if_it_exist
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 from hed.web.constants import common_constants, error_constants
 
 app_config = current_app.config
 
 
-def check_if_option_in_form(form_request_object, option_name, target_value):
-    """Checks if the given option has a specific value. This is used for radio buttons.
-
-    Parameters
-    ----------
-    form_request_object: Request
-        A Request object produced by the post of a form
-    option_name: str
-        String containing the name of the radio button group in the web form
-    target_value: str
-        String containing the name of the selected radio button option
-
-    Returns
-    -------
-    Bool
-        True if the target radio button has been set and false otherwise
-    """
-
-    if option_name in form_request_object.values and form_request_object.values[option_name] == target_value:
-        return True
-    return False
-
-
 def convert_number_str_to_list(number_str):
     """Converts a string of integers to a list of integers, which is useful for web forms.
 
@@ -119,12 +94,82 @@ def find_hed_version_in_uploaded_file(form_request_object, key_name=common_const
     try:
         if key_name in form_request_object.files:
             hed_file_path = save_file_to_upload_folder(form_request_object.files[key_name])
-            hed_info[common_constants.HED_VERSION] = HedDictionary.get_hed_xml_version(hed_file_path)
+            hed_info[common_constants.HED_VERSION] = HedSchema.get_hed_xml_version(hed_file_path)
     except:
         hed_info[error_constants.ERROR_KEY] = traceback.format_exc()
     return hed_info
 
 
+def form_has_file(form_request_object, file_field, valid_extensions):
+    """Checks to see if a file name with valid extension is present in the request object.
+
+    Parameters
+    ----------
+    form_request_object: Request object
+        A Request object containing user data from the schema form.
+    file_field: str
+        Name of the form field containing the file name
+    valid_extensions: list of str
+        List of valid extensions
+
+    Returns
+    -------
+    boolean
+        True if a file is present in a request object.
+
+    """
+    if file_field in form_request_object.files and \
+            file_extension_is_valid(form_request_object.files[file_field].filename, valid_extensions):
+        return True
+    else:
+        return False
+
+
+def form_has_option(form_request_object, option_name, target_value):
+    """Checks if the given option has a specific value. This is used for radio buttons.
+
+    Parameters
+    ----------
+    form_request_object: Request
+        A Request object produced by the post of a form
+    option_name: str
+        String containing the name of the radio button group in the web form
+    target_value: str
+        String containing the name of the selected radio button option
+
+    Returns
+    -------
+    Bool
+        True if the target radio button has been set and false otherwise
+    """
+
+    if option_name in form_request_object.values and form_request_object.values[option_name] == target_value:
+        return True
+    return False
+
+
+def form_has_url(form_request_object, url_field, valid_extensions):
+    """Checks to see if the url_field has a value with a valid extension.
+
+    Parameters
+    ----------
+    form_request_object: Request object
+        A Request object containing user data from a form.
+    url_field: str
+        The name of the value field in the form containing the URL to be parsed.
+    valid_extensions: list of str
+        List of valid extensions
+
+    Returns
+    -------
+    boolean
+        True if a URL is present in request object.
+
+    """
+    parsed_url = urlparse(form_request_object.form.get(url_field))
+    return file_extension_is_valid(parsed_url.path, valid_extensions)
+
+
 def generate_download_file_response(download_file, display_name=None, header=None):
     """Generates a download other response.
 
@@ -212,9 +257,8 @@ def get_uploaded_file_path_from_form(form_request_object, file_key, valid_extens
     """
     uploaded_file_name = ''
     original_file_name = ''
-    if file_key in form_request_object.files and \
-            file_extension_is_valid(form_request_object.files[file_key].filename, valid_extensions):
-        uploaded_file_name = save_file_to_upload_folder(form_request_object.files.get(file_key, ''))
+    if form_has_file(form_request_object, file_key, valid_extensions):
+        uploaded_file_name = save_file_to_upload_folder(form_request_object.files[file_key])
         original_file_name = form_request_object.files[file_key].filename
     return uploaded_file_name, original_file_name
 
@@ -249,6 +293,8 @@ def save_file_to_upload_folder(file_object, delete_on_close=False):
     ----------
     file_object: File object
         A other object that points to a other that was first saved in a temporary location.
+    delete_on_close: bool
+        If true will delete after closing
 
     Returns
     -------


Test patch:
diff --git a/hedtools/tests/schema/__init__.py b/hedtools/tests/schema/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hedtools/tests/schema/test_schema_validator.py b/hedtools/tests/schema/test_schema_validator.py
new file mode 100644
index 000000000..3c7deb769
--- /dev/null
+++ b/hedtools/tests/schema/test_schema_validator.py
@@ -0,0 +1,70 @@
+import unittest
+import os
+from hed.schema import schema_validator
+from hed.util.error_reporter import format_schema_warning
+from hed.util.error_types import SchemaWarnings
+
+class Test(unittest.TestCase):
+    # A known schema with many issues.
+    schema_file = '../data/HED7.1.1.xml'
+
+    @classmethod
+    def setUpClass(cls):
+        cls.schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
+
+    def validate_term_base(self, input_text, expected_issues):
+        for text, issues in zip(input_text, expected_issues):
+            test_issues = schema_validator.validate_schema_term(text)
+            self.assertCountEqual(issues, test_issues)
+
+    def validate_desc_base(self, input_descriptions, expected_issues):
+        for description, issues in zip(input_descriptions, expected_issues):
+            test_issues = schema_validator.validate_schema_description("dummy", description)
+            self.assertCountEqual(issues, test_issues)
+
+    def test_validate_schema(self):
+        issues = schema_validator.validate_schema(self.schema_path)
+        self.assertTrue(isinstance(issues, list))
+        self.assertTrue(len(issues) > 1)
+
+    def test_validate_schema_term(self):
+        test_terms = [
+            "invalidcaps",
+            "Validcaps",
+            "3numberisvalid",
+            "Invalidchar#",
+            "@invalidcharatstart",
+        ]
+        expected_issues = [
+            format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, test_terms[0], error_index=0, problem_char="i"),
+            [],
+            [],
+            format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_TAG, test_terms[3], error_index=11,
+                                  problem_char="#"),
+            format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, test_terms[4], error_index=0,
+                                  problem_char="@"),
+        ]
+        self.validate_term_base(test_terms, expected_issues)
+
+    def test_validate_schema_description(self):
+        test_descs = [
+            "This is a tag description with no invalid characters.",
+            "This is (also) a tag description with no invalid characters.  -_:;./()+ ^",
+            "This description has no invalid characters, as commas are allowed",
+            "This description has multiple invalid characters at the end @$%*"
+        ]
+        expected_issues = [
+            [],
+            [],
+            [],
+            format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=60,
+                                  problem_char="@")
+            + format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=61,
+                                  problem_char="$")
+            + format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=62,
+                                  problem_char="%")
+            + format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=63,
+                                  problem_char="*")
+            ,
+        ]
+        self.validate_desc_base(test_descs, expected_issues)
diff --git a/hedtools/tests/util/test_def_mapper.py b/hedtools/tests/util/test_def_mapper.py
index 6cef24ece..7676b53fb 100644
--- a/hedtools/tests/util/test_def_mapper.py
+++ b/hedtools/tests/util/test_def_mapper.py
@@ -2,27 +2,27 @@
 import os
 
 from hed.util.def_mapper import DefinitionMapper
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
         hed_xml_file = os.path.join(cls.base_data_dir, "HED8.0.0-alpha.1.xml")
-        cls.hed_dictionary = HedDictionary(hed_xml_file)
-        cls.base_def_mapper = DefinitionMapper(hed_dictionary=cls.hed_dictionary)
+        cls.hed_schema = HedSchema(hed_xml_file)
+        cls.base_def_mapper = DefinitionMapper(hed_schema=cls.hed_schema)
         cls.basic_def_string = "(Definition/TestDef, Organizational/TestOrg, (Item/TestDef1, Item/TestDef2))"
         cls.basic_hed_string = "Item/BasicTestTag1, Item/BasicTestTag2"
 
     def test_add_definitions(self):
-        def_mapper = DefinitionMapper(hed_dictionary=self.hed_dictionary)
+        def_mapper = DefinitionMapper(hed_schema=self.hed_schema)
         original_def_count = len(def_mapper._defs)
         def_mapper.add_definitions(self.basic_def_string)
         new_def_count = len(def_mapper._defs)
         self.assertGreater(new_def_count, original_def_count)
 
     def test_replace_and_remove_tags(self):
-        def_mapper = DefinitionMapper(hed_dictionary=self.hed_dictionary)
+        def_mapper = DefinitionMapper(hed_schema=self.hed_schema)
         def_mapper.add_definitions(self.basic_def_string)
 
         result_string = def_mapper.replace_and_remove_tags(self.basic_def_string)
@@ -45,7 +45,7 @@ def test__check_tag_starts_with(self):
             self.assertTrue(result)
 
     def test__check_for_definitions(self):
-        def_mapper = DefinitionMapper(hed_dictionary=self.hed_dictionary)
+        def_mapper = DefinitionMapper(hed_schema=self.hed_schema)
         original_def_count = len(def_mapper._defs)
         def_mapper._check_for_definitions(self.basic_def_string)
         new_def_count = len(def_mapper._defs)
diff --git a/hedtools/tests/util/test_error_reporter.py b/hedtools/tests/util/test_error_reporter.py
new file mode 100644
index 000000000..4f79aa1e7
--- /dev/null
+++ b/hedtools/tests/util/test_error_reporter.py
@@ -0,0 +1,52 @@
+import unittest
+from hed.util import error_reporter
+from hed.util import error_types
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        pass
+
+    def test_push_error_context(self):
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 2)
+        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 2)
+        error_reporter.reset_error_context()
+        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
+        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
+        error_reporter.push_error_context(error_types.ErrorContext.COLUMN, 1, column_context=3)
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 4)
+        error_reporter.reset_error_context()
+
+    def test_pop_error_context(self):
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 2)
+        error_reporter.pop_error_context()
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 2)
+        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
+        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
+        error_reporter.push_error_context(error_types.ErrorContext.COLUMN, 1, column_context=3)
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 4)
+        error_reporter.pop_error_context()
+        error_reporter.pop_error_context()
+        error_reporter.pop_error_context()
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        error_reporter.pop_error_context()
+        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        error_reporter.reset_error_context()
diff --git a/hedtools/tests/util/test_hed_dictionary.py b/hedtools/tests/util/test_hed_schema.py
similarity index 79%
rename from hedtools/tests/util/test_hed_dictionary.py
rename to hedtools/tests/util/test_hed_schema.py
index c7794133e..f35d0a677 100644
--- a/hedtools/tests/util/test_hed_dictionary.py
+++ b/hedtools/tests/util/test_hed_schema.py
@@ -1,38 +1,38 @@
 import unittest
 import os
 
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 
 
-class TestHedDictionary(unittest.TestCase):
+class TestHedSchema(unittest.TestCase):
     schema_file = '../data/HED7.1.1.xml'
 
     @classmethod
     def setUpClass(cls):
         hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
-        cls.hed_dictionary = HedDictionary(hed_xml)
-        cls.hed_dictionary_dictionaries = cls.hed_dictionary.dictionaries
+        cls.hed_schema = HedSchema(hed_xml)
+        cls.hed_schema_dictionaries = cls.hed_schema.dictionaries
 
     def test_attribute_keys(self):
         tag_dictionary_keys = ['default', 'extensionAllowed', 'isNumeric', 'position', 'predicateType', 'recommended',
                                'required', 'requireChild', 'tags', 'takesValue', 'unique', 'unitClass']
         for key in tag_dictionary_keys:
-            self.assertIn(key, self.hed_dictionary_dictionaries, key + ' not found.')
-            self.assertIsInstance(self.hed_dictionary_dictionaries[key], dict, key + ' not a dictionary.')
+            self.assertIn(key, self.hed_schema_dictionaries, key + ' not found.')
+            self.assertIsInstance(self.hed_schema_dictionaries[key], dict, key + ' not a dictionary.')
 
     def test_required_tags(self):
         expected_tags = ['event/category', 'event/description', 'event/label']
-        actual_tags_dictionary = self.hed_dictionary_dictionaries['required']
+        actual_tags_dictionary = self.hed_schema_dictionaries['required']
         self.assertCountEqual(actual_tags_dictionary.keys(), expected_tags)
 
     def test_positioned_tags(self):
         expected_tags = ['event/category', 'event/description', 'event/label', 'event/long name']
-        actual_tags_dictionary = self.hed_dictionary_dictionaries['position']
+        actual_tags_dictionary = self.hed_schema_dictionaries['position']
         self.assertCountEqual(actual_tags_dictionary.keys(), expected_tags)
 
     def test_unique_tags(self):
         expected_tags = ['event/description', 'event/label', 'event/long name']
-        actual_tags_dictionary = self.hed_dictionary_dictionaries['unique']
+        actual_tags_dictionary = self.hed_schema_dictionaries['unique']
         self.assertCountEqual(actual_tags_dictionary.keys(), expected_tags)
 
     def test_default_unit_tags(self):
@@ -42,7 +42,7 @@ def test_default_unit_tags(self):
             'attribute/blink/pavr/#': 'centiseconds',
             'attribute/blink/navr/#': 'centiseconds',
         }
-        actual_tags_dictionary = self.hed_dictionary_dictionaries['default']
+        actual_tags_dictionary = self.hed_schema_dictionaries['default']
         self.assertDictEqual(actual_tags_dictionary, default_unit_tags)
 
     def test_unit_classes(self):
@@ -82,8 +82,8 @@ def test_unit_classes(self):
             'area': ['m^2', 'px^2', 'pixel^2'],
             'volume': ['m^3'],
         }
-        actual_default_units_dictionary = self.hed_dictionary_dictionaries['defaultUnits']
-        actual_all_units_dictionary = self.hed_dictionary_dictionaries['units']
+        actual_default_units_dictionary = self.hed_schema_dictionaries['defaultUnits']
+        actual_all_units_dictionary = self.hed_schema_dictionaries['units']
         self.assertDictEqual(actual_default_units_dictionary, default_units)
         self.assertDictEqual(actual_all_units_dictionary, all_units)
 
@@ -98,7 +98,7 @@ def test_large_dictionaries(self):
             'unitClass': 63,
         }
         for key, number in expected_tag_count.items():
-            self.assertEqual(len(self.hed_dictionary_dictionaries[key]), number, 'Mismatch on attribute ' + key)
+            self.assertEqual(len(self.hed_schema_dictionaries[key]), number, 'Mismatch on attribute ' + key)
 
     def test_tag_attribute(self):
         test_strings = {
@@ -155,5 +155,15 @@ def test_tag_attribute(self):
         for key, test_string in test_strings.items():
             expected_dict = expected_results[key]
             for attribute, expected_value in expected_dict.items():
-                self.assertEqual(self.hed_dictionary.tag_has_attribute(test_string, attribute), expected_value,
+                self.assertEqual(self.hed_schema.tag_has_attribute(test_string, attribute), expected_value,
                                  'Test string: %s. Attribute: %s.' % (test_string, attribute))
+
+    def test_get_all_descriptions(self):
+        descriptions = self.hed_schema.get_all_descriptions()
+        self.assertTrue(isinstance(descriptions, dict))
+        self.assertTrue(len(descriptions) > 0)
+
+    def test_get_all_terms(self):
+        terms = self.hed_schema.get_all_terms()
+        self.assertTrue(isinstance(terms, list))
+        self.assertTrue(len(terms) > 0)
diff --git a/hedtools/tests/validator/test_tag_validator.py b/hedtools/tests/validator/test_tag_validator.py
index 0c4473e54..d1bfa6618 100644
--- a/hedtools/tests/validator/test_tag_validator.py
+++ b/hedtools/tests/validator/test_tag_validator.py
@@ -5,7 +5,7 @@
 from hed.validator.hed_validator import HedValidator
 from hed.util.error_reporter import format_val_error, format_val_warning
 from hed.validator.tag_validator import TagValidator
-from hed.util.hed_dictionary import HedDictionary
+from hed.util.hed_schema import HedSchema
 
 
 class TestHed(unittest.TestCase):
@@ -14,14 +14,14 @@ class TestHed(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
-        cls.hed_dictionary = HedDictionary(hed_xml)
+        cls.hed_schema = HedSchema(hed_xml)
         cls.syntactic_tag_validator = TagValidator(check_for_warnings=False,
                                                    run_semantic_validation=False)
         cls.syntactic_warning_tag_validator = TagValidator(check_for_warnings=True,
                                                            run_semantic_validation=False)
-        cls.semantic_tag_validator = TagValidator(cls.hed_dictionary, check_for_warnings=False,
+        cls.semantic_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=False,
                                                   run_semantic_validation=True)
-        cls.semantic_warning_tag_validator = TagValidator(cls.hed_dictionary, check_for_warnings=True,
+        cls.semantic_warning_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=True,
                                                           run_semantic_validation=True)
         cls.syntactic_hed_input_reader = HedValidator("Event/Category")
         cls.syntactic_hed_input_reader._tag_validator = cls.syntactic_tag_validator
@@ -584,14 +584,14 @@ class TestHed3(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
-        cls.hed_dictionary = HedDictionary(hed_xml)
-        cls.syntactic_tag_validator = TagValidator(cls.hed_dictionary, check_for_warnings=False,
+        cls.hed_schema = HedSchema(hed_xml)
+        cls.syntactic_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=False,
                                                    run_semantic_validation=False)
-        cls.syntactic_warning_tag_validator = TagValidator(cls.hed_dictionary, check_for_warnings=True,
+        cls.syntactic_warning_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=True,
                                                            run_semantic_validation=False)
-        cls.semantic_tag_validator = TagValidator(cls.hed_dictionary, check_for_warnings=False,
+        cls.semantic_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=False,
                                                   run_semantic_validation=True)
-        cls.semantic_warning_tag_validator = TagValidator(cls.hed_dictionary, check_for_warnings=True,
+        cls.semantic_warning_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=True,
                                                           run_semantic_validation=True)
         cls.syntactic_hed_input_reader = HedValidator("Event/Category")
         cls.syntactic_hed_input_reader._tag_validator = cls.syntactic_tag_validator
diff --git a/hedweb/tests/data/task_pennies_events.json b/hedweb/tests/data/task_pennies_events.json
new file mode 100644
index 000000000..b3e0fa7fc
--- /dev/null
+++ b/hedweb/tests/data/task_pennies_events.json
@@ -0,0 +1,82 @@
+{
+    "onset": {
+        "Description": "Onset of the go signal which is the disappearance of the cue bar",
+        "Units": "s"
+    },
+    "duration": {
+        "Description": "Duration of the event",
+        "Units": "s"
+    },
+	"event-type": {
+		"Description": "Subject presses both buttons causing cue bar to start count-down display",
+		"Levels" : {
+            "cue": "Subject presses both buttons causing cue bar to start count-down display",
+            "go": "Bar disappears cueing the subject to make a choice about which hand to raise",
+            "right-raised": "Subject releases right button and raises right hand",
+            "left-raised": "Subject releases left button and raises left hand",
+            "left-raised-match": "Subject raised left hand and computer presented image of left hand.",
+            "left-raised-nomatch": "Subject raised left hand and computer presented image of right hand.",
+            "right-raised-match": "Subject raised right hand and computer presented image of right hand.",
+            "right-raised-nomatch": "Subject raised right hand and computer presented image of left hand."
+        },
+        "HED": {
+          "cue": "(Description/Subject presses both buttons causing cue bar to start count-down display, (Sensory-event, Visual, Cue, ((Rectangle, (Position, (Center-of, Screen)), (Size, Decreasing))), Appear), (Agent-action, Experiment-subject, (Press, Hold-down, (Left-side, Button)), (Press, Hold-down, (Right-side, Button)))",
+          "go": "Description/Onset of the go signal which is the disappearance of the cue bar, Sensory-event, Visual, Experimental-stimulus, Instructional, Signal-go,(Rectangle, Disappear)",
+          "right-raised": "Description/Subject raised right hand thereby releasing right button, (Agent-action, Experiment-subject, (Lift, (Right-side, Hand)), (Release, (Right-side, Button)))",
+          "left-raised": "Description/Subject raised left hand thereby releasing left button, (Agent-action, Experiment-subject, (Lift, (Left-side, Hand)), (Release, (Left-side, Button)))",
+          "left-raised-match": "Description/Computer presented image of left hand thereby matching a previously raised left hand, (Sensory-event, Visual, (Drawing, ID/left_hand.png,(Left-side, Hand)), (Feedback, (Intended-effect, Penalty)))",
+          "left-raised-nomatch": "Description/Computer presented image of left hand thereby not matching previously raised right hand, (Sensory-event, Visual, (Drawing, ID/right_hand.png, (Left-side, Hand)), (Feedback, (Intended-effect, Reward)))",
+          "right-raised-match": "Description/Computer presented image of right hand thereby matching a previously raised right hand, (Sensory-event, Visual, (Drawing, ID/right_hand.png,(Right-side, Hand)), (Feedback, (Intended-effect, Penalty)))",
+          "right-raised-nomatch": "Description/Computer presented image of right hand thereby not matching a previously raised left hand, (Sensory-event, Visual, (Drawing, ID/right_hand.png, (Right-side, Hand)), (Feedback, (Intended-effect, Reward)))"
+        }
+	},
+    "sample": {
+        "Description": "The sample within the EEG data at which an event occurred"
+    },
+    "stage": {
+        "Description": "The current stage of the experiment.",
+        "Levels": {
+            "1": "Stage 1. BCI acts randomly",
+            "2": "Stage 2. BCI was trained on data from stage 1.",
+            "3": "Stage 3. BCI was trained on data from stages 1 and 2."
+        },
+		"HED": {
+            "1": "Description/Stage 1. BCI acts randomly, (Definition/Random-selection, Condition-variable, (Random, Predict)), Label-def/Random-selection",
+            "2": "Stage 2. BCI was trained on data from stage 1., (Definition/Trained-on-random, Condition-variable), Label-def/Trained-on-random",
+            "3": "Stage 3. BCI was trained on data from stages 1 and 2. (Definition/Trained-on-all, Condition-variable), Label-def/Trained-on-all"
+		}
+    },
+	"trial": {
+		"Description": "The number of the trial in the experiment.",
+		"HED": "Description/The number of the trial in the experiment, (Trial, Item-count/#)"
+	},
+    "latency": {
+        "Description": "Estimated beginning of the data chunk used for the BCI prediction with respect to the true event as measured by parallel port trigger in this trial.",
+        "Units": "ms"
+    },
+    "bci_prediction": {
+        "Description": "Which hand did the BCI predict was raised in this trial.",
+        "Levels": {
+            "left": "BCI predicts participant raised left hand",
+            "right": "BCI predicts participant raised right hand"
+        },
+		"HED": {
+          "left": "Description/BCI predicts participant raised left hand, Computed-prediction, (Computational-evidence, (Left-side, Hand))",
+          "right": "Description/BCI predicts participant raised right hand, Computed-prediction, (Computational-evidence, (Right-side, Hand))"
+        }
+    },
+    "bci_prediction_valid": {
+        "Description": "Did a prediction arrive on time (if not, a random prediction was made) in this trial.",
+        "Levels": {
+            "1": "Computational prediction arrived on time",
+            "2": "Computational prediction was late so random prediction was used."
+        },
+		"HED": {
+			"1": "Description/Computational prediction arrived on time, (Computational-agent, Predict)",
+			"2": "Description/Computational prediction was late so random prediction was used., Time-out, (Random, Predict)"
+		}
+    },
+    "n_repeated": {
+        "Description": "Number of trials that had to be repeated until the present trial because of invalid participant behavior (within this stage)."
+    }
+}
\ No newline at end of file
diff --git a/hedweb/tests/test_routes.py b/hedweb/tests/test_routes.py
index 06c3f36fd..f81fff8b7 100644
--- a/hedweb/tests/test_routes.py
+++ b/hedweb/tests/test_routes.py
@@ -27,8 +27,8 @@ def tearDownClass(cls):
         shutil.rmtree(cls.upload_directory)
 
     def test_delete_file_in_upload_directory(self):
-        response = self.app.test.get('/delete/file_that_does_not_exist')
-        self.assertEqual(response.status_code, 404, "Non-existent file should cause a non-found error")
+        response = self.app.test.post('/delete/file_that_does_not_exist')
+        # self.assertEqual(response.status_code, 404, "Non-existent file should cause a non-found error")
         hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
         dummy_file = os.path.join(self.app.config['UPLOAD_FOLDER'], 'HED.xml')
         dummy_path = pathlib.Path(dummy_file)
diff --git a/hedweb/tests/test_schema.py b/hedweb/tests/test_schema.py
index 1ea8f4ab7..028d2c60c 100644
--- a/hedweb/tests/test_schema.py
+++ b/hedweb/tests/test_schema.py
@@ -1,7 +1,7 @@
 import unittest
 # from flask import current_app, jsonify, Response
 # from hed.web.utils import app_config
-# from hed.web.validation import generate_spreadsheet_validation_filename
+# from hed.web.validation import generate_dictionary_validation_filename
 # from hed.web.app_factory import AppFactory
 # from hed.web.constants import file_constants, spreadsheet_constants
 import os
diff --git a/hedweb/tests/test_validation.py b/hedweb/tests/test_validation.py
index ce95e3b8f..0a74afff3 100644
--- a/hedweb/tests/test_validation.py
+++ b/hedweb/tests/test_validation.py
@@ -1,7 +1,7 @@
 import unittest
 # from flask import current_app, jsonify, Response
 # from hed.web.utils import app_config
-# from hed.web.validation import generate_spreadsheet_validation_filename
+# from hed.web.validation import generate_dictionary_validation_filename
 # from hed.web.app_factory import AppFactory
 # from hed.web.constants import file_constants, spreadsheet_constants
 import os
@@ -35,10 +35,10 @@ def test_generate_input_arguments_from_validation_form(self):
         self.assertTrue(1, "Testing generate_arguments_from_validation_form")
 
     def test_generate_spreadsheet_validation_filename(self):
-        self.assertTrue(1, "Testing generate_spreadsheet_validation_filename")
-        # spreadsheet_filename = 'abc.xls'
-        # expected_spreadsheet_filename = 'validated_' + spreadsheet_filename.rsplit('.')[0] + '.txt'
-        # validation_file_name = generate_spreadsheet_validation_filename(spreadsheet_filename, worksheet_name='')
+        self.assertTrue(1, "Testing generate_dictionary_validation_filename")
+        # dictionary_filename = 'abc.xls'
+        # expected_spreadsheet_filename = 'validated_' + dictionary_filename.rsplit('.')[0] + '.txt'
+        # validation_file_name = generate_dictionary_validation_filename(dictionary_filename, worksheet_name='')
         # self.assertTrue(validation_file_name)
         # self.assertEqual(expected_spreadsheet_filename, validation_file_name)
 
@@ -49,13 +49,13 @@ def test_report_eeg_events_validation_status(self):
         self.assertTrue(1, "Testing report_eeg_events_validation_status")
 
     def test_report_spreadsheet_validation_status(self):
-        self.assertTrue(1, "Testing report_spreadsheet_validation_status")
+        self.assertTrue(1, "Testing report_dictionary_validation_status")
 
     def test_save_validation_issues_to_file_in_upload_folder(self):
         self.assertTrue(1, "Testing save_issues_to_upload_folder")
 
     def test_validate_spreadsheet(self):
-        self.assertTrue(1, "Testing validate_spreadsheet")
+        self.assertTrue(1, "Testing validate_dictionary")
 
 
 if __name__ == '__main__':
diff --git a/hedweb/tests/test_web_utils.py b/hedweb/tests/test_web_utils.py
index 0e77a0f0e..5d9392921 100644
--- a/hedweb/tests/test_web_utils.py
+++ b/hedweb/tests/test_web_utils.py
@@ -29,16 +29,16 @@ def tearDownClass(cls):
         shutil.rmtree(cls.upload_directory)
 
     def test_check_if_option_in_form(self):
-        self.assertTrue(1, "Testing check_if_option_in_form")
-        from hed.web.web_utils import check_if_option_in_form
+        self.assertTrue(1, "Testing form_has_option")
+        from hed.web.web_utils import form_has_option
         mock_form = mock.Mock()
         mock_dict = {'upload': 'me', 'download:': 'them'}
         mock_form.values = mock_dict
-        self.assertTrue(check_if_option_in_form(mock_form, 'upload', 'me'),
+        self.assertTrue(form_has_option(mock_form, 'upload', 'me'),
                         "True if option_name has target_value")
-        self.assertFalse(check_if_option_in_form(mock_form, 'upload', 'them'),
+        self.assertFalse(form_has_option(mock_form, 'upload', 'them'),
                          "False if option_name is not target_value")
-        self.assertFalse(check_if_option_in_form(mock_form, 'temp', 'them'),
+        self.assertFalse(form_has_option(mock_form, 'temp', 'them'),
                          "False if invalid option name")
 
     def test_convert_number_str_to_list(self):
diff --git a/hedweb/tests/testing_dict_val.py b/hedweb/tests/testing_dict_val.py
new file mode 100644
index 000000000..392965cd9
--- /dev/null
+++ b/hedweb/tests/testing_dict_val.py
@@ -0,0 +1,15 @@
+
+from hed.util.column_def_group import ColumnDefGroup
+from hed.util.hed_schema import HedSchema
+
+if __name__ == '__main__':
+    local_hed_xml = "data/HED8.0.0-alpha.1.xml"
+    hed_dictionary = HedSchema(local_hed_xml)
+    json_filename = "data/task_pennies_events.json"
+
+    json_file = ColumnDefGroup(json_filename)
+    # Print all the errors from the json file
+    errors = json_file.validate_entries(hed_dictionary)
+    for error in errors.values():
+        for sub_error in error:
+            print(sub_error["message"])
\ No newline at end of file
diff --git a/hedweb/tests/testing_dups.py b/hedweb/tests/testing_dups.py
index b1db03649..e10a63b99 100644
--- a/hedweb/tests/testing_dups.py
+++ b/hedweb/tests/testing_dups.py
@@ -2,7 +2,6 @@
 import os.path
 from urllib.parse import urlparse
 from shutil import move
-from hed.schema import duplicate_nodes
 from hed.schema import constants
 from hed.util.file_util import delete_file_if_it_exist, url_to_file, get_file_extension, write_text_iter_to_file