1346 lines
33 KiB
C
1346 lines
33 KiB
C
/*
|
|
* Copyright 2005-2007 James Bursa <bursa@users.sourceforge.net>
|
|
*
|
|
* This file is part of NetSurf, http://www.netsurf-browser.org/
|
|
*
|
|
* NetSurf 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; version 2 of the License.
|
|
*
|
|
* NetSurf 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/** \file
|
|
* Content handling (implementation).
|
|
*
|
|
* This implementation is based on the ::handler_map array, which maps
|
|
* ::content_type to the functions which implement that type.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <inttypes.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <time.h>
|
|
#include "utils/config.h"
|
|
#include "content/content.h"
|
|
#include "content/fetch.h"
|
|
#include "content/fetchcache.h"
|
|
#include "css/css.h"
|
|
#include "image/bitmap.h"
|
|
#include "desktop/options.h"
|
|
#include "render/directory.h"
|
|
#include "render/html.h"
|
|
#include "render/textplain.h"
|
|
#ifdef WITH_JPEG
|
|
#include "image/jpeg.h"
|
|
#endif
|
|
#ifdef WITH_MNG
|
|
#include "image/mng.h"
|
|
#endif
|
|
#ifdef WITH_GIF
|
|
#include "image/gif.h"
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
#include "image/bmp.h"
|
|
#include "image/ico.h"
|
|
#endif
|
|
#ifdef WITH_NS_SVG
|
|
#include "image/svg.h"
|
|
#endif
|
|
#ifdef WITH_RSVG
|
|
#include "image/rsvg.h"
|
|
#endif
|
|
#ifdef WITH_SPRITE
|
|
#include "riscos/sprite.h"
|
|
#endif
|
|
#ifdef WITH_NSSPRITE
|
|
#include "image/nssprite.h"
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
#include "riscos/draw.h"
|
|
#endif
|
|
#ifdef WITH_PLUGIN
|
|
#include "riscos/plugin.h"
|
|
#endif
|
|
#ifdef WITH_ARTWORKS
|
|
#include "riscos/artworks.h"
|
|
#endif
|
|
#ifdef WITH_PNG
|
|
#include "image/png.h"
|
|
#endif
|
|
#include "utils/log.h"
|
|
#include "utils/messages.h"
|
|
#include "utils/talloc.h"
|
|
#include "utils/utils.h"
|
|
|
|
|
|
/** Linked list of all content structures. May include more than one content
|
|
* per URL. Doubly-linked. */
|
|
struct content *content_list = 0;
|
|
|
|
/** An entry in mime_map. */
|
|
struct mime_entry {
|
|
char mime_type[40];
|
|
content_type type;
|
|
};
|
|
/** A map from MIME type to ::content_type. Must be sorted by mime_type. */
|
|
static const struct mime_entry mime_map[] = {
|
|
#ifdef WITH_BMP
|
|
{"application/bmp", CONTENT_BMP},
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
{"application/drawfile", CONTENT_DRAW},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"application/ico", CONTENT_ICO},
|
|
{"application/preview", CONTENT_BMP},
|
|
{"application/x-bmp", CONTENT_BMP},
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
{"application/x-drawfile", CONTENT_DRAW},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"application/x-ico", CONTENT_ICO},
|
|
#endif
|
|
{"application/x-netsurf-directory", CONTENT_DIRECTORY},
|
|
#ifdef WITH_THEME_INSTALL
|
|
{"application/x-netsurf-theme", CONTENT_THEME},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"application/x-win-bitmap", CONTENT_BMP},
|
|
#endif
|
|
{"application/xhtml+xml", CONTENT_HTML},
|
|
#ifdef WITH_BMP
|
|
{"image/bmp", CONTENT_BMP},
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
{"image/drawfile", CONTENT_DRAW},
|
|
#endif
|
|
#ifdef WITH_GIF
|
|
{"image/gif", CONTENT_GIF},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"image/ico", CONTENT_ICO},
|
|
#endif
|
|
#ifdef WITH_MNG
|
|
{"image/jng", CONTENT_JNG},
|
|
#endif
|
|
#ifdef WITH_JPEG
|
|
{"image/jpeg", CONTENT_JPEG},
|
|
{"image/jpg", CONTENT_JPEG},
|
|
#endif
|
|
#ifdef WITH_MNG
|
|
{"image/mng", CONTENT_MNG},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"image/ms-bmp", CONTENT_BMP},
|
|
#endif
|
|
#ifdef WITH_JPEG
|
|
{"image/pjpeg", CONTENT_JPEG},
|
|
#endif
|
|
#if defined(WITH_MNG) || defined(WITH_PNG)
|
|
{"image/png", CONTENT_PNG},
|
|
#endif
|
|
#if defined(WITH_NS_SVG) || defined (WITH_RSVG)
|
|
{"image/svg", CONTENT_SVG},
|
|
{"image/svg+xml", CONTENT_SVG},
|
|
#endif
|
|
#ifdef WITH_ARTWORKS
|
|
{"image/x-artworks", CONTENT_ARTWORKS},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"image/x-bitmap", CONTENT_BMP},
|
|
{"image/x-bmp", CONTENT_BMP},
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
{"image/x-drawfile", CONTENT_DRAW},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"image/x-icon", CONTENT_ICO},
|
|
#endif
|
|
#ifdef WITH_MNG
|
|
{"image/x-jng", CONTENT_JNG},
|
|
{"image/x-mng", CONTENT_MNG},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"image/x-ms-bmp", CONTENT_BMP},
|
|
#endif
|
|
#if defined(WITH_SPRITE) || defined(WITH_NSSPRITE)
|
|
{"image/x-riscos-sprite", CONTENT_SPRITE},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{"image/x-win-bitmap", CONTENT_BMP},
|
|
{"image/x-windows-bmp", CONTENT_BMP},
|
|
{"image/x-xbitmap", CONTENT_BMP},
|
|
#endif
|
|
{"text/css", CONTENT_CSS},
|
|
{"text/html", CONTENT_HTML},
|
|
{"text/plain", CONTENT_TEXTPLAIN},
|
|
#ifdef WITH_MNG
|
|
{"video/mng", CONTENT_MNG},
|
|
{"video/x-mng", CONTENT_MNG},
|
|
#endif
|
|
};
|
|
#define MIME_MAP_COUNT (sizeof(mime_map) / sizeof(mime_map[0]))
|
|
|
|
const char * const content_type_name[] = {
|
|
"HTML",
|
|
"TEXTPLAIN",
|
|
"CSS",
|
|
#ifdef WITH_JPEG
|
|
"JPEG",
|
|
#endif
|
|
#ifdef WITH_GIF
|
|
"GIF",
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
"BMP",
|
|
"ICO",
|
|
#endif
|
|
#if defined(WITH_MNG) || defined(WITH_PNG)
|
|
"PNG",
|
|
#endif
|
|
#ifdef WITH_MNG
|
|
"JNG",
|
|
"MNG",
|
|
#endif
|
|
#if defined(WITH_SPRITE) || defined(WITH_NSSPRITE)
|
|
"SPRITE",
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
"DRAW",
|
|
#endif
|
|
#ifdef WITH_PLUGIN
|
|
"PLUGIN",
|
|
#endif
|
|
"DIRECTORY",
|
|
#ifdef WITH_THEME_INSTALL
|
|
"THEME",
|
|
#endif
|
|
#ifdef WITH_ARTWORKS
|
|
"ARTWORKS",
|
|
#endif
|
|
#if defined(WITH_NS_SVG) || defined(WITH_RSVG)
|
|
"SVG",
|
|
#endif
|
|
"OTHER",
|
|
"UNKNOWN"
|
|
};
|
|
|
|
const char * const content_status_name[] = {
|
|
"TYPE_UNKNOWN",
|
|
"LOADING",
|
|
"READY",
|
|
"DONE",
|
|
"ERROR"
|
|
};
|
|
|
|
/** An entry in handler_map. */
|
|
struct handler_entry {
|
|
bool (*create)(struct content *c, struct content *parent,
|
|
const char *params[]);
|
|
bool (*process_data)(struct content *c, char *data, unsigned int size);
|
|
bool (*convert)(struct content *c, int width, int height);
|
|
void (*reformat)(struct content *c, int width, int height);
|
|
void (*destroy)(struct content *c);
|
|
void (*stop)(struct content *c);
|
|
bool (*redraw)(struct content *c, int x, int y,
|
|
int width, int height,
|
|
int clip_x0, int clip_y0, int clip_x1, int clip_y1,
|
|
float scale, colour background_colour);
|
|
bool (*redraw_tiled)(struct content *c, int x, int y,
|
|
int width, int height,
|
|
int clip_x0, int clip_y0, int clip_x1, int clip_y1,
|
|
float scale, colour background_colour,
|
|
bool repeat_x, bool repeat_y);
|
|
void (*open)(struct content *c, struct browser_window *bw,
|
|
struct content *page, unsigned int index,
|
|
struct box *box,
|
|
struct object_params *params);
|
|
void (*close)(struct content *c);
|
|
/** There must be one content per user for this type. */
|
|
bool no_share;
|
|
};
|
|
/** A table of handler functions, indexed by ::content_type.
|
|
* Must be ordered as enum ::content_type. */
|
|
static const struct handler_entry handler_map[] = {
|
|
{html_create, html_process_data, html_convert,
|
|
html_reformat, html_destroy, html_stop, html_redraw, 0,
|
|
html_open, html_close,
|
|
true},
|
|
{textplain_create, textplain_process_data, textplain_convert,
|
|
textplain_reformat, textplain_destroy, 0, textplain_redraw, 0,
|
|
0, 0, true},
|
|
{nscss_create, nscss_process_data, nscss_convert, 0, nscss_destroy,
|
|
0, 0, 0, 0, 0, true},
|
|
#ifdef WITH_JPEG
|
|
{0, 0, nsjpeg_convert, 0, nsjpeg_destroy, 0,
|
|
nsjpeg_redraw, nsjpeg_redraw_tiled, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_GIF
|
|
{nsgif_create, 0, nsgif_convert, 0, nsgif_destroy, 0,
|
|
nsgif_redraw, nsgif_redraw_tiled, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_BMP
|
|
{nsbmp_create, 0, nsbmp_convert, 0, nsbmp_destroy, 0,
|
|
nsbmp_redraw, nsbmp_redraw_tiled, 0, 0, false},
|
|
{nsico_create, 0, nsico_convert, 0, nsico_destroy, 0,
|
|
nsico_redraw, nsico_redraw_tiled, 0, 0, false},
|
|
#endif
|
|
|
|
#ifdef WITH_PNG
|
|
{nspng_create, nspng_process_data, nspng_convert,
|
|
0, nspng_destroy, 0, nspng_redraw, nspng_redraw_tiled,
|
|
0, 0, false},
|
|
#else
|
|
#ifdef WITH_MNG
|
|
{nsmng_create, nsmng_process_data, nsmng_convert,
|
|
0, nsmng_destroy, 0, nsmng_redraw, nsmng_redraw_tiled,
|
|
0, 0, false},
|
|
#endif
|
|
#endif
|
|
#ifdef WITH_MNG
|
|
{nsmng_create, nsmng_process_data, nsmng_convert,
|
|
0, nsmng_destroy, 0, nsmng_redraw, nsmng_redraw_tiled,
|
|
0, 0, false},
|
|
{nsmng_create, nsmng_process_data, nsmng_convert,
|
|
0, nsmng_destroy, 0, nsmng_redraw, nsmng_redraw_tiled,
|
|
0, 0, false},
|
|
#endif
|
|
#ifdef WITH_SPRITE
|
|
{0, 0, sprite_convert,
|
|
0, sprite_destroy, 0, sprite_redraw, 0, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_NSSPRITE
|
|
{0, 0, nssprite_convert,
|
|
0, nssprite_destroy, 0, nssprite_redraw, 0, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_DRAW
|
|
{0, 0, draw_convert,
|
|
0, draw_destroy, 0, draw_redraw, 0, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_PLUGIN
|
|
{plugin_create, 0, plugin_convert,
|
|
plugin_reformat, plugin_destroy, 0, plugin_redraw, 0,
|
|
plugin_open, plugin_close,
|
|
true},
|
|
#endif
|
|
{directory_create, 0, directory_convert,
|
|
0, directory_destroy, 0, 0, 0, 0, 0, true},
|
|
#ifdef WITH_THEME_INSTALL
|
|
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_ARTWORKS
|
|
{0, 0, artworks_convert,
|
|
0, artworks_destroy, 0, artworks_redraw, 0, 0, 0, false},
|
|
#endif
|
|
#ifdef WITH_NS_SVG
|
|
{svg_create, 0, svg_convert,
|
|
0, svg_destroy, 0, svg_redraw, 0, 0, 0, true},
|
|
#endif
|
|
#ifdef WITH_RSVG
|
|
{rsvg_create, rsvg_process_data, rsvg_convert,
|
|
0, rsvg_destroy, 0, rsvg_redraw, 0, 0, 0, false},
|
|
#endif
|
|
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false}
|
|
};
|
|
#define HANDLER_MAP_COUNT (sizeof(handler_map) / sizeof(handler_map[0]))
|
|
|
|
|
|
static void content_update_status(struct content *c);
|
|
static void content_destroy(struct content *c);
|
|
static void content_stop_check(struct content *c);
|
|
|
|
|
|
/**
|
|
* Convert a MIME type to a content_type.
|
|
*
|
|
* The returned ::content_type will always be suitable for content_set_type().
|
|
*/
|
|
|
|
content_type content_lookup(const char *mime_type)
|
|
{
|
|
struct mime_entry *m;
|
|
m = bsearch(mime_type, mime_map, MIME_MAP_COUNT, sizeof(mime_map[0]),
|
|
(int (*)(const void *, const void *)) strcasecmp);
|
|
if (m == 0) {
|
|
#ifdef WITH_PLUGIN
|
|
if (plugin_handleable(mime_type))
|
|
return CONTENT_PLUGIN;
|
|
#endif
|
|
return CONTENT_OTHER;
|
|
}
|
|
return m->type;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new content structure.
|
|
*
|
|
* \param url URL of content, copied
|
|
* \return the new content structure, or 0 on memory exhaustion
|
|
*
|
|
* The type is initialised to CONTENT_UNKNOWN, and the status to
|
|
* CONTENT_STATUS_TYPE_UNKNOWN.
|
|
*/
|
|
|
|
struct content * content_create(const char *url)
|
|
{
|
|
struct content *c;
|
|
struct content_user *user_sentinel;
|
|
|
|
c = talloc_zero(0, struct content);
|
|
if (!c)
|
|
return 0;
|
|
|
|
LOG(("url %s -> %p", url, c));
|
|
|
|
user_sentinel = talloc(c, struct content_user);
|
|
if (!user_sentinel) {
|
|
talloc_free(c);
|
|
return 0;
|
|
}
|
|
c->url = talloc_strdup(c, url);
|
|
if (!c->url) {
|
|
talloc_free(c);
|
|
return 0;
|
|
}
|
|
talloc_set_name_const(c, c->url);
|
|
c->type = CONTENT_UNKNOWN;
|
|
c->mime_type = 0;
|
|
c->status = CONTENT_STATUS_TYPE_UNKNOWN;
|
|
c->width = 0;
|
|
c->height = 0;
|
|
c->available_width = 0;
|
|
c->refresh = 0;
|
|
c->bitmap = 0;
|
|
c->fresh = false;
|
|
c->time = wallclock();
|
|
c->size = 0;
|
|
c->title = 0;
|
|
c->active = 0;
|
|
user_sentinel->callback = 0;
|
|
user_sentinel->p1 = user_sentinel->p2 = 0;
|
|
user_sentinel->next = 0;
|
|
c->user_list = user_sentinel;
|
|
c->sub_status[0] = 0;
|
|
c->locked = false;
|
|
c->fetch = 0;
|
|
c->source_data = 0;
|
|
c->source_size = 0;
|
|
c->source_allocated = 0;
|
|
c->total_size = 0;
|
|
c->http_code = 0;
|
|
c->no_error_pages = false;
|
|
c->download = false;
|
|
c->tried_with_auth = false;
|
|
c->redirect_count = 0;
|
|
c->error_count = 0;
|
|
c->cache_data.req_time = 0;
|
|
c->cache_data.res_time = 0;
|
|
c->cache_data.date = 0;
|
|
c->cache_data.expires = 0;
|
|
c->cache_data.age = INVALID_AGE;
|
|
c->cache_data.max_age = INVALID_AGE;
|
|
c->cache_data.no_cache = false;
|
|
c->cache_data.etag = 0;
|
|
c->cache_data.last_modified = 0;
|
|
|
|
content_set_status(c, messages_get("Loading"));
|
|
|
|
c->prev = 0;
|
|
c->next = content_list;
|
|
if (content_list)
|
|
content_list->prev = c;
|
|
content_list = c;
|
|
|
|
return c;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a content from the memory cache.
|
|
*
|
|
* \param url URL of content
|
|
* \return content if found, or 0
|
|
*
|
|
* Searches the list of contents for one corresponding to the given url, and
|
|
* which is fresh and shareable.
|
|
*/
|
|
|
|
struct content * content_get(const char *url)
|
|
{
|
|
struct content *c;
|
|
|
|
for (c = content_list; c; c = c->next) {
|
|
if (!c->fresh)
|
|
/* not fresh */
|
|
continue;
|
|
if (c->status == CONTENT_STATUS_ERROR)
|
|
/* error state */
|
|
continue;
|
|
/** \todo We need to reconsider the entire caching strategy in
|
|
* the light of data being shared between specific contents.
|
|
*
|
|
* For example, string dictionaries are owned by the document,
|
|
* and all stylesheets used by the document share the same
|
|
* dictionary.
|
|
*
|
|
* The CSS content handler retrieves the dictionary from its
|
|
* parent content. This relies upon there being a 1:1 mapping
|
|
* between documents and stylesheets.
|
|
*
|
|
* The type of a content is only known once we've received the
|
|
* headers from the fetch layer (and potentially some of the
|
|
* content data, too, if we ever sniff for the type). There
|
|
* is thus a problem with returning contents of unknown type
|
|
* here -- when we subsequently discover that they must only
|
|
* have one user, we clone them. By that point, however, we've
|
|
* no idea what the parent content is, which means that they
|
|
* end up with the wrong parent (and thus wrong dictionary).
|
|
*
|
|
* Of course, the problem with ignoring unknown content types
|
|
* here is that, for all the content types which may be shared,
|
|
* we end up duplicating them and wasting memory. Hence the
|
|
* need to reconsider everything.
|
|
*/
|
|
if (c->type == CONTENT_UNKNOWN)
|
|
continue;
|
|
if (c->type != CONTENT_UNKNOWN &&
|
|
handler_map[c->type].no_share &&
|
|
c->user_list->next)
|
|
/* not shareable, and has a user already */
|
|
continue;
|
|
if (strcmp(c->url, url))
|
|
continue;
|
|
return c;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a READY or DONE content from the memory cache.
|
|
*
|
|
* \param url URL of content
|
|
* \return content if found, or 0
|
|
*
|
|
* Searches the list of contents for one corresponding to the given url, and
|
|
* which is fresh, shareable and either READY or DONE.
|
|
*/
|
|
|
|
struct content * content_get_ready(const char *url)
|
|
{
|
|
struct content *c;
|
|
|
|
for (c = content_list; c; c = c->next) {
|
|
if (!c->fresh)
|
|
/* not fresh */
|
|
continue;
|
|
if (c->status != CONTENT_STATUS_READY &&
|
|
c->status != CONTENT_STATUS_DONE)
|
|
/* not ready or done */
|
|
continue;
|
|
if (c->type != CONTENT_UNKNOWN &&
|
|
handler_map[c->type].no_share &&
|
|
c->user_list->next)
|
|
/* not shareable, and has a user already */
|
|
continue;
|
|
if (strcmp(c->url, url))
|
|
continue;
|
|
return c;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get whether a content can reformat
|
|
*
|
|
* \param c content to check
|
|
* \return whether the content can reformat
|
|
*/
|
|
bool content_can_reformat(struct content *c)
|
|
{
|
|
return (handler_map[c->type].reformat != NULL);
|
|
}
|
|
|
|
|
|
/**
|
|
* Initialise the content for the specified type.
|
|
*
|
|
* \param c content structure
|
|
* \param type content_type to initialise to
|
|
* \param mime_type MIME-type string for this content
|
|
* \param params array of strings, ordered attribute, value, attribute, ..., 0
|
|
* \return true on success, false on error and error broadcast to users and
|
|
* possibly reported
|
|
*
|
|
* The type is updated to the given type, and a copy of mime_type is taken. The
|
|
* status is changed to CONTENT_STATUS_LOADING. CONTENT_MSG_LOADING is sent to
|
|
* all users. The create function for the type is called to initialise the type
|
|
* specific parts of the content structure.
|
|
*/
|
|
|
|
bool content_set_type(struct content *c, content_type type,
|
|
const char *mime_type, const char *params[],
|
|
struct content *parent)
|
|
{
|
|
union content_msg_data msg_data;
|
|
struct content *clone;
|
|
void (*callback)(content_msg msg, struct content *c, intptr_t p1,
|
|
intptr_t p2, union content_msg_data data);
|
|
intptr_t p1, p2;
|
|
|
|
assert(c != 0);
|
|
assert(c->status == CONTENT_STATUS_TYPE_UNKNOWN);
|
|
assert(type < CONTENT_UNKNOWN);
|
|
|
|
LOG(("content %s (%p), type %i", c->url, c, type));
|
|
|
|
c->mime_type = talloc_strdup(c, mime_type);
|
|
if (!c->mime_type) {
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
msg_data.error = messages_get("NoMemory");
|
|
content_broadcast(c, CONTENT_MSG_ERROR, msg_data);
|
|
return false;
|
|
}
|
|
|
|
c->type = type;
|
|
c->status = CONTENT_STATUS_LOADING;
|
|
|
|
if (handler_map[type].no_share && c->user_list->next &&
|
|
c->user_list->next->next) {
|
|
/* type not shareable, and more than one user: split into
|
|
* a content per user */
|
|
const char *referer =
|
|
c->fetch ? fetch_get_referer(c->fetch) : NULL;
|
|
struct content *parent =
|
|
c->fetch ? fetch_get_parent(c->fetch) : NULL;
|
|
|
|
while (c->user_list->next->next) {
|
|
clone = content_create(c->url);
|
|
if (!clone) {
|
|
c->type = CONTENT_UNKNOWN;
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
msg_data.error = messages_get("NoMemory");
|
|
content_broadcast(c, CONTENT_MSG_ERROR,
|
|
msg_data);
|
|
return false;
|
|
}
|
|
|
|
clone->width = c->width;
|
|
clone->height = c->height;
|
|
clone->fresh = c->fresh;
|
|
|
|
callback = c->user_list->next->next->callback;
|
|
p1 = c->user_list->next->next->p1;
|
|
p2 = c->user_list->next->next->p2;
|
|
if (!content_add_user(clone, callback, p1, p2)) {
|
|
c->type = CONTENT_UNKNOWN;
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
content_destroy(clone);
|
|
msg_data.error = messages_get("NoMemory");
|
|
content_broadcast(c, CONTENT_MSG_ERROR,
|
|
msg_data);
|
|
return false;
|
|
}
|
|
content_remove_user(c, callback, p1, p2);
|
|
msg_data.new_url = NULL;
|
|
content_broadcast(clone, CONTENT_MSG_NEWPTR, msg_data);
|
|
fetchcache_go(clone, referer,
|
|
callback, p1, p2,
|
|
clone->width, clone->height,
|
|
0, 0, false, parent);
|
|
}
|
|
}
|
|
|
|
if (handler_map[type].create) {
|
|
if (!handler_map[type].create(c, parent, params)) {
|
|
c->type = CONTENT_UNKNOWN;
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
content_broadcast(c, CONTENT_MSG_LOADING, msg_data);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates content with new status.
|
|
*
|
|
* The textual status contained in the content is updated with given string.
|
|
*
|
|
* \param status_message new textual status
|
|
*/
|
|
|
|
void content_set_status(struct content *c, const char *status_message, ...)
|
|
{
|
|
va_list ap;
|
|
int len;
|
|
|
|
va_start(ap, status_message);
|
|
if ((len = vsnprintf(c->sub_status, sizeof (c->sub_status),
|
|
status_message, ap)) < 0 ||
|
|
(int)sizeof (c->sub_status) <= len)
|
|
c->sub_status[sizeof (c->sub_status) - 1] = '\0';
|
|
va_end(ap);
|
|
|
|
content_update_status(c);
|
|
}
|
|
|
|
|
|
void content_update_status(struct content *c)
|
|
{
|
|
char token[20];
|
|
const char *status;
|
|
unsigned int time;
|
|
|
|
snprintf(token, sizeof token, "HTTP%li", c->http_code);
|
|
status = messages_get(token);
|
|
if (status == token)
|
|
status = token + 4;
|
|
|
|
if (c->status == CONTENT_STATUS_TYPE_UNKNOWN ||
|
|
c->status == CONTENT_STATUS_LOADING ||
|
|
c->status == CONTENT_STATUS_READY)
|
|
time = wallclock() - c->time;
|
|
else
|
|
time = c->time;
|
|
|
|
snprintf(c->status_message, sizeof (c->status_message),
|
|
"%s (%.1fs) %s", status,
|
|
(float) time / 100, c->sub_status);
|
|
/* LOG(("%s", c->status_message)); */
|
|
}
|
|
|
|
|
|
/**
|
|
* Process a block of source data.
|
|
*
|
|
* Calls the process_data function for the content.
|
|
*
|
|
* \param c content structure
|
|
* \param data new data to process
|
|
* \param size size of data
|
|
* \return true on success, false on error and error broadcast to users and
|
|
* possibly reported
|
|
*/
|
|
|
|
bool content_process_data(struct content *c, const char *data,
|
|
unsigned int size)
|
|
{
|
|
char *source_data;
|
|
union content_msg_data msg_data;
|
|
unsigned int extra_space;
|
|
|
|
assert(c);
|
|
assert(c->type < HANDLER_MAP_COUNT);
|
|
assert(c->status == CONTENT_STATUS_LOADING);
|
|
|
|
if ((c->source_size + size) > c->source_allocated) {
|
|
extra_space = (c->source_size + size) / 4;
|
|
if (extra_space < 65536)
|
|
extra_space = 65536;
|
|
source_data = talloc_realloc(c, c->source_data, char,
|
|
c->source_size + size + extra_space);
|
|
if (!source_data) {
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
msg_data.error = messages_get("NoMemory");
|
|
content_broadcast(c, CONTENT_MSG_ERROR, msg_data);
|
|
return false;
|
|
}
|
|
c->source_data = source_data;
|
|
c->source_allocated = c->source_size + size + extra_space;
|
|
}
|
|
memcpy(c->source_data + c->source_size, data, size);
|
|
c->source_size += size;
|
|
|
|
if (handler_map[c->type].process_data) {
|
|
if (!handler_map[c->type].process_data(c,
|
|
c->source_data + c->source_size - size, size)) {
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* All data has arrived, convert for display.
|
|
*
|
|
* Calls the convert function for the content.
|
|
*
|
|
* - If the conversion succeeds, but there is still some processing required
|
|
* (eg. loading images), the content gets status CONTENT_STATUS_READY, and a
|
|
* CONTENT_MSG_READY is sent to all users.
|
|
* - If the conversion succeeds and is complete, the content gets status
|
|
* CONTENT_STATUS_DONE, and CONTENT_MSG_READY then CONTENT_MSG_DONE are sent.
|
|
* - If the conversion fails, CONTENT_MSG_ERROR is sent. The content will soon
|
|
* be destroyed and must no longer be used.
|
|
*/
|
|
|
|
void content_convert(struct content *c, int width, int height)
|
|
{
|
|
union content_msg_data msg_data;
|
|
char *source_data;
|
|
|
|
assert(c);
|
|
assert(c->type < HANDLER_MAP_COUNT);
|
|
assert(c->status == CONTENT_STATUS_LOADING);
|
|
assert(!c->locked);
|
|
LOG(("content %s (%p)", c->url, c));
|
|
|
|
if (c->source_allocated != c->source_size) {
|
|
source_data = talloc_realloc(c, c->source_data, char,
|
|
c->source_size);
|
|
if (source_data) {
|
|
c->source_data = source_data;
|
|
c->source_allocated = c->source_size;
|
|
}
|
|
}
|
|
|
|
c->locked = true;
|
|
c->available_width = width;
|
|
if (handler_map[c->type].convert) {
|
|
if (!handler_map[c->type].convert(c, width, height)) {
|
|
c->status = CONTENT_STATUS_ERROR;
|
|
c->locked = false;
|
|
return;
|
|
}
|
|
} else {
|
|
c->status = CONTENT_STATUS_DONE;
|
|
}
|
|
c->locked = false;
|
|
|
|
assert(c->status == CONTENT_STATUS_READY ||
|
|
c->status == CONTENT_STATUS_DONE);
|
|
content_broadcast(c, CONTENT_MSG_READY, msg_data);
|
|
if (c->status == CONTENT_STATUS_DONE)
|
|
content_set_done(c);
|
|
}
|
|
|
|
|
|
/**
|
|
* Put a content in status CONTENT_STATUS_DONE.
|
|
*/
|
|
|
|
void content_set_done(struct content *c)
|
|
{
|
|
union content_msg_data msg_data;
|
|
|
|
c->status = CONTENT_STATUS_DONE;
|
|
c->time = wallclock() - c->time;
|
|
content_update_status(c);
|
|
content_broadcast(c, CONTENT_MSG_DONE, msg_data);
|
|
}
|
|
|
|
|
|
/**
|
|
* Reformat to new size.
|
|
*
|
|
* Calls the reformat function for the content.
|
|
*/
|
|
|
|
void content_reformat(struct content *c, int width, int height)
|
|
{
|
|
union content_msg_data data;
|
|
assert(c != 0);
|
|
assert(c->status == CONTENT_STATUS_READY ||
|
|
c->status == CONTENT_STATUS_DONE);
|
|
assert(!c->locked);
|
|
LOG(("%p %s", c, c->url));
|
|
c->locked = true;
|
|
c->available_width = width;
|
|
if (handler_map[c->type].reformat) {
|
|
handler_map[c->type].reformat(c, width, height);
|
|
content_broadcast(c, CONTENT_MSG_REFORMAT, data);
|
|
}
|
|
c->locked = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Clean unused contents from the content_list.
|
|
*
|
|
* Destroys any contents in the content_list with no users or in
|
|
* CONTENT_STATUS_ERROR. Fresh contents in CONTENT_STATUS_DONE may be kept even
|
|
* with no users.
|
|
*
|
|
* Each content is also checked for stop requests.
|
|
*/
|
|
|
|
void content_clean(void)
|
|
{
|
|
unsigned int size;
|
|
struct content *c, *next, *prev;
|
|
|
|
/* destroy unused stale contents and contents with errors */
|
|
for (c = content_list; c; c = next) {
|
|
next = c->next;
|
|
|
|
/* this function must not be called from a content function */
|
|
assert(!c->locked);
|
|
|
|
if (c->user_list->next && c->status != CONTENT_STATUS_ERROR)
|
|
/* content has users */
|
|
continue;
|
|
|
|
if (c->fresh && c->status == CONTENT_STATUS_DONE)
|
|
/* content is fresh */
|
|
continue;
|
|
|
|
/* content can be destroyed */
|
|
content_destroy(c);
|
|
}
|
|
|
|
/* check for pending stops */
|
|
for (c = content_list; c; c = c->next) {
|
|
if (c->status == CONTENT_STATUS_READY)
|
|
content_stop_check(c);
|
|
}
|
|
|
|
/* attempt to shrink the memory cache (unused fresh contents) */
|
|
size = 0;
|
|
next = 0;
|
|
for (c = content_list; c; c = c->next) {
|
|
next = c;
|
|
c->talloc_size = talloc_total_size(c);
|
|
size += c->size + c->talloc_size;
|
|
}
|
|
for (c = next; c && (unsigned int) option_memory_cache_size < size;
|
|
c = prev) {
|
|
prev = c->prev;
|
|
if (c->user_list->next)
|
|
continue;
|
|
size -= c->size + c->talloc_size;
|
|
content_destroy(c);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Destroy and free a content.
|
|
*
|
|
* Calls the destroy function for the content, and frees the structure.
|
|
*/
|
|
|
|
void content_destroy(struct content *c)
|
|
{
|
|
assert(c);
|
|
LOG(("content %p %s", c, c->url));
|
|
assert(!c->locked);
|
|
|
|
if (c->fetch)
|
|
fetch_abort(c->fetch);
|
|
|
|
if (c->next)
|
|
c->next->prev = c->prev;
|
|
if (c->prev)
|
|
c->prev->next = c->next;
|
|
else
|
|
content_list = c->next;
|
|
|
|
if (c->type < HANDLER_MAP_COUNT && handler_map[c->type].destroy)
|
|
handler_map[c->type].destroy(c);
|
|
talloc_free(c);
|
|
}
|
|
|
|
|
|
/**
|
|
* Reset a content.
|
|
*
|
|
* Calls the destroy function for the content, but does not free
|
|
* the structure.
|
|
*/
|
|
|
|
void content_reset(struct content *c)
|
|
{
|
|
assert(c != 0);
|
|
LOG(("content %p %s", c, c->url));
|
|
assert(!c->locked);
|
|
if (c->type < HANDLER_MAP_COUNT && handler_map[c->type].destroy)
|
|
handler_map[c->type].destroy(c);
|
|
c->type = CONTENT_UNKNOWN;
|
|
c->status = CONTENT_STATUS_TYPE_UNKNOWN;
|
|
c->size = 0;
|
|
talloc_free(c->mime_type);
|
|
c->mime_type = 0;
|
|
talloc_free(c->refresh);
|
|
c->refresh = 0;
|
|
talloc_free(c->title);
|
|
c->title = 0;
|
|
talloc_free(c->source_data);
|
|
c->source_data = 0;
|
|
c->source_size = c->source_allocated = 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Free all contents in the content_list.
|
|
*/
|
|
|
|
void content_quit(void)
|
|
{
|
|
bool progress = true;
|
|
struct content *c, *next;
|
|
|
|
while (content_list && progress) {
|
|
progress = false;
|
|
for (c = content_list; c; c = next) {
|
|
assert(c->next != c);
|
|
next = c->next;
|
|
|
|
if (c->user_list->next &&
|
|
c->status != CONTENT_STATUS_ERROR)
|
|
/* content has users */
|
|
continue;
|
|
|
|
/* content can be destroyed */
|
|
content_destroy(c);
|
|
progress = true;
|
|
}
|
|
}
|
|
|
|
if (content_list) {
|
|
LOG(("bug: some contents could not be destroyed"));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Display content on screen.
|
|
*
|
|
* Calls the redraw function for the content, if it exists.
|
|
*
|
|
* \param c content
|
|
* \param x coordinate for top-left of redraw
|
|
* \param y coordinate for top-left of redraw
|
|
* \param width available width (not used for HTML redraw)
|
|
* \param height available height (not used for HTML redraw)
|
|
* \param clip_x0 clip rectangle left
|
|
* \param clip_y0 clip rectangle top
|
|
* \param clip_x1 clip rectangle right
|
|
* \param clip_y1 clip rectangle bottom
|
|
* \param scale scale for redraw
|
|
* \param background_colour the background colour
|
|
* \return true if successful, false otherwise
|
|
*
|
|
* x, y and clip_* are coordinates from the top left of the canvas area.
|
|
*
|
|
* The top left corner of the clip rectangle is (clip_x0, clip_y0) and
|
|
* the bottom right corner of the clip rectangle is (clip_x1, clip_y1).
|
|
* Units for x, y and clip_* are pixels.
|
|
*/
|
|
|
|
bool content_redraw(struct content *c, int x, int y,
|
|
int width, int height,
|
|
int clip_x0, int clip_y0, int clip_x1, int clip_y1,
|
|
float scale, colour background_colour)
|
|
{
|
|
assert(c != 0);
|
|
// LOG(("%p %s", c, c->url));
|
|
if (c->locked)
|
|
/* not safe to attempt redraw */
|
|
return true;
|
|
if (handler_map[c->type].redraw)
|
|
return handler_map[c->type].redraw(c, x, y, width, height,
|
|
clip_x0, clip_y0, clip_x1, clip_y1, scale,
|
|
background_colour);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Display content on screen with optional tiling.
|
|
*
|
|
* Calls the redraw_tile function for the content, or emulates it with the
|
|
* redraw function if it doesn't exist.
|
|
*/
|
|
|
|
bool content_redraw_tiled(struct content *c, int x, int y,
|
|
int width, int height,
|
|
int clip_x0, int clip_y0, int clip_x1, int clip_y1,
|
|
float scale, colour background_colour,
|
|
bool repeat_x, bool repeat_y)
|
|
{
|
|
int x0, y0, x1, y1;
|
|
|
|
assert(c != 0);
|
|
|
|
// LOG(("%p %s", c, c->url));
|
|
|
|
if (c->locked)
|
|
/* not safe to attempt redraw */
|
|
return true;
|
|
if (handler_map[c->type].redraw_tiled) {
|
|
return handler_map[c->type].redraw_tiled(c, x, y, width, height,
|
|
clip_x0, clip_y0, clip_x1, clip_y1, scale,
|
|
background_colour, repeat_x, repeat_y);
|
|
} else {
|
|
/* ensure we have a redrawable content */
|
|
if ((!handler_map[c->type].redraw) || (width == 0) ||
|
|
(height == 0))
|
|
return true;
|
|
/* simple optimisation for no repeat (common for backgrounds) */
|
|
if ((!repeat_x) && (!repeat_y))
|
|
return handler_map[c->type].redraw(c, x, y, width,
|
|
height, clip_x0, clip_y0, clip_x1, clip_y1,
|
|
scale, background_colour);
|
|
/* find the redraw boundaries to loop within*/
|
|
x0 = x;
|
|
if (repeat_x) {
|
|
for (; x0 > clip_x0; x0 -= width);
|
|
x1 = clip_x1;
|
|
} else {
|
|
x1 = x + 1;
|
|
}
|
|
y0 = y;
|
|
if (repeat_y) {
|
|
for (; y0 > clip_y0; y0 -= height);
|
|
y1 = clip_y1;
|
|
} else {
|
|
y1 = y + 1;
|
|
}
|
|
/* repeatedly plot our content */
|
|
for (y = y0; y < y1; y += height)
|
|
for (x = x0; x < x1; x += width)
|
|
if (!handler_map[c->type].redraw(c, x, y,
|
|
width, height,
|
|
clip_x0, clip_y0,
|
|
clip_x1, clip_y1,
|
|
scale, background_colour))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Register a user for callbacks.
|
|
*
|
|
* \param c the content to register
|
|
* \param callback the callback function
|
|
* \param p1, p2 callback private data
|
|
* \return true on success, false otherwise on memory exhaustion
|
|
*
|
|
* The callback will be called with p1 and p2 when content_broadcast() is
|
|
* called with the content.
|
|
*/
|
|
|
|
bool content_add_user(struct content *c,
|
|
void (*callback)(content_msg msg, struct content *c,
|
|
intptr_t p1, intptr_t p2, union content_msg_data data),
|
|
intptr_t p1, intptr_t p2)
|
|
{
|
|
struct content_user *user;
|
|
|
|
LOG(("content %s (%p), user %p 0x%" PRIxPTR " 0x%" PRIxPTR,
|
|
c->url, c, callback, p1, p2));
|
|
user = talloc(c, struct content_user);
|
|
if (!user)
|
|
return false;
|
|
user->callback = callback;
|
|
user->p1 = p1;
|
|
user->p2 = p2;
|
|
user->stop = false;
|
|
user->next = c->user_list->next;
|
|
c->user_list->next = user;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Search the users of a content for the specified user.
|
|
*
|
|
* \return a content_user struct for the user, or 0 if not found
|
|
*/
|
|
|
|
struct content_user * content_find_user(struct content *c,
|
|
void (*callback)(content_msg msg, struct content *c,
|
|
intptr_t p1, intptr_t p2, union content_msg_data data),
|
|
intptr_t p1, intptr_t p2)
|
|
{
|
|
struct content_user *user;
|
|
|
|
/* user_list starts with a sentinel */
|
|
for (user = c->user_list; user->next &&
|
|
!(user->next->callback == callback &&
|
|
user->next->p1 == p1 &&
|
|
user->next->p2 == p2); user = user->next)
|
|
;
|
|
return user->next;
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove a callback user.
|
|
*
|
|
* The callback function, p1, and p2 must be identical to those passed to
|
|
* content_add_user().
|
|
*/
|
|
|
|
void content_remove_user(struct content *c,
|
|
void (*callback)(content_msg msg, struct content *c,
|
|
intptr_t p1, intptr_t p2, union content_msg_data data),
|
|
intptr_t p1, intptr_t p2)
|
|
{
|
|
struct content_user *user, *next;
|
|
LOG(("content %s (%p), user %p 0x%" PRIxPTR " 0x%" PRIxPTR,
|
|
c->url, c, callback, p1, p2));
|
|
|
|
/* user_list starts with a sentinel */
|
|
for (user = c->user_list; user->next != 0 &&
|
|
!(user->next->callback == callback &&
|
|
user->next->p1 == p1 &&
|
|
user->next->p2 == p2); user = user->next)
|
|
;
|
|
if (user->next == 0) {
|
|
LOG(("user not found in list"));
|
|
assert(0);
|
|
return;
|
|
}
|
|
next = user->next;
|
|
user->next = next->next;
|
|
talloc_free(next);
|
|
}
|
|
|
|
|
|
/**
|
|
* Send a message to all users.
|
|
*/
|
|
|
|
void content_broadcast(struct content *c, content_msg msg,
|
|
union content_msg_data data)
|
|
{
|
|
struct content_user *user, *next;
|
|
assert(c);
|
|
// LOG(("%p %s -> %d", c, c->url, msg));
|
|
for (user = c->user_list->next; user != 0; user = next) {
|
|
next = user->next; /* user may be destroyed during callback */
|
|
if (user->callback != 0)
|
|
user->callback(msg, c, user->p1, user->p2, data);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Stop a content loading.
|
|
*
|
|
* May only be called in CONTENT_STATUS_READY only. If all users have requested
|
|
* stop, the loading is stopped and the content placed in CONTENT_STATUS_DONE.
|
|
*/
|
|
|
|
void content_stop(struct content *c,
|
|
void (*callback)(content_msg msg, struct content *c,
|
|
intptr_t p1, intptr_t p2, union content_msg_data data),
|
|
intptr_t p1, intptr_t p2)
|
|
{
|
|
struct content_user *user;
|
|
|
|
assert(c->status == CONTENT_STATUS_READY);
|
|
|
|
user = content_find_user(c, callback, p1, p2);
|
|
if (!user) {
|
|
LOG(("user not found in list"));
|
|
assert(0);
|
|
return;
|
|
}
|
|
|
|
LOG(("%p %s: stop user %p 0x%" PRIxPTR " 0x%" PRIxPTR,
|
|
c, c->url, callback, p1, p2));
|
|
user->stop = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if all users have requested a stop, and do it if so.
|
|
*/
|
|
|
|
void content_stop_check(struct content *c)
|
|
{
|
|
struct content_user *user;
|
|
union content_msg_data data;
|
|
|
|
assert(c->status == CONTENT_STATUS_READY);
|
|
|
|
/* user_list starts with a sentinel */
|
|
for (user = c->user_list->next; user; user = user->next)
|
|
if (!user->stop)
|
|
return;
|
|
|
|
LOG(("%p %s", c, c->url));
|
|
|
|
/* all users have requested stop */
|
|
assert(handler_map[c->type].stop);
|
|
handler_map[c->type].stop(c);
|
|
assert(c->status == CONTENT_STATUS_DONE);
|
|
|
|
content_set_status(c, messages_get("Stopped"));
|
|
content_broadcast(c, CONTENT_MSG_DONE, data);
|
|
}
|
|
|
|
|
|
/**
|
|
* A window containing the content has been opened.
|
|
*
|
|
* \param c content that has been opened
|
|
* \param bw browser window containing the content
|
|
* \param page content of type CONTENT_HTML containing c, or 0 if not an
|
|
* object within a page
|
|
* \param index index in page->data.html.object, or 0 if not an object
|
|
* \param box box containing c, or 0 if not an object
|
|
* \param params object parameters, or 0 if not an object
|
|
*
|
|
* Calls the open function for the content.
|
|
*/
|
|
|
|
void content_open(struct content *c, struct browser_window *bw,
|
|
struct content *page, unsigned int index, struct box *box,
|
|
struct object_params *params)
|
|
{
|
|
assert(c != 0);
|
|
assert(c->type < CONTENT_UNKNOWN);
|
|
LOG(("content %p %s", c, c->url));
|
|
if (handler_map[c->type].open)
|
|
handler_map[c->type].open(c, bw, page, index, box, params);
|
|
}
|
|
|
|
|
|
/**
|
|
* The window containing the content has been closed.
|
|
*
|
|
* Calls the close function for the content.
|
|
*/
|
|
|
|
void content_close(struct content *c)
|
|
{
|
|
assert(c != 0);
|
|
assert(c->type < CONTENT_UNKNOWN);
|
|
LOG(("content %p %s", c, c->url));
|
|
if (handler_map[c->type].close)
|
|
handler_map[c->type].close(c);
|
|
}
|
|
|
|
|
|
void content_add_error(struct content *c, const char *token,
|
|
unsigned int line)
|
|
{
|
|
}
|