File: Internal\Synthesis\SSmlParser.cs
Web Access
Project: src\src\runtime\src\libraries\System.Speech\src\System.Speech.csproj (System.Speech)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Net;
using System.Speech.Synthesis;
using System.Speech.Synthesis.TtsEngine;
using System.Text;
using System.Xml;

namespace System.Speech.Internal.Synthesis
{
    internal static class SsmlParser
    {
        #region Internal Methods

        /// <summary>
        /// Parse an SSML stream and build a set of SSML Text Fragments
        /// </summary>
        internal static void Parse(string ssml, ISsmlParser engine, object voice)
        {
            // Remove the CR and LF
            string ssmlNoCrLf = ssml.Replace('\n', ' ');
            ssmlNoCrLf = ssmlNoCrLf.Replace('\r', ' ');
            XmlTextReader reader = new(new StringReader(ssmlNoCrLf));

            // Parse the stream
            Parse(reader, engine, voice);
        }

        /// <summary>
        /// Parse an SSML stream and build a set of SSML Text Fragments
        /// </summary>
        internal static void Parse(XmlReader reader, ISsmlParser engine, object voice)
        {
            try
            {
                bool isSpeakElementFound = false;

                while (reader.Read())
                {
                    // Ignore XmlDeclaration, ProcessingInstruction, Comment, DocumentType, Entity, Notation.
                    if ((reader.NodeType == XmlNodeType.Element) && (reader.LocalName == "speak"))
                    {
                        // SSML documents must start with the "speak" element
                        if (isSpeakElementFound)
                        {
                            ThrowFormatException(SRID.GrammarDefTwice);
                        }
                        else
                        {
                            // The XML header is read, real work starts here
                            ProcessSpeakElement(reader, engine, voice);
                            isSpeakElementFound = true;
                        }
                    }
                }

                if (!isSpeakElementFound)
                {
                    ThrowFormatException(SRID.SynthesizerNoSpeak);
                }
            }
            catch (XmlException eXml)
            {
                throw new FormatException(SR.Get(SRID.InvalidXml), eXml);
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Validate the Speak element
        /// </summary>
        private static void ProcessSpeakElement(XmlReader reader, ISsmlParser engine, object voice)
        {
            SsmlAttributes ssmlAttributes = new(voice, new List<SsmlXmlAttribute>());
            ssmlAttributes._age = VoiceAge.NotSet;
            ssmlAttributes._gender = VoiceGender.NotSet;

            string? sVersion = null;
            string? sCulture = null;
            string? sBaseUri = null;
            CultureInfo? culture = null;
            List<SsmlXmlAttribute> extraSpeakAttributes = new();
            Exception? innerException = null;

            // Process attributes.
            while (reader.MoveToNextAttribute())
            {
                bool isInvalidAttribute = false;

                // emptyNamespace
                if (reader.NamespaceURI.Length == 0)
                {
                    switch (reader.LocalName)
                    {
                        case "version":
                            CheckForDuplicates(ref sVersion, reader);
                            if (sVersion != "1.0")
                            {
                                ThrowFormatException(SRID.InvalidVersion);
                            }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                else if (reader.NamespaceURI == xmlNamespace)
                {
                    switch (reader.LocalName)
                    {
                        case "lang":
                            CheckForDuplicates(ref sCulture, reader);
                            try
                            {
                                culture = new CultureInfo(sCulture);
                            }
                            catch (ArgumentException e)
                            {
                                innerException = e;
                                // Unknown Culture info, fall back to the base culture.
                                int pos = reader.Value.IndexOf('-');
                                if (pos > 0)
                                {
                                    try
                                    {
                                        culture = new CultureInfo(reader.Value.Substring(0, pos));
                                    }
                                    catch (ArgumentException)
                                    {
                                        isInvalidAttribute = true;
                                    }
                                }
                                else
                                {
                                    isInvalidAttribute = true;
                                }
                            }
                            break;

                        case "base":
                            CheckForDuplicates(ref sBaseUri, reader);
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                else if (reader.NamespaceURI == xmlNamespaceXmlns)
                {
                    if (reader.Value != xmlNamespaceSsml && reader.Value != xmlNamespacePrompt)
                    {
                        ssmlAttributes._unknownNamespaces.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI));
                    }
                    else if (reader.Value == xmlNamespacePrompt)
                    {
                        engine.ContainsPexml(reader.LocalName);
                    }
                }
                else
                {
                    extraSpeakAttributes.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI));
                }

                if (isInvalidAttribute)
                {
                    ThrowFormatException(innerException, SRID.InvalidElement, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sVersion))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "version", "speak");
            }

            if (string.IsNullOrEmpty(sCulture))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "lang", "speak");
            }

            // append the local attributes to list of unknown attributes
            List<SsmlXmlAttribute>? extraAttributes = null;
            foreach (SsmlXmlAttribute attribute in extraSpeakAttributes)
            {
                ssmlAttributes.AddUnknowAttribute(attribute, ref extraAttributes);
            }

            System.Diagnostics.Debug.Assert(culture != null);
            voice = engine.ProcessSpeak(sVersion, sBaseUri, culture, ssmlAttributes._unknownNamespaces);

            ssmlAttributes._fragmentState.LangId = culture.LCID;
            ssmlAttributes._voice = voice;
            ssmlAttributes._baseUri = sBaseUri;

            // Process child elements.
            SsmlElement possibleChild = SsmlElement.Lexicon | SsmlElement.Meta | SsmlElement.MetaData | SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes);
            ProcessElement(reader, engine, "speak", possibleChild, ssmlAttributes, false, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndSpeakElement();
        }

