File: Recognition\GrammarBuilder.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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Speech.Internal;
using System.Speech.Internal.GrammarBuilding;
using System.Speech.Internal.SrgsCompiler;
using System.Speech.Internal.SrgsParser;
using System.Text;

namespace System.Speech.Recognition
{
    [DebuggerDisplay("{DebugSummary}")]
    public class GrammarBuilder
    {
        #region Constructors

        public GrammarBuilder()
        {
            _grammarBuilder = new InternalGrammarBuilder();
        }

        public GrammarBuilder(string phrase)
            : this()
        {
            Append(phrase);
        }

        public GrammarBuilder(string phrase, SubsetMatchingMode subsetMatchingCriteria)
            : this()
        {
            Append(phrase, subsetMatchingCriteria);
        }

        public GrammarBuilder(string phrase, int minRepeat, int maxRepeat)
            : this()
        {
            Append(phrase, minRepeat, maxRepeat);
        }

        public GrammarBuilder(GrammarBuilder builder, int minRepeat, int maxRepeat)
            : this()
        {
            Append(builder, minRepeat, maxRepeat);
        }

        public GrammarBuilder(Choices alternateChoices)
            : this()
        {
            Append(alternateChoices);
        }

        public GrammarBuilder(SemanticResultKey key)
            : this()
        {
            Append(key);
        }

        public GrammarBuilder(SemanticResultValue value)
            : this()
        {
            Append(value);
        }

        #endregion Constructors

        #region Public Methods

        // Append connecting words

        public void Append(string phrase)
        {
            Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase));

