ObjFW  Check-in [9d79d92d9a]

Overview
Comment:Several OFXMLParser improvements. See details.

* found* renamed to didFind*.
* Support for parsing CDATA.
* Handle comments with - correctly.
* Don't strip leading and trailing whitespaces in comments.
* Rename a few states for more clarity.
* More OFXMLParser tests.

Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 9d79d92d9a61f5966ec67d4944564091b305abda4f89072520d65388c08bfff4
User & Date: js on 2010-06-12 19:47:43
Other Links: manifest | tags
Context
2010-06-12
20:47
Move a few classes and categories so they have their own files. check-in: 67917b918a user: js tags: trunk
19:47
Several OFXMLParser improvements. See details. check-in: 9d79d92d9a user: js tags: trunk
2010-06-06
23:31
Fix a warning with Apple GCC 4.0.1. check-in: 889f63ab01 user: js tags: trunk
Changes

Modified src/OFXMLParser.h from [d8f4c394d4] to [9635aaee4f].

48
49
50
51
52
53
54



55
56
57
58
59
60

61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78
79
80
81
82


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

100
101
102
103
104
105
106
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
79
80
81
82
83


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109







+
+
+





-
+








-
+











-
-
+
+
















-
+







-   (void)xmlParser: (OFXMLParser*)parser
  didEndTagWithName: (OFString*)name
	     prefix: (OFString*)prefix
	  namespace: (OFString*)ns;

/**
 * This callback is called when the XML parser found a string.
 *
 * In case there are comments or CDATA, it is possible that this callback is
 * called multiple times in a row.
 *
 * \param parser The parser which found a string
 * \param string The string the XML parser found
 */
- (void)xmlParser: (OFXMLParser*)parser
      foundString: (OFString*)string;
    didFindString: (OFString*)string;

/**
 * This callback is called when the XML parser found a comment.
 *
 * \param parser The parser which found a comment
 * \param comment The comment the XML parser found
 */
- (void)xmlParser: (OFXMLParser*)parser
     foundComment: (OFString*)comment;
   didFindComment: (OFString*)comment;

/**
 * This callback is called when the XML parser found an entity it doesn't know.
 * The callback is supposed to return a substitution for the entity or nil if
 * it is not known to the callback as well, in which case an exception will be
 * risen.
 *
 * \param parser The parser which found an unknown entity
 * \param entity The name of the entity the XML parser didn't know
 * \return A substitution for the entity or nil
 */
-    (OFString*)xmlParser: (OFXMLParser*)parser
  foundUnknownEntityNamed: (OFString*)entity;
-      (OFString*)xmlParser: (OFXMLParser*)parser
  didFindUnknownEntityNamed: (OFString*)entity;
@end

/**
 * \brief A protocol that needs to be implemented by delegates for
 *	  -[stringByXMLUnescapingWithHandler:].
 */
@protocol OFXMLUnescapingDelegate
/**
 * This callback is called when an unknown entity was found while trying to
 * unescape XML. The callback is supposed to return a substitution for the
 * entity or nil if it is unknown to the callback as well, in which case an
 * exception will be thrown.
 *
 * \param entity The name of the entity that is unknown
 * \return A substitution for the entity or nil
 */
- (OFString*)foundUnknownEntityNamed: (OFString*)entity;
- (OFString*)didFindUnknownEntityNamed: (OFString*)entity;
@end

