mirror of
https://gitlab.winehq.org/wine/wine.git
synced 2025-08-29 18:53:55 +02:00
800 lines
21 KiB
C
800 lines
21 KiB
C
/*
|
|
* Speech API (SAPI) XML parser implementation.
|
|
*
|
|
* Copyright 2025 Shaun Ren for CodeWeavers
|
|
*
|
|
* Based on ntdll/actctx.c
|
|
* Copyright 2004 Jon Griffiths
|
|
* Copyright 2007 Eric Pouech
|
|
* Copyright 2007 Jacek Caban for CodeWeavers
|
|
* Copyright 2007 Alexandre Julliard
|
|
* Copyright 2013 Nikolay Sivov
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <assert.h>
|
|
|
|
#define COBJMACROS
|
|
|
|
#include "objbase.h"
|
|
|
|
#include "sapiddk.h"
|
|
#include "sperror.h"
|
|
|
|
#include "wine/debug.h"
|
|
|
|
#include "sapi_private.h"
|
|
|
|
WINE_DEFAULT_DEBUG_CHANNEL(sapi);
|
|
|
|
|
|
#define MAX_NAMESPACES 64
|
|
|
|
typedef struct
|
|
{
|
|
const WCHAR *ptr;
|
|
unsigned int len;
|
|
} xmlstr_t;
|
|
|
|
struct xml_elem
|
|
{
|
|
xmlstr_t name;
|
|
xmlstr_t ns;
|
|
int ns_pos;
|
|
};
|
|
|
|
struct xml_attr
|
|
{
|
|
xmlstr_t name;
|
|
xmlstr_t value;
|
|
};
|
|
|
|
struct xml_parser
|
|
{
|
|
const WCHAR *base;
|
|
const WCHAR *ptr;
|
|
const WCHAR *end;
|
|
struct xml_attr namespaces[MAX_NAMESPACES];
|
|
int ns_pos;
|
|
BOOL error;
|
|
|
|
SPVTEXTFRAG *tail_frag;
|
|
};
|
|
|
|
static const xmlstr_t empty_xmlstr;
|
|
|
|
static const WCHAR ssml_ns[] = L"http://www.w3.org/2001/10/synthesis";
|
|
|
|
static inline const char *debugstr_xmlstr(const xmlstr_t *str)
|
|
{
|
|
return debugstr_wn(str->ptr, str->len);
|
|
}
|
|
|
|
static WCHAR *xmlstrcpyW(WCHAR *dst, const xmlstr_t* src)
|
|
{
|
|
memcpy(dst, src->ptr, src->len * sizeof(WCHAR));
|
|
dst[src->len] = 0;
|
|
return dst;
|
|
}
|
|
|
|
static WCHAR *xmlstrdupW(const xmlstr_t* str)
|
|
{
|
|
WCHAR *strW;
|
|
|
|
if (!(strW = malloc((str->len + 1) * sizeof(WCHAR))))
|
|
return NULL;
|
|
return xmlstrcpyW(strW, str);
|
|
}
|
|
|
|
static inline BOOL xmlstr_eq(const xmlstr_t* xmlstr, const WCHAR *str)
|
|
{
|
|
return !wcsncmp(xmlstr->ptr, str, xmlstr->len) && !str[xmlstr->len];
|
|
}
|
|
|
|
static inline BOOL xml_attr_eq(const struct xml_attr *attr, const WCHAR *name)
|
|
{
|
|
return xmlstr_eq(&attr->name, name);
|
|
}
|
|
|
|
static inline BOOL xml_elem_eq(const struct xml_elem *elem, const WCHAR *ns, const WCHAR *name)
|
|
{
|
|
if (!xmlstr_eq(&elem->name, name)) return FALSE;
|
|
return xmlstr_eq(&elem->ns, ns);
|
|
}
|
|
|
|
static BOOL xml_name_eq(const struct xml_elem *elem1, const struct xml_elem *elem2)
|
|
{
|
|
return (elem1->name.len == elem2->name.len &&
|
|
elem1->ns.len == elem2->ns.len &&
|
|
!wcsncmp(elem1->name.ptr, elem2->name.ptr, elem1->name.len) &&
|
|
!wcsncmp(elem1->ns.ptr, elem2->ns.ptr, elem1->ns.len));
|
|
}
|
|
|
|
static BOOL set_error(struct xml_parser *parser)
|
|
{
|
|
parser->error = TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL is_xmlns_attr(const struct xml_attr *attr)
|
|
{
|
|
const int len = wcslen(L"xmlns");
|
|
if (attr->name.len < len) return FALSE;
|
|
if (wcsncmp(attr->name.ptr, L"xmlns", len)) return FALSE;
|
|
return (attr->name.len == len || attr->name.ptr[len] == ':');
|
|
}
|
|
|
|
static void push_xmlns(struct xml_parser *parser, const struct xml_attr *attr)
|
|
{
|
|
const int len = wcslen(L"xmlns");
|
|
struct xml_attr *ns;
|
|
|
|
if (parser->ns_pos == MAX_NAMESPACES - 1)
|
|
{
|
|
FIXME("Too many namespaces.\n");
|
|
set_error(parser);
|
|
return;
|
|
}
|
|
ns = &parser->namespaces[parser->ns_pos++];
|
|
ns->value = attr->value;
|
|
if (attr->name.len > len)
|
|
{
|
|
ns->name.ptr = attr->name.ptr + len + 1;
|
|
ns->name.len = attr->name.len - len - 1;
|
|
}
|
|
else ns->name = empty_xmlstr;
|
|
}
|
|
|
|
static xmlstr_t find_xmlns(struct xml_parser *parser, const xmlstr_t *name)
|
|
{
|
|
int i;
|
|
|
|
for (i = parser->ns_pos - 1; i >= 0; i--)
|
|
{
|
|
if (parser->namespaces[i].name.len == name->len &&
|
|
!wcsncmp(parser->namespaces[i].name.ptr, name->ptr, name->len))
|
|
return parser->namespaces[i].value;
|
|
}
|
|
if (parser->ns_pos) WARN("Namespace %s not found.\n", debugstr_xmlstr(name));
|
|
return empty_xmlstr;
|
|
}
|
|
|
|
static void skip_xml_spaces(struct xml_parser *parser)
|
|
{
|
|
while (parser->ptr < parser->end && isxmlspace(*parser->ptr))
|
|
parser->ptr++;
|
|
}
|
|
|
|
static BOOL next_xml_attr(struct xml_parser *parser, struct xml_attr *attr, BOOL *end)
|
|
{
|
|
const WCHAR *ptr;
|
|
WCHAR quote;
|
|
|
|
*end = FALSE;
|
|
|
|
if (parser->error) return FALSE;
|
|
|
|
skip_xml_spaces(parser);
|
|
|
|
if (parser->ptr == parser->end)
|
|
return set_error(parser);
|
|
|
|
if (*parser->ptr == '/')
|
|
{
|
|
parser->ptr++;
|
|
if (parser->ptr == parser->end || *parser->ptr != '>')
|
|
return set_error(parser);
|
|
|
|
parser->ptr++;
|
|
*end = TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
if (*parser->ptr == '>')
|
|
{
|
|
parser->ptr++;
|
|
return FALSE;
|
|
}
|
|
|
|
ptr = parser->ptr;
|
|
while (ptr < parser->end && *ptr != '=' && *ptr != '>' && !isxmlspace(*ptr)) ptr++;
|
|
|
|
if (ptr == parser->end)
|
|
return set_error(parser);
|
|
|
|
attr->name.ptr = parser->ptr;
|
|
attr->name.len = ptr-parser->ptr;
|
|
parser->ptr = ptr;
|
|
|
|
/* skip spaces before '=' */
|
|
while (ptr < parser->end && *ptr != '=' && isxmlspace(*ptr)) ptr++;
|
|
if (ptr == parser->end || *ptr != '=')
|
|
return set_error(parser);
|
|
|
|
/* skip '=' itself */
|
|
ptr++;
|
|
if (ptr == parser->end)
|
|
return set_error(parser);
|
|
|
|
/* skip spaces after '=' */
|
|
while (ptr < parser->end && *ptr != '"' && *ptr != '\'' && isxmlspace(*ptr)) ptr++;
|
|
|
|
if (ptr == parser->end || (*ptr != '"' && *ptr != '\'')) return set_error(parser);
|
|
|
|
quote = *ptr++;
|
|
attr->value.ptr = ptr;
|
|
if (ptr == parser->end)
|
|
return set_error(parser);
|
|
|
|
while (ptr < parser->end && *ptr != quote) ptr++;
|
|
if (ptr == parser->end)
|
|
{
|
|
parser->ptr = parser->end;
|
|
return set_error(parser);
|
|
}
|
|
|
|
attr->value.len = ptr - attr->value.ptr;
|
|
parser->ptr = ptr + 1;
|
|
if (parser->ptr != parser->end) return TRUE;
|
|
|
|
return set_error(parser);
|
|
}
|
|
|
|
static void read_xml_elem(struct xml_parser *parser, struct xml_elem *elem)
|
|
{
|
|
const WCHAR *ptr = parser->ptr;
|
|
|
|
elem->ns = empty_xmlstr;
|
|
elem->name.ptr = ptr;
|
|
while (ptr < parser->end && !isxmlspace(*ptr) && *ptr != '>' && *ptr != '/')
|
|
{
|
|
if (*ptr == ':')
|
|
{
|
|
elem->ns.ptr = elem->name.ptr;
|
|
elem->ns.len = ptr - elem->ns.ptr;
|
|
elem->name.ptr = ptr + 1;
|
|
}
|
|
ptr++;
|
|
}
|
|
elem->name.len = ptr - elem->name.ptr;
|
|
parser->ptr = ptr;
|
|
}
|
|
|
|
static inline BOOL is_special_xml_markup(const struct xml_elem *elem)
|
|
{
|
|
return *elem->name.ptr == '!' || *elem->name.ptr == '?';
|
|
}
|
|
|
|
static BOOL skip_special_xml_markup(struct xml_parser *parser, struct xml_elem *elem)
|
|
{
|
|
const WCHAR *ptr;
|
|
|
|
if (parser->error) return FALSE;
|
|
|
|
if (elem->name.len > 1 && elem->name.ptr[0] == '!' && elem->name.ptr[1] == '[')
|
|
{
|
|
/* <![...]]> */
|
|
for (ptr = parser->ptr; ptr < parser->end - 3; ptr++)
|
|
{
|
|
if (ptr[0] == ']' && ptr[1] == ']' && ptr[2] == '>')
|
|
{
|
|
parser->ptr = ptr + 3;
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
else if (xmlstr_eq(&elem->name, L"!--"))
|
|
{
|
|
/* <!--...--> */
|
|
for (ptr = parser->ptr; ptr < parser->end - 2; ptr++)
|
|
{
|
|
if (ptr[0] == '-' && ptr[1] == '-' && ptr[2] == '>')
|
|
{
|
|
parser->ptr = ptr + 3;
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
else if (*elem->name.ptr == '!')
|
|
{
|
|
/* <!...> */
|
|
for (ptr = parser->ptr; ptr < parser->end; ptr++)
|
|
{
|
|
if (*ptr == '>')
|
|
{
|
|
parser->ptr = ptr + 1;
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
else if (*elem->name.ptr == '?')
|
|
{
|
|
/* <?...?> */
|
|
for (ptr = parser->ptr; ptr < parser->end - 1; ptr++)
|
|
{
|
|
if (ptr[0] == '?' && ptr[1] == '>')
|
|
{
|
|
parser->ptr = ptr + 2;
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL next_xml_elem(struct xml_parser *parser, struct xml_elem *elem, const struct xml_elem *parent)
|
|
{
|
|
const WCHAR *ptr;
|
|
struct xml_attr attr;
|
|
BOOL end = FALSE;
|
|
|
|
parser->ns_pos = parent->ns_pos; /* restore namespace stack to parent state */
|
|
|
|
if (parser->error) return FALSE;
|
|
|
|
skip_xml_spaces(parser);
|
|
|
|
if (parser->ptr == parser->end || *parser->ptr != '<')
|
|
return set_error(parser);
|
|
parser->ptr++;
|
|
|
|
/* check for element terminating the parent element */
|
|
if (parser->ptr < parser->end && *parser->ptr == '/')
|
|
{
|
|
parser->ptr++;
|
|
read_xml_elem(parser, elem);
|
|
elem->ns = find_xmlns(parser, &elem->ns);
|
|
if (!xml_name_eq(elem, parent))
|
|
{
|
|
ERR("Wrong closing element %s for %s.\n",
|
|
debugstr_xmlstr(&elem->name), debugstr_xmlstr(&parent->name));
|
|
return set_error(parser);
|
|
}
|
|
skip_xml_spaces(parser);
|
|
if (parser->ptr == parser->end || *parser->ptr++ != '>')
|
|
return set_error(parser);
|
|
return FALSE;
|
|
}
|
|
|
|
read_xml_elem(parser, elem);
|
|
if (!elem->name.len)
|
|
return set_error(parser);
|
|
|
|
if (!is_special_xml_markup(elem))
|
|
{
|
|
/* parse namespace attributes */
|
|
ptr = parser->ptr;
|
|
while (next_xml_attr(parser, &attr, &end))
|
|
{
|
|
if (is_xmlns_attr(&attr)) push_xmlns(parser, &attr);
|
|
}
|
|
parser->ptr = ptr;
|
|
elem->ns = find_xmlns(parser, &elem->ns);
|
|
elem->ns_pos = parser->ns_pos;
|
|
}
|
|
|
|
if (parser->ptr != parser->end) return TRUE;
|
|
else return set_error(parser);
|
|
}
|
|
|
|
static BOOL next_text_or_xml_elem(struct xml_parser *parser, BOOL *is_text, struct xml_elem *elem,
|
|
const struct xml_elem *parent)
|
|
{
|
|
if (parser->error) return FALSE;
|
|
|
|
while (parser->ptr < parser->end)
|
|
{
|
|
if (*parser->ptr == '<')
|
|
{
|
|
*is_text = FALSE;
|
|
|
|
if (!next_xml_elem(parser, elem, parent))
|
|
return FALSE;
|
|
|
|
if (!is_special_xml_markup(elem))
|
|
return TRUE;
|
|
else if (!skip_special_xml_markup(parser, elem))
|
|
return set_error(parser);
|
|
}
|
|
else
|
|
{
|
|
*is_text = TRUE;
|
|
return TRUE;
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static HRESULT add_sapi_text_fragment(struct xml_parser *parser, const SPVSTATE *state)
|
|
{
|
|
const WCHAR *text, *ptr;
|
|
SPVTEXTFRAG *frag;
|
|
size_t len;
|
|
WCHAR *buf;
|
|
|
|
if (parser->error)
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
|
|
text = parser->ptr;
|
|
|
|
for (ptr = parser->ptr; ptr < parser->end; ptr++) if (*ptr == '<') break;
|
|
len = ptr - parser->ptr;
|
|
parser->ptr = ptr;
|
|
|
|
if (!len)
|
|
return S_OK;
|
|
|
|
if (!(frag = malloc(sizeof(*frag) + (len + 1) * sizeof(WCHAR))))
|
|
return E_OUTOFMEMORY;
|
|
|
|
buf = (WCHAR *)(frag + 1);
|
|
memcpy(buf, text, len * sizeof(WCHAR));
|
|
buf[len] = 0;
|
|
|
|
frag->pNext = NULL;
|
|
frag->State = *state;
|
|
frag->pTextStart = buf;
|
|
frag->ulTextLen = len;
|
|
frag->ulTextSrcOffset = text - parser->base;
|
|
|
|
parser->tail_frag->pNext = frag;
|
|
parser->tail_frag = frag;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
struct string_value
|
|
{
|
|
const WCHAR *str;
|
|
LONG value;
|
|
};
|
|
|
|
static BOOL lookup_string_value(const struct string_value *table, size_t count, const xmlstr_t *str, LONG *res)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
if (xmlstr_eq(str, table[i].str))
|
|
{
|
|
*res = table[i].value;
|
|
return TRUE;
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static HRESULT parse_double_value(const xmlstr_t *value, double *res, size_t *read_len)
|
|
{
|
|
WCHAR *buf, *end;
|
|
|
|
if (!value->len)
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
|
|
if (!(buf = xmlstrdupW(value)))
|
|
return E_OUTOFMEMORY;
|
|
|
|
*res = wcstod(buf, &end);
|
|
*read_len = end - buf;
|
|
|
|
free(buf);
|
|
return S_OK;
|
|
}
|
|
|
|
static inline long lclamp(long value, long value_min, long value_max)
|
|
{
|
|
if (value < value_min) return value_min;
|
|
if (value > value_max) return value_max;
|
|
return value;
|
|
}
|
|
|
|
static HRESULT parse_ssml_elems(struct xml_parser *parser, const SPVSTATE *state, const struct xml_elem *parent);
|
|
|
|
static HRESULT parse_ssml_prosody_elem(struct xml_parser *parser, SPVSTATE state, const struct xml_elem *parent)
|
|
{
|
|
static const struct string_value rate_values[] =
|
|
{
|
|
{ L"x-slow", -9 },
|
|
{ L"slow", -4 },
|
|
{ L"medium", 0 },
|
|
{ L"fast", 4 },
|
|
{ L"x-fast", 9 },
|
|
};
|
|
|
|
static const struct string_value volume_values[] =
|
|
{
|
|
{ L"silent", 0 },
|
|
{ L"x-soft", 20 },
|
|
{ L"soft", 40 },
|
|
{ L"medium", 60 },
|
|
{ L"loud", 80 },
|
|
{ L"x-loud", 100 },
|
|
};
|
|
|
|
static const struct string_value pitch_values[] =
|
|
{
|
|
{ L"x-low", -9 },
|
|
{ L"low", -4 },
|
|
{ L"medium", 0 },
|
|
{ L"high", 4 },
|
|
{ L"x-high", 9 },
|
|
};
|
|
|
|
struct xml_attr attr;
|
|
BOOL end = FALSE;
|
|
size_t read_len;
|
|
HRESULT hr;
|
|
|
|
while (next_xml_attr(parser, &attr, &end))
|
|
{
|
|
if (xml_attr_eq(&attr, L"rate"))
|
|
{
|
|
double rate;
|
|
|
|
if (lookup_string_value(rate_values, ARRAY_SIZE(rate_values), &attr.value, &state.RateAdj))
|
|
continue;
|
|
|
|
if (FAILED(hr = parse_double_value(&attr.value, &rate, &read_len)))
|
|
return hr;
|
|
if (read_len < attr.value.len - 1 ||
|
|
(read_len == attr.value.len - 1 && attr.value.ptr[read_len] != '%'))
|
|
{
|
|
ERR("Invalid value %s for the rate attribute in <prosody>.\n", debugstr_xmlstr(&attr.value));
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
if (attr.value.ptr[attr.value.len - 1] == '%')
|
|
rate = 1 + rate / 100;
|
|
|
|
if (rate < 0)
|
|
state.RateAdj = 0;
|
|
else if (rate <= 0.01)
|
|
state.RateAdj = -10;
|
|
else
|
|
state.RateAdj = lround(log(rate) * (10 / log(3)));
|
|
}
|
|
else if (xml_attr_eq(&attr, L"volume"))
|
|
{
|
|
double volume;
|
|
|
|
if (lookup_string_value(volume_values, ARRAY_SIZE(volume_values), &attr.value, (LONG *)&state.Volume))
|
|
continue;
|
|
|
|
if (FAILED(hr = parse_double_value(&attr.value, &volume, &read_len)))
|
|
return hr;
|
|
if (read_len < attr.value.len - 1 ||
|
|
(read_len == attr.value.len - 1 && attr.value.ptr[read_len] != '%'))
|
|
{
|
|
ERR("Invalid value %s for the volume attribute in <prosody>.\n", debugstr_xmlstr(&attr.value));
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
if (attr.value.ptr[attr.value.len - 1] == '%')
|
|
volume = state.Volume * (1 + volume / 100);
|
|
else if (attr.value.ptr[0] == '+' || attr.value.ptr[0] == '-')
|
|
volume = state.Volume + volume;
|
|
|
|
state.Volume = lclamp(lround(volume), 0, 100);
|
|
}
|
|
else if (xml_attr_eq(&attr, L"pitch"))
|
|
{
|
|
double pitch;
|
|
|
|
if (lookup_string_value(pitch_values, ARRAY_SIZE(pitch_values), &attr.value, &state.PitchAdj.MiddleAdj))
|
|
continue;
|
|
|
|
if (FAILED(hr = parse_double_value(&attr.value, &pitch, &read_len)))
|
|
return hr;
|
|
|
|
if (attr.value.len > 2 && read_len == attr.value.len - 2 &&
|
|
attr.value.ptr[read_len] == 'H' && attr.value.ptr[read_len + 1] == 'z')
|
|
{
|
|
WARN("Ignoring Hz pitch value %s in <prosody>.\n", debugstr_xmlstr(&attr.value));
|
|
continue;
|
|
}
|
|
else if (read_len != attr.value.len - 1 || attr.value.ptr[read_len] != '%')
|
|
{
|
|
ERR("Invalid value %s for the pitch attribute in <prosody>.\n", debugstr_xmlstr(&attr.value));
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
if (pitch > -99)
|
|
state.PitchAdj.MiddleAdj += lround(log2(1 + pitch / 100) * 24);
|
|
else
|
|
state.PitchAdj.MiddleAdj -= 10;
|
|
}
|
|
else
|
|
{
|
|
FIXME("Unknown <prosody> attribute %s.\n", debugstr_xmlstr(&attr.name));
|
|
return E_NOTIMPL;
|
|
}
|
|
}
|
|
|
|
if (end) return S_OK;
|
|
return parse_ssml_elems(parser, &state, parent);
|
|
}
|
|
|
|
static HRESULT parse_ssml_elems(struct xml_parser *parser, const SPVSTATE *state, const struct xml_elem *parent)
|
|
{
|
|
struct xml_attr attr;
|
|
struct xml_elem elem;
|
|
BOOL is_text, end;
|
|
HRESULT hr = S_OK;
|
|
|
|
while (SUCCEEDED(hr) && next_text_or_xml_elem(parser, &is_text, &elem, parent))
|
|
{
|
|
if (is_text)
|
|
{
|
|
hr = add_sapi_text_fragment(parser, state);
|
|
}
|
|
else if (xml_elem_eq(&elem, ssml_ns, L"p") || xml_elem_eq(&elem, ssml_ns, L"s"))
|
|
{
|
|
while (next_xml_attr(parser, &attr, &end)) ;
|
|
if (end) continue;
|
|
hr = parse_ssml_elems(parser, state, &elem);
|
|
}
|
|
else if (xml_elem_eq(&elem, ssml_ns, L"prosody"))
|
|
{
|
|
hr = parse_ssml_prosody_elem(parser, *state, &elem);
|
|
}
|
|
else
|
|
{
|
|
FIXME("Unknown element %s.\n", debugstr_xmlstr(&elem.name));
|
|
hr = E_NOTIMPL;
|
|
}
|
|
}
|
|
|
|
if (SUCCEEDED(hr) && parser->error)
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
return hr;
|
|
}
|
|
|
|
static HRESULT parse_ssml_speak_elem(struct xml_parser *parser, const struct xml_elem *parent, SPVSTATE *state)
|
|
{
|
|
struct xml_attr attr;
|
|
BOOL end = FALSE;
|
|
BOOL has_version = FALSE;
|
|
LCID lcid = 0;
|
|
|
|
while (next_xml_attr(parser, &attr, &end))
|
|
{
|
|
if (xml_attr_eq(&attr, L"version"))
|
|
{
|
|
if (!xmlstr_eq(&attr.value, L"1.0"))
|
|
{
|
|
ERR("Invalid SSML version %s.\n", debugstr_xmlstr(&attr.value));
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
has_version = TRUE;
|
|
}
|
|
else if (xml_attr_eq(&attr, L"xml:lang"))
|
|
{
|
|
WCHAR *lang = xmlstrdupW(&attr.value);
|
|
|
|
if (!lang) return E_OUTOFMEMORY;
|
|
lcid = LocaleNameToLCID(lang, 0);
|
|
free(lang);
|
|
|
|
if (!lcid)
|
|
{
|
|
ERR("Invalid xml:lang %s.\n", debugstr_xmlstr(&attr.value));
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!has_version)
|
|
{
|
|
ERR("Missing version attribute.\n");
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
if (!lcid)
|
|
{
|
|
ERR("Missing xml:lang attribute.\n");
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
if (end) return S_OK;
|
|
|
|
state->eAction = SPVA_Speak;
|
|
state->LangID = LANGIDFROMLCID(lcid);
|
|
|
|
return parse_ssml_elems(parser, state, parent);
|
|
}
|
|
|
|
static HRESULT parse_ssml_contents(const WCHAR *contents, const WCHAR *end, SPVSTATE *state, SPVTEXTFRAG **frag_list)
|
|
{
|
|
struct xml_parser parser = {0};
|
|
struct xml_elem parent = {0};
|
|
SPVTEXTFRAG head_frag = {0};
|
|
struct xml_elem elem;
|
|
HRESULT hr;
|
|
|
|
parser.base = parser.ptr = contents;
|
|
parser.end = end;
|
|
parser.tail_frag = &head_frag;
|
|
|
|
/* Default SSML namespace. */
|
|
parser.namespaces[0].name = empty_xmlstr;
|
|
parser.namespaces[0].value.ptr = ssml_ns;
|
|
parser.namespaces[0].value.len = wcslen(ssml_ns);
|
|
parser.ns_pos = 1;
|
|
|
|
parent.ns = parser.namespaces[0].value;
|
|
parent.ns_pos = 1;
|
|
|
|
for (;;)
|
|
{
|
|
if (!next_xml_elem(&parser, &elem, &parent))
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
|
|
if (!is_special_xml_markup(&elem))
|
|
break;
|
|
if (!skip_special_xml_markup(&parser, &elem))
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
if (!xml_elem_eq(&elem, ssml_ns, L"speak"))
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
if (FAILED(hr = parse_ssml_speak_elem(&parser, &elem, state)))
|
|
return hr;
|
|
|
|
if (next_xml_elem(&parser, &elem, &parent))
|
|
{
|
|
ERR("Unexpected element %s after <speak>.\n", debugstr_xmlstr(&elem.name));
|
|
return SPERR_UNSUPPORTED_FORMAT;
|
|
}
|
|
|
|
*frag_list = head_frag.pNext;
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT parse_sapi_xml(const WCHAR *contents, DWORD parse_flag, BOOL persist, SPVSTATE *global_state,
|
|
SPVTEXTFRAG **frag_list)
|
|
{
|
|
SPVSTATE state = *global_state;
|
|
const WCHAR *end;
|
|
HRESULT hr;
|
|
|
|
TRACE("(%p, %#lx, %d, %p, %p).\n", contents, parse_flag, persist, global_state, frag_list);
|
|
|
|
if (parse_flag == SPF_PARSE_SAPI)
|
|
{
|
|
FIXME("SAPI XML parsing is not implemented.\n");
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
assert(!state.pPhoneIds);
|
|
assert(!state.Context.pCategory && !state.Context.pBefore && !state.Context.pAfter);
|
|
|
|
end = contents + wcslen(contents);
|
|
|
|
if (FAILED(hr = parse_ssml_contents(contents, end, &state, frag_list)))
|
|
return hr;
|
|
|
|
if (persist)
|
|
{
|
|
assert(!state.pPhoneIds);
|
|
assert(!state.Context.pCategory && !state.Context.pBefore && !state.Context.pAfter);
|
|
|
|
*global_state = state;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|