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

Issue:
Bump jinja2 from 2.11.2 to 2.11.3 in /hedweb
Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.2 to 2.11.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/pallets/jinja/releases">jinja2's releases</a>.</em></p>
<blockquote>
<h2>2.11.3</h2>
<p>This contains a fix for a speed issue with the <code>urlize</code> filter. <code>urlize</code> is likely to be called on untrusted user input. For certain inputs some of the regular expressions used to parse the text could take a very long time due to backtracking. As part of the fix, the email matching became slightly stricter. The various speedups apply to <code>urlize</code> in general, not just the specific input cases.</p>
<ul>
<li>PyPI: <a href="https://pypi.org/project/Jinja2/2.11.3/">https://pypi.org/project/Jinja2/2.11.3/</a></li>
<li>Changes: <a href="https://jinja.palletsprojects.com/en/2.11.x/changelog/#version-2-11-3">https://jinja.palletsprojects.com/en/2.11.x/changelog/#version-2-11-3</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/pallets/jinja/blob/master/CHANGES.rst">jinja2's changelog</a>.</em></p>
<blockquote>
<h2>Version 2.11.3</h2>
<p>Released 2021-01-31</p>
<ul>
<li>Improve the speed of the <code>urlize</code> filter by reducing regex
backtracking. Email matching requires a word character at the start
of the domain part, and only word characters in the TLD. :pr:<code>1343</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pallets/jinja/commit/cf215390d4a4d6f0a4de27e2687eed176878f13d"><code>cf21539</code></a> release version 2.11.3</li>
<li><a href="https://github.com/pallets/jinja/commit/15ef8f09b659f9100610583938005a7a10472d4d"><code>15ef8f0</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/pallets/jinja/issues/1343">#1343</a> from pallets/urlize-speedup</li>
<li><a href="https://github.com/pallets/jinja/commit/ef658dc3b6389b091d608e710a810ce8b87995b3"><code>ef658dc</code></a> speed up urlize matching</li>
<li><a href="https://github.com/pallets/jinja/commit/eeca0fecc3318d43f61bc340ad61db641b861ade"><code>eeca0fe</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/pallets/jinja/issues/1207">#1207</a> from mhansen/patch-1</li>
<li><a href="https://github.com/pallets/jinja/commit/2dd769111cbb1a2637f805b3b4c652ec8096d371"><code>2dd7691</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/pallets/jinja/issues/1209">#1209</a> from mhansen/patch-3</li>
<li><a href="https://github.com/pallets/jinja/commit/48929401db7228db04dfd8e88115dd5c30dc2d86"><code>4892940</code></a> do_dictsort: update example ready to copy/paste</li>
<li><a href="https://github.com/pallets/jinja/commit/7db7d336ba12574e6205fdd929386fd529e3fad4"><code>7db7d33</code></a> api.rst: bugfix in docs, import PackageLoader</li>
<li><a href="https://github.com/pallets/jinja/commit/9ec465baefe32e305bd4e61da49e6c39360c194e"><code>9ec465b</code></a> fix changelog header</li>
<li>See full diff in <a href="https://github.com/pallets/jinja/compare/2.11.2...2.11.3">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jinja2&package-manager=pip&previous-version=2.11.2&new-version=2.11.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/hed-standard/hed-python/network/alerts).

</details>


Discussion / hints:

Fix patch:
diff --git a/README.md_old b/README.md_old
deleted file mode 100644
index 549d6d4fe..000000000
--- a/README.md_old
+++ /dev/null
@@ -1,97 +0,0 @@
-# HEDTools
-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.
-
-Once an experiment is annotated using HED, this HEDTools repository provides a suite of powerful tools for MATLAB and python to extract, search, and combine events based on these annotations. The user manual can be found online at: http://vislab.github.io/HEDTools/. 
-
-### Installation
-
-You can run HEDTools as a standalone toolbox or as a plugin for EEGLAB. In both cases, you should install EEGLAB unless you plan to use HEDTools only to validate tags from spreadsheets.
-
-#### Running as a standalone application
-
-If your data files are .mat files, you can simply unzip the EEGLABPlugin/HEDToolsx.x.x.zip anywhere you choose. Execute the *setup* script to set the paths each time you run MATLAB. Alternatively, you can add the code contained in *setup* to your startup script.
-
-#### Running with .set data file types
-
-If you wish to use EEGLAB, you should follow the directions above also.
-
-#### Running as a plugin to EEGLAB
-
-To install HEDTools unzip the HEDToolsx.x.x.zip file inside the EEGLAB plugin directory. If you don’t install HEDTools via the EEGLAB menu, you can find this file at:
-
-https://github.com/VisLab/HEDTools/tree/master/EEGLABPlugin
-
-When you start EEGLAB again, HEDTools should be ready to use. Note: EEGLAB requires that each EEGLAB plugin be in its own subdirectory in the plugins directory of EEGLAB. Thus, if you have unzipped HEDTools correctly, you should see …/eeglab/plugins/HEDToolsx.x.x/eegplugin_hedtools.m.
-
-### Notes
-
-**Note:** For convenience, EEGLABPlugin directory contains the latest released version of the
-HEDTools that can be unzipped into your EEGLAB plugins directory. 
-
-**Note:** The HEDTools will be slow to load if you have a large amount of data in your workspace when you start EEGLAB. This is because the MATLAB function to add to the Java class path clears all workspace variables. The EEGLAB startup saves all workspace variables temporarily before setting up the path and then restores the variables. 
-
-### Citing HEDTools
-HEDTools is freely available under the GUN General Public License. Please cite the following publications if using:
-
-N. Bigdely-Shamlo, J. Cockfield, S. Makeig, T. Rognon, C. LaValle, M. Miyakoshi, and K. Robbins (2016). Hierarchical Event Descriptors (HED): Semi-structured tagging for real-world events in large-scale EEG, Frontiers in Neuroinformatics doi: 10.3389/fninf.2016.00042.
-
-N. Bigdely-Shamlo, S. Makeig, and K. Robbins (2016). Preparing laboratory and real-world EEG data for large-scale analysis: A containerized approach, Frontiers in Neuroinformatics 1 08 March 2016 | http://dx.doi.org/10.3389/fninf.2016.00007. PMID: 27014048, PMCID: PMC4782059.
-
-### Releases
-Version 2.4.0 Released 5/31/2018
-* Tag epoch search bar only works with comma delimiters.
-* Added Attribute/Participant indication as an exclusive tag.
-
-Version 2.3.0 Released 5/14/2018
-* HEDTools is distributed with HED schema 7.0.1.
-* HEDTools validation checks for duplicate tags and comma errors.
-* Refactored validation code. 
-
-Version 2.2.0 Released 1/18/2018
-* HEDTools validation functions accept time unit class tags.
-* Tags with parentheses no longer report a missing comma error.
-* findhedevents function search ignores slashes at the beginning and end of tags. 
-
-Version 2.1.0 Released 1/4/2018
-* HEDTools validation functions report missing commas and unequal number of tag group parentheses.
-
-Version 2.0.5 Released 11/10/2017
-* HEDTools is compatible with jsonlab version 1.5
-
-Version 2.0.4 Released 11/9/2017
-* HED tag validation messages for groups only print out the tag that raised an issue
-* Renamed validatestr function to validatehedstr  
-
-Version 2.0.3 Released 10/31/2017
-* Fixed tsvhed2map prefix issue 
-
-Version 2.0.2 Released 9/11/2017
-* HEDTools is distributed with HED schema 6.0.4
-* findhedevents function is more robust
-
-Version 2.0.1 Released 7/22/2017
-* HEDTools is distributed with HED schema 6.0.3
-
-Version 2.0.0 Released 7/16/2017
-* epochhed and findhedtags search functionality has been re-implemented. Added exclusive tags feature and removed boolean search functionality. 
-* implemented validateworksheethedtags function for validating the HED tags in an Excel spreadsheet.
-
-Version 1.0.4 Released 4/17/2017 
-
-* Nested groups are validated 
-* Added pop_updatehed function
-* Added validatestr function 
-* Restructured top-level directory
-
-Version 1.0.3 Released 2/22/2017
-
-* Changed NOT operator to AND NOT operator in epochhed function.
-* Group tilde count is correctly checked during validation.
-* Fixed preserve prefix option when set to true.
-* Multi-lined fieldMap descriptions parse correctly.
-
-Version 1.0.2 Released 12/21/2016
-
-* Refactored so that all pop functions comply with EEGLAB standards.
-* Fixed tag epoching search bar and several minor bugs. 
-* Added menu for saving multiple datasets.
diff --git a/hedtools/examples/run_hed_conversion_examples.py b/hedtools/examples/run_hed_conversion_examples.py
index 8d4ca15f1..5d8501d4c 100644
--- a/hedtools/examples/run_hed_conversion_examples.py
+++ b/hedtools/examples/run_hed_conversion_examples.py
@@ -20,22 +20,47 @@ def long_to_short_file(input_file):
     converted_file.save(include_formatting=True, add_suffix="_test_long_to_short")
 
 
+def long_to_short_string(input_string):
+    tag_formatter = TagFormat(local_hed_file_no_dupe)
+    converted_string, errors = tag_formatter.convert_hed_string_to_short(input_string)
+    return converted_string, errors
+
+
 def short_to_long_file(input_file):
     tag_formatter = TagFormat(local_hed_file_no_dupe)
     converted_file, errors = tag_formatter.convert_file_to_long_tags(input_file)
     converted_file.save(include_formatting=True, add_suffix="_test_short_to_long")
 
+
+def short_to_long_string(input_string):
+    tag_formatter = TagFormat(local_hed_file_no_dupe)
+    converted_string, errors = tag_formatter.convert_hed_string_to_long(input_string)
+    return converted_string, errors
+
+
 if __name__ == '__main__':
     example_data_path = 'data'
     hed3_tags_single_sheet = os.path.join(example_data_path, 'hed3_tags_single_sheet.xlsx')
 
     prefixed_needed_tag_columns = {2: 'Event/Label/', 3: 'Event/Description/'}
-    input_file = HedFileInput(hed3_tags_single_sheet, tag_columns=[4],
-                              column_prefix_dictionary=prefixed_needed_tag_columns,
-                              worksheet_name='LKT Events')
-    long_to_short_file(input_file)
-
-    input_file = HedFileInput(hed3_tags_single_sheet, tag_columns=[4],
-                              column_prefix_dictionary=prefixed_needed_tag_columns,
-                              worksheet_name='LKT Events')
-    short_to_long_file(input_file)
\ No newline at end of file
+    inputs = HedFileInput(hed3_tags_single_sheet, tag_columns=[4],
+                          column_prefix_dictionary=prefixed_needed_tag_columns,
+                          worksheet_name='LKT Events')
+    long_to_short_file(inputs)
+
+    inputs = HedFileInput(hed3_tags_single_sheet, tag_columns=[4],
+                          column_prefix_dictionary=prefixed_needed_tag_columns,
+                          worksheet_name='LKT Events')
+    short_to_long_file(inputs)
+
+    inputs = 'Attribute/Sensory/Visual/Color/CSS-color/White-color/White'
+    tag, error = long_to_short_string(inputs)
+    print(f"Tag= {tag}, errors=[{error}]")
+
+    inputs =  'Attribute/Visual/Color/CSS-color/White-color/White'
+    tag, error = long_to_short_string(inputs)
+    print(f"Tag= {tag}, errors=[{error}]")
+
+    inputs =  'White'
+    tag, error = short_to_long_string(inputs)
+    print(f"Tag= {tag}, errors=[{error}]")
diff --git a/hedtools/examples/run_hed_def_mapper_example.py b/hedtools/examples/run_hed_def_mapper_example.py
index cef5926c4..af48575cf 100644
--- a/hedtools/examples/run_hed_def_mapper_example.py
+++ b/hedtools/examples/run_hed_def_mapper_example.py
@@ -13,7 +13,7 @@
 import os
 import hed
 from hed.util.event_file_input import EventFileInput
-from hed import schema
+from hed.schema.hed_schema_file import load_schema
 from hed.validator.hed_validator import HedValidator
 from hed.util.column_def_group import ColumnDefGroup
 
@@ -22,14 +22,14 @@
     example_data_path = 'data'  # path to example data
     hed3_tags_single_sheet = os.path.join(example_data_path, 'hed_tag_def_example.xlsx')
 
-    hed_schema = schema.load_schema(local_hed_file)
+    hed_schema = load_schema(local_hed_file)
     prefixed_needed_tag_columns = {2: 'Event/Label/', 3: 'Event/Description/'}
     json_file = "data/both_types_events_def_example.json"
     column_group = ColumnDefGroup(json_file)
     def_dict, def_issues = column_group.extract_defs(hed_schema)
     if def_issues:
         print(hed.get_printable_issue_string(def_issues,
-                                                      title="There should be no errors in the definitions from the sidecars:"))
+                                             title="There should be no errors in the definitions from the sidecars:"))
     input_file = EventFileInput(hed3_tags_single_sheet, json_def_files=column_group,
                                 tag_columns=[4], column_prefix_dictionary=prefixed_needed_tag_columns,
                                 worksheet_name='LKT Events',
@@ -39,11 +39,11 @@
     validation_issues = input_file.validate_file_sidecars(hed_schema=hed_schema)
     if validation_issues:
         print(hed.get_printable_issue_string(validation_issues,
-                                                      title="There should be no errors with the sidecar.  This will likely cause other errors if there are."))
+                                             title="There should be no errors with the sidecar.  \""
+                                             "This will likely cause other errors if there are."))
     validator = HedValidator(hed_schema=hed_schema)
     validation_issues = validator.validate_input(input_file)
-    print(hed.get_printable_issue_string(validation_issues,
-                                               "Normal hed string errors"))
+    print(hed.get_printable_issue_string(validation_issues, "Normal hed string errors"))
 
     input_file.save(include_formatting=True, add_suffix="_test_output", output_processed_file=False)
     input_file.save(include_formatting=True, add_suffix="_proc_test_output", output_processed_file=True)
diff --git a/hedtools/examples/run_hed_files_examples.py b/hedtools/examples/run_hed_files_examples.py
index 1919d66a1..73233299f 100644
--- a/hedtools/examples/run_hed_files_examples.py
+++ b/hedtools/examples/run_hed_files_examples.py
@@ -33,7 +33,8 @@
     print(valid_tsv_file)
     input_file = HedFileInput(valid_tsv_file, tag_columns=[2])
     validation_issues = hed_validator_old.validate_input(input_file)
-    print(hed.get_printable_issue_string(validation_issues, title='[Example 1a] ValidTwoColumnHED7_1_1 is probably okay with default version of HED'))
+    the_title = '[Example 1a] ValidTwoColumnHED7_1_1 is probably okay with default version of HED'
+    print(hed.get_printable_issue_string(validation_issues, title=the_title))
 
     # Example 1b: Valid TSV file with specified local version of HED
     print(valid_tsv_file)
@@ -45,7 +46,8 @@
     print(valid_tsv_file_no_header)
     input_file = HedFileInput(valid_tsv_file_no_header, has_column_names=False, tag_columns=[2, 3])
     validation_issues = hed_validator_local.validate_input(input_file)
-    print(hed.get_printable_issue_string(validation_issues, title='[Example 1c] ValidTwoColumnHED7_1_1NoHeaders should have no issues with version 7.1.1'))
+    the_title = '[Example 1c] ValidTwoColumnHED7_1_1NoHeaders should have no issues with version 7.1.1'
+    print(hed.get_printable_issue_string(validation_issues, title=the_title))
 
     # Example 1d: Valid TSV with separate columns for required fields
     print(valid_tsv_file_separate_cols)
diff --git a/hedtools/examples/run_json_validation_and_conversion.py b/hedtools/examples/run_json_validation_and_conversion.py
index e5cb11943..5430ff255 100644
--- a/hedtools/examples/run_json_validation_and_conversion.py
+++ b/hedtools/examples/run_json_validation_and_conversion.py
@@ -12,11 +12,11 @@
 """
 import hed
 from hed.util.column_def_group import ColumnDefGroup
-from hed import schema
+from hed.schema.hed_schema_file import load_schema
 from hed.tools.tag_format import TagFormat
 
 local_hed_xml = "data/HED8.0.0-alpha.1.xml"
-hed_schema = schema.load_schema(local_hed_xml)
+hed_schema = load_schema(local_hed_xml)
 json_filename = "data/both_types_events_errors.json"
 
 # Example 1
diff --git a/hedtools/examples/run_validate_schema.py b/hedtools/examples/run_validate_schema.py
index 695f713b8..1b6897823 100644
--- a/hedtools/examples/run_validate_schema.py
+++ b/hedtools/examples/run_validate_schema.py
@@ -4,17 +4,17 @@
 Functions Demonstrated:
 hed_schema.check_compliance - Checks if a HedSchema object is hed3 compatible.
 """
-
-import hed
+from hed.util.error_reporter import get_printable_issue_string
+from hed.schema.hed_schema_file import load_schema
 
 if __name__ == '__main__':
     # This old schema should produce many issues, including many duplicate terms
     local_hed_xml = "data/HED7.1.1.xml"
-    hed_schema = hed.schema.load_schema(local_hed_xml)
+    hed_schema = load_schema(local_hed_xml)
     issues = hed_schema.check_compliance()
-    print(hed.get_printable_issue_string(issues))
+    print(get_printable_issue_string(issues))
 
     # this should produce fairly minimal issues.
     local_hed_xml = "data/HED8.0.0-alpha.1.xml"
     issues = hed_schema.check_compliance()
-    print(hed.get_printable_issue_string(issues))
+    print(get_printable_issue_string(issues))
diff --git a/hedtools/examples/run_wiki2xml.py b/hedtools/examples/run_wiki2xml.py
index f104cbe18..3b2e1b349 100644
--- a/hedtools/examples/run_wiki2xml.py
+++ b/hedtools/examples/run_wiki2xml.py
@@ -8,12 +8,12 @@
 """
 from shutil import move
 import hed
-from hed import schema
+from hed.schema.hed_schema_file import convert_schema_to_format
 
 if __name__ == '__main__':
     hed_wiki_url = 'https://raw.githubusercontent.com/hed-standard/hed-specification/master/HED-generation2-schema-V2.0.0.mediawiki'
     local_hed_file = None
-    xml_location, errors = schema.convert_schema_to_format(hed_wiki_url, local_hed_file=local_hed_file)
+    xml_location, errors = convert_schema_to_format(hed_wiki_url, local_hed_file=local_hed_file)
     hed.get_printable_issue_string(validation_issues=errors, title="Errors in HED-generation3-schema.mediawiki")
     if xml_location:
         move(xml_location, "outputGen2.xml")
@@ -21,9 +21,9 @@
     try:
         hed_wiki_url = 'https://raw.githubusercontent.com/hed-standard/hed-specification/master/HED-generation3-schema-V2.0.0.mediawiki'
         local_hed_file = None
-        xml_location, errors = schema.convert_schema_to_format(hed_wiki_url, local_hed_file=local_hed_file)
+        xml_location, errors = convert_schema_to_format(hed_wiki_url, local_hed_file=local_hed_file)
         hed.get_printable_issue_string(validation_issues=errors, title="Errors in HED-generation3-schema.mediawiki")
         if xml_location:
             move(xml_location, "outputGen3.xml")
     except KeyError:
-        print("Error: Cannot convert current Hed 3g schema.  Most likely due to attributes section being unsupported.")
\ No newline at end of file
+        print("Error: Cannot convert current Hed 3g schema.  Most likely due to attributes section being unsupported.")
diff --git a/hedtools/hed/__init__.py b/hedtools/hed/__init__.py
index 27f861bee..54d6ff241 100644
--- a/hedtools/hed/__init__.py
+++ b/hedtools/hed/__init__.py
@@ -1,2 +1,12 @@
 from hed.util.error_reporter import get_printable_issue_string
-from hed import schema
+from hed.schema.hed_schema import HedSchema
+from hed.schema import hed_schema_file
+from hed.util import hed_cache
+from hed.util.error_reporter import get_printable_issue_string
+from hed.util import file_util
+from hed.validator.hed_validator import HedValidator
+from hed.util.exceptions import HedFileError
+from hed.util.column_def_group import ColumnDefGroup
+from hed.util.error_types import ErrorContext
+from hed.util.hed_file_input import HedFileInput
+
diff --git a/hedtools/hed/schema/__init__.py b/hedtools/hed/schema/__init__.py
deleted file mode 100644
index edd9b9628..000000000
--- a/hedtools/hed/schema/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from hed.schema.hed_schema import HedSchema
-from hed.schema.hed_schema_file import load_schema, get_hed_xml_version, convert_schema_to_format
-from hed.schema.hed_schema_constants import HedKey
diff --git a/hedtools/hed/schema/wiki2schema.py b/hedtools/hed/schema/wiki2schema.py
index 6035f7037..6d0eca920 100644
--- a/hedtools/hed/schema/wiki2schema.py
+++ b/hedtools/hed/schema/wiki2schema.py
@@ -5,7 +5,7 @@
 from hed.schema import hed_schema_constants as constants
 from hed.schema.hed_schema_constants import HedKey
 from hed.util.exceptions import HedFileError, HedExceptions
-from hed.schema import HedSchema
+from hed.schema.hed_schema import HedSchema
 from hed.schema import schema_validation_util
 
 ROOT_TAG = '\'\'\''
diff --git a/hedtools/hed/schema/xml2schema.py b/hedtools/hed/schema/xml2schema.py
index 5ce2e9f1c..ca6a17619 100644
--- a/hedtools/hed/schema/xml2schema.py
+++ b/hedtools/hed/schema/xml2schema.py
@@ -7,7 +7,7 @@
 from hed.util.exceptions import HedFileError, HedExceptions
 from hed.schema import hed_schema_constants as constants
 from hed.schema.hed_schema_constants import HedKey
-from hed.schema import HedSchema
+from hed.schema.hed_schema import HedSchema
 
 
 class HedSchemaXMLParser:
diff --git a/hedtools/hed/tools/tag_format.py b/hedtools/hed/tools/tag_format.py
index 99fbf76eb..d1a0e5ef7 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, ErrorContext
 from hed.util import hed_string_util
-from hed import schema
+from hed.schema.hed_schema_file import load_schema
 
 
 class TagFormat:
@@ -22,7 +22,7 @@ def __init__(self, hed_file=None, hed_schema=None, error_handler=None):
             Used to report errors.  Uses a default one if none passed in.
         """
         if hed_schema is None:
-            hed_schema = schema.load_schema(hed_file)
+            hed_schema = load_schema(hed_file)
         if error_handler is None:
             error_handler = error_reporter.ErrorHandler()
         self._error_handler = error_handler
diff --git a/hedtools/hed/util/column_def_group.py b/hedtools/hed/util/column_def_group.py
index bf279bdb5..2d7010fa7 100644
--- a/hedtools/hed/util/column_def_group.py
+++ b/hedtools/hed/util/column_def_group.py
@@ -156,7 +156,10 @@ def extract_defs_from_list(column_group_defs, hed_schema):
         def_dicts: [DefDict]
 
         """
-        return [column_group_def.extract_defs(hed_schema, check_for_issues=False) for column_group_def in column_group_defs]
+        if column_group_defs:
+            return [column_group_def.extract_defs(hed_schema) for column_group_def in column_group_defs]
+        else:
+            return []
 
     def hed_string_iter(self, include_position=False):
         """
diff --git a/hedtools/hed/util/event_file_input.py b/hedtools/hed/util/event_file_input.py
index 5496105f2..d2d3d62b6 100644
--- a/hedtools/hed/util/event_file_input.py
+++ b/hedtools/hed/util/event_file_input.py
@@ -52,7 +52,10 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
 
         # This stuff is mostly here to make this easier to use for development
         # you should generally create the DefDict and ColumnDefGroup before reaching this point.
-        self.column_group_defs = ColumnDefGroup.load_multiple_json_files(json_def_files)
+        if json_def_files:
+            self.column_group_defs = ColumnDefGroup.load_multiple_json_files(json_def_files)
+        else:
+            self.column_group_defs = None
         if def_dicts is None:
             self.def_dicts = ColumnDefGroup.extract_defs_from_list(self.column_group_defs, hed_schema)
         else:
diff --git a/hedtools/hed/util/hed_cache.py b/hedtools/hed/util/hed_cache.py
index d6516eb0f..349843b25 100644
--- a/hedtools/hed/util/hed_cache.py
+++ b/hedtools/hed/util/hed_cache.py
@@ -24,7 +24,7 @@
 HED_XML_PREFIX = 'HED'
 HED_XML_EXTENSION = '.xml'
 DEFAULT_HED_LIST_VERSIONS_URL = """https://api.github.com/repos/hed-standard/hed-specification/contents/hedxml"""
-HED_CACHE_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../validator/schema_cache/')
+HED_CACHE_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../validator/hed_cache/')
 TIMESTAMP_FILENAME = "last_update.txt"
 CACHE_TIME_THRESHOLD = 300
 
diff --git a/hedtools/hed/validator/hed_validator.py b/hedtools/hed/validator/hed_validator.py
index 513aa55fe..f6d0cb59d 100644
--- a/hedtools/hed/validator/hed_validator.py
+++ b/hedtools/hed/validator/hed_validator.py
@@ -7,7 +7,8 @@
 from hed.util.error_types import ErrorContext
 from hed.util import hed_cache
 from hed.util import error_reporter
-from hed import schema
+
+from hed.schema.hed_schema_file import load_schema
 from hed.util.hed_string import HedString
 from hed.validator.tag_validator import TagValidator
 from hed.util.hed_file_input import BaseFileInput
@@ -134,7 +135,7 @@ def _get_hed_schema(hed_xml_file, get_specific_version=None):
         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_schema = schema.load_schema(final_hed_xml_file)
+        hed_schema = load_schema(final_hed_xml_file)
         return hed_schema
 
     def _validate_hed_tags_in_file(self, hed_input):
diff --git a/hedtools/hed/validator/tag_validator.py b/hedtools/hed/validator/tag_validator.py
index 9191a2eb9..14c6fec22 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.schema import HedKey
+from hed.schema.hed_schema_constants import HedKey
 
 pluralize = inflect.engine()
 pluralize.defnoun("hertz", "hertz")
diff --git a/hedweb/deploy_hed/requirements.txt b/hedweb/deploy_hed/requirements.txt
index e1d5190d6..cd4b8384e 100644
--- a/hedweb/deploy_hed/requirements.txt
+++ b/hedweb/deploy_hed/requirements.txt
@@ -6,7 +6,7 @@ inflect==5.0.2
 et-xmlfile==1.0.1
 jdcal==1.4.1
 itsdangerous==1.1.0
-Jinja2==2.11.2
+Jinja2==2.11.3
 MarkupSafe==1.1.1
 Werkzeug==1.0.1
 WTForms==2.3.3
diff --git a/hedweb/examples/data/good_dictionary.json b/hedweb/examples/data/good_dictionary.json
new file mode 100644
index 000000000..c6b11aec6
--- /dev/null
+++ b/hedweb/examples/data/good_dictionary.json
@@ -0,0 +1,94 @@
+{
+    "onset": {
+        "Description": "Onset of the event of the computer presentation of the hand image",
+        "Units": "s"
+    },
+    "duration": {
+        "Description": "Duration of the event",
+        "Units": "s"
+    },
+	"cue-start": {
+		"Description": "Subject presses both buttons causing cue bar to start count-down display",
+		"HED": "(Delay/#s, (Description/Subject presses both buttons causing cue bar to start count-down display, (Sensory-event, Visual, Cue, (Rectangle,  (Center-of, Screen), (Size, Decreasing))), (Experimental-participant, (Press, (Left-side, Button)), (Press, (Right-side, Button)))))"
+	},
+	"cue-end": {
+		"Description": "Count-down display bar shrinks to zero size.",
+		"HED": "(Delay/# s, (Description/Count-down display bar shrinks to zero size., (Sensory-event, Visual, Cue)))"
+	},	
+    "trial_type": {
+        "Description": "Which hand was lifted by the subject in this trial.",
+        "Levels": {
+            "left": "Subject raised left hand",
+            "right": "Subject raised right hand"
+        },
+		"HED": {
+			"left": "Description/Subject raised left hand, (Experimental-participant, (Lift, (Left-side, Hand)))",
+			"right": "Description/Subject raised right hand, (Experimental-participant, (Lift, (Right-side, Hand)))"
+		}
+    },
+    "response_time": {
+        "Description": "Reaction time to lift the left or right hand after the countdown reached zero. this is the difference between event onset and cue-end in milliseconds.",
+        "Units": "ms",
+        "HED" : "Delay/# ms"
+    },
+    "stim_file": {
+        "Description": "Stimulus file showing hand presented on screen.",
+		"HED": "Description/Stimulus file showing hand is presented on screen, Sensory-event, Visual,((Image, ID/#),(Center-of, Screen)), Experimental-stimulus"
+    },
+    "value": {
+        "Description": "The code (EEG Marker value) associated with an event ",
+        "Levels": {
+            "1": "Release of left button (i.e., raising left hand)",
+            "2": "Release of right button (i.e., raising right hand)"
+        }
+    },
+    "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, (Experimental-condition, (Random, Predict))), Def/Random-selection",
+            "2": "Description/Stage 2 BCI was trained on data from stage 1, (Definition/Trained-on-random, (Experimental-condition)), Def/Trained-on-random",
+            "3": "Description/Stage 3 BCI was trained on data from stages 1 and 2, (Definition/Trained-on-all, (Experimental-condition)), Def/Trained-on-all"
+		}
+    },
+    "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"
+    },
+    "trial": {
+        "Description": "The current trial within this stage.",
+		"HED": "Experimental-trial, Item-count/#"
+    },
+    "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, (Computational-evidence, (Left-side, Hand))",
+          "right": "Description/BCI predicts participant raised right hand, (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/examples/matlab/getSessionInfo.m b/hedweb/examples/matlab/getSessionInfo.m
new file mode 100644
index 000000000..cd71f52c5
--- /dev/null
+++ b/hedweb/examples/matlab/getSessionInfo.m
@@ -0,0 +1,12 @@
+function [cookie, csrftoken] = getSessionInfo(csrf_url)
+
+    request = matlab.net.http.RequestMessage;
+    uri = matlab.net.URI(csrf_url);
+    response1 = send(request,uri);
+    cookies = response1.getFields('Set-Cookie');
+    cookie = cookies.Value;
+    data = response1.Body.char;
+    csrfIdx = strfind(data,'csrf_token');
+    tmp = data(csrfIdx(1)+length('csrf_token')+1:end);
+    csrftoken = regexp(tmp,'".*?"','match');
+    csrftoken = string(csrftoken{1}(2:end-1));
diff --git a/hedweb/examples/matlab/showServices.m b/hedweb/examples/matlab/showServices.m
new file mode 100644
index 000000000..b63f1a34c
--- /dev/null
+++ b/hedweb/examples/matlab/showServices.m
@@ -0,0 +1,34 @@
+function [] = showServices(result)
+
+if ~isfield(result, 'supported_services')
+    fprintf('No supported_services field found in result\n');
+    return;
+end
+services = result.supported_services;
+fields = fieldnames(services);
+fprintf('Services:\n');
+for k = 1:length(fields)
+    info = services.(fields{k});
+    fprintf('  %s  [%s]\n', info.Name, info.Description);
+    showParameters(info.Parameters)
+    showReturnValues(info.Returns);
+end
+
+
+function showParameters(params)
+    if isempty(params)
+        return;
+    end
+    fprintf('      Parameters:\n');
+    for p = 1:length(params)
+        fprintf('         %s [%s]\n', params(p).Name, params(p).Description);
+    end
+    
+function showReturnValues(returns)
+    if isempty(returns)
+        return;
+    end
+    fprintf('      Returns:\n');
+    for p = 1:length(returns)
+        fprintf('         %s [%s]\n', returns(p).Name, returns(p).Description);
+    end
\ No newline at end of file
diff --git a/hedweb/hed/web/constants/common_constants.py b/hedweb/hed/web/constants/common.py
similarity index 74%
rename from hedweb/hed/web/constants/common_constants.py
rename to hedweb/hed/web/constants/common.py
index 894437100..64b3dc050 100644
--- a/hedweb/hed/web/constants/common_constants.py
+++ b/hedweb/hed/web/constants/common.py
@@ -1,9 +1,12 @@
 CHECK_FOR_WARNINGS = 'check-for-warnings'
 COLUMN_NAMES = 'column-names'
-COLUMNS_NAMES_TABLE = 'columns-names-table'
+COLUMN_NAMES_TABLE = 'column-names-table'
 HAS_COLUMN_NAMES = 'has-column-names'
 COLUMN_POSTFIX = '-column'
 COLUMN_PREFIX_DICTIONARY = 'column-prefix-dictionary'
+COLUMNS_FILE = 'columns-file'
+COLUMNS_PATH = 'columns-path'
+COLUMNS_DISPLAY_NAME = 'columns-display-name'
 TAG_COLUMNS = 'tag-columns'
 TAG_COLUMN_INDICES = 'tag-column-indices'
 