/**
 * \brief An event-based XML parser.
 *
 * OFXMLParser is an event-based XML parser which calls the delegate's callbacks
 * as soon asit finds something, thus suitable for streams as well.
115
116
117
118
119
120
121







122
123
124
125







126
127
128
129
130
131
132
118
119
120
121
122
123
124
125
126
127
128
129
130
131




132
133
134
135
136
137
138
139
140
141
142
143
144
145







+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+







		OF_XMLPARSER_IN_CLOSE_TAG_NAME,
		OF_XMLPARSER_IN_TAG,
		OF_XMLPARSER_IN_ATTR_NAME,
		OF_XMLPARSER_EXPECT_DELIM,
		OF_XMLPARSER_IN_ATTR_VALUE,
		OF_XMLPARSER_EXPECT_CLOSE,
		OF_XMLPARSER_EXPECT_SPACE_OR_CLOSE,
		OF_XMLPARSER_IN_CDATA_OR_COMMENT,
		OF_XMLPARSER_IN_CDATA_OPENING_1,
		OF_XMLPARSER_IN_CDATA_OPENING_2,
		OF_XMLPARSER_IN_CDATA_OPENING_3,
		OF_XMLPARSER_IN_CDATA_OPENING_4,
		OF_XMLPARSER_IN_CDATA_OPENING_5,
		OF_XMLPARSER_IN_CDATA_OPENING_6,
		OF_XMLPARSER_IN_COMMENT_1,
		OF_XMLPARSER_IN_COMMENT_2,
		OF_XMLPARSER_IN_COMMENT_3,
		OF_XMLPARSER_IN_COMMENT_4
		OF_XMLPARSER_IN_CDATA_1,
		OF_XMLPARSER_IN_CDATA_2,
		OF_XMLPARSER_IN_CDATA_3,
		OF_XMLPARSER_IN_COMMENT_OPENING,
		OF_XMLPARSER_IN_COMMENT_1,
		OF_XMLPARSER_IN_COMMENT_2,
		OF_XMLPARSER_IN_COMMENT_3
	} state;
	OFMutableString *cache;
	OFString *name;
	OFString *prefix;
	OFMutableArray *namespaces;
	OFMutableArray *attrs;
	OFString *attrName;

Modified src/OFXMLParser.m from [04405d22e4] to [379d043fd1].

184
185
186
187
188
189
190
191

192
193
194
195
196
197
198
199
200
201
202
203
204
205
206

207

208
209
210
211
212
213
214
215
216
184
185
186
187
188
189
190

191
192
193
194
195
196
197
198
199
200
201
202
203
204

205
206
207
208
209

210
211
212
213
214
215
216







-
+













-

+

+

-








				if ([cache cStringLength] > 0) {
					OFString *str;

					pool = [[OFAutoreleasePool alloc] init];
					str = transform_string(cache, self);
					[delegate xmlParser: self
						foundString: str];
					      didFindString: str];
					[pool release];
				}

				[cache setToCString: ""];

				last = i + 1;
				state = OF_XMLPARSER_TAG_OPENED;
			}
			break;

		/* Tag was just opened */
		case OF_XMLPARSER_TAG_OPENED:
			if (buf[i] == '/') {
				last = i + 1;
				state = OF_XMLPARSER_IN_CLOSE_TAG_NAME;
				last = i + 1;
			} else if(buf[i] == '!') {
				state = OF_XMLPARSER_IN_CDATA_OR_COMMENT;
				last = i + 1;
				state = OF_XMLPARSER_IN_COMMENT_1;
			} else {
				state = OF_XMLPARSER_IN_TAG_NAME;
				i--;
			}
			break;

		/* Inside a tag, no name yet */
525
526
527
528
529
530
531
532
533
534
535





































































































536
537
538
539
540

541
542

543
544

545
546
547






548
549
550
551
552
553
554
555
556
557
558
559
560

561
562
563
564

565
566
567
568
569
570

571
572


573
574
575
576
577
578
579
580
581
582
583
584
585

586
587
588

589
590
591
592
593
594
595
525
526
527
528
529
530
531
532



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637

638
639

640
641

642
643


644
645
646
647
648
649
650
651
652
653
654
655
656
657

658
659
660

661
662

663

664
665
666
667
668
669

670
671

672
673
674
675
676
677
678
679
680
681
682
683
684
685

686
687
688

689
690
691
692
693
694
695
696








-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+

-
+

-
+

-
-
+
+
+
+
+
+