        /// <summary>
        /// Generic method to process an SSML element.
        /// The element name is fetch from the element name array and
        /// the delegate for that element will be called.
        /// </summary>
        private static void ProcessElement(XmlReader reader, ISsmlParser engine, string? sElement, SsmlElement possibleElements, SsmlAttributes ssmAttributesParent, bool fIgnore, List<SsmlXmlAttribute>? extraAttributes)
        {
            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            // Flush any remaining attributes from the previous element list
            if (extraAttributes != null && extraAttributes.Count > 0)
            {
                engine.StartProcessUnknownAttributes(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, sElement, extraAttributes);
            }

            // Move to containing element of attributes
            reader.MoveToElement();
            if (!reader.IsEmptyElement)
            {
                // Process each child element while not at end element
                reader.Read();
                do
                {
                    switch (reader.NodeType)
                    {
                        case XmlNodeType.Element:
                            int iElement = Array.BinarySearch<string>(s_elementsName, reader.LocalName);
                            if (iElement >= 0)
                            {
                                s_parseElements[iElement](reader, engine, possibleElements, ssmlAttributes, fIgnore);
                            }
                            else
                            {
                                // Could be an element from some undefined namespace
                                if (!ssmlAttributes.IsOtherNamespaceElement(reader))
                                {
                                    ThrowFormatException(SRID.InvalidElement, reader.Name);
                                }
                                else
                                {
                                    engine.ProcessUnknownElement(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, reader);
                                    continue;
                                }
                            }
                            reader.Read();
                            break;

                        case XmlNodeType.Text:
                            if ((possibleElements & SsmlElement.Text) != 0)
                            {
                                engine.ProcessText(reader.Value, ssmlAttributes._voice, ref ssmlAttributes._fragmentState, GetColumnPosition(reader), fIgnore);
                            }
                            else
                            {
                                ThrowFormatException(SRID.InvalidElement, reader.Name);
                            }
                            reader.Read();
                            break;

                        case XmlNodeType.EndElement:
                            break;

                        default:
                            reader.Read();
                            break;
                    }
                }
                while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);
            }