@@ -11,7 +14,10 @@
 JSON_FILE = 'json-file'
 JSON_PATH = 'json-path'
 ERROR_COUNT = 'error-count'
-
+EVENTS_DISPLAY_NAME = 'events-display-name'
+EVENTS_FILE = 'events-file'
+EVENTS_FLASH = 'events-flash'
+EVENTS_PATH = 'events-path'
 EVENTS_VALIDATION_SUBMIT = 'events-validation-submit'
 EVENTS_SUBMIT_FLASH = 'events-validation-submit-flash'
 
@@ -22,10 +28,15 @@
 HED_SCHEMA_FILE = 'hed-schema-file'
 HED_SCHEMA_PATH = 'hed-schema-path'
 HED_SCHEMA_URL = 'hed-schema-url'
-HED_STRINGS = 'hed-strings'
+HED_SELECT_FLASH = 'hed-select-flash'
+HEDSTRING = 'hedstring'
+HEDSTRING_LIST = 'hedstring_list'
+HEDSTRING_OPTION = 'hedstring-option'
+HEDSTRING_TO_LONG = 'hedstring-to-long'
+HEDSTRING_TO_SHORT = 'hedstring-to-short'
+HEDSTRING_VALIDATE = 'hedstring-validate'
 HED_VERSION = 'hed-version'
 HED_XML_FILE = 'hed-xml-file'
-
 ISSUE_COUNT = 'issue-count'
 REQUIRED_TAG_COLUMN_INDICES = 'required-tag-column-indices'
 SPREADSHEET_DISPLAY_NAME = 'spreadsheet-display-name'
@@ -36,7 +47,8 @@
 
 WORKSHEET_NAME = 'worksheet-name'
 WORKSHEET_NAMES = 'worksheet-names'
-
+WORKSHEET_SELECT = 'worksheet-select'
+WORKSHEET_SELECTED = 'worksheet-selected'
 
 # Schema-specific constants
 SCHEMA_FORM = 'schema-form'
@@ -55,16 +67,6 @@
 SCHEMA_URL_DISPLAY_NAME = 'schema-url-display-name'
 SCHEMA_URL_OPTION = 'schema-url-option'
 
-SERVICE_SUBMIT = 'service-submit'
-
 # Type constants
 BOOLEAN = 'boolean'
 STRING = 'string'
-
-
-# Validation-specific constants
-VALIDATION_FORM = 'validation-form'
-VALIDATION_OUTPUT_FILE_PREFIX = 'validation_errors'
-VALIDATION_SUBMIT = 'validation-submit'
-VALIDATION_SUBMIT_FLASH = 'validation-submit-flash'
-
diff --git a/hedweb/hed/web/constants/error_constants.py b/hedweb/hed/web/constants/error_constants.py
index a0cad738d..403a9a4cc 100644
--- a/hedweb/hed/web/constants/error_constants.py
+++ b/hedweb/hed/web/constants/error_constants.py
@@ -1,7 +1,8 @@
-INTERNAL_SERVER_ERROR = 500
 NOT_FOUND_ERROR = 404
 NO_CONTENT_SUCCESS = 204
 ERROR_KEY = 'error'
-FILE_DOES_NOT_EXIST = "File doesn't exist"
-INVALID_URL_ERROR = "Invalid URL.  URL should point to a direct file download."
-NO_URL_CONNECTION_ERROR = "Cannot connect to target URL.  Specified server might be offline or invalid."
+FILE_DOES_NOT_EXIST_NOW = "File doesn't exist"
+
+
+
+
diff --git a/hedweb/hed/web/constants/file_constants.py b/hedweb/hed/web/constants/file_constants.py
index 9b565a556..a59beb38d 100644
--- a/hedweb/hed/web/constants/file_constants.py
+++ b/hedweb/hed/web/constants/file_constants.py
@@ -1,6 +1,8 @@
 TEXT_EXTENSION = '.txt'
 SPREADSHEET_FILE_EXTENSIONS = ['.xls', '.xlsx', '.txt', '.tsv']
+EXCEL_FILE_EXTENSIONS = ['.xls', '.xlsx']
 DICTIONARY_FILE_EXTENSIONS = ['.json']
+TEXT_FILE_EXTENSIONS = ['.txt', '.tsv']
 SCHEMA_WIKI_EXTENSION = '.mediawiki'
 SCHEMA_XML_EXTENSION = '.xml'
 SCHEMA_EXTENSIONS = [SCHEMA_XML_EXTENSION, SCHEMA_WIKI_EXTENSION]
diff --git a/hedweb/hed/web/constants/page_constants.py b/hedweb/hed/web/constants/page_constants.py
index 77129f215..a32af6f14 100644
--- a/hedweb/hed/web/constants/page_constants.py
+++ b/hedweb/hed/web/constants/page_constants.py
@@ -3,6 +3,7 @@
 DICTIONARY_VALIDATION_PAGE = 'dictionary.html'
 EVENTS_PAGE = 'events.html'
 HED_SERVICES_PAGE = 'hed-services.html'
+HEDSTRING_PAGE = 'hedstring.html'
 HED_TOOLS_HELP_PAGE = 'hed-tools-help.html'
 HED_TOOLS_HOME_PAGE = 'hed-tools-home.html'
 SCHEMA_PAGE = 'schema.html'
diff --git a/hedweb/hed/web/constants/route_constants.py b/hedweb/hed/web/constants/route_constants.py
index 9e3ad3279..cd5fd40db 100644
--- a/hedweb/hed/web/constants/route_constants.py
+++ b/hedweb/hed/web/constants/route_constants.py
@@ -1,24 +1,27 @@
 ROUTE_BLUEPRINT = 'route_blueprint'
 
 ADDITIONAL_EXAMPLES_ROUTE = '/additional-examples'
+
+COLUMN_INFO_ROUTE = '/get-columns-info'
+
 COMMON_ERRORS_ROUTE = '/common-errors'
-DELETE_FILE_ROUTE = '/delete/<filename>'
 DICTIONARY_ROUTE = '/dictionary'
 DICTIONARY_VALIDATION_SUBMIT_ROUTE = '/dictionary-validation-submit'
-DOWNLOAD_FILE_ROUTE = '/download-file/<filename>'
 
 EVENTS_ROUTE = '/events'
 EVENTS_VALIDATION_SUBMIT_ROUTE = '/events-validation-submit'
 HED_MAJOR_VERSION_ROUTE = '/get-hed-major-versions'
 HED_SERVICES_SUBMIT_ROUTE = '/hed-services-submit'
 HED_SERVICES_ROUTE = '/hed-services'
+HEDSTRING_ROUTE = '/hedstring'
+HEDSTRING_SUBMIT_ROUTE = '/hedstring-submit'
 HED_TOOLS_HELP_ROUTE = '/hed-tools-help'
 HED_TOOLS_HOME_ROUTE = '/'
 HED_VERSION_ROUTE = '/get-hed-version'
 SCHEMA_ROUTE = '/schema'
 SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE = '/schema-compliance-check-submit'
 SCHEMA_CONVERSION_SUBMIT_ROUTE = '/schema-conversion-submit'
-SPREADSHEET_COLUMN_INFO_ROUTE = '/get-spreadsheet-columns-info'
+
 SPREADSHEET_ROUTE = '/spreadsheet'
 SPREADSHEET_VALIDATION_SUBMIT_ROUTE = '/spreadsheet-validation-submit'
 WORKSHEET_COLUMN_INFO_ROUTE = '/get-worksheets-info'
diff --git a/hedweb/hed/web/dictionary.py b/hedweb/hed/web/dictionary.py
index ce3c4eac2..fe0a5ee0f 100644
--- a/hedweb/hed/web/dictionary.py
+++ b/hedweb/hed/web/dictionary.py
@@ -1,19 +1,17 @@
-from urllib.error import URLError, HTTPError
 from flask import current_app
 
-from hed.schema import HedSchema, load_schema
-from hed.util.error_reporter import get_printable_issue_string
-from hed.util.file_util import delete_file_if_it_exists
 from hed.util.column_def_group import ColumnDefGroup
-from hed.web.constants import common_constants, error_constants, file_constants
+from hed.util.error_reporter import get_printable_issue_string
+from hed.schema.hed_schema_file import load_schema
+from hed.web.constants import common, file_constants
 from hed.web.web_utils import generate_filename, generate_download_file_response, \
-    handle_http_error, get_hed_path_from_pull_down, \
+    generate_text_response, get_hed_path_from_pull_down, \
     get_uploaded_file_path_from_form, save_text_to_upload_folder, get_optional_form_field
 
 app_config = current_app.config
 
 
-def generate_arguments_from_dictionary_form(request):
+def generate_input_from_dictionary_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
@@ -27,50 +25,20 @@ def generate_arguments_from_dictionary_form(request):
         A dictionary containing input arguments for calling the underlying validation function.
     """
     hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
-    uploaded_file_name, original_file_name = get_uploaded_file_path_from_form(request, common_constants.JSON_FILE,
+    uploaded_file_name, original_file_name = get_uploaded_file_path_from_form(request, common.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,
-                       common_constants.CHECK_FOR_WARNINGS: get_optional_form_field(request,
-                                                                                    common_constants.CHECK_FOR_WARNINGS,
-                                                                                    common_constants.BOOLEAN)
-                       }
-
+    input_arguments = {
+        common.HED_XML_FILE: hed_file_path,
+        common.HED_DISPLAY_NAME: hed_display_name,
+        common.JSON_PATH: uploaded_file_name,
+        common.JSON_FILE: original_file_name,
+        common.CHECK_FOR_WARNINGS: get_optional_form_field(request, common.CHECK_FOR_WARNINGS, common.BOOLEAN)
+    }
     return input_arguments
 
 
-def report_dictionary_validation_status(request):
-    """Reports the spreadsheet validation status.
-
-    Parameters
-    ----------
-    request: Request object
-        A Request object containing user data from the validation form.
-
-    Returns
-    -------
-    string
-        A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
-        500 error message is returned.
-    """
-    input_arguments = []
-    try:
-        input_arguments = generate_arguments_from_dictionary_form(request)
-        return validate_dictionary(input_arguments)
-    except HTTPError:
-        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_exists(input_arguments.get(common_constants.JSON_PATH, ''))
-
-
-def validate_dictionary(input_arguments, hed_schema=None, return_response=True):
+def dictionary_validate(input_arguments, hed_schema=None):
     """Validates the dictionary and returns a response or a printable string depending on return_response value
 
     Parameters
@@ -79,29 +47,23 @@ def validate_dictionary(input_arguments, hed_schema=None, return_response=True):
         Dictionary containing standard input form arguments
     hed_schema: str or HedSchema
         Version number or path or HedSchema object to be used
-    return_response: bool
-        If true, return a Response. If false return a printable issue string
 
     Returns
     -------
-    Response or str
-        Either a Response or a printable issue string
+    Response
+        Response object containing the results of the dictionary validation.
     """
 
-    json_dictionary = ColumnDefGroup(input_arguments.get(common_constants.JSON_PATH, ''))
+    json_dictionary = ColumnDefGroup(input_arguments.get(common.JSON_PATH, ''))
     if not hed_schema:
-        hed_schema = load_schema(input_arguments.get(common_constants.HED_XML_FILE, ''))
+        hed_schema = load_schema(input_arguments.get(common.HED_XML_FILE, ''))
     issues = json_dictionary.validate_entries(hed_schema)
     if issues:
-        display_name = input_arguments.get(common_constants.JSON_FILE, '')
+        display_name = input_arguments.get(common.JSON_FILE, '')
         issue_str = get_printable_issue_string(issues, f"HED validation errors for {display_name}")
-        if not return_response:
-            return issue_str
         file_name = generate_filename(display_name, suffix='validation_errors', extension='.txt')
         issue_file = save_text_to_upload_folder(issue_str, file_name)
-        download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
-                                                            msg='JSON dictionary had validation errors')
-        if isinstance(download_response, str):
-            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-        return download_response
-    return ""
+        return generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                               msg='JSON dictionary had validation errors')
+    else:
+        return generate_text_response("", msg='JSON dictionary had no validation errors')
diff --git a/hedweb/hed/web/events.py b/hedweb/hed/web/events.py
index a89ee42c7..a6b3b19b6 100644
--- a/hedweb/hed/web/events.py
+++ b/hedweb/hed/web/events.py
@@ -1,16 +1,14 @@
-
-from urllib.error import URLError, HTTPError
 from flask import current_app
 
-from hed.util.file_util import delete_file_if_it_exists
+from hed.schema.hed_schema_file import load_schema
+from hed.util.column_def_group import ColumnDefGroup
+from hed.util.error_reporter import get_printable_issue_string
+from hed.util.event_file_input import EventFileInput
 from hed.validator.hed_validator import HedValidator
-from hed.schema import hed_schema_file
-
-from hed.web.constants import common_constants, error_constants, file_constants
-from hed.web.dictionary import validate_dictionary
-from hed.web.spreadsheet import validate_spreadsheet
-from hed.web.web_utils import get_hed_path_from_pull_down, get_uploaded_file_path_from_form, get_optional_form_field
+from hed.web.constants import common, file_constants
 
+from hed.web.web_utils import generate_download_file_response, generate_filename, generate_text_response, \
+    get_hed_path_from_pull_down, get_uploaded_file_path_from_form, get_optional_form_field, save_text_to_upload_folder
 app_config = current_app.config
 
 
@@ -28,66 +26,62 @@ def generate_input_from_events_form(request):
         A dictionary containing input arguments for calling the underlying validation function.
     """
     hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
-    uploaded_events_name, original_events_name = \
-        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
-                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
-
+    uploaded_events_path, original_events_name = \
+        get_uploaded_file_path_from_form(request, common.EVENTS_FILE, file_constants.TEXT_FILE_EXTENSIONS)
     uploaded_json_name, original_json_name = \
-        get_uploaded_file_path_from_form(request, common_constants.JSON_FILE,
-                                         file_constants.DICTIONARY_FILE_EXTENSIONS)
+        get_uploaded_file_path_from_form(request, common.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.SPREADSHEET_PATH: uploaded_events_name,
-        common_constants.SPREADSHEET_FILE: original_events_name,
-        common_constants.JSON_PATH: uploaded_json_name,
-        common_constants.JSON_FILE: original_json_name,
-        common_constants.WORKSHEET_NAME: get_optional_form_field(request, common_constants.WORKSHEET_NAME,
-                                                                 common_constants.STRING),
-        common_constants.HAS_COLUMN_NAMES: get_optional_form_field(request,
-                                                                   common_constants.HAS_COLUMN_NAMES,
-                                                                   common_constants.BOOLEAN),
-        common_constants.CHECK_FOR_WARNINGS: get_optional_form_field(request,
-                                                                     common_constants.CHECK_FOR_WARNINGS,
-                                                                     common_constants.BOOLEAN)
+        common.HED_XML_FILE: hed_file_path,
+        common.HED_DISPLAY_NAME: hed_display_name,
+        common.EVENTS_PATH: uploaded_events_path,
+        common.EVENTS_FILE: original_events_name,
+        common.JSON_PATH: uploaded_json_name,
+        common.JSON_FILE: original_json_name,
+        common.HAS_COLUMN_NAMES: get_optional_form_field(request, common.HAS_COLUMN_NAMES, common.BOOLEAN),
+        common.CHECK_FOR_WARNINGS: get_optional_form_field(request, common.CHECK_FOR_WARNINGS, common.BOOLEAN)
     }
     return input_arguments
 
 
-def report_events_validation_status(request):
+def events_validate(input_arguments):
     """Reports the spreadsheet validation status.
 
     Parameters
     ----------
-    request: Request object
-        A Request object containing user data from the validation form.
+    input_arguments: dict
+        A dictionary of the values extracted from the form
 
     Returns
     -------
-    string
-        A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
-        500 error message is returned.
+    Response
+         The validation results as a Response object
     """
-    input_arguments = []
-    try:
-        input_arguments = generate_input_from_events_form(request)
-        hed_schema = hed_schema_file.load_schema(input_arguments.get(common_constants.HED_XML_FILE, ''))
-        download_response = validate_dictionary(input_arguments, hed_schema=hed_schema)
-        if download_response:
-            return download_response
-        hed_validator = HedValidator(hed_schema=hed_schema,
-                                     check_for_warnings=input_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
-        download_response = validate_spreadsheet(input_arguments, hed_validator)
-        if download_response:
-            return download_response
-    except HTTPError:
-        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_exists(input_arguments.get(common_constants.SPREADSHEET_PATH, ''))
-        delete_file_if_it_exists(input_arguments.get(common_constants.JSON_PATH, ''))
-    return ""
+
+    hed_schema = load_schema(input_arguments.get(common.HED_XML_FILE, ''))
+    json_sidecar = None
+    if input_arguments.get(common.JSON_PATH, ''):  # If dictionary is provided and it has errors return those errors
+        json_sidecar = ColumnDefGroup(input_arguments.get(common.JSON_PATH, ''))
+        issues = json_sidecar.validate_entries(hed_schema)
+        if issues:
+            issue_str = get_printable_issue_string(issues, f"{common.JSON_FILE} HED validation errors")
+            file_name = generate_filename(common.JSON_FILE, suffix='_validation_errors', extension='.txt')
+            issue_file = save_text_to_upload_folder(issue_str, file_name)
+            return generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                   msg='JSON dictionary had validation errors')
+
+    input_file = EventFileInput(input_arguments.get(common.EVENTS_PATH),
+                                json_def_files=json_sidecar, hed_schema=hed_schema)
+    validator = HedValidator(hed_schema=hed_schema,
+                             check_for_warnings=input_arguments.get(common.CHECK_FOR_WARNINGS, False))
+    issues = validator.validate_input(input_file)
+    if issues:
+        display_name = input_arguments.get(common.EVENTS_FILE, None)
+        issue_str = get_printable_issue_string(issues, f"{display_name} HED validation errors")
+
+        file_name = generate_filename(display_name, suffix='_validation_errors', extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        return generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                               msg='Events file had validation errors')
+    else:
+        return generate_text_response("", msg='Events file had no validation errors')
diff --git a/hedweb/hed/web/hedstring.py b/hedweb/hed/web/hedstring.py
new file mode 100644
index 000000000..ae30a5d72
--- /dev/null
+++ b/hedweb/hed/web/hedstring.py
@@ -0,0 +1,116 @@
+from flask import current_app
+
+from hed.tools.tag_format import TagFormat
+from hed.util.error_reporter import get_printable_issue_string
+from hed.validator.hed_validator import HedValidator
+from hed.web.constants import common
+from hed.web.web_utils import form_has_option, get_hed_path_from_pull_down
+from hed.web.web_exceptions import HedError
+app_config = current_app.config
+
+
+def generate_input_from_hedstring_form(request):
+    """Gets the validation function input arguments from a request object associated with the validation form.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+
+    Returns
+    -------
+    dictionary
+        A dictionary containing input arguments for calling the underlying validation function.
+    """
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
+    input_arguments = {common.HED_XML_FILE: hed_file_path,
+                       common.HED_DISPLAY_NAME: hed_display_name,
+                       common.HEDSTRING: request.form['hedstring-input'],
+                       common.HEDSTRING_VALIDATE: False,
+                       common.HEDSTRING_TO_SHORT: False,
+                       common.HEDSTRING_TO_LONG: False}
+    if form_has_option(request, common.HEDSTRING_OPTION, common.HEDSTRING_VALIDATE):
+        input_arguments[common.HEDSTRING_VALIDATE] = True
+    elif form_has_option(request, common.HEDSTRING_OPTION, common.HEDSTRING_TO_SHORT):
+        input_arguments[common.HEDSTRING_TO_SHORT] = True
+    elif form_has_option(request, common.HEDSTRING_OPTION, common.HEDSTRING_TO_LONG):
+        input_arguments[common.HEDSTRING_TO_LONG] = True
+    return input_arguments
+
+
+def hedstring_process(input_arguments):
+    """Perform the requested action on the HED string.
+
+    Parameters
+    ----------
+    input_arguments: dict
+        A dictionary with the input arguments from the hedstring form
+
+    Returns
+    -------
+    string
+        A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
+        500 error message is returned.
+    """
+
+    if not input_arguments[common.HEDSTRING]:
+        raise HedError('EmptyHEDString', "Please enter a nonempty HED string to process")
+    if input_arguments[common.HEDSTRING_VALIDATE]:
+        return hedstring_validate(input_arguments)
+    elif input_arguments[common.HEDSTRING_TO_SHORT]:
+        return hedstring_convert(input_arguments, short_to_long=False)
+    elif input_arguments[common.HEDSTRING_TO_LONG]:
+        return hedstring_convert(input_arguments)
+    else:
+        raise HedError('UnknownProcessingMethod', "Select a hedstring processing method")
+
+
+def hedstring_convert(input_arguments, short_to_long=True):
+    """Converts a hedstring from short to long unless short_to_long is set to False, then long_to_short
+
+    Parameters
+    ----------
+    input_arguments: dict
+        Dictionary containing standard input form arguments
+    short_to_long: bool
+        If True convert the hedstring
+    Returns
+    -------
+    dict
+        A dictionary with hedstring-results as the key
+    """
+
+    hed_file_path = input_arguments.get(common.HED_XML_FILE)
+    tag_formatter = TagFormat(hed_file_path)
+    if short_to_long:
+        return_str, issues = tag_formatter.convert_hed_string_to_long(input_arguments.get(common.HEDSTRING))
+    else:
+        return_str, issues = tag_formatter.convert_hed_string_to_short(input_arguments.get(common.HEDSTRING))
+
+    if issues:
+        return_str = get_printable_issue_string(issues, "Unable to convert HED string:")
+    return {'hedstring-result': return_str}
+
+
+def hedstring_validate(input_arguments):
+    """Validates a hedstring and returns a dictionary containing the issues or a no errors message
+
+    Parameters
+    ----------
+    input_arguments: dict
+        Dictionary containing standard input form arguments
+
+    Returns
+    -------
+    dict
+        A dictionary with hedstring-results as the key
+    """
+
+    hed_file_path = input_arguments.get(common.HED_XML_FILE)
+    hed_validator = HedValidator(hed_xml_file=hed_file_path)
+    issues = hed_validator.validate_input(input_arguments.get(common.HEDSTRING))
+    if issues:
+        return_str = get_printable_issue_string(issues, "HED validation errors:")
+    else:
+        return_str = "No validation errors found..."
+    return {'hedstring-result': return_str}
diff --git a/hedweb/hed/web/routes.py b/hedweb/hed/web/routes.py
index 3c310b783..6d7141d09 100644
--- a/hedweb/hed/web/routes.py
+++ b/hedweb/hed/web/routes.py
@@ -1,60 +1,43 @@
 from flask import render_template, Response, request, Blueprint, current_app
-import os
 import json
-import traceback
 
 from hed.util import hed_cache
 from hed.schema import hed_schema_file
-from hed.web import events, spreadsheet, schema, services, spreadsheet_utils
-from hed.web.constants import common_constants, error_constants, page_constants, route_constants
-from hed.web.web_utils import delete_file_if_it_exists, save_file_to_upload_folder, \
-    generate_download_file_response, handle_http_error
-from hed.web.dictionary import report_dictionary_validation_status
+from hed.web.constants import common, page_constants, route_constants
+from hed.web.web_utils import delete_file_no_exceptions, \
+   handle_http_error, handle_error, save_file_to_upload_folder
+from hed.web import dictionary, events, schema, spreadsheet, services
+from hed.web.hedstring import generate_input_from_hedstring_form, hedstring_process
+from hed.web.spreadsheet_utils import generate_input_columns_info, get_columns_info
 
 app_config = current_app.config
 route_blueprint = Blueprint(route_constants.ROUTE_BLUEPRINT, __name__)
 
 
-@route_blueprint.route(route_constants.DELETE_FILE_ROUTE, strict_slashes=False, methods=['GET'])
-def delete_file_in_upload_directory(filename):
-    """Deletes the specified other from the upload other.
-
-    Parameters
-    ----------
-    filename: string
-        The name of the other to delete from the upload other.
-
-    Returns
-    -------
-
-    """
-    if delete_file_if_it_exists(os.path.join(app_config['UPLOAD_FOLDER'], filename)):
-        return Response(status=error_constants.NO_CONTENT_SUCCESS)
-    else:
-        return handle_http_error(error_constants.NOT_FOUND_ERROR, error_constants.FILE_DOES_NOT_EXIST)
-
+@route_blueprint.route(route_constants.COLUMN_INFO_ROUTE, methods=['POST'])
+def get_columns_info_results():
+    """Gets information related to the spreadsheet columns.
 
-@route_blueprint.route(route_constants.DOWNLOAD_FILE_ROUTE, strict_slashes=False, methods=['GET'])
-def download_file_in_upload_directory(filename, header=None):
-    """Downloads the specified file from the download folder.
+    This information contains the names of the spreadsheet columns and column indices that contain HED tags.
 
     Parameters
     ----------
-    filename: str
-        The name of the file to download from the upload other.
-    header: str
-        Header string to be inserted in text files --- particularly files reporting errors or status
 
     Returns
     -------
-    File
-        The contents of a other in the upload directory to send to the client.
+    string
+        A serialized JSON string containing information related to the spreadsheet columns.
 
     """
-    download_response = generate_download_file_response(filename, header)
-    if not download_response or isinstance(download_response, str):
-        handle_http_error(error_constants.NOT_FOUND_ERROR, download_response, as_text=True)
-    return download_response
+    input_arguments = {}
+    try:
+        input_arguments = generate_input_columns_info(request)
+        columns_info = get_columns_info(input_arguments)
+        return json.dumps(columns_info)
+    except Exception as ex:
+        return handle_error(ex)
+    finally:
+        delete_file_no_exceptions(input_arguments.get(common.COLUMNS_PATH, ''))
 
 
 @route_blueprint.route(route_constants.DICTIONARY_VALIDATION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
@@ -69,8 +52,14 @@ def get_dictionary_validation_results():
         download file
         A text file with the validation errors.
     """
