function normalisePropertyExpression(str, msg, toString) {
    // This must be kept in sync with validatePropertyExpression
    // in editor/js/ui/utils.js

    var length = str.length;
    if (length === 0) {
        throw createError("INVALID_EXPR","Invalid property expression: zero-length");
    }
    var parts = [];
    var start = 0;
    var inString = false;
    var inBox = false;
    var boxExpression = false;
    var quoteChar;
    var v;
    for (var i=0;i<length;i++) {
        var c = str[i];
        if (!inString) {
            if (c === "'" || c === '"') {
                if (i != start) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
                }
                inString = true;
                quoteChar = c;
                start = i+1;
            } else if (c === '.') {
                if (i===0) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected . at position 0");
                }
                if (start != i) {
                    v = str.substring(start,i);
                    if (/^\d+$/.test(v)) {
                        parts.push(parseInt(v));
                    } else {
                        parts.push(v);
                    }
                }
                if (i===length-1) {
                    throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
                }
                // Next char is first char of an identifier: a-z 0-9 $ _
                if (!/[a-z0-9\$\_]/i.test(str[i+1])) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
                }
                start = i+1;
            } else if (c === '[') {
                if (i === 0) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
                }
                if (start != i) {
                    parts.push(str.substring(start,i));
                }
                if (i===length-1) {
                    throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
                }
                // Start of a new expression. If it starts with msg it is a nested expression
                // Need to scan ahead to find the closing bracket
                if (/^msg[.\[]/.test(str.substring(i+1))) {
                    var depth = 1;
                    var inLocalString = false;
                    var localStringQuote;
                    for (var j=i+1;j<length;j++) {
                        if (/["']/.test(str[j])) {
                            if (inLocalString) {
                                if (str[j] === localStringQuote) {
                                    inLocalString = false
                                }
                            } else {
                                inLocalString = true;
                                localStringQuote = str[j]
                            }
                        }
                        if (str[j] === '[') {
                            depth++;
                        } else if (str[j] === ']') {
                            depth--;
                        }
                        if (depth === 0) {
                            try {
                                if (msg) {
                                    var crossRefProp = getMessageProperty(msg, str.substring(i+1,j));
                                    if (crossRefProp === undefined) {
                                        throw createError("INVALID_EXPR","Invalid expression: undefined reference at position "+(i+1)+" : "+str.substring(i+1,j))
                                    }
                                    parts.push(crossRefProp)
                                } else {
                                    parts.push(normalisePropertyExpression(str.substring(i+1,j), msg));
                                }
                                inBox = false;
                                i = j;
                                start = j+1;
                                break;
                            } catch(err) {
                                throw createError("INVALID_EXPR","Invalid expression started at position "+(i+1))
                            }
                        }
                    }
                    if (depth > 0) {
                        throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i);
                    }
                    continue;
                } else if (!/["'\d]/.test(str[i+1])) {
                    // Next char is either a quote or a number
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
                }
                start = i+1;
                inBox = true;
            } else if (c === ']') {
                if (!inBox) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
                }
                if (start != i) {
                    v = str.substring(start,i);
                    if (/^\d+$/.test(v)) {
                        parts.push(parseInt(v));
                    } else {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
                    }
                }
                start = i+1;
                inBox = false;
            } else if (c === ' ') {
                throw createError("INVALID_EXPR","Invalid property expression: unexpected ' ' at position "+i);
            }
        } else {
            if (c === quoteChar) {
                if (i-start === 0) {
                    throw createError("INVALID_EXPR","Invalid property expression: zero-length string at position "+start);
                }
                parts.push(str.substring(start,i));
                // If inBox, next char must be a ]. Otherwise it may be [ or .
                if (inBox && !/\]/.test(str[i+1])) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
                } else if (!inBox && i+1!==length && !/[\[\.]/.test(str[i+1])) {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1));
                }
                start = i+1;
                inString = false;
            }
        }

    }
    if (inBox || inString) {
        throw new createError("INVALID_EXPR","Invalid property expression: unterminated expression");
    }
    if (start < length) {
        parts.push(str.substring(start));
    }

    if (toString) {
        var result = parts.shift();
        while(parts.length > 0) {
            var p = parts.shift();
            if (typeof p === 'string') {
                if (/"/.test(p)) {
                    p = "'"+p+"'";
                } else {
                    p = '"'+p+'"';
                }
            }
            result = result+"["+p+"]";
        }
        return result;
    }

    return parts;
}
Parses a property expression, such as `msg.foo.bar[3]` to validate it
and convert it to a canonical version expressed as an Array of property
names.

For example, `a["b"].c` returns `['a','b','c']`

If `msg` is provided, any internal cross-references will be evaluated against that
object. Otherwise, it will return a nested set of properties

For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]`
But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]`

@param  {String} str - the property expression
@return {Array} the normalised expression
@memberof @node-red/util_util