            // Flush any remaining attributes from the previous element list
            if (extraAttributes != null && extraAttributes.Count > 0)
            {
                engine.EndProcessUnknownAttributes(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, sElement, extraAttributes);
            }
        }

        private static void ParseAudio(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Audio, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sUri = null;
            bool fRenderDesc = false;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "src":
                            CheckForDuplicates(ref sUri, reader);
                            // Audio element
                            try
                            {
                                engine.ProcessAudio(ssmlAttributes._voice, sUri, ssmlAttributes._baseUri, fIgnore);
                            }
                            catch (IOException)
                            {
                                fRenderDesc = true;
                            }
                            catch (WebException)
                            {
                                fRenderDesc = true;
                            }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            ssmlAttributes._fRenderDesc = fRenderDesc;

            // Process child elements.
            SsmlElement possibleChild = SsmlElement.Desc | SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes);
            ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, !fRenderDesc, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseBreak(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Break, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;
            ssmlAttributes._fragmentState.Action = TtsEngineAction.Silence;
            ssmlAttributes._fragmentState.Emphasis = (int)EmphasisBreak.Default;

            string? sTime = null;
            string? sStrength = null;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "time":
                        {
                            CheckForDuplicates(ref sTime, reader);
                            ssmlAttributes._fragmentState.Emphasis = (int)EmphasisBreak.None;
                            ssmlAttributes._fragmentState.Duration = ParseCSS2Time(sTime);
                            isInvalidAttribute = ssmlAttributes._fragmentState.Duration < 0;
                        }
                        break;

                        case "strength":
                            CheckForDuplicates(ref sStrength, reader);
                            if (sTime == null)
                            {
                                ssmlAttributes._fragmentState.Duration = 0;
                                int pos = Array.BinarySearch<string>(s_breakStrength, sStrength);
                                if (pos < 0)
                                {
                                    isInvalidAttribute = true;
                                }
                                else
                                {
                                    // SSML Spec if both strength and time are supplied, ignore strength
                                    if (ssmlAttributes._fragmentState.Emphasis != (int)EmphasisBreak.None)
                                    {
                                        ssmlAttributes._fragmentState.Emphasis = (int)s_breakEmphasis[pos];
                                    }
                                }
                            }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidSpeakAttribute, reader.Name, "break");
                }
            }

            engine.ProcessBreak(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, (EmphasisBreak)ssmlAttributes._fragmentState.Emphasis, ssmlAttributes._fragmentState.Duration, fIgnore);

            // No Children allowed .
            ProcessElement(reader, engine, sElement, 0, ssmlAttributes, true, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseDesc(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Desc, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sCulture = null;
            CultureInfo? culture = null;
            while (reader.MoveToNextAttribute())
            {
                bool isInvalidAttribute = reader.NamespaceURI != xmlNamespace;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        // The W3C spec says ignore
                        case "lang":
                            CheckForDuplicates(ref sCulture, reader);
                            try
                            {
                                culture = new CultureInfo(sCulture);
                            }
                            catch (ArgumentException)
                            {
                                isInvalidAttribute = true;
                            }
                            isInvalidAttribute &= culture != null;
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            engine.ProcessDesc(culture);

            // Process child elements.
            ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, true, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseEmphasis(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Emphasis, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            // Set the default value
            ssmlAttributes._fragmentState.Emphasis = (int)EmphasisWord.Moderate;

            string? sLevel = null;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        // The W3C spec says ignore
                        case "level":
                            CheckForDuplicates(ref sLevel, reader);
                            int pos = Array.BinarySearch<string>(s_emphasisNames, sLevel);
                            if (pos < 0)
                            {
                                isInvalidAttribute = true;
                            }
                            else
                            {
                                ssmlAttributes._fragmentState.Emphasis = (int)s_emphasisWord[pos];
                            }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            engine.ProcessEmphasis(!string.IsNullOrEmpty(sLevel), (EmphasisWord)ssmlAttributes._fragmentState.Emphasis);

            // Process child elements.
            SsmlElement possibleChild = SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes);
            ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseMark(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Mark, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sName = null;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        // The W3C spec says ignore
                        case "name":
                            CheckForDuplicates(ref sName, reader);
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sName))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "name", "mark");
            }

            ssmlAttributes._fragmentState.Action = TtsEngineAction.Bookmark;
            engine.ProcessMark(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, sName, fIgnore);

            // No Children allowed.
            ProcessElement(reader, engine, sElement, 0, ssmlAttributes, true, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseMetaData(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            ValidateElement(element, SsmlElement.MetaData, reader.Name);

            // No processing for this element, skip
            if (!reader.IsEmptyElement)
            {
                int cEndNode = 1;
                do
                {
                    // Loop until we reach the end of the metadata element
                    reader.Read();

                    // Count the number of elements processed
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        cEndNode++;
                    }
                    if (reader.NodeType == XmlNodeType.EndElement || reader.NodeType == XmlNodeType.None)
                    {
                        cEndNode--;
                    }
                }
                while (cEndNode > 0);

                // Consume the end element
                System.Diagnostics.Debug.Assert(reader.NodeType == XmlNodeType.EndElement);
            }
        }

        private static void ParseParagraph(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Paragraph, reader.Name);

            ParseTextBlock(reader, engine, true, sElement, ssmAttributesParent, fIgnore);
        }

        private static void ParseSentence(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Sentence, reader.Name);

            ParseTextBlock(reader, engine, false, sElement, ssmAttributesParent, fIgnore);
        }

        private static void ParseTextBlock(XmlReader reader, ISsmlParser engine, bool isParagraph, string sElement, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sCulture = null;
            CultureInfo? culture = null;
            while (reader.MoveToNextAttribute())
            {
                bool isInvalidAttribute = reader.NamespaceURI != xmlNamespace;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        // The W3C spec says ignore
                        case "lang":
                            CheckForDuplicates(ref sCulture, reader);
                            try
                            {
                                culture = new CultureInfo(sCulture);
                            }
                            catch (ArgumentException)
                            {
                                isInvalidAttribute = true;
                            }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            // Try to change the voice
            bool fNewCulture = culture != null && culture.LCID != ssmlAttributes._fragmentState.LangId;
            ssmlAttributes._voice = engine.ProcessTextBlock(isParagraph, ssmlAttributes._voice, ref ssmlAttributes._fragmentState, culture, fNewCulture, ssmlAttributes._gender, ssmlAttributes._age);
            if (fNewCulture)
            {
                ssmlAttributes._fragmentState.LangId = culture!.LCID;
            }

            // Process child elements.
            SsmlElement possibleChild = SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes);
            if (isParagraph)
            {
                possibleChild |= SsmlElement.Sentence;
            }
            ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes);

            engine.EndProcessTextBlock(isParagraph);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParsePhoneme(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Phoneme, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sAlphabet = null;
            AlphabetType alphabet = AlphabetType.Ipa;
            string? sPh = null;
            char[]? aPhoneIds = null;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "alphabet":
                            CheckForDuplicates(ref sAlphabet, reader);
                            switch (sAlphabet)
                            {
                                case "ipa":
                                    alphabet = AlphabetType.Ipa;
                                    break;

                                case "sapi":
                                case "x-sapi":
                                case "x-microsoft-sapi":
                                    alphabet = AlphabetType.Sapi;
                                    break;

                                case "ups":
                                case "x-ups":
                                case "x-microsoft-ups":
                                    alphabet = AlphabetType.Ups;
                                    break;

                                default:
                                    throw new FormatException(SR.Get(SRID.UnsupportedAlphabet, sAlphabet));
                            }
                            break;

                        case "ph":
                            CheckForDuplicates(ref sPh, reader);
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sPh))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "ph", "phoneme");
            }

            // Try to convert the phoneme set
            try
            {
                switch (alphabet)
                {
                    case AlphabetType.Sapi:
                        aPhoneIds = PhonemeConverter.ConvertPronToId(sPh, ssmlAttributes._fragmentState.LangId).ToCharArray();
                        break;

                    case AlphabetType.Ups:
                        aPhoneIds = PhonemeConverter.UpsConverter.ConvertPronToId(sPh).ToCharArray();
                        alphabet = AlphabetType.Ipa;
                        break;

                    case AlphabetType.Ipa:
                    default:
                        aPhoneIds = sPh.ToCharArray();
                        try
                        {
                            PhonemeConverter.ValidateUpsIds(aPhoneIds);
                        }
                        catch (FormatException)
                        {
                            if (sAlphabet != null)
                            {
                                throw;
                            }
                            else
                            {
                                // try with sapi (backward compatibility)
                                // if not a sapi phoneme either throw the IPA exception
                                aPhoneIds = PhonemeConverter.ConvertPronToId(sPh, ssmlAttributes._fragmentState.LangId).ToCharArray();
                                alphabet = AlphabetType.Sapi;
                            }
                        }
                        break;
                }
            }
            catch (FormatException)
            {
                ThrowFormatException(SRID.InvalidItemAttribute, "phoneme");
            }

            engine.ProcessPhoneme(ref ssmlAttributes._fragmentState, alphabet, sPh, aPhoneIds);

            // Process child elements.
            ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, fIgnore, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseProsody(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Prosody, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sPitch = null;
            string? sContour = null;
            string? sRange = null;
            string? sRate = null;
            string? sDuration = null;
            string? sVolume = null;
            Prosody prosody = ssmlAttributes._fragmentState.Prosody != null ? ssmlAttributes._fragmentState.Prosody.Clone() : new Prosody();
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "pitch":
                            isInvalidAttribute = ParseNumberHz(reader, ref sPitch, s_pitchNames, s_pitchWords, ref prosody._pitch);
                            break;

                        case "range":
                            isInvalidAttribute = ParseNumberHz(reader, ref sRange, s_rangeNames, s_rangeWords, ref prosody._range);
                            break;

                        case "rate":
                            isInvalidAttribute = ParseNumberRelative(reader, ref sRate, s_rateNames, s_rateWords, ref prosody._rate);
                            break;

                        case "volume":
                            isInvalidAttribute = ParseNumberRelative(reader, ref sVolume, s_volumeNames, s_volumeWords, ref prosody._volume);
                            break;

                        case "duration":
                            CheckForDuplicates(ref sDuration, reader);
                            prosody.Duration = ParseCSS2Time(sDuration);
                            break;

                        case "contour":
                            CheckForDuplicates(ref sContour, reader);
                            prosody.SetContourPoints(ParseContour(sContour)!); // Validation issues in ParseContour manifest as ArgumentNullException out of SetContourPoints
                            if (prosody.GetContourPoints() == null) { isInvalidAttribute = true; }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sPitch) && string.IsNullOrEmpty(sContour) && string.IsNullOrEmpty(sRange) && string.IsNullOrEmpty(sRate) && string.IsNullOrEmpty(sDuration) && string.IsNullOrEmpty(sVolume))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "pitch, contour, range, rate, duration, volume", "prosody");
            }

            ssmlAttributes._fragmentState.Prosody = prosody;

            engine.ProcessProsody(sPitch, sRange, sRate, sVolume, sDuration, sContour);

            // Process child elements.
            SsmlElement possibleChild = SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes);
            ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseSayAs(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.SayAs, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sInterpretAs = null;
            string? sFormat = null;
            string? sDetail = null;
            System.Speech.Synthesis.TtsEngine.SayAs sayAs = new();
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "type":
                        case "interpret-as":
                            CheckForDuplicates(ref sInterpretAs, reader);
                            sayAs.InterpretAs = sInterpretAs;
                            break;

                        case "format":
                            CheckForDuplicates(ref sFormat, reader);
                            sayAs.Format = sFormat;
                            break;

                        case "detail":
                            CheckForDuplicates(ref sDetail, reader);
                            sayAs.Detail = sDetail;
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sInterpretAs))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "interpret-as", "say-as");
            }

            // Create SayAs attribute
            ssmlAttributes._fragmentState.SayAs = sayAs;

            engine.ProcessSayAs(sInterpretAs, sFormat, sDetail);

            // Process child elements.
            ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, fIgnore, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseSub(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Sub, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sAlias = null;
            int textPosition = 0;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        // The W3C spec says ignore
                        case "alias":
                            CheckForDuplicates(ref sAlias, reader);
                            if (reader is XmlTextReader textReader && engine.Ssml != null)
                            {
                                textPosition = engine.Ssml.IndexOf(reader.Value, textReader.LinePosition + reader.LocalName.Length, StringComparison.Ordinal);
                            }
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sAlias))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "alias", "sub");
            }

            engine.ProcessSub(sAlias, ssmlAttributes._voice, ref ssmlAttributes._fragmentState, textPosition, fIgnore);

            // The only allowed children element is text. Ignore it
            ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, true, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }
        private static void ParseVoice(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Voice, reader.Name);

            // Cannot have a voice element in a Prompt bout
            if (ssmAttributesParent._cPromptOutput > 0)
            {
                ThrowFormatException(SRID.InvalidVoiceElementInPromptOutput);
            }

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sCulture = null;
            string? sGender = null;
            string? sVariant = null;
            string? sName = null;
            string? sAge = null;
            string? xmlns = null;
            CultureInfo? culture = null;
            int variant = -1;

            List<SsmlXmlAttribute>? extraAttributes = null;
            List<SsmlXmlAttribute>? extraAttributesVoice = null;
            List<SsmlXmlAttribute>? localUnknownNamespaces = null;

            while (reader.MoveToNextAttribute())
            {
                bool isInvalidAttribute = false;

                // empty namespace
                if (reader.NamespaceURI.Length == 0)
                {
                    switch (reader.LocalName)
                    {
                        case "gender":
                            CheckForDuplicates(ref sGender, reader);
                            VoiceGender gender;
                            if (!SsmlParserHelpers.TryConvertGender(sGender, out gender))
                            {
                                isInvalidAttribute = true;
                            }
                            else
                            {
                                ssmlAttributes._gender = gender;
                            }
                            break;

                        case "age":
                            CheckForDuplicates(ref sAge, reader);
                            VoiceAge age;
                            if (!SsmlParserHelpers.TryConvertAge(sAge, out age))
                            {
                                isInvalidAttribute = true;
                            }
                            else
                            {
                                ssmlAttributes._age = age;
                            }
                            break;

                        case "variant":
                            // Ignore this field. We have no way with the current tokens to
                            // use it
                            CheckForDuplicates(ref sVariant, reader);
                            if (!int.TryParse(sVariant, out variant) || variant <= 0)
                            {
                                isInvalidAttribute = true;
                            }
                            break;

                        case "name":
                            CheckForDuplicates(ref sName, reader);
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                else
                {
                    if (reader.Prefix == "xmlns" && reader.Value == xmlNamespacePrompt)
                    {
                        CheckForDuplicates(ref xmlns, reader);
                    }
                    else
                    {
                        if (reader.NamespaceURI == xmlNamespace)
                        {
                            switch (reader.LocalName)
                            {
                                // The W3C spec says ignore
                                case "lang":
                                    CheckForDuplicates(ref sCulture, reader);
                                    try
                                    {
                                        culture = new CultureInfo(sCulture);
                                    }
                                    catch (ArgumentException)
                                    {
                                        isInvalidAttribute = true;
                                    }
                                    break;

                                default:
                                    isInvalidAttribute = true;
                                    break;
                            }
                        }
                        else if (reader.NamespaceURI == xmlNamespaceXmlns)
                        {
                            if (reader.Value != xmlNamespaceSsml)
                            {
                                localUnknownNamespaces ??= new List<SsmlXmlAttribute>();

                                SsmlXmlAttribute ns = new(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI);
                                localUnknownNamespaces.Add(ns);
                                ssmlAttributes._unknownNamespaces.Add(ns);
                            }
                        }
                        else
                        {
                            extraAttributesVoice ??= new List<SsmlXmlAttribute>();
                            extraAttributesVoice.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI));
                        }
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            // append the local attributes to list of unknown attributes
            if (extraAttributesVoice != null)
            {
                foreach (SsmlXmlAttribute attribute in extraAttributesVoice)
                {
                    ssmlAttributes.AddUnknowAttribute(attribute, ref extraAttributes);
                }
            }

            if (string.IsNullOrEmpty(sCulture) && string.IsNullOrEmpty(sGender) && string.IsNullOrEmpty(sAge) && string.IsNullOrEmpty(sVariant) && string.IsNullOrEmpty(sName) && string.IsNullOrEmpty(xmlns))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "'xml:lang' or 'gender' or 'age' or 'variant' or 'name'", "voice");
            }

            // Try to change the voice
            culture ??= new CultureInfo(ssmlAttributes._fragmentState.LangId);
            bool fNewCulture = culture.LCID != ssmlAttributes._fragmentState.LangId;
            ssmlAttributes._voice = engine.ProcessVoice(sName, culture, ssmlAttributes._gender, ssmlAttributes._age, variant, fNewCulture, localUnknownNamespaces);
            ssmlAttributes._fragmentState.LangId = culture.LCID;

            // Process child elements.
            SsmlElement possibleChild = SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes);
            ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes);

            // remove the local namespaces
            if (localUnknownNamespaces != null)
            {
                foreach (SsmlXmlAttribute ns in localUnknownNamespaces)
                {
                    ssmlAttributes._unknownNamespaces.Remove(ns);
                }
            }

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseLexicon(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.Lexicon, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;
            List<SsmlXmlAttribute>? extraAttributes = null;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            string? sUri = null;
            string? sMediaType = null;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = reader.NamespaceURI.Length != 0;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "uri":
                            CheckForDuplicates(ref sUri, reader);
                            break;

                        case "type":
                            CheckForDuplicates(ref sMediaType, reader);
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes))
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            if (string.IsNullOrEmpty(sUri))
            {
                ThrowFormatException(SRID.MissingRequiredAttribute, "uri", "lexicon");
            }

            // Add the base path if it exist
            Uri uri = new(sUri, UriKind.RelativeOrAbsolute);
            if (!uri.IsAbsoluteUri && ssmlAttributes._baseUri != null)
            {
                sUri = ssmlAttributes._baseUri + '/' + sUri;
                uri = new Uri(sUri, UriKind.RelativeOrAbsolute);
            }

            engine.ProcessLexicon(uri, sMediaType);

            // No Children allowed.
            ProcessElement(reader, engine, sElement, 0, ssmlAttributes, true, extraAttributes);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        #region Prompt Engine

        private delegate bool ProcessPromptEngine0(object voice);
        private delegate bool ProcessPromptEngine1(object voice, string? value);

        private static void ParsePromptEngine0(XmlReader reader, ISsmlParser engine, SsmlElement elementAllowed, SsmlElement element, ProcessPromptEngine0 process, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(elementAllowed, element, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            // No attributes allowed
            while (reader.MoveToNextAttribute())
            {
                if (reader.NamespaceURI == xmlNamespaceXmlns && reader.Value == xmlNamespacePrompt)
                {
                    engine.ContainsPexml(reader.LocalName);
                }
                else
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            // Notify the engine that the element is processed
            if (!process(ssmlAttributes._voice))
            {
                ThrowFormatException(SRID.InvalidElement, reader.Name);
            }

            // Process Children
            ProcessElement(reader, engine, sElement, SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes), ssmlAttributes, fIgnore, null);
        }

        private static string? ParsePromptEngine1(XmlReader reader, ISsmlParser engine, SsmlElement elementAllowed, SsmlElement element, string attribute, ProcessPromptEngine1 process, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(elementAllowed, element, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            // 1 attribute
            string? value = null;
            while (reader.MoveToNextAttribute())
            {
                if (reader.LocalName == attribute)
                {
                    CheckForDuplicates(ref value, reader);
                }
                else
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }

            // Notify the engine that the element is processed
            if (!process(ssmlAttributes._voice, value))
            {
                ThrowFormatException(SRID.InvalidElement, reader.Name);
            }

            // No Children allowed
            ProcessElement(reader, engine, sElement, SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes), ssmlAttributes, fIgnore, null);
            return value;
        }

        private static void ParsePromptOutput(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Increase the ref count for the Prompt output
            ssmAttributesParent._cPromptOutput++;

            ParsePromptEngine0(reader, engine, element, SsmlElement.PromptEngineOutput, new ProcessPromptEngine0(engine.BeginPromptEngineOutput), ssmAttributesParent, fIgnore);

            // Notify the engine that the element is processed
            engine.EndElement();

            // Decrease the ref count for the Prompt output
            ssmAttributesParent._cPromptOutput--;
            engine.EndPromptEngineOutput(ssmAttributesParent._voice);
        }

        private static void ParseDiv(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            ParsePromptEngine0(reader, engine, element, SsmlElement.PromptEngineDiv, new ProcessPromptEngine0(engine.ProcessPromptEngineDiv), ssmAttributesParent, fIgnore);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseDatabase(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            // Validate the SSML markup
            string sElement = ValidateElement(element, SsmlElement.PromptEngineDatabase, reader.Name);

            // Make a local copy of the ssmlAttribute
            SsmlAttributes ssmlAttributes = default;

            // This is equivalent to a memcpy
            ssmlAttributes = ssmAttributesParent;

            // No attributes allowed
            string? fname = null;
            string? delta = null;
            string? idset = null;
            while (reader.MoveToNextAttribute())
            {
                // Namespace must be empty
                bool isInvalidAttribute = false;

                if (!isInvalidAttribute)
                {
                    switch (reader.LocalName)
                    {
                        case "fname":
                            CheckForDuplicates(ref fname, reader);
                            break;

                        case "idset":
                            CheckForDuplicates(ref idset, reader);
                            break;

                        case "delta":
                            CheckForDuplicates(ref delta, reader);
                            break;

                        default:
                            isInvalidAttribute = true;
                            break;
                    }
                }
                if (isInvalidAttribute)
                {
                    ThrowFormatException(SRID.InvalidItemAttribute, reader.Name);
                }
            }
            // Notify the engine that the element is processed
            if (!engine.ProcessPromptEngineDatabase(ssmlAttributes._voice, fname, delta, idset))
            {
                ThrowFormatException(SRID.InvalidElement, reader.Name);
            }

            // No Children allowed
            ProcessElement(reader, engine, sElement, 0, ssmlAttributes, fIgnore, null);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseId(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            ParsePromptEngine1(reader, engine, element, SsmlElement.PromptEngineId, "id", new ProcessPromptEngine1(engine.ProcessPromptEngineId), ssmAttributesParent, fIgnore);

            // Notify the engine that the element is processed
            engine.EndElement();
        }

        private static void ParseTts(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            ParsePromptEngine0(reader, engine, element, SsmlElement.PromptEngineTTS, new ProcessPromptEngine0(engine.BeginPromptEngineTts), ssmAttributesParent, fIgnore);

            // Notify the engine that the element is processed
            engine.EndElement();
            engine.EndPromptEngineTts(ssmAttributesParent._voice);
        }

        private static void ParseWithTag(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            string? tag = ParsePromptEngine1(reader, engine, element, SsmlElement.PromptEngineWithTag, "tag", new ProcessPromptEngine1(engine.BeginPromptEngineWithTag), ssmAttributesParent, fIgnore);

            // Notify the engine that the element is processed
            engine.EndElement();
            engine.EndPromptEngineWithTag(ssmAttributesParent._voice, tag);
        }

        private static void ParseRule(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore)
        {
            string? name = ParsePromptEngine1(reader, engine, element, SsmlElement.PromptEngineRule, "name", new ProcessPromptEngine1(engine.BeginPromptEngineRule), ssmAttributesParent, fIgnore);

            // Notify the engine that the element is processed
            engine.EndElement();
            engine.EndPromptEngineRule(ssmAttributesParent._voice, name);
        }

        #endregion

        private static void CheckForDuplicates([NotNull] ref string? dest, XmlReader reader)
        {
            if (!string.IsNullOrEmpty(dest))
            {
                StringBuilder attribute = new(reader.LocalName);
                if (reader.NamespaceURI.Length > 0)
                {
                    attribute.Append(reader.NamespaceURI);
                    attribute.Append(':');
                }
                ThrowFormatException(SRID.InvalidAttributeDefinedTwice, reader.Value, attribute);
            }
            dest = reader.Value;
        }

        private static int ParseCSS2Time(string time)
        {
            time = time.Trim(Helpers._achTrimChars);
            int pos = time.IndexOf("ms", StringComparison.Ordinal);
            int duration = -1;
            float fDuration;
            if (pos > 0 && time.Length == pos + 2)
            {
                if (!float.TryParse(time.Substring(0, pos), out fDuration))
                {
                    duration = -1;
                }
                else
                {
                    duration = (int)(fDuration + 0.5);
                }
            }
            else
                if ((pos = time.IndexOf('s')) > 0 && time.Length == pos + 1)
            {
                if (!float.TryParse(time.Substring(0, pos), out fDuration))
                {
                    duration = -1;
                }
                else
                {
                    duration = (int)(fDuration * 1000);
                }
            }
            return duration;
        }

        private static ContourPoint[]? ParseContour(string contour)
        {
            char[] achContour = contour.ToCharArray();
            List<ContourPoint> points = new();
            int start = 0;

            try
            {
                while (start < achContour.Length)
                {
                    bool percent, ignored, hz;
                    // Form is (0%, +20Hz)
                    if ((start = NextChar(achContour, start, '(', false, out ignored)) < 0)
                    {
                        // End of the string found exit
                        break;
                    }

                    int comma = NextChar(achContour, start, ',', true, out percent);
                    int parenthesis = NextChar(achContour, comma, ')', true, out ignored);

                    ProsodyNumber timePosition = new();
                    ProsodyNumber target = new();

                    // Parse the 2 numbers
                    if (!percent || !TryParseNumber(contour.Substring(start, comma - (start + 1)), ref timePosition) || timePosition.SsmlAttributeId == ProsodyNumber.AbsoluteNumber)
                    {
                        return null;
                    }
                    if (!TryParseHz(contour.Substring(comma, parenthesis - (comma + 1)), ref target, true, out hz))
                    {
                        return null;
                    }

                    // First point
                    if (points.Count == 0)
                    {
                        // fake a zero entry if none is provided by duplicating the first entry
                        if (timePosition.Number > 0 && timePosition.Number < 100)
                        {
                            points.Add(new ContourPoint(0, target.Number, ContourPointChangeType.Hz));
                        }
                    }
                    else
                    {
                        // Accept only increasing start points
                        // Add a 100% if necessary
                        if (points[points.Count - 1].Start > timePosition.Number)
                        {
                            return null;
                        }
                    }

                    if (timePosition.Number >= 0 && timePosition.Number <= 1)
                    {
                        points.Add(new ContourPoint(timePosition.Number, target.Number, (hz ? ContourPointChangeType.Hz : ContourPointChangeType.Percentage)));
                    }
                    start = parenthesis;
                }
            }
            catch (InvalidOperationException)
            {
                return null;
            }

            if (points.Count < 1)
            {
                return null;
            }

            // Add a 100% if necessary
            if (!points[points.Count - 1].Start.Equals(1.0f))
            {
                points.Add(new ContourPoint(1, points[points.Count - 1].Change, points[points.Count - 1].ChangeType));
            }
            return points.ToArray();
        }

        private static int NextChar(char[] ach, int start, char expected, bool skipDigit, out bool percent)
        {
            percent = false;

            // skip the whitespace
            while (start < ach.Length && (ach[start] == ' ' || ach[start] == '\t' || ach[start] == '\n' || ach[start] == '\r'))
            {
                start++;
            }

            // skip the digits
            if (skipDigit)
            {
                while (start < ach.Length && ach[start] != expected && ((percent = ach[start] == '%') || char.IsDigit(ach[start]) || ach[start] == 'H' || ach[start] == 'z' || ach[start] == '.' || ach[start] == '+' || ach[start] == '-'))
                {
                    start++;
                }

                // skip the trailing white spaces
                while (start < ach.Length && (ach[start] == ' ' || ach[start] == '\t' || ach[start] == '\n' || ach[start] == '\r'))
                {
                    start++;
                }
            }

            // Check if we found the character we wanted
            if (!(start < ach.Length && ach[start] == expected))
            {
                // Check for the end of the string
                if (!skipDigit && start == ach.Length)
                {
                    return -1;
                }
                // bail out
                throw new InvalidOperationException();
            }
            return start + 1;
        }

        private static bool ParseNumberHz(XmlReader reader, ref string? attribute, string[] attributeValues, int[] attributeConst, ref ProsodyNumber number)
        {
            bool isInvalidAttribute = false;
            bool isHz;

            CheckForDuplicates(ref attribute, reader);
            int pos = Array.BinarySearch<string>(attributeValues, attribute);
            if (pos < 0)
            {
                if (!TryParseHz(attribute, ref number, false, out isHz))
                {
                    isInvalidAttribute = true;
                }
            }
            else
            {
                number = new ProsodyNumber(attributeConst[pos]);
            }
            return isInvalidAttribute;
        }

        private static bool ParseNumberRelative(XmlReader reader, ref string? attribute, string[] attributeValues, int[] attributeConst, ref ProsodyNumber number)
        {
            bool isInvalidAttribute = false;

            CheckForDuplicates(ref attribute, reader);
            int pos = Array.BinarySearch<string>(attributeValues, attribute);
            if (pos < 0)
            {
                if (!TryParseNumber(attribute, ref number))
                {
                    isInvalidAttribute = true;
                }
            }
            else
            {
                number = new ProsodyNumber(attributeConst[pos]);
            }
            return isInvalidAttribute;
        }

        private static bool TryParseNumber(string sNumber, ref ProsodyNumber number)
        {
            bool fResult = false;
            decimal value = 0;

            // always reset the unit to Default
            number.Unit = ProsodyUnit.Default;
            sNumber = sNumber.Trim(Helpers._achTrimChars);
            if (!string.IsNullOrEmpty(sNumber))
            {
                if (!decimal.TryParse(sNumber, out value))
                {
                    if (sNumber[sNumber.Length - 1] == '%')
                    {
                        if (decimal.TryParse(sNumber.Substring(0, sNumber.Length - 1), out value))
                        {
                            float percent = (float)value / 100f;
                            if (sNumber[0] != '+' && sNumber[0] != '-')
                            {
                                number.Number *= percent;
                            }
                            else
                            {
                                number.Number += number.Number * (percent);
                            }

                            fResult = true;
                        }
                    }
                }
                else
                {
                    if (sNumber[0] != '+' && sNumber[0] != '-')
                    {
                        number.Number = (float)value;
                        number.SsmlAttributeId = ProsodyNumber.AbsoluteNumber;
                    }
                    else
                    {
                        if (number.IsNumberPercent)
                        {
                            number.Number *= (float)value;
                        }
                        else
                        {
                            number.Number += (float)value;
                        }
                    }
                    number.IsNumberPercent = false;
                    fResult = true;
                }
            }
            return fResult;
        }

        private static bool TryParseHz(string sNumber, ref ProsodyNumber number, bool acceptHzRelative, out bool isHz)
        {
            isHz = false;

            // Find the Hz at the end of the number
            bool fResult = false;
            number.SsmlAttributeId = ProsodyNumber.AbsoluteNumber;
            ProsodyUnit unit = ProsodyUnit.Default;

            sNumber = sNumber.Trim(Helpers._achTrimChars);
            if (sNumber.IndexOf("Hz", StringComparison.Ordinal) == sNumber.Length - 2)
            {
                unit = ProsodyUnit.Hz;
            }
            else if (sNumber.IndexOf("st", StringComparison.Ordinal) == sNumber.Length - 2)
            {
                unit = ProsodyUnit.Semitone;
            }

            if (unit != ProsodyUnit.Default)
            {
                // Try as an Absolute Hz value
                fResult = TryParseNumber(sNumber.Substring(0, sNumber.Length - 2), ref number) && (acceptHzRelative || number.SsmlAttributeId == ProsodyNumber.AbsoluteNumber);
                isHz = true;
            }
            else
            {
                // Must be a relative number
                fResult = TryParseNumber(sNumber, ref number) && number.SsmlAttributeId == ProsodyNumber.AbsoluteNumber;
            }

            return fResult;
        }

        /// <summary>
        /// Ensure the this element is properly placed in the SSML markup
        /// </summary>
        private static string ValidateElement(SsmlElement possibleElements, SsmlElement currentElement, string sElement)
        {
            if ((possibleElements & currentElement) == 0)
            {
                ThrowFormatException(SRID.InvalidElement, sElement);
            }
            return sElement;
        }

        /// <summary>
        /// Throws an Exception with the error specified by the resource ID.
        /// </summary>
        [DoesNotReturn]
        private static void ThrowFormatException(SRID id, params object[] args)
        {
            throw new FormatException(SR.Get(id, args));
        }

        /// <summary>
        /// Throws an Exception with the error specified by the resource ID.
        /// </summary>
        [DoesNotReturn]
        private static void ThrowFormatException(Exception? innerException, SRID id, params object[] args)
        {
            throw new FormatException(SR.Get(id, args), innerException);
        }

        /// <summary>
        /// Non speakable element
        /// </summary>
        private static void NoOp(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmlAttributes, bool fIgnore)
        {
            // No Children allowed .
            ProcessElement(reader, engine, null, 0, ssmlAttributes, true, null);
        }

        private static SsmlElement ElementPromptEngine(SsmlAttributes ssmlAttributes)
        {
            return ssmlAttributes._cPromptOutput > 0 ? SsmlElement.PromptEngineChildren : 0;
        }

        private static int GetColumnPosition(XmlReader reader)
        {
            XmlTextReader? textReader = reader as XmlTextReader;
            return textReader != null ? textReader.LinePosition - 1 : 0;
        }

        #endregion

        #region Private Types

        private struct SsmlAttributes
        {
            public SsmlAttributes(object voice, List<SsmlXmlAttribute> unknownNamespaces)
            {
                _voice = voice;
                _unknownNamespaces = unknownNamespaces;
            }

            internal object _voice;
            internal FragmentState _fragmentState;
            internal bool _fRenderDesc;
            internal VoiceAge _age;
            internal VoiceGender _gender;
            internal string? _baseUri;
            internal short _cPromptOutput;
            internal List<SsmlXmlAttribute> _unknownNamespaces;

            internal bool AddUnknowAttribute(SsmlXmlAttribute attribute, ref List<SsmlXmlAttribute>? extraAttributes)
            {
                foreach (SsmlXmlAttribute ns in _unknownNamespaces)
                {
                    if (ns._name == attribute._prefix)
                    {
                        extraAttributes ??= new List<SsmlXmlAttribute>();
                        extraAttributes.Add(attribute);
                        return true;
                    }
                }
                return false;
            }

            internal bool AddUnknowAttribute(XmlReader reader, ref List<SsmlXmlAttribute>? extraAttributes)
            {
                foreach (SsmlXmlAttribute ns in _unknownNamespaces)
                {
                    if (ns._name == reader.Prefix)
                    {
                        extraAttributes ??= new List<SsmlXmlAttribute>();
                        extraAttributes.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI));
                        return true;
                    }
                }
                return false;
            }

            internal bool IsOtherNamespaceElement(XmlReader reader)
            {
                foreach (SsmlXmlAttribute ns in _unknownNamespaces)
                {
                    if (ns._name == reader.Prefix)
                    {
                        return true;
                    }
                }
                return false;
            }
        }

        private delegate void ParseElementDelegates(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmlAttributes, bool fIgnore);

        #endregion

        #region Private Fields

        private static readonly string[] s_elementsName = new string[]
        {
            "audio",
            "break",
            "database",
            "desc",
            "div",
            "emphasis",
            "id",
            "lexicon",
            "mark",
            "meta",
            "metadata",
            "p",
            "paragraph",
            "phoneme",
            "prompt_output",
            "prosody",
            "rule",
            "s",
            "say-as",
            "sentence",
            "speak",
            "sub",
            "tts",
            "voice",
            "withtag",
        };

        private static readonly ParseElementDelegates[] s_parseElements = new ParseElementDelegates[]
            {
                new ParseElementDelegates (ParseAudio),
                new ParseElementDelegates (ParseBreak),
                new ParseElementDelegates (ParseDatabase),
                new ParseElementDelegates (ParseDesc),
                new ParseElementDelegates (ParseDiv),
                new ParseElementDelegates (ParseEmphasis),
                new ParseElementDelegates (ParseId),
                new ParseElementDelegates (ParseLexicon),
                new ParseElementDelegates (ParseMark),
                new ParseElementDelegates (NoOp),
                new ParseElementDelegates (ParseMetaData),
                new ParseElementDelegates (ParseParagraph),
                new ParseElementDelegates (ParseParagraph),
                new ParseElementDelegates (ParsePhoneme),
                new ParseElementDelegates (ParsePromptOutput),
                new ParseElementDelegates (ParseProsody),
                new ParseElementDelegates (ParseRule),
                new ParseElementDelegates (ParseSentence),
                new ParseElementDelegates (ParseSayAs),
                new ParseElementDelegates (ParseSentence),
                new ParseElementDelegates (NoOp),
                new ParseElementDelegates (ParseSub),
                new ParseElementDelegates (ParseTts),
                new ParseElementDelegates (ParseVoice),
                new ParseElementDelegates (ParseWithTag)
            };

        private static readonly string[] s_breakStrength = new string[]
        {
            "medium", "none", "strong", "weak", "x-strong", "x-weak"
        };

        /// <summary>
        /// Must be in the same order as the _breakStrength enumeration
        /// </summary>
        private static readonly EmphasisBreak[] s_breakEmphasis = new EmphasisBreak[]
        {
            EmphasisBreak.Medium, EmphasisBreak.None, EmphasisBreak.Strong, EmphasisBreak.Weak, EmphasisBreak.ExtraStrong, EmphasisBreak.ExtraWeak
        };

        private static readonly string[] s_emphasisNames = new string[]
        {
            "moderate", "none", "reduced", "strong"
        };

        /// <summary>
        /// Must be in the same order as the _emphasisNames enumeration
        /// </summary>
        private static readonly EmphasisWord[] s_emphasisWord = new EmphasisWord[]
        {
            EmphasisWord.Moderate, EmphasisWord.None, EmphasisWord.Reduced, EmphasisWord.Strong
        };

        /// <summary>
        /// Must be in the same order as the _emphasisNames enumeration
        /// </summary>
        private static readonly int[] s_pitchWords = new int[]
        {
            (int) ProsodyPitch.Default, (int) ProsodyPitch.High, (int) ProsodyPitch.Low, (int) ProsodyPitch.Medium, (int) ProsodyPitch.ExtraHigh, (int) ProsodyPitch.ExtraLow
        };

        private static readonly string[] s_pitchNames = new string[]
        {
            "default", "high", "low", "medium", "x-high", "x-low",
        };

        /// <summary>
        /// Must be in the same order as the _emphasisNames enumeration
        /// </summary>
        private static readonly int[] s_rangeWords = new int[]
        {
            (int) ProsodyRange.Default, (int) ProsodyRange.High, (int) ProsodyRange.Low, (int) ProsodyRange.Medium, (int) ProsodyRange.ExtraHigh, (int) ProsodyRange.ExtraLow
        };

        private static readonly string[] s_rangeNames = new string[]
        {
            "default", "high", "low", "medium", "x-high", "x-low",
        };

        /// <summary>
        /// Must be in the same order as the _emphasisNames enumeration
        /// </summary>
        private static readonly int[] s_rateWords = new int[]
        {
            (int) ProsodyRate.Default, (int) ProsodyRate.Fast, (int) ProsodyRate.Medium, (int) ProsodyRate.Slow, (int) ProsodyRate.ExtraFast, (int) ProsodyRate.ExtraSlow
        };

        private static readonly string[] s_rateNames = new string[]
        {
            "default", "fast", "medium", "slow", "x-fast", "x-slow",
        };

        /// <summary>
        /// Must be in the same order as the _emphasisNames enumeration
        /// </summary>
        private static readonly int[] s_volumeWords = new int[]
        {
            (int) ProsodyVolume.Default, (int) ProsodyVolume.Loud, (int) ProsodyVolume.Medium, (int) ProsodyVolume.Silent, (int) ProsodyVolume.Soft, (int) ProsodyVolume.ExtraLoud, (int) ProsodyVolume.ExtraSoft
        };

        private static readonly string[] s_volumeNames = new string[]
        {
            "default", "loud", "medium", "silent", "soft", "x-loud", "x-soft",
        };

        private const string xmlNamespace = "http://www.w3.org/XML/1998/namespace";
        private const string xmlNamespaceSsml = "http://www.w3.org/2001/10/synthesis";
        private const string xmlNamespaceXmlns = "http://www.w3.org/2000/xmlns/";
        private const string xmlNamespacePrompt = "http://schemas.microsoft.com/Speech/2003/03/PromptEngine";

        #endregion
    }

    internal static class SsmlParserHelpers
    {
        internal static bool TryConvertAge(string sAge, out VoiceAge age)
        {
            bool fResult = false;
            int iAge;
            age = VoiceAge.NotSet;

            switch (sAge)
            {
                case "child":
                    age = VoiceAge.Child;
                    break;

                case "teenager":
                case "teen":
                    age = VoiceAge.Teen;
                    break;

                case "adult":
                    age = VoiceAge.Adult;
                    break;

                case "elder":
                case "senior":
                    age = VoiceAge.Senior;
                    break;
            }
            if (age != VoiceAge.NotSet)
            {
                fResult = true;
            }
            else if (int.TryParse(sAge, out iAge))
            {
                if (iAge <= ((int)VoiceAge.Teen + (int)VoiceAge.Child) / 2)
                {
                    age = VoiceAge.Child;
                }
                else if (iAge <= ((int)VoiceAge.Adult + (int)VoiceAge.Teen) / 2)
                {
                    age = VoiceAge.Teen;
                }
                else if (iAge <= ((int)VoiceAge.Senior + (int)VoiceAge.Adult) / 2)
                {
                    age = VoiceAge.Adult;
                }
                else
                {
                    age = VoiceAge.Senior;
                }
                fResult = true;
            }
            return fResult;
        }

        internal static bool TryConvertGender(string sGender, out VoiceGender gender)
        {
            bool fResult = false;
            gender = VoiceGender.NotSet;

            int pos = Array.BinarySearch<string>(s_genderNames, sGender);
            if (pos >= 0)
            {
                gender = s_genders[pos];
                fResult = true;
            }
            return fResult;
        }

        private static readonly string[] s_genderNames = new string[]
        {
            "female", "male", "neutral"
        };

        /// <summary>
        /// Must be in the same order as the _genderNames enumeration
        /// </summary>
        private static readonly VoiceGender[] s_genders = new VoiceGender[]
        {
            VoiceGender.Female, VoiceGender.Male, VoiceGender.Neutral
        };
    }

    #region Internal Types

    [Flags]
    internal enum SsmlElement
    {
        Speak = 0x0001,
        Voice = 0x0002,
        Audio = 0x0004,
        Lexicon = 0x0008,
        Meta = 0x0010,
        MetaData = 0x0020,
        Sentence = 0x0040,
        Paragraph = 0x0080,
        SayAs = 0x0100,
        Phoneme = 0x0200,
        Sub = 0x0400,
        Emphasis = 0x0800,
        Break = 0x1000,
        Prosody = 0x2000,
        Mark = 0x4000,
        Desc = 0x8000,
        Text = 0x10000,
        PromptEngineOutput = 0x20000,
        PromptEngineDatabase = 0x40000,
        PromptEngineDiv = 0x80000,
        PromptEngineId = 0x100000,
        PromptEngineTTS = 0x200000,
        PromptEngineWithTag = 0x400000,
        PromptEngineRule = 0x800000,

        ParagraphOrSentence = Sentence | Paragraph,

        AudioMarkTextWithStyle = Audio | Mark | Break | Emphasis | Phoneme | Prosody | SayAs | Sub | Voice | Text | PromptEngineOutput,
        PromptEngineChildren = PromptEngineDatabase | PromptEngineDiv | PromptEngineId | PromptEngineTTS | PromptEngineWithTag | PromptEngineRule
    }

    #endregion
}