-



-
+

-

-
+





-
+

-
+
+












-
+


-
+







				last = i + 1;
				state = OF_XMLPARSER_OUTSIDE_TAG;
			} else if (buf[i] != ' ' && buf[i] != '\n' &&
			    buf[i] != '\r')
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			break;

		/* Comment */
		case OF_XMLPARSER_IN_COMMENT_1:
		case OF_XMLPARSER_IN_COMMENT_2:
		/* CDATA or comment */
		case OF_XMLPARSER_IN_CDATA_OR_COMMENT:
			if (buf[i] == '-')
				state = OF_XMLPARSER_IN_COMMENT_OPENING;
			else if (buf[i] == '[')
				state = OF_XMLPARSER_IN_CDATA_OPENING_1;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];

			last = i + 1;
			break;

		/* CDATA */
		case OF_XMLPARSER_IN_CDATA_OPENING_1:
			if (buf[i] == 'C')
				state = OF_XMLPARSER_IN_CDATA_OPENING_2;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			break;
		case OF_XMLPARSER_IN_CDATA_OPENING_2:
			if (buf[i] == 'D')
				state = OF_XMLPARSER_IN_CDATA_OPENING_3;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			break;
		case OF_XMLPARSER_IN_CDATA_OPENING_3:
			if (buf[i] == 'A')
				state = OF_XMLPARSER_IN_CDATA_OPENING_4;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			break;
		case OF_XMLPARSER_IN_CDATA_OPENING_4:
			if (buf[i] == 'T')
				state = OF_XMLPARSER_IN_CDATA_OPENING_5;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			break;
		case OF_XMLPARSER_IN_CDATA_OPENING_5:
			if (buf[i] == 'A')
				state = OF_XMLPARSER_IN_CDATA_OPENING_6;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			break;
		case OF_XMLPARSER_IN_CDATA_OPENING_6:
			if (buf[i] == '[')
				state = OF_XMLPARSER_IN_CDATA_1;
			else
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			break;
		case OF_XMLPARSER_IN_CDATA_1:
			if (buf[i] == ']')
				state = OF_XMLPARSER_IN_CDATA_2;
			break;
		case OF_XMLPARSER_IN_CDATA_2:
			if (buf[i] == ']')
				state = OF_XMLPARSER_IN_CDATA_3;
			else
				state = OF_XMLPARSER_IN_CDATA_1;
			break;
		case OF_XMLPARSER_IN_CDATA_3:
			if (buf[i] == '>') {
				OFMutableString *cdata;
				size_t len;

				pool = [[OFAutoreleasePool alloc] init];

				[cache
				    appendCStringWithoutUTF8Checking: buf + last
							      length: i - last];
				cdata = [[cache mutableCopy] autorelease];
				len = [cdata length];

				[cdata removeCharactersFromIndex: len - 2
							 toIndex: len];
				[delegate xmlParser: self
				      didFindString: cdata];
				[pool release];

				[cache setToCString: ""];

				last = i + 1;
				state = OF_XMLPARSER_OUTSIDE_TAG;
			} else if (buf[i] != ']')
				state = OF_XMLPARSER_IN_CDATA_1;
			break;

		/* Comment */
		case OF_XMLPARSER_IN_COMMENT_OPENING:
			if (buf[i] != '-')
				@throw [OFMalformedXMLException
				    newWithClass: isa];
			last = i + 1;
			state++;
			state = OF_XMLPARSER_IN_COMMENT_1;
			break;
		case OF_XMLPARSER_IN_COMMENT_3:
		case OF_XMLPARSER_IN_COMMENT_1:
			if (buf[i] == '-')
				state = OF_XMLPARSER_IN_COMMENT_4;
				state = OF_XMLPARSER_IN_COMMENT_2;
			break;
		case OF_XMLPARSER_IN_COMMENT_4:
			if (buf[i] == '-') {
		case OF_XMLPARSER_IN_COMMENT_2:
			state = (buf[i] == '-' ? OF_XMLPARSER_IN_COMMENT_3 :
			    OF_XMLPARSER_IN_COMMENT_1);
			break;
		case OF_XMLPARSER_IN_COMMENT_3:
			if (buf[i] == '>') {
				OFMutableString *comment;
				size_t len;

				pool = [[OFAutoreleasePool alloc] init];

				[cache
				    appendCStringWithoutUTF8Checking: buf + last
							      length: i - last];

				comment = [[cache mutableCopy] autorelease];
				len = [comment length];

				[comment removeCharactersFromIndex: len - 1
				[comment removeCharactersFromIndex: len - 2
							   toIndex: len];
				[comment removeLeadingAndTrailingWhitespaces];
				[delegate xmlParser: self
				       foundComment: comment];
				     didFindComment: comment];
				[pool release];

				[cache setToCString: ""];

				last = i + 1;
				state = OF_XMLPARSER_EXPECT_CLOSE;
				state = OF_XMLPARSER_OUTSIDE_TAG;
			} else
				state = OF_XMLPARSER_IN_COMMENT_3;
				@throw [OFMalformedXMLException
				    newWithClass: isa];

			break;
		}
	}

	len = size - last;
	/* In OF_XMLPARSER_IN_TAG, there can be only spaces */
	if (len > 0 && state != OF_XMLPARSER_IN_TAG)
		[cache appendCStringWithoutUTF8Checking: buf + last
						 length: len];
}

