Skip to content

Commit 25d66dc

Browse files
committed
fixes to pattern matching and newlines on error messages
1 parent d7200ea commit 25d66dc

18 files changed

+5776
-552159
lines changed

SRC/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ mongoserver: INCLUDEDIRS=-Iinclude -I/usr/local/include/libbson-1.0 -I/usr/local
7777
mongoserver: CFLAGS=-c -std=c++11 -Wall -funsigned-char -Wno-write-strings -Wno-char-subscripts -Wno-strict-aliasing -g
7878

7979
executable.treetaggerserver=../BINARIES/ChatScriptTT
80-
treetaggerserver: DEFINES+= -DLOCKUSERFILE=1 -DEVSERVER=1 -DEVSERVER_FORK=1 -DDISCARDPOSTGRES=1 -DDISCARDMYSQL=1 -DDISCARDMONGO=1
80+
treetaggerserver: DEFINES+= -DLOCKUSERFILE=1 -DEVSERVER=1 -DEVSERVER_FORK=1 -DDISCARDPOSTGRES=1 -DDISCARDMYSQL=1 -DDISCARDMONGO=1 -DTREETAGGER=1
8181
treetaggerserver: PGLOAD= -pthread -L../BINARIES -ltreetagger
8282
treetaggerserver: binary
8383
treetaggerserver: EXECUTABLE=$(executable.treetaggerserver)

SRC/dictionarySystem.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ typedef unsigned int DICTINDEX; // indexed ref to a dictionary entry
304304
// unused 0x20000000
305305
// unused 0x40000000
306306
// unused 0x80000000
307-
307+
// DO NOT MOVE FLAGS AROUND, since user topic files assume them as they are
308308
// transient flags
309309
#define MARKED_FACT 0x08000000 // TRANSIENT : used during inferencing sometimes to see if fact is marked, also in user save to avoid repeated save
310310
#define ITERATOR_FACT MARKED_FACT // used by iterator