            AddItem(new GrammarBuilderPhrase(phrase));
        }

        public void Append(string phrase, SubsetMatchingMode subsetMatchingCriteria)
        {
            Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase));
            GrammarBuilder.ValidateSubsetMatchingCriteriaArgument(subsetMatchingCriteria, nameof(subsetMatchingCriteria));

            AddItem(new GrammarBuilderPhrase(phrase, subsetMatchingCriteria));
        }

        public void Append(string phrase, int minRepeat, int maxRepeat)
        {
            Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase));
            GrammarBuilder.ValidateRepeatArguments(minRepeat, maxRepeat, "minRepeat", "maxRepeat");

            // Wrap the phrase in an item if min and max repeat are set
            GrammarBuilderPhrase elementPhrase = new(phrase);
            if (minRepeat != 1 || maxRepeat != 1)
            {
                AddItem(new ItemElement(elementPhrase, minRepeat, maxRepeat));
            }
            else
            {
                AddItem(elementPhrase);
            }
        }

        // Append list of rulerefs

        public void Append(GrammarBuilder builder)
        {
            ArgumentNullException.ThrowIfNull(builder);

            // Should never happens has it is a RO value
            ArgumentNullException.ThrowIfNull(builder.InternalBuilder, "builder.InternalBuilder");
            ArgumentNullException.ThrowIfNull(builder.InternalBuilder.Items, "builder.InternalBuilder.Items");

            // Clone the items if we are playing with the local list.
            foreach (GrammarBuilderBase item in builder.InternalBuilder.Items)
            {
                if (item == null)
                {
                    // This should never happen!
                    throw new ArgumentException(SR.Get(SRID.ArrayOfNullIllegal), nameof(builder));
                }
            }

            // Clone the items if we are playing with the local list.
            List<GrammarBuilderBase> items = builder == this ? builder.Clone().InternalBuilder.Items : builder.InternalBuilder.Items;

            foreach (GrammarBuilderBase item in items)
            {
                AddItem(item);
            }
        }

        // Append one-of

        public void Append(Choices alternateChoices)
        {
            ArgumentNullException.ThrowIfNull(alternateChoices);

            AddItem(alternateChoices.OneOf);
        }

        public void Append(SemanticResultKey key)
        {
            ArgumentNullException.ThrowIfNull(key, "builder");

            AddItem(key.SemanticKeyElement);
        }

        public void Append(SemanticResultValue value)
        {
            ArgumentNullException.ThrowIfNull(value, "builder");

            AddItem(value.Tag);
        }

        public void Append(GrammarBuilder builder, int minRepeat, int maxRepeat)
        {
            ArgumentNullException.ThrowIfNull(builder);
            GrammarBuilder.ValidateRepeatArguments(minRepeat, maxRepeat, "minRepeat", "maxRepeat");

            // Should never happens has it is a RO value
            ArgumentNullException.ThrowIfNull(builder.InternalBuilder, "builder.InternalBuilder");

            // Wrap the phrase in an item if min and max repeat are set
            if (minRepeat != 1 || maxRepeat != 1)
            {
                AddItem(new ItemElement(builder.InternalBuilder.Items, minRepeat, maxRepeat));
            }
            else
            {
                Append(builder);
            }
        }

        // Append dictation element

        public void AppendDictation()
        {
            AddItem(new GrammarBuilderDictation());
        }

        public void AppendDictation(string category)
        {
            Helpers.ThrowIfEmptyOrNull(category, nameof(category));

            AddItem(new GrammarBuilderDictation(category));
        }

        // Append wildcard element

        public void AppendWildcard()
        {
            AddItem(new GrammarBuilderWildcard());
        }

        /// <summary>
        /// Append external rule ref
        /// </summary>
        public void AppendRuleReference(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);
            }

            AddItem(new GrammarBuilderRuleRef(uri, null));
        }

        /// <summary>
        /// Append external rule ref
        /// </summary>
        public void AppendRuleReference(string path, string rule)
        {
            Helpers.ThrowIfEmptyOrNull(path, nameof(path));
            Helpers.ThrowIfEmptyOrNull(rule, nameof(rule));
            Uri uri;

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

            AddItem(new GrammarBuilderRuleRef(uri, rule));
        }
        public string DebugShowPhrases
        {
            get
            {
                return DebugSummary;
            }
        }

        #endregion Constructors

        #region Public Properties
        public CultureInfo Culture
        {
            get
            {
                return _culture;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(value);

                _culture = value;
            }
        }

        #endregion

        #region Operator Overloads

        public static GrammarBuilder operator +(string phrase, GrammarBuilder builder)
        {
            return Add(phrase, builder);
        }

        public static GrammarBuilder Add(string phrase, GrammarBuilder builder)
        {
            ArgumentNullException.ThrowIfNull(builder);

            GrammarBuilder grammar = new(phrase);
            grammar.Append(builder);
            return grammar;
        }

        public static GrammarBuilder operator +(GrammarBuilder builder, string phrase)
        {
            return Add(builder, phrase);
        }

        public static GrammarBuilder Add(GrammarBuilder builder, string phrase)
        {
            ArgumentNullException.ThrowIfNull(builder);

            GrammarBuilder grammar = builder.Clone();
            grammar.Append(phrase);
            return grammar;
        }

        public static GrammarBuilder operator +(Choices choices, GrammarBuilder builder)
        {
            return Add(choices, builder);
        }

        public static GrammarBuilder Add(Choices choices, GrammarBuilder builder)
        {
            ArgumentNullException.ThrowIfNull(choices);
            ArgumentNullException.ThrowIfNull(builder);

            GrammarBuilder grammar = new(choices);
            grammar.Append(builder);
            return grammar;
        }

        public static GrammarBuilder operator +(GrammarBuilder builder, Choices choices)
        {
            return Add(builder, choices);
        }

        public static GrammarBuilder Add(GrammarBuilder builder, Choices choices)
        {
            ArgumentNullException.ThrowIfNull(builder);
            ArgumentNullException.ThrowIfNull(choices);

            GrammarBuilder grammar = builder.Clone();
            grammar.Append(choices);
            return grammar;
        }

        public static GrammarBuilder operator +(GrammarBuilder builder1, GrammarBuilder builder2)
        {
            return Add(builder1, builder2);
        }

        public static GrammarBuilder Add(GrammarBuilder builder1, GrammarBuilder builder2)
        {
            ArgumentNullException.ThrowIfNull(builder1);
            ArgumentNullException.ThrowIfNull(builder2);

            GrammarBuilder grammar = builder1.Clone();
            grammar.Append(builder2);
            return grammar;
        }
        public static implicit operator GrammarBuilder(string phrase) { return new GrammarBuilder(phrase); }
        public static implicit operator GrammarBuilder(Choices choices) { return new GrammarBuilder(choices); }
        public static implicit operator GrammarBuilder(SemanticResultKey semanticKey) { return new GrammarBuilder(semanticKey); }
        public static implicit operator GrammarBuilder(SemanticResultValue semanticValue) { return new GrammarBuilder(semanticValue); }

        #endregion

        #region Internal Methods

        internal static void ValidateRepeatArguments(int minRepeat, int maxRepeat, string minParamName, string maxParamName)
        {
            if (minRepeat < 0)
            {
                throw new ArgumentOutOfRangeException(minParamName, SR.Get(SRID.InvalidMinRepeat, minRepeat));
            }
            if (minRepeat > maxRepeat)
            {
                throw new ArgumentException(SR.Get(SRID.MinGreaterThanMax), maxParamName);
            }
        }

        internal static void ValidateSubsetMatchingCriteriaArgument(SubsetMatchingMode subsetMatchingCriteria, string paramName)
        {
            switch (subsetMatchingCriteria)
            {
                case SubsetMatchingMode.OrderedSubset:
                case SubsetMatchingMode.OrderedSubsetContentRequired:
                case SubsetMatchingMode.Subsequence:
                case SubsetMatchingMode.SubsequenceContentRequired:
                    break;
                default:
                    throw new ArgumentException(SR.Get(SRID.EnumInvalid, paramName), paramName);
            }
        }

        internal void CreateGrammar(IElementFactory elementFactory)
        {
            // Create a new Identifier Collection which will provide unique ids
            // for each rule
            IdentifierCollection ruleIds = new();
            elementFactory.Grammar.Culture = Culture;

            _grammarBuilder.CreateElement(elementFactory, null, null, ruleIds);
        }

        internal void Compile(Stream stream)
        {
            Backend backend = new();
            CustomGrammar cg = new();
            SrgsElementCompilerFactory elementFactory = new(backend, cg);
            CreateGrammar(elementFactory);

            // Optimize in-memory graph representation of the grammar.
            backend.Optimize();

            using (StreamMarshaler streamHelper = new(stream))
            {
                backend.Commit(streamHelper);
            }

            stream.Position = 0;
        }

        internal GrammarBuilder Clone()
        {
            GrammarBuilder builder = new();
            builder._grammarBuilder = (InternalGrammarBuilder)_grammarBuilder.Clone();

            return builder;
        }

        #endregion

        #region Internal Properties

        internal virtual string DebugSummary
        {
            get
            {
                StringBuilder sb = new();

                foreach (GrammarBuilderBase item in InternalBuilder.Items)
                {
                    if (sb.Length > 0)
                    {
                        sb.Append(' ');
                    }
                    sb.Append(item.DebugSummary);
                }
                return sb.ToString();
            }
        }

        internal BuilderElements InternalBuilder
        {
            get
            {
                return _grammarBuilder;
            }
        }

        #endregion

        #region Private Methods

        private void AddItem(GrammarBuilderBase item)
        {
            InternalBuilder.Items.Add(item.Clone());
        }

        #endregion

        #region Private Fields

        private InternalGrammarBuilder _grammarBuilder;

        private CultureInfo _culture = CultureInfo.CurrentUICulture;

        #endregion

        #region Private Type

        private sealed class InternalGrammarBuilder : BuilderElements
        {
            #region Internal Methods

            internal override GrammarBuilderBase Clone()
            {
                InternalGrammarBuilder newGrammarbuilder = new();
                foreach (GrammarBuilderBase i in Items)
                {
                    newGrammarbuilder.Items.Add(i.Clone());
                }
                return newGrammarbuilder;
            }

            internal override IElement? CreateElement(IElementFactory elementFactory, IElement? parent, IRule? rule, IdentifierCollection ruleIds)
            {
                Collection<RuleElement> newRules = new();
                CalcCount(null);
                Optimize(newRules);

                foreach (GrammarBuilderBase baseRule in newRules)
                {
                    Items.Add(baseRule);
                }

                // The id of the root rule
                string rootId = ruleIds.CreateNewIdentifier("root");

                // Set the grammar's root rule
                elementFactory.Grammar.Root = rootId;
                elementFactory.Grammar.TagFormat = System.Speech.Recognition.SrgsGrammar.SrgsTagFormat.KeyValuePairs;

                // Create the root rule
                IRule root = elementFactory.Grammar.CreateRule(rootId, RulePublic.False, RuleDynamic.NotSet, false);

                // Create all the rules
                foreach (GrammarBuilderBase item in Items)
                {
                    if (item is RuleElement)
                    {
                        item.CreateElement(elementFactory, root, root, ruleIds);
                    }
                }
                // Create an item which represents the grammar
                foreach (GrammarBuilderBase item in Items)
                {
                    if (!(item is RuleElement))
                    {
                        IElement? element = item.CreateElement(elementFactory, root, root, ruleIds);

                        if (element != null)
                        {
                            element.PostParse(root);
                            elementFactory.AddElement(root, element);
                        }
                    }
                }
                // Post parse the root rule
                root.PostParse(elementFactory.Grammar);

                elementFactory.Grammar.PostParse(null!);
                return null;
            }

            #endregion
        }

        #endregion
    }
}