openMSX
rapidsax.hh
Go to the documentation of this file.
1#ifndef RAPIDSAX_HH
2#define RAPIDSAX_HH
3
4// This code is _heavily_ based on RapidXml 1.13
5// http://rapidxml.sourceforge.net/
6//
7// RapidXml is a very fast XML parser.
8// http://xmlbench.sourceforge.net/results/benchmark200910/index.html
9// One of the main reasons it can be this fast is that doesn't do any string
10// copies. Instead the XML input data is modified in-place (e.g. for stuff like
11// < replacements). Though this also means the output produced by the parser
12// is tied to the lifetime of the XML input data.
13//
14// RapidXml produces a DOM-like output. This parser has a SAX-like interface.
15
16#include "one_of.hh"
17#include "small_compare.hh"
18#include <cassert>
19#include <cstdint>
20#include <string_view>
21
22namespace rapidsax {
23
24// Parse given XML text and call callback functions in the given handler.
25// - XML text must be zero-terminated
26// - Handler must implement the methods defined in NullHandler (below). An
27// easy way to do this is to inherit from NullHandler and only reimplement
28// the methods that you need.
29// - The behavior of the parser can be fine-tuned with the FLAGS parameter,
30// see below for more details.
31// - When a parse error is encounter, an instance of ParseError is thrown.
32// - The lifetime of the string_view's in the callback handler is the same as
33// the lifetime of the input XML data (no string copies are made, instead
34// the XML file is modified in-place and references to this data are passed).
35template<int FLAGS, typename HANDLER> void parse(HANDLER& handler, char* xml);
36
37// When loading an XML file from disk, the buffer needs to be 8 bytes bigger
38// than the filesize. The first of these bytes must be filled with zero
39// (zero-terminate the xml data). The other bytes are only there to allow to
40// read up-to 8 bytes past the end without triggering memory protection errors.
41constexpr size_t EXTRA_BUFFER_SPACE = 8;
42
43
44// Flags that influence parsing behavior. The flags can be OR'ed together.
45
46// Should XML entities like &lt; be expanded or not?
47constexpr int noEntityTranslation = 0x1;
48// Should leading and trailing whitespace be trimmed?
49constexpr int trimWhitespace = 0x2;
50// Should sequences of whitespace characters be replaced with a single
51// space character?
52constexpr int normalizeWhitespace = 0x4;
53// Should strings be modified (in-place) with a zero-terminator?
54constexpr int zeroTerminateStrings = 0x8;
55
56
57// Callback handler with all empty implementations (can be used as a base
58// class in case you only need to reimplement a few of the methods).
60{
61public:
62 // Called when an opening XML tag is encountered.
63 // 'name' is the name of the XML tag.
64 void start(std::string_view /*name*/) {}
65
66 // Called when a XML tag is closed.
67 // Note: the parser does currently not check whether the name of the
68 // opening nd closing tags matches.
69 void stop() {}
70
71 // Called when text inside a tag is parsed.
72 // XML entities are replaced (optional)
73 // Whitespace is (optionally) trimmed or normalized.
74 // This method is not called for an empty text string.
75 // (Unlike other SAX parsers) the whole text string is always
76 // passed in a single chunk (so no need to concatenate this text
77 // with previous chunks in the callback).
78 void text(std::string_view /*text*/) {}
79
80 // Called for each parsed attribute.
81 // Attributes can occur inside xml tags or inside XML declarations.
82 void attribute(std::string_view /*name*/, std::string_view /*value*/) {}
83
84 // Called for parsed CDATA sections.
85 void cdata(std::string_view /*value*/) {}
86
87 // Called when a XML comment (<!-- ... -->) is parsed.
88 void comment(std::string_view /*value*/) {}
89
90 // Called when XML declaration (<?xml .. ?>) is parsed.
91 // Inside a XML declaration there can be attributes.
93 void declAttribute(std::string_view /*name*/, std::string_view /*value*/) {}
95
96 // Called when the <!DOCTYPE ..> is parsed.
97 void doctype(std::string_view /*text*/) {}
98
99 // Called when XML processing instructions (<? .. ?>) are parsed.
100 void procInstr(std::string_view /*target*/, std::string_view /*instr*/) {}
101};
102
103
105{
106public:
107 ParseError(const char* what_, char* where_)
108 : m_what(what_)
109 , m_where(where_)
110 {
111 }
112
113 [[nodiscard]] const char* what() const { return m_what; }
114 [[nodiscard]] char* where() const { return m_where; }
115
116private:
117 const char* m_what;
118 char* m_where;
119};
120
121
122namespace internal {
123
124extern const uint8_t lutChar [256]; // Character class
125extern const uint8_t lutDigits[256]; // Digits
126
127// Detect whitespace character (space \n \r \t)
129 [[nodiscard]] static bool test(char ch) { return (lutChar[uint8_t(ch)] & 0x02) != 0; }
130};
131
132// Detect node name character (anything but space \n \r \t / > ? \0)
134 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x43); }
135};
136
137// Detect attribute name character (anything but space \n \r \t / < > = ? ! \0)
139 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0xC7); }
140};
141
142// Detect text character (PCDATA) (anything but < \0)
143struct TextPred {
144 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x05); }
145};
146
147// Detect text character (PCDATA) that does not require processing when ws
148// normalization is disabled (anything but < \0 &)
150 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x0D); }
151};
152
153// Detect text character (PCDATA) that does not require processing when ws
154// normalization is enabled (anything but < \0 & space \n \r \t)
156 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x0F); }
157};
158
159// Detect attribute value character, single quote (anything but ' \0)
160struct AttPred1 {
161 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x11); }
162};
163// Detect attribute value character, double quote (anything but " \0)
164struct AttPred2 {
165 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x21); }
166};
167
168// Detect attribute value character, single quote, that does not require
169// processing (anything but ' \0 &)
171 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x19); }
172};
173// Detect attribute value character, double quote, that does not require
174// processing (anything but " \0 &)
176 [[nodiscard]] static bool test(char ch) { return !(lutChar[uint8_t(ch)] & 0x29); }
177};
178
179// Insert coded character, using UTF8
180inline void insertUTF8char(char*& text, uint32_t code)
181{
182 if (code < 0x80) { // 1 byte sequence
183 text[0] = char(code);
184 text += 1;
185 } else if (code < 0x800) {// 2 byte sequence
186 text[1] = char((code | 0x80) & 0xBF); code >>= 6;
187 text[0] = char (code | 0xC0);
188 text += 2;
189 } else if (code < 0x10000) { // 3 byte sequence
190 text[2] = char((code | 0x80) & 0xBF); code >>= 6;
191 text[1] = char((code | 0x80) & 0xBF); code >>= 6;
192 text[0] = char (code | 0xE0);
193 text += 3;
194 } else if (code < 0x110000) { // 4 byte sequence
195 text[3] = char((code | 0x80) & 0xBF); code >>= 6;
196 text[2] = char((code | 0x80) & 0xBF); code >>= 6;
197 text[1] = char((code | 0x80) & 0xBF); code >>= 6;
198 text[0] = char (code | 0xF0);
199 text += 4;
200 } else { // Invalid, only codes up to 0x10FFFF are allowed in Unicode
201 throw ParseError("invalid numeric character entity", text);
202 }
203}
204
205template<StringLiteral Str> [[nodiscard]] static inline bool next(const char* p)
206{
207 return small_compare<Str>(p);
208}
209
210
211// Skip characters until predicate evaluates to true
212template<typename StopPred> static inline void skip(char*& text)
213{
214 char* tmp = text;
215 while (StopPred::test(*tmp)) ++tmp;
216 text = tmp;
217}
218
219// Skip characters until predicate evaluates to true while doing the following:
220// - replacing XML character entity references with proper characters
221// (&apos; &amp; &quot; &lt; &gt; &#...;)
222// - condensing whitespace sequences to single space character
223template<typename StopPred, class StopPredPure, int FLAGS>
224[[nodiscard]] static inline char* skipAndExpand(char*& text)
225{
226 // If entity translation, whitespace condense and whitespace
227 // trimming is disabled, use plain skip.
228 if constexpr ( (FLAGS & noEntityTranslation) &&
229 !(FLAGS & normalizeWhitespace) &&
230 !(FLAGS & trimWhitespace)) {
231 skip<StopPred>(text);
232 return text;
233 }
234
235 // Use simple skip until first modification is detected
236 skip<StopPredPure>(text);
237
238 // Use translation skip
239 char* src = text;
240 char* dest = src;
241 while (StopPred::test(*src)) {
242 // Test if replacement is needed
243 if (!(FLAGS & noEntityTranslation) &&
244 (src[0] == '&')) {
245 switch (src[1]) {
246 case 'a': // &amp; &apos;
247 if (next<"amp;">(&src[1])) {
248 *dest = '&';
249 ++dest;
250 src += 5;
251 continue;
252 }
253 if (next<"pos;">(&src[2])) {
254 *dest = '\'';
255 ++dest;
256 src += 6;
257 continue;
258 }
259 break;
260
261 case 'q': // &quot;
262 if (next<"uot;">(&src[2])) {
263 *dest = '"';
264 ++dest;
265 src += 6;
266 continue;
267 }
268 break;
269
270 case 'g': // &gt;
271 if (next<"t;">(&src[2])) {
272 *dest = '>';
273 ++dest;
274 src += 4;
275 continue;
276 }
277 break;
278
279 case 'l': // &lt;
280 if (next<"t;">(&src[2])) {
281 *dest = '<';
282 ++dest;
283 src += 4;
284 continue;
285 }
286 break;
287
288 case '#': // &#...; - assumes ASCII
289 if (src[2] == 'x') {
290 uint32_t code = 0;
291 src += 3; // skip &#x
292 while (true) {
293 uint8_t digit = lutDigits[uint8_t(*src)];
294 if (digit == 0xFF) break;
295 code = code * 16 + digit;
296 ++src;
297 }
298 insertUTF8char(dest, code);
299 } else {
300 uint32_t code = 0;
301 src += 2; // skip &#
302 while (true) {
303 uint8_t digit = lutDigits[uint8_t(*src)];
304 if (digit == 0xFF) break;
305 code = code * 10 + digit;
306 ++src;
307 }
308 insertUTF8char(dest, code);
309 }
310 if (*src != ';') {
311 throw ParseError("expected ;", src);
312 }
313 ++src;
314 continue;
315
316 default:
317 // Something else, ignore, just copy '&' verbatim
318 break;
319 }
320 }
321
322 // Test if condensing is needed
323 if ((FLAGS & normalizeWhitespace) &&
324 (WhitespacePred::test(*src))) {
325 *dest++ = ' '; // single space in dest
326 ++src; // skip first whitespace char
327 // Skip remaining whitespace chars
328 while (WhitespacePred::test(*src)) ++src;
329 continue;
330 }
331
332 // No replacement, only copy character
333 *dest++ = *src++;
334 }
335
336 // Return new end
337 text = src;
338 return dest;
339}
340
341inline void skipBOM(char*& text)
342{
343 if (next<"\357\273\277">(text)) { // char(0xEF), char(0xBB), char(0xBF)
344 text += 3; // skip utf-8 bom
345 }
346}
347
348
349template<int FLAGS, typename HANDLER> class Parser
350{
351 HANDLER& handler;
352
353public:
354 Parser(HANDLER& handler_, char* text)
355 : handler(handler_)
356 {
357 skipBOM(text);
358 while (true) {
359 // Skip whitespace before node
360 skip<WhitespacePred>(text);
361 if (*text == 0) break;
362
363 if (*text != '<') {
364 throw ParseError("expected <", text);
365 }
366 ++text; // skip '<'
367 parseNode(text);
368 }
369 }
370
371private:
372 // Parse XML declaration (<?xml...)
373 void parseDeclaration(char*& text)
374 {
375 handler.declarationStart();
376 skip<WhitespacePred>(text); // skip ws before attributes or ?>
377 parseAttributes(text, true);
378 handler.declarationStop();
379
380 // skip ?>
381 if (!next<"?>">(text)) {
382 throw ParseError("expected ?>", text);
383 }
384 text += 2;
385 }
386
387 // Parse XML comment (<!--...)
388 void parseComment(char*& text)
389 {
390 // Skip until end of comment
391 char* value = text; // remember value start
392 while (!next<"-->">(text)) {
393 if (text[0] == 0) {
394 throw ParseError("unexpected end of data", text);
395 }
396 ++text;
397 }
398 if (FLAGS & zeroTerminateStrings) {
399 *text = '\0';
400 }
401 handler.comment(std::string_view(value, text - value));
402 text += 3; // skip '-->'
403 }
404
405 void parseDoctype(char*& text)
406 {
407 char* value = text; // remember value start
408
409 // skip to >
410 while (*text != '>') {
411 switch (*text) {
412 case '[': {
413 // If '[' encountered, scan for matching ending
414 // ']' using naive algorithm with depth. This
415 // works for all W3C test files except for 2
416 // most wicked.
417 ++text; // skip '['
418 int depth = 1;
419 while (depth > 0) {
420 switch (*text) {
421 case char('['): ++depth; break;
422 case char(']'): --depth; break;
423 case 0: throw ParseError(
424 "unexpected end of data", text);
425 }
426 ++text;
427 }
428 break;
429 }
430 case '\0':
431 throw ParseError("unexpected end of data", text);
432
433 default:
434 ++text;
435 }
436 }
437
438 if (FLAGS & zeroTerminateStrings) {
439 *text = '\0';
440 }
441 handler.doctype(std::string_view(value, text - value));
442 text += 1; // skip '>'
443 }
444
445 void parsePI(char*& text)
446 {
447 // Extract PI target name
448 char* name = text;
449 skip<NodeNamePred>(text);
450 char* nameEnd = text;
451 if (name == nameEnd) {
452 throw ParseError("expected PI target", text);
453 }
454
455 // Skip whitespace between pi target and pi
456 skip<WhitespacePred>(text);
457
458 // Skip to '?>'
459 char* value = text; // Remember start of pi
460 while (!next<"?>">(text)) {
461 if (*text == 0) {
462 throw ParseError("unexpected end of data", text);
463 }
464 ++text;
465 }
466 // Set pi value (verbatim, no entity expansion or ws normalization)
467 if (FLAGS & zeroTerminateStrings) {
468 *nameEnd = '\0';
469 *text = '\0';
470 }
471 handler.procInstr(std::string_view(name, nameEnd - name),
472 std::string_view(value, text - value));
473 text += 2; // skip '?>'
474 }
475
476 void parseText(char*& text, char* contentsStart)
477 {
478 // Backup to contents start if whitespace trimming is disabled
479 if constexpr (!(FLAGS & trimWhitespace)) {
480 text = contentsStart;
481 }
482 // Skip until end of data
483 char* value = text;
484 char* end = (FLAGS & normalizeWhitespace)
485 ? skipAndExpand<TextPred, TextPureWithWsPred, FLAGS>(text)
486 : skipAndExpand<TextPred, TextPureNoWsPred , FLAGS>(text);
487
488 // Trim trailing whitespace; leading was already trimmed by
489 // whitespace skip after >
490 if constexpr ((FLAGS & trimWhitespace) != 0) {
491 if constexpr (FLAGS & normalizeWhitespace) {
492 // Whitespace is already condensed to single
493 // space characters by skipping function, so
494 // just trim 1 char off the end.
495 if (end[-1] == ' ') {
496 --end;
497 }
498 } else {
499 // Backup until non-whitespace character is found
500 while (WhitespacePred::test(end[-1])) {
501 --end;
502 }
503 }
504 }
505
506 // check next char before calling handler.text()
507 if (*text == '\0') {
508 throw ParseError("unexpected end of data", text);
509 } else {
510 assert(*text == '<');
511 }
512
513 // Handle text, but only if non-empty.
514 auto len = end - value;
515 if (len) {
516 if (FLAGS & zeroTerminateStrings) {
517 *text = '\0';
518 }
519 handler.text(std::string_view(value, len));
520 }
521 }
522
523 void parseCdata(char*& text)
524 {
525 // Skip until end of cdata
526 char* value = text;
527 while (!next<"]]>">(text)) {
528 if (text[0] == 0) {
529 throw ParseError("unexpected end of data", text);
530 }
531 ++text;
532 }
533 if (FLAGS & zeroTerminateStrings) {
534 *text = '\0';
535 }
536 handler.cdata(std::string_view(value, text - value));
537 text += 3; // skip ]]>
538 }
539
540 void parseElement(char*& text)
541 {
542 // Extract element name
543 char* name = text;
544 skip<NodeNamePred>(text);
545 char* nameEnd = text;
546 if (name == nameEnd) {
547 throw ParseError("expected element name", text);
548 }
549 handler.start(std::string_view(name, nameEnd - name));
550
551 skip<WhitespacePred>(text); // skip ws before attributes or >
552 parseAttributes(text, false);
553
554 // Determine ending type
555 if (*text == '>') {
556 if (FLAGS & zeroTerminateStrings) {
557 *nameEnd = '\0';
558 }
559 ++text;
560 parseNodeContents(text);
561 } else if (*text == '/') {
562 if (FLAGS & zeroTerminateStrings) {
563 *nameEnd = '\0';
564 }
565 handler.stop();
566 ++text;
567 if (*text != '>') {
568 throw ParseError("expected >", text);
569 }
570 ++text;
571 } else {
572 throw ParseError("expected >", text);
573 }
574 }
575
576 // Determine node type, and parse it
577 void parseNode(char*& text)
578 {
579 switch (text[0]) {
580 case '?': // <?...
581 ++text; // skip ?
582 // Note: this doesn't detect mixed case (xMl), does
583 // that matter?
584 if ((next<"xml">(text) || next<"XML">(text)) &&
585 WhitespacePred::test(text[3])) {
586 // '<?xml ' - xml declaration
587 text += 4; // skip 'xml '
588 parseDeclaration(text);
589 } else {
590 parsePI(text);
591 }
592 break;
593
594 case '!': // <!...
595 // Parse proper subset of <! node
596 switch (text[1]) {
597 case '-': // <!-
598 if (text[2] == '-') {
599 // '<!--' - xml comment
600 text += 3; // skip '!--'
601 parseComment(text);
602 return;
603 }
604 break;
605
606 case '[': // <![
607 if (next<"CDATA[">(&text[2])) {
608 // '<![CDATA[' - cdata
609 text += 8; // skip '![CDATA['
610 parseCdata(text);
611 return;
612 }
613 break;
614
615 case 'D': // <!D
616 if (next<"OCTYPE">(&text[2]) &&
617 WhitespacePred::test(text[8])) {
618 // '<!DOCTYPE ' - doctype
619 text += 9; // skip '!DOCTYPE '
620 parseDoctype(text);
621 return;
622 }
623 break;
624 }
625 // Attempt to skip other, unrecognized types starting with <!
626 ++text; // skip !
627 while (*text != '>') {
628 if (*text == 0) {
629 throw ParseError(
630 "unexpected end of data", text);
631 }
632 ++text;
633 }
634 ++text; // skip '>'
635 break;
636
637 default: // <...
638 parseElement(text);
639 break;
640 }
641 }
642
643 // Parse contents of the node - children, data etc.
644 void parseNodeContents(char*& text)
645 {
646 while (true) {
647 char* contentsStart = text; // start before ws is skipped
648 skip<WhitespacePred>(text); // Skip ws between > and contents
649
650 switch (*text) {
651 case '<': // Node closing or child node
652afterText: // After parseText() jump here instead of continuing
653 // the loop, because skipping whitespace is unnecessary.
654 if (text[1] == '/') {
655 // Node closing
656 text += 2; // skip '</'
657 skip<NodeNamePred>(text);
658 // TODO validate closing tag??
659 handler.stop();
660 // Skip remaining whitespace after node name
661 skip<WhitespacePred>(text);
662 if (*text != '>') {
663 throw ParseError("expected >", text);
664 }
665 ++text; // skip '>'
666 return;
667 } else {
668 // Child node
669 ++text; // skip '<'
670 parseNode(text);
671 }
672 break;
673
674 case '\0':
675 throw ParseError("unexpected end of data", text);
676
677 default:
678 parseText(text, contentsStart);
679 goto afterText;
680 }
681 }
682 }
683
684 // Parse XML attributes of the node
685 void parseAttributes(char*& text, bool declaration)
686 {
687 // For all attributes
688 while (AttributeNamePred::test(*text)) {
689 // Extract attribute name
690 char* name = text;
691 ++text; // Skip first character of attribute name
692 skip<AttributeNamePred>(text);
693 char* nameEnd = text;
694 if (name == nameEnd) {
695 throw ParseError("expected attribute name", name);
696 }
697
698 skip<WhitespacePred>(text); // skip ws after name
699 if (*text != '=') {
700 throw ParseError("expected =", text);
701 }
702 ++text; // skip =
703 skip<WhitespacePred>(text); // skip ws after =
704
705 // Skip quote and remember if it was ' or "
706 char quote = *text;
707 if (quote != one_of('\'', '"')) {
708 throw ParseError("expected ' or \"", text);
709 }
710 ++text;
711
712 // Extract attribute value and expand char refs in it
713 // No whitespace normalization in attributes
714 constexpr int FLAGS2 = FLAGS & ~normalizeWhitespace;
715 char* value = text;
716 char* valueEnd = (quote == '\'')
717 ? skipAndExpand<AttPred1, AttPurePred1, FLAGS2>(text)
718 : skipAndExpand<AttPred2, AttPurePred2, FLAGS2>(text);
719 // Make sure that end quote is present
720 // check before calling handler.xxx()
721 if (*text != quote) {
722 throw ParseError("expected ' or \"", text);
723 }
724 ++text; // skip quote
725
726 if (FLAGS & zeroTerminateStrings) {
727 *nameEnd = '\0';
728 *valueEnd = '\0';
729 }
730 if (!declaration) {
731 handler.attribute(std::string_view(name, nameEnd - name),
732 std::string_view(value, valueEnd - value));
733 } else {
734 handler.declAttribute(std::string_view(name, nameEnd - name),
735 std::string_view(value, valueEnd - value));
736 }
737
738 skip<WhitespacePred>(text); // skip ws after value
739 }
740 }
741};
742
743} // namespace internal
744
745template<int FLAGS, typename HANDLER>
746inline void parse(HANDLER& handler, char* xml)
747{
748 internal::Parser<FLAGS, HANDLER> parser(handler, xml);
749}
750
751} // namespace rapidsax
752
753#endif
Definition: one_of.hh:7
void declAttribute(std::string_view, std::string_view)
Definition: rapidsax.hh:93
void text(std::string_view)
Definition: rapidsax.hh:78
void comment(std::string_view)
Definition: rapidsax.hh:88
void procInstr(std::string_view, std::string_view)
Definition: rapidsax.hh:100
void attribute(std::string_view, std::string_view)
Definition: rapidsax.hh:82
void doctype(std::string_view)
Definition: rapidsax.hh:97
void cdata(std::string_view)
Definition: rapidsax.hh:85
void start(std::string_view)
Definition: rapidsax.hh:64
char * where() const
Definition: rapidsax.hh:114
ParseError(const char *what_, char *where_)
Definition: rapidsax.hh:107
const char * what() const
Definition: rapidsax.hh:113
Parser(HANDLER &handler_, char *text)
Definition: rapidsax.hh:354
void insertUTF8char(char *&text, uint32_t code)
Definition: rapidsax.hh:180
const uint8_t lutDigits[256]
Definition: rapidsax.cc:36
void skipBOM(char *&text)
Definition: rapidsax.hh:341
const uint8_t lutChar[256]
Definition: rapidsax.cc:14
constexpr int noEntityTranslation
Definition: rapidsax.hh:47
constexpr int zeroTerminateStrings
Definition: rapidsax.hh:54
constexpr int trimWhitespace
Definition: rapidsax.hh:49
void parse(HANDLER &handler, char *xml)
Definition: rapidsax.hh:746
constexpr int normalizeWhitespace
Definition: rapidsax.hh:52
constexpr size_t EXTRA_BUFFER_SPACE
Definition: rapidsax.hh:41
static bool test(char ch)
Definition: rapidsax.hh:161
static bool test(char ch)
Definition: rapidsax.hh:165
static bool test(char ch)
Definition: rapidsax.hh:171
static bool test(char ch)
Definition: rapidsax.hh:176
static bool test(char ch)
Definition: rapidsax.hh:134
static bool test(char ch)
Definition: rapidsax.hh:144
static bool test(char ch)
Definition: rapidsax.hh:129
constexpr auto end(const zstring_view &x)