-    validation_response = report_dictionary_validation_status(request)
-    return validation_response
+    input_arguments = {}
+    try:
+        input_arguments = dictionary.generate_input_from_dictionary_form(request)
+        return dictionary.dictionary_validate(input_arguments)
+    except Exception as ex:
+        return handle_http_error(ex)
+    finally:
+        delete_file_no_exceptions(input_arguments.get(common.JSON_PATH, ''))
 
 
 @route_blueprint.route(route_constants.EVENTS_VALIDATION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
@@ -86,15 +75,16 @@ def get_events_validation_results():
         A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
         500 error message is returned.
     """
-    validation_response = events.report_events_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 ""
+    input_arguments = {}
+    try:
+        input_arguments = events.generate_input_from_events_form(request)
+        a = events.events_validate(input_arguments)
+        return a
+    except Exception as ex:
+        return handle_http_error(ex)
+    finally:
+        delete_file_no_exceptions(input_arguments.get(common.SPREADSHEET_PATH, ''))
+        delete_file_no_exceptions(input_arguments.get(common.JSON_PATH, ''))
 
 
 @route_blueprint.route(route_constants.HED_SERVICES_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
@@ -108,14 +98,36 @@ def get_hed_services_results():
     Returns
     -------
         string
-        A serialized JSON string containing information related to the EEG events' hed-strings.
+        A serialized JSON string containing information related to the EEG events' hedstrings.
         If the validation fails then a 500 error message is returned.
     """
-    status = services.report_services_status(request)
+    try:
+        form_data = request.data
+        status = services.report_services_status(form_data)
+        return json.dumps(status)
+    except Exception as ex:
+        return handle_error(ex)
 
-    if error_constants.ERROR_KEY in status:
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, status[error_constants.ERROR_KEY])
-    return json.dumps(status)
+
+@route_blueprint.route(route_constants.HEDSTRING_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_hedstring_results():
+    """Process hed strings entered in a text box.
+
+    Parameters
+    ----------
+
+    Returns
+    -------
+        Response
+
+    """
+    # return hedstring_process(request)
+
+    try:
+        input_arguments = generate_input_from_hedstring_form(request)
+        return json.dumps(hedstring_process(input_arguments))
+    except Exception as ex:
+        return handle_error(ex)
 
 
 @route_blueprint.route(route_constants.HED_VERSION_ROUTE, methods=['POST'])
@@ -131,21 +143,22 @@ def get_hed_version():
         A serialized JSON string containing information related to the spreadsheet columns.
 
     """
-    hed_info = {}
+
     try:
-        if common_constants.HED_XML_FILE in request.files:
-            hed_file_path = save_file_to_upload_folder(request.files[common_constants.HED_XML_FILE])
-            hed_info[common_constants.HED_VERSION] = hed_schema_file.get_hed_xml_version(hed_file_path)
-    except:
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, traceback.format_exc())
-    return json.dumps(hed_info)
+        hed_info = {}
+        if common.HED_XML_FILE in request.files:
+            hed_file_path = save_file_to_upload_folder(request.files[common.HED_XML_FILE])
+            hed_info[common.HED_VERSION] = hed_schema_file.get_hed_xml_version(hed_file_path)
+        return json.dumps(hed_info)
+    except Exception as ex:
+        return handle_error(ex)
+    finally:
+        delete_file_no_exceptions(request.files[common.HED_XML_FILE])
 
 
 @route_blueprint.route(route_constants.HED_MAJOR_VERSION_ROUTE, methods=['GET'])
 def get_major_hed_versions():
-    """Gets information related to the spreadsheet columns.
-
-    This information contains the names of the spreadsheet columns and column indices that contain HED tags.
+    """Gets a list of major hed versions from the hed_cache and returns as a serialized JSON string
 
     Parameters
     ----------
@@ -153,16 +166,16 @@ def get_major_hed_versions():
     Returns
     -------
     string
-        A serialized JSON string containing information related to the spreadsheet columns.
+        A serialized JSON string containing a list of the HED versions.
 
     """
-    hed_info = {}
+
     try:
         hed_cache.cache_all_hed_xml_versions()
-        hed_info[common_constants.HED_MAJOR_VERSIONS] = hed_cache.get_all_hed_versions()
-    except:
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, traceback.format_exc())
-    return json.dumps(hed_info)
+        hed_info = {common.HED_MAJOR_VERSIONS: hed_cache.get_all_hed_versions()}
+        return json.dumps(hed_info)
+    except Exception as ex:
+        return handle_error(ex)
 
 
 @route_blueprint.route(route_constants.SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
@@ -178,18 +191,14 @@ def get_schema_compliance_check_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_compliance_check(request)
-    # Success
-    if isinstance(comparison_response, Response):
-        return comparison_response
-    if isinstance(comparison_response, str):
-        if comparison_response:
-            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, comparison_response, as_text=True)
-        else:
-            return ""
-
-    return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
-                             "Invalid duplicate tag check. This should not happen.", as_text=True)
+    input_arguments = {}
+    try:
+        input_arguments = schema.generate_input_from_schema_form(request)
+        return schema.schema_check(input_arguments)
+    except Exception as ex:
+        return handle_http_error(ex)
+    finally:
+        delete_file_no_exceptions(input_arguments.get(common.SCHEMA_PATH, ''))
 
 
 @route_blueprint.route(route_constants.SCHEMA_CONVERSION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
@@ -205,38 +214,14 @@ def get_schema_conversion_results():
         A serialized JSON string containing information related file to convert. If the conversion fails then a
         500 error message is returned.
     """
-    conversion_response = schema.run_schema_conversion(request)
-    # Success
-    if isinstance(conversion_response, Response):
-        return conversion_response
-    if isinstance(conversion_response, str):
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, conversion_response, as_text=True)
-
-    return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
-                             "Invalid response type in get_duplicate_tag_results.  This should not happen.",
-                             as_text=True)
-
-
-@route_blueprint.route(route_constants.SPREADSHEET_COLUMN_INFO_ROUTE, methods=['POST'])
-def get_spreadsheet_columns_info():
-    """Gets information related to the spreadsheet columns.
-
-    This information contains the names of the spreadsheet columns and column indices that contain HED tags.
-
-    Parameters
-    ----------
-
-    Returns
-    -------
-    string
-        A serialized JSON string containing information related to the spreadsheet columns.
-
-    """
-    spreadsheet_columns_info = spreadsheet_utils.find_spreadsheet_columns_info(request)
-    if error_constants.ERROR_KEY in spreadsheet_columns_info:
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
-                                 spreadsheet_columns_info[error_constants.ERROR_KEY])
-    return json.dumps(spreadsheet_columns_info)
+    input_arguments = {}
+    try:
+        input_arguments = schema.generate_input_from_schema_form(request)
+        return schema.schema_convert(input_arguments)
+    except Exception as ex:
+        return handle_http_error(ex)
+    finally:
+        delete_file_no_exceptions(input_arguments.get(common.SCHEMA_PATH, ''))
 
 
 @route_blueprint.route(route_constants.SPREADSHEET_VALIDATION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
@@ -252,47 +237,14 @@ def get_spreadsheet_validation_results():
         A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
         500 error message is returned.
     """
-    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 ""
-
-
-@route_blueprint.route(route_constants.WORKSHEET_COLUMN_INFO_ROUTE, methods=['POST'])
-def get_worksheets_info():
-    """Gets information related to the Excel worksheets.
-
-    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.
-
-    Parameters
-    ----------
-
-    Returns
-    -------
-    string
-        A serialized JSON string containing information related to the Excel worksheets.
-
-    """
-    worksheets_info = {common_constants.WORKSHEET_NAMES: [], common_constants.COLUMN_NAMES: [],
-                       common_constants.TAG_COLUMN_INDICES: []}
-    workbook_file_path = ''
+    input_arguments = {}
     try:
-        if common_constants.SPREADSHEET_FILE in request.files:
-            workbook_file = request.files[common_constants.SPREADSHEET_FILE]
-            workbook_file_path = save_file_to_upload_folder(workbook_file)
-            if workbook_file_path:
-                worksheets_info = spreadsheet_utils.populate_worksheets_info_dictionary(worksheets_info, workbook_file_path)
-    except:
-        worksheets_info[error_constants.ERROR_KEY] = traceback.format_exc()
+        input_arguments = spreadsheet.generate_input_from_spreadsheet_form(request)
+        return spreadsheet.spreadsheet_validate(input_arguments)
+    except Exception as ex:
+        return handle_http_error(ex)
     finally:
-        delete_file_if_it_exists(workbook_file_path)
-    return json.dumps(worksheets_info)
+        delete_file_no_exceptions(input_arguments.get(common.SPREADSHEET_PATH, ''))
 
 
 @route_blueprint.route(route_constants.ADDITIONAL_EXAMPLES_ROUTE, strict_slashes=False, methods=['GET'])
@@ -379,6 +331,23 @@ def render_hed_services_form():
     return render_template(page_constants.HED_SERVICES_PAGE)
 
 
+@route_blueprint.route(route_constants.HEDSTRING_ROUTE, strict_slashes=False, methods=['GET'])
+def render_hedstring_form():
+    """Renders a form for different hedstring operations.
+
+    Parameters
+    ----------
+
+    Returns
+    -------
+    Rendered template
+        A rendered template for the hed_string_form. If the HTTP method is a GET then the form will be
+        displayed. If the HTTP method is a POST then the form is submitted.
+
+    """
+    return render_template(page_constants.HEDSTRING_PAGE)
+
+
 @route_blueprint.route(route_constants.HED_TOOLS_HELP_ROUTE, strict_slashes=False, methods=['GET'])
 def render_help_page():
     """Handles the site help page.
diff --git a/hedweb/hed/web/schema.py b/hedweb/hed/web/schema.py
index 16f27c6bd..2df2ae099 100644
--- a/hedweb/hed/web/schema.py
+++ b/hedweb/hed/web/schema.py
@@ -1,19 +1,16 @@
 from os.path import basename, splitext
-import traceback
-from urllib.error import URLError, HTTPError
 from urllib.parse import urlparse
-from flask import current_app, Response
+from flask import current_app
 
 from hed.schema.hed_schema_file import load_schema, convert_schema_to_format
 from hed.util.error_reporter import get_printable_issue_string
-from hed.util.file_util import delete_file_if_it_exists, url_to_file, get_file_extension
-from hed.util.exceptions import HedFileError
+from hed.util.file_util import url_to_file, get_file_extension
 
 from hed.web.web_utils import form_has_file, form_has_option, form_has_url, \
-    generate_download_file_response, generate_filename,  \
-    handle_http_error, save_file_to_upload_folder, save_text_to_upload_folder
+    generate_download_file_response, generate_filename, generate_text_response, \
+    save_file_to_upload_folder, save_text_to_upload_folder
 
-from hed.web.constants import common_constants, error_constants, file_constants
+from hed.web.constants import common, file_constants
 
 app_config = current_app.config
 
@@ -33,23 +30,23 @@ def generate_input_from_schema_form(request):
     """
     arguments = {}
    
-    if form_has_option(request, common_constants.SCHEMA_UPLOAD_OPTIONS,
-                       common_constants.SCHEMA_FILE_OPTION) and \
-            form_has_file(request, common_constants.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
-        schema_file = request.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_option(request, common_constants.SCHEMA_UPLOAD_OPTIONS,
-                         common_constants.SCHEMA_URL_OPTION) and \
-            form_has_url(request, common_constants.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
-        schema_url = request.values[common_constants.SCHEMA_URL]
-        arguments[common_constants.SCHEMA_PATH] = url_to_file(schema_url)
+    if form_has_option(request, common.SCHEMA_UPLOAD_OPTIONS,
+                       common.SCHEMA_FILE_OPTION) and \
+            form_has_file(request, common.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
+        schema_file = request.files[common.SCHEMA_FILE]
+        arguments[common.SCHEMA_PATH] = save_file_to_upload_folder(schema_file)
+        arguments[common.SCHEMA_DISPLAY_NAME] = schema_file.filename
+    elif form_has_option(request, common.SCHEMA_UPLOAD_OPTIONS,
+                         common.SCHEMA_URL_OPTION) and \
+            form_has_url(request, common.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
+        schema_url = request.values[common.SCHEMA_URL]
+        arguments[common.SCHEMA_PATH] = url_to_file(schema_url)
         url_parsed = urlparse(schema_url)
-        arguments[common_constants.SCHEMA_DISPLAY_NAME] = basename(url_parsed.path)
+        arguments[common.SCHEMA_DISPLAY_NAME] = basename(url_parsed.path)
     return arguments
 
 
-def run_schema_compliance_check(request):
+def schema_check(input_arguments):
     """Run tag comparison(map_schema from converter)
 
     returns: Response or string.
@@ -57,76 +54,42 @@ def run_schema_compliance_check(request):
         Non empty string is an error
         Response is a download success.
     """
-    hed_file_path = ''
-    try:
-        input_arguments = generate_input_from_schema_form(request)
-        hed_file_path = input_arguments.get(common_constants.SCHEMA_PATH, '')
-        this_schema = load_schema(hed_file_path)
-        issues = this_schema.check_compliance()
-        if issues:
-            display_name = input_arguments.get(common_constants.SCHEMA_DISPLAY_NAME)
-            issue_str = get_printable_issue_string(issues, f"Schema HED 3G compliance errors for {display_name}")
-            file_name = generate_filename(display_name, suffix='schema_3G_compliance_errors', extension='.txt')
-            issue_file = save_text_to_upload_folder(issue_str, file_name)
-            download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
-                                                                msg='Schema is not HED 3G compliant')
-            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 HedFileError as e:
-        return e.message
-    except Exception as e:
-        return "Unexpected processing error: " + str(e)
-    finally:
-        delete_file_if_it_exists(hed_file_path)
-    return ""
 
+    hed_file_path = input_arguments.get(common.SCHEMA_PATH, '')
+    this_schema = load_schema(hed_file_path)
+    issues = this_schema.check_compliance()
+    if issues:
+        display_name = input_arguments.get(common.SCHEMA_DISPLAY_NAME)
+        issue_str = get_printable_issue_string(issues, f"Schema HED 3G compliance errors for {display_name}")
+        file_name = generate_filename(display_name, suffix='schema_3G_compliance_errors', extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        return generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                               msg='Schema is not HED 3G compliant')
+    else:
+        return generate_text_response("", msg='Schema had no HED-3G validation errors')
 
-def run_schema_conversion(request):
+
+def schema_convert(input_arguments):
     """Run conversion(wiki2xml or xml2wiki from converter)
 
     returns: Response or string.
         Non empty string is an error
         Response is a download success.
     """
-    schema_file_path = ''
-    try:
-        input_arguments = generate_input_from_schema_form(request)
-        schema_file_path = input_arguments.get(common_constants.SCHEMA_PATH)
-        display_name = input_arguments.get(common_constants.SCHEMA_DISPLAY_NAME)
-        input_extension = get_file_extension(schema_file_path)
-        save_wiki = input_extension == file_constants.SCHEMA_XML_EXTENSION
-        schema_file, issues = convert_schema_to_format(local_hed_file=schema_file_path, check_for_issues=False,
-                                                       display_filename=display_name, save_as_mediawiki=save_wiki)
-        if issues:
-            issue_str = get_printable_issue_string(issues, f"Schema conversion errors for {display_name}")
-            file_name = generate_filename(display_name, suffix='conversion_errors', extension='.txt')
-            issue_file = save_text_to_upload_folder(issue_str, file_name)
-            download_response = \
-                generate_download_file_response(issue_file, display_name=file_name, category='warning',
-                                                msg='Schema had validation errors and could not be converted')
-        else:
-            schema_name, schema_ext = splitext(schema_file)
-            file_name = generate_filename(display_name,  extension=schema_ext)
-            download_response = generate_download_file_response(schema_file, display_name=file_name, category='success',
-                                                                msg='Schema was successfully converted')
-        if isinstance(download_response, Response):
-            return download_response
-        elif isinstance(download_response, str):
-            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-        else:
-            raise Exception("Unable to download schema file for unknown reasons")
-    except HedFileError as e:
-        return e.message
-    except HTTPError:
-        return error_constants.NO_URL_CONNECTION_ERROR
-    except URLError:
-        return error_constants.INVALID_URL_ERROR
-    except:
-        return traceback.format_exc()
-    finally:
-        delete_file_if_it_exists(schema_file_path)
+    schema_file_path = input_arguments.get(common.SCHEMA_PATH)
+    display_name = input_arguments.get(common.SCHEMA_DISPLAY_NAME)
+    input_extension = get_file_extension(schema_file_path)
+    save_wiki = input_extension == file_constants.SCHEMA_XML_EXTENSION
+    schema_file, issues = convert_schema_to_format(local_hed_file=schema_file_path, check_for_issues=False,
+                                                   display_filename=display_name, save_as_mediawiki=save_wiki)
+    if issues:
+        issue_str = get_printable_issue_string(issues, f"Schema conversion errors for {display_name}")
+        file_name = generate_filename(display_name, suffix='conversion_errors', extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        return generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                               msg='Schema had validation errors and could not be converted')
+    else:
+        schema_name, schema_ext = splitext(schema_file)
+        file_name = generate_filename(display_name,  extension=schema_ext)
+        return generate_download_file_response(schema_file, display_name=file_name, category='success',
+                                               msg='Schema was successfully converted')
diff --git a/hedweb/hed/web/services.py b/hedweb/hed/web/services.py
index 5dd92f016..eb26f4211 100644
--- a/hedweb/hed/web/services.py
+++ b/hedweb/hed/web/services.py
@@ -1,26 +1,24 @@
 import os
 import json
-import traceback
-from flask import current_app
 
+from flask import current_app
 from hed.schema import hed_schema_file
 from hed.util import hed_cache
-from hed.util.file_util import delete_file_if_it_exists
+from hed.util.error_reporter import get_printable_issue_string
 from hed.validator.hed_validator import HedValidator
 from hed.util.column_def_group import ColumnDefGroup
-from hed.util.error_types import ErrorContext
 
 
 app_config = current_app.config
 
 
-def report_services_status(request):
+def report_services_status(form_data):
     """
     Reports validation status of hed strings associated with EEG events received from EEGLAB plugin HEDTools
 
     Parameters
     ----------
-    request: Request object
+    form_data: JSON
         A Request object containing user data submitted by HEDTools.
         Keys include "hed_strings", "check_for_warnings", and "hed_xml_file"
 
@@ -30,32 +28,25 @@ def report_services_status(request):
         A serialized JSON string containing information related to the hed strings validation result.
         If the validation fails then a 500 error message is returned.
     """
-    response = {"result": "", "errors": "", "error_details": ""}
-    hed_xml_file = ''
-    try:
-        form_data = request.data
-        form_string = form_data.decode()
-        param_dict = json.loads(form_string)
-        service = param_dict.get("service", "")
-        supported_services = get_services()
-        if not service:
-            response["errors"] = "Must specify a service"
-        elif service not in supported_services.keys():
-            response["errors"] = f"{service} not supported"
-        elif service == "get_services":
-            response["result"] = {"supported_services": supported_services}
-        elif service == "validate_json":
-            response["result"] = get_validate_dictionary(param_dict)
-        elif service == "validate_strings":
-            response["result"] = get_validate_strings(param_dict)
-        else:
-            response["errors"] = f"{service} not implemented"
-    except:
-        response["error_details"] = traceback.format_exc()
-        response["errors"] = "An exception thrown during execution of service"
-    finally:
-        delete_file_if_it_exists(hed_xml_file)
-
+    response = {"result": "", "error_type": "", "message": ""}
+    form_string = form_data.decode()
+    param_dict = json.loads(form_string)
+    service = param_dict.get("service", "")
+    supported_services = get_services()
+    if not service:
+        response["error_type"] = 'HEDServiceMissing'
+        response["message"] = "Must specify a valid service"
+    elif service not in supported_services.keys():
+        response["error_type"] = 'HEDServiceNotSupported'
+        response["message"] = f"{service} not supported"
+    elif service == "get_services":
+        response["result"] = {"supported_services": supported_services}
+    elif service == "validate_json":
+        response["result"] = get_validate_dictionary(param_dict)
+    elif service == "validate_strings":
+        response["result"] = get_validate_strings(param_dict)
+    else:
+        response["errors"] = f"{service} not implemented"
     return response
 
 
@@ -88,23 +79,11 @@ def get_validate_dictionary(params):
     json_text = params.get("json_dictionary", "")
     json_dictionary = ColumnDefGroup(json_string=json_text)
     issues = json_dictionary.validate_entries(hed_schema)
-    issue_list = []
-    for issue in issues:
-        code = issue.get('code', "")
-        message = issue.get('message', "")
-        column_name = issue.get(ErrorContext.SIDECAR_COLUMN_NAME, "")
-        if column_name:
-            column_name = column_name[0]
-        key_name = issue.get(ErrorContext.SIDECAR_KEY_NAME, "")
-        if key_name:
-            key_name = key_name[0]
-        hed_string = issue.get(ErrorContext.SIDECAR_HED_STRING, "")
-        if hed_string:
-            hed_string = hed_string[0]
-        issue_dict = {"code": code, "message": message, "column_name": column_name,
-                      "key_name": key_name, "hed_string": hed_string}
-        issue_list.append(issue_dict)
-    result = {'hed_version': hed, 'validation_errors': issue_list}
+    if issues:
+        issue_str = get_printable_issue_string(issues, f"HED validation errors")
+    else:
+        issue_str = ''
+    result = {'hed_version': hed, 'validation_errors': issue_str}
     return result
 
 
@@ -127,11 +106,13 @@ def get_validate_strings(params):
     hed_file_path = hed_cache.get_path_from_hed_version(hed)
     hed_strings = params.get("hed_strings", "")
     hed_validator = HedValidator(hed_xml_file=hed_file_path)
-    validation_issues = {}
-    str_num = 1
-    for hed_string in hed_strings:
-        issue = hed_validator.validate_input(hed_string)
+    issues = hed_validator.validate_input(hed_strings)
+    validation_errors = []
+    for i in range(len(hed_strings)):
+        issue = issues[i]
         if issue:
-            validation_issues[f"String_{str_num}"] = issue
-        str_num += 1
-    return {'hed_version': hed, 'validation_errors': validation_issues}
+            validation_errors.append(get_printable_issue_string(issue, f"Errors for HED string {i+1}:"))
+        else:
+            validation_errors.append('')
+
+    return {'hed_version': hed, 'validation_errors': validation_errors}
diff --git a/hedweb/hed/web/spreadsheet.py b/hedweb/hed/web/spreadsheet.py
index f607cba50..07603f865 100644
--- a/hedweb/hed/web/spreadsheet.py
+++ b/hedweb/hed/web/spreadsheet.py
@@ -1,22 +1,19 @@
-from urllib.error import URLError, HTTPError
 from flask import current_app
 
 from hed.util.error_reporter import get_printable_issue_string
-from hed.util.file_util import delete_file_if_it_exists
 from hed.validator.hed_validator import HedValidator
 from hed.util.hed_file_input import HedFileInput
 
-import hed.web.web_utils
-from hed.web.constants import common_constants, error_constants, file_constants
+from hed.web.constants import common, file_constants
 from hed.web.web_utils import convert_number_str_to_list, generate_filename, \
     generate_download_file_response, get_hed_path_from_pull_down, get_uploaded_file_path_from_form, \
-    handle_http_error, save_text_to_upload_folder
-from hed.web import spreadsheet_utils
+    get_optional_form_field, save_text_to_upload_folder, generate_text_response
+from hed.web.spreadsheet_utils import get_specific_tag_columns_from_form
 
 app_config = current_app.config
 
 
-def generate_input_from_validation_form(request):
+def generate_input_from_spreadsheet_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
@@ -31,58 +28,23 @@ def generate_input_from_validation_form(request):
     """
     hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
     uploaded_file_name, original_file_name = \
-        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
-                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
+        get_uploaded_file_path_from_form(request, common.SPREADSHEET_FILE, file_constants.SPREADSHEET_FILE_EXTENSIONS)
 
     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,
-        common_constants.TAG_COLUMNS: convert_number_str_to_list(request.form[common_constants.TAG_COLUMNS]),
-        common_constants.COLUMN_PREFIX_DICTIONARY: spreadsheet_utils.get_specific_tag_columns_from_form(request),
-        common_constants.WORKSHEET_NAME: hed.web.web_utils.get_optional_form_field(request,
-                                                                                   common_constants.WORKSHEET_NAME,
-                                                                                   common_constants.STRING),
-        common_constants.HAS_COLUMN_NAMES:
-            hed.web.web_utils.get_optional_form_field(request, common_constants.HAS_COLUMN_NAMES,
-                                                      common_constants.BOOLEAN),
-        common_constants.CHECK_FOR_WARNINGS:
-            hed.web.web_utils.get_optional_form_field(request, common_constants.CHECK_FOR_WARNINGS,
-                                                      common_constants.BOOLEAN)
+        common.HED_XML_FILE: hed_file_path,
+        common.HED_DISPLAY_NAME: hed_display_name,
+        common.SPREADSHEET_PATH: uploaded_file_name,
+        common.SPREADSHEET_FILE: original_file_name,
+        common.TAG_COLUMNS: convert_number_str_to_list(request.form[common.TAG_COLUMNS]),
+        common.COLUMN_PREFIX_DICTIONARY: get_specific_tag_columns_from_form(request),
+        common.WORKSHEET_SELECTED: get_optional_form_field(request, common.WORKSHEET_SELECTED, common.STRING),
+        common.HAS_COLUMN_NAMES: get_optional_form_field(request, common.HAS_COLUMN_NAMES, common.BOOLEAN),
+        common.CHECK_FOR_WARNINGS: get_optional_form_field(request, common.CHECK_FOR_WARNINGS, common.BOOLEAN)
     }
     return input_arguments
 
 
-def report_spreadsheet_validation_status(request):
-    """Reports the spreadsheet validation status.
-
-    Parameters
-    ----------
-    request: Request object
-        A Request object containing user data from the validation form.
-
-    Returns
-    -------
-    string
-        A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
-        500 error message is returned.
-    """
-    input_arguments = []
-    try:
-        input_arguments = generate_input_from_validation_form(request)
-        return validate_spreadsheet(input_arguments)
-    except HTTPError:
-        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_exists(input_arguments.get(common_constants.SPREADSHEET_PATH, ''))
-
-
-def validate_spreadsheet(input_arguments, hed_validator=None, return_response=True):
+def spreadsheet_validate(input_arguments, hed_validator=None):
     """Validates the spreadsheet.
 
     Parameters
@@ -91,42 +53,37 @@ def validate_spreadsheet(input_arguments, hed_validator=None, return_response=Tr
         A dictionary containing the arguments for the validation function.
     hed_validator: HedValidator
         Validator passed if previously created in another phase
-    return_response: bool
-        If true returns a response object, otherwise directly returns a printable issue string
     Returns
     -------
     HedValidator object
         A HedValidator object containing the validation results.
     """
 
-    file_input = HedFileInput(input_arguments.get(common_constants.SPREADSHEET_PATH, None),
-                              worksheet_name=input_arguments.get(common_constants.WORKSHEET_NAME, None),
-                              tag_columns=input_arguments.get(common_constants.TAG_COLUMNS, None),
-                              has_column_names=input_arguments.get(common_constants.HAS_COLUMN_NAMES, None),
-                              column_prefix_dictionary=input_arguments.get(common_constants.COLUMN_PREFIX_DICTIONARY,
+    file_input = HedFileInput(input_arguments.get(common.SPREADSHEET_PATH, None),
+                              worksheet_name=input_arguments.get(common.WORKSHEET_SELECTED, None),
+                              tag_columns=input_arguments.get(common.TAG_COLUMNS, None),
+                              has_column_names=input_arguments.get(common.HAS_COLUMN_NAMES, None),
+                              column_prefix_dictionary=input_arguments.get(common.COLUMN_PREFIX_DICTIONARY,
                                                                            None))
     if not hed_validator:
-        hed_validator = HedValidator(hed_xml_file=input_arguments.get(common_constants.HED_XML_FILE, ''),
-                                     check_for_warnings=input_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
+        hed_validator = HedValidator(hed_xml_file=input_arguments.get(common.HED_XML_FILE, ''),
+                                     check_for_warnings=input_arguments.get(common.CHECK_FOR_WARNINGS, False))
 
     issues = hed_validator.validate_input(file_input)
 
     if issues:
-        display_name = input_arguments.get(common_constants.SPREADSHEET_FILE, None)
-        worksheet_name = input_arguments.get(common_constants.WORKSHEET_NAME, None)
+        display_name = input_arguments.get(common.SPREADSHEET_FILE, None)
+        worksheet_name = input_arguments.get(common.WORKSHEET_SELECTED, None)
         title_string = display_name
         suffix = 'validation_errors'
         if worksheet_name:
             title_string = display_name + ' [worksheet ' + worksheet_name + ']'
             suffix = '_worksheet_' + worksheet_name + '_' + suffix
         issue_str = get_printable_issue_string(issues, f"{title_string} HED validation errors")
-        if not return_response:
-            return issue_str
+
         file_name = generate_filename(display_name, suffix=suffix, extension='.txt')
         issue_file = save_text_to_upload_folder(issue_str, file_name)
-        download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
-                                                            msg='Spreadsheet had validation errors')
-        if isinstance(download_response, str):
-            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-        return download_response
-    return ""
+        return generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                               msg='Spreadsheet had validation errors')
+    else:
+        return generate_text_response("", msg='Spreadsheet had no validation errors')
diff --git a/hedweb/hed/web/spreadsheet_utils.py b/hedweb/hed/web/spreadsheet_utils.py
index 0248543ff..ebb26a426 100644
--- a/hedweb/hed/web/spreadsheet_utils.py
+++ b/hedweb/hed/web/spreadsheet_utils.py
@@ -1,46 +1,26 @@
 import xlrd
-import traceback
 from flask import current_app
-from hed.util.file_util import get_file_extension, delete_file_if_it_exists
-from hed.web.constants import common_constants, error_constants, spreadsheet_constants
-from hed.web.web_utils import save_file_to_upload_folder, find_all_str_indices_in_list
+from hed.util.exceptions import HedFileError
+from hed.util.file_util import get_file_extension
+from hed.web.constants import common, file_constants, spreadsheet_constants
+from hed.web.web_utils import file_extension_is_valid, find_all_str_indices_in_list, save_file_to_upload_folder
 
 app_config = current_app.config
 
 
-def find_spreadsheet_columns_info(request):
-    """Finds the info associated with the spreadsheet columns.
-
-    Parameters
-    ----------
-    request: Request object
-        A Request object containing user data from the validation form.
-
-    Returns
-    -------
-    dictionary
-        A dictionary populated with information related to the spreadsheet columns.
-    """
-    spreadsheet_file_path = ''
-    spreadsheet_columns_info = []
-    try:
-        spreadsheet_columns_info = {common_constants.COLUMN_NAMES: [], common_constants.TAG_COLUMN_INDICES: []}
-        if common_constants.SPREADSHEET_FILE in request.files:
-            spreadsheet_file = request.files[common_constants.SPREADSHEET_FILE]
-            spreadsheet_file_path = save_file_to_upload_folder(spreadsheet_file)
-            if spreadsheet_file_path and common_constants.WORKSHEET_NAME in request.form:
-                worksheet_name = request.form[common_constants.WORKSHEET_NAME]
-                spreadsheet_columns_info = populate_spreadsheet_columns_info_dictionary(spreadsheet_columns_info,
-                                                                                        spreadsheet_file_path,
-                                                                                        worksheet_name)
-            else:
-                spreadsheet_columns_info = populate_spreadsheet_columns_info_dictionary(spreadsheet_columns_info,
-                                                                                        spreadsheet_file_path)
-    except:
-        spreadsheet_columns_info[error_constants.ERROR_KEY] = traceback.format_exc()
-    finally:
-        delete_file_if_it_exists(spreadsheet_file_path)
-    return spreadsheet_columns_info
+def generate_input_columns_info(request):
+    columns_file = request.files.get(common.COLUMNS_FILE, '')
+    if columns_file:
+        filename = columns_file.filename
+    else:
+        filename = ''
+    columns_path = save_file_to_upload_folder(columns_file)
+    input_arguments = {
+        common.COLUMNS_PATH: columns_path,
+        common.COLUMNS_DISPLAY_NAME: filename,
+        common.WORKSHEET_NAME: request.form.get(common.WORKSHEET_SELECTED, None)
+    }
+    return input_arguments
 
 
 def get_column_delimiter_based_on_file_extension(file_name_or_path):
@@ -64,6 +44,21 @@ def get_column_delimiter_based_on_file_extension(file_name_or_path):
     return column_delimiter
 
 
+def get_columns_info(input_arguments):
+    columns_path = input_arguments.get(common.COLUMNS_PATH)
+    worksheet_name = input_arguments.get(common.WORKSHEET_NAME, None)
+    columns_display_name = input_arguments.get(common.COLUMNS_DISPLAY_NAME, 'Unknown')
+    if file_extension_is_valid(columns_path, file_constants.EXCEL_FILE_EXTENSIONS):
+        columns_info = get_worksheet_info(columns_path, worksheet_name)
+    elif file_extension_is_valid(columns_path, file_constants.TEXT_FILE_EXTENSIONS):
+        columns_info = get_text_file_info(columns_path)
+    else:
+        raise HedFileError('BadFileExtension',
+                           f'File {columns_display_name} extension does not correspond to an Excel or tsv file',
+                           columns_display_name)
+    return columns_info
+
+
 def get_excel_worksheet_names(workbook_file_path):
     """Gets the worksheet names in an Excel workbook.
 
@@ -83,39 +78,7 @@ def get_excel_worksheet_names(workbook_file_path):
     return worksheet_names
 
 
-def get_specific_tag_columns_from_form(request):
-    """Gets the specific tag columns from the validation form.
-
-    Parameters
-    ----------
-    request: Request object
-        A Request object containing user data from the validation form.
-
-    Returns
-    -------
-    dictionary
-        A dictionary containing the required tag columns. The keys will be the column numbers and the values will be
-        the name of the column.
-    """
-    column_prefix_dictionary = {}
-    for tag_column_name in spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES:
-        form_tag_column_name = tag_column_name.lower() + common_constants.COLUMN_POSTFIX
-        if form_tag_column_name in request.form:
-            tag_column_name_index = request.form[form_tag_column_name].strip()
-            if tag_column_name_index:
-                tag_column_name_index = int(tag_column_name_index)
-
-                # todo: Remove these giant kludges at some point
-                if tag_column_name == "Long":
-                    tag_column_name = "Long Name"
-                tag_column_name = "Event/" + tag_column_name + "/"
-                # End giant kludges
-
-                column_prefix_dictionary[tag_column_name_index] = tag_column_name
-    return column_prefix_dictionary
-
-
-def get_spreadsheet_other_tag_column_indices(column_names):
+def get_other_tag_column_indices(column_names):
     """Gets the other tag column indices in a spreadsheet. The indices found will be one-based.
 
     Parameters
@@ -137,7 +100,7 @@ def get_spreadsheet_other_tag_column_indices(column_names):
     return tag_column_indices
 
 
-def get_spreadsheet_specific_tag_column_indices(column_names):
+def get_specific_tag_column_indices(column_names):
     """Gets the required tag column indices in a spreadsheet. The indices found will be one-based.
 
     Parameters
@@ -163,6 +126,38 @@ def get_spreadsheet_specific_tag_column_indices(column_names):
     return specific_tag_column_indices
 
 
+def get_specific_tag_columns_from_form(request):
+    """Gets the specific tag columns from the validation form.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+
+    Returns
+    -------
+    dictionary
+        A dictionary containing the required tag columns. The keys will be the column numbers and the values will be
+        the name of the column.
+    """
+    column_prefix_dictionary = {}
+    for tag_column_name in spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES:
+        form_tag_column_name = tag_column_name.lower() + common.COLUMN_POSTFIX
+        if form_tag_column_name in request.form:
+            tag_column_name_index = request.form[form_tag_column_name].strip()
+            if tag_column_name_index:
+                tag_column_name_index = int(tag_column_name_index)
+
+                # todo: Remove these giant kludges at some point
+                if tag_column_name == "Long":
+                    tag_column_name = "Long Name"
+                tag_column_name = "Event/" + tag_column_name + "/"
+                # End giant kludges
+
+                column_prefix_dictionary[tag_column_name_index] = tag_column_name
+    return column_prefix_dictionary
+
+
 def get_text_file_column_names(text_file_path, column_delimiter):
     """Gets the text spreadsheet column names.
 
@@ -185,86 +180,46 @@ def get_text_file_column_names(text_file_path, column_delimiter):
     return text_file_columns
 
 
-def get_worksheet_column_names(workbook_file_path, worksheet_name):
-    """Get the worksheet columns in a Excel workbook.
-
-    Parameters
-    ----------
-    workbook_file_path : string
-        The full path to an Excel workbook other.
-    worksheet_name : string
-        The name of an Excel worksheet.
-
-    Returns
-    -------
-    list
-        A list containing the worksheet columns in an Excel workbook.
-
-    """
-    opened_workbook_file = xlrd.open_workbook(workbook_file_path)
-    opened_worksheet = opened_workbook_file.sheet_by_name(worksheet_name)
-    worksheet_column_names = [opened_worksheet.cell(0, col_index).value for col_index in range(opened_worksheet.ncols)]
-    return worksheet_column_names
-
-
-def populate_spreadsheet_columns_info_dictionary(spreadsheet_columns_info, spreadsheet_file_path, worksheet_name=''):
+def get_text_file_info(file_path):
     """Populate dictionary with information related to the spreadsheet columns.
 
     This information contains the names of the spreadsheet columns and column indices that contain HED tags.
 
     Parameters
     ----------
-    spreadsheet_columns_info: dictionary
-        A dictionary that contains information related to the spreadsheet column.
-    spreadsheet_file_path: string
+
+    file_path: string
         The full path to a spreadsheet other.
-    worksheet_name: string
-        The name of an Excel worksheet.
 
     Returns
     -------
     dictionary
-        A dictionary populated with information related to the spreadsheet columns.
+        A dictionary populated with information related to the text file columns.
 
     """
-    if worksheet_name:
-        spreadsheet_columns_info[common_constants.COLUMN_NAMES] = \
-            get_worksheet_column_names(spreadsheet_file_path, worksheet_name)
-    else:
-        column_delimiter = get_column_delimiter_based_on_file_extension(spreadsheet_file_path)
-        spreadsheet_columns_info[common_constants.COLUMN_NAMES] = \
-            get_text_file_column_names(spreadsheet_file_path, column_delimiter)
-    spreadsheet_columns_info[common_constants.TAG_COLUMN_INDICES] = \
-        get_spreadsheet_other_tag_column_indices(spreadsheet_columns_info[common_constants.COLUMN_NAMES])
-    spreadsheet_columns_info[common_constants.REQUIRED_TAG_COLUMN_INDICES] = \
-        get_spreadsheet_specific_tag_column_indices(spreadsheet_columns_info[common_constants.COLUMN_NAMES])
-    return spreadsheet_columns_info
 
-
-def populate_worksheets_info_dictionary(worksheets_info, spreadsheet_file_path):
-    """Populate dictionary with information related to the Excel worksheets.
-
-    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.
-
-    Parameters
-    ----------
-    worksheets_info: dictionary
-        A dictionary that contains information related to the Excel worksheets.
-    spreadsheet_file_path: string
-        The full path to an Excel workbook other.
-
-    Returns
-    -------
-    dictionary
-        A dictionary populated with information related to the Excel worksheets.
-
-    """
-    worksheets_info[common_constants.WORKSHEET_NAMES] = get_excel_worksheet_names(spreadsheet_file_path)
-    worksheets_info[common_constants.COLUMN_NAMES] = \
-        get_worksheet_column_names(spreadsheet_file_path, worksheets_info[common_constants.WORKSHEET_NAMES][0])
-    worksheets_info[common_constants.TAG_COLUMN_INDICES] = get_spreadsheet_other_tag_column_indices(
-        worksheets_info[common_constants.COLUMN_NAMES])
-    worksheets_info[common_constants.REQUIRED_TAG_COLUMN_INDICES] = \
-        get_spreadsheet_specific_tag_column_indices(worksheets_info[common_constants.COLUMN_NAMES])
-    return worksheets_info
+    column_info = {}
+    column_delimiter = get_column_delimiter_based_on_file_extension(file_path)
+    column_info[common.COLUMN_NAMES] = get_text_file_column_names(file_path, column_delimiter)
+    column_info[common.TAG_COLUMN_INDICES] = get_other_tag_column_indices(column_info[common.COLUMN_NAMES])
+    column_info[common.REQUIRED_TAG_COLUMN_INDICES] = get_specific_tag_column_indices(column_info[common.COLUMN_NAMES])
+    return column_info
+
+
+def get_worksheet_info(file_path, worksheet_name=None):
+    opened_file = xlrd.open_workbook(file_path, on_demand=True)
+    worksheet_names = opened_file.sheet_names()
+    info = {common.COLUMNS_FILE: file_path, common.WORKSHEET_NAMES: worksheet_names}
+    if not worksheet_names:
+        raise HedFileError('BadExcelFile', 'Excel files must worksheets', None)
+    elif not worksheet_name:
+        worksheet_name = worksheet_names[0]
+    elif worksheet_name and worksheet_name not in worksheet_names:
+        raise HedFileError('BadWorksheetName', f'Worksheet {worksheet_name} not in Excel file', '')
+    worksheet = opened_file.sheet_by_name(worksheet_name)
+    info[common.COLUMN_NAMES] = [worksheet.cell(0, col_index).value for col_index in range(worksheet.ncols)]
+    info[common.TAG_COLUMN_INDICES] = get_other_tag_column_indices(info[common.COLUMN_NAMES])
+    info[common.REQUIRED_TAG_COLUMN_INDICES] = get_specific_tag_column_indices(info[common.COLUMN_NAMES])
+    info[common.WORKSHEET_SELECTED] = worksheet_name
+    opened_file.release_resources()
+    return info
diff --git a/hedweb/hed/web/templates/column-info.html b/hedweb/hed/web/templates/column-info.html
new file mode 100644
index 000000000..1af7d4276
--- /dev/null
+++ b/hedweb/hed/web/templates/column-info.html
@@ -0,0 +1,29 @@
+{% macro create_column_info(show_indices) %}
+
+    <div class="form-group" id="column-names">
+        <h3>Column names:</h3>
+        <table id="column-names-table"></table>
+    </div>
+    {% if show_indices in ['true'] %}
+        <div class="form-group">
+        <h3>Column indices:</h3>
+            <label class="secondary-label">Indicate specific non-prefixed tag columns (column numbers start with
+                1):</label><br>
+            <div class="textbox-group">
+                <label for="category-column">Category</label>
+                <input class='indices' type="text" name="category-column" id="category-column">
+                <label for="description-column">Description</label>
+                <input class='indices' type="text" name="description-column" id="description-column">
+                <label for="label-column">Label</label>
+                <input class='indices' type="text" name="label-column" id="label-column">
+                <label for="long-column">Long name</label>
+                <input class='indices' type="text" name="long-column" id="long-column">
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="secondary-label" for="tag-columns">List columns (comma-separated) with HED strings:</label>
+            <input class="text" type="text" name="tag-columns" id="tag-columns">
+        </div>
+    {% endif %}
+    <p class="flash" id="tag-columns-flash"></p>
+{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/dictionary.html b/hedweb/hed/web/templates/dictionary.html
index 8fa6b03b5..772500741 100644
--- a/hedweb/hed/web/templates/dictionary.html
+++ b/hedweb/hed/web/templates/dictionary.html
@@ -3,29 +3,36 @@
 {% from "hed-pulldown.html" import create_hed_pulldown %}
 
 {% block content %}
-    <h2>Validate JSON dictionary containing HED tags</h2>
+    <h2>Validate BIDS-style JSON dictionary</h2>
+    <p>
+        <i>Upload a BIDS-style JSON events file, select a HED schema, and press Validate.<br>
+        You will see either a downloadable results file or a status message.
+        </i>
+    </p>
+
         <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
     <form id="dictionary-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
-         <div class="form-group">
+        <div class="form-group">
 
-        {{ create_json_input() }}
-        {{ create_hed_pulldown() }}
+            {{ 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 class="form-group">
+                <h3>Specify additional options:</h3>
+                <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>
 
-        <div class="form-group">
-            <button id="dictionary-validation-submit" type="button">Validate</button>
-            <p class="flash" id="dictionary-validation-submit-flash"></p>
+            <div class="form-group">
+                <button id="dictionary-validation-submit" type="button">Validate</button>
+                <p class="flash" id="dictionary-validation-submit-flash"></p>
+            </div>
         </div>
     </form>
     <script type="module">
diff --git a/hedweb/hed/web/templates/events.html b/hedweb/hed/web/templates/events.html
index 86a3af406..bf94bfa38 100644
--- a/hedweb/hed/web/templates/events.html
+++ b/hedweb/hed/web/templates/events.html
@@ -1,14 +1,32 @@
 {% extends "layout.html" %}
 {% from "hed-pulldown.html" import create_hed_pulldown %}
+{% from "column-info.html" import create_column_info %}
 {% from "json-input.html" import create_json_input %}
-{% from "spreadsheet-input.html" import spreadsheet_input %}
 {% block content %}
-    <h2>Validate spreadsheet containing HED tags</h2>
+    <h2>Validate BIDS-style event file</h2>
+    <p>
+        <i>Upload a BIDS-style tsv events file and an optional JSON events file.<br>
+            Select a HED schema, and press Validate.<br>
+            You will see either a downloadable results file or a status message.<br>
+            Note: this tool is still under development
+        </i>
+    </p>
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
     <form id="events-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
-        {{ spreadsheet_input() }}
+        <h3>Upload BIDS-style events file:</h3>
+        <div class="form-group">
+            <label class="secondary-label" for="events-file">Events file:</label>
+            <label class="btn-group file-browse-group">
+                <button for="events-file">Browse ...</button>
+                <label id="events-display-name"></label>
+                <input type="file" name="events-file" id="events-file"/>
+            </label>
+            <p class="flash" id="events-flash"></p>
+        </div>
+
+        {{ create_column_info('false') }}
         {{ create_json_input() }}
         {{ create_hed_pulldown() }}
 
@@ -16,11 +34,11 @@ <h2>Validate spreadsheet containing HED tags</h2>
             <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>
+                    <input type="checkbox" name="has-column-names" id="has-column-names" checked>
+                    <label for="has-column-names">Events file has column names</label>
                 </div>
                 <div class="inline-field">
-                    <input type="checkbox" name="check-for-warnings" id="check-for-warnings">
+                    <input type="checkbox" name="check-for-warnings" id="check-for-warnings" checked>
                     <label for="check-for-warnings">Include warnings in output file</label>
                 </div>
             </div>
@@ -32,13 +50,11 @@ <h2>Validate spreadsheet containing HED tags</h2>
         </div>
     </form>
     <script type="module">
-
+        {%  include 'js/column-info.js' %}
         {%  include 'js/form-helpers.js' %}
         {%  include 'js/hed-pulldown.js' %}
         {%  include 'js/json-input.js' %}
-        {%  include 'js/spreadsheet-input.js' %}
         {%  include 'js/events-form.js' %}
-
     </script>
 
 {% 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
index b7471c307..c6b280693 100644
--- a/hedweb/hed/web/templates/hed-pulldown.html
+++ b/hedweb/hed/web/templates/hed-pulldown.html
@@ -1,6 +1,7 @@
 {% macro create_hed_pulldown() %}
+        <h3>Choose a HED version:</h3>
         <div class="form-group">
-            <label for="hed-version">Choose HED version:</label><br>
+            <label class="secondary-label" for="hed-version">HED version:</label>
             <select name="hed-version" id="hed-version"></select><br>
         </div>
 
@@ -11,6 +12,6 @@
                 <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>
+            <p class="flash" id="hed-select-flash"></p>
         </div>
 {% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/hed-tools-home.html b/hedweb/hed/web/templates/hed-tools-home.html
index 1d5a26cdc..595c8e7b1 100644
--- a/hedweb/hed/web/templates/hed-tools-home.html
+++ b/hedweb/hed/web/templates/hed-tools-home.html
@@ -5,7 +5,8 @@ <h2>What is HED?</h2>
         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,
         so that the following two things can be accomplished. First, you and/or other researchers can
-        better understand the experience and responses of the participant. Secondly, data analysis and meta-analysis can
+        better understand the experience and responses of the participant as well as the relationship of these
+        events to the task and experimental conditions. Secondly, data analysis and meta-analysis can
         more easily and flexibly compare events (and responses to events) across datasets and studies to better isolate
         common “cognitive aspects”.
     </p>
@@ -17,9 +18,9 @@ <h2>What is HED?</h2>
         <a href="https://github.com/hed-standard">https://github.com/hed-standard</a>.
     </p>
 
-    <h2>HED 2G and HED 3G</h2>
+    <h2>HED-2G and HED-3G</h2>
     <p>
-        HED is currently undergoing a significant revision (HED 3G or HED third generation) so that it can better
+        HED is currently undergoing a significant revision (HED-3G or HED third generation) so that it can better
         capture not only a basic description of the events, but also event relationship to task intent. The redesigned
         schema can be viewed at <a href="http://www.hedtags.org/display_hed.html?version=8.0.0-alpha.1">HED 3G viewer</a>.
     </p>
@@ -28,17 +29,45 @@ <h2>HED online tools</h2>
     <p>
         Several online HED tools are currently available:
         <ul>
-            <li><a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}"><b>Validate spreadsheet</b></a>
-            validates HED tags in a tab-separated value or Excel spreadsheet and produces a download file with any errors.</li>
-        <li><a href="{{ url_for('route_blueprint.render_dictionary_form') }}"><b>Validate dictionary</b></a>
-            validates a JSON dictionary containing HED tags.</li>
-        <li><a href="{{ url_for('route_blueprint.render_schema_form') }}"><b>Process schema</b></a> converts HED vocabulary
-            schema between .mediawiki and .xml format. This tool also has options for checking that individual HED
-            tags conform to the requirements in the HED specification and are not duplicated elsewhere in the schema.
+            <li><a href="{{ url_for('route_blueprint.render_events_form') }}"><b>[Validate events]</b></a>
+                <b>Validate a BIDS-style events file with optional JSON sidecar.</b><br>
+                The user uploads the two files to validate. The user also specifies which version of HED to
+                validate against either by selecting a standard version from Github or uploading a local HED schema.
+                The tool first validates the sidecar if present and if the sidecar contains no errors
+                validates the events file in conjunction with the sidecar. If there are errors, the tool returns
+                a downloadable file of error messages.
+            </li>
+            <li><a href="{{ url_for('route_blueprint.render_dictionary_form') }}"><b>[Validate dictionary]</b></a>
+                <b>Validate a single BIDS-style events JSON sidecar.</b><br>
+                The user uploads the file. The user also specifies which version of HED to validate against either
+                by selecting a standard version from Github or uploading a local HED schema. If there are errors,
+                the tool returns a downloadable file of error messages.
+            </li>
+            <li><a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}"><b>[Validate spreadsheet]</b></a>
+                <b>Validate an Excel or tsv file containing HED tags.</b><br>
+                The user uploads an Excel or tsv file and selects a worksheet if the file is an Excel file.
+                The user indicates which columns contain HED tags and also specifies which version of HED to
+                validate against either by selecting a standard version from Github or uploading a local HED schema.
+                If there are errors, the tool returns a downloadable file of error messages.
+            </li>
+            <li><a href="{{ url_for('route_blueprint.render_hedstring_form') }}"><b>[Process string]</b></a>
+                <b>Validate or convert a HED string.</b><br>
+                The user enters or pastes a HED string into a text box. The user also specifies which version
+                of HED to validate against either by selecting a standard version from Github or uploading a
+                local HED schema. The user selects one of three processing options: 1) Validate,  2) Convert
+                from long to short form, or 3) Convert from short to long form. The results appear in the results text box.
+            </li>
+            <li><a href="{{ url_for('route_blueprint.render_schema_form') }}"><b>[Process schema]</b></a>
+                <b>Validate or check a HED schema.</b><br>
+                The user enters the URL of a HED schema or uploads a HED schema file in either .mediawiki or
+                .xml format. If the user presses the Convert button, the tools convert the schema to the other
+                format and make the converted file available for download. If the user presses the Check button,
+                the tools make a file of HED-3G compliance errors available for download.
+            </li>
         </ul>
     </p>
     <p>
-        Additional online tools are under development. All code is open-source and available in the
+        Additional web-based tools are under development. All code is open-source and available in the
         <a href="https:/github.com/hed-standard/hed-python">hed-python</a> repository on Github.
         For more information on HED, visit the <a href="http://www.hedtags.org">documentation</a> page.
     </p>
diff --git a/hedweb/hed/web/templates/hedstring.html b/hedweb/hed/web/templates/hedstring.html
new file mode 100644
index 000000000..ce9446032
--- /dev/null
+++ b/hedweb/hed/web/templates/hedstring.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% from "json-input.html" import create_json_input %}
+{% from "hed-pulldown.html" import create_hed_pulldown %}
+
+{% block content %}
+    <h2>HED string quick checks</h2>
+        <p>
+        <i>Enter or paste a HED string in the upper text box.<br>
+        Choose a HED schema version, pick an action, and press Process.<br>
+        You will see either the results in the lower text box or a status message below.
+        </i>
+    </p>
+        <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
+    <form id="hedstring-form" method="POST" enctype="multipart/form-data">
+        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+
+     <h3>Enter HED string in the top box [result appears in lower box]: </h3>
+        <div class="form-group">
+                <textarea rows="5" cols="60" wrap="soft" maxlength="25000"
+                          name="hedstring-input" id="hedstring-input"></textarea>
+            <p> </p>
+                <textarea rows="5" cols="60" wrap="soft" maxlength="25000"
+                          name="hedstring-result" id="hedstring-result"></textarea>
+            <p class="flash" id="hedstring-flash"></p>
+        </div>
+
+        {{ create_hed_pulldown() }}
+
+
+        <h3>Pick an action: </h3>
+        <div class="form-group">
+            <input class="radio-btn" type="radio" name="hedstring-option"
+                   id="hedstring-validate" value="hedstring-validate"
+                   aria-label="Select this button to validate entered HED string">
+            <label class="secondary-label">Validate</label>
+        </div>
+        <div class="form-group">
+            <input class="radio-btn" type="radio" name="hedstring-option"
+                   id="hedstring-to-short" value="hedstring-to-short"
+                   aria-label="Select this button to convert hedstring to short form">
+            <label class="secondary-label">Convert to short</label>
+      </div>
+        <div class="form-group">
+            <input class="radio-btn" type="radio" name="hedstring-option"
+                   id="hedstringto-long" value="hedstring-to-long"
+                   aria-label="Select this button to convert hedstring to long form">
+            <label class="secondary-label">Convert to long</label>
+        </div>
+
+        <div class="form-group">
+            <button id="hedstring-submit" type="button">Process</button>
+        </div>
+
+        <div class="form-group">
+            <p class="flash" id="hedstring-submit-flash"></p>
+        </div>
+
+    </form>
+    <script type="module">
+        {%  include 'js/form-helpers.js' %}
+        {%  include 'js/hed-pulldown.js' %}
+        {%  include 'js/hedstring-form.js' %}
+    </script>
+{% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/js/column-info.js b/hedweb/hed/web/templates/js/column-info.js
new file mode 100644
index 000000000..deb878c4d
--- /dev/null
+++ b/hedweb/hed/web/templates/js/column-info.js
@@ -0,0 +1,202 @@
+
+function clearColumnInfoFlashMessages() {
+    flashMessageOnScreen('', 'success', 'tag-columns-flash');
+}
+/**
+ * Clears tag column text boxes.
+ */
+function clearTagColumnTextboxes() {
+    $('.indices').val('');
+}
+
+/**
+ * Checks to see if any entries in an array of names are empty.
+ * @param {Array} names - An array containing a list of names.
+ * @returns {boolean} - True if any of the names in the array are all empty.
+ */
+function columnNamesAreEmpty(names) {
+    if (names !== undefined) {
+        let numberOfNames = names.length;
+        for (let i = 0; i < numberOfNames; i++) {
+            if (!isEmptyStr(names[i].trim())) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+/**
+ * Gets the columns information from a spreadsheet-like data file
+ * @param {string} formID - ID of the form that is from
+ * @param {string} pathID - Form ID of the path of a spreadsheet-like file with column names.
+ * @returns string or array
+ */
+
+/**
+ * Gets information a file with columns. This information the names of the columns in the specified
+ * worksheet and indices that contain HED tags.
+ * @param {Object} columnsFile - A file with columns.
+ * @param {string} worksheetName - Name of worksheet or undefined.
+ * @param {boolean} repopulateWorksheet - If true repopulate the select pull down with worksheet names.
+ * @param {string} flashMessage - ID name of the flash message element in which to display errors.
+ */
+function setColumnsInfo(columnsFile, worksheetName=undefined, repopulateWorksheet=true, flashMessage) {
+    let formData = new FormData();
+    formData.append('columns-file', columnsFile);
+    if (worksheetName !== undefined) {
+        formData.append('worksheet-selected', worksheetName)
+    }
+    $.ajax({
+        type: 'POST',
+        url: "{{url_for('route_blueprint.get_columns_info_results')}}",
+        data: formData,
+        contentType: false,
+        processData: false,
+        dataType: 'json',
+        success: function (info) {
+               if (info['message'])
+                    flashMessageOnScreen(info['message'], 'error', flashMessage)
+               else {
+                   if (repopulateWorksheet) {
+                       populateWorksheetDropdown(info['worksheet-names']);
+                   }
+                   let hasColumns = $('#has-column-names').prop('checked')
+                   setComponentsRelatedToColumns(info, hasColumns, true);
+               }
+        },
+        error: function () {
+            flashMessageOnScreen('File could not be processed.', 'error', 'flashMessage');
+        }
+    });
+}
+
+/**
+ * Hides  columns section in the form.
+ */
+function hideColumnNames() {
+    $('#column-names').hide();
+}
+
+
+/**
+ * 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
+ * in the spreadsheet. The keys are the column names and the values are the indices.
+ */
+function populateRequiredTagColumnTextboxes(requiredTagColumnIndices) {
+    for (let key in requiredTagColumnIndices) {
+        $('#' + key.toLowerCase() + '-column').val(requiredTagColumnIndices[key].toString());
+    }
+}
+
+/**
+ * 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
+ * columns.
+ */
+function populateTagColumnsTextbox(tagColumnIndices) {
+    $('#tag-columns').val(tagColumnIndices.sort().map(String));
+}
+
+/**
+ * Clears tag column text boxes.
+ */
+function removeColumnTable() {
+    $('#column-names-table').children().remove();
+}
+
+/**
+ * Sets the components related to the Excel worksheet columns.
+ * @param {JSON} columnsInfo - A JSON object containing information related to columns.
+ * @param {boolean} hasColumns - if false then don't show the columns even if column names not empty
+ * @param {boolean} showIndices -
+ * This information contains the names of the columns and column indices that contain HED tags.
+ */
+function setComponentsRelatedToColumns(columnsInfo, hasColumns = true, showIndices = false) {
+    clearTagColumnTextboxes();
+    if (!hasColumns || columnNamesAreEmpty(columnsInfo['column-names']) ) {
+        setComponentsRelatedToEmptyColumnNames();
+    } else {
+        setComponentsRelatedToNonEmptyColumnNames(columnsInfo['column-names']);
+    }
+    if (showIndices) {
+        if (!tagColumnsIndicesAreEmpty(columnsInfo['tag-column-indices'])) {
+            populateTagColumnsTextbox(columnsInfo['tag-column-indices']);
+        }
+        if (Object.keys(columnsInfo['required-tag-column-indices']).length !== 0) {
+            populateRequiredTagColumnTextboxes(columnsInfo['required-tag-column-indices']);
+        }
+    }
+}
+
+/**
+ * Sets the components related to Excel worksheet columns when they are all empty.
+ */
+function setComponentsRelatedToEmptyColumnNames() {
+    clearTagColumnTextboxes();
+    setHasColumnNamesCheckbox(false);
+    hideColumnNames();
+}
+
+/**
+ * Sets the components related to the spreadsheet columns when they are not empty.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
+ */
+function setComponentsRelatedToNonEmptyColumnNames(columnNames) {
+    showColumnNames(columnNames)
+    setHasColumnNamesCheckbox(true);
+}
+
+/**
+ * Sets the spreadsheet has column names checkbox to false.
+ * @param {boolean} value - is checked if true and unchecked if false
+ */
+function setHasColumnNamesCheckbox(value) {
+    $('#has-column-names').prop('checked', value);
+}
+
+/**
+ * Sets the components related to the spreadsheet columns when they are not empty.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
+ */
+function showColumnNames(columnNames) {
+    $('#column-names').show();
+    let columnTable = $('#column-names-table');
+    let columnNamesRow = $('<tr/>');
+    let numberOfColumnNames = columnNames.length;
+    columnTable.empty();
+    for (let i = 0; i < numberOfColumnNames; i++) {
+        columnNamesRow.append('<td>' + columnNames[i] + '</td>');
+    }
+    columnTable.append(columnNamesRow);
+}
+
+/**
+ * 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 tagColumnsIndicesAreEmpty(tagColumnsIndices) {
+    return tagColumnsIndices.length <= 0
+}
+
+/**
+ * Checks to see if the tag columns textbox has valid input. Valid input is an integer or a comma-separated list of
+ * integers that are the column indices in a Excel worksheet that contain HED tags.
+ * @returns {boolean} - True if the tags columns textbox is valid.
+ */
+function tagColumnsTextboxIsValid() {
+    let otherTagColumns = $('#tag-columns').val().trim();
+    let valid = true;
+    if (!isEmptyStr(otherTagColumns)) {
+        let pattern = new RegExp('^([ \\d]+,)*[ \\d]+$');
+        let valid = pattern.test(otherTagColumns);
+        if (!valid) {
+            flashMessageOnScreen('Tag column(s) must be a number or a comma-separated list of numbers',
+                'error', 'tag-columns-flash')
+        }
+    }
+    return valid;
+}
diff --git a/hedweb/hed/web/templates/js/dictionary-form.js b/hedweb/hed/web/templates/js/dictionary-form.js
index 98b006943..1eca3963a 100644
--- a/hedweb/hed/web/templates/js/dictionary-form.js
+++ b/hedweb/hed/web/templates/js/dictionary-form.js
@@ -7,37 +7,38 @@ $(function () {
  * Submits the form if the tag columns textbox is valid.
  */
 $('#dictionary-validation-submit').on('click', function () {
-    if (jsonIsSpecified() && hedSpecifiedWhenOtherIsSelected()) {
+    if (fileIsSpecified('#json-file', 'json-flash', 'JSON is not specified.' ) && 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.
+ * Clear the fields in the form.
  */
-function prepareForm() {
-    resetForm();
-    getHEDVersions()
-    hideOtherHEDVersionFileUpload();
+function clearForm() {
+    $('#dictionary-form')[0].reset();
+    clearFlashMessages()
+    clearJsonFileLabel();
+    hideOtherHEDVersionFileUpload()
 }
 
 /**
- * Resets the flash messages that aren't related to the form submission.
+ * Clear the flash messages that aren't related to the form submission.
  */
-function resetFormFlashMessages() {
-    clearJsonFlashMessage();
-    clearHEDFlashMessage();
+function clearFlashMessages() {
+    clearJsonInputFlashMessages();
+    clearHedSelectFlashMessages();
     flashMessageOnScreen('', 'success', 'dictionary-validation-submit-flash');
 }
 
 /**
- * Resets the fields in the form.
+ * 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 resetForm() {
-    $('#dictionary-form')[0].reset();
-    clearJsonFileLabel();
-    hideOtherHEDVersionFileUpload()
+function prepareForm() {
+    clearForm();
+    getHedVersions()
+    hideOtherHEDVersionFileUpload();
 }
 
 /**
@@ -50,7 +51,7 @@ function submitForm() {
 
     let dictionaryFile = getJsonFileLabel();
     let display_name = convertToResultsName(dictionaryFile, 'issues')
-    resetFormFlashMessages();
+    clearFlashMessages();
     flashMessageOnScreen('Dictionary is being validated ...', 'success', 'dictionary-validation-submit-flash')
     $.ajax({
             type: 'POST',
diff --git a/hedweb/hed/web/templates/js/events-form.js b/hedweb/hed/web/templates/js/events-form.js
index d48415e93..77f0a232b 100644
--- a/hedweb/hed/web/templates/js/events-form.js
+++ b/hedweb/hed/web/templates/js/events-form.js
@@ -1,149 +1,77 @@
+const TEXT_FILE_EXTENSIONS = ['tsv', 'txt'];
 
 $(function () {
     prepareForm();
 });
 
+
 /**
- * Submits the form if there is a spreadsheet file and an available hed schema
+ * Events file handler function. Checks if the file uploaded has a valid spreadsheet extension.
  */
-$('#events-validation-submit').on('click', function () {
-    if (spreadsheetIsSpecified()  && hedSpecifiedWhenOtherIsSelected()) {
-        submitForm();
+$('#events-file').on('change', function () {
+    let events = $('#events-file');
+    let eventsPath = events.val();
+    let eventsFile = events[0].files[0];
+    clearFlashMessages();
+    removeColumnTable();
+    if (cancelWasPressedInChromeFileUpload(eventsPath)) {
+        clearForm();
+        return;
+    }
+    updateFileLabel(eventsPath, '#events-display-name');
+    if (fileHasValidExtension(eventsPath, TEXT_FILE_EXTENSIONS)) {
+        setColumnsInfo(eventsFile, undefined, false, 'events-flash');
+    } else {
+        clearForm();
+        flashMessageOnScreen('Please upload a tsv file (.tsv, .txt)', 'error', 'events-flash');
     }
 });
 
 /**
- * Gets the spreadsheet columns.
- * @param {Object} spreadsheetFile - A spreadsheet file.
- * @param {String} worksheetName - An Excel worksheet name.
+ * Submits the form if there is an events file and an available hed schema
  */
-function getColumnsInfo(spreadsheetFile, worksheetName) {
-    let formData = new FormData();
-    formData.append('spreadsheet-file', spreadsheetFile);
-    if (typeof worksheetName !== 'undefined') {
-        formData.append('worksheet-name', worksheetName);
+$('#events-validation-submit').on('click', function () {
+    if (fileIsSpecified('#events-file', 'events-flash', 'Events file is not specified.')
+        && hedSpecifiedWhenOtherIsSelected()) {
+        submitForm();
     }
-    $.ajax({
-        type: 'POST',
-        url: "{{url_for('route_blueprint.get_spreadsheet_columns_info')}}",
-        data: formData,
-        contentType: false,
-        processData: false,
-        dataType: 'json',
-        success: function (columnsInfo) {
-            setComponentsRelatedToColumns(columnsInfo);
-        },
-        error: function (jqXHR) {
-            console.log(jqXHR.responseJSON.message);
-            flashMessageOnScreen('Event annotations could not be processed.', 'error',
-                'spreadsheet-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
- * first worksheet.
- * @param {Object} workbookFile - An Excel workbook file.
- */
-function getWorksheetsInfo(workbookFile) {
-    let formData = new FormData();
-    formData.append('spreadsheet-file', workbookFile);
-    $.ajax({
-        type: 'POST',
-        url: "{{url_for('route_blueprint.get_worksheets_info')}}",
-        data: formData,
-        contentType: false,
-        processData: false,
-        dataType: 'json',
-        success: function (worksheetsInfo) {
-            populateWorksheetSelectbox(worksheetsInfo['worksheet-names']);
-            setComponentsRelatedToColumns(worksheetsInfo);
-            flashWorksheetNumberMessage(worksheetsInfo['worksheet-names']);
-        },
-        error: function (jqXHR) {
-            console.log(jqXHR);
-            // console.log(jqXHR.responseJSON.message);
-            flashMessageOnScreen('Event annotations could not be processed.', 'error',
-                'events-validation-submit-flash');
-        }
-    });
-}
+});
 
 
 /**
- * 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.
+ * Clears the fields in the form.
  */
-function prepareForm() {
-    resetForm();
-    getHEDVersions()
+function clearForm() {
+    $('#events-form')[0].reset();
+    $('#events-display-name').text('');
+    clearFlashMessages();
     hideColumnNames();
     hideOtherHEDVersionFileUpload();
 }
 
 /**
- * Resets the flash messages that aren't related to the form submission.
+ * Clear the flash messages that aren't related to the form submission.
  */
-function resetFormFlashMessages() {
-    flashMessageOnScreen('', 'success', 'spreadsheet-flash');
-    flashMessageOnScreen('', 'success', 'worksheet-flash');
-    flashMessageOnScreen('', 'success', 'hed-flash');
+function clearFlashMessages() {
+    clearColumnInfoFlashMessages();
+    clearHedSelectFlashMessages();
+    clearJsonInputFlashMessages();
+    flashMessageOnScreen('', 'success', 'events-flash');
     flashMessageOnScreen('', 'success', 'events-validation-submit-flash');
 }
 
-/**
- * Resets the fields in the form.
- */
-function resetForm() {
-    $('#events-form')[0].reset();
-    clearSpreadsheetFileLabel();
-    clearWorksheetSelectbox();
-    hideColumnNames();
-    hideOtherHEDVersionFileUpload()
-}
 
 /**
- * Sets the components related to Excel worksheet columns when they are all empty.
+ * 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 setComponentsRelatedToEmptyColumnNames() {
-    setHasColumnNamesCheckbox(false);
+function prepareForm() {
+    clearForm();
+    getHedVersions()
     hideColumnNames();
+    hideOtherHEDVersionFileUpload();
 }
 
-/**
- * Sets the components related to the spreadsheet columns when they are not empty.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- */
-function setComponentsRelatedToNonEmptyColumnNames(columnNames) {
-    showColumnNames(columnNames)
-    setHasColumnNamesCheckbox(true);
-}
-
-/**
- * Sets the components related to the Excel worksheet columns.
- * @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 setComponentsRelatedToColumns(columnsInfo) {
-    if (columnNamesAreEmpty(columnsInfo['column-names'])) {
-        setComponentsRelatedToEmptyColumnNames();
-    } else {
-        setComponentsRelatedToNonEmptyColumnNames(columnsInfo['column-names']);
-    }
-}
-
-/**
- * Checks or unchecks the spreadsheet column names checkbox to false.
- * @param {boolean} value  sets the checkbox to checked if true
- */
-function setHasColumnNamesCheckbox(value) {
-    $('#has-column-names').prop('checked', value);
-}
-
-
 /**
  * Submit the form and return the validation results. If there are issues then they are returned in an attachment
  * file.
@@ -151,14 +79,10 @@ function setHasColumnNamesCheckbox(value) {
 function submitForm() {
     let eventsForm = document.getElementById("events-form");
     let formData = new FormData(eventsForm);
-    let worksheetName = $('#worksheet-name option:selected').text();
     let prefix = 'issues';
-    if(worksheetName) {
-        prefix = prefix + '_worksheet_' + worksheetName;
-    }
-    let spreadsheetFile = $('#spreadsheet-file')[0].files[0].name;
-    let display_name = convertToResultsName(spreadsheetFile, prefix)
-    resetFormFlashMessages();
+    let eventsFile = $('#events-file')[0].files[0].name;
+    let display_name = convertToResultsName(eventsFile, prefix)
+    clearFlashMessages();
     flashMessageOnScreen('Worksheet is being validated ...', 'success',
         'events-validation-submit-flash')
     $.ajax({
diff --git a/hedweb/hed/web/templates/js/form-helpers.js b/hedweb/hed/web/templates/js/form-helpers.js
index ffc8594ba..e2f53c39e 100644
--- a/hedweb/hed/web/templates/js/form-helpers.js
+++ b/hedweb/hed/web/templates/js/form-helpers.js
@@ -8,20 +8,6 @@ function cancelWasPressedInChromeFileUpload(filePath) {
     return isEmptyStr(filePath) && (window.chrome)
 }
 
-/**
- * Checks to see if any entries in an array of names are empty.
- * @param {Array} names - An array containing a list of names.
- * @returns {boolean} - True if any of the names in the array are all empty.
- */
-function columnNamesAreEmpty(names) {
-    let numberOfNames = names.length;
-    for (let i = 0; i < numberOfNames; i++) {
-        if (!isEmptyStr(names[i].trim())) {
-            return false;
-        }
-    }
-    return true;
-}
 
 /**
  * Converts a path and prefix to a text results name
@@ -43,13 +29,25 @@ function convertToResultsName(filename, prefix) {
  */
 function fileHasValidExtension(filePath, acceptedFileExtensions) {
     let fileExtension = filePath.split('.').pop();
-    if ($.inArray(fileExtension.toLowerCase(), acceptedFileExtensions) != -1) {
-        return true;
-    }
-    return false;
+    return $.inArray(fileExtension.toLowerCase(), acceptedFileExtensions) != -1
 }
 
 
+/**
+ * Checks to see if a file has been specified.
+ * @param {string} nameID - #id of the element holding the name
+ * @param {string} flashID - id of the flash element to display error message
+ * @param {string} errorMsg - error message to be displayed if file isn't in form
+ * @returns {boolean} - returns true if file is specified
+ */
+function fileIsSpecified(nameID, flashID, errorMsg) {
+    let theFile = $(nameID);
+    if (theFile[0].files.length === 0) {
+        flashMessageOnScreen(errorMsg, 'error', flashID);
+        return false;
+    }
+    return true;
+}
 
 /**
  * Flash a message on the screen.
@@ -100,6 +98,7 @@ function getFilenameFromResponseHeader(xhr, default_name) {
     return filename
 }
 
+
 /**
  * Gets standard failure response for download
  * @param {String} download - Downloaded string response blob
@@ -108,34 +107,27 @@ function getFilenameFromResponseHeader(xhr, default_name) {
  * @param {String} flash_location - ID of the flash location element for displaying response Message
  */
 function getResponseFailure(download, xhr, display_name, flash_location) {
-    console.log(download.responseText);
-    if (download.responseText.length < 100) {
-        flashMessageOnScreen(download.responseText, 'error', flash_location);
+    let info = xhr.getResponseHeader('Message')
+    let category =  xhr.getResponseHeader('Category')
+    if (info) {
+        flashMessageOnScreen(info, category, flash_location);
     } else {
-        flashMessageOnScreen('Not processed', 'error', flash_location);
+        flashMessageOnScreen('Unknown processing error occurred for ' + display_name, 'error', flash_location);
     }
 }
 
-/**
- * Gets standard response with download
- * @param {String} download - Downloaded string response blob
- * @param {Object} xhr - Dictionary containing Response header information
- * @param {String} display_name - Name used for the downloaded blob file
- * @param {String} flash_location - ID of the flash location element for displaying response Message
- */
+
 function getResponseSuccess(download, xhr, display_name, flash_location) {
     if (download) {
         let filename = getFilenameFromResponseHeader(xhr, display_name)
         triggerDownloadBlob(download, filename);
-        let info = xhr.getResponseHeader('Message')
-        let category =  xhr.getResponseHeader('Category')
-        if (info) {
-            flashMessageOnScreen(info, category, flash_location);
-        } else {
-            flashMessageOnScreen('', 'success', flash_location);
-        }
+    }
+    let info = xhr.getResponseHeader('Message')
+    let category =  xhr.getResponseHeader('Category')
+    if (info) {
+        flashMessageOnScreen(info, category, flash_location);
     } else {
-        flashMessageOnScreen('No errors found.', 'success', flash_location);
+        flashMessageOnScreen('', 'success', flash_location);
     }
 }
 
@@ -170,6 +162,17 @@ function setFlashMessageCategory(flashMessage, category) {
     }
 }
 
+/**
+ * Returns the extension of a file.
+ * @param {string} filename - The filename of the file whose extension should be split off.
+ * @returns string - The file extension or an empty string if there is no extension.
+ *
+ */
+function splitExt(filename) {
+    const index = filename.lastIndexOf('.');
+    return (-1 !== index) ? [filename.substring(0, index), filename.substring(index + 1)] : [filename, ''];
+}
+
 /**
  * Trigger the "save as" dialog for a text blob to save as a file with display name.
  */
diff --git a/hedweb/hed/web/templates/js/hed-pulldown.js b/hedweb/hed/web/templates/js/hed-pulldown.js
index 3076af023..d0032aa14 100644
--- a/hedweb/hed/web/templates/js/hed-pulldown.js
+++ b/hedweb/hed/web/templates/js/hed-pulldown.js
@@ -11,7 +11,7 @@ $('#hed-version').on('change',function () {
     } else {
         hideOtherHEDVersionFileUpload()
     }
-    flashMessageOnScreen('', 'success', 'hed-flash');
+    flashMessageOnScreen('', 'success', 'hed-select-flash');
 });
 
 /**
@@ -26,22 +26,21 @@ $('#hed-xml-file').on('change', function () {
         updateFileLabel(hedPath, '#hed-display-name');
     } else {
         flashMessageOnScreen('Please upload a valid HED file (.xml)', 'error',
-            'hed-flash')
+            'hed-select-flash')
     }
 })
 
 /**
  * Resets the flash messages that aren't related to the form submission.
  */
-function clearHEDFlashMessage() {
-    flashMessageOnScreen('', 'success', 'hed-flash');
+function clearHedSelectFlashMessages() {
+    flashMessageOnScreen('', 'success', 'hed-select-flash');
 }
 
-
 /**
  * Gets the HED versions that are in the HED version drop-down menu.
  */
-function getHEDVersions() {
+function getHedVersions() {
     $.ajax({
             type: 'GET',
             url: "{{url_for('route_blueprint.get_major_hed_versions')}}",
@@ -49,12 +48,18 @@ function getHEDVersions() {
             processData: false,
             dataType: 'json',
             success: function (hedInfo) {
-                populateHEDVersionsDropdown(hedInfo['hed-major-versions']);
+                if (hedInfo['hed-major-versions']) {
+                     populateHEDVersionsDropdown(hedInfo['hed-major-versions']);
+                } else if (hedInfo['message'])
+                    flashMessageOnScreen(hedInfo['message'], 'error', 'hed-select-flash')
+                else {
+                    flashMessageOnScreen('Server could retrieve HED versions. Please provide your own.',
+                        'error', 'hed-select-flash')
+                }
             },
             error: function (jqXHR) {
-                console.log(jqXHR.responseJSON.message);
-                flashMessageOnScreen('Major HED versions could not be retrieved. Please provide your own.',
-                    'error', 'dictionary-flash');
+                flashMessageOnScreen('Server could retrieve HED versions. Please provide your own.',
+                    'error', 'hed-select-flash');
             }
         }
     );
@@ -75,14 +80,19 @@ function getVersionFromHEDFile(hedXMLFile) {
         processData: false,
         dataType: 'json',
         success: function (hedInfo) {
-            resetFormFlashMessages();
-            flashMessageOnScreen('Using HED version ' + hedInfo['hed-version'],
-                'success', 'hed-flash');
+            if (hedInfo['hed-version']) {
+                flashMessageOnScreen('Using HED version ' + hedInfo['hed-version'], 'success', 'hed-select-flash');
+            } else if (hedInfo['message'])
+                flashMessageOnScreen(hedInfo['message'], 'error', 'hed-select-flash')
+            else {
+                flashMessageOnScreen('Server could retrieve HED versions. Please provide your own.',
+                        'error', 'hed-select-flash')
+            }
         },
         error: function (jqXHR) {
-            console.log(jqXHR.responseJSON.message);
+            //console.log(jqXHR.responseJSON.message);
             flashMessageOnScreen('Could not get version number from HED XML file.',
-                'error', 'hed-flash');
+                'error', 'hed-select-flash');
         }
     });
 }
@@ -95,7 +105,7 @@ 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');
+        flashMessageOnScreen('HED version is not specified.', 'error', 'hed-select-flash');
         return false;
     }
     return true;
@@ -116,6 +126,7 @@ function hideOtherHEDVersionFileUpload() {
  */
 function populateHEDVersionsDropdown(hedVersions) {
     let hedVersionDropdown = $('#hed-version');
+    $('#hed-version').empty()
     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>');
diff --git a/hedweb/hed/web/templates/js/hedstring-form.js b/hedweb/hed/web/templates/js/hedstring-form.js
new file mode 100644
index 000000000..19704c7ef
--- /dev/null
+++ b/hedweb/hed/web/templates/js/hedstring-form.js
@@ -0,0 +1,90 @@
+
+$(function () {
+    prepareForm();
+});
+
+/**
+ * Submits the form for tag comparison if we have a valid file.
+ */
+$('#hedstring-submit').on('click', function () {
+   if (!hedStringIsSpecified()) {
+        flashMessageOnScreen('Must give a non-empty hedstring.  See above.', 'error',
+            'hedstring-submit-flash')
+    } else {
+        submitHedStringForm();
+    }
+});
+
+/**
+ * Resets the fields in the form.
+ */
+function clearForm() {
+    $('#hedstring-form')[0].reset();
+    clearFormFlashMessages();
+    hideOtherHEDVersionFileUpload()
+}
+
+/**
+ * Clear the flash messages that aren't related to the form submission.
+ */
+function clearFormFlashMessages() {
+    clearHedSelectFlashMessages();
+    flashMessageOnScreen('', 'success', 'hedstring-flash');
+    flashMessageOnScreen('', 'success', 'hedstring-submit-flash');
+}
+
+/**
+ * Checks to see if a hedstring has been specified.
+ */
+function hedStringIsSpecified() {
+/*    let jsonFile = $('#json-file');
+    if (jsonFile[0].files.length === 0) {
+        flashMessageOnScreen('JSON is not specified.', 'error', 'json-flash');
+        return false;
+    }*/
+    return true;
+}
+
+/**
+ * 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 prepareForm() {
+    clearForm();
+    getHedVersions()
+    hideOtherHEDVersionFileUpload()
+}
+
+/**
+ * Submit the form and return the validation results. If there are issues then they are returned in an attachment
+ * file.
+ */
+function submitHedStringForm() {
+    let stringForm = document.getElementById("hedstring-form");
+    let formData = new FormData(stringForm);
+    clearFormFlashMessages();
+    flashMessageOnScreen('HED string is being processed ...', 'success', 'hedstring-submit-flash')
+    $.ajax({
+            type: 'POST',
+            url: "{{url_for('route_blueprint.get_hedstring_results')}}",
+            data: formData,
+            contentType: false,
+            processData: false,
+            dataType: 'json',
+            success: function (hedInfo) {
+                clearFormFlashMessages();
+                if (hedInfo['hedstring-result']) {
+                    $('#hedstring-result').val(hedInfo['hedstring-result'])
+                    flashMessageOnScreen('Processing completed', 'success', 'hedstring-submit-flash')
+                } else if (hedInfo['message'])
+                    flashMessageOnScreen(hedInfo['message'], 'error', 'hedstring-submit-flash')
+                else {
+                    flashMessageOnScreen('Server could not respond to this request', 'error', 'hedstring-submit-flash')
+                }
+            },
+            error: function (jqXHR) {
+                flashMessageOnScreen(jqXHR.responseJSON.message, 'error', 'hedstring-submit-flash')
+            }
+        }
+    )
+}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/js/json-input.js b/hedweb/hed/web/templates/js/json-input.js
index 32b81d568..19eb3d57b 100644
--- a/hedweb/hed/web/templates/js/json-input.js
+++ b/hedweb/hed/web/templates/js/json-input.js
@@ -5,14 +5,14 @@ const JSON_FILE_EXTENSIONS = ['json'];
  */
 $('#json-file').on('change',function () {
     let jsonPath = $('#json-file').val();
-    resetFormFlashMessages();
+    clearFlashMessages();
     if (cancelWasPressedInChromeFileUpload(jsonPath)) {
-        resetForm();
+        clearForm();
     }
     else if (fileHasValidExtension(jsonPath, JSON_FILE_EXTENSIONS)) {
         updateFileLabel(jsonPath, '#json-display-name');
     } else {
-        resetForm();
+        clearForm();
         flashMessageOnScreen('Please upload a JSON dictionary (.json)', 'error', 'json-flash');
     }
 });
@@ -27,7 +27,7 @@ function clearJsonFileLabel() {
 /**
  * Resets the flash messages that aren't related to the form submission.
  */
-function clearJsonFlashMessage() {
+function clearJsonInputFlashMessages() {
     flashMessageOnScreen('', 'success', 'json-flash');
 }
 
@@ -35,14 +35,3 @@ function getJsonFileLabel() {
     return $('#json-file')[0].files[0].name;
 }
 
-/**
- * Checks to see if a dictionary has been specified.
- */
-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
index 60ee07e37..417bc9c46 100644
--- a/hedweb/hed/web/templates/js/multi-file-input.js
+++ b/hedweb/hed/web/templates/js/multi-file-input.js
@@ -321,14 +321,14 @@
         '   <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>';
+    tBtnDefault = '<button field_type="{field_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' +
+        '        <button field_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' +
@@ -354,11 +354,11 @@
         '    <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}" ' +
+    tActionDelete = '<button field_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}">' +
+    tActionUpload = '<button field_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' +
+    tZoom = '<button field_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' +
@@ -366,7 +366,7 @@
         '   {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' +
+        '    <object class="file-object" data="{data}" field_type="{field_type}" width="{width}" height="{height}">\n' +
         '       ' + DEFAULT_PREVIEW + '\n' +
         '    </object>\n' +
         '   {footer}\n' +
@@ -383,7 +383,7 @@
     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' +
+        '       <source src="{data}" field_type="{field_type}">\n' +
         '       ' + DEFAULT_PREVIEW + '\n' +
         '   </video>\n' +
         '   {footer}\n' +
@@ -391,21 +391,21 @@
     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' +
+        '       <source src="' + '{data}' + '" field_type="{field_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 class="file-object" field_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' +
+        '   <object class="file-object" data="{data}" field_type="{field_type}" width="{width}" height="{height}">\n' +
         '       <param name="movie" value="{caption}" />\n' +
         OBJECT_PARAMS + '         ' + DEFAULT_PREVIEW + '\n' +
         '   </object>\n' +
@@ -636,7 +636,7 @@
             }
             $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.' +
+                'You must set an input <code>field_type = file</code> for <b>bootstrap-fileinput</b> plugin to initialize.' +
                 '</div>';
             self.$element.after($exception);
             return false;
@@ -2172,7 +2172,7 @@
             if (!isEmpty(label)) {
                 label = ' <span class="' + self.buttonLabelClass + '">' + label + '</span>';
             }
-            return tmplt.replace('{type}', btnType)
+            return tmplt.replace('{field_type}', btnType)
                 .replace('{css}', css)
                 .replace('{title}', title)
                 .replace('{status}', status)
@@ -2730,7 +2730,7 @@
         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.',
+        msgInvalidFileType: 'Invalid field_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',
@@ -2753,7 +2753,7 @@
      * Convert automatically file inputs with class 'file' into a bootstrap fileinput control.
      */
     $(document).ready(function () {
-        var $input = $('input.file[type=file]');
+        var $input = $('input.file[field_type=file]');
         if ($input.length) {
             $input.fileinput();
         }
diff --git a/hedweb/hed/web/templates/js/schema-form.js b/hedweb/hed/web/templates/js/schema-form.js
index 9eb884839..f9f184c58 100644
--- a/hedweb/hed/web/templates/js/schema-form.js
+++ b/hedweb/hed/web/templates/js/schema-form.js
@@ -3,7 +3,7 @@ const SCHEMA_MEDIAWIKI_EXTENSION = 'mediawiki';
 const DEFAULT_XML_URL = "https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED7.1.1.xml";
 
 $(function () {
-    prepareSchemaForm();
+    prepareForm();
 });
 
 /**
@@ -13,14 +13,14 @@ $('#schema-file').on('change', function () {
     updateFileLabel($('#schema-file').val(), '#schema-file-display-name');
     $('#schema-file-option').prop('checked', true);
     clearSchemaSubmitFlash()
-    updateFormGui();
+    updateForm();
 });
 
 $('#schema-url').on('change', function () {
     updateFileLabel($('#schema-url').val(), '#schema-url-display-name');
     $('#schema-url-option').prop('checked', true);
     clearSchemaSubmitFlash()
-    updateFormGui();
+    updateForm();
 });
 
 /**
@@ -31,7 +31,7 @@ $('#schema-conversion-submit').on('click', function () {
         flashMessageOnScreen('No valid source input file.  See above.', 'error',
             'schema-submit-flash')
     } else {
-        submitSchemaConversionForm();
+        submitConversionForm();
     }
 });
 
@@ -43,18 +43,39 @@ $('#schema-check-submit').on('click', function () {
         flashMessageOnScreen('No valid source schema file.  See above.', 'error',
             'schema-submit-flash')
     } else {
-        submitSchemaComplianceCheckForm();
+        submitComplianceCheckForm();
     }
 });
 
 $('#schema-file-option').on('change', function () {
-    updateFormGui();
+    updateForm();
 });
 
 $('#schema-url-option').on('change',function () {
-    updateFormGui();
+    updateForm();
 });
 
+/**
+ * Clear the fields in the form.
+ */
+function clearForm() {
+    $('#schema-form')[0].reset();
+    $('#schema-url-option').prop('checked', false);
+    $('#schema-file-option').prop('checked', false);
+}
+
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ A * @param {String} message - If true, reset the flash message related to the submit button.
+ */
+function clearFormFlashMessages(resetSubmitFlash) {
+    flashMessageOnScreen('', 'success', 'schema-file-flash');
+    flashMessageOnScreen('', 'success', 'schema-url-flash');
+    if (resetSubmitFlash) {
+        flashMessageOnScreen('', 'success', 'schema-submit-flash');
+    }
+}
+
 function clearSchemaSubmitFlash() {
     flashMessageOnScreen('', 'success', 'schema-submit-flash')
 }
@@ -76,8 +97,7 @@ function convertToOutputName(original_filename) {
 function getSchemaFilename() {
     let checkRadio = document.querySelector('input[name="schema-upload-options"]:checked');
     if (checkRadio == null) {
-        flashMessageOnScreen('No source file specified.', 'error',
-            'schema-url-flash');
+        flashMessageOnScreen('No source file specified.', 'error', 'schema-url-flash');
         return "";
     }
     let checkRadioVal = checkRadio.id
@@ -85,8 +105,7 @@ function getSchemaFilename() {
     let schemaFileIsEmpty = schemaFile[0].files.length === 0;
     if (checkRadioVal === "schema-file-option") {
         if (schemaFileIsEmpty) {
-            flashMessageOnScreen('Schema file not specified.', 'error',
-                'schema-file-flash');
+            flashMessageOnScreen('Schema file not specified.', 'error', 'schema-file-flash');
             return '';
         }
 
@@ -109,45 +128,20 @@ function getSchemaFilename() {
  * 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 prepareSchemaForm() {
-    resetForm();
+function prepareForm() {
+    clearForm();
     $('#schema-url').val(DEFAULT_XML_URL);
 }
 
-/**
- * Resets the flash messages that aren't related to the form submission.
- A * @param {String} message - If true, reset the flash message related to the submit button.
- */
-function resetFlashMessages(resetSubmitFlash) {
-    flashMessageOnScreen('', 'success', 'schema-file-flash');
-    flashMessageOnScreen('', 'success', 'schema-url-flash');
-    if (resetSubmitFlash) {
-        flashMessageOnScreen('', 'success', 'schema-submit-flash');
-    }
-}
-
-/**
- * Resets the fields in the form.
- */
-function resetForm() {
-    $('#schema-form')[0].reset();
-    $('#schema-url-option').prop('checked', false);
-    $('#schema-file-option').prop('checked', false);
-}
-
-function splitExt(filename) {
-    const index = filename.lastIndexOf('.');
-    return (-1 !== index) ? [filename.substring(0, index), filename.substring(index + 1)] : [filename, ''];
-}
 
 /**
  * Submit the form and return the conversion results as an attachment
  */
-function submitSchemaConversionForm() {
+function submitConversionForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
     let display_name = convertToOutputName(getSchemaFilename())
-    resetFlashMessages(false);
+    clearFormFlashMessages(false);
     flashMessageOnScreen('Specification is being converted...', 'success',
         'schema-submit-flash')
     $.ajax({
@@ -168,11 +162,11 @@ function submitSchemaConversionForm() {
     ;
 }
 
-function submitSchemaComplianceCheckForm() {
+function submitComplianceCheckForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
     let display_name = convertToResultsName(getSchemaFilename(), 'HED3G_format_issues')
-    resetFlashMessages(false);
+    clearFormFlashMessages(false);
     flashMessageOnScreen('Checking schema for HED-3G compliance...', 'success',
         'schema-submit-flash')
     $.ajax({
@@ -192,7 +186,7 @@ function submitSchemaComplianceCheckForm() {
     )
 }
 
-function updateFormGui() {
+function updateForm() {
      let filename = getSchemaFilename();
      let isXMLFilename = fileHasValidExtension(filename, [SCHEMA_XML_EXTENSION]);
      let isMediawikiFilename = fileHasValidExtension(filename, [SCHEMA_MEDIAWIKI_EXTENSION]);
diff --git a/hedweb/hed/web/templates/js/spreadsheet-form.js b/hedweb/hed/web/templates/js/spreadsheet-form.js
index 394fbd7fb..b5431c53a 100644
--- a/hedweb/hed/web/templates/js/spreadsheet-form.js
+++ b/hedweb/hed/web/templates/js/spreadsheet-form.js
@@ -1,235 +1,124 @@
-
+const EXCEL_FILE_EXTENSIONS = ['xlsx', 'xls'];
+const TEXT_FILE_EXTENSIONS = ['tsv', 'txt'];
+const VALID_FILE_EXTENSIONS = ['xlsx', 'xls', 'tsv', 'txt']
 $(function () {
     prepareForm();
 });
 
 /**
- * Submits the form if the tag columns textbox is valid.
+ * Spreadsheet event handler function. Checks if the file uploaded has a valid spreadsheet extension.
  */
-$('#spreadsheet-validation-submit').on('click', function () {
-    if (spreadsheetIsSpecified() && tagColumnsTextboxIsValid() && hedSpecifiedWhenOtherIsSelected()) {
-        submitForm();
+$('#spreadsheet-file').on('change', function () {
+    let spreadsheet = $('#spreadsheet-file');
+    let spreadsheetPath = spreadsheet.val();
+    let spreadsheetFile = spreadsheet[0].files[0];
+    clearFlashMessages();
+    removeColumnTable()
+    if (!fileHasValidExtension(spreadsheetPath, VALID_FILE_EXTENSIONS)) {
+        clearForm();
+        flashMessageOnScreen('Upload a valid spreadsheet (.xlsx, .xls, .tsv, .txt)', 'error', 'spreadsheet-flash');
+        return
     }
-});
-
-
-/**
- * Clears tag column text boxes.
- */
-function clearTagColumnTextboxes() {
-    $('.textbox-group input[type="text"]').val('');
-}
-
-/**
- * Flash a message showing the number of column columns that contain tags.
- * @param {Array} tagColumnIndices - An array of indices of columns containing tags
- * @param {Array} requiredTagColumnIndices - An array of indices of columns containing required tags
- * contain tags.
- */
-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',
-            'tag-columns-flash');
-    } else {
-        flashMessageOnScreen(numberOfTagColumns + ' tag column(s) found', 'success',
-            'tag-columns-flash');
+    clearTagColumnTextboxes();
+    updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
+    if (fileHasValidExtension(spreadsheetPath, EXCEL_FILE_EXTENSIONS)) {
+        setColumnsInfo(spreadsheetFile, undefined, true, 'spreadsheet-flash');
+        showWorksheetSelect();
     }
-}
-
-/**
- * Gets the spreadsheet columns.
- * @param {Object} spreadsheetFile - A spreadsheet file.
- * @param {String} worksheetName - An Excel worksheet name.
- */
-function getColumnsInfo(spreadsheetFile, worksheetName) {
-    let formData = new FormData();
-    formData.append('spreadsheet-file', spreadsheetFile);
-    if (typeof worksheetName !== 'undefined') {
-        formData.append('worksheet-name', worksheetName);
+    else if (fileHasValidExtension(spreadsheetPath, TEXT_FILE_EXTENSIONS)) {
+        $('#worksheet-name').empty();
+        setColumnsInfo(spreadsheetFile, undefined, true, 'spreadsheet-flash');
+        hideWorksheetSelect();
     }
-    $.ajax({
-        type: 'POST',
-        url: "{{url_for('route_blueprint.get_spreadsheet_columns_info')}}",
-        data: formData,
-        contentType: false,
-        processData: false,
-        dataType: 'json',
-        success: function (columnsInfo) {
-            setComponentsRelatedToColumns(columnsInfo);
-            flashTagColumnCountMessage(columnsInfo['tag-column-indices'],
-                columnsInfo['required-tag-column-indices']);
-        },
-        error: function (jqXHR) {
-            console.log(jqXHR.responseJSON.message);
-            flashMessageOnScreen('Spreadsheet could not be processed.', 'error',
-                'spreadsheet-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
- * first worksheet.
- * @param {Object} workbookFile - An Excel workbook file.
- */
-function getWorksheetsInfo(workbookFile) {
-    let formData = new FormData();
-    formData.append('spreadsheet-file', workbookFile);
-    $.ajax({
-        type: 'POST',
-        url: "{{url_for('route_blueprint.get_worksheets_info')}}",
-        data: formData,
-        contentType: false,
-        processData: false,
-        dataType: 'json',
-        success: function (worksheetsInfo) {
-            populateWorksheetSelectbox(worksheetsInfo['worksheet-names']);
-            setComponentsRelatedToColumns(worksheetsInfo);
-            flashWorksheetNumberMessage(worksheetsInfo['worksheet-names']);
-            flashTagColumnCountMessage(worksheetsInfo['tag-column-indices'],
-                worksheetsInfo['required-tag-column-indices']);
-        },
-        error: function (jqXHR) {
-            console.log(jqXHR);
-            // console.log(jqXHR.responseJSON.message);
-            flashMessageOnScreen('Spreadsheet could not be processed.', 'error',
-                'spreadsheet-validation-submit-flash');
-        }
-    });
-}
-
+});
 
 /**
- * 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
- * in the spreadsheet. The keys are the column names and the values are the indices.
+ * Submits the form if the tag columns textbox is valid.
  */
-function populateRequiredTagColumnTextboxes(requiredTagColumnIndices) {
-    for (let key in requiredTagColumnIndices) {
-        $('#' + key.toLowerCase() + '-column').val(requiredTagColumnIndices[key].toString());
+$('#spreadsheet-validation-submit').on('click', function () {
+    if (fileIsSpecified('#spreadsheet-file', 'spreadsheet-flash', 'Spreadsheet is not specified.') &&
+        tagColumnsTextboxIsValid() && hedSpecifiedWhenOtherIsSelected()) {
+        submitForm();
     }
-}
+});
 
 /**
- * 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
- * columns.
+ * 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.
  */
-function populateTagColumnsTextbox(tagColumnIndices) {
-    $('#tag-columns').val(tagColumnIndices.sort().map(String));
-}
-
+$('#worksheet-name').on('change', function () {
+    let spreadsheetFile = $('#spreadsheet-file')[0].files[0];
+    let worksheetName = $('#worksheet-name option:selected').text();
+    // $('#worksheet-name').val(worksheetName)
+    clearFlashMessages();
+    setColumnsInfo(spreadsheetFile, worksheetName, false, 'spreadsheet-flash');
+});
 
 /**
- * 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.
+ * Clear the fields in the form.
  */
-function prepareForm() {
-    resetForm();
-    getHEDVersions()
+function clearForm() {
+    $('#spreadsheet-form')[0].reset();
+    $('#spreadsheet-display-name').text('');
+    $('#worksheet-name').empty();
+    clearTagColumnTextboxes();
     hideColumnNames();
-    hideOtherHEDVersionFileUpload();
+    hideWorksheetSelect()
+    hideOtherHEDVersionFileUpload()
 }
 
 /**
- * Resets the flash messages that aren't related to the form submission.
+ * Clear the flash messages that aren't related to the form submission.
  */
-function resetFormFlashMessages() {
+function clearFlashMessages() {
+    clearColumnInfoFlashMessages();
+    clearHedSelectFlashMessages();
     flashMessageOnScreen('', 'success', 'spreadsheet-flash');
-    flashMessageOnScreen('', 'success', 'worksheet-flash');
-    resetTagColumnMessages();
-    flashMessageOnScreen('', 'success', 'hed-flash');
     flashMessageOnScreen('', 'success', 'spreadsheet-validation-submit-flash');
 }
 
 /**
- * Resets the fields in the form.
+ * Hides  worksheet select section in the form.
  */
-function resetForm() {
-    $('#spreadsheet-form')[0].reset();
-    clearSpreadsheetFileLabel();
-    clearWorksheetSelectbox();
-    hideColumnNames();
-    hideOtherHEDVersionFileUpload()
+function hideWorksheetSelect() {
+    $('#worksheet-select').hide();
 }
 
 /**
- * Resets the flash messages that aren't related to the form submission.
+ * Populate the Excel worksheet select box.
+ * @param {Array} worksheetNames - An array containing the Excel worksheet names.
  */
-function resetTagColumnMessages() {
-    flashMessageOnScreen('', 'success', 'tag-columns-flash');
+function populateWorksheetDropdown(worksheetNames) {
+    if (Array.isArray(worksheetNames) && worksheetNames.length > 0) {
+        let worksheetDropdown = $('#worksheet-name');
+        showWorksheetSelect();
+        worksheetDropdown.empty();
+        for (let i = 0; i < worksheetNames.length; i++) {
+            $('#worksheet-name').append(new Option(worksheetNames[i], worksheetNames[i]) );
+        }
+    }
 }
 
 /**
- * Sets the components related to Excel worksheet columns when they are all empty.
+ * 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 setComponentsRelatedToEmptyColumnNames() {
-    clearTagColumnTextboxes();
-    setHasColumnNamesCheckbox(false);
+function prepareForm() {
+    clearForm();
+    getHedVersions()
     hideColumnNames();
+    hideWorksheetSelect();
+    hideOtherHEDVersionFileUpload();
 }
 
-/**
- * Sets the components related to the spreadsheet tag column indices when they are empty.
- */
-function setComponentsRelatedToEmptyTagColumnIndices() {
-    $('#tag-columns').val('2');
-}
-
-/**
- * Sets the components related to the spreadsheet columns when they are not empty.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- */
-function setComponentsRelatedToNonEmptyColumnNames(columnNames) {
-    showColumnNames(columnNames)
-    setHasColumnNamesCheckbox(true);
-}
-
-/**
- * Sets the components related to the Excel worksheet columns.
- * @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 setComponentsRelatedToColumns(columnsInfo) {
-    clearTagColumnTextboxes();
-    if (columnNamesAreEmpty(columnsInfo['column-names'])) {
-        setComponentsRelatedToEmptyColumnNames();
-    } else {
-        setComponentsRelatedToNonEmptyColumnNames(columnsInfo['column-names']);
-    }
-    if (tagColumnsIndicesAreEmpty(columnsInfo['tag-column-indices'])) {
-        setComponentsRelatedToEmptyTagColumnIndices();
-    } else {
-        populateTagColumnsTextbox(columnsInfo['tag-column-indices']);
-    }
-    if (Object.keys(columnsInfo['required-tag-column-indices']).length !== 0) {
-        populateRequiredTagColumnTextboxes(columnsInfo['required-tag-column-indices']);
-    }
-}
 
 /**
- * Sets the spreadsheet has column names checkbox to false.
- * @param {boolean} Box is checked if true and unchecked if false
+ * Show the worksheet select section.
  */
-function setHasColumnNamesCheckbox(value) {
-    $('#has-column-names').prop('checked', value);
+function showWorksheetSelect() {
+    $('#worksheet-select').show();
 }
 
-/**
- * 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 tagColumnsIndicesAreEmpty(tagColumnsIndices) {
-    if (tagColumnsIndices.length > 0) {
-        return false;
-    }
-    return true;
-}
 
 /**
  * Submit the form and return the validation results. If there are issues then they are returned in an attachment
@@ -238,14 +127,15 @@ function tagColumnsIndicesAreEmpty(tagColumnsIndices) {
 function submitForm() {
     let spreadsheetForm = document.getElementById("spreadsheet-form");
     let formData = new FormData(spreadsheetForm);
-    let worksheetName = $('#worksheet-name option:selected').text();
+    let worksheetName = $('#worksheet-select option:selected').text();
+    formData.append('worksheet-selected', worksheetName)
     let prefix = 'issues';
     if(worksheetName) {
         prefix = prefix + '_worksheet_' + worksheetName;
     }
     let spreadsheetFile = $('#spreadsheet-file')[0].files[0].name;
     let display_name = convertToResultsName(spreadsheetFile, prefix)
-    resetFormFlashMessages();
+    clearFlashMessages();
     flashMessageOnScreen('Worksheet is being validated ...', 'success',
         'spreadsheet-validation-submit-flash')
     $.ajax({
@@ -264,22 +154,3 @@ function submitForm() {
         }
     )
 }
-
-/**
- * Checks to see if the tag columns textbox has valid input. Valid input is an integer or a comma-separated list of
- * integers that are the column indices in a Excel worksheet that contain HED tags.
- * @returns {boolean} - True if the tags columns textbox is valid.
- */
-function tagColumnsTextboxIsValid() {
-    let otherTagColumns = $('#tag-columns').val().trim();
-    let valid = true;
-    if (!isEmptyStr(otherTagColumns)) {
-        let pattern = new RegExp('^([ \\d]+,)*[ \\d]+$');
-        let valid = pattern.test(otherTagColumns);
-        if (!valid) {
-            flashMessageOnScreen('Tag column(s) must be a number or a comma-separated list of numbers',
-                'error', 'tag-columns-flash')
-        }
-    }
-    return valid;
-}
diff --git a/hedweb/hed/web/templates/js/spreadsheet-input.js b/hedweb/hed/web/templates/js/spreadsheet-input.js
deleted file mode 100644
index 18e97ce12..000000000
--- a/hedweb/hed/web/templates/js/spreadsheet-input.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * This code handles the input of a spreadsheet name and worksheet names and displays table headers. It has
- * the form handling for spreadsheet-input.html.
- * @type {string[]}
- */
-const EXCEL_FILE_EXTENSIONS = ['xlsx', 'xls'];
-const TEXT_FILE_EXTENSIONS = ['tsv', 'txt'];
-const spreadsheetInputIDs = ['spreadsheet-file', 'spreadsheet-display-name', 'spreadsheet-flash',
-                             'worksheet-name', 'columns-names-table']
-
-/**
- * Spreadsheet event handler function. Checks if the file uploaded has a valid spreadsheet extension.
- */
-$('#spreadsheet-file').on('change', function () {
-    let spreadsheet = $('#spreadsheet-file');
-    let spreadsheetPath = spreadsheet.val();
-    let spreadsheetFile = spreadsheet[0].files[0];
-    resetFormFlashMessages();
-    if (cancelWasPressedInChromeFileUpload(spreadsheetPath)) {
-        resetForm();
-    }
-    else if (fileHasValidExtension(spreadsheetPath, EXCEL_FILE_EXTENSIONS)) {
-        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
-        getWorksheetsInfo(spreadsheetFile);
-    }
-    else if (fileHasValidExtension(spreadsheetPath, TEXT_FILE_EXTENSIONS)) {
-        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
-        clearWorksheetSelectbox();
-        getColumnsInfo(spreadsheetFile, '');
-    } else {
-        resetForm();
-        flashMessageOnScreen('Please upload a excel or text spreadsheet (.xlsx, .xls, .tsv, .txt)',
-            'error', 'spreadsheet-flash');
-    }
-});
-
-
-/**
- * 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').on('change', function () {
-    let spreadsheetFile = $('#spreadsheet-file')[0].files[0];
-    let worksheetName = $('#worksheet-name option:selected').text();
-    resetFormFlashMessages();
-    getColumnsInfo(spreadsheetFile, worksheetName);
-});
-
-/**
- * Clears the spreadsheet file label.
- */
-function clearSpreadsheetFileLabel() {
-    $('#spreadsheet-display-name').text('');
-}
-
-/**
- * Clears the worksheet select box.
- */
-function clearWorksheetSelectbox() {
-    $('#worksheet-name').empty();
-}
-
-/**
- * Flash a message showing the number of worksheets in an Excel workbook.
- * @param {Array} worksheetNames - An array containing the names of Excel workbook worksheets.
- */
-function flashWorksheetNumberMessage(worksheetNames) {
-    let numberOfWorksheets = worksheetNames.length.toString();
-    flashMessageOnScreen(numberOfWorksheets + ' worksheet(s) found',
-        'success', 'worksheet-flash');
-}
-
-/**
- * Hides  columns section in the form.
- */
-function hideColumnNames() {
-    $('#column-names').hide();
-}
-
-/**
- * Populates a table containing one row.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- * @param {String} tableID - String containing the ID of the table
- */
-function populateTableHeaders(columnNames, tableID) {
-    let columnTable = $('#columns-names-table');
-    let columnNamesRow = $('<tr/>');
-    let numberOfColumnNames = columnNames.length;
-    columnTable.empty();
-    for (let i = 0; i < numberOfColumnNames; i++) {
-        columnNamesRow.append('<td>' + columnNames[i] + '</td>');
-    }
-    columnTable.append(columnNamesRow);
-}
-
-/**
- * Populate the Excel worksheet select box.
- * @param {Array} worksheetNames - An array containing the Excel worksheet names.
- */
-function populateWorksheetSelectbox(worksheetNames) {
-    let worksheetSelectbox = $('#worksheet-name');
-    let numberOfWorksheetNames = worksheetNames.length;
-    worksheetSelectbox.empty();
-    for (let i = 0; i < numberOfWorksheetNames; i++) {
-        worksheetSelectbox.append(new Option(worksheetNames[i], worksheetNames[i]));
-    }
-}
-
-/**
- * Resets the flash messages that aren't related to the form submission.
- */
-function resetSpreadsheetFlashMessage() {
-    flashMessageOnScreen('', 'success', 'spreadsheet-flash');
-}
-
-/**
- * Sets the components related to the spreadsheet columns when they are not empty.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- */
-function showColumnNames(columnNames) {
-    populateTableHeaders(columnNames);
-    $('#column-names').show();
-}
-
-/**
- * Checks to see if a spreadsheet has been specified.
- */
-function spreadsheetIsSpecified() {
-    let spreadsheetFile = $('#spreadsheet-file');
-    if (spreadsheetFile[0].files.length === 0) {
-        flashMessageOnScreen('Spreadsheet is not specified.', 'error',
-            'spreadsheet-flash');
-        return false;
-    }
-    return true;
-}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/json-input.html b/hedweb/hed/web/templates/json-input.html
index 31fbe849e..642e6888a 100644
--- a/hedweb/hed/web/templates/json-input.html
+++ b/hedweb/hed/web/templates/json-input.html
@@ -1,6 +1,7 @@
 {% macro create_json_input() %}
+    <h3>Upload BIDS-style JSON dictionary:</h3>
         <div class="form-group">
-            <label for="json-file">Select JSON-formatted dictionary or side-car:</label><br>
+            <label class="secondary-label" for="json-file">JSON file:</label>
             <label class="btn-group file-browse-group">
                 <button for="json-file">Browse ...</button>
                 <label id="json-display-name"></label>
diff --git a/hedweb/hed/web/templates/layout.html b/hedweb/hed/web/templates/layout.html
index d0fee1655..4d506fc30 100644
--- a/hedweb/hed/web/templates/layout.html
+++ b/hedweb/hed/web/templates/layout.html
@@ -25,14 +25,15 @@
 <body>
 <section class="page-header">
     <h1 class="project-name">Hierarchical Event Descriptor (HED) online tools</h1>
-    <h2 class="project-tagline">Online tools for HED schema, HED tags, and event files</h2>
-    <a href="{{ url_for('route_blueprint.render_home_page') }}" class="btn">HED tools online</a>
-     <a href="{{ url_for('route_blueprint.render_events_form') }}" class="btn">Validate events</a>
-    <a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}" class="btn">Validate spreadsheet</a>
+    <h2 class="project-tagline">Web-based tools for HED schema, HED tags, and event files</h2>
+    <a href="{{ url_for('route_blueprint.render_home_page') }}" class="btn">HED tools home</a>
+    <a href="{{ url_for('route_blueprint.render_events_form') }}" class="btn">Validate events</a>
     <a href="{{ url_for('route_blueprint.render_dictionary_form') }}" class="btn">Validate dictionary</a>
+    <a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}" class="btn">Validate spreadsheet</a>
+    <a href="{{ url_for('route_blueprint.render_hedstring_form') }}" class="btn">Process string</a>
     <a href="{{ url_for('route_blueprint.render_schema_form') }}" class="btn">Process schema</a>
-   <!-- <a href="{{ url_for('route_blueprint.render_help_page') }}" class="btn">HED tools help</a> -->
-    <a href="https://github.com/hed-standard" class="btn">HED standard</a>
+    <!-- <a href="{{ url_for('route_blueprint.render_help_page') }}" class="btn">HED tools help</a>
+    <a href="https://github.com/hed-standard" class="btn">HED standard</a> -->
 </section>
 <section class="main-content">
     {% block content %}
diff --git a/hedweb/hed/web/templates/schema.html b/hedweb/hed/web/templates/schema.html
index 560b4c9b7..a0913046b 100644
--- a/hedweb/hed/web/templates/schema.html
+++ b/hedweb/hed/web/templates/schema.html
@@ -1,6 +1,11 @@
 {% extends "layout.html" %}
 {% block content %}
 <h2>Process HED Schema</h2>
+    <p>
+        <i>Select a HED schema source URL or upload a file and pick an action.<br>
+        You will see either a downloadable results file or a status message.
+        </i>
+    </p>
     <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() }}"/>
diff --git a/hedweb/hed/web/templates/spreadsheet-input.html b/hedweb/hed/web/templates/spreadsheet-input.html
deleted file mode 100644
index 67bbb8d3d..000000000
--- a/hedweb/hed/web/templates/spreadsheet-input.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% macro spreadsheet_input() %}
-        <div class="form-group">
-            <label for="spreadsheet-file">Select spreadsheet (Excel or tab-separated):</label><br>
-            <label class="btn-group file-browse-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>
-
-{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/spreadsheet.html b/hedweb/hed/web/templates/spreadsheet.html
index af59e60fd..30eae2946 100644
--- a/hedweb/hed/web/templates/spreadsheet.html
+++ b/hedweb/hed/web/templates/spreadsheet.html
@@ -1,44 +1,50 @@
 {% extends "layout.html" %}
 {% from "hed-pulldown.html" import create_hed_pulldown %}
-{% from "spreadsheet-input.html" import spreadsheet_input %}
+{% from "column-info.html" import create_column_info %}
 {% block content %}
-    <h2>Validate spreadsheet containing HED tags</h2>
+    <h2>Validate a spreadsheet with HED tags</h2>
+        <p>
+        <i>Upload a spreadsheet (tsv, txt, or Excel) and select a worksheet.<br>
+        Enter the column numbers, select a HED schema, and press Validate.<br>
+        You will see either a downloadable results file or a status message.<br>
+        </i>
+    </p>
+
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
     <form id="spreadsheet-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
-        {{ spreadsheet_input() }}
-
+      <h3>Upload spreadsheet file (tab-separated or Excel):</h3>
         <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>
+            <label class="secondary-label" for="spreadsheet-file">Spreadsheet file:</label>
+            <label class="btn-group file-browse-group">
+                <button>Browse ...</button>
+                <label for="spreadsheet-file" id="spreadsheet-display-name"></label>
+                <input type="file" name="spreadsheet-file" id="spreadsheet-file"/>
+            </label>
+
+        <div class="form-group" name="worksheet-select" id="worksheet-select">
+            <label class="secondary-label" for="worksheet-name">Worksheet name:</label>
+            <select name="worksheet-name" id="worksheet-name"></select>
         </div>
+        <p class="flash" id="spreadsheet-flash"></p>
+         </div>
 
+        {{ create_column_info('true') }}
         {{ create_hed_pulldown() }}
 
+
+        <h3>Specify additional options:</h3>
         <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>
+                    <input type="checkbox" name="has-column-names" id="has-column-names" checked>
+                    <label for="has-column-names">Has column names</label>
+                </div><br>
+
                 <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>
+                    <input type="checkbox" name="check-for-warnings" id="check-for-warnings" checked>
+                    <label for="check-for-warnings">Include warnings</label>
                 </div>
             </div>
         </div>
@@ -49,9 +55,9 @@ <h2>Validate spreadsheet containing HED tags</h2>
         </div>
     </form>
     <script type="module">
+        {%  include 'js/column-info.js' %}
         {%  include 'js/form-helpers.js' %}
         {%  include 'js/hed-pulldown.js' %}
-        {%  include 'js/spreadsheet-input.js' %}
         {%  include 'js/spreadsheet-form.js' %}
     </script>
 
diff --git a/hedweb/hed/web/web_exceptions.py b/hedweb/hed/web/web_exceptions.py
new file mode 100644
index 000000000..123fd4bfb
--- /dev/null
+++ b/hedweb/hed/web/web_exceptions.py
@@ -0,0 +1,9 @@
+
+
+class HedError(Exception):
+    """Exception raised when a file cannot be parsed due to being malformed, file IO, etc."""
+    def __init__(self, error_type, message, error_code=None, severity=1):
+        self.error_type = error_type
+        self.message = message
+        self.error_code = error_code
+        self.severity = severity
diff --git a/hedweb/hed/web/web_utils.py b/hedweb/hed/web/web_utils.py
index 707966461..f68760b05 100644
--- a/hedweb/hed/web/web_utils.py
+++ b/hedweb/hed/web/web_utils.py
@@ -1,14 +1,15 @@
 import os
+import json
 import pathlib
 import tempfile
-import traceback
 from urllib.parse import urlparse
 from werkzeug.utils import secure_filename
-from flask import current_app, jsonify, Response
+from flask import current_app, Response
 
 from hed.util import hed_cache
+from hed.util.exceptions import HedFileError
 from hed.util.file_util import get_file_extension, delete_file_if_it_exists
-from hed.web.constants import common_constants
+from hed.web.constants import common
 
 app_config = current_app.config
 
@@ -31,6 +32,13 @@ def convert_number_str_to_list(number_str):
     return []
 
 
+def delete_file_no_exceptions(file_path):
+    try:
+        return delete_file_if_it_exists(file_path)
+    except:
+        return False
+
+
 def file_extension_is_valid(filename, accepted_file_extensions=None):
     """Checks the other extension against a list of accepted ones.
 
@@ -167,25 +175,24 @@ def generate_download_file_response(download_file, display_name=None, header=Non
     """
     if not display_name:
         display_name = download_file
-    try:
-        if not download_file:
-            return f"No download file given"
-
-        if not pathlib.Path(download_file).is_file():
-            return f"File {download_file} not found"
-
-        def generate():
-            with open(download_file, 'r', encoding='utf-8') as download:
-                if header:
-                    yield header
-                for line in download:
-                    yield line
-            delete_file_if_it_exists(download_file)
-        return Response(generate(), mimetype='text/plain charset=utf-8',
-                        headers={'Content-Disposition': f"attachment filename={display_name}",
-                                 'Category': category, 'Message': msg})
-    except:
-        return traceback.format_exc()
+
+    if not download_file:
+        raise HedFileError('FileInvalid', f"No download file given", "")
+
+    if not pathlib.Path(download_file).is_file():
+        raise HedFileError('FileDoesNotExist', f"File {download_file} not found", "")
+
+    def generate():
+        with open(download_file, 'r', encoding='utf-8') as download:
+            if header:
+                yield header
+            for line in download:
+                yield line
+            delete_file_no_exceptions(download_file)
+
+    return Response(generate(), mimetype='text/plain charset=utf-8',
+                    headers={'Content-Disposition': f"attachment filename={display_name}",
+                             'Category': category, 'Message': msg})
 
 
 def generate_filename(basename, prefix=None, suffix=None, extension=None):
@@ -223,6 +230,30 @@ def generate_filename(basename, prefix=None, suffix=None, extension=None):
     return filename
 
 
+def generate_text_response(download_text, category='success', msg=''):
+    """Generates a download other response.
+
+    Parameters
+    ----------
+    download_text: str
+        Text to be downloaded as part of the response.
+    category: str
+        Category of the message to be displayed ('Success', 'Error', 'Warning')
+    msg: str
+        Optional message to be displayed in the submit-flash-field
+
+    Returns
+    -------
+    response object
+        A response object containing the downloaded file.
+
+    """
+    headers = {'Category': category, 'Message': msg}
+    if len(download_text) > 0:
+        headers['Content-Length'] = len(download_text)
+    return Response(download_text, mimetype='text/plain charset=utf-8', headers=headers)
+
+
 def get_hed_path_from_pull_down(request):
     """Gets the hed path from a section of form that uses a pull-down box and hed_cache
     Parameters
@@ -235,23 +266,23 @@ def get_hed_path_from_pull_down(request):
     tuple: str, str
         The HED XML local path and the HED display file name
     """
-    if common_constants.HED_VERSION not in request.form:
+    if common.HED_VERSION not in request.form:
         hed_file_path = ''
         hed_display_name = ''
-    elif request.form[common_constants.HED_VERSION] != common_constants.HED_OTHER_VERSION_OPTION:
-        hed_file_path = hed_cache.get_path_from_hed_version(request.form[common_constants.HED_VERSION])
+    elif request.form[common.HED_VERSION] != common.HED_OTHER_VERSION_OPTION:
+        hed_file_path = hed_cache.get_path_from_hed_version(request.form[common.HED_VERSION])
         hed_display_name = os.path.basename(hed_file_path)
-    elif request.form[common_constants.HED_VERSION] == common_constants.HED_OTHER_VERSION_OPTION and \
-            common_constants.HED_XML_FILE in request.files:
-        hed_file_path = save_file_to_upload_folder(request.files[common_constants.HED_XML_FILE])
-        hed_display_name = request.files[common_constants.HED_XML_FILE].filename
+    elif request.form[common.HED_VERSION] == common.HED_OTHER_VERSION_OPTION and \
+            common.HED_XML_FILE in request.files:
+        hed_file_path = save_file_to_upload_folder(request.files[common.HED_XML_FILE])
+        hed_display_name = request.files[common.HED_XML_FILE].filename
     else:
         hed_file_path = ''
         hed_display_name = ''
     return hed_file_path, hed_display_name
 
 
-def get_optional_form_field(request, form_field_name, type=''):
+def get_optional_form_field(request, form_field_name, field_type=''):
     """Gets the specified optional form field if present.
 
     Parameters
@@ -260,7 +291,7 @@ def get_optional_form_field(request, form_field_name, type=''):
         A Request object containing user data from the validation form.
     form_field_name: string
         The name of the optional form field.
-    type: str
+    field_type: str
         Name of expected type: 'boolean' or 'string'
 
     Returns
@@ -270,11 +301,11 @@ def get_optional_form_field(request, form_field_name, type=''):
 
     """
     form_field_value = ''
-    if type == common_constants.BOOLEAN:
+    if field_type == common.BOOLEAN:
         form_field_value = False
         if form_field_name in request.form:
             form_field_value = True
-    elif type == common_constants.STRING:
+    elif field_type == common.STRING:
         if form_field_name in request.form:
             form_field_value = request.form[form_field_name]
     return form_field_value
@@ -305,30 +336,97 @@ def get_uploaded_file_path_from_form(request, file_key, valid_extensions=None):
     return uploaded_file_name, original_file_name
 
 
-def handle_http_error(error_code, error_message, as_text=False):
+def handle_error(ex, hed_info=None, title=None):
+    """Handles an error by logging and running an error as Response or simple string
+
+    Parameters
+    ----------
+    ex: Exception
+        The exception raised.
+    hed_info: dict
+        A dictionary of information.
+    title: str
+        A title to be included with the message.
+    Returns
+    -------
+    str
+
+    """
+
+    if not hed_info:
+        hed_info = {}
+    if hasattr(ex, 'error_type'):
+        error_code = ex.error_type
+    else:
+        error_code = type(ex).__name__
+
+    if not title:
+        title = ''
+    if hasattr(ex, 'message'):
+        message = ex.message
+    else:
+        message = str(ex)
+
+    hed_info['message'] = f"{title}[{error_code}: {message}]"
+    return json.dumps(hed_info)
+
+
+def handle_http_error(ex):
     """Handles an http error.
 
     Parameters
     ----------
-    error_code: string
-        The code associated with the error.
-    error_message: string
-        The message associated with the error.
-    as_text: Bool
-        If we should encode this as text or json.
+    ex: Exception
+        A class that extends python Exception class
     Returns
     -------
-    boolean
-        A tuple containing a HTTP response object and a code.
+    Response
+        A response object indicating the field_type of error
+
 
     """
+    if hasattr(ex, 'error_type'):
+        error_code = ex.error_type
+    else:
+        error_code = type(ex).__name__
+    if hasattr(ex, 'message'):
+        message = ex.message
+    else:
+        message = str(ex)
+    error_message = f"{error_code}: [{message}]"
     current_app.logger.error(error_message)
-    if as_text:
-        return error_message, error_code
-    return jsonify(message=error_message), error_code
+    return generate_text_response('', category='error', msg=error_message)
 
 
 def save_file_to_upload_folder(file_object, delete_on_close=False):
+    """Save a file_object to the upload folder.
+
+    Parameters
+    ----------
+    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
+    -------
+    string
+        The path to the other that was saved to the temporary folder.
+
+    """
+
+    if file_object.filename:
+        file_extension = get_file_extension(file_object.filename)
+        temporary_upload_file = tempfile.NamedTemporaryFile(suffix=file_extension, delete=delete_on_close,
+                                                            dir=current_app.config['UPLOAD_FOLDER'])
+        for line in file_object:
+            temporary_upload_file.write(line)
+        return temporary_upload_file.name
+    else:
+        raise("UnableToUploadFile", "File could not uploaded", "UnknownFile")
+
+
+def save_file_to_upload_folder_no_exception(file_object, delete_on_close=False):
     """Save a other to the upload folder.
 
     Parameters
diff --git a/hedweb/requirements.txt b/hedweb/requirements.txt
index ea73d03d7..c6e93d7f0 100644
--- a/hedweb/requirements.txt
+++ b/hedweb/requirements.txt
@@ -5,7 +5,7 @@ inflect==5.0.2
 et-xmlfile==1.0.1
 jdcal==1.4.1
 itsdangerous==1.1.0
-Jinja2==2.11.2
+Jinja2==2.11.3
 MarkupSafe==1.1.1
 Werkzeug==1.0.1
 WTForms==2.3.3


Test patch:
diff --git a/hedtools/examples/data/hed3_tags_single_sheet_test_long_to_short.xlsx b/hedtools/examples/data/hed3_tags_single_sheet_test_long_to_short.xlsx
new file mode 100644
index 000000000..874748d7b
Binary files /dev/null and b/hedtools/examples/data/hed3_tags_single_sheet_test_long_to_short.xlsx differ
diff --git a/hedtools/examples/data/hed3_tags_single_sheet_test_short_to_long.xlsx b/hedtools/examples/data/hed3_tags_single_sheet_test_short_to_long.xlsx
new file mode 100644
index 000000000..c5c8d1657
Binary files /dev/null and b/hedtools/examples/data/hed3_tags_single_sheet_test_short_to_long.xlsx differ
diff --git a/hedweb/examples/matlab/testGetServices.m b/hedweb/examples/matlab/testGetServices.m
new file mode 100644
index 000000000..3d0223385
--- /dev/null
+++ b/hedweb/examples/matlab/testGetServices.m
@@ -0,0 +1,41 @@
+%% Shows how to call hed-services to obtain a list of services
+host = 'http://127.0.0.1:5000';
+%host = 'https://hedtools.ucsd.edu/hed';
+csrf_url = [host '/hed-services']; 
+services_url = [host '/hed-services-submit'];
+
+%% Send an empty request to get the CSRF TOKEN and the session cookie
+[cookie, csrftoken] = getSessionInfo(csrf_url);
+
+%% Set the header and weboptions
+header = ["Content-Type" "application/json"; ...
+          "Accept" "application/json"; ...
+          "X-CSRFToken" csrftoken; ...
+          "Cookie" cookie];
+
+options = weboptions('MediaType', 'application/json', 'Timeout', 60, ...
+                     'HeaderFields', header);
+data = struct();
+data.service = 'get_services';
+data.hed_version = '7.1.2';
+data.check_for_warnings = true;
+
+%% Send the request and get the response
+response = webwrite(services_url, data, options);
+response = jsondecode(response);
+result = response.result;
+if isfield(response, 'error_type')
+    error_type = response.error_type;
+else
+    error_type = '';
+end
+if isfield(response, 'message')
+    message = response.message;
+else
+    message = '';
+end
+
+showServices(result)
+
+fprintf('Error report: [%s] %s\n', error_type, message)
+
diff --git a/hedweb/examples/matlab/testValidateDictionary.m b/hedweb/examples/matlab/testValidateDictionary.m
new file mode 100644
index 000000000..8d533f322
--- /dev/null
+++ b/hedweb/examples/matlab/testValidateDictionary.m
@@ -0,0 +1,44 @@
+%% Shows how to call hed-services to obtain a list of services
+host = 'http://127.0.0.1:5000';
+csrf_url = [host '/hed-services']; 
+services_url = [host '/hed-services-submit'];
+dictionary_file = '../data/good_dictionary.json';
+json_text = fileread(dictionary_file);
+
+%% Send an empty request to get the CSRF TOKEN and the session cookie
+[cookie, csrftoken] = getSessionInfo(csrf_url);
+
+%% Set the header and weboptions
+header = ["Content-Type" "application/json"; ...
+          "Accept" "application/json"; ...
+          "X-CSRFToken" csrftoken; ...
+          "Cookie" cookie];
+
+options = weboptions('MediaType', 'application/json', 'Timeout', Inf, ...
+                     'HeaderFields', header);
+data = struct();
+data.service = 'validate_json';
+data.hed_version = '7.1.2';
+%data.hed_version = '8.0.0-alpha.1';
+data.check_for_warnings = true;
+data.json_dictionary = string(json_text);
+
+%% Send the request and get the response for version 7.1.2
+response = webwrite(services_url, data, options);
+response = jsondecode(response);
+validation_errors = response.result.validation_errors;
+
+if isfield(response, 'error_type')
+    error_type = response.error_type;
+else
+    error_type = '';
+end
+if isfield(response, 'message')
+    message = response.message;
+else
+    message = '';
+end
+
+fprintf('Validation results for %s\n', dictionary_file)
+fprintf('%s\n', validation_errors)
+fprintf('Error report: [%s] %s\n', error_type, message)
diff --git a/hedweb/examples/matlab/testValidateStrings.m b/hedweb/examples/matlab/testValidateStrings.m
new file mode 100644
index 000000000..ae96a7352
--- /dev/null
+++ b/hedweb/examples/matlab/testValidateStrings.m
@@ -0,0 +1,44 @@
+%% Shows how to call hed-services to obtain a list of services
+host = 'http://127.0.0.1:5000';
+csrf_url = [host '/hed-services']; 
+services_url = [host '/hed-services-submit'];
+
+hedStrings = {['Event/Category/Experimental stimulus, ' ...
+               'Event/Label/Stimulus, Event/Description/Square display,'... 
+               'Sensory presentation/Visual, Invalid/Tag'], ...
+               ['Event/Category/Experimental stimulus, ' ...
+                'Event/Label/Stimulus, Event/Description/Square display, ' ...
+                'Sensory presentation/Visual']};
+%% Send an empty request to get the CSRF TOKEN and the session cookie
+[cookie, csrftoken] = getSessionInfo(csrf_url);
+
+%% Set the header and weboptions
+header = ["Content-Type" "application/json"; ...
+          "Accept" "application/json"; ...
+          "X-CSRFToken" csrftoken; ...
+          "Cookie" cookie];
+
+options = weboptions('MediaType', 'application/json', 'Timeout', 120, ...
+                     'HeaderFields', header);
+data = struct();
+data.service = 'validate_strings';
+%data.hed_version = '7.1.2';
+data.hed_version = '8.0.0-alpha.1';
+data.check_for_warnings = true;
+data.hed_strings = hedStrings;
+
+%% Send the request and get the response for version 7.1.2
+response = webwrite(services_url, data, options);
+response = jsondecode(response);
+validation_errors = response.result.validation_errors;
+error_type = response.error_type;
+message = response.message;
+fprintf('Validation results for %d HED strings...\n\n', length(validation_errors))
+for k = 1:length(validation_errors)
+    if ~isempty(validation_errors{k})
+        fprintf('%s\n', validation_errors{k});
+    else
+        fprintf('HED string %d has no errors\n', k);
+    end
+end
+fprintf('\nError report: [%s] %s\n', error_type, message)
diff --git a/hedweb/tests/data/ExcelMultipleSheets.xlsx b/hedweb/tests/data/ExcelMultipleSheets.xlsx
index 7afc9e7e3..3d8613777 100644
Binary files a/hedweb/tests/data/ExcelMultipleSheets.xlsx and b/hedweb/tests/data/ExcelMultipleSheets.xlsx differ
diff --git a/hedweb/tests/data/HED8.0.0-alpha.1.xml b/hedweb/tests/data/HED8.0.0-alpha.1.xml
index d71da44e6..dfd5b9210 100644
--- a/hedweb/tests/data/HED8.0.0-alpha.1.xml
+++ b/hedweb/tests/data/HED8.0.0-alpha.1.xml
@@ -2,7 +2,7 @@
 <HED version="8.0.0-alpha.1">
    <node>
       <name>Event</name>
-      <node suggestedTag="Task-property/Task-role">
+      <node>
          <name>Sensory-event</name>
          <description>Something perceivable by the participant. An event meant to be an experimental stimulus should also be tagged with Task-property/Task-role/Experiment-stimulus.</description>
       </node>
@@ -566,7 +566,13 @@
             <node>
                <name>Media</name>
                <node>
-                  <name>Sound-clip</name>
+                  <name>Media-clip</name>
+                  <node>
+                     <name>Audio-clip</name>
+                  </node>
+                  <node>
+                     <name>Video-clip</name>
+                  </node>
                </node>
                <node>
                   <name>Visualization</name>
@@ -662,18 +668,18 @@
                      <node>
                         <name>Keyboard</name>
                         <node>
-                           <name>Key</name>
+                           <name>Keyboard-key</name>
                            <node takesValue="true">
                               <name>#</name>
-                              <description>Value of key.</description>
+                              <description>Value of a keyboard key.</description>
                            </node>
                         </node>
-                     </node>
-                     <node>
-                        <name>Keypad</name>
-                        <node takesValue="true">
-                           <name>#</name>
-                           <description>Value of keypad key.</description>
+                        <node>
+                           <name>Keypad-key</name>
+                           <node takesValue="true">
+                              <name>#</name>
+                              <description>Value of keypad key.</description>
+                           </node>
                         </node>
                      </node>
                      <node>
@@ -1082,7 +1088,7 @@
    </node>
    <node>
       <name>Agent-property</name>
-      <node extensionAllowed="true" requireChild="true">
+      <node requireChild="true" extensionAllowed="true">
          <name>Cognitive-state</name>
          <node>
             <name>Alert</name>
@@ -1125,7 +1131,7 @@
             <name>Vegetative</name>
          </node>
       </node>
-      <node extensionAllowed="true" requireChild="true">
+      <node requireChild="true" extensionAllowed="true">
          <name>Emotional-state</name>
          <node>
             <name>Angry</name>
@@ -1218,10 +1224,19 @@
                <name>#</name>
             </node>
          </node>
+         <node>
+            <name>Gender</name>
+         </node>
          <node>
             <name>Sex</name>
-            <node takesValue="true">
-               <name>#</name>
+            <node>
+               <name>Female</name>
+            </node>
+            <node>
+               <name>Male</name>
+            </node>
+            <node>
+               <name>Intersex</name>
             </node>
          </node>
          <node>
@@ -1254,10 +1269,10 @@
          <name>Agent-task-role</name>
          <description>The role of the agent in the task.</description>
          <node>
-            <name>Experiment-controller</name>
+            <name>Experimental-controller</name>
          </node>
          <node>
-            <name>Experiment-participant</name>
+            <name>Experimental-participant</name>
          </node>
          <node>
             <name>Experimenter</name>
@@ -1345,7 +1360,7 @@
             </node>
             <node>
                <name>Temperature</name>
-               <node isNumeric="true" takesValue="true">
+               <node takesValue="true" isNumeric="true">
                   <name>#</name>
                </node>
             </node>
@@ -1406,14 +1421,14 @@
             <name>Rate-of-change</name>
             <node>
                <name>Acceleration</name>
-               <node isNumeric="true" takesValue="true" unitClass="acceleration">
+               <node takesValue="true" isNumeric="true" unitClass="acceleration">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Jerk-rate</name>
                <description>Rate at which the acceleration of an object changes with respect to time.</description>
-               <node isNumeric="true" takesValue="true" unitClass="jerk">
+               <node takesValue="true" isNumeric="true" unitClass="jerk">
                   <name>#</name>
                </node>
             </node>
@@ -1427,20 +1442,20 @@
             <node requireChild="true">
                <name>Refresh-rate</name>
                <description>Number of times per second a device displays an image. Also known as the frame rate.</description>
-               <node isNumeric="true" takesValue="true">
+               <node takesValue="true" isNumeric="true">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Temporal-rate</name>
                <description>Number of occurrences in a unit of time.</description>
-               <node isNumeric="true" takesValue="true">
+               <node takesValue="true" isNumeric="true">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Velocity</name>
-               <node isNumeric="true" takesValue="true" unitClass="speed">
+               <node takesValue="true" isNumeric="true" unitClass="speed">
                   <name>#</name>
                </node>
             </node>
@@ -1488,20 +1503,20 @@
             <name>Spatial</name>
             <node>
                <name>Direction</name>
-               <node isNumeric="true" takesValue="true">
+               <node takesValue="true" isNumeric="true">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Distance</name>
                <description>Distance from a specified position in a specified Direction or grouped with an item from which the distance is measured.</description>
-               <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+               <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Orientation</name>
-               <node isNumeric="true" takesValue="true">
+               <node takesValue="true" isNumeric="true">
                   <name>#</name>
                </node>
             </node>
@@ -1510,19 +1525,19 @@
                <description>Coordinates with respect a specified frame of reference or the default Screen-frame if no frame is given.</description>
                <node>
                   <name>X-position</name>
-                  <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+                  <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                      <name>#</name>
                   </node>
                </node>
                <node>
                   <name>Y-position</name>
-                  <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+                  <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                      <name>#</name>
                   </node>
                </node>
                <node>
                   <name>Z-position</name>
-                  <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+                  <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                      <name>#</name>
                   </node>
                </node>
@@ -1553,34 +1568,34 @@
                <name>Size</name>
                <node>
                   <name>Area</name>
-                  <node isNumeric="true" takesValue="true" unitClass="area">
+                  <node takesValue="true" isNumeric="true" unitClass="area">
                      <name>#</name>
                   </node>
                </node>
                <node>
                   <name>Length</name>
                   <description>A measurement of the size along the longest dimension.</description>
-                  <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+                  <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                      <name>#</name>
                   </node>
                </node>
                <node>
                   <name>Width</name>
                   <description>A measurement along the shortest of two or three dimensions.</description>
-                  <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+                  <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                      <name>#</name>
                   </node>
                </node>
                <node>
                   <name>Height</name>
                   <description>A measurement in a direction perpendicular to Length and Width in 3D. In 2D, it is perpendicular to both the Length and Height.</description>
-                  <node isNumeric="true" takesValue="true" unitClass="physicalLength">
+                  <node takesValue="true" isNumeric="true" unitClass="physicalLength">
                      <name>#</name>
                   </node>
                </node>
                <node>
                   <name>Volume</name>
-                  <node isNumeric="true" takesValue="true" unitClass="volume">
+                  <node takesValue="true" isNumeric="true" unitClass="volume">
                      <name>#</name>
                   </node>
                </node>
@@ -1600,14 +1615,14 @@
             <node>
                <name>Delay</name>
                <description>An indicator that the associated tag group represents something temporally offset from the current event. The delay value can be positive or negative and should always appear in a tag group indicating what is delayed.</description>
-               <node isNumeric="true" takesValue="true" unitClass="time">
+               <node takesValue="true" isNumeric="true" unitClass="time">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Duration</name>
                <description>Time extent of something.</description>
-               <node isNumeric="true" takesValue="true" unitClass="time">
+               <node takesValue="true" isNumeric="true" unitClass="time">
                   <name>#</name>
                </node>
             </node>
@@ -1623,7 +1638,7 @@
             </node>
             <node>
                <name>Temporal-value</name>
-               <node isNumeric="true" takesValue="true" unitClass="time">
+               <node takesValue="true" isNumeric="true" unitClass="time">
                   <name>#</name>
                </node>
             </node>
@@ -1768,10 +1783,10 @@
       <node>
          <name>Task-response-type</name>
          <description>How the participant response should be interpreted in terms of the task specification.</description>
-         <node relatedTag="Inappropriate">
+         <node>
             <name>Appropriate</name>
          </node>
-         <node relatedTag="Successful">
+         <node>
             <name>Correction</name>
          </node>
          <node>
@@ -1781,19 +1796,19 @@
             <name>Imagined</name>
             <description>This is used to identity something that only happened in the imagination of the participant as in imagined movements in motor imagery paradigms.</description>
          </node>
-         <node relatedTag="Appropriate">
+         <node>
             <name>Inappropriate</name>
          </node>
-         <node relatedTag="Near-miss">
+         <node>
             <name>Indeterminate</name>
          </node>
-         <node relatedTag="Near-miss">
+         <node>
             <name>Missed</name>
          </node>
-         <node relatedTag="Miss">
+         <node>
             <name>Near-miss</name>
          </node>
-         <node relatedTag="Correction">
+         <node>
             <name>Successful</name>
          </node>
       </node>
@@ -1887,7 +1902,7 @@
             </node>
          </node>
          <node requireChild="true">
-            <name>Defexp</name>
+            <name>Def-expand</name>
             <description>A label that specifically corresponds to a Definition name and indicates that this label is in a group containing an expanded definition and should not be expanded again.</description>
             <node takesValue="true">
                <name>#</name>
@@ -1907,6 +1922,12 @@
                <name>#</name>
             </node>
          </node>
+         <node>
+            <name>Experimental-note</name>
+            <node takesValue="true">
+               <name>#</name>
+            </node>
+         </node>
          <node requireChild="true">
             <name>ID</name>
             <description>An alphanumeric code usually used to identify a particular instance of an item.</description>
@@ -1950,6 +1971,12 @@
                   <name>#</name>
                </node>
             </node>
+            <node>
+               <name>Path-name</name>
+               <node takesValue="true">
+                  <name>#</name>
+               </node>
+            </node>
             <node>
                <name>Subject</name>
                <node takesValue="true">
@@ -1977,6 +2004,10 @@
             <name>Collection</name>
             <description>A tag designating a grouping of items such as in a set or list.</description>
          </node>
+         <node>
+            <name>Control-variable</name>
+            <description>An aspect of the experiment that is fixed throughout the study and usually is explicitly controlled.</description>
+         </node>
          <node>
             <name>Event-context</name>
             <description>A tag indicating the overall environment and setup of the experiment or a portion of the experiment. This event is often inserted into a recording at the beginning of an experimental record to describe information such as whether the experiment is conducted indoors or outdoors or whether the experiment is conducted in the real-world or in a controlled laboratory setting. Tags specifying subject metadata might also be included.</description>
@@ -1986,32 +2017,32 @@
             <description>An ordered list of events</description>
          </node>
          <node>
-            <name>Permutation</name>
-            <description>A tag used within a tag group also containing a Label tag to indicate that the Label represents events that are permutations of each other.</description>
+            <name>Experimental-condition</name>
+            <description>An aspect of the experiment or task that is to be varied during the experiment. Task-conditions are sometimes called independent variables or contrasts.</description>
          </node>
          <node>
-            <name>Recording</name>
-            <description>Indicates an uninterrupted block of time during which data was acquired using a fixed scanning protocol or sensor positioning. Recording tags are usually have temporal scope which is the entire recording.</description>
+            <name>Experimental-indicator</name>
+            <description>An aspect of the experiment or task that is measured as task conditions are varied during the experiment. Experiment indicators are sometimes called dependent variables.</description>
          </node>
          <node>
-            <name>Task</name>
-            <description>A tag used to indicate a linkage the structured activities performed as part of the experiment.</description>
+            <name>Experimental-intertrial</name>
+            <description>A tag used to indicate a part of the experiment between trials usually where nothing is happening.</description>
          </node>
          <node>
-            <name>Task-condition</name>
-            <description>An aspect of the experiment or task that is to be varied during the experiment. Task-conditions are sometimes called independent variables or contrasts.</description>
+            <name>Experimental-trial</name>
+            <description>A tag used to indicate a particular organizational part in the experimental design often containing a stimulus-response pair or stimulus-response-feedback triad.</description>
          </node>
          <node>
-            <name>Task-control</name>
-            <description>An aspect of the experiment that is fixed throughout the study and usually is explicitly controlled.</description>
+            <name>Permutation</name>
+            <description>A tag used within a tag group also containing a Label tag to indicate that the Label represents events that are permutations of each other.</description>
          </node>
          <node>
-            <name>Task-response</name>
-            <description>An aspect of the experiment or task that is measured as task conditions are varied during the experiment. Task-responses are sometimes called dependent variables.</description>
+            <name>Recording</name>
+            <description>Indicates an uninterrupted block of time during which data was acquired using a fixed scanning protocol or sensor positioning. Recording tags are usually have temporal scope which is the entire recording.</description>
          </node>
          <node>
-            <name>Task-trial</name>
-            <description>A tag used to indicate a particular organizational part in the experimental design often containing a stimulus-response pair.</description>
+            <name>Task</name>
+            <description>A tag used to indicate a linkage the structured activities performed as part of the experiment.</description>
          </node>
          <node>
             <name>Time-block</name>
@@ -2331,106 +2362,106 @@
       </node>
       <node extensionAllowed="true">
          <name>Categorical</name>
-         <node relatedTag="Normal">
+         <node>
             <name>Abnormal</name>
          </node>
-         <node relatedTag="None">
+         <node>
             <name>All</name>
          </node>
-         <node relatedTag="Symmetric">
+         <node>
             <name>Asymmetric</name>
          </node>
-         <node relatedTag="Incongruent">
+         <node>
             <name>Congruent</name>
          </node>
-         <node relatedTag="Unconstrained">
+         <node>
             <name>Constrained</name>
          </node>
-         <node relatedTag="Incorrect">
+         <node>
             <name>Correct</name>
          </node>
-         <node relatedTag="Shallow">
+         <node>
             <name>Deep</name>
          </node>
-         <node relatedTag="Ordered">
+         <node>
             <name>Disordered</name>
          </node>
-         <node relatedTag="True">
+         <node>
             <name>False</name>
          </node>
-         <node relatedTag="Famous">
+         <node>
             <name>Familiar</name>
             <description>Something known or encountered before.</description>
          </node>
-         <node relatedTag="Unfamiliar">
+         <node>
             <name>Famous</name>
             <description>Something known by many people but necessarily because of direct contact.</description>
          </node>
-         <node relatedTag="Medium">
+         <node>
             <name>High</name>
          </node>
-         <node relatedTag="Congruent">
+         <node>
             <name>Incongruent</name>
          </node>
-         <node relatedTag="Correct">
+         <node>
             <name>Incorrect</name>
          </node>
-         <node relatedTag="Valid">
+         <node>
             <name>Invalid</name>
          </node>
-         <node relatedTag="Voluntary">
+         <node>
             <name>Involuntary</name>
          </node>
-         <node relatedTag="Supraliminal">
+         <node>
             <name>Liminal</name>
          </node>
-         <node relatedTag="High">
+         <node>
             <name>Low</name>
          </node>
          <node>
             <name>Masked</name>
             <description>Ability to perceive influenced by presence of another stimulus</description>
          </node>
-         <node relatedTag="High">
+         <node>
             <name>Medium</name>
          </node>
-         <node relatedTag="Some">
+         <node>
             <name>None</name>
          </node>
-         <node relatedTag="Abnormal">
+         <node>
             <name>Normal</name>
          </node>
-         <node relatedTag="Positive">
+         <node>
             <name>Negative</name>
          </node>
-         <node relatedTag="Disordered">
+         <node>
             <name>Ordered</name>
          </node>
-         <node relatedTag="Negative">
+         <node>
             <name>Positive</name>
          </node>
-         <node relatedTag="Deep">
+         <node>
             <name>Shallow</name>
          </node>
-         <node relatedTag="None">
+         <node>
             <name>Some</name>
          </node>
-         <node relatedTag="Supraliminal">
+         <node>
             <name>Subliminal</name>
          </node>
-         <node relatedTag="Subliminal">
+         <node>
             <name>Supraliminal</name>
          </node>
-         <node relatedTag="Unsymmetric">
+         <node>
             <name>Symmetric</name>
          </node>
-         <node relatedTag="False">
+         <node>
             <name>True</name>
          </node>
-         <node relatedTag="Constrained">
+         <node>
             <name>Unconstrained</name>
          </node>
-         <node relatedTag="Famous">
+         <node>
             <name>Unfamiliar</name>
             <description>Something not known or encountered before.</description>
          </node>
@@ -2438,10 +2469,10 @@
             <name>Unmasked</name>
             <description>Ability to perceive not influenced by presence of another stimulus</description>
          </node>
-         <node relatedTag="Invalid">
+         <node>
             <name>Valid</name>
          </node>
-         <node relatedTag="Involuntary">
+         <node>
             <name>Voluntary</name>
          </node>
       </node>
@@ -2476,13 +2507,13 @@
             </node>
             <node>
                <name>Frequency</name>
-               <node isNumeric="true" takesValue="true" unitClass="frequency">
+               <node takesValue="true" isNumeric="true" unitClass="frequency">
                   <name>#</name>
                </node>
             </node>
             <node>
                <name>Loudness</name>
-               <node isNumeric="true" takesValue="true">
+               <node takesValue="true" isNumeric="true">
                   <name>#</name>
                </node>
             </node>
@@ -2579,21 +2610,21 @@
                   <name>HSV-color</name>
                   <node>
                      <name>Hue</name>
-                     <node isNumeric="true" takesValue="true">
+                     <node takesValue="true" isNumeric="true">
                         <name>#</name>
                         <description>Angular value between 0 and 360</description>
                      </node>
                   </node>
                   <node>
                      <name>Saturation</name>
-                     <node isNumeric="true" takesValue="true">
+                     <node takesValue="true" isNumeric="true">
                         <name>#</name>
                         <description>B value of RGB between 0 and 1</description>
                      </node>
                   </node>
                   <node>
                      <name>HSV-value</name>
-                     <node isNumeric="true" takesValue="true">
+                     <node takesValue="true" isNumeric="true">
                         <name>#</name>
                         <description>G value of RGB between 0 and 1</description>
                      </node>
@@ -2603,21 +2634,21 @@
                   <name>RGB-color</name>
                   <node>
                      <name>RGB-red</name>
-                     <node isNumeric="true" takesValue="true">
+                     <node takesValue="true" isNumeric="true">
                         <name>#</name>
                         <description>R value of RGB between 0 and 1</description>
                      </node>
                   </node>
                   <node>
                      <name>RGB-blue</name>
-                     <node isNumeric="true" takesValue="true">
+                     <node takesValue="true" isNumeric="true">
                         <name>#</name>
                         <description>B value of RGB between 0 and 1</description>
                      </node>
                   </node>
                   <node>
                      <name>RGB-green</name>
-                     <node isNumeric="true" takesValue="true">
+                     <node takesValue="true" isNumeric="true">
                         <name>#</name>
                         <description>G value of RGB between 0 and 1</description>
                      </node>
@@ -2626,7 +2657,7 @@
                <node>
                   <name>Grayscale</name>
                   <description>Indicates gray scale</description>
-                  <node isNumeric="true" takesValue="true">
+                  <node takesValue="true" isNumeric="true">
                      <name>#</name>
                      <description>White intensity between 0 and 1</description>
                   </node>
@@ -3204,34 +3235,10 @@
       </node>
    </node>
    <unitClasses>
-      <unitClass defaultUnits="s">
-         <name>time</name>
-         <units>
-            <unit SIUnit="true">second</unit>
-            <unit SIUnit="true" unitSymbol="true">s</unit>
-            <unit>day</unit>
-            <unit>minute</unit>
-            <unit>hour</unit>
-         </units>
-      </unitClass>
-      <unitClass defaultUnits="YYYY-MM-DDThh:mm:ss">
-         <name>dateTime</name>
-         <units>
-            <unit>YYYY-MM-DDThh:mm:ss</unit>
-         </units>
-      </unitClass>
-      <unitClass defaultUnits="hour:min">
-         <name>clockTime</name>
-         <units>
-            <unit>hour:min</unit>
-            <unit>hour:min:sec</unit>
-         </units>
-      </unitClass>
-      <unitClass defaultUnits="Hz">
-         <name>frequency</name>
+      <unitClass defaultUnits="m-per-s^2">
+         <name>acceleration</name>
          <units>
-            <unit SIUnit="true">hertz</unit>
-            <unit SIUnit="true" unitSymbol="true">Hz</unit>
+            <unit SIUnit="true" unitSymbol="true">m-per-s^2</unit>
          </units>
       </unitClass>
       <unitClass defaultUnits="radian">
@@ -3242,22 +3249,6 @@
             <unit>degree</unit>
          </units>
       </unitClass>
-      <unitClass defaultUnits="m">
-         <name>physicalLength</name>
-         <units>
-            <unit SIUnit="true">metre</unit>
-            <unit SIUnit="true" unitSymbol="true">m</unit>
-            <unit>foot</unit>
-            <unit>mile</unit>
-         </units>
-      </unitClass>
-      <unitClass defaultUnits="px">
-         <name>pixels</name>
-         <units>
-            <unit>pixel</unit>
-            <unit unitSymbol="true">px</unit>
-         </units>
-      </unitClass>
       <unitClass defaultUnits="m^2">
          <name>area</name>
          <units>
@@ -3266,30 +3257,25 @@
             <unit>pixel^2</unit>
          </units>
       </unitClass>
-      <unitClass defaultUnits="m^3">
-         <name>volume</name>
-         <units>
-            <unit SIUnit="true" unitSymbol="true">m^3</unit>
-         </units>
-      </unitClass>
-      <unitClass defaultUnits="m-per-s">
-         <name>speed</name>
+      <unitClass defaultUnits="$">
+         <name>currency</name>
          <units>
-            <unit SIUnit="true" unitSymbol="true">m-per-s</unit>
-            <unit unitSymbol="true">mph</unit>
-            <unit unitSymbol="true">kph</unit>
+            <unit>dollar</unit>
+            <unit unitSymbol="true">$</unit>
+            <unit>point</unit>
          </units>
       </unitClass>
-      <unitClass defaultUnits="m-per-s^2">
-         <name>acceleration</name>
+      <unitClass defaultUnits="YYYY-MM-DDThh:mm:ss">
+         <name>dateTime</name>
          <units>
-            <unit SIUnit="true" unitSymbol="true">m-per-s^2</unit>
+            <unit>YYYY-MM-DDThh:mm:ss</unit>
          </units>
       </unitClass>
-      <unitClass defaultUnits="m-per-s^3">
-         <name>jerk</name>
+      <unitClass defaultUnits="Hz">
+         <name>frequency</name>
          <units>
-            <unit unitSymbol="true">m-per-s^3</unit>
+            <unit SIUnit="true">hertz</unit>
+            <unit SIUnit="true" unitSymbol="true">Hz</unit>
          </units>
       </unitClass>
       <unitClass defaultUnits="dB">
@@ -3298,6 +3284,12 @@
             <unit unitSymbol="true">dB</unit>
          </units>
       </unitClass>
+      <unitClass defaultUnits="m-per-s^3">
+         <name>jerk</name>
+         <units>
+            <unit unitSymbol="true">m-per-s^3</unit>
+         </units>
+      </unitClass>
       <unitClass defaultUnits="cd">
          <name>luminousIntensity</name>
          <units>
@@ -3312,13 +3304,44 @@
             <unit SIUnit="true" unitSymbol="true">B</unit>
          </units>
       </unitClass>
-      <unitClass defaultUnits="$">
-         <name>currency</name>
+      <unitClass defaultUnits="m">
+         <name>physicalLength</name>
          <units>
-            <unit>dollar</unit>
-            <unit unitSymbol="true">$</unit>
-            <unit>point</unit>
-            <unit>fraction</unit>
+            <unit SIUnit="true">metre</unit>
+            <unit SIUnit="true" unitSymbol="true">m</unit>
+            <unit>foot</unit>
+            <unit>mile</unit>
+         </units>
+      </unitClass>
+      <unitClass defaultUnits="px">
+         <name>pixels</name>
+         <units>
+            <unit>pixel</unit>
+            <unit unitSymbol="true">px</unit>
+         </units>
+      </unitClass>
+      <unitClass defaultUnits="m-per-s">
+         <name>speed</name>
+         <units>
+            <unit SIUnit="true" unitSymbol="true">m-per-s</unit>
+            <unit unitSymbol="true">mph</unit>
+            <unit unitSymbol="true">kph</unit>
+         </units>
+      </unitClass>
+      <unitClass defaultUnits="s">
+         <name>time</name>
+         <units>
+            <unit SIUnit="true">second</unit>
+            <unit SIUnit="true" unitSymbol="true">s</unit>
+            <unit>day</unit>
+            <unit>minute</unit>
+            <unit>hour</unit>
+         </units>
+      </unitClass>
+      <unitClass defaultUnits="m^3">
+         <name>volume</name>
+         <units>
+            <unit SIUnit="true" unitSymbol="true">m^3</unit>
          </units>
       </unitClass>
    </unitClasses>
@@ -3483,5 +3506,73 @@
          <name>y</name>
          <description>SI unit submultiple representing 10^-24</description>
       </unitModifier>
+      <unitModifier>
+         <name>Schema attributes</name>
+      </unitModifier>
+      <unitModifier>
+         <name>defaultUnits</name>
+         <description>Default units for a tag.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>extensionAllowed</name>
+         <description>Users can add unlimited levels of child nodes under this tag.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>isNumeric</name>
+         <description>The tag hashtag placeholder must be replaced by a numerical value.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>position</name>
+         <description>Used to specify the order of the required and recommended tags in canonical order for display. The position attribute value should be an integer and the order can start at 0 or 1. Required or recommended tags without this attribute or with negative position will be shown after the others in canonical ordering.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>predicateType</name>
+         <description>One of propertyOf, subclassOf, passThrough -- used to facilitate mapping to OWL or RDF.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>recommended</name>
+         <description>A HED string tagging an event is recommended to include this tag.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>relatedTag</name>
+         <description>Additional HED tags suggested to be used with this tag. This attribute is used by tagging tools.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>requireChild</name>
+         <description>One of its descendants must be chosen to tag an event.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>required</name>
+         <description>Every HED string tagging an event should include this tag.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>SIUnit</name>
+         <description>Designates the name of an SI unit so it can be modified by multiple and submultiple names. Note that some units such as byte are designated as SI units although they are not part of the standard.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>SIUnitModifier</name>
+         <description>SI unit modifier indicating a multiple or submultiple of a base unit.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>SIUnitSymbolModifier</name>
+         <description>SI unit symbol modifier indicating a multiple or submultiple of a base unit symbol.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>suggestedTag</name>
+         <description>Additional HED tags suggested to be used with this tag. This attribute is used by tagging tools.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>takesValue</name>
+         <description>This tag will have a hashtag placeholder child in the schema which is expected to be replaced with a user-defined value.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>unique</name>
+         <description>Only one of this tag or its descendants can be used within a single tag group or event.</description>
+      </unitModifier>
+      <unitModifier>
+         <name>unitSymbol</name>
+         <description>Abbreviation or symbol representing a type of unit. Unit symbols  represent both the singular and the plural and thus cannot be pluralized.</description>
+      </unitModifier>
    </unitModifiers>
+   <epilogue>This is the new format for the mediawiki schema</epilogue>
 </HED>
diff --git a/hedweb/tests/data/bids_events.json b/hedweb/tests/data/bids_events.json
new file mode 100644
index 000000000..1d1c34109
--- /dev/null
+++ b/hedweb/tests/data/bids_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)))), (Agent-action, Experimental-participant, (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)",
+          "right-raised": "Description/Subject raised right hand thereby releasing right button, (Agent-action, Experimental-participant, (Lift, (Right-side, Hand)), (Release, (Right-side, Button)))",
+          "left-raised": "Description/Subject raised left hand thereby releasing left button, (Agent-action, Experimental-participant, (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/BCI acts randomly, (Definition/Random-selection, Experimental-condition, (Random, Predict)), Def/Random-selection",
+            "2": "Description/BCI was trained on data from stage 1., (Definition/Trained-on-random, (Experimental-condition)), Def/Trained-on-random",
+            "3": "Description/BCI was trained on data from stages 1 and 2., (Definition/Trained-on-all, (Experimental-condition)), Def/Trained-on-all"
+		}
+    },
+	"trial": {
+		"Description": "The number of the trial in the experiment.",
+		"HED": "Description/The number of the trial in the experiment, (Experimental-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/data/bids_events.tsv b/hedweb/tests/data/bids_events.tsv
new file mode 100644
index 000000000..e272ee311
--- /dev/null
+++ b/hedweb/tests/data/bids_events.tsv
@@ -0,0 +1,22 @@
+	onset	duration	event_type	sample	stage	trial	bci_prediction	bci_prediction_valid	n_repeated	latency
+0	15.038933333333	3.0	cue	75195.0	1.0	1.0	right	1.0	2.0	-4.2
+1	18.038933333333002	0.0	go	90195.0	1.0	1.0	right	1.0	2.0	-4.2
+2	18.1556	0.0	right-raised	90778.0	1.0	1.0	right	1.0	2.0	-4.2
+3	18.2236	0.0	right-raised-nomatch	91118.0	1.0	1.0	right	1.0	2.0	-4.2
+4	19.8922	3.0	cue	99461.0	1.0	2.0	left	1.0	2.0	-2.4
+5	22.8922	0.0	go	114461.0	1.0	2.0	left	1.0	2.0	-2.4
+6	22.9922	0.0	right-raised	114961.0	1.0	2.0	left	1.0	2.0	-2.4
+7	23.060200000000002	0.0	right-raised-match	115301.0	1.0	2.0	left	1.0	2.0	-2.4
+8	24.530466666666698	3.0	cue	122652.0	1.0	3.0	left	1.0	2.0	-17.2
+9	27.530466666666698	0.0	go	137652.0	1.0	3.0	left	1.0	2.0	-17.2
+10	27.5638	0.0	left-raised	137819.0	1.0	3.0	left	1.0	2.0	-17.2
+11	27.631800000000002	0.0	left-raised-nomatch	138159.0	1.0	3.0	left	1.0	2.0	-17.2
+12	32.6353333333333	3.0	cue	163177.0	1.0	4.0	left	1.0	3.0	9.8
+13	35.6353333333333	0.0	go	178177.0	1.0	4.0	left	1.0	3.0	9.8
+14	35.652	0.0	right-raised	178260.0	1.0	4.0	left	1.0	3.0	9.8
+15	35.72	0.0	right-raised-match	178600.0	1.0	4.0	left	1.0	3.0	9.8
+16	37.192733333333294	3.0	cue	185964.0	1.0	5.0	right	1.0	3.0	-15.2
+17	40.192733333333294	0.0	go	200964.0	1.0	5.0	right	1.0	3.0	-15.2
+18	40.2594	0.0	right-raised	201297.0	1.0	5.0	right	1.0	3.0	-15.2
+19	40.3274	0.0	right-raised-nomatch	201637.0	1.0	5.0	right	1.0	3.0	-15.2
+20	45.9162	3.0	cue	229581.0	1.0	6.0	right	1.0	5.0	-16.8
diff --git a/hedweb/tests/data/bids_events_fake.xlsx b/hedweb/tests/data/bids_events_fake.xlsx
new file mode 100644
index 000000000..1d1c34109
--- /dev/null
+++ b/hedweb/tests/data/bids_events_fake.xlsx
@@ -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)))), (Agent-action, Experimental-participant, (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)",
+          "right-raised": "Description/Subject raised right hand thereby releasing right button, (Agent-action, Experimental-participant, (Lift, (Right-side, Hand)), (Release, (Right-side, Button)))",
+          "left-raised": "Description/Subject raised left hand thereby releasing left button, (Agent-action, Experimental-participant, (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/BCI acts randomly, (Definition/Random-selection, Experimental-condition, (Random, Predict)), Def/Random-selection",
+            "2": "Description/BCI was trained on data from stage 1., (Definition/Trained-on-random, (Experimental-condition)), Def/Trained-on-random",
+            "3": "Description/BCI was trained on data from stages 1 and 2., (Definition/Trained-on-all, (Experimental-condition)), Def/Trained-on-all"
+		}
+    },
+	"trial": {
+		"Description": "The number of the trial in the experiment.",
+		"HED": "Description/The number of the trial in the experiment, (Experimental-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/data/good_events.json b/hedweb/tests/data/good_events.json
new file mode 100644
index 000000000..c6b11aec6
--- /dev/null
+++ b/hedweb/tests/data/good_events.json
@@ -0,0 +1,94 @@
+{
+    "onset": {
+        "Description": "Onset of the event of the computer presentation of the hand image",
+        "Units": "s"
+    },
+    "duration": {
+        "Description": "Duration of the event",
+        "Units": "s"
+    },
+	"cue-start": {
+		"Description": "Subject presses both buttons causing cue bar to start count-down display",
+		"HED": "(Delay/#s, (Description/Subject presses both buttons causing cue bar to start count-down display, (Sensory-event, Visual, Cue, (Rectangle,  (Center-of, Screen), (Size, Decreasing))), (Experimental-participant, (Press, (Left-side, Button)), (Press, (Right-side, Button)))))"
+	},
+	"cue-end": {
+		"Description": "Count-down display bar shrinks to zero size.",
+		"HED": "(Delay/# s, (Description/Count-down display bar shrinks to zero size., (Sensory-event, Visual, Cue)))"
+	},	
+    "trial_type": {
+        "Description": "Which hand was lifted by the subject in this trial.",
+        "Levels": {
+            "left": "Subject raised left hand",
+            "right": "Subject raised right hand"
+        },
+		"HED": {
+			"left": "Description/Subject raised left hand, (Experimental-participant, (Lift, (Left-side, Hand)))",
+			"right": "Description/Subject raised right hand, (Experimental-participant, (Lift, (Right-side, Hand)))"
+		}
+    },
+    "response_time": {
+        "Description": "Reaction time to lift the left or right hand after the countdown reached zero. this is the difference between event onset and cue-end in milliseconds.",
+        "Units": "ms",
+        "HED" : "Delay/# ms"
+    },
+    "stim_file": {
+        "Description": "Stimulus file showing hand presented on screen.",
+		"HED": "Description/Stimulus file showing hand is presented on screen, Sensory-event, Visual,((Image, ID/#),(Center-of, Screen)), Experimental-stimulus"
+    },
+    "value": {
+        "Description": "The code (EEG Marker value) associated with an event ",
+        "Levels": {
+            "1": "Release of left button (i.e., raising left hand)",
+            "2": "Release of right button (i.e., raising right hand)"
+        }
+    },
+    "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, (Experimental-condition, (Random, Predict))), Def/Random-selection",
+            "2": "Description/Stage 2 BCI was trained on data from stage 1, (Definition/Trained-on-random, (Experimental-condition)), Def/Trained-on-random",
+            "3": "Description/Stage 3 BCI was trained on data from stages 1 and 2, (Definition/Trained-on-all, (Experimental-condition)), Def/Trained-on-all"
+		}
+    },
+    "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"
+    },
+    "trial": {
+        "Description": "The current trial within this stage.",
+		"HED": "Experimental-trial, Item-count/#"
+    },
+    "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, (Computational-evidence, (Left-side, Hand))",
+          "right": "Description/BCI predicts participant raised right hand, (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/data/wakeman-henson-tags.tsv b/hedweb/tests/data/wakeman-henson-tags.tsv
new file mode 100644
index 000000000..073eb1b3f
--- /dev/null
+++ b/hedweb/tests/data/wakeman-henson-tags.tsv
@@ -0,0 +1,15 @@
+Event code	Event category	Event label	Event long name	Description	Other tags
+boundary	Event/Category/Experiment control	Event/Label/Run boundary		/Event/Description/The boundary mark of each run as the run data was merged	
+famous_new	Event/Category/Experimental stimulus	Event/Label/Famous face first time		/Event/Description/A grayscale photograph of a famous person was presented for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Item/Face/Whole face with hair,Participant/Effect/Cognitive/Familiar,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+famous_second_early	Event/Category/Experimental stimulus	Event/Label/Famous face immediate repeat		/Event/Description/A grayscale photograph of a famous person was presented the second time; immediately after its first presentation; for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Whole face with hair,Participant/Effect/Cognitive/Familiar,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic,Attribute/Temporal/Repetition/Immediate),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+famous_second_late	Event/Category/Experimental stimulus	Event/Label/Famous face delayed repeat		/Event/Description/A grayscale photograph of a famous person was presented the second time; delayed by 5-15 intervening stimuli after its first presentation; for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Whole face with hair,Participant/Effect/Cognitive/Familiar,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic,Attribute/Temporal/Repetition/Delayed),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+left_nonsym	Event/Category/Participant response	Event/Label/Button press left index nonsymmetric		/Event/Description/Participants pressed; with their left index finger; the key to indicate that the face shown was deemed -less symmetric- than average.	"Action/Button press,(Participant/Effect/Body part/Arm/Hand/Finger/Index,Attribute/Spatial/Object side/Left),(Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry/Non-symmetric)"
+left_sym	Event/Category/Participant response	Event/Label/Button press left index symmetric		/Event/Description/Participants pressed; with their left index finger; the key to indicate that the face shown was deemed -more symmetric- than average.	"Action/Button press,(Participant/Effect/Body part/Arm/Hand/Finger/Index,Attribute/Spatial/Object side/Left),(Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry/Symmetric)"
+right_nonsym	Event/Category/Participant response	Event/Label/Button press right index non symmetric		/Event/Description/Participants pressed; with their right index finger; the key to indicate that the face shown was deemed -less symmetric- than average.	"Action/Button press,(Participant/Effect/Body part/Arm/Hand/Finger/Index,Attribute/Spatial/Object side/Right),(Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry/Non-symmetric)"
+right_sym	Event/Category/Participant response	Event/Label/Button press right index symmetric		/Event/Description/Participants pressed; with their right index finger; the key to indicate that the face shown was deemed -more symmetric- than average.	"Action/Button press,(Participant/Effect/Body part/Arm/Hand/Finger/Index,Attribute/Spatial/Object side/Right),(Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry/Symmetric)"
+scrambled_new	Event/Category/Experimental stimulus	Event/Label/Scrambled face first show		/Event/Description/A grayscale photograph of a scrambled face was presented for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image.	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Scrambled,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+scrambled_second_early	Event/Category/Experimental stimulus	Event/Label/Scrambled face immediate repeat		/Event/Description/A grayscale photograph of a scrambled face was presented the second time; immediately after its first presentation; for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image.	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Scrambled,Attribute/Temporal/Repetition/Immediate,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+scrambled_second_late	Event/Category/Experimental stimulus	Event/Label/Scrambled face delayed repeat		/Event/Description/A grayscale photograph of a scrambled face was presented the second time; delayed by 5-15 intervening stimuli after its first presentation; for a random duration between 800 and 1000ms. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image.	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Scrambled,Attribute/Temporal/Repetition/Delayed,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+unfamiliar_new	Event/Category/Experimental stimulus	Event/Label/Unfamiliar face first show		/Event/Description/A grayscale photograph of the face of a person unknown to participants was presented for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image.	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Whole face with hair,Participant/Effect/Cognitive/Unfamiliar,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+unfamiliar_second_early	Event/Category/Experimental stimulus	Event/Label/Unfamiliar face immediate repeat		/Event/Description/A grayscale photograph of the face of a person unknown to participants was presented the second time; immediately after its first presentation; for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image.	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Whole face with hair,Participant/Effect/Cognitive/Unfamiliar,Attribute/Temporal/Repetition/Immediate,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Attribute/Sensory presentation/Visual/Scene,Attribute/Visual/Background,Attribute/Visual/Color/Black),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
+unfamiliar_second_late	Event/Category/Experimental stimulus	Event/Label/Unfamiliar face delayed repeat		/Event/Description/A grayscale photograph of the face of a person unknown to participants was presented the second time; delayed by 5-15 intervening stimuli after its first presentation; for a random duration between 800 and 1000ms. Participants were told to fixate centrally throughout the experiment. To ensure attention participants were asked to press one of two keys with either their left or right index finger to indicate how symmetric they regarded each image.	"Attribute/Temporal/Onset,(Attribute/Sensory presentation/Visual/Rendering type/Screen/2D,Attribute/Spatial/Location/Reference frame/Relative to participant/Distance/1.3m),(Item/Face/Whole face with hair,Participant/Effect/Cognitive/Unfamiliar,Attribute/Temporal/Repetition/Delayed,Attribute/Spatial/Location/Screen/Center displacement/Horizontal/3.66 degrees,Attribute/Spatial/Location/Screen/Center displacement/Vertical/5.38 degree,Attribute/Visual/Color/Achromatic),(Attribute/Visual/Background,Attribute/Visual/Color/Black,Attribute/Sensory presentation/Visual/Scene),(Participant/Effect/Cognitive/Imperative,Action/Button press,Participant/Effect/Cognitive/Make a judgement,Attribute/Visual/Symmetry),(Action/Eye fixation,Attribute/Spatial/Location/Screen/Center)"
diff --git a/hedweb/tests/test_dictionary.py b/hedweb/tests/test_dictionary.py
index 30dd3b795..124592656 100644
--- a/hedweb/tests/test_dictionary.py
+++ b/hedweb/tests/test_dictionary.py
@@ -1,8 +1,7 @@
-
 import os
 import shutil
 import unittest
