    /*

    Siesta 5.3.0
    Copyright(c) 2009-2019 Bryntum AB
    https://bryntum.com/contact
    https://bryntum.com/products/siesta/license

    */
    Role('Siesta.Util.Role.CanFormatStrings', {

        has     : {
            serializeFormatingPlaceholders      : true
        },

        methods : {

            formatString: function (string, data) {
                if (!data) return string

                var match
                var variables           = []
                var isRaw               = []
                var regexp              = /\{(\!)?((?:\w|-|_)+?)\}/g

                while (match = regexp.exec(string)) {
                    isRaw.push(match[ 1 ])
                    variables.push(match[ 2 ])
                }

                var result              = string

                Joose.A.each(variables, function (variable, index) {
                    var varIsRaw        = isRaw[ index ]

                    result              = result.replace(
                        new RegExp('\\{' + (varIsRaw ? '!' : '') + variable + '\\}', 'g'),
                        data.hasOwnProperty(variable) ?
                            varIsRaw || !this.serializeFormatingPlaceholders ? data[ variable ] + '' : Siesta.Util.Serializer.stringify(data[ variable ])
                        :
                            ''
                    )
                }, this)

                return result
            },

            // Extract normal chars, or special keys in brackets such as [TAB], [RIGHT] or [ENTER]
            extractKeysAndSpecialKeys : function (string) {
                var KeyCodes        = Siesta.Test.UserAgent.KeyCodes().keys
                var me              = this

                var res             = []
                // either: [[SPECIAL]] (meaning plain text), [SPECIAL] (meaning special char), or any character
                var tokens          = string.match(/(\[\[(?:\w|-){1,11}\]\])|(\[(?:\w|-){1,11}\])|([\s\S])/g) || [];

                Joose.A.each(tokens, function (token, index) {
                    if (token.length > 1) {
                        var isDouble        = /\[\[/.test(token)

                        var specialKey      = token.substring(isDouble ? 2 : 1, token.length - (isDouble ? 2 : 1))
                        var normalText      = KeyCodes[ specialKey.toUpperCase() ] === undefined

                        if (normalText || isDouble) {
                            res.push('[')
                            if (normalText && isDouble) res.push('[')
                            res.push.apply(res, me.extractKeysAndSpecialKeys(specialKey))
                            res.push(']')
                            if (normalText && isDouble) res.push(']')
                        } else {
                            res.push(token)
                        }
                    } else
                        res.push(token)
                })

                return res
            }
        }
    })