Experiment with porting SGML to YAP, and trying to preserve SWI code as much
as possible.
This commit is contained in:
672
packages/sgml/catalog.c
Normal file
672
packages/sgml/catalog.c
Normal file
@@ -0,0 +1,672 @@
|
||||
/* $Id$
|
||||
|
||||
Part of SWI-Prolog
|
||||
|
||||
Author: Jan Wielemaker and Richard O'Keefe
|
||||
E-mail: wielemak@science.uva.nl
|
||||
WWW: http://www.swi-prolog.org
|
||||
Copyright (C): 1985-2006, University of Amsterdam
|
||||
|
||||
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#define _ISOC99_SOURCE 1 /* fwprintf(), etc prototypes */
|
||||
#include "util.h"
|
||||
#include "catalog.h"
|
||||
#include <stdio.h>
|
||||
#include <wctype.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#define DTD_MINOR_ERRORS 1
|
||||
#include <dtd.h> /* error codes */
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
#define swprintf _snwprintf
|
||||
#endif
|
||||
|
||||
#ifdef _REENTRANT
|
||||
#include <pthread.h>
|
||||
|
||||
static pthread_mutex_t catalog_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define LOCK() pthread_mutex_lock(&catalog_mutex)
|
||||
#define UNLOCK() pthread_mutex_unlock(&catalog_mutex)
|
||||
#else
|
||||
#define LOCK()
|
||||
#define UNLOCK()
|
||||
#endif
|
||||
|
||||
#ifndef MAXPATHLEN
|
||||
#define MAXPATHLEN 1024
|
||||
#endif
|
||||
#ifndef MAXLINE
|
||||
#define MAXLINE 1024
|
||||
#endif
|
||||
#ifndef EOS
|
||||
#define EOS '\0'
|
||||
#endif
|
||||
#ifndef TRUE
|
||||
#define TRUE 1
|
||||
#define FALSE 0
|
||||
#endif
|
||||
|
||||
#define streq(s1, s2) istreq(s1, s2)
|
||||
#define uc(p) (*(p))
|
||||
|
||||
typedef struct catalogue_item *catalogue_item_ptr;
|
||||
struct catalogue_item
|
||||
{ catalogue_item_ptr next;
|
||||
int kind;
|
||||
ichar const *target;
|
||||
ichar const *replacement;
|
||||
};
|
||||
|
||||
static catalogue_item_ptr first_item = 0, last_item = 0;
|
||||
|
||||
typedef struct _catalog_file
|
||||
{ ichar *file;
|
||||
struct _catalog_file *next;
|
||||
int loaded; /* did we parse this file? */
|
||||
catalogue_item_ptr first_item; /* List of items in the file */
|
||||
catalogue_item_ptr last_item;
|
||||
} catalog_file;
|
||||
|
||||
static catalog_file *catalog;
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
#define isDirSep(c) ((c) == '/' || (c) == '\\')
|
||||
#define DIRSEPSTR L"\\"
|
||||
#else
|
||||
#define isDirSep(c) ((c) == '/')
|
||||
#define DIRSEPSTR L"/"
|
||||
#endif
|
||||
|
||||
static ichar *
|
||||
DirName(const ichar *f, ichar *dir)
|
||||
{ const ichar *base, *p;
|
||||
|
||||
for (base = p = f; *p; p++)
|
||||
{ if (isDirSep(*p) && p[1] != EOS)
|
||||
base = p;
|
||||
}
|
||||
if (base == f)
|
||||
{ if (isDirSep(*f))
|
||||
istrcpy(dir, DIRSEPSTR);
|
||||
else
|
||||
istrcpy(dir, L".");
|
||||
} else
|
||||
{ istrncpy(dir, f, base - f);
|
||||
dir[base - f] = EOS;
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
is_absolute_path(const ichar *name)
|
||||
{ if (isDirSep(name[0])
|
||||
#ifdef __WINDOWS__
|
||||
|| (iswalpha(uc(name)) && name[1] == ':')
|
||||
#endif
|
||||
)
|
||||
return TRUE;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
localpath() creates an absolute path for name relative to ref. The
|
||||
returned path must be freed using sgml_free() when done.
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
|
||||
ichar *
|
||||
localpath(const ichar *ref, const ichar *name)
|
||||
{ ichar *local;
|
||||
|
||||
if (!ref || is_absolute_path(name))
|
||||
local = istrdup(name);
|
||||
else
|
||||
{ ichar buf[MAXPATHLEN];
|
||||
|
||||
DirName(ref, buf);
|
||||
istrcat(buf, DIRSEPSTR);
|
||||
istrcat(buf, name);
|
||||
|
||||
local = istrdup(buf);
|
||||
}
|
||||
|
||||
if (!local)
|
||||
sgml_nomem();
|
||||
|
||||
return local;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
register_catalog_file_unlocked(const ichar *file, catalog_location where)
|
||||
{ catalog_file **f = &catalog;
|
||||
catalog_file *cf;
|
||||
|
||||
for (; *f; f = &(*f)->next)
|
||||
{ cf = *f;
|
||||
|
||||
if (istreq(cf->file, file))
|
||||
return TRUE; /* existing, move? */
|
||||
}
|
||||
|
||||
cf = sgml_malloc(sizeof(*cf));
|
||||
memset(cf, 0, sizeof(*cf));
|
||||
cf->file = istrdup(file);
|
||||
if (!cf->file)
|
||||
sgml_nomem();
|
||||
|
||||
if (where == CTL_END)
|
||||
{ cf->next = NULL;
|
||||
*f = cf;
|
||||
} else
|
||||
{ cf->next = catalog;
|
||||
catalog = cf;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
static wchar_t *
|
||||
wgetenv(const char *name)
|
||||
{ const char *vs;
|
||||
|
||||
if ( (vs = getenv(name)) )
|
||||
{ size_t wl = mbstowcs(NULL, vs, 0);
|
||||
|
||||
if ( wl > 0 )
|
||||
{ wchar_t *ws = sgml_malloc((wl+1)*sizeof(wchar_t));
|
||||
mbstowcs(ws, vs, wl+1);
|
||||
|
||||
return ws;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
init_catalog(void)
|
||||
{ static int done = FALSE;
|
||||
|
||||
LOCK();
|
||||
if ( !done++ )
|
||||
{ ichar *path = wgetenv("SGML_CATALOG_FILES");
|
||||
|
||||
if (!path)
|
||||
{ UNLOCK();
|
||||
return;
|
||||
}
|
||||
|
||||
while (*path)
|
||||
{ ichar buf[MAXPATHLEN];
|
||||
ichar *s;
|
||||
|
||||
if ((s = istrchr(path, L':')))
|
||||
{ istrncpy(buf, path, s - path);
|
||||
buf[s - path] = '\0';
|
||||
path = s + 1;
|
||||
if ( buf[0] ) /* skip empty entries */
|
||||
register_catalog_file_unlocked(buf, CTL_START);
|
||||
} else
|
||||
{ if ( path[0] ) /* skip empty entries */
|
||||
register_catalog_file_unlocked(path, CTL_START);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
UNLOCK();
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
register_catalog_file(const ichar *file, catalog_location where)
|
||||
{ int rc;
|
||||
|
||||
init_catalog();
|
||||
|
||||
LOCK();
|
||||
rc = register_catalog_file_unlocked(file, where);
|
||||
UNLOCK();
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/*******************************
|
||||
* CATALOG FILE PARSING *
|
||||
*******************************/
|
||||
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
The code from here to the end of this file was written by Richard
|
||||
O'Keefe and modified by Jan Wielemaker to fit in with the rest of the
|
||||
parser.
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* OVERRIDE YES/NO
|
||||
sets a boolean flag initialised to NO.
|
||||
The value of this flag is stored as part of each entry.
|
||||
(PUBLIC|DOCTYPE|ENTITY)&YES will match whether a system identifier
|
||||
was provided in the source document or not;
|
||||
(PUBLIC|DOCTYPE|ENTITY)&NO will only match if a system identifier
|
||||
was not provided.
|
||||
*/
|
||||
|
||||
/* catalogue =
|
||||
( PUBLIC pubid filename
|
||||
| SYSTEM sysid filename
|
||||
| DOCTYPE name filename
|
||||
| ENTITY name filename
|
||||
| OVERRIDE YES
|
||||
| OVERRIDE NO
|
||||
| BASE filename
|
||||
| junk
|
||||
)*
|
||||
*/
|
||||
|
||||
|
||||
/* Keywords are matched ignoring case. */
|
||||
|
||||
static int
|
||||
ci_streql(ichar const *a, ichar const *b)
|
||||
{ return istrcaseeq(a, b);
|
||||
}
|
||||
|
||||
/* Names may be matched heading case in XML. */
|
||||
|
||||
static int
|
||||
cs_streql(ichar const *a, ichar const *b)
|
||||
{ return istreq(a, b);
|
||||
}
|
||||
|
||||
/* Any other word or any quoted string is reported as CAT_OTHER.
|
||||
When we are not looking for the beginning of an entry, the only
|
||||
positive outcome is CAT_OTHER.
|
||||
*/
|
||||
|
||||
static int
|
||||
scan_overflow(size_t buflen)
|
||||
{ gripe(ERC_REPRESENTATION, L"token length");
|
||||
|
||||
return EOF;
|
||||
}
|
||||
|
||||
static int
|
||||
scan(FILE* src, ichar *buffer, size_t buflen, int kw_expected)
|
||||
{ int c, q;
|
||||
ichar *p = buffer, *e = p + buflen - 1;
|
||||
|
||||
for (;;)
|
||||
{ c = getc(src);
|
||||
if (c <= ' ')
|
||||
{ if (c < 0)
|
||||
return EOF;
|
||||
continue;
|
||||
}
|
||||
if (c == '-')
|
||||
{ c = getc(src);
|
||||
if (c != '-')
|
||||
{ *p++ = '-';
|
||||
break;
|
||||
}
|
||||
for (;;)
|
||||
{ c = getc(src);
|
||||
if (c < 0)
|
||||
return EOF;
|
||||
if (c == '-')
|
||||
{ c = getc(src);
|
||||
if (c < 0)
|
||||
return EOF;
|
||||
if (c == '-')
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '"' || c == '\'')
|
||||
{ q = c;
|
||||
for (;;)
|
||||
{ c = getc(src);
|
||||
if (c < 0)
|
||||
return EOF;
|
||||
if (c == q)
|
||||
{ *p = '\0';
|
||||
return CAT_OTHER;
|
||||
}
|
||||
if (p == e)
|
||||
return scan_overflow(buflen);
|
||||
*p++ = c;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* We reach here if there is an unquoted token. */
|
||||
/* Don't try "PUBLIC--well/sortof--'foo' 'bar'" */
|
||||
/* because hyphens are allowed in unquoted words */
|
||||
/* and so are slashes and a bunch of other stuff. */
|
||||
/* To keep this code simple, an unquoted token */
|
||||
/* ends at EOF, ', ", or layout. */
|
||||
while (c > ' ' && c != '"' && c != '\'')
|
||||
{ if (p == e)
|
||||
return scan_overflow(buflen);
|
||||
*p++ = c;
|
||||
c = getc(src);
|
||||
}
|
||||
*p = '\0';
|
||||
if (kw_expected)
|
||||
{ if (ci_streql(buffer, L"public"))
|
||||
return CAT_PUBLIC;
|
||||
if (ci_streql(buffer, L"system"))
|
||||
return CAT_SYSTEM;
|
||||
if (ci_streql(buffer, L"entity"))
|
||||
return CAT_ENTITY;
|
||||
if (ci_streql(buffer, L"doctype"))
|
||||
return CAT_DOCTYPE;
|
||||
if (ci_streql(buffer, L"override"))
|
||||
return CAT_OVERRIDE;
|
||||
if (ci_streql(buffer, L"base"))
|
||||
return CAT_BASE;
|
||||
}
|
||||
return CAT_OTHER;
|
||||
}
|
||||
|
||||
/* The strings can represent names (taken verbatim),
|
||||
system identifiers (ditto), or public identifiers (squished).
|
||||
We need to squish, and we need to copy. When it comes to
|
||||
squishing, we don't need to worry about Unicode spaces,
|
||||
because public identifiers aren't allow to have any characters
|
||||
that aren't in ASCII.
|
||||
*/
|
||||
|
||||
static void
|
||||
squish(ichar *pubid)
|
||||
{ ichar const *s = (ichar const *) pubid;
|
||||
ichar *d = (ichar *) pubid;
|
||||
ichar c;
|
||||
int w;
|
||||
|
||||
w = 1;
|
||||
while ((c = *s++) != '\0')
|
||||
{ if (c <= ' ')
|
||||
{ if (!w)
|
||||
*d++ = ' ', w = 1;
|
||||
} else
|
||||
{ *d++ = c, w = 0;
|
||||
}
|
||||
}
|
||||
if (w && d != (ichar *) pubid)
|
||||
d--;
|
||||
*d = '\0';
|
||||
}
|
||||
|
||||
/* We represent a catalogue internally by a list of
|
||||
(CAT_xxx, string, string)
|
||||
triples.
|
||||
*/
|
||||
|
||||
static void
|
||||
load_one_catalogue(catalog_file * file)
|
||||
{ FILE *src = wfopen(file->file, "r");
|
||||
ichar buffer[2 * FILENAME_MAX];
|
||||
ichar base[2 * FILENAME_MAX];
|
||||
ichar *p;
|
||||
int t;
|
||||
catalogue_item_ptr this_item;
|
||||
int override = 0;
|
||||
|
||||
if ( !src )
|
||||
{ gripe(ERC_NO_CATALOGUE, file->file);
|
||||
return;
|
||||
}
|
||||
|
||||
(void) istrcpy(base, file->file);
|
||||
p = base + istrlen(base);
|
||||
while (p != base && !isDirSep(p[-1]))
|
||||
p--;
|
||||
|
||||
for (;;)
|
||||
{ t = scan(src, buffer, sizeof(buffer), 1);
|
||||
switch (t)
|
||||
{ case CAT_BASE:
|
||||
if (scan(src, buffer, sizeof(buffer), 0) == EOF)
|
||||
break;
|
||||
(void) istrcpy(base, buffer);
|
||||
p = base + istrlen(base);
|
||||
if (p != base && !isDirSep(p[-1]))
|
||||
*p++ = '/';
|
||||
continue;
|
||||
case CAT_OVERRIDE:
|
||||
if (scan(src, buffer, sizeof(buffer), 0) == EOF)
|
||||
break;
|
||||
override = towlower(buffer[0]) == 'y' ? CAT_OVERRIDE : 0;
|
||||
continue;
|
||||
case CAT_PUBLIC:
|
||||
case CAT_SYSTEM:
|
||||
case CAT_ENTITY:
|
||||
case CAT_DOCTYPE:
|
||||
this_item = sgml_malloc(sizeof *this_item);
|
||||
if (scan(src, buffer, sizeof buffer, 0) == EOF)
|
||||
break;
|
||||
if (t == CAT_PUBLIC)
|
||||
squish(buffer);
|
||||
this_item->next = 0;
|
||||
this_item->kind = t == CAT_SYSTEM ? t : t + override;
|
||||
this_item->target = istrdup(buffer);
|
||||
|
||||
if (scan(src, buffer, sizeof buffer, 0) == EOF)
|
||||
break;
|
||||
|
||||
if (is_absolute_path(buffer) || p == base)
|
||||
{ this_item->replacement = istrdup(buffer);
|
||||
} else
|
||||
{ (void) istrcpy(p, buffer);
|
||||
this_item->replacement = istrdup(base);
|
||||
}
|
||||
|
||||
if (file->first_item == 0)
|
||||
{ file->first_item = this_item;
|
||||
} else
|
||||
{ file->last_item->next = this_item;
|
||||
}
|
||||
|
||||
file->last_item = this_item;
|
||||
continue;
|
||||
case EOF:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
fclose(src);
|
||||
}
|
||||
|
||||
|
||||
/* To look up a DTD:
|
||||
f = find_in_catalogue(CAT_DOCTYPE, name, pubid, sysid, ci);
|
||||
If it cannot otherwise be found and name is not null,
|
||||
${name}.dtd will be returned.
|
||||
|
||||
To look up a parameter entity:
|
||||
f = find_in_catalogue(CAT_PENTITY, name, pubid, sysid, ci);
|
||||
The name may begin with a % but need not; if it doesn't
|
||||
a % will be prefixed for the search.
|
||||
If it cannot otherwise be found ${name}.pen will be returned.
|
||||
|
||||
To look up an ordinary entity:
|
||||
f = find_in_catalogue(CAT_ENTITY, name, pubid, sysid, ci);
|
||||
If the name begins with a % this is just like a CAT_PENTITY search.
|
||||
If it cannot otherwise be found %{name}.ent will be returned.
|
||||
|
||||
The full catalogue format allows for NOTATION (which we still need
|
||||
for XML), SGMLDECL, DTDDECL, and LINKTYPE. At the moment, only
|
||||
notation is plausible. To handle such things,
|
||||
f = find_in_catalogue(CAT_OTHER, name, pubid, sysid, ci);
|
||||
If it cannot be found, NULL is returned.
|
||||
|
||||
The name, pubid, and sysid may each be NULL. It doesn't really
|
||||
make sense for them all to be NULL.
|
||||
|
||||
For SGML, name matching (DOCTYPE, ENTITY) should normally ignore
|
||||
alphabetic case. Pass ci=1 to make this happen. For XML, name
|
||||
matching must heed alphabetic case. Pass ci=0 to make that happen.
|
||||
|
||||
A CAT_DOCTYPE, CAT_ENTITY, or CAT_PENTITY search doesn't really make
|
||||
sense withint a name, so if the name should happen to be 0, the search
|
||||
kind is converted to CAT_OTHER.
|
||||
*/
|
||||
|
||||
ichar const *
|
||||
find_in_catalogue(int kind,
|
||||
ichar const *name,
|
||||
ichar const *pubid, ichar const *sysid, int ci)
|
||||
{ ichar penname[FILENAME_MAX];
|
||||
const size_t penlen = sizeof(penname)/sizeof(ichar);
|
||||
catalogue_item_ptr item;
|
||||
ichar const *result;
|
||||
catalog_file *catfile;
|
||||
|
||||
init_catalog();
|
||||
|
||||
if ( name == 0 )
|
||||
{ kind = CAT_OTHER;
|
||||
} else
|
||||
{ switch (kind)
|
||||
{ case CAT_OTHER:
|
||||
case CAT_DOCTYPE:
|
||||
break;
|
||||
case CAT_PENTITY:
|
||||
if (name[0] != '%')
|
||||
{ penname[0] = '%';
|
||||
(void) istrcpy(penname + 1, name);
|
||||
name = penname;
|
||||
}
|
||||
break;
|
||||
case CAT_ENTITY:
|
||||
if (name[0] == '%')
|
||||
{ kind = CAT_PENTITY;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
result = 0;
|
||||
for (catfile = catalog;; catfile = catfile->next)
|
||||
{ if (catfile)
|
||||
{ if (!catfile->loaded)
|
||||
{ load_one_catalogue(catfile);
|
||||
catfile->loaded = TRUE;
|
||||
}
|
||||
item = catfile->first_item;
|
||||
} else
|
||||
item = first_item;
|
||||
|
||||
for (; item != 0; item = item->next)
|
||||
{ switch (item->kind)
|
||||
{ case CAT_PUBLIC:
|
||||
if (sysid != 0)
|
||||
break;
|
||||
/*FALLTHROUGH*/
|
||||
case OVR_PUBLIC:
|
||||
if (pubid != 0 && result == 0 && cs_streql(pubid, item->target))
|
||||
result = item->replacement;
|
||||
break;
|
||||
case CAT_SYSTEM:
|
||||
if (sysid != 0 && cs_streql(sysid, item->target))
|
||||
return item->replacement;
|
||||
break;
|
||||
case CAT_DOCTYPE:
|
||||
if (sysid != 0)
|
||||
break;
|
||||
/*FALLTHROUGH*/
|
||||
case OVR_DOCTYPE:
|
||||
if (name != 0 && kind == CAT_DOCTYPE && result == 0
|
||||
&& (ci ? ci_streql : cs_streql) (name, item->target))
|
||||
result = item->replacement;
|
||||
break;
|
||||
case CAT_ENTITY:
|
||||
if (sysid != 0)
|
||||
break;
|
||||
/*FALLTHROUGH*/ case OVR_ENTITY:
|
||||
if (name != 0 && kind >= CAT_ENTITY && result == 0
|
||||
&& (ci ? ci_streql : cs_streql) (name, item->target))
|
||||
result = item->replacement;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!catfile)
|
||||
break;
|
||||
}
|
||||
if ( result != 0 )
|
||||
return result;
|
||||
if ( sysid != 0 )
|
||||
return sysid;
|
||||
if ( kind == CAT_OTHER || kind == CAT_DOCTYPE )
|
||||
return 0;
|
||||
|
||||
if ( istrlen(name)+4+1 > penlen )
|
||||
{ gripe(ERC_REPRESENTATION, L"entity name");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
item = sgml_malloc(sizeof(*item));
|
||||
item->next = 0;
|
||||
item->kind = kind;
|
||||
item->target = istrdup(name);
|
||||
|
||||
switch (kind)
|
||||
{ case CAT_DOCTYPE:
|
||||
(void) swprintf(penname, penlen, L"%ls.dtd", name);
|
||||
break;
|
||||
case CAT_PENTITY:
|
||||
item->kind = CAT_ENTITY;
|
||||
(void) swprintf(penname, penlen, L"%ls.pen", name + 1);
|
||||
break;
|
||||
case CAT_ENTITY:
|
||||
(void) swprintf(penname, penlen, L"%ls.ent", name);
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
}
|
||||
|
||||
item->replacement = istrdup(penname);
|
||||
if (first_item == 0)
|
||||
{ first_item = item;
|
||||
} else
|
||||
{ last_item->next = item;
|
||||
}
|
||||
last_item = item;
|
||||
|
||||
return item->replacement;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user