-
+from flask import Response
 from hed.web.app_factory import AppFactory
 
 
@@ -34,32 +33,92 @@ def setUpClass(cls):
     def tearDownClass(cls):
         shutil.rmtree(cls.upload_directory)
 
-    def test_generate_input_arguments_from_validation_form(self):
+    def test_generate_input_from_dictionary_form(self):
         from hed.web import dictionary
-        self.assertRaises(TypeError, dictionary.generate_arguments_from_dictionary_form, {},
+        self.assertRaises(TypeError, dictionary.generate_input_from_dictionary_form, {},
                           "An exception should be raised if an empty request is passed")
 
-    def test_report_dictionary_validation_status(self):
-        self.assertTrue(1, "Testing report_dictionary_validation_status")
-
-    def test_validate_dictionary(self):
-        from hed.web.dictionary import validate_dictionary
+    def test_validate_dictionary_success(self):
+        from hed.web.dictionary import dictionary_validate
         from hed.schema import hed_schema_file
         inputs = test_dictionaries()
-        self.assertEqual("", validate_dictionary(inputs), "This dictionary should have no errors for 8.0.0")
+        response = dictionary_validate(inputs)
+        print(type(response) is Response)
+        self.assertTrue(type(response) is Response, "dictionary_validate should return a response if no error")
+
         hed8_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
 
         hed7_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
         hed7 = hed_schema_file.load_schema(hed7_path)
-        # response = validate_dictionary(inputs, hed_schema=hed7)
+        # response = dictionary_validate(inputs, hed_schema=hed7)
         # inputs["hed-xml-file"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
         # hed7 = hed_schema_file.load_schema(inputs["hed-xml_file"])
-        # self.assertEqual("", validate_dictionary(input_arguments, hed_schema=schema8),
+        # self.assertEqual("", dictionary_validate(input_arguments, hed_schema=schema8),
         #                  "This dictionary should have no errors for directly created 8.0.0")
         # schema7 = HedSchema(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml'))
-        # x = validate_dictionary(input_arguments, hed_schema=schema7, return_response=False)
-        # self.assertEqual("", validate_dictionary(input_arguments, hed_schema=schema7,return_response=False),
+        # x = dictionary_validate(input_arguments, hed_schema=schema7, return_response=False)
+        # self.assertEqual("", dictionary_validate(input_arguments, hed_schema=schema7,return_response=False),
         #                  "This dictionary should have no errors for directly created 8.0.0")
 
+        # from hed.web import dictionary
+        # print("First test.....")
+        # r = Response("download_text", mimetype='text/plain charset=utf-8',
+        #              headers={'Category': "success", 'Message': "This is a test of the response"})
+        # the_list = r.headers.getlist('Content-Length')
+        # print("HERE", int(the_list[0]))
+        # for prop, value in vars(r).items():
+        #     print(prop, ":", value)
+        #
+        # print("\nSecond test.....")
+        # r = Response("", mimetype='text/plain charset=utf-8',
+        #              headers={'Category': "success", 'Message': "This is empty"})
+        # for prop, value in vars(r).items():
+        #     print(prop, ":", value)
+        # headers = r.headers
+        # print(r.__dict__)
+        # print(dir(r))
+        # print('Headers....', r.headers)
+        # print("headers field", headers.__dict__)
+        # print("headers field", dir(headers))
+        # print("headers_item", headers.__getitem__('Content-Length'))
+        # the_list = headers.getlist('Content-Length')
+        # print("CL", the_list)
+        # the_len = int(the_list[0])
+        # print('len:', the_len)
+        # keys = headers.keys()
+        # print("Keys", keys)
+        # if r.response:
+        #     print(r.response)
+        # else:
+        #     print("Second response is empty")
+
+
+    # def test_generate_input_arguments_from_validation_form(self):
+    #     from hed.web import dictionary
+    #     self.assertRaises(TypeError, dictionary.generate_input_from_dictionary_form, {},
+    #                       "An exception should be raised if an empty request is passed")
+    #
+    # def test_report_dictionary_validation_status(self):
+    #     self.assertTrue(1, "Testing report_dictionary_validation_status")
+    #
+    # def test_validate_dictionary(self):
+    #     from hed.web.dictionary import dictionary_validate
+    #     from hed.schema import hed_schema_file
+    #     inputs = test_dictionaries()
+    #     self.assertEqual("", dictionary_validate(inputs), "This dictionary should have no errors for 8.0.0")
+    #     hed8_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
+    #
+    #     hed7_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
+    #     hed7 = hed_schema_file.load_schema(hed7_path)
+    #     # response = dictionary_validate(inputs, hed_schema=hed7)
+    #     # inputs["hed-xml-file"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
+    #     # hed7 = hed_schema_file.load_schema(inputs["hed-xml_file"])
+    #     # self.assertEqual("", dictionary_validate(input_arguments, hed_schema=schema8),
+    #     #                  "This dictionary should have no errors for directly created 8.0.0")
+    #     # schema7 = HedSchema(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml'))
+    #     # x = dictionary_validate(input_arguments, hed_schema=schema7, return_response=False)
+    #     # self.assertEqual("", dictionary_validate(input_arguments, hed_schema=schema7,return_response=False),
+    #     #                  "This dictionary should have no errors for directly created 8.0.0")
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/hedweb/tests/test_events.py b/hedweb/tests/test_events.py
index 4b6e8b61d..6644f37d4 100644
--- a/hedweb/tests/test_events.py
+++ b/hedweb/tests/test_events.py
@@ -1,41 +1,54 @@
-import unittest
-# from flask import current_app, jsonify, Response
-# from hed.web.utils import app_config
-# 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
-
-# app = AppFactory.create_app('config.TestConfig')
-# with app.app_context():
-#     from hed.web import web_utils
-#     from hed.web.routes import route_blueprint
-#
-#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
-#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+import shutil
+import unittest
+from flask import Response
+from hed.web.app_factory import AppFactory
+from hed.web.constants import common
+
+
+def event_input():
+    base_path = os.path.dirname(os.path.abspath(__file__))
+    test_events = {common.HED_XML_FILE: os.path.join(base_path, 'data/HED8.0.0-alpha.1.xml'),
+                   common.HED_DISPLAY_NAME: 'HED8.0.0-alpha.1.xml',
+                   common.JSON_PATH: os.path.join(base_path, 'data/short_form_valid.json'),
+                   common.JSON_FILE: 'short_form_valid.json',
+                   common.SPREADSHEET_PATH: os.path.join(base_path, 'data/ExcelMultipleSheets.xlsx'),
+                   common.SPREADSHEET_FILE: 'ExcelMultipleSheets.xlsx',
+                   common.WORKSHEET_NAME: 'LKT Events',
+                   common.HAS_COLUMN_NAMES: True,
+                   common.CHECK_FOR_WARNINGS: True
+    }
+    return test_events
 
 
 class Test(unittest.TestCase):
-    def setUp(self):
-        print("Stuff")
-        # self.create_test_app()
-        # self.app = app.app.test_client()
-        # self.major_version_key = 'major_versions'
-        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
-        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
-
-    # def create_test_app(self):
-    #     app = AppFactory.create_app('config.TestConfig')
-    #     with app.app_context():
-    #         from hed.web.routes import route_blueprint
-    #         app.register_blueprint(route_blueprint)
-    #         self.app = app.test_client()
+    @classmethod
+    def setUpClass(cls):
+        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
+        app = AppFactory.create_app('config.TestConfig')
+        with app.app_context():
+            from hed.web.routes import route_blueprint
+            app.register_blueprint(route_blueprint)
+            if not os.path.exists(cls.upload_directory):
+                os.mkdir(cls.upload_directory)
+            app.config['UPLOAD_FOLDER'] = cls.upload_directory
+            cls.app = app
+            cls.app.test = app.test_client()
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.upload_directory)
 
     def test_generate_input_from_events_form(self):
         self.assertTrue(1, "Testing generate_input_from_events_form")
 
