/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "glk/adrift/scare.h"
#include "glk/adrift/scprotos.h"
namespace Glk {
namespace Adrift {
/*
* Module notes:
*
* o Is the whole interpolation and ALR passes thing right? There's no
* documentation on it, and it's not intuitively implemented in Adrift.
*
* o Is dissecting HTML tags the right thing to do?
*/
/* Assorted definitions and constants. */
static const sc_uint PRINTFILTER_MAGIC = 0xb4736417;
enum {
BUFFER_GROW_INCREMENT = 32,
ITERATION_LIMIT = 32
};
static const sc_char NUL = '\0';
static const sc_char LESSTHAN = '<';
static const sc_char GREATERTHAN = '>';
static const sc_char PERCENT = '%';
static const sc_char *const ENTITY_LESSTHAN = "<",
*const ENTITY_GREATERTHAN = ">",
*const ENTITY_PERCENT = "+percent+";
enum {
ENTITY_LENGTH = 4,
PERCENT_LENGTH = 9
};
static const sc_char *const ESCAPES = "<>%&+";
static const sc_char *const WHITESPACE = "\t\n\v\f\r ";
/* Trace flag, set before running. */
static sc_bool pf_trace = FALSE;
/*
* Table tying HTML-like tag strings to enumerated tag types. Since it's
* scanned sequentially by strncmp(), it's ordered so that longer strings
* come before shorter ones. The
tag is missing because this is
* handled separately, as a simple put of '\n'.
*/
struct sc_html_tags_t {
const sc_char *const name;
const sc_int length;
const sc_int tag;
};
static const sc_html_tags_t HTML_TAGS_TABLE[] = {
{"bgcolour", 8, SC_TAG_BGCOLOR}, {"bgcolor", 7, SC_TAG_BGCOLOR},
{"waitkey", 7, SC_TAG_WAITKEY},
{"center", 6, SC_TAG_CENTER}, {"/center", 7, SC_TAG_ENDCENTER},
{"centre", 6, SC_TAG_CENTER}, {"/centre", 7, SC_TAG_ENDCENTER},
{"right", 5, SC_TAG_RIGHT}, {"/right", 6, SC_TAG_ENDRIGHT},
{"font", 4, SC_TAG_FONT}, {"/font", 5, SC_TAG_ENDFONT},
{"wait", 4, SC_TAG_WAIT}, {"cls", 3, SC_TAG_CLS},
{"i", 1, SC_TAG_ITALICS}, {"/i", 2, SC_TAG_ENDITALICS},
{"b", 1, SC_TAG_BOLD}, {"/b", 2, SC_TAG_ENDBOLD},
{"u", 1, SC_TAG_UNDERLINE}, {"/u", 2, SC_TAG_ENDUNDERLINE},
{"c", 1, SC_TAG_COLOR}, {"/c", 2, SC_TAG_ENDCOLOR},
{NULL, 0, SC_TAG_UNKNOWN}
};
/*
* Printfilter structure definition. It defines a buffer for output,
* associated size and length, a note of any conversion to apply to the next
* buffered character, and a flag to let the filter ignore incoming text.
*/
struct sc_filter_s {
sc_uint magic;
sc_int buffer_length;
sc_int buffer_allocation;
sc_char *buffer;
sc_bool new_sentence;
sc_bool is_muted;
sc_bool needs_filtering;
};
typedef sc_filter_s sc_filter_t;
/*
* pf_is_valid()
*
* Return TRUE if pointer is a valid printfilter, FALSE otherwise.
*/
static sc_bool pf_is_valid(sc_filterref_t filter) {
return filter && filter->magic == PRINTFILTER_MAGIC;
}
/*
* pf_create()
*
* Create and return a new printfilter.
*/
sc_filterref_t pf_create(void) {
static sc_bool initialized = FALSE;
sc_filterref_t filter;
/* On first call only, verify the string lengths in the table. */
if (!initialized) {
const sc_html_tags_t *entry;
/* Compare table lengths with string lengths. */
for (entry = HTML_TAGS_TABLE; entry->name; entry++) {
if (entry->length != (sc_int) strlen(entry->name)) {
sc_fatal("pf_create:"
" table string length is wrong for \"%s\"\n",
entry->name);
}
}
initialized = TRUE;
}
/* Create a new printfilter. */
filter = (sc_filterref_t)sc_malloc(sizeof(*filter));
filter->magic = PRINTFILTER_MAGIC;
filter->buffer_length = 0;
filter->buffer_allocation = 0;
filter->buffer = NULL;
filter->new_sentence = FALSE;
filter->is_muted = FALSE;
filter->needs_filtering = FALSE;
return filter;
}
/*
* pf_destroy()
*
* Destroy a printfilter and free its allocated memory.
*/
void pf_destroy(sc_filterref_t filter) {
assert(pf_is_valid(filter));
/* Free buffer space, and poison and free the printfilter. */
sc_free(filter->buffer);
memset(filter, 0xaa, sizeof(*filter));
sc_free(filter);
}
/*
* pf_interpolate_vars()
*
* Replace %...% elements in a string by their variable values. If any
* variables were interpolated, returns an allocated string with replacements
* done, otherwise returns NULL.
*
* If a %...% element exists that is not a variable, then it's left in as is.
* Similarly, an unmatched (single) % in a string is also left as is. There
* appears to be no facility in the file format for escaping literal '%'
* characters, and since some games have strings with this character in them,
* this is probably all that can be done.
*/
static sc_char *pf_interpolate_vars(const sc_char *string, sc_var_setref_t vars) {
sc_char *buffer, *name;
const sc_char *cursor;
const sc_char *marker;
sc_bool is_interpolated;
/*
* Begin with NULL buffer and name strings for lazy allocation, and clear
* interpolation detection flag.
*/
buffer = NULL;
name = NULL;
is_interpolated = FALSE;
/* Run through the string looking for variables. */
marker = string;
for (cursor = (const sc_char *)strchr(marker, PERCENT);
cursor; cursor = (const sc_char *)strchr(marker, PERCENT)) {
sc_int type;
sc_vartype_t vt_rvalue;
sc_char close;
/*
* If not yet allocated, allocate a buffer for the return string and
* copy up to the percent character into it; otherwise append to buffer
* up to percent character. And if not yet done, allocate a name
* buffer guaranteed long enough.
*/
if (!buffer) {
buffer = (sc_char *)sc_malloc(cursor - marker + 1);
memcpy(buffer, marker, cursor - marker);
buffer[cursor - marker] = NUL;
} else {
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + cursor - marker + 1);
strncat(buffer, marker, cursor - marker);
}
if (!name)
name = (sc_char *)sc_malloc(strlen(string) + 1);
/*
* Get the variable name, and from that, the value. If we encounter a
* mismatched '%' or unknown variable, skip it.
*/
if (sscanf(cursor, "%%%[^%]%c", name, &close) != 2
|| close != PERCENT
|| !var_get(vars, name, &type, &vt_rvalue)) {
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + 2);
strncat(buffer, cursor, 1);
marker = cursor + 1;
continue;
}
/* Get variable value and append to the string. */
switch (type) {
case VAR_INTEGER: {
sc_char value[32];
sprintf(value, "%ld", vt_rvalue.integer);
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + strlen(value) + 1);
strcat(buffer, value);
break;
}
case VAR_STRING:
buffer = (sc_char *)sc_realloc(buffer,
strlen(buffer) + strlen(vt_rvalue.string) + 1);
strcat(buffer, vt_rvalue.string);
break;
default:
sc_fatal("pf_interpolate_vars: invalid variable type, %ld\n", type);
}
/* Advance over the %...% variable name, and note success. */
marker = cursor + strlen(name) + 2;
is_interpolated = TRUE;
}
/*
* If we allocated a buffer and interpolated into it, append the remainder
* of the string. If we didn't interpolate successfully (the input contained
* a rogue '%' character), throw out the buffer as it will be the same as
* our input.
*/
if (buffer) {
if (is_interpolated) {
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + strlen(marker) + 1);
strcat(buffer, marker);
} else {
sc_free(buffer);
buffer = NULL;
}
}
/* Clean up, and return either the updated string or NULL. */
sc_free(name);
return buffer;
}
/*
* pf_replace_alr()
*
* Helper for pf_replace_alrs(). Replace one ALR found in the string with
* its equivalent, updating the buffer at the address passed in, including
* reallocating if necessary. Return TRUE if the buffer was changed.
*/
static sc_bool pf_replace_alr(const sc_char *string, sc_char **buffer, sc_int alr, sc_prop_setref_t bundle) {
sc_vartype_t vt_key[3];
const sc_char *marker, *cursor, *original, *replacement;
sc_char *buffer_ = *buffer;
/* Retrieve the ALR original string, set replacement to NULL for now. */
vt_key[0].string = "ALRs";
vt_key[1].integer = alr;
vt_key[2].string = "Original";
original = prop_get_string(bundle, "S<-sis", vt_key);
replacement = NULL;
/* Ignore pathological empty originals. */
if (original[0] == NUL)
return FALSE;
/* Run through the marker string looking for things to replace. */
marker = string;
for (cursor = strstr(marker, original);
cursor; cursor = strstr(marker, original)) {
/* Optimize by retrieving the replacement string only on demand. */
if (!replacement) {
vt_key[2].string = "Replacement";
replacement = prop_get_string(bundle, "S<-sis", vt_key);
}
/*
* If not yet allocated, allocate a buffer for the return string and
* copy; else append to the existing buffer: basic copy-on-write.
*/
if (!buffer_) {
buffer_ = (sc_char *)sc_malloc(cursor - marker + strlen(replacement) + 1);
memcpy(buffer_, marker, cursor - marker);
buffer_[cursor - marker] = NUL;
strcat(buffer_, replacement);
} else {
buffer_ = (sc_char *)sc_realloc(buffer_, strlen(buffer_) +
cursor - marker + strlen(replacement) + 1);
strncat(buffer_, marker, cursor - marker);
strcat(buffer_, replacement);
}
/* Advance over the original. */
marker = cursor + strlen(original);
}
/* If any pending text, append it to the buffer. */
if (replacement) {
buffer_ = (sc_char *)sc_realloc(buffer_, strlen(buffer_) + strlen(marker) + 1);
strcat(buffer_, marker);
}
/* Write back buffer, and if replacement set, the buffer was altered. */
*buffer = buffer_;
return replacement != NULL;
}
/*
* pf_replace_alrs()
*
* Replace any ALRs found in the string with their equivalents. If any
* ALRs were replaced, returns an allocated string with replacements done,
* otherwise returns NULL.
*/
static sc_char *pf_replace_alrs(const sc_char *string, sc_prop_setref_t bundle,
sc_bool alr_applied[], sc_int alr_count) {
sc_int index_;
sc_char *buffer1, *buffer2, **buffer;
const sc_char *marker;
/*
* Begin with NULL buffers and alternate for lazy allocation. To avoid a
* lot of allocation and copying, we use two buffers to help with repeated
* ALR replacement.
*/
buffer1 = buffer2 = NULL;
buffer = &buffer1;
/* Run through each ALR that exists. */
marker = string;
for (index_ = 0; index_ < alr_count; index_++) {
sc_vartype_t vt_key[3];
sc_int alr;
/*
* Ignore ALR indexes that have already been applied. This prevents
* endless loops in ALR replacement.
*/
if (alr_applied[index_])
continue;
/*
* Get the actual ALR number for the ALR. This comes from the index
* that we sorted earlier by length of original string. Try replacing
* that ALR in the current marker string.
*/
vt_key[0].string = "ALRs2";
vt_key[1].integer = index_;
vt_key[2].string = "ALRIndex";
alr = prop_get_integer(bundle, "I<-sis", vt_key);
if (pf_replace_alr(marker, buffer, alr, bundle)) {
/*
* The current buffer in use has been altered. This means that we
* have to switch the marker string to the buffer containing the
* replacement, and switch 'buffer' to the other one for the next
* ALR iteration.
*/
marker = *buffer;
buffer = (buffer == &buffer1) ? &buffer2 : &buffer1;
/* Discard any content in the buffer switched to above. */
if (*buffer)
(*buffer)[0] = NUL;
/* Note this ALR as "used up", and unavailable for future passes. */
alr_applied[index_] = TRUE;
}
}
/*
* If marker points to one or other of the buffers, that buffer is the
* return string, and the other is garbage, and should now be freed (or
* was never used, in which case it is NULL).
*/
if (marker == buffer1) {
sc_free(buffer2);
return buffer1;
} else if (marker == buffer2) {
sc_free(buffer1);
return buffer2;
} else
return NULL;
}
/*
* pf_output_text()
*
* Edit the tag-stripped text element passed in, substituting < >
* +percent+ with < > %, then send to the OS-specific output functions.
*/
static void pf_output_text(const sc_char *string) {
sc_int index_, b_index;
sc_char *buffer;
/* Optimize away the allocation and copy if possible. */
if (!(strstr(string, ENTITY_LESSTHAN)
|| strstr(string, ENTITY_GREATERTHAN)
|| strstr(string, ENTITY_PERCENT))) {
if_print_string(string);
return;
}
/*
* Copy characters from the string into the buffer, replacing any &..;
* elements by their single-character equivalents. We also replace any
* +percent+ elements by percent characters; apparently an undocumented
* Adrift Runner extension.
*/
buffer = (sc_char *)sc_malloc(strlen(string) + 1);
for (index_ = 0, b_index = 0;
string[index_] != NUL; index_++, b_index++) {
if (sc_strncasecmp(string + index_,
ENTITY_LESSTHAN, ENTITY_LENGTH) == 0) {
buffer[b_index] = LESSTHAN;
index_ += ENTITY_LENGTH - 1;
} else if (sc_strncasecmp(string + index_,
ENTITY_GREATERTHAN, ENTITY_LENGTH) == 0) {
buffer[b_index] = GREATERTHAN;
index_ += ENTITY_LENGTH - 1;
} else if (sc_strncasecmp(string + index_,
ENTITY_PERCENT, PERCENT_LENGTH) == 0) {
buffer[b_index] = PERCENT;
index_ += PERCENT_LENGTH - 1;
} else
buffer[b_index] = string[index_];
}
/* Terminate, print, and free the buffer. */
buffer[b_index] = NUL;
if_print_string(buffer);
sc_free(buffer);
}
/*
* pf_output_tag()
*
* Output an HTML-like tag element to the OS-specific tag handling function.
*/
static void pf_output_tag(const sc_char *contents) {
const sc_html_tags_t *entry;
const sc_char *argument;
/* For a simple
tag, just print out a newline. */
if (sc_compare_word(contents, "br", 2)) {
if_print_character('\n');
return;
}
/*
* Search for the name in the HTML tags table. It should be a full match,
* that is, the character after the matched name must be space or NUL.
* The tag is the exception; here the terminator is '='.
*/
for (entry = HTML_TAGS_TABLE; entry->name; entry++) {
if (sc_strncasecmp(contents, entry->name, entry->length) == 0) {
sc_char next;
next = contents[entry->length];
if (next == NUL || sc_isspace(next)
|| (entry->tag == SC_TAG_BGCOLOR && next == '='))
break;
}
}
/* If not matched, output an unknown tag with contents as its argument. */
if (!entry->name) {
if_print_tag(SC_TAG_UNKNOWN, contents);
return;
}
/*
* Find the argument by skipping the tag name and any spaces. Again, for
* , make a special case, passing the complete contents as
* argument (to match for the client.
*/
argument = contents;
argument += (entry->tag != SC_TAG_BGCOLOR) ? entry->length : 0;
while (sc_isspace(argument[0]))
argument++;
if_print_tag(entry->tag, argument);
}
/*
* pf_output_untagged()
*
* Break apart HTML-like string into normal text elements, and HTML-like
* tags.
*/
static void pf_output_untagged(const sc_char *string) {
sc_char *temporary, *untagged, *contents;
const sc_char *cursor;
const sc_char *marker;
/*
* Optimize away the allocation and copy if possible. We need to check
* here both for tags and for entities; only if neither occurs is it safe
* to output the string directly.
*/
if (!strchr(string, LESSTHAN)
&& !(strstr(string, ENTITY_LESSTHAN)
|| strstr(string, ENTITY_GREATERTHAN)
|| strstr(string, ENTITY_PERCENT))) {
if_print_string(string);
return;
}
/*
* Create a general temporary string, and alias it to both untagged text
* and the tag name, for sharing inside the loop.
*/
temporary = (sc_char *)sc_malloc(strlen(string) + 1);
untagged = contents = temporary;
/* Run through the string looking for <...> tags. */
marker = string;
for (cursor = (const sc_char *)strchr(marker, LESSTHAN);
cursor; cursor = (const sc_char *)strchr(marker, LESSTHAN)) {
sc_char close;
/* Handle characters up to the tag start; untagged text. */
memcpy(untagged, marker, cursor - marker);
untagged[cursor - marker] = NUL;
pf_output_text(untagged);
/* Catch and ignore completely empty tags. */
if (cursor[1] == GREATERTHAN) {
marker = cursor + 2;
continue;
}
/*
* Get the text within the tag, reusing the temporary buffer. If this
* fails, allow the remainder of the line to be delivered as a tag;
* unknown, probably.
*/
if (sscanf(cursor, "<%[^>]%c", contents, &close) != 2
|| close != GREATERTHAN) {
if (sscanf(cursor, "<%[^>]", contents) != 1) {
sc_error("pf_output_untagged: mismatched '%c'\n", LESSTHAN);
if_print_character(LESSTHAN);
marker = cursor + 1;
continue;
}
}
/* Output tag, and advance marker over the <...> tag. */
if (!sc_strempty(contents))
pf_output_tag(contents);
marker = cursor + strlen(contents) + 1;
marker += (marker[0] == GREATERTHAN) ? 1 : 0;
}
/* Output any remaining string text, and free the temporary buffer. */
pf_output_text(marker);
sc_free(temporary);
}
/*
* pf_filter_internal()
*
* Filters an output string, interpolating variables and replacing ALR's. If
* any filtering was done, returns an allocated string that the caller needs
* to free; otherwise, return NULL.
*
* Bundle may be NULL, requesting that the function suppress ALR replacements,
* and do only variables; used for game info strings.
*
* The way Adrift does this is somewhat obscure, but the following seems to
* replicate it well enough for most practical purposes (it's unlikely that
* any game assumes or relies on anything not covered by this):
*
* repeat some number of times
* repeat some number of times
* interpolate variables
* repeat [some number of times?]
* for each ALR unused so far this pass
* search the current string for the ALR original
* if found
* replace this ALR in the current string
* mark this ALR as used
* until no more changes in the current string
*
*/
static sc_char *pf_filter_internal(const sc_char *string, sc_var_setref_t vars, sc_prop_setref_t bundle) {
sc_int alr_count, iteration;
sc_char *current;
sc_bool *alr_applied;
assert(string && vars);
if (pf_trace)
sc_trace("Printfilter: initial \"%s\"\n", string);
/* If including ALRs, create a common set of ALR application flags. */
if (bundle) {
sc_vartype_t vt_key;
/* Obtain a count of ALRs. */
vt_key.string = "ALRs";
alr_count = prop_get_child_count(bundle, "I<-s", &vt_key);
/*
* Create a new set of ALR application flags. These are used to ensure
* that a given ALR is applied only once on a given pass. If the game
* has no ALRs, don't create a flag set.
*/
if (alr_count > 0) {
alr_applied = (sc_bool *)sc_malloc(alr_count * sizeof(*alr_applied));
memset(alr_applied, FALSE, alr_count * sizeof(*alr_applied));
} else
alr_applied = NULL;
} else {
/* Not including ALRs, so set alr count to 0, and flags to NULL. */
alr_count = 0;
alr_applied = NULL;
}
/* Loop for a sort-of arbitrary number of passes; probably enough. */
current = NULL;
for (iteration = 0; iteration < ITERATION_LIMIT; iteration++) {
sc_int inner_iteration;
const sc_char *initial;
sc_char *intermediate;
/* Note the initial string, so we can check for no change. */
initial = current;
for (inner_iteration = 0;
inner_iteration < ITERATION_LIMIT; inner_iteration++) {
/*
* Interpolate variables. If any changes were made, advance current
* to the interpolated version, and free the old current if required.
* Work on the current string, if any, otherwise the input string.
*/
intermediate = pf_interpolate_vars(current ? current : string, vars);
if (intermediate) {
sc_free(current);
current = intermediate;
if (pf_trace) {
sc_trace("Printfilter: interpolated [%ld,%ld] \"%s\"\n",
iteration, inner_iteration, current);
}
} else
break;
}
/* If we have ALRs to process, search out and replace all findable. */
if (alr_count > 0) {
/* Replace ALRs until no more ALRs can be found. */
inner_iteration = 0;
while (TRUE) {
/*
* Replace ALRs, and advance current as for variables above.
* Leave the loop when ALR replacements stop. Again, work on
* the current string if any, otherwise the input string.
*/
intermediate = pf_replace_alrs(current ? current : string,
bundle, alr_applied, alr_count);
if (intermediate) {
sc_free(current);
current = intermediate;
if (pf_trace) {
sc_trace("Printfilter: replaced [%ld,%ld] \"%s\"\n",
iteration, inner_iteration, current);
}
} else
break;
inner_iteration++;
}
}
/* If nothing changed this iteration, stop now. */
if (current == initial)
break;
}
/* Free any ALR application flags, and return current, NULL if no change. */
sc_free(alr_applied);
return current;
}
/*
* pf_filter()
*
* A facet of pf_filter_internal(). Filter an output string, interpolating
* variables and replacing ALR's. Returns an allocated string that the caller
* needs to free.
*/
sc_char *pf_filter(const sc_char *string, sc_var_setref_t vars, sc_prop_setref_t bundle) {
sc_char *current;
/* Filter this string, including ALRs replacements. */
current = pf_filter_internal(string, vars, bundle);
/* Our contract is to return an allocated string; copy if required. */
if (!current) {
current = (sc_char *)sc_malloc(strlen(string) + 1);
strcpy(current, string);
}
return current;
}
/*
* pf_filter_for_info()
*
* A facet of pf_filter_internal(). Filters output, interpolating variables
* only (no ALR replacement), and returns the resulting string to the caller.
* Used on informational strings such as the game title and author. Returns
* an allocated string that the caller needs to free.
*/
sc_char *pf_filter_for_info(const sc_char *string, sc_var_setref_t vars) {
sc_char *current;
/* Filter this string, excluding ALRs replacements. */
current = pf_filter_internal(string, vars, NULL);
/* Our contract is to return an allocated string; copy if required. */
if (!current) {
current = (sc_char *)sc_malloc(strlen(string) + 1);
strcpy(current, string);
}
return current;
}
/*
* pf_flush()
*
* Filter buffered data, interpolating variables and replacing ALR's, and
* send the resulting string to the output channel.
*/
void pf_flush(sc_filterref_t filter, sc_var_setref_t vars, sc_prop_setref_t bundle) {
assert(pf_is_valid(filter));
assert(vars && bundle);
/* See if there is any buffered data to flush. */
if (filter->buffer_length > 0) {
/*
* Filter the buffered string, then print it untagged. Remember to free
* the filtered version. If filtering made no difference, or if the
* buffer was already filtered by, say, checkpointing, just print the
* original buffer untagged instead.
*/
if (filter->needs_filtering) {
sc_char *filtered;
filtered = pf_filter_internal(filter->buffer, vars, bundle);
if (filtered) {
pf_output_untagged(filtered);
sc_free(filtered);
} else
pf_output_untagged(filter->buffer);
} else
pf_output_untagged(filter->buffer);
/* Remove buffered data by resetting length to zero. */
filter->buffer_length = 0;
filter->needs_filtering = FALSE;
}
/* Reset new sentence and mute flags. */
filter->new_sentence = FALSE;
filter->is_muted = FALSE;
}
/*
* pf_append_string()
*
* Append a string to the filter buffer.
*/
static void pf_append_string(sc_filterref_t filter, const sc_char *string) {
sc_int length, required;
/*
* Calculate the required buffer size to append string. Remember to add
* one for the terminating NUL.
*/
length = strlen(string);
required = filter->buffer_length + length + 1;
/* If this is more than the current buffer allocation, resize it. */
if (required > filter->buffer_allocation) {
sc_int new_allocation;
/* Calculate the new malloc size, in increment chunks. */
new_allocation = ((required + BUFFER_GROW_INCREMENT - 1)
/ BUFFER_GROW_INCREMENT) * BUFFER_GROW_INCREMENT;
/* Grow the buffer. */
filter->buffer = (sc_char *)sc_realloc(filter->buffer, new_allocation);
filter->buffer_allocation = new_allocation;
}
/* If empty, put a NUL into the buffer to permit strcat. */
if (filter->buffer_length == 0)
filter->buffer[0] = NUL;
/* Append the string to the buffer and extend length. */
strcat(filter->buffer, string);
filter->buffer_length += length;
}
/*
* pf_checkpoint()
*
* Filter buffered data, interpolating variables and replacing ALR's, and
* store the result back in the buffer. This allows a string to be inter-
* polated in between main flushes; used to update buffered text with variable
* values before those values are updated by task actions.
*/
void pf_checkpoint(sc_filterref_t filter, sc_var_setref_t vars, sc_prop_setref_t bundle) {
assert(pf_is_valid(filter));
assert(vars && bundle);
/* See if there is any buffered data to filter. */
if (filter->buffer_length > 0) {
/*
* Filter the buffered string, and place the filtered result, if any,
* back into the filter buffer. We do this by setting the buffer length
* back to zero, then appending the filtered string; this keeps the
* grown buffer intact.
*/
if (filter->needs_filtering) {
sc_char *filtered;
filtered = pf_filter_internal(filter->buffer, vars, bundle);
if (filtered) {
filter->buffer_length = 0;
pf_append_string(filter, filtered);
sc_free(filtered);
}
}
/* Note the buffer as filtered, to avoid pointless filtering. */
filter->needs_filtering = FALSE;
}
}
/*
* pf_get_buffer()
* pf_transfer_buffer()
*
* Return the raw, unfiltered, buffered text. Returns NULL if no buffered
* data available. Transferring the buffer transfers ownership of the buffer
* string to the caller, who is then responsible for freeing it.
*
* The second function is an optimization to avoid allocations and copying
* in client code.
*/
const sc_char *pf_get_buffer(sc_filterref_t filter) {
assert(pf_is_valid(filter));
/*
* Return buffer if filter length is greater than zero. Note that this
* assumes that the buffer is a nul-terminated string.
*/
if (filter->buffer_length > 0) {
assert(filter->buffer[filter->buffer_length] == NUL);
return filter->buffer;
} else
return NULL;
}
sc_char *pf_transfer_buffer(sc_filterref_t filter) {
assert(pf_is_valid(filter));
/*
* If the filter length is greater than zero, pass out the buffer (a nul-
* terminated string) and zero our length, allocation, and set the buffer
* back to NULL; an empty in all except the free-ing.
*/
if (filter->buffer_length > 0) {
sc_char *retval;
/* Set the return value to be the buffered text. */
assert(filter->buffer[filter->buffer_length] == NUL);
retval = filter->buffer;
/* Clear all filter fields down to empty values. */
filter->buffer_length = 0;
filter->buffer_allocation = 0;
filter->buffer = NULL;
filter->new_sentence = FALSE;
filter->is_muted = FALSE;
filter->needs_filtering = FALSE;
/* Return the allocated buffered text. */
return retval;
} else
return NULL;
}
/*
* pf_empty()
*
* Empty any text currently buffered in the filter.
*/
void pf_empty(sc_filterref_t filter) {
assert(pf_is_valid(filter));
/* Free any allocation, and return the filter to initialization state. */
filter->buffer_length = 0;
filter->buffer_allocation = 0;
sc_free(filter->buffer);
filter->buffer = NULL;
filter->new_sentence = FALSE;
filter->is_muted = FALSE;
filter->needs_filtering = FALSE;
}
/*
* pf_buffer_string()
* pf_buffer_character()
*
* Add a string, and a single character, to the printfilter buffer. If muted,
* these functions do nothing.
*/
void pf_buffer_string(sc_filterref_t filter, const sc_char *string) {
assert(pf_is_valid(filter));
assert(string);
/* Ignore the call if the printfilter is muted. */
if (!filter->is_muted) {
sc_int noted;
/* Note append start, then append the string to the buffer. */
noted = filter->buffer_length;
pf_append_string(filter, string);
/* Adjust the first character of the appended string if flagged. */
if (filter->new_sentence)
filter->buffer[noted] = sc_toupper(filter->buffer[noted]);
/* Clear new sentence, and note as currently needing filtering. */
filter->needs_filtering = TRUE;
filter->new_sentence = FALSE;
}
}
void pf_buffer_character(sc_filterref_t filter, sc_char character) {
sc_char buffer[2];
assert(pf_is_valid(filter));
buffer[0] = character;
buffer[1] = NUL;
pf_buffer_string(filter, buffer);
}
/*
* pf_prepend_string()
*
* Add a string to the front of the printfilter buffer, rather than to the
* end. Generally less efficient than an append, these are for use by task
* running code, which needs to run task actions and then prepend the task's
* completion text. If muted, this function does nothing.
*/
void pf_prepend_string(sc_filterref_t filter, const sc_char *string) {
assert(pf_is_valid(filter));
assert(string);
/* Ignore the call if the printfilter is muted. */
if (!filter->is_muted) {
if (filter->buffer_length > 0) {
sc_char *copy;
/* Take a copy of the current buffered string. */
assert(filter->buffer[filter->buffer_length] == NUL);
copy = (sc_char *)sc_malloc(filter->buffer_length + 1);
strcpy(copy, filter->buffer);
/*
* Now restart buffering with the input string passed in. Removing
* the current content by zeroing the length preserves the grown
* allocation of the main buffer.
*/
filter->buffer_length = 0;
pf_append_string(filter, string);
/* Append the string saved above and then free it. */
pf_append_string(filter, copy);
sc_free(copy);
/* Adjust the first character of the prepended string if flagged. */
if (filter->new_sentence)
filter->buffer[0] = sc_toupper(filter->buffer[0]);
/* Clear new sentence, and note as currently needing filtering. */
filter->needs_filtering = TRUE;
filter->new_sentence = FALSE;
} else
/* No data, so the call is equivalent to a normal buffer. */
pf_buffer_string(filter, string);
}
}
/*
* pf_new_sentence()
*
* Tells the printfilter to force the next non-space character to uppercase.
* Ignored if the printfilter is muted.
*/
void pf_new_sentence(sc_filterref_t filter) {
assert(pf_is_valid(filter));
if (!filter->is_muted)
filter->new_sentence = TRUE;
}
/*
* pf_mute()
* pf_clear_mute()
*
* A muted printfilter ignores all new text additions.
*/
void pf_mute(sc_filterref_t filter) {
assert(pf_is_valid(filter));
filter->is_muted = TRUE;
}
void pf_clear_mute(sc_filterref_t filter) {
assert(pf_is_valid(filter));
filter->is_muted = FALSE;
}
/*
* pf_buffer_tag()
*
* Insert an HTML-like tag into the buffered output data. The call is ignored
* if the printfilter is muted.
*/
void pf_buffer_tag(sc_filterref_t filter, sc_int tag) {
const sc_html_tags_t *entry;
assert(pf_is_valid(filter));
/* Search the tags table for this tag. */
for (entry = HTML_TAGS_TABLE; entry->name; entry++) {
if (tag == entry->tag)
break;
}
/* If found, output the equivalent string, enclosed in '<>' characters. */
if (entry->name) {
pf_buffer_character(filter, LESSTHAN);
pf_buffer_string(filter, entry->name);
pf_buffer_character(filter, GREATERTHAN);
} else
sc_error("pf_buffer_tag: invalid tag, %ld\n", tag);
}
/*
* pf_strip_tags_common()
*
* Strip HTML-like tags from a string. Used to process strings used in ways
* other than being passed to if_print_string(), for example room names and
* status lines. It ignores all tags except
, which it replaces with
* a newline if requested by allow_newlines.
*/
static void pf_strip_tags_common(sc_char *string, sc_bool allow_newlines) {
sc_char *marker, *cursor;
/* Run through the string looking for <...> tags. */
marker = string;
for (cursor = strchr(marker, LESSTHAN);
cursor; cursor = strchr(marker, LESSTHAN)) {
sc_char *tag_end;
/* Locate tag end, and break if unterminated. */
tag_end = strchr(cursor, GREATERTHAN);
if (!tag_end)
break;
/* If the tag is
, replace with newline if requested. */
if (allow_newlines) {
if (tag_end - cursor == 3
&& sc_strncasecmp(cursor + 1, "br", 2) == 0)
*cursor++ = '\n';
}
/* Remove the tag from the string, then advance input. */
memmove(cursor, tag_end + 1, strlen(tag_end));
marker = cursor;
}
}
/*
* pf_strip_tags()
* pf_strip_tags_for_hints()
*
* Public interfaces to pf_strip_tags_common(). The hints version will
* allow
tags to map into newlines in hints strings.
*/
void pf_strip_tags(sc_char *string) {
pf_strip_tags_common(string, FALSE);
}
void pf_strip_tags_for_hints(sc_char *string) {
pf_strip_tags_common(string, TRUE);
}
/*
* pf_escape()
*
* Escape <, >, and % characters in the input string. Used to filter player
* input prior to storing in referenced text.
*
* Adrift offers no escapes for & and + escapes, so for these we convert to
* the character itself followed by a space. The return string is malloc'ed,
* so the caller needs to remember to free it.
*/
sc_char *pf_escape(const sc_char *string) {
const sc_char *marker, *cursor;
sc_char *buffer;
/* Start with an empty return buffer. */
buffer = (sc_char *)sc_malloc(strlen(string) + 1);
buffer[0] = NUL;
/* Run through the string looking for <, >, %, or other escapes. */
marker = string;
for (cursor = marker + strcspn(marker, ESCAPES);
cursor[0] != NUL; cursor = marker + strcspn(marker, ESCAPES)) {
const sc_char *escape;
sc_char escape_buffer[3];
/* Extend buffer to hold the string so far. */
if (cursor > marker) {
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + cursor - marker + 1);
buffer[strlen(buffer) + cursor - marker] = NUL;
memcpy(buffer + strlen(buffer), marker, cursor - marker);
}
/* Determine the appropriate character escape. */
if (cursor[0] == LESSTHAN)
escape = ENTITY_LESSTHAN;
else if (cursor[0] == GREATERTHAN)
escape = ENTITY_GREATERTHAN;
else if (cursor[0] == PERCENT)
escape = ENTITY_PERCENT;
else {
/*
* No real escape available, so fake, badly, by appending a space
* for cases where we've encountered a character entity; leave
* others untouched.
*/
escape_buffer[0] = cursor[0];
if (sc_strncasecmp(cursor,
ENTITY_LESSTHAN, ENTITY_LENGTH) == 0
|| sc_strncasecmp(cursor,
ENTITY_GREATERTHAN, ENTITY_LENGTH) == 0
|| sc_strncasecmp(cursor,
ENTITY_PERCENT, PERCENT_LENGTH) == 0) {
escape_buffer[1] = ' ';
escape_buffer[2] = NUL;
} else
escape_buffer[1] = NUL;
escape = escape_buffer;
}
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + strlen(escape) + 1);
strcat(buffer, escape);
/* Pass over character escaped and continue. */
cursor++;
marker = cursor;
}
/* Add all remaining characters to the buffer. */
if (cursor > marker) {
buffer = (sc_char *)sc_realloc(buffer, strlen(buffer) + cursor - marker + 1);
buffer[strlen(buffer) + cursor - marker] = NUL;
memcpy(buffer + strlen(buffer), marker, cursor - marker);
}
return buffer;
}
/*
* pf_compare_words()
*
* Matches multiple words from words in string. Returns the extent of
* the match if the string matched, 0 otherwise.
*/
static sc_int pf_compare_words(const sc_char *string, const sc_char *words) {
sc_int word_posn, posn;
/* None expected, but skip leading space. */
for (word_posn = 0; sc_isspace(words[word_posn]) && words[word_posn] != NUL;)
word_posn++;
/* Match characters from words with the string at position. */
posn = 0;
while (TRUE) {
/* Any character mismatch means no words match. */
if (sc_tolower(words[word_posn]) != sc_tolower(string[posn]))
return 0;
/* Move to next character in each. */
word_posn++;
posn++;
/*
* If at space, advance over whitespace in words list. Stop when we
* hit the end of the words list.
*/
while (sc_isspace(words[word_posn]) && words[word_posn] != NUL)
word_posn++;
if (words[word_posn] == NUL)
break;
/*
* About to match another word, so advance over whitespace in the
* current string too.
*/
while (sc_isspace(string[posn]) && string[posn] != NUL)
posn++;
}
/*
* We reached the end of words. If we're at the end of the match string,
* or at spaces, we've matched.
*/
if (sc_isspace(string[posn]) || string[posn] == NUL)
return posn;
/* More text after the match, so it's not quite a match. */
return 0;
}
/*
* pf_filter_input()
*
* Applies synonym changes to a player input string, and returns the resulting
* string to the caller, or NULL if no synonym changes were needed. The
* return string is malloc'ed, so the caller needs to remember to free it.
*/
sc_char *pf_filter_input(const sc_char *string, sc_prop_setref_t bundle) {
sc_vartype_t vt_key[3];
sc_int synonym_count, buffer_allocation;
sc_char *buffer;
const sc_char *current;
assert(string && bundle);
if (pf_trace)
sc_trace("Printfilter: input \"%s\"\n", string);
/* Obtain a count of synonyms. */
vt_key[0].string = "Synonyms";
synonym_count = prop_get_child_count(bundle, "I<-s", vt_key);
/* Begin with a NULL buffer for lazy allocation. */
buffer_allocation = 0;
buffer = NULL;
/* Loop over each word in the string. */
current = string + strspn(string, WHITESPACE);
while (current[0] != NUL) {
sc_int index_, extent;
/* Search for a synonym match at this index into the buffer. */
extent = 0;
for (index_ = 0; index_ < synonym_count; index_++) {
const sc_char *original;
/* Retrieve the synonym original string. */
vt_key[0].string = "Synonyms";
vt_key[1].integer = index_;
vt_key[2].string = "Original";
original = prop_get_string(bundle, "S<-sis", vt_key);
/* Compare the original at this point. */
extent = pf_compare_words(current, original);
if (extent > 0)
break;
}
/*
* If a synonym found was, index_ indicates it, and extent shows how
* much of the buffer to replace with it.
*/
if (index_ < synonym_count && extent > 0) {
const sc_char *replacement;
sc_char *position;
sc_int length, final;
/*
* If not yet allocated, allocate a buffer now, and copy the input
* string into it. Then switch current to the equivalent location
* in the allocated buffer. More basic copy-on-write.
*/
if (!buffer) {
buffer_allocation = strlen(string) + 1;
buffer = (sc_char *)sc_malloc(buffer_allocation);
strcpy(buffer, string);
current = buffer + (current - string);
}
/* Find the replacement text for this synonym. */
vt_key[0].string = "Synonyms";
vt_key[1].integer = index_;
vt_key[2].string = "Replacement";
replacement = prop_get_string(bundle, "S<-sis", vt_key);
length = strlen(replacement);
/*
* If necessary, grow the output buffer for the replacement,
* remembering to adjust current for the new buffer allocated.
* At the same time, note the last character index for the move.
*/
if (length > extent) {
sc_int offset;
offset = current - buffer;
buffer_allocation += length - extent;
buffer = (sc_char *)sc_realloc(buffer, buffer_allocation);
current = buffer + offset;
final = length;
} else
final = extent;
/* Insert the replacement string into the buffer. */
position = buffer + (current - buffer);
memmove(position + length,
position + extent,
buffer_allocation - (current - buffer) - final);
memcpy(position, replacement, length);
/* Adjust current to skip over the replacement. */
current += length;
if (pf_trace)
sc_trace("Printfilter: synonym \"%s\"\n", buffer);
} else {
/* If no match, advance current over the unmatched word. */
current += strcspn(current, WHITESPACE);
}
/* Set current to the next word start. */
current += strspn(current, WHITESPACE);
}
/* Return the final string, or NULL if no synonym replacements. */
return buffer;
}
/*
* pf_debug_trace()
*
* Set filter tracing on/off.
*/
void pf_debug_trace(sc_bool flag) {
pf_trace = flag;
}
} // End of namespace Adrift
} // End of namespace Glk