- (OFString*)foundUnknownEntityNamed: (OFString*)entity
- (OFString*)didFindUnknownEntityNamed: (OFString*)entity
{
	return [delegate xmlParser: self
	   foundUnknownEntityNamed: entity];
	 didFindUnknownEntityNamed: entity];
}
@end

@implementation OFString (OFXMLUnescaping)
- (OFString*)stringByXMLUnescaping
{
	return [self stringByXMLUnescapingWithHandler: nil];
657
658
659
660
661
662
663
664

665
666
667
668
669
670
671
758
759
760
761
762
763
764

765
766
767
768
769
770
771
772







-
+







				OFAutoreleasePool *pool;
				OFString *n, *tmp;

				pool = [[OFAutoreleasePool alloc] init];

				n = [OFString stringWithCString: entity
							 length: len];
				tmp = [h foundUnknownEntityNamed: n];
				tmp = [h didFindUnknownEntityNamed: n];

				if (tmp == nil)
					@throw [OFInvalidEncodingException
					    newWithClass: isa];

				[ret appendString: tmp];
				[pool release];
701
702
703
704
705
706
707
708

709
710
711
712
713

714
715
716
717
718


719
720
721
722
802
803
804
805
806
807
808

809
810
811
812
813

814
815
816
817


818
819
820
821
822
823







-
+




-
+



-
-
+
+




  didEndTagWithName: (OFString*)name
	     prefix: (OFString*)prefix
	  namespace: (OFString*)ns
{
}

- (void)xmlParser: (OFXMLParser*)parser
      foundString: (OFString*)string
    didFindString: (OFString*)string
{
}

- (void)xmlParser: (OFXMLParser*)parser
     foundComment: (OFString*)comment
   didFindComment: (OFString*)comment
{
}

-    (OFString*)xmlParser: (OFXMLParser*)parser
  foundUnknownEntityNamed: (OFString*)entity
-      (OFString*)xmlParser: (OFXMLParser*)parser
  didFindUnknownEntityNamed: (OFString*)entity
{
	return nil;
}
@end

Modified tests/OFStringTests.m from [de363dd9e3] to [b8a81af19d].

24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38







-
+







	@" \t\t  \t\t  \t \t"
};

@interface EntityHandler: OFObject <OFXMLUnescapingDelegate>
@end