-    def test_report_events_validation_status(self):
-        self.assertTrue(1, "report_events_validation_status")
+    def test_events_validate(self):
+        from hed.web.events import events_validate
+        from hed.schema import hed_schema_file
+        inputs = event_input()
+        response = events_validate(inputs)
+        print(type(response) is Response)
+        self.assertTrue(type(response) is Response, "events_validate should return a response")
 
 
 if __name__ == '__main__':
diff --git a/hedweb/tests/test_hedstring.py b/hedweb/tests/test_hedstring.py
new file mode 100644
index 000000000..37678efbf
--- /dev/null
+++ b/hedweb/tests/test_hedstring.py
@@ -0,0 +1,70 @@
+
+import os
+import shutil
+import unittest
+
+from hed.web.app_factory import AppFactory
+
+
+def test_input():
+    base_path = os.path.dirname(os.path.abspath(__file__))
+    test_inputs = {'hed-xml-file': os.path.join(base_path, 'data/HED8.0.0-alpha.1.xml'),
+                   'hed-display-name': 'HED8.0.0-alpha.1.xml', 'hedstring': 'White'}
+
+    return test_inputs
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
+        app = AppFactory.create_app('config.TestConfig')
+        with app.app_context():
+            from hed.web.routes import route_blueprint
+            app.register_blueprint(route_blueprint)
+            if not os.path.exists(cls.upload_directory):
+                os.mkdir(cls.upload_directory)
+            app.config['UPLOAD_FOLDER'] = cls.upload_directory
+            cls.app = app
+            cls.app.test = app.test_client()
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.upload_directory)
+
+    def test_generate_input_from_hedstring_form(self):
+        from hed.web import hedstring
+        self.assertRaises(TypeError, hedstring.generate_input_from_hedstring_form, {},
+                          "An exception should be raised if an empty request is passed")
+
+    def test_hedstring_process(self):
+        self.assertTrue(1, "process_hedstring")
+
+    def test_hedstring_convert(self):
+        from hed.web import hedstring
+        input_arguments = {'hed-xml-file': 'data/'}
+        self.assertTrue(1, "process_hedstring")
+
+    def test_hedstring_validate(self):
+        from hed.web.hedstring import hedstring_validate
+        input = test_input()
+        result = hedstring_validate(input)
+        self.assertIsInstance(result, dict, "hedstring_validate should return a dictionary")
+        self.assertEqual("No validation errors found...", result['hedstring-result'], "This is a valid hedstring for 8.0.0")
+        # hed8_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
+        #
+        # hed7_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
+        # hed7 = hed_schema_file.load_schema(hed7_path)
+        # response = dictionary_validate(inputs, hed_schema=hed7)
+        # inputs["hed-xml-file"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml')
+        # hed7 = hed_schema_file.load_schema(inputs["hed-xml_file"])
+        # self.assertEqual("", dictionary_validate(input_arguments, hed_schema=schema8),
+        #                  "This dictionary should have no errors for directly created 8.0.0")
+        # schema7 = HedSchema(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED7.1.2.xml'))
+        # x = dictionary_validate(input_arguments, hed_schema=schema7, return_response=False)
+        # self.assertEqual("", dictionary_validate(input_arguments, hed_schema=schema7,return_response=False),
+        #                  "This dictionary should have no errors for directly created 8.0.0")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_routes.py b/hedweb/tests/test_routes.py
index 15ae56b31..454557bec 100644
--- a/hedweb/tests/test_routes.py
+++ b/hedweb/tests/test_routes.py
@@ -26,32 +26,9 @@ def setUpClass(cls):
     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")
