File: Synthesis\PromptBuilder.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.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Speech.Internal;
using System.Speech.Internal.Synthesis;
using System.Xml;

namespace System.Speech.Synthesis
{
    [Serializable]
    public class PromptBuilder
    {
        #region Constructors
        public PromptBuilder()
            : this(CultureInfo.CurrentUICulture)
        {
        }
        public PromptBuilder(CultureInfo culture)
        {
            ArgumentNullException.ThrowIfNull(culture);

            if (culture.Equals(CultureInfo.InvariantCulture))
            {
                throw new ArgumentException(SR.Get(SRID.InvariantCultureInfo), nameof(culture));
            }
            _culture = culture;

            // Reset all value to default
            ClearContent();
        }

        #endregion

        #region Public Methods

        // Use Append* naming convention.

        /// <summary>
        /// Clear the content of the prompt builder
        /// </summary>
        public void ClearContent()
        {
            _elements.Clear();
            _elementStack.Push(new StackElement(SsmlElement.Lexicon | SsmlElement.Meta | SsmlElement.MetaData | SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle, SsmlState.Header, _culture));
        }

        /// <summary>
        /// Append Text to the SSML stream
        /// </summary>
        public void AppendText(string textToSpeak)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            // Validate that text can be added in this context
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            _elements.Add(new Element(ElementType.Text, textToSpeak));
        }
        public void AppendText(string textToSpeak, PromptRate rate)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            if (rate < PromptRate.NotSet || rate > PromptRate.ExtraSlow)
            {
                throw new ArgumentOutOfRangeException(nameof(rate));
            }