SRC/english.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ uint64 GetPosData( int at, char* original,WORDP& revise, WORDP &entry,WORDP &can
661661
if (!csEnglish)
662662
{
663663
if (!entry) entry = FindWord(original, 0, SECONDARY_CASE_ALLOWED); // Try harder to find the foreign word, e.g. German wochentag -> Wochentag
664-
if (entry) canonical = GetCanonical(entry);
664+
if (entry && !properties) canonical = GetCanonical(entry);
665665
}
666666
if (*original == '$' && !original[1]) { ; } // $
667667
else if (*original == '~' || (*original == USERVAR_PREFIX && !IsDigit(original[1])) || *original == '^' || (*original == SYSVAR_PREFIX && original[1]))
@@ -840,7 +840,7 @@ uint64 GetPosData( int at, char* original,WORDP& revise, WORDP &entry,WORDP &can
840840
if (participle && !strcmp(participle,original)) properties |= NOUN_ADJECTIVE;
841841
}
842842
WORDP canon = GetCanonical(entry);
843-
canonical = canon;
843+
if (canon) canonical = canon;
844844
if (canonical) cansysflags = canonical->systemFlags;
845845

846846
// possessive pronoun-determiner like my is always a determiner, not a pronoun.

SRC/outputSystem.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ char* GetCommandArg(char* ptr, char* buffer,FunctionResult& result,unsigned int
9494
if (!(control & ASSIGNMENT)) impliedSet = ALREADY_HANDLED;
9595
if (control == 0) control |= OUTPUT_KEEPSET | OUTPUT_ONCE | OUTPUT_NOCOMMANUMBER;
9696
else control |= OUTPUT_ONCE | OUTPUT_NOCOMMANUMBER;
97-
ptr = FreshOutput(ptr, buffer, result, control, MAX_WORD_SIZE);
97+
98+
unsigned int size = MAX_BUFFER_SIZE - (buffer - currentOutputBase); // how much used
99+
if (size > MAX_BUFFER_SIZE) size = MAX_WORD_SIZE * 4; // arbitrary assumption
100+
ptr = FreshOutput(ptr, buffer, result, control, size);
98101
if (!(control & ASSIGNMENT)) impliedSet = oldImpliedSet; // assignment of @0 = ^querytopics needs to be allowed to change to alreadyhandled
99102
return ptr;
100103
}

SRC/patternSystem.cpp

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@
1919

2020
#define INFINITE_MATCH (-(200 << 8)) // allowed to match anywhere
2121

22-
#define NOT_BIT 0X00010000
23-
#define FREEMODE_BIT 0X00020000
24-
#define QUOTE_BIT 0X00080000
22+
#define NOT_BIT 0X00010000
23+
#define FREEMODE_BIT 0X00020000
24+
#define QUOTE_BIT 0X00080000
25+
#define NOTNOT_BIT 0X00400000
2526
#define WILDGAP 0X20000000 // start of gap is 0x000000ff, limit of gap is 0x0000ff00
2627
#define WILDMEMORIZEGAP 0X40000000 // start of gap is 0x000000ff, limit of gap is 0x0000ff00
27-
#define WILDMEMORIZESPECIFIC 0X80000000 // while 0x1f0000 is wildcard index to use
28+
#define WILDMEMORIZESPECIFIC 0X80000000 // while 0x1f0000 is wildcard index to use
2829
#define GAP_SHIFT 16
2930
#define SPECIFIC_SHIFT 24
3031
#define GAPLIMITSHIFT 8
31-
#define NOTNOT_BIT 0X00400000
3232

3333
bool matching = false;
3434
bool clearUnmarks = false;
@@ -286,14 +286,14 @@ static bool FindPhrase(char* word, int start,bool reverse, int & actualStart, in
286286
return matched;
287287
}
288288

289-
char* PushMatch()
289+
static char* PushMatch(int used)
290290
{
291291
char* limit;
292292
char* base = InfiniteStack64(limit,"PushMatch");
293293
int* vals = (int*)base;
294-
for (int i = 0; i < MAX_WILDCARDS; ++i) *vals++ = wildcardPosition[i];
294+
for (int i = 0; i < used; ++i) *vals++ = wildcardPosition[i];
295295
char* rest = (char*) vals;
296-
for (int i = 0; i < MAX_WILDCARDS; ++i)
296+
for (int i = 0; i < used; ++i)
297297
{
298298
strcpy(rest, wildcardOriginalText[i]);
299299
rest += strlen(rest) + 1;
@@ -304,12 +304,12 @@ char* PushMatch()
304304
return base;
305305
}
306306

307-
void PopMatch(char* base)
307+
static void PopMatch(char* base,int used)
308308
{
309309
int* vals = (int*)base;
310-
for (int i = 0; i < MAX_WILDCARDS; ++i) wildcardPosition[i] = *vals++;
310+
for (int i = 0; i < used; ++i) wildcardPosition[i] = *vals++;
311311
char* rest = (char*)vals;
312-
for (int i = 0; i < MAX_WILDCARDS; ++i)
312+
for (int i = 0; i < used; ++i)
313313
{
314314
strcpy(wildcardOriginalText[i],rest);
315315
rest += strlen(rest) + 1;
@@ -318,13 +318,35 @@ void PopMatch(char* base)
318318
}
319319
ReleaseStack(base);
320320
}
321+
#ifdef INFORMATION
322+
323+
We keep a positional range reference within the sentence where we are(positionStart and positionEnd).
324+
Before we attempt the next match we make a backup copy(oldStart and oldEnd)
325+
so that if the match fails, we can revert back to where we were(under some circumstances).
326+
327+
We keep a variable firstMatched to track the first real word we have matched so far.
328+
If the whole match is declared a failure eventually, we may be allowed to go back and
329+
retry matching starting immediately after that location. That is, we do not do all possible backtracking
330+
as Prolog might, but we do a cheaper form where we simply try again farther in the sentence.
331+
Also, firstMatched is returned from a subcall, so the caller can know where to end a wildcard memorization
332+
started before the subcall.
333+
334+
Some tokens create a wildcard effect, where the next thing is allowed to be some distance away.
335+
This is tracked by wildcardSelector, and the token after the wildcard, when found, is checked to see
336+
if its position is allowed.When we enter a choice construct like[] and {}, when a choice fails,
337+
we reset the wildcardSelector back to originalWildcardSelector so the next choice sees the same environment.
338+
339+
In reverse mode, the range of positionStart and positionEnd continue to be earlier and later in the sentence,
340+
but validation treats positionStart as the basis of measuring distance.
341+
342+
Rebindable refers to ability to relocate firstmatched on failure (1 means we can shift from here, 3 means we enforce spacing and cannot rebind)
343+
Some operations like < or @_0+ force a specific position, and if no firstMatch has yet happened, then you cannot change
344+
the start location.
345+
346+
returnStart and returnEnd are the range of the match that happened when making a subcall.
347+
Startposition is where we start matching from.
348+
#endif
321349

322-
// NOTE: in reverse mode, positionStart is still earlier in the sentence than PositionEnd. We do not flip viewpoint.
323-
// rebindable refers to ability to relocate firstmatched on failure (1 means we can shift from here, 3 means we enforce spacing and cannot rebind)
324-
// returnStart and returnEnd are the range of the match that happened
325-
// Firstmatched is a real word (not wildcard) where we first bound a match (for rebinding restarts)
326-
// Startposition is where we start matching from
327-
// wildcardSelector is current wildcard hunting status
328350
bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char* kind, int rebindable,unsigned int wildcardSelector,
329351
int &returnstart,int& returnend,int &uppercasem,int& firstMatched,int positionStart,int positionEnd, bool reverse)
330352
{// always STARTS past initial opening thing ( [ { and ends with closing matching thing
@@ -350,7 +372,8 @@ bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char*
350372
int slidingStart = startposition;
351373
firstMatched = -1; // () should return spot it started (firstMatched) so caller has ability to bind any wild card before it
352374
if (rebindable == 1) slidingStart = positionStart = INFINITE_MATCH; // INFINITE_MATCH means we are in initial startup, allows us to match ANYWHERE forward to start
353-
positionEnd = startposition; // we scan starting 1 after this
375+
int originalWildcardSelector = wildcardSelector;
376+
positionEnd = startposition; // we scan starting 1 after this
354377
int basicStart = startposition; // we must not match real stuff any earlier than here
355378
char* argumentText = NULL; // pushed original text from a function arg -- function arg never decodes to name another function arg, we would have expanded it instead
356379
uppercaseFind = -1;
@@ -366,6 +389,7 @@ bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char*
366389
if (*word == '>' && word[1] == '>') ++nextTokenStart; // skip the 1st > of >> form
367390
nextTokenStart = SkipWhitespace(nextTokenStart+1); // ignore blanks after if token is a simple single thing like !
368391
char c = *word;
392+
bool foundaword = false;
369393
if (deeptrace) Log(STDTRACELOG,(char*)" token:%s ",word);
370394
switch(c)
371395
{
@@ -482,7 +506,7 @@ bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char*
482506
if (*end == '-')
483507
{
484508
reverse = true;
485-
positionEnd = positionStart = WILDCARD_START(wild);
509+
oldEnd = positionEnd = positionStart = WILDCARD_START(wild);
486510
}
487511
else // + and nothing both move forward.
488512
{
@@ -665,7 +689,7 @@ bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char*
665689
positionStart,positionEnd); // wildword match like st*m* or *team* matches steamroller
666690
else // variable gap
667691
{
668-
int start = (reverse) ? (positionStart - 1) : (positionEnd + 1);
692+
int start = (reverse) ? (positionStart-1) : (positionEnd+1);
669693
wildcardSelector |= start | WILDGAP; // cannot conflict, two wilds in a row change no position
670694
if (word[1] == '~')
671695
{
@@ -675,6 +699,9 @@ bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char*
675699
bidirectional = 1; // now aiming backwards
676700
priorPiece = ptr;
677701
reverse = !reverse; // run inverted first
702+
start = (reverse) ? (positionStart - 1) : (positionEnd + 1);
703+
wildcardSelector &= -1 ^ 0x000000ff;
704+
wildcardSelector |= start;
678705
bidirectionalSelector = wildcardSelector;
679706
bidirectionalWildcardIndex = wildcardIndex;
680707
}
@@ -712,13 +739,14 @@ bool Match(char* buffer,char* ptr, unsigned int depth, int startposition, char*
712739
if (!D || !(D->internalBits & FUNCTION_NAME)) matched = false; // shouldnt fail
713740
else if (D->x.codeIndex || D->internalBits & IS_OUTPUT_MACRO) // system function or output macro- execute it
714741
{
715-
char* base = PushMatch();
742+
char* base = PushMatch(wildcardIndex);
743+
int baseindex = wildcardIndex;
716744
AllocateOutputBuffer();
717745
FunctionResult result;
718746
if (!stricmp(D->word,"^match") || !stricmp(D->word, "^mark") || !stricmp(D->word, "^unmark")) matching = true;
719747
ptr = DoFunction(word,ptr,currentOutputBase,result);
720748
matching = false;
721-
PopMatch(base);
749+
PopMatch(base, baseindex);
722750
matched = !(result & ENDCODES);
723751

724752
// allowed to do comparisons on answers from system functions but cannot have space before them, but not from user macros
@@ -881,7 +909,14 @@ DOUBLELEFT: case '(': case '[': case '{': // nested condition (required or opt
881909
int localRebindable = 0; // not allowed to try rebinding start again by default
882910
if (positionStart == INFINITE_MATCH) localRebindable = 1; // we can move the start
883911
if (oldselect & WILDGAP) localRebindable = 2; // allowed to gap in
884-
matched = Match(buffer,ptr,depth+1,positionEnd,type, localRebindable,0,returnStart,
912+
int select = wildcardSelector;
913+
if (select & WILDMEMORIZEGAP) // dont memorize within, do it out here.
914+
{
915+
select ^= WILDMEMORIZEGAP;
916+
select |= WILDGAP;
917+
}
918+
else if (select & WILDMEMORIZESPECIFIC) select ^= WILDMEMORIZESPECIFIC;
919+
matched = Match(buffer,ptr,depth+1,positionEnd,type, localRebindable,select,returnStart,
885920
returnEnd,uppercasemat,whenmatched,positionStart,positionEnd,reverse); // subsection ok - it is allowed to set position vars, if ! get used, they dont matter because we fail
886921
wildcardSelector = oldselect; // restore outer environment
887922
if (matched)
@@ -1106,37 +1141,36 @@ DOUBLELEFT: case '(': case '[': case '{': // nested condition (required or opt
11061141
matchit:
11071142
matched = MatchTest(reverse,FindWord(word),(positionEnd < basicStart && firstMatched < 0) ? basicStart : positionEnd,NULL,NULL,
11081143
statusBits & QUOTE_BIT,positionStart,positionEnd,false);
1144+
11091145
if (!matched) uppercaseFind = -1;
1146+
else foundaword = true;
11101147
if (!(statusBits & NOT_BIT) && matched && firstMatched < 0) firstMatched = positionStart;
1111-
if (matched && !(statusBits & NOT_BIT))
1112-
{
1113-
MarkMatchLocation(positionStart, positionEnd,depth);
1114-
if (beginmatch == -1) beginmatch = positionStart; // first match in this level
1115-
}
11161148
}
11171149

11181150
statusBits &= -1 ^ QUOTE_BIT; // turn off any pending quote
1151+
bool inverted = false;
11191152
if (statusBits & NOT_BIT) // flip success to failure maybe
11201153
{
1154+
inverted = true;
11211155
if (matched)
11221156
{
11231157
if (statusBits & NOTNOT_BIT) // is match immediately after or not
11241158
{
11251159
if (!reverse && positionStart == (oldEnd + 1))
11261160
{
1127-
matched = 0;
1161+
matched = false;
11281162
uppercaseFind = -1;
11291163
}
11301164
else if (reverse && positionEnd == (oldStart - 1))
11311165
{
1132-
matched = 0;
1166+
matched = false;
11331167
uppercaseFind = -1;
11341168
}
11351169
}
11361170
else
11371171
{
1138-
uppercaseFind = -1;
11391172
matched = false;
1173+
uppercaseFind = -1;
11401174
}
11411175
statusBits &= -1 ^ (NOT_BIT|NOTNOT_BIT);
11421176
positionStart = oldStart; // restore any changed position values (if we succeed we would and if we fail it doesnt harm us)
@@ -1157,7 +1191,7 @@ DOUBLELEFT: case '(': case '[': case '{': // nested condition (required or opt
11571191
unsigned int memorizationStart = positionStart;
11581192
if ((wildcardSelector & WILDGAP) && matched) // test for legality of gap
11591193
{
1160-
unsigned int begin = started; // where we think we are now
1194+
unsigned int begin = started; // where we think we are now at match
11611195
memorizationStart = started = (wildcardSelector & 0x000000ff); // actual word we started at
11621196
unsigned int ignore = started;
11631197
int x;
@@ -1188,6 +1222,11 @@ DOUBLELEFT: case '(': case '[': case '{': // nested condition (required or opt
11881222
}
11891223
if (matched) // perform any memorization
11901224
{
1225+
if (matched && !inverted && foundaword) // we accept this positive match
1226+
{
1227+
MarkMatchLocation(positionStart, positionEnd, depth);
1228+
if (beginmatch == -1) beginmatch = positionStart; // first match in this level
1229+
}
11911230
if (oldEnd == positionEnd && oldStart == positionStart) // something like function call or variable existence, didnt change position
11921231
{
11931232
if (wildcardSelector == WILDMEMORIZESPECIFIC)
@@ -1214,7 +1253,7 @@ DOUBLELEFT: case '(': case '[': case '{': // nested condition (required or opt
12141253
if (reverse)
12151254
{
12161255
if ((started - positionStart) == 0) SetWildCardGivenValue((char*)"",(char*)"",0,positionEnd+1,index); // empty gap
1217-
else SetWildCardGiven(positionStart,started,true,index ); // wildcard legal swallow between elements
1256+
else SetWildCardGiven(positionStart +1, memorizationStart,true,index ); // wildcard legal swallow between elements
12181257
}
12191258
else if ((positionStart - memorizationStart) == 0) SetWildCardGivenValue((char*)"",(char*)"",0,oldEnd+1, index); // empty gap
12201259
else
@@ -1240,14 +1279,20 @@ DOUBLELEFT: case '(': case '[': case '{': // nested condition (required or opt
12401279
}
12411280
}
12421281
}
1243-
wildcardSelector = 0; // completes all memorization at this level
1282+
wildcardSelector = 0; // completes all memorization/gaps at this level
12441283
}
12451284
else // fix side effects of anything that failed to match by reverting
12461285
{
12471286
positionStart = oldStart;
12481287
positionEnd = oldEnd;
12491288
if (*kind == '(' || *kind == '<') wildcardSelector = 0; /// should NOT clear this inside a [] or a {} on failure since they must try again
1250-
}
1289+
else // [] and {} get alternate attemps so reset context
1290+
{
1291+
wildcardSelector = originalWildcardSelector;
1292+
beginmatch = -1;
1293+
firstMatched = -1;
1294+
}
1295+
}
12511296

12521297
// manage bidirectional failure or success
12531298
if (bidirectional)

SRC/testing.cpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8518,12 +8518,29 @@ static void C_TranslateConcept(char* input) // give the language & filename to w
85188518

85198519
MakeUpperCase(language);
85208520
char* into = NULL;
8521-
if (!stricmp(language,"GERMAN")) into = "de";
8521+
if (!stricmp(language,"ARABIC")) into = "ar";
8522+
else if (!stricmp(language,"BENGALI")) into = "bn";
8523+
else if (!stricmp(language,"CHINESE")) into = "zh";
8524+
else if (!stricmp(language,"CZECH")) into = "cs";
8525+
else if (!stricmp(language,"DUTCH")) into = "nl";
8526+
else if (!stricmp(language,"FINNISH")) into = "fi";
85228527
else if (!stricmp(language,"FRENCH")) into = "fr";
8528+
else if (!stricmp(language,"GERMAN")) into = "de";
8529+
else if (!stricmp(language,"GREEK")) into = "el";
8530+
else if (!stricmp(language,"HINDI")) into = "hi";
8531+
else if (!stricmp(language,"HUNGARIAN")) into = "hu";
85238532
else if (!stricmp(language,"ITALIAN")) into = "it";
8524-
else if (!stricmp(language,"SPANISH")) into = "es";
8533+
else if (!stricmp(language,"JAPANESE")) into = "ja";
8534+
else if (!stricmp(language,"KOREAN")) into = "ko";
8535+
else if (!stricmp(language,"MALAY")) into = "ms";
8536+
else if (!stricmp(language,"NORWEGIAN")) into = "no";
8537+
else if (!stricmp(language,"PUNJABI")) into = "pa";
8538+
else if (!stricmp(language,"POLISH")) into = "pl";
8539+
else if (!stricmp(language,"PORTUGUESE")) into = "pt";
85258540
else if (!stricmp(language,"RUSSIAN")) into = "ru";
8526-
else if (!stricmp(language,"HINDI")) into = "hi";
8541+
else if (!stricmp(language,"SPANISH")) into = "es";
8542+
else if (!stricmp(language,"SWEDISH")) into = "sv";
8543+
else if (!stricmp(language,"TELUGU")) into = "te";
85278544

85288545
FILE* in = FopenReadOnly(source);
85298546
if (!in)

SRC/textUtilities.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,7 +2288,7 @@ char* ReadQuote(char* ptr, char* buffer,bool backslash,bool noblank,int limit)
22882288
// d) literal quote \" - system outputs the quote only (script has nothing or blank or tab or ` after it
22892289
// e) internal "`xxxxx`" - argument to tcpopen pass back untouched stripping the markers on both ends - allows us to pay no attention to OTHER quotes within
22902290
char c;
2291-
int n = limit; // quote must close within this limit
2291+
int n = limit - 2; // quote must close within this limit
22922292
char* start = ptr;
22932293
char* original = buffer;
22942294
// "` is an internal marker of argument passed from TCPOPEN "'arguments'" ) , return the section untouched as one lump
@@ -2319,6 +2319,11 @@ char* ReadQuote(char* ptr, char* buffer,bool backslash,bool noblank,int limit)
23192319
*buffer++ = *++ptr;
23202320
}
23212321
else *buffer++ = c;
2322+
if ((buffer - original) > limit) // overflowing
2323+
{
2324+
c = 0;
2325+
break;
2326+
}
23222327
}
23232328
if (n == 0 || !c) // ran dry instead of finding the end
23242329
{
@@ -2331,7 +2336,8 @@ char* ReadQuote(char* ptr, char* buffer,bool backslash,bool noblank,int limit)
23312336
*buffer = 0;
23322337
return ptr;
23332338
}
2334-
if (!n) Log(STDTRACELOG,(char*)"bad double-quoting? %s %d %s - size is %d but limit is %d\r\n",start,currentFileLine,currentFilename,buffer-start,MAX_WORD_SIZE);
2339+
*buffer = 0;
2340+
if (!n) Log(STDTRACELOG,(char*)"bad double-quoting? %s %d %s - string size exceeds limit of %d\r\n",start,currentFileLine,currentFilename,limit);
23352341
else Log(STDTRACELOG,(char*)"bad double-quoting1? %s %d %s missing tail doublequote \r\n",start,currentFileLine,currentFilename);
23362342
return NULL; // no closing quote... refuse
23372343
}
@@ -2967,7 +2973,7 @@ int64 Convert2Integer(char* number, int useNumberStyle) // non numbers return
29672973
}
29682974
if (hyphen && num != -1) // do last piece
29692975
{
2970-
num *= 10;
2976+
if ((num % 10) > 0) num *= 10; // don't multiply if twenty-three
29712977
val1 = Convert2Integer(oldhyphen, useNumberStyle);
29722978
if (val1 > 9) num = -1;
29732979
else num += val1;

0 commit comments

Comments
 (0)