-        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)
-        self.assertFalse(dummy_path.is_file(), "Dummy file does not exist yet")
-        copyfile(hed_file, dummy_file)
-        self.assertTrue(dummy_path.is_file(), "Dummy file now exists")
-        response = self.app.test.get('/delete/HED.xml')
-        self.assertEqual(response.status_code, 204, "Dummy file should be deleted")
-
-    def test_download_file_in_upload_directory(self):
-        response = self.app.test.get('/download-file/file_that_does_not_exist')
-
-        # self.assertEqual(response.status_code, 404, "Non-existent file should cause a non-found error")
-        upload_dir = self.app.config['UPLOAD_FOLDER']
-        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)
-        self.assertFalse(dummy_path.is_file(), "Dummy file does not exist yet")
-        copyfile(hed_file, dummy_file)
-        self.assertTrue(dummy_path.is_file(), "Dummy file now exists")
-        response = self.app.test.get('/download-file/HED.xml')
-
-        # self.assertEqual(response.status_code, 204, "Dummy file should be deleted")
+    def test_get_columns_info_results(self):
+        response = self.app.test.post('/get-columns-info')
+        self.assertEqual(400, response.status_code, 'Columns info requires data')
 
     def test_get_dictionary_validation_results(self):
         response = self.app.test.post('/dictionary-validation-submit')