@implementation EntityHandler
- (OFString*)foundUnknownEntityNamed: (OFString*)entity
- (OFString*)didFindUnknownEntityNamed: (OFString*)entity
{
	if ([entity isEqual: @"foo"])
		return @"bar";

	return nil;
}
@end

Modified tests/OFXMLParserTests.m from [24b4ef9bb7] to [ff036615fe].

44
45
46
47
48
49
50




51

52
53




54
55
56
57

58
59
60
61
62


63
64

65
66
67
68
69
70
71
72
73
74
75


76
77

78
79
80
81
82
83
84
85
86
87
88
89


90
91

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108


109
110

111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

126
127
128
129
130


131
132

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

154
155
156
157
158
159


160
161

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181

182
183
184
185
186


187
188

189
190
191
192
193
194


195
196
197


198
199
200


201
202

203
204
205
206
207


208
209

210
211
212







213
214
215
216
217
218
219
44
45
46
47
48
49
50
51
52
53
54

55
56

57
58
59
60
61
62
63

64
65
66
67


68
69
70

71
72
73
74
75
76
77
78
79
80


81
82
83

84
85
86
87
88
89
90
91
92
93
94


95
96
97

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113


114
115
116

117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

132
133
134
135


136
137
138

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

160
161
162
163
164


165
166
167

168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187

188
189
190
191


192
193
194

195
196
197
198
199


200
201
202


203
204
205


206
207
208

209
210
211
212


213
214
215

216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233







+
+
+
+
-
+

-
+
+
+
+



-
+



-
-
+
+

-
+









-
-
+
+

-
+










-
-
+
+

-
+















-
-
+
+

-
+














-
+



-
-
+
+

-
+




















-
+




-
-
+
+

-
+



















-
+



-
-
+
+

-
+




-
-
+
+

-
-
+
+

-
-
+
+

-
+



-
-
+
+

-
+