            // Validate that text can be added in this context
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            Element prosodyElement = new(ElementType.Prosody, textToSpeak);
            _elements.Add(prosodyElement);
            string? sPromptRate = null;
            switch (rate)
            {
                case PromptRate.NotSet:
                    break;

                case PromptRate.ExtraFast:
                    sPromptRate = "x-fast";
                    break;

                case PromptRate.ExtraSlow:
                    sPromptRate = "x-slow";
                    break;

                default:
                    sPromptRate = rate.ToString().ToLowerInvariant();
                    break;
            }
            if (!string.IsNullOrEmpty(sPromptRate))
            {
                prosodyElement._attributes = new Collection<AttributeItem>();
                prosodyElement._attributes.Add(new AttributeItem("rate", sPromptRate));
            }
        }
        public void AppendText(string textToSpeak, PromptVolume volume)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            if (volume < PromptVolume.NotSet || volume > PromptVolume.Default)
            {
                throw new ArgumentOutOfRangeException(nameof(volume));
            }

            // Validate that text can be added in this context
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            Element prosodyElement = new(ElementType.Prosody, textToSpeak);
            _elements.Add(prosodyElement);

            string? sVolumeLevel = null;
            switch (volume)
            {
                // No volume do not set the attribute
                case PromptVolume.NotSet:
                    break;

                case PromptVolume.ExtraSoft:
                    sVolumeLevel = "x-soft";
                    break;

                case PromptVolume.ExtraLoud:
                    sVolumeLevel = "x-loud";
                    break;

                default:
                    sVolumeLevel = volume.ToString().ToLowerInvariant();
                    break;
            }
            if (!string.IsNullOrEmpty(sVolumeLevel))
            {
                prosodyElement._attributes = new Collection<AttributeItem>();
                prosodyElement._attributes.Add(new AttributeItem("volume", sVolumeLevel));
            }
        }
        public void AppendText(string textToSpeak, PromptEmphasis emphasis)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            if (emphasis < PromptEmphasis.NotSet || emphasis > PromptEmphasis.Reduced)
            {
                throw new ArgumentOutOfRangeException(nameof(emphasis));
            }

            // Validate that text can be added in this context
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            Element emphasisElement = new(ElementType.Emphasis, textToSpeak);
            _elements.Add(emphasisElement);

            if (emphasis != PromptEmphasis.NotSet)
            {
                emphasisElement._attributes = new Collection<AttributeItem>();
                emphasisElement._attributes.Add(new AttributeItem("level", emphasis.ToString().ToLowerInvariant()));
            }
        }
        public void StartStyle(PromptStyle style)
        {
            ArgumentNullException.ThrowIfNull(style);

            // Validate that text can be added in this context
            StackElement stackElement = _elementStack.Peek();
            ValidateElement(stackElement, SsmlElement.Prosody);

            // For emphasis or Prosody the list of possible elements that can be children is different.
            SsmlState ssmlState = 0;
            SsmlElement possibleChildren = stackElement._possibleChildren;

            _elements.Add(new Element(ElementType.StartStyle));

            if (style.Emphasis != PromptEmphasis.NotSet)
            {
                Element emphasisElement = new(ElementType.Emphasis);
                _elements.Add(emphasisElement);

                emphasisElement._attributes = new Collection<AttributeItem>();
                emphasisElement._attributes.Add(new AttributeItem("level", style.Emphasis.ToString().ToLowerInvariant()));

                // Set the expected children and mark the element used
                possibleChildren = SsmlElement.AudioMarkTextWithStyle;
                ssmlState = SsmlState.StyleEmphasis;
            }

            if (style.Rate != PromptRate.NotSet || style.Volume != PromptVolume.NotSet)
            {
                // two elements add a second start style
                if (ssmlState != 0)
                {
                    _elements.Add(new Element(ElementType.StartStyle));
                }

                Element prosodyElement = new(ElementType.Prosody);
                _elements.Add(prosodyElement);

                if (style.Rate != PromptRate.NotSet)
                {
                    string sPromptRate;
                    switch (style.Rate)
                    {
                        case PromptRate.ExtraFast:
                            sPromptRate = "x-fast";
                            break;

                        case PromptRate.ExtraSlow:
                            sPromptRate = "x-slow";
                            break;

                        default:
                            sPromptRate = style.Rate.ToString().ToLowerInvariant();
                            break;
                    }
                    prosodyElement._attributes = new Collection<AttributeItem>();
                    prosodyElement._attributes.Add(new AttributeItem("rate", sPromptRate));
                }

                if (style.Volume != PromptVolume.NotSet)
                {
                    string sVolumeLevel;
                    switch (style.Volume)
                    {
                        case PromptVolume.ExtraSoft:
                            sVolumeLevel = "x-soft";
                            break;

                        case PromptVolume.ExtraLoud:
                            sVolumeLevel = "x-loud";
                            break;

                        default:
                            sVolumeLevel = style.Volume.ToString().ToLowerInvariant();
                            break;
                    }
                    prosodyElement._attributes ??= new Collection<AttributeItem>();
                    prosodyElement._attributes.Add(new AttributeItem("volume", sVolumeLevel));
                }

                // Set the expected children and mark the element used
                possibleChildren = SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle;
                ssmlState |= SsmlState.StyleProsody;
            }

            _elementStack.Push(new StackElement(possibleChildren, ssmlState, stackElement._culture));
        }
        public void EndStyle()
        {
            StackElement stackElement = _elementStack.Pop();
            if (stackElement._state != 0)
            {
                if ((stackElement._state & (SsmlState.StyleEmphasis | SsmlState.StyleProsody)) == 0)
                {
                    throw new InvalidOperationException(SR.Get(SRID.PromptBuilderMismatchStyle));
                }

                _elements.Add(new Element(ElementType.EndStyle));

                // Check if 2 xml elements have been created
                if (stackElement._state == (SsmlState.StyleEmphasis | SsmlState.StyleProsody))
                {
                    _elements.Add(new Element(ElementType.EndStyle));
                }
            }
        }
        public void StartVoice(VoiceInfo voice)
        {
            ArgumentNullException.ThrowIfNull(voice);

            if (!VoiceInfo.ValidateGender(voice.Gender))
            {
                throw new ArgumentException(SR.Get(SRID.EnumInvalid, "VoiceGender"), nameof(voice));
            }

            if (!VoiceInfo.ValidateAge(voice.Age))
            {
                throw new ArgumentException(SR.Get(SRID.EnumInvalid, "VoiceAge"), nameof(voice));
            }

            StackElement stackElement = _elementStack.Peek();
            ValidateElement(stackElement, SsmlElement.Voice);

            CultureInfo culture = voice.Culture ?? stackElement._culture;

            Element startVoice = new(ElementType.StartVoice);
            startVoice._attributes = new Collection<AttributeItem>();
            _elements.Add(startVoice);

            if (!string.IsNullOrEmpty(voice.Name))
            {
                startVoice._attributes.Add(new AttributeItem("name", voice.Name));
            }

            if (voice.Culture != null)
            {
                startVoice._attributes.Add(new AttributeItem("xml", "lang", voice.Culture.Name));
            }

            if (voice.Gender != VoiceGender.NotSet)
            {
                startVoice._attributes.Add(new AttributeItem("gender", voice.Gender.ToString().ToLowerInvariant()));
            }

            if (voice.Age != VoiceAge.NotSet)
            {
                startVoice._attributes.Add(new AttributeItem("age", ((int)voice.Age).ToString(CultureInfo.InvariantCulture)));
            }

            if (voice.Variant >= 0)
            {
                startVoice._attributes.Add(new AttributeItem("variant", voice.Variant.ToString(CultureInfo.InvariantCulture)));
            }

            _elementStack.Push(new StackElement(SsmlElement.Sentence | SsmlElement.AudioMarkTextWithStyle, SsmlState.Voice, culture));
        }
        public void StartVoice(string name)
        {
            Helpers.ThrowIfEmptyOrNull(name, nameof(name));

            StartVoice(new VoiceInfo(name));
        }
        public void StartVoice(VoiceGender gender)
        {
            StartVoice(new VoiceInfo(gender));
        }
        public void StartVoice(VoiceGender gender, VoiceAge age)
        {
            StartVoice(new VoiceInfo(gender, age));
        }
        public void StartVoice(VoiceGender gender, VoiceAge age, int voiceAlternate)
        {
            StartVoice(new VoiceInfo(gender, age, voiceAlternate));
        }
        public void StartVoice(CultureInfo culture)
        {
            StartVoice(new VoiceInfo(culture));
        }
        public void EndVoice()
        {
            if (_elementStack.Pop()._state != SsmlState.Voice)
            {
                throw new InvalidOperationException(SR.Get(SRID.PromptBuilderMismatchVoice));
            }

            _elements.Add(new Element(ElementType.EndVoice));
        }

        // <paragraph>, <sentence>
        public void StartParagraph()
        {
            StartParagraph(null);
        }
        public void StartParagraph(CultureInfo? culture)
        {
            // check for well formed document
            StackElement stackElement = _elementStack.Peek();
            ValidateElement(stackElement, SsmlElement.Paragraph);

            Element startParagraph = new(ElementType.StartParagraph);
            _elements.Add(startParagraph);

            if (culture != null)
            {
                if (culture.Equals(CultureInfo.InvariantCulture))
                {
                    throw new ArgumentException(SR.Get(SRID.InvariantCultureInfo), nameof(culture));
                }
                startParagraph._attributes = new Collection<AttributeItem>();
                startParagraph._attributes.Add(new AttributeItem("xml", "lang", culture.Name));
            }
            else
            {
                culture = stackElement._culture;
            }
            _elementStack.Push(new StackElement(SsmlElement.AudioMarkTextWithStyle | SsmlElement.Sentence, SsmlState.Paragraph, culture));
        }
        public void EndParagraph()
        {
            if (_elementStack.Pop()._state != SsmlState.Paragraph)
            {
                throw new InvalidOperationException(SR.Get(SRID.PromptBuilderMismatchParagraph));
            }
            _elements.Add(new Element(ElementType.EndParagraph));
        }
        public void StartSentence()
        {
            StartSentence(null);
        }
        public void StartSentence(CultureInfo? culture)
        {
            // check for well formed document
            StackElement stackElement = _elementStack.Peek();
            ValidateElement(stackElement, SsmlElement.Sentence);

            Element startSentence = new(ElementType.StartSentence);
            _elements.Add(startSentence);

            if (culture != null)
            {
                if (culture.Equals(CultureInfo.InvariantCulture))
                {
                    throw new ArgumentException(SR.Get(SRID.InvariantCultureInfo), nameof(culture));
                }

                startSentence._attributes = new Collection<AttributeItem>();
                startSentence._attributes.Add(new AttributeItem("xml", "lang", culture.Name));
            }
            else
            {
                culture = stackElement._culture;
            }
            _elementStack.Push(new StackElement(SsmlElement.AudioMarkTextWithStyle, SsmlState.Sentence, culture));
        }
        public void EndSentence()
        {
            if (_elementStack.Pop()._state != SsmlState.Sentence)
            {
                throw new InvalidOperationException(SR.Get(SRID.PromptBuilderMismatchSentence));
            }
            _elements.Add(new Element(ElementType.EndSentence));
        }
        public void AppendTextWithHint(string textToSpeak, SayAs sayAs)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            if (sayAs < SayAs.SpellOut || sayAs > SayAs.Text)
            {
                throw new ArgumentOutOfRangeException(nameof(sayAs));
            }

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            if (sayAs != SayAs.Text)
            {
                Element sayAsElement = new(ElementType.SayAs, textToSpeak);
                _elements.Add(sayAsElement);

                sayAsElement._attributes = new Collection<AttributeItem>();
                string? sInterpretAs = null;
                string? sFormat = null;

                switch (sayAs)
                {
                    case SayAs.SpellOut:
                        sInterpretAs = "characters";
                        break;

                    case SayAs.NumberOrdinal:
                        sInterpretAs = "ordinal";
                        break;

                    case SayAs.NumberCardinal:
                        sInterpretAs = "cardinal";
                        break;

                    case SayAs.Date:
                        sInterpretAs = "date";
                        break;

                    case SayAs.DayMonthYear:
                        sInterpretAs = "date";
                        sFormat = "dmy";
                        break;

                    case SayAs.MonthDayYear:
                        sInterpretAs = "date";
                        sFormat = "mdy";
                        break;

                    case SayAs.YearMonthDay:
                        sInterpretAs = "date";
                        sFormat = "ymd";
                        break;

                    case SayAs.YearMonth:
                        sInterpretAs = "date";
                        sFormat = "ym";
                        break;

                    case SayAs.MonthYear:
                        sInterpretAs = "date";
                        sFormat = "my";
                        break;

                    case SayAs.MonthDay:
                        sInterpretAs = "date";
                        sFormat = "md";
                        break;

                    case SayAs.DayMonth:
                        sInterpretAs = "date";
                        sFormat = "dm";
                        break;

                    case SayAs.Year:
                        sInterpretAs = "date";
                        sFormat = "y";
                        break;

                    case SayAs.Month:
                        sInterpretAs = "date";
                        sFormat = "m";
                        break;

                    case SayAs.Day:
                        sInterpretAs = "date";
                        sFormat = "d";
                        break;

                    case SayAs.Time:
                        sInterpretAs = "time";
                        break;

                    case SayAs.Time24:
                        sInterpretAs = "time";
                        sFormat = "hms24";
                        break;

                    case SayAs.Time12:
                        sInterpretAs = "time";
                        sFormat = "hms12";
                        break;

                    case SayAs.Telephone:
                        sInterpretAs = "telephone";
                        break;
                }

                sayAsElement._attributes.Add(new AttributeItem("interpret-as", sInterpretAs));
                if (!string.IsNullOrEmpty(sFormat))
                {
                    sayAsElement._attributes.Add(new AttributeItem("format", sFormat));
                }
            }
            else
            {
                AppendText(textToSpeak);
            }
        }
        public void AppendTextWithHint(string textToSpeak, string sayAs)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);
            Helpers.ThrowIfEmptyOrNull(sayAs, nameof(sayAs));

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            Element sayAsElement = new(ElementType.SayAs, textToSpeak);
            _elements.Add(sayAsElement);

            sayAsElement._attributes = new Collection<AttributeItem>();
            sayAsElement._attributes.Add(new AttributeItem("interpret-as", sayAs));
        }
        public void AppendTextWithPronunciation(string textToSpeak, string pronunciation)
        {
            Helpers.ThrowIfEmptyOrNull(textToSpeak, nameof(textToSpeak));
            Helpers.ThrowIfEmptyOrNull(pronunciation, nameof(pronunciation));

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            // validate the pronunciation
            PhonemeConverter.ValidateUpsIds(pronunciation);

            Element phoneElement = new(ElementType.Phoneme, textToSpeak);
            _elements.Add(phoneElement);

            phoneElement._attributes = new Collection<AttributeItem>();
            phoneElement._attributes.Add(new AttributeItem("ph", pronunciation));
        }
        public void AppendTextWithAlias(string textToSpeak, string substitute)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);
            ArgumentNullException.ThrowIfNull(substitute);

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Text);

            Element subElement = new(ElementType.Sub, textToSpeak);
            _elements.Add(subElement);

            subElement._attributes = new Collection<AttributeItem>();
            subElement._attributes.Add(new AttributeItem("alias", substitute));
        }
        public void AppendBreak()
        {
            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Break);

            _elements.Add(new Element(ElementType.Break));
        }
        public void AppendBreak(PromptBreak strength)
        {
            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Break);

            Element breakElement = new(ElementType.Break);
            _elements.Add(breakElement);

            string? sBreak = null;

            switch (strength)
            {
                case PromptBreak.None:
                    sBreak = "none";
                    break;

                case PromptBreak.ExtraSmall:
                    sBreak = "x-weak";
                    break;

                case PromptBreak.Small:
                    sBreak = "weak";
                    break;

                case PromptBreak.Medium:
                    sBreak = "medium";
                    break;

                case PromptBreak.Large:
                    sBreak = "strong";
                    break;

                case PromptBreak.ExtraLarge:
                    sBreak = "x-strong";
                    break;

                default:
                    throw new ArgumentNullException(nameof(strength));
            }

            breakElement._attributes = new Collection<AttributeItem>();
            breakElement._attributes.Add(new AttributeItem("strength", sBreak));
        }
        public void AppendBreak(TimeSpan duration)
        {
            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Break);

            if (duration.Ticks < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(duration));
            }

            Element breakElement = new(ElementType.Break);
            _elements.Add(breakElement);

            breakElement._attributes = new Collection<AttributeItem>();
            breakElement._attributes.Add(new AttributeItem("time", duration.TotalMilliseconds + "ms"));
        }

        // <audio>
        public void AppendAudio(string path)
        {
            Helpers.ThrowIfEmptyOrNull(path, nameof(path));
            Uri uri;

            try
            {
                uri = new Uri(path, UriKind.RelativeOrAbsolute);
            }
            catch (UriFormatException e)
            {
                throw new ArgumentException(e.Message, path, e);
            }

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Audio);

            AppendAudio(uri);
        }
        public void AppendAudio(Uri audioFile)
        {
            ArgumentNullException.ThrowIfNull(audioFile);

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Audio);

            Element audioElement = new(ElementType.Audio);
            _elements.Add(audioElement);

            audioElement._attributes = new Collection<AttributeItem>();
            audioElement._attributes.Add(new AttributeItem("src", audioFile.ToString()));
        }
        public void AppendAudio(Uri audioFile, string alternateText)
        {
            ArgumentNullException.ThrowIfNull(audioFile);
            ArgumentNullException.ThrowIfNull(alternateText);

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Audio);

            Element audioElement = new(ElementType.Audio, alternateText);
            _elements.Add(audioElement);

            audioElement._attributes = new Collection<AttributeItem>();
            audioElement._attributes.Add(new AttributeItem("src", audioFile.ToString()));
        }

        // <mark>
        public void AppendBookmark(string bookmarkName)
        {
            Helpers.ThrowIfEmptyOrNull(bookmarkName, nameof(bookmarkName));

            // check for well formed document
            ValidateElement(_elementStack.Peek(), SsmlElement.Mark);

            Element bookmarkElement = new(ElementType.Bookmark);
            _elements.Add(bookmarkElement);

            bookmarkElement._attributes = new Collection<AttributeItem>();
            bookmarkElement._attributes.Add(new AttributeItem("name", bookmarkName));
        }
        public void AppendPromptBuilder(PromptBuilder promptBuilder)
        {
            ArgumentNullException.ThrowIfNull(promptBuilder);

            StringReader sr = new(promptBuilder.ToXml());
            XmlTextReader reader = new(sr);
            AppendSsml(reader);
            reader.Close();
            sr.Close();
        }
        public void AppendSsml(string path)
        {
            Helpers.ThrowIfEmptyOrNull(path, nameof(path));

            AppendSsml(new Uri(path, UriKind.Relative));
        }
        public void AppendSsml(Uri ssmlFile)
        {
            ArgumentNullException.ThrowIfNull(ssmlFile);

            string? localFile;
            Uri? redirectUri;
            using (Stream stream = s_resourceLoader.LoadFile(ssmlFile, out localFile, out redirectUri))
            {
                try
                {
                    AppendSsml(new XmlTextReader(stream));
                }
                finally
                {
                    s_resourceLoader.UnloadFile(localFile);
                }
            }
        }
        public void AppendSsml(XmlReader ssmlFile)
        {
            ArgumentNullException.ThrowIfNull(ssmlFile);

            AppendSsmlInternal(ssmlFile);
        }

        // Advanced: Extensibility model to write through to the underlying stream writer.
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void AppendSsmlMarkup(string ssmlMarkup)
        {
            Helpers.ThrowIfEmptyOrNull(ssmlMarkup, nameof(ssmlMarkup));

            _elements.Add(new Element(ElementType.SsmlMarkup, ssmlMarkup));
        }

        public string ToXml()
        {
            using (StringWriter sw = new(CultureInfo.InvariantCulture))
            {
                using (XmlTextWriter writer = new(sw))
                {
                    WriteXml(writer);

                    SsmlState state = _elementStack.Peek()._state;
                    if (state != SsmlState.Header)
                    {
                        string sMsg = SR.Get(SRID.PromptBuilderInvalideState);
                        switch (state)
                        {
                            case SsmlState.Ended:
                                sMsg += SR.Get(SRID.PromptBuilderStateEnded);
                                break;

                            case SsmlState.Sentence:
                                sMsg += SR.Get(SRID.PromptBuilderStateSentence);
                                break;

                            case SsmlState.Paragraph:
                                sMsg += SR.Get(SRID.PromptBuilderStateParagraph);
                                break;

                            case SsmlState.StyleEmphasis:
                            case SsmlState.StyleProsody:
                            case (SsmlState.StyleProsody | SsmlState.StyleEmphasis):
                                sMsg += SR.Get(SRID.PromptBuilderStateStyle);
                                break;

                            case SsmlState.Voice:
                                sMsg += SR.Get(SRID.PromptBuilderStateVoice);
                                break;

                            default:
                                System.Diagnostics.Debug.Assert(false);
                                throw new NotSupportedException();
                        }

                        throw new InvalidOperationException(sMsg);
                    }

                    return sw.ToString();
                }
            }
        }

        #endregion

        #region public Properties
        public bool IsEmpty
        {
            get
            {
                return _elements.Count == 0;
            }
        }
        public CultureInfo Culture
        {
            get
            {
                return _culture;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(value);

                _culture = value;
            }
        }

        #endregion

        #region Internal Enums

        internal enum SsmlState
        {
            Header = 1,
            Paragraph = 2,
            Sentence = 4,
            StyleEmphasis = 8,
            StyleProsody = 16,
            Voice = 32,
            Ended = 64
        }

        #endregion

        #region Protected Methods

        #endregion

        #region Private Methods
        private void WriteXml(XmlTextWriter writer)
        {
            writer.WriteStartElement("speak");

            // Add the required elements.
            writer.WriteAttributeString("version", "1.0");
            writer.WriteAttributeString("xmlns", _xmlnsDefault);
            writer.WriteAttributeString("xml", "lang", null, _culture.Name);

            bool noEndElement = false;

            foreach (Element element in _elements)
            {
                noEndElement = noEndElement || element._type == ElementType.StartSentence || element._type == ElementType.StartParagraph || element._type == ElementType.StartStyle || element._type == ElementType.StartVoice;
                switch (element._type)
                {
                    case ElementType.Text:
                        writer.WriteString(element._text);
                        break;

                    case ElementType.SsmlMarkup:
                        System.Diagnostics.Debug.Assert(element._text != null, "All SsmlMarkup elements should have a text value");
                        writer.WriteRaw(element._text);
                        break;

                    case ElementType.StartVoice:
                    case ElementType.StartParagraph:
                    case ElementType.StartSentence:
                    case ElementType.Audio:
                    case ElementType.Break:
                    case ElementType.Bookmark:
                    case ElementType.Emphasis:
                    case ElementType.Phoneme:
                    case ElementType.Prosody:
                    case ElementType.SayAs:
                    case ElementType.Sub:
                        writer.WriteStartElement(s_promptBuilderElementName[(int)element._type]);

                        // Write the attributes if any
                        if (element._attributes != null)
                        {
                            foreach (AttributeItem attribute in element._attributes)
                            {
                                if (attribute._namespace == null)
                                {
                                    writer.WriteAttributeString(attribute._key, attribute._value);
                                }
                                else
                                {
                                    writer.WriteAttributeString(attribute._namespace, attribute._key, null, attribute._value);
                                }
                            }
                        }

                        // Write the text if any
                        if (element._text != null)
                        {
                            writer.WriteString(element._text);
                        }

                        // Close the element unless it should wait
                        if (!noEndElement)
                        {
                            writer.WriteEndElement();
                        }
                        noEndElement = false;
                        break;

                    // Ignore just set the bool to not close the element
                    case ElementType.StartStyle:
                        break;

                    // Close the current element
                    case ElementType.EndStyle:
                    case ElementType.EndVoice:
                    case ElementType.EndParagraph:
                    case ElementType.EndSentence:
                        writer.WriteEndElement();
                        break;

                    default:
                        throw new NotSupportedException();
                }
            }

            writer.WriteEndElement();
        }

        /// <summary>
        /// Ensure the this element is properly placed in the SSML markup
        /// </summary>
        private static void ValidateElement(StackElement stackElement, SsmlElement currentElement)
        {
            if ((stackElement._possibleChildren & currentElement) == 0)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, SR.Get(SRID.PromptBuilderInvalidElement), currentElement.ToString(), stackElement._state.ToString()));
            }
        }
        private void AppendSsmlInternal(XmlReader ssmlFile)
        {
            // check for well formed document
            StackElement stackElement = _elementStack.Peek();
            ValidateElement(_elementStack.Peek(), SsmlElement.Voice);

            using (StringWriter sw = new(CultureInfo.InvariantCulture))
            {
                using (XmlTextWriter writer = new(sw))
                {
                    TextWriterEngine engine = new(writer, stackElement._culture);
                    SsmlParser.Parse(ssmlFile, engine, voice: null!); // TextWriterEngine ignores the voice
                }
                _elements.Add(new Element(ElementType.SsmlMarkup, sw.ToString()));
            }
        }

        #endregion

        #region Private Fields

        // Stack of elements for the SSML document
        private Stack<StackElement> _elementStack = new();

        // <xml:lang>
        private CultureInfo _culture;

        // list of all the elements for this prompt builder
        private List<Element> _elements = new();

        // Resource loader for the prompt builder
        private static readonly ResourceLoader s_resourceLoader = new();

        private const string _xmlnsDefault = @"http://www.w3.org/2001/10/synthesis";

        #endregion

        #region Private Type

        [Serializable]
        private struct StackElement
        {
            internal SsmlElement _possibleChildren;
            internal SsmlState _state;
            internal CultureInfo _culture;

            internal StackElement(SsmlElement possibleChildren, SsmlState state, CultureInfo culture)
            {
                _possibleChildren = possibleChildren;
                _state = state;
                _culture = culture;
            }
        }

        private enum ElementType
        {
            Prosody,
            Emphasis,
            SayAs,
            Phoneme,
            Sub,
            Break,
            Audio,
            Bookmark,
            StartVoice,
            StartParagraph,
            StartSentence,
            EndSentence,
            EndParagraph,
            StartStyle,
            EndStyle,
            EndVoice,
            Text,
            SsmlMarkup
        }

        private static readonly string[] s_promptBuilderElementName = new string[]
        {
            "prosody",
            "emphasis",
            "say-as",
            "phoneme",
            "sub",
            "break",
            "audio",
            "mark",
            "voice",
            "p",
            "s"
        };

        [Serializable]
        private struct AttributeItem
        {
            internal string _key;
            internal string? _value;
            internal string? _namespace;

            internal AttributeItem(string key, string? value)
            {
                _key = key;
                _value = value;
                _namespace = null;
            }

            internal AttributeItem(string ns, string key, string value)
                : this(key, value)
            {
                _namespace = ns;
            }
        }

        [Serializable]
        private sealed class Element
        {
            internal ElementType _type;
            internal string? _text;
            internal Collection<AttributeItem>? _attributes;

            internal Element(ElementType type)
            {
                _type = type;
            }

            internal Element(ElementType type, string text)
                : this(type)
            {
                _text = text;
            }
        }

        #endregion
    }
}