@@ -65,6 +42,10 @@ def test_get_hed_services_results(self):
         response = self.app.test.get('/hed-services-submit')
         self.assertEqual(405, response.status_code, 'HED services require data')
 
+    def test_get_hedstring_results(self):
+        response = self.app.test.get('/hed-services-submit')
+        self.assertEqual(405, response.status_code, 'HED services require data')
+
     def test_get_hed_version(self):
         response = self.app.test.post('/get-hed-version')
         self.assertEqual(400, response.status_code, 'Returning HED version requires data')
@@ -86,18 +67,10 @@ def test_get_schema_conversion_results(self):
         response = self.app.test.post('/schema-conversion-submit')
         self.assertEqual(400, response.status_code, 'Converting schema requires data')
 
-    def test_get_spreadsheet_columns_info(self):
-        response = self.app.test.post('/get-spreadsheet-columns-info')
-        self.assertEqual(400, response.status_code, 'Returning spreadsheet column info requires data')
-
     def test_get_spreadsheet_validation_results(self):
         response = self.app.test.post('/spreadsheet-validation-submit')
         self.assertEqual(400, response.status_code, 'Validating spreadsheet requires data')
 
-    def test_get_worksheets_info(self):
-        response = self.app.test.post('/get-worksheets-info')
-        self.assertEqual(400, response.status_code, 'Returning worksheet info requires data')
-
     def test_render_additional_examples_page(self):
         response = self.app.test.get('/additional-examples')
         self.assertEqual(response.status_code, 200, "The additional-examples content page should exist")