+
+
+
+
+
+
+







	OFString *msg;

	i++;
	msg = [OFString stringWithFormat: @"Parsing part #%d", i];

	switch (i) {
	case 1:
		TEST(msg, et == TAG_START && [name isEqual: @"root"] &&
		    prefix == nil && ns == nil && [attrs count] == 0)
		break;
	case 2:
		TEST(msg, et == STRING && [string isEqual: @"foo"])
		TEST(msg, et == STRING && [string isEqual: @"\n "])
		break;
	case 2:
	case 3:
		TEST(msg, et == STRING && [string isEqual: @"f<oo"])
		break;
	case 4:
		TEST(msg, et == TAG_START && [name isEqual: @"bar"] &&
		    prefix == nil && ns == nil && attrs == nil)
		break;
	case 3:
	case 5:
		TEST(msg, et == TAG_END && [name isEqual: @"bar"] &&
		    prefix == nil && ns == nil && attrs == nil)
		break;
	case 4:
		TEST(msg, et == STRING && [string isEqual: @"\n"])
	case 6:
		TEST(msg, et == STRING && [string isEqual: @"\n "])
		break;
	case 5:
	case 7:
		TEST(msg, et == TAG_START && [name isEqual: @"foobar"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:foobar"] &&
		    [attrs count] == 1 &&
		    /* xmlns attr */
		    [[[attrs objectAtIndex: 0] name] isEqual: @"xmlns"] &&
		    [[attrs objectAtIndex: 0] namespace] == nil &&
		    [[[attrs objectAtIndex: 0] stringValue] isEqual:
		    @"urn:objfw:test:foobar"])
		break;
	case 6:
		TEST(msg, et == STRING && [string isEqual: @"\n "])
	case 8:
		TEST(msg, et == STRING && [string isEqual: @"\n  "])
		break;
	case 7:
	case 9:
		TEST(msg, et == TAG_START && [name isEqual: @"qux"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:foobar"] &&
		    [attrs count] == 1 &&
		    /* xmlns:foo attr */
		    [[[attrs objectAtIndex: 0] name] isEqual: @"foo"] &&
		    [[[attrs objectAtIndex: 0] namespace] isEqual:
		    @"http://www.w3.org/2000/xmlns/"] &&
		    [[[attrs objectAtIndex: 0] stringValue] isEqual:
		    @"urn:objfw:test:foo"])
		break;
	case 8:
		TEST(msg, et == STRING && [string isEqual: @"\n  "])
	case 10:
		TEST(msg, et == STRING && [string isEqual: @"\n   "])
		break;
	case 9:
	case 11:
		TEST(msg, et == TAG_START && [name isEqual: @"bla"] &&
		    [prefix isEqual: @"foo"] &&
		    [ns isEqual: @"urn:objfw:test:foo"] &&
		    [attrs count] == 2 &&
		    /* foo:bla attr */
		    [[[attrs objectAtIndex: 0] name] isEqual: @"bla"] &&
		    [[[attrs objectAtIndex: 0] namespace] isEqual:
		    @"urn:objfw:test:foo"] &&
		    [[[attrs objectAtIndex: 0] stringValue] isEqual: @"bla"] &&
		    /* blafoo attr */
		    [[[attrs objectAtIndex: 1] name] isEqual: @"blafoo"] &&
		    [[[attrs objectAtIndex: 1] namespace] isEqual:
		    @"urn:objfw:test:foo"] &&
		    [[[attrs objectAtIndex: 1] stringValue] isEqual: @"foo"])
		break;
	case 10:
		TEST(msg, et == STRING && [string isEqual: @"\n   "])
	case 12:
		TEST(msg, et == STRING && [string isEqual: @"\n    "])
		break;
	case 11:
	case 13:
		TEST(msg, et == TAG_START && [name isEqual: @"blup"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:foobar"] &&
		    [attrs count] == 2 &&
		    /* foo:qux attr */
		    [[[attrs objectAtIndex: 0] name] isEqual: @"qux"] &&
		    [[[attrs objectAtIndex: 0] namespace] isEqual:
		    @"urn:objfw:test:foo"] &&
		    [[[attrs objectAtIndex: 0] stringValue] isEqual: @"asd"] &&
		    /* quxqux attr */
		    [[[attrs objectAtIndex: 1] name] isEqual: @"quxqux"] &&
		    [[[attrs objectAtIndex: 1] namespace] isEqual:
		    @"urn:objfw:test:foobar"] &&
		    [[[attrs objectAtIndex: 1] stringValue] isEqual: @"test"])
		break;
	case 12:
	case 14:
		TEST(msg, et == TAG_END && [name isEqual: @"blup"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:foobar"])
		break;
	case 13:
		TEST(msg, et == STRING && [string isEqual: @"\n   "])
	case 15:
		TEST(msg, et == STRING && [string isEqual: @"\n    "])
		break;
	case 14:
	case 16:
		TEST(msg, et == TAG_START && [name isEqual: @"bla"] &&
		    [prefix isEqual: @"bla"] &&
		    [ns isEqual: @"urn:objfw:test:bla"] && [attrs count] == 3 &&
		    /* xmlns:bla attr */
		    [[[attrs objectAtIndex: 0] name] isEqual: @"bla"] &&
		    [[[attrs objectAtIndex: 0] namespace] isEqual:
		    @"http://www.w3.org/2000/xmlns/"] &&
		    [[[attrs objectAtIndex: 0] stringValue] isEqual:
		    @"urn:objfw:test:bla"] &&
		    /* qux attr */
		    [[[attrs objectAtIndex: 1] name] isEqual: @"qux"] &&
		    [[[attrs objectAtIndex: 1] namespace] isEqual:
		    @"urn:objfw:test:bla"] &&
		    [[[attrs objectAtIndex: 1] stringValue] isEqual: @"qux"] &&
		    /* bla:foo attr */
		    [[[attrs objectAtIndex: 2] name] isEqual: @"foo"] &&
		    [[[attrs objectAtIndex: 2] namespace] isEqual:
		    @"urn:objfw:test:bla"] &&
		    [[[attrs objectAtIndex: 2] stringValue] isEqual: @"blafoo"])
		break;
	case 15:
	case 17:
		TEST(msg, et == TAG_END && [name isEqual: @"bla"] &&
		    [prefix isEqual: @"bla"] &&
		    [ns isEqual: @"urn:objfw:test:bla"])
		break;
	case 16:
		TEST(msg, et == STRING && [string isEqual: @"\n   "])
	case 18:
		TEST(msg, et == STRING && [string isEqual: @"\n    "])
		break;
	case 17:
	case 19:
		TEST(msg, et == TAG_START && [name isEqual: @"abc"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:abc"] &&
		    [attrs count] == 3 &&
		    /* xmlns attr */
		    [[[attrs objectAtIndex: 0] name] isEqual: @"xmlns"] &&
		    [[attrs objectAtIndex: 0] namespace] == nil &&
		    [[[attrs objectAtIndex: 0] stringValue] isEqual:
		    @"urn:objfw:test:abc"] &&
		    /* abc attr */
		    [[[attrs objectAtIndex: 1] name] isEqual: @"abc"] &&
		    [[[attrs objectAtIndex: 1] namespace] isEqual:
		    @"urn:objfw:test:abc"] &&
		    [[[attrs objectAtIndex: 1] stringValue] isEqual: @"abc"] &&
		    /* foo:abc attr */
		    [[[attrs objectAtIndex: 2] name] isEqual: @"abc"] &&
		    [[[attrs objectAtIndex: 2] namespace] isEqual:
		    @"urn:objfw:test:foo"] &&
		    [[[attrs objectAtIndex: 2] stringValue] isEqual: @"abc"])
		break;
	case 18:
	case 20:
		TEST(msg, et == TAG_END && [name isEqual: @"abc"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:abc"])
		break;
	case 19:
		TEST(msg, et == STRING && [string isEqual: @"\n  "])
	case 21:
		TEST(msg, et == STRING && [string isEqual: @"\n   "])
		break;
	case 20:
	case 22:
		TEST(msg, et == TAG_END && [name isEqual: @"bla"] &&
		    [prefix isEqual: @"foo"] &&
		    [ns isEqual: @"urn:objfw:test:foo"])
		break;
	case 21:
		TEST(msg, et == STRING && [string isEqual: @"\n  "])
	case 23:
		TEST(msg, et == STRING && [string isEqual: @"\n   "])
		break;
	case 22:
		TEST(msg, et == COMMENT && [comment isEqual: @"commänt"])
	case 24:
		TEST(msg, et == COMMENT && [comment isEqual: @" commänt "])
		break;
	case 23:
		TEST(msg, et == STRING && [string isEqual: @"\n "])
	case 25:
		TEST(msg, et == STRING && [string isEqual: @"\n  "])
		break;
	case 24:
	case 26:
		TEST(msg, et == TAG_END && [name isEqual: @"qux"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:foobar"])
		break;
	case 25:
		TEST(msg, et == STRING && [string isEqual: @"\n"])
	case 27:
		TEST(msg, et == STRING && [string isEqual: @"\n "])
		break;
	case 26:
	case 28:
		TEST(msg, et == TAG_END && [name isEqual: @"foobar"] &&
		    prefix == nil && [ns isEqual: @"urn:objfw:test:foobar"])
		break;
	case 29:
		TEST(msg, et == STRING && [string isEqual: @"\n"])
		break;
	case 30:
		TEST(msg, et == TAG_END && [name isEqual: @"root"] &&
		    prefix == nil && ns == nil);
		break;
	}
}

-     (void)xmlParser: (OFXMLParser*)parser
  didStartTagWithName: (OFString*)name
	       prefix: (OFString*)prefix
	    namespace: (OFString*)ns
239
240
241
242
243
244
245
246

247
248
249
250
251
252
253
254
255
256
257
258

259
260
261
262
263
264
265
266
267
268
269
270


271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293














294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312

313
314
315
316
253
254
255
256
257
258
259

260
261
262
263
264
265
266
267
268
269
270
271

272
273
274
275
276
277
278
279
280
281
282


283
284
285
286
287
288
289
290
291
292
293
294
295












296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327

328
329
330
331
332







-
+











-
+










-
-
+
+











-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+


















-
+




				   namespace: ns
				  attributes: nil
				      string: nil
				     comment: nil];
}

- (void)xmlParser: (OFXMLParser*)parser
      foundString: (OFString*)string
    didFindString: (OFString*)string
{
	[self xmlParserCallbackWithEventType: STRING
					name: nil
				      prefix: nil
				   namespace: nil
				  attributes: nil
				      string: string
				     comment: nil];
}

- (void)xmlParser: (OFXMLParser*)parser
     foundComment: (OFString*)comment
   didFindComment: (OFString*)comment
{
	[self xmlParserCallbackWithEventType: COMMENT
					name: nil
				      prefix: nil
				   namespace: nil
				  attributes: nil
				      string: nil
				     comment: comment];
}

-    (OFString*)xmlParser: (OFXMLParser*)parser
  foundUnknownEntityNamed: (OFString*)entity
-      (OFString*)xmlParser: (OFXMLParser*)parser
  didFindUnknownEntityNamed: (OFString*)entity
{
	if ([entity isEqual: @"foo"])
		return @"foobar";

	return nil;
}

- (void)XMLParserTests
{
	OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init];
	OFXMLParser *parser;
	const char *str = "foo<bar/>\n"
	    "<foobar xmlns='urn:objfw:test:foobar'>\n"
	    " <qux xmlns:foo='urn:objfw:test:foo'>\n"
	    "  <foo:bla foo:bla='&#x62;&#x6c;&#x61;' blafoo='foo'>\n"
	    "   <blup foo:qux='asd' quxqux='test'/>\n"
	    "   <bla:bla\r\rxmlns:bla=\"urn:objfw:test:bla\" qux='qux'\r\n"
	    "    bla:foo='blafoo'/>\n"
	    "   <abc xmlns='urn:objfw:test:abc' abc='abc' foo:abc='abc'/>\n"
	    "  </foo:bla>\n"
	    "  <!-- commänt -->\n"
	    " </qux>\n"
	    "</foobar>";
	const char *str = "<root>\n"
	    " <![CDATA[f<oo]]><bar/>\n"
	    " <foobar xmlns='urn:objfw:test:foobar'>\n"
	    "  <qux xmlns:foo='urn:objfw:test:foo'>\n"
	    "   <foo:bla foo:bla='&#x62;&#x6c;&#x61;' blafoo='foo'>\n"
	    "    <blup foo:qux='asd' quxqux='test'/>\n"
	    "    <bla:bla\r\rxmlns:bla=\"urn:objfw:test:bla\" qux='qux'\r\n"
	    "     bla:foo='blafoo'/>\n"
	    "    <abc xmlns='urn:objfw:test:abc' abc='abc' foo:abc='abc'/>\n"
	    "   </foo:bla>\n"
	    "   <!-- commänt -->\n"
	    "  </qux>\n"
	    " </foobar>\n"
	    "</root>";
	size_t j, len;

	TEST(@"+[xmlParser]", (parser = [OFXMLParser xmlParser]))

	TEST(@"-[setDelegate:]", R([parser setDelegate: self]))

	/* Simulate a stream where we only get chunks */
	len = strlen(str);

	for (j = 0; j < len; j+= 2) {
		if (j + 2 > len)
			[parser parseBuffer: str + j
				   withSize: 1];
		else
			[parser parseBuffer: str + j
				   withSize: 2];
	}

	TEST(@"Checking if everything was parsed", i == 26)
	TEST(@"Checking if everything was parsed", i == 30)

	[pool drain];
}
@end