@@ -118,6 +91,10 @@ def test_render_hed_services_form(self):
         response = self.app.test.get('/hed-services')
         self.assertEqual(response.status_code, 200, "The hed-services content page should exist")
 
+    def test_render_hedstring_form(self):
+        response = self.app.test.get('/hedstring')
+        self.assertEqual(response.status_code, 200, "The hedstring content page should exist")
+
     def test_render_help_page(self):
         response = self.app.test.get('/hed-tools-help')
         self.assertEqual(response.status_code, 200, "The hed-tools-help content page should exist" )
diff --git a/hedweb/tests/test_schema.py b/hedweb/tests/test_schema.py
index 0134f9567..4a81e0a40 100644
--- a/hedweb/tests/test_schema.py
+++ b/hedweb/tests/test_schema.py
@@ -1,46 +1,36 @@
+import os
+import shutil
 import unittest
-# from flask import current_app, jsonify, Response
-# from hed.web.utils import app_config
-# 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
-
-# app = AppFactory.create_app('config.TestConfig')
-# with app.app_context():
-#     from hed.web import web_utils
-#     from hed.web.routes import route_blueprint
-#
-#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
-#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+from flask import Response
+from hed.web.app_factory import AppFactory
 
 
 class Test(unittest.TestCase):
-    def setUp(self):
-        print("Stuff")
-        # self.create_test_app()
-        # self.app = app.app.test_client()
-        # self.major_version_key = 'major_versions'
-        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
-        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
-
-    # def create_test_app(self):
-    #     app = AppFactory.create_app('config.TestConfig')
-    #     with app.app_context():
-    #         from hed.web.routes import route_blueprint
-    #         app.register_blueprint(route_blueprint)
-    #         self.app = app.test_client()
+    @classmethod
+    def setUpClass(cls):
+        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
+        app = AppFactory.create_app('config.TestConfig')
+        with app.app_context():
+            from hed.web.routes import route_blueprint
+            app.register_blueprint(route_blueprint)
+            if not os.path.exists(cls.upload_directory):
+                os.mkdir(cls.upload_directory)
+            app.config['UPLOAD_FOLDER'] = cls.upload_directory
+            cls.app = app
+            cls.app.test = app.test_client()
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.upload_directory)
 
     def test_generate_input_from_schema_form(self):
         self.assertTrue(1, "Testing generate_input_from_schema_form")
 
-    def test_get_schema_conversion(self):
-        self.assertTrue(1, "Testing convert_schema")
-
-    def test_run_schema_compliance_check(self):
+    def test_schema_check(self):
         self.assertTrue(1, "Testing run_schema_compliance_check")
 
-    def test_run_schema_conversion(self):
-        self.assertTrue(1, "Testing run_schema_conversion")
+    def test_schema_convert(self):
+        self.assertTrue(1, "Testing convert_schema")
 
 
 if __name__ == '__main__':
diff --git a/hedweb/tests/test_services.py b/hedweb/tests/test_services.py
index 1e9b1409f..23738899b 100644
--- a/hedweb/tests/test_services.py
+++ b/hedweb/tests/test_services.py
@@ -31,10 +31,10 @@ def setUp(self):
     #         app.register_blueprint(route_blueprint)
     #         self.app = app.test_client()
 
-    def test_generate_input_arguments_from_validation_form(self):
-        self.assertTrue(1, "Testing generate_arguments_from_dictionary_form")
+    def test_report_services_status(self):
+        self.assertTrue(1, "Testing generate_input_from_dictionary_form")
 
-    def test_generate_spreadsheet_validation_filename(self):
+    def test_get_services(self):
         self.assertTrue(1, "Testing generate_dictionary_validation_filename")
         # dictionary_filename = 'abc.xls'
         # expected_spreadsheet_filename = 'validated_' + dictionary_filename.rsplit('.')[0] + '.txt'
@@ -42,21 +42,12 @@ def test_generate_spreadsheet_validation_filename(self):
         # self.assertTrue(validation_file_name)
         # self.assertEqual(expected_spreadsheet_filename, validation_file_name)
 
-    def test_get_uploaded_file_paths_from_forms(self):
+    def test_get_validate_dictionary(self):
         self.assertTrue(1, "Testing get_uploaded_file_paths_from_forms")
 
-    def test_report_eeg_events_validation_status(self):
+    def test_get_validate_strings(self):
         self.assertTrue(1, "Testing report_eeg_events_validation_status")
 
-    def test_report_spreadsheet_validation_status(self):
-        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_dictionary")
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/hedweb/tests/test_spreadsheet.py b/hedweb/tests/test_spreadsheet.py
index fb23a16a2..b50d8b664 100644
--- a/hedweb/tests/test_spreadsheet.py
+++ b/hedweb/tests/test_spreadsheet.py
@@ -24,15 +24,12 @@ def setUpClass(cls):
     def tearDownClass(cls):
         shutil.rmtree(cls.upload_directory)
 
-    def test_generate_input_from_validation_form(self):
+    def test_generate_input_from_spreadsheet_form(self):
         self.assertTrue(1, "Testing generate_input_from_validation_form")
 
-    def test_report_spreadsheet_validation_status(self):
-        self.assertTrue(1, "Testing report_spreadsheet_validation_status")
-
-    def test_validate_spreadsheet(self):
-        self.assertTrue(1, "Testing validate_spreadsheet")
-        # from hed.web.spreadsheet import validate_spreadsheet
+    def test_spreadsheet_validate(self):
+        self.assertTrue(1, "Testing spreadsheet_validate")
+        # from hed.web.spreadsheet import spreadsheet_validate
         # temp_name = save_file_to_upload_folder('')
         # self.assertEqual(temp_name, '', "A file with empty name cnnot be copied copied")
         # some_file = '3k32j23kj1.txt'
diff --git a/hedweb/tests/test_spreadsheet_utils.py b/hedweb/tests/test_spreadsheet_utils.py
index b4dce4f0d..5c886f0b4 100644
--- a/hedweb/tests/test_spreadsheet_utils.py
+++ b/hedweb/tests/test_spreadsheet_utils.py
@@ -23,8 +23,8 @@ def setUpClass(cls):
     def tearDownClass(cls):
         shutil.rmtree(cls.upload_directory)
 
-    def test_find_spreadsheet_columns_info(self):
-        self.assertTrue(1, "Testing find_spreadsheet_columns_info")
+    def test_generate_input_columns_info(self):
+        self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
 
     def test_get_column_delimiter_based_on_file_extension(self):
         self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
@@ -38,6 +38,12 @@ def test_get_column_delimiter_based_on_file_extension(self):
     #     delimiter = get_column_delimiter_based_on_file_extension('test.xlsx')
     #     self.assertEqual('', delimiter, "Excel files should have an empty delimiter")
 
+    def test_get_columns_info(self):
+        self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
+
+    def test_get_column_info_dictionary(self):
+        self.excel_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/ExcelMultipleSheets.xlsx')
+
     def test_get_excel_worksheet_names(self):
         from hed.web.spreadsheet_utils import get_excel_worksheet_names
         self.excel_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/ExcelMultipleSheets.xlsx')
@@ -46,30 +52,35 @@ def test_get_excel_worksheet_names(self):
         self.assertIn('PVT Events', worksheet_names, "PVT Events is one of the worksheet names")
         self.assertNotIn('Temp', worksheet_names, "Temp is not one of the worksheet names")
 
-    def test_get_original_spreadsheet_filename(self):
-        self.assertTrue(1, "Testing get_original_filename")
-
-    def test_get_specific_tag_columns_from_form(self):
-        self.assertTrue(1, "Testing get_specific_tag_columns_from_form")
-
-    def test_get_spreadsheet_other_tag_column_indices(self):
-        self.assertTrue(1, "Testing get_spreadsheet_other_tag_column_indices")
+    def test_get_other_tag_column_indices(self):
+        self.assertTrue(1, "Testing get_other_tag_column_indices")
         # def test_get_spreadsheet_other_tag_column_indices(self):
         #     column_names = ['a,', spreadsheet_constants.OTHER_TAG_COLUMN_NAMES[0]]
         #     expected_indices = [2]
-        #     indices = utils.get_spreadsheet_other_tag_column_indices(column_names)
+        #     indices = utils.get_other_tag_column_indices(column_names)
         #     self.assertTrue(indices)
         #     self.assertIsInstance(indices, list)
         #     self.assertEqual(indices, expected_indices)
         #
 
-    def test_get_spreadsheet_specific_tag_column_indices(self):
-        self.assertTrue(1, "Testing get_spreadsheet_specific_tag_column_indices")
+    def test_get_specific_tag_column_indices(self):
+        self.assertTrue(1, "Testing get_specific_tag_column_indices")
+        # def test_get_spreadsheet_specific_tag_column_indices(self):
+        #     column_names = ['a,', spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES_DICTIONARY[
+        #         spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES[0]][0]]
+        #     # print(column_names)
+        #     indices = utils.get_specific_tag_column_indices(column_names)
+        #     self.assertTrue(indices)
+        #     self.assertIsInstance(indices, dict)
+        #
+
+    def test_get_specific_tag_columns_from_form(self):
+        self.assertTrue(1, "Testing get_specific_tag_column_indices")
         # def test_get_spreadsheet_specific_tag_column_indices(self):
         #     column_names = ['a,', spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES_DICTIONARY[
         #         spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES[0]][0]]
         #     # print(column_names)
-        #     indices = utils.get_spreadsheet_specific_tag_column_indices(column_names)
+        #     indices = utils.get_specific_tag_column_indices(column_names)
         #     self.assertTrue(indices)
         #     self.assertIsInstance(indices, dict)
         #
@@ -82,15 +93,17 @@ def test_get_text_file_column_names(self):
         #     self.assertIsInstance(column_names, list)
         #
 
-    def test_get_worksheet_column_names(self):
-        self.assertTrue(1, "Testing get_worksheet_column_names")
-
-    def test_populate_spreadsheet_columns_info_dictionary(self):
-        self.assertTrue(1, "Testing populate_spreadsheet_columns_info_dictionary")
+    def test_get_text_file_info(self):
+        self.assertTrue(1, "Testing get_text_file_column_names")
+        # def test_get_text_file_column_names(self):
+        #     column_names = utils.get_text_file_column_names(self.tsv_file1, '\t')
+        #     self.assertTrue(column_names)
+        #     self.assertIsInstance(column_names, list)
+        #
 
-    def test_populate_worksheets_info_dictionary(self):
+    def test_get_worksheets_info(self):
         info = {}
-        self.assertTrue(1, "Testing populate_worksheets_info_dictionary")
+        self.assertTrue(1, "Testing get_worksheets_info_dictionary")
 
 
 if __name__ == '__main__':
diff --git a/hedweb/tests/test_web_utils.py b/hedweb/tests/test_web_utils.py
index 05c275567..9bc7f248a 100644
--- a/hedweb/tests/test_web_utils.py
+++ b/hedweb/tests/test_web_utils.py
@@ -28,7 +28,6 @@ def setUpClass(cls):
     def tearDownClass(cls):
         shutil.rmtree(cls.upload_directory)
 
-
     def test_convert_number_str_to_list(self):
         from hed.web.web_utils import convert_number_str_to_list
         other_tag_columns_str = '1,2,3'
@@ -49,6 +48,9 @@ def test_convert_number_str_to_list(self):
         with self.assertRaises(ValueError):
             other_tag_columns = convert_number_str_to_list(other_tag_columns_str)
 
+    def test_delete_file_no_exceptions(self):
+        print("Test")
+
     def test_file_extension_is_valid(self):
         from hed.web import web_utils
         is_valid = web_utils.file_extension_is_valid('abc.xml', ['.xml', '.txt'])
@@ -148,8 +150,11 @@ def test_generate_filename(self):
         filename = generate_filename('HED7.1.2.xml', suffix='blech', extension='.txt')
         self.assertEqual('HED7.1.2_blech.txt', filename, "Returns correct string when basename has periods")
 
+    def test_generate_text_response(self):
+        print("Stuff")
+
     def test_get_hed_path_from_pull_down(self):
-        from hed.web.constants import common_constants
+        from hed.web.constants import common
         from hed.web.web_utils import get_hed_path_from_pull_down
         mock_form = mock.Mock()
         mock_form.values = {}
@@ -181,14 +186,20 @@ def test_get_uploaded_file_path_from_form(self):
         # actual_path = os.path.join(self.upload_directory, filename)
         # self.assertEqual(0, os.path.isfile(actual_path), f"{actual_path} should not exist before saving")
         # with self.app.app_context():
-        #     the_path = save_text_to_upload_folder(text, filename)
-        #     self.assertEqual(1, os.path.isfile(the_path), f"{the_path} should exist after saving")
+            # the_path = save_text_to_upload_folder(text, filename)
+            # self.assertEqual(1, os.path.isfile(the_path), f"{the_path} should exist after saving")
 
-    def test_handle_http_error(self):
-        error_code = "CODE"
-        error_message = "Test"
+    # def test_handle_http_error(self):
+    #     error_code = "CODE"
+    #     error_message = "Test"
+    #
+    #     self.assertTrue(1, "Testing handle_http_error")
+
+    def test_handle_error(self):
+        print("stuff")
 
-        self.assertTrue(1, "Testing handle_http_error")
+    def test_handle_http_error(self):
+        print("stuff")
 
     def test_save_file_to_upload_folder(self):
         self.assertTrue(1, "Testing save_file_to_upload_folder")
@@ -225,13 +236,19 @@ def test_save_file_to_upload_folder(self):
         # self.assertNotEqual(mock_file, '', "It should create an actual file in the upload directory")
         # self.assertTrue(os.path.isfile(temp_name), "File should exist after it is uploaded")
 
-    def test_text_to_upload_folder(self):
+    def test_save_file_to_upload_folder_no_exception(self):
+        print("Stuff")
+
+    def test_save_text_to_upload_folder(self):
         from hed.web.web_utils import save_text_to_upload_folder
+        # from flask import Flask
+        # app = Flask(__name__)
         text = 'save me now'
         filename = 'test_save.txt'
         actual_path = os.path.join(self.upload_directory, filename)
         self.assertEqual(0, os.path.isfile(actual_path), f"{actual_path} should not exist before saving")
         with self.app.app_context():
+            from hed.web.web_utils import save_text_to_upload_folder
             the_path = save_text_to_upload_folder(text, filename)
             self.assertEqual(1, os.path.isfile(the_path), f"{the_path} should exist after saving")
 
diff --git a/hedweb/tests/testing_responses.py b/hedweb/tests/testing_responses.py
new file mode 100644
index 000000000..f6bd4f6a6
--- /dev/null
+++ b/hedweb/tests/testing_responses.py
@@ -0,0 +1,12 @@
+import json
+import os.path
+from hed.web.web_utils import generate_filename, generate_download_file_response, \
+    generate_text_response,get_hed_path_from_pull_down, \
+    get_uploaded_file_path_from_form, save_text_to_upload_folder, get_optional_form_field
+
+
+if __name__ == '__main__':
+    response = generate_text_response("", msg='JSON dictionary had no validation errors')
+    